Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,8 @@ INKBOX_SIGNING_KEY=whsec_xxxxxxxxxxxx
# INKBOX_GATEWAY_AGENT=inkbox-channel # opencode agent for gateway sessions
# INKBOX_GATEWAY_MODEL=openai/gpt-5.4-mini # "provider/model" override
# OPENCODE_SERVER_URL=http://127.0.0.1:4096 # attach to an existing server instead
# INKBOX_OPENCODE_SERVER_PASSWORD= # explicit V2 server authentication
# INKBOX_OPENCODE_SERVER_USERNAME=opencode
# INKBOX_GATEWAY_SERVE_PORT=4097 # port for the managed `opencode serve`
# INKBOX_OPENCODE_BIN=opencode # binary for the managed server
# INKBOX_TEXT_BATCH_WINDOW_MS=0 # batch rapid-fire texts into one turn
Expand Down
15 changes: 15 additions & 0 deletions .github/workflows/canary.yml
Original file line number Diff line number Diff line change
Expand Up @@ -48,3 +48,18 @@
run: bash "$GITHUB_WORKSPACE/tests/ci/npm_with_retry.sh" install -g opencode-ai@latest
- name: Packed native loader on latest host
run: bash scripts/smoke-loader.sh

loader-v2:
runs-on: blacksmith-2vcpu-ubuntu-2404
steps:
- uses: actions/checkout@v7
- uses: actions/setup-node@v7
with:
node-version: 22
cache: npm
- run: bash tests/ci/npm_with_retry.sh ci
- run: bash tests/ci/npm_with_retry.sh install -g @opencode/cli@latest
- run: npm run build
- run: |
ARCHIVE=$(npm pack --silent | tail -1)
node scripts/smoke-v2.mjs "$ARCHIVE"
Comment on lines +53 to +65
26 changes: 23 additions & 3 deletions .github/workflows/tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,27 @@
run: |
test "$(opencode --version)" = "1.15.0"
bash scripts/smoke-loader.sh
- name: Install latest native host
run: bash "$GITHUB_WORKSPACE/tests/ci/npm_with_retry.sh" install -g opencode-ai@latest
- name: Packed native loader on latest host
- name: Install recent V1 host
run: bash "$GITHUB_WORKSPACE/tests/ci/npm_with_retry.sh" install -g opencode-ai@1.19.0
- name: Packed native loader on recent V1 host
run: bash scripts/smoke-loader.sh

loader-v2:
runs-on: blacksmith-2vcpu-ubuntu-2404
strategy:
fail-fast: false
matrix:
version: ["2.0.26", "latest"]
steps:
- uses: actions/checkout@v7
- uses: actions/setup-node@v7
with:
node-version: 22
cache: npm
- run: bash tests/ci/npm_with_retry.sh ci
- run: bash tests/ci/npm_with_retry.sh install -g @opencode/cli@${{ matrix.version }}
- run: npm run build
- name: Packed V2 host contract
run: |
ARCHIVE=$(npm pack --silent | tail -1)
node scripts/smoke-v2.mjs "$ARCHIVE"
Comment on lines +78 to +95
11 changes: 11 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,16 @@
# Changelog

## 0.2.18 (unreleased)

- Updates the SDK to 0.7.19.
- Prevents duplicate automatic email replies after a successful threaded tool send, including recovered turns.
- Fixes V2 diagnostics before the gateway's first startup and preserves completed tool output from V2 history.
- Selects the channel agent during noninteractive setup while preserving custom agent choices.
- Adds an OpenCode V2 entrypoint while retaining the existing V1 entrypoint and minimum supported version.
- Adds V2 gateway sessions, shared-service connections, and native tool approvals.
- Updates generated wrappers for the installed OpenCode version and preserves custom wrappers for manual migration.
- Updates the development source-map dependency to its patched release.

## 0.2.17 (unreleased)

- Delivers actual local Slack attachments with exact native tool/source ownership and durable unknown-outcome protection.
Expand Down
77 changes: 66 additions & 11 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@

## Prerequisites

- **Supported hosts:** OpenCode 1.15.0–1.x or 2.0.26–2.0.x. Verified on 1.15.0, 1.18.35, 1.19.0, and 2.0.26. V1 and V2 use separate plugin entrypoints; the installer selects the installed host version.
- **opencode installed and logged in.** The plugin loads into real opencode
sessions and the gateway drives an `opencode serve` — install it
([opencode.ai](https://opencode.ai)) and authenticate a model provider.
Expand All @@ -25,7 +26,7 @@
```bash
curl -fsSL https://opencode.ai/install | bash
```
- **Node.js 20+ and git.** The installer builds the plugin from source.
- **Node.js 20+ for V1, Node.js 22+ for the V2 gateway, and git.** The installer builds the plugin from source.
- **macOS or Linux.** Boot persistence uses a systemd user unit on Linux and
a launchd agent on macOS.
- **An Inkbox agent** — nothing to set up in advance; the setup wizard
Expand Down Expand Up @@ -55,6 +56,38 @@ run `./install.sh` instead; it uses the checkout in place. Rerun the wizard
anytime with `inkbox-opencode setup` (`setup --print` for the static
checklist), and check the wiring with `inkbox-opencode doctor`.

### Upgrading to OpenCode V2

Rerun `./install.sh --no-setup` from the checkout after upgrading OpenCode.
The installer backs up and updates its generated wrapper. Custom wrappers and
their options are preserved; migrate them using the V2 example below. Check
project-local wrappers too, then run `inkbox-opencode doctor`.

V2 uses OpenCode's authenticated shared service. Configure the same Inkbox
identity and channel settings for the service and gateway. Tool selection and
outbound approval options remain in the plugin wrapper. Environment variables loaded
by the gateway do not change an already-running service's environment. Update
the service's launch environment or use the shared `~/.inkbox/config` credential
file, and restart OpenCode when configuration changes require it.

For an explicitly selected V2 server, set `OPENCODE_SERVER_URL` and
`INKBOX_OPENCODE_SERVER_PASSWORD` in both the plugin and gateway environments.
`INKBOX_OPENCODE_SERVER_USERNAME` defaults to `opencode`. These settings also
apply to standalone V2 servers. Stopping the gateway leaves an attached shared
service running.

Set `INKBOX_GATEWAY_MODEL=provider/model` to pin the gateway model. An interactive
session's model selection does not configure the default for new gateway sessions.

The gateway and plugin must share the same accessible `INKBOX_OPENCODE_HOME`
directory (default `~/.inkbox-opencode`), including when using an explicit server.

Finish outstanding conversations and stop the gateway before switching host versions. If doctor or
startup reports unfinished work from another host, reconnect that host and
resolve the work first. Rerun the installer after switching back to V1 as well.
If using boot autostart, rerun `inkbox-opencode autostart install` after changing
host versions to update the service definition.

### Bootstrap an existing identity without prompts

An agent can complete setup non-interactively after a human assigns it an
Expand Down Expand Up @@ -123,20 +156,40 @@ local wrapper:
```bash
git clone https://github.com/inkbox-ai/opencode-plugin.git ~/.inkbox-opencode/app
cd ~/.inkbox-opencode/app
npm install && npm run build && npm pack
npm install && npm run build
```

Then, from your opencode project (or your global `~/.config/opencode`
directory), install the packed build and add a one-file plugin wrapper:
Then, from your opencode project, install the packed build and add a one-file
plugin wrapper:

```bash
npm install ~/.inkbox-opencode/app/inkbox-opencode-plugin-*.tgz
archive=$(cd ~/.inkbox-opencode/app && npm pack --silent | tail -1)
npm install "$HOME/.inkbox-opencode/app/$archive"
mkdir -p .opencode/plugins
```

Create `.opencode/plugins/inkbox.ts` — opencode auto-loads every file under
`.opencode/plugins/`. **Every option shown in this README goes inside the
`InkboxPlugin(input, { ... })` call here**:
Create `.opencode/plugins/inkbox.ts`; for global installation use
`~/.config/opencode/plugins/inkbox.ts` and install the tarball from
`~/.config/opencode` instead. Configure only one wrapper or package
entry for each location.

**OpenCode V2:** options go inside `options` in this wrapper:

```ts
import InkboxPlugin from "@inkbox/opencode-plugin/v2";

export default {
...InkboxPlugin,
setup: (ctx: Parameters<typeof InkboxPlugin.setup>[0]) => InkboxPlugin.setup({
...ctx,
options: { ...ctx.options, tools: { enable: ["inkbox_place_call"] } },
}),
};
```

**OpenCode V1:** options go inside the second argument below. Later examples
using `InkboxPlugin(input, { ... })` show this V1 form; on V2 place the same
option object in `options` above.

```ts
import InkboxPlugin from "@inkbox/opencode-plugin";
Expand Down Expand Up @@ -221,7 +274,8 @@ Sends, calls, and A2A writes are gated before anything leaves:
`inkbox_send_imessage`, `inkbox_forward_email`, `inkbox_place_call`,
`inkbox_a2a_call`, and `inkbox_a2a_reply`
request approval through opencode's native permission system. Approve once,
or persist an allow rule from the prompt.
or persist an allow rule from the prompt. OpenCode `allow` rules can approve
without a prompt; configure explicit `ask` rules when prompts are required.
- **Recipient allowlist**: set `outbound.allowedRecipients` (exact email
addresses / E.164 numbers) and anything else is rejected regardless of
approval mode.
Expand Down Expand Up @@ -340,8 +394,9 @@ Two ways to run it:
```

The sidecar needs an opencode server and finds one on its own: an explicit
`gateway.serverUrl` / `OPENCODE_SERVER_URL` must answer; otherwise it
attaches to a server already on `http://127.0.0.1:4096`, and failing that it
`gateway.serverUrl` / `OPENCODE_SERVER_URL` must answer. V2 discovers or starts
the shared authenticated service and leaves it running when the gateway stops.
V1 otherwise attaches to a server already on `http://127.0.0.1:4096`, and failing that it
launches its own managed `opencode serve` (port `4097` by default; tune with
`gateway.serve.{bin,port}` or `INKBOX_OPENCODE_BIN` /
`INKBOX_GATEWAY_SERVE_PORT`). A managed server is stopped with the gateway,
Expand Down
32 changes: 16 additions & 16 deletions install.sh
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ command -v node >/dev/null 2>&1 || die "Node.js 20+ is required."
node -e 'process.exit(Number(process.versions.node.split(".")[0]) >= 20 ? 0 : 1)' ||
die "Node.js 20+ is required (found $(node --version))."
command -v opencode >/dev/null 2>&1 ||
warn "the opencode CLI is not on PATH — install it (npm install -g opencode-ai); the gateway launches \`opencode serve\` and the plugin loads into opencode sessions."
warn "the opencode CLI is not on PATH — install OpenCode from https://opencode.ai before configuring the plugin."

# --- 2. source: local checkout, or clone/update the app dir -----------------
SOURCE_DIR=""
Expand Down Expand Up @@ -82,22 +82,22 @@ if [ ! -f "$OPENCODE_CONFIG_DIR/agent/inkbox-channel.md" ]; then
fi

WRAPPER="$OPENCODE_CONFIG_DIR/plugins/inkbox.ts"
if [ -f "$WRAPPER" ]; then
say "Keeping your existing plugin wrapper at $WRAPPER"
else
cat > "$WRAPPER" <<'EOF'
// Inkbox plugin wrapper — written by install.sh; edit freely (never overwritten).
// Credentials resolve from env vars or ~/.inkbox/config; options: see README.
// inkbox_place_call is enabled so the agent can dial out (calls stay gated
// by outbound approval like every other send).
import InkboxPlugin from "@inkbox/opencode-plugin";

export default async (input: any) => InkboxPlugin(input, {
tools: { enable: ["inkbox_place_call"] },
});
node --input-type=module - "$SOURCE_DIR" "$WRAPPER" <<'EOF'
import { pathToFileURL } from "node:url";
import { dirname } from "node:path";
const [source, file] = process.argv.slice(2);
const { hostMajor, installWrapper, wrapperSource, configuredPackages } = await import(pathToFileURL(`${source}/dist/cli/wrappers.js`).href);
const configured = configuredPackages(dirname(dirname(file)));
if (configured.length) throw new Error(`Inkbox already has package entries in ${configured.join(", ")}. Migrate those entries or remove them before installing a wrapper; preserve their options.`);
const major = hostMajor(process.env.INKBOX_OPENCODE_BIN ?? "opencode");
if (!major) throw new Error("Install a supported OpenCode version before configuring the plugin.");
const result = installWrapper(file, major);
console.log(`OpenCode V${major} wrapper: ${result} (${file})`);
if (result === "custom") {
console.log("Custom wrapper preserved. Adapt its options to this entrypoint before starting OpenCode:");
console.log(wrapperSource(major));
}
EOF
say "Wrote plugin wrapper $WRAPPER"
fi

# --- 5. launcher on PATH ------------------------------------------------------
mkdir -p "$BIN_DIR"
Expand Down
Loading
Loading