4.9 KiB
@opencode-ai/client
Promise and Effect clients derived from OpenCode's authoritative Effect HttpApi, plus handwritten Node transports.
Entrypoints
@opencode-ai/client: zero-Effect Promise client usingfetch.@opencode-ai/client/node: Promise client plus Node-hosted browser attachments.@opencode-ai/client/effect: rich Effect network client using an environment-providedHttpClient.
The generated surface includes every standard HTTP group from Server's concrete API. The build compiler reads @opencode-ai/server/api; the generated Effect runtime imports a client-local projection built from Protocol, with a generation-equivalence test preventing transport drift. Custom transports such as the PTY WebSocket connection remain outside the generic HTTP client. Run bun run generate after changing the contract and bun run check:generated to detect committed-output drift.
The Effect entrypoint uses canonical decoded values such as Session.ID, Location.Ref, and Prompt. These datatypes come from the lightweight @opencode-ai/schema package and are re-exported so callers depend only on the client surface. Protocol owns endpoint construction and middleware placement; Server supplies the concrete middleware keys used by the build-time API.
The Promise root remains structural and has no Core, Effect, Schema, Protocol, or WebSocket runtime dependency. /node adds Effect, Schema, Protocol, and ws, but never Core or Server. /effect depends only on Effect, Schema, and Protocol and is browser-bundle safe. Bundle-boundary tests enforce these import graphs.
Node browser attachments
The Node entrypoint owns the control connection, Session lease, authenticated proxy, and network tunnels. Chromium hosts provide a small platform port once with BrowserDriver.chromium; the SDK owns command semantics, CDP input, snapshots, generations, cancellation, and limits.
import { BrowserDriver, OpenCode } from "@opencode-ai/client/node"
const driver = BrowserDriver.chromium(async ({ proxy, signal }) => {
const view = await createChromiumView({ proxy, signal })
return {
resource: view,
state: () => view.state(),
subscribe: (listener) => view.subscribe((state, mainDocumentChanged) => listener({ state, mainDocumentChanged })),
navigate: (url) => view.navigate(url),
back: () => view.back(),
forward: () => view.forward(),
reload: () => view.reload(),
stop: () => view.stop(),
send: (method, params) => view.sendCDP(method, params),
viewport: () => view.viewport(),
screenshot: ({ maxDimension }) => view.capturePNG({ maxDimension }),
dispose: () => view.close(),
}
})
const client = OpenCode.make({
baseUrl: "https://opencode.example",
headers: { authorization: `Basic ${credentials}` },
})
const attachment = await client.browser.attach({ sessionID, driver })
await attachment.resource.navigate("example.com")
const view = attachment.resource.resource
await attachment.close()
attach resolves only after the server acknowledges the exact Session lease. Each attachment has its own proxy and driver resource; close() and Symbol.asyncDispose are idempotent. A single Node client multiplexes up to 16 distinct Sessions over one lazily opened control WebSocket.
Driver factories should return after configuring their resource rather than await a proxied navigation: tunnel dialing is deliberately held behind the first lease acknowledgement, which is published after the driver supplies its initial state.
Port state events set mainDocumentChanged only when the main document changes; this advances the public generation and invalidates element refs. send must dispatch CDP calls in invocation order. screenshot returns PNG bytes and dimensions, proportionally scaled to maxDimension without upscaling. The returned controller serializes local navigation with remote commands; stop immediately interrupts active work, and controller disposal is idempotent. An aborted or timed-out operation that reached the platform disposes the port so late native completion cannot cross the queue fence.
BrowserDriver descriptors are structural factory functions, so adapters remain compatible across duplicate client package instances. The Node entrypoint also re-exports canonical Browser contracts. BrowserDriver.define remains the advanced escape hatch for non-Chromium semantics; throw BrowserDriverError for typed command failures there. Structurally equivalent errors are accepted only when their code is a valid Browser.ErrorCode.
Effect consumers construct canonical decoded inputs:
import { AbsolutePath, Location, OpenCode, Prompt } from "@opencode-ai/client/effect"
const client = yield * OpenCode.make({ baseUrl: "https://opencode.example" })
yield *
client.sessions.create({
location: Location.Ref.make({ directory: AbsolutePath.make("/workspace") }),
})
yield * client.sessions.prompt({ sessionID, prompt: Prompt.make({ text: "Hello" }) })