Skip to content

fix(cli): support OpenCode v2 servers in the OpenCode agent - #118

Open
som4tress wants to merge 1 commit into
shellular-org:mainfrom
som4tress:fix/opencode-v2-support
Open

som4tress wants to merge 1 commit into
shellular-org:mainfrom
som4tress:fix/opencode-v2-support

Conversation

@som4tress

Copy link
Copy Markdown

Fixes #117.

Problem

Tapping OpenCode in the app always failed against an OpenCode v2.x server. Three separate v2 incompatibilities were involved.

1. Ready-line banner (the 15s timeout)

createOpencodeServer() from the bundled @opencode-ai/sdk@1.18.18 waits for a stdout line starting with opencode server listening:

if (line.startsWith("opencode server listening")) { ... }

OpenCode v2 prints server listening on http://127.0.0.1:40201 instead, so the match never fires and the SDK kills the server after timeout ms, surfacing as:

Timeout waiting for server to start after 15000ms

2. Per-server password

v2 generates a password per server. The CLI built its client with a random Basic header (Basic base64(shellular:<random32>)) that v2 rejects with 401, and never read the printed password.

3. Session list endpoint returns HTML

experimental.session.list requests /experimental/session. On v2.1.4 that path serves the web UI:

$ curl -i -H "Authorization: Basic ..." http://127.0.0.1:49374/experimental/session
HTTP/1.1 200
content-type: text/html

so the SDK's response interceptor throws:

Request is not supported by this version of OpenCode Server (Server responded with text/html)

The real JSON API is at /api/session:

$ curl -i -H "Authorization: Basic ..." http://127.0.0.1:49374/api/session
HTTP/1.1 200
content-type: application/json

The v2 payload also differs in shape from what the app expects: directory lives under location.directory, and slug/version/project are absent.

Changes

All in cli/src/agents/opencode.ts:

  • Reuse a running server instead of depending on the v1 banner. Resolution order: OPENCODE_SERVER_URL → default port 4096 probe → ~/.config/opencode/service.json (written by opencode serve --service). Discovered servers are marked external and are never killed in destroy(); only servers we spawn are closed.
  • Use the real password from service.json for Basic auth instead of a random one.
  • Fall back to /api/session when the SDK call fails, and normalize the v2 response into the v1 shape: directory from location.directory, slug derived from the title when missing, version from /api/info, project reconstructed from projectID + directory. v1-shaped payloads pass through unchanged, so older servers keep working.

Verification

Against OpenCode v2.1.4 on Linux, paired with the iOS app (0.0.43):

  • tsc --noEmit clean
  • biome check clean
  • pnpm run build succeeds
  • In the app: pairing succeeds, OpenCode connects to the already-running server, and the session list loads instead of erroring.
  ✓ Client c_... connected
  reusing running opencode server at http://127.0.0.1:49374

Note the server was already running as a background service, so the spawn path (and therefore the banner fix itself) is not exercised here — it remains the fallback when no server is discoverable.

The OpenCode integration could not launch or talk to OpenCode v2.x servers,
so tapping OpenCode in the app always failed.

Three separate v2 incompatibilities are addressed:

1. Ready-line banner. `createOpencodeServer()` from the bundled
   `@opencode-ai/sdk` waits for a line starting with
   "opencode server listening". v2 prints "server listening on <url>",
   so the match never fired and the SDK killed the server after the
   15s timeout ("Timeout waiting for server to start after 15000ms").
   We now discover and reuse an already-running server instead of
   relying on that banner:
     - `OPENCODE_SERVER_URL` (explicit override)
     - probe the default port (4096)
     - read `~/.config/opencode/service.json` written by
       `opencode serve --service`, including its password
   Servers discovered this way are marked external and are never
   killed on `destroy()`; only servers we spawned are closed.

2. Authentication. v2 servers generate a per-server password. The CLI
   built its client with a random Basic header that v2 rejects with 401.
   The password read from `service.json` is now used for Basic auth.

3. Session list endpoint and payload shape. The SDK's
   `experimental.session.list` requests `/experimental/session`, which
   v2.1.4 serves as the web UI (text/html) rather than JSON, so the SDK
   threw "Request is not supported by this version of OpenCode Server
   (Server responded with text/html)". We now fall back to a direct
   request against `/api/session`, and normalize the v2 payload into
   the v1 shape the app expects: `directory` comes from
   `location.directory`, `slug` is derived when absent, `version` comes
   from `/api/info`, and `project` is reconstructed. v1-shaped
   responses pass through unchanged.

Fixes shellular-org#117
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

OpenCode agent fails on OpenCode v2: "Timeout waiting for server to start after 15000ms" (v1 SDK banner + auth assumptions)

1 participant