feat(client): add root promise entrypoint

This commit is contained in:
Dax Raad
2026-07-10 01:09:06 -04:00
parent fbb0fdf88a
commit 6a85f0d3db
4 changed files with 156 additions and 56 deletions
+1
View File
@@ -16,6 +16,7 @@
"dist" "dist"
], ],
"exports": { "exports": {
".": "./src/promise/index.ts",
"./promise": "./src/promise/index.ts", "./promise": "./src/promise/index.ts",
"./promise/api": "./src/promise/api.ts", "./promise/api": "./src/promise/api.ts",
"./effect": "./src/effect/index.ts", "./effect": "./src/effect/index.ts",
@@ -12,7 +12,7 @@ const server = resolve(import.meta.dir, "../../server")
describe("public import boundaries", () => { describe("public import boundaries", () => {
test("isolates each public entrypoint", async () => { test("isolates each public entrypoint", async () => {
const root = await bundleInputs("@opencode-ai/client/promise", "browser") const root = await bundleInputs("@opencode-ai/client", "browser")
expect(within(root, effect)).toEqual([]) expect(within(root, effect)).toEqual([])
expect(within(root, schema)).toEqual([]) expect(within(root, schema)).toEqual([])
+68 -14
View File
@@ -16,22 +16,15 @@ network. Its types and methods are generated from the same contract as the
## Install ## Install
```sh ```sh
bun add @opencode-ai/client bun add @opencode-ai/client@next
``` ```
The package has two entrypoints: ## Create a client
- `@opencode-ai/client/promise` uses `fetch` and returns Promises or async
iterables. It has no Effect runtime dependency.
- `@opencode-ai/client/effect` returns Effects and Streams, decodes values into
the V2 schema types, and requires an `HttpClient` service from Effect.
## Promise client
Create a client with the server URL, then call methods grouped by API resource: Create a client with the server URL, then call methods grouped by API resource:
```ts ```ts
import { OpenCode } from "@opencode-ai/client/promise" import { OpenCode } from "@opencode-ai/client"
const client = OpenCode.make({ const client = OpenCode.make({
baseUrl: "http://localhost:4096", baseUrl: "http://localhost:4096",
@@ -47,11 +40,28 @@ await client.session.prompt({
}) })
``` ```
## Headers and requests
Pass default authentication or application headers to `OpenCode.make` with Pass default authentication or application headers to `OpenCode.make` with
`headers`. You can also supply a custom `fetch` implementation. Each operation `headers`. You can also supply a custom `fetch` implementation. Each operation
accepts request options as its final argument for an `AbortSignal` or accepts request options as its final argument for an `AbortSignal` or
per-request headers. per-request headers.
```ts
const client = OpenCode.make({
baseUrl: "https://opencode.example.com",
headers: {
authorization: `Bearer ${process.env.OPENCODE_TOKEN}`,
},
})
await client.session.list(undefined, {
signal: AbortSignal.timeout(10_000),
})
```
## Stream events
Streaming endpoints return async iterables: Streaming endpoints return async iterables:
```ts ```ts
@@ -60,11 +70,17 @@ for await (const event of client.event.subscribe()) {
} }
``` ```
## Effect client ## Effect
Install the `effect` peer dependency when using the Effect entrypoint. The OpenCode provides a first-class Effect client through the
client uses canonical V2 values such as `Location.Ref` and `Session.ID`, and `@opencode-ai/client/effect` entrypoint. It returns typed Effects and Streams
returns typed failures in the Effect error channel. and decodes responses into OpenCode schema values.
```sh
bun add @opencode-ai/client@next effect
```
### Create a client
```ts ```ts
import { AbsolutePath, Location, OpenCode } from "@opencode-ai/client/effect" import { AbsolutePath, Location, OpenCode } from "@opencode-ai/client/effect"
@@ -89,3 +105,41 @@ const session = await Effect.runPromise(
Streaming operations, including `client.event.subscribe()` and Streaming operations, including `client.event.subscribe()` and
`client.session.log(...)`, return Effect `Stream` values. `client.session.log(...)`, return Effect `Stream` values.
### Service
`Service` discovers and manages the local OpenCode background service from a
Node application:
- `Service.discover()` returns a healthy registered endpoint without starting
a process.
- `Service.start()` reuses a compatible service or starts one when needed.
- `Service.stop()` stops the registered service.
- `Service.headers(endpoint)` creates the authentication headers for a client.
```sh
bun add @effect/platform-node
```
```ts
import { NodeFileSystem } from "@effect/platform-node"
import { OpenCode, Service } from "@opencode-ai/client/effect"
import { Effect } from "effect"
import { FetchHttpClient } from "effect/unstable/http"
const program = Effect.gen(function* () {
const endpoint = yield* Service.start()
const client = yield* OpenCode.make({
baseUrl: endpoint.url,
headers: Service.headers(endpoint),
})
return yield* client.health.get()
})
const health = await Effect.runPromise(
program.pipe(
Effect.provide(FetchHttpClient.layer),
Effect.provide(NodeFileSystem.layer),
),
)
```
+86 -41
View File
@@ -5,12 +5,12 @@ description: "Extend OpenCode with plugins."
Plugins extend OpenCode in-process. They can transform agents, models, commands, Plugins extend OpenCode in-process. They can transform agents, models, commands,
integrations, references, skills, and tools; intercept model requests and tool integrations, references, skills, and tools; intercept model requests and tool
execution; and call a location-scoped subset of the V2 client. execution; and call a subset of the V2 client.
<Warning> <Warning>
The V2 plugin API is beta. Entrypoints, hooks, draft shapes, and configuration The V2 plugin API is beta. Entrypoints, hooks, draft shapes, and configuration
may change before the stable release. Use only the `/v2` exports described on may change before the stable release. Use the `/v2` exports described on this
this page; the root `@opencode-ai/plugin` API is the legacy API. page.
</Warning> </Warning>
## Load plugins ## Load plugins
@@ -112,7 +112,7 @@ visible from the plugin file, for example:
```sh ```sh
cd .opencode cd .opencode
bun add @opencode-ai/plugin bun add @opencode-ai/plugin@next
``` ```
Match the plugin package version to the OpenCode release you target. Match the plugin package version to the OpenCode release you target.
@@ -147,14 +147,15 @@ export default Plugin.define({
}) })
``` ```
`setup` runs each time the plugin is activated for a Location. Register `setup` runs each time the plugin is activated. Register long-lived behavior
long-lived behavior during setup; do not wait there on an infinite event during setup; do not wait there on an infinite event stream.
stream.
## Context ### Context
Context methods return Promises. Read and action methods use the same inputs The plugin context is essentially an [OpenCode server client](/build/client).
and location-aware responses as the V2 client APIs. Its read and action methods use the same inputs and responses as the client. It
adds plugin-only methods for transforms, runtime hooks, reloads, registrations,
and plugin options.
| Capability | Available operations | | Capability | Available operations |
| --- | --- | | --- | --- |
@@ -173,16 +174,11 @@ and location-aware responses as the V2 client APIs.
| `ctx.event` | `subscribe` to the current public server event stream | | `ctx.event` | `subscribe` to the current public server event stream |
| `ctx.options` | Readonly options from the matching config object | | `ctx.options` | Readonly options from the matching config object |
Unlike the legacy API, V2 does not provide `$`, `directory`, `worktree`, or a
general SDK client on the context. A plugin is Location-scoped, and the exposed
domain clients apply that Location by default.
### Transform hooks ### Transform hooks
Transforms synchronously edit a draft whenever a stateful domain is built. Transform hooks let a plugin modify how OpenCode is configured. Use them to add
Registering or disposing a transform rebuilds the domain from fresh state and or remove definitions, override settings, choose defaults, and provide tools or
runs all active transforms in order. Call the domain's `reload()` method when other sources.
external data captured by a transform changes.
| Transform | Draft operations | | Transform | Draft operations |
| --- | --- | | --- | --- |
@@ -194,10 +190,41 @@ external data captured by a transform changes.
| `skill.transform` | `source`, `list` | | `skill.transform` | `source`, `list` |
| `tool.transform` | `add` | | `tool.transform` | `add` |
Hook registrations are owned by the plugin scope. Transform and runtime hook Here's an example that keeps models synced from a remote source:
calls also return a `Registration` with `dispose` for explicit cleanup. Tool
contributions currently remain until the owning plugin scope closes, so prefer ```js title=".opencode/plugins/remote-models.js"
scope cleanup for plugin-wide teardown while this API is beta. import { Plugin } from "@opencode-ai/plugin/v2"
export default Plugin.define({
id: "acme.remote-models",
setup: async (ctx) => {
let models = []
await ctx.catalog.transform((catalog) => {
for (const model of models) {
catalog.model.update(model.providerID, model.id, (draft) => Object.assign(draft, model))
}
})
const refresh = async () => {
const response = await fetch("https://example.com/opencode/models.json", {
signal: AbortSignal.timeout(10_000),
})
if (!response.ok) return
models = await response.json()
await ctx.catalog.reload()
}
await refresh()
setInterval(() => void refresh().catch(console.error), 60_000)
},
})
```
`ctx.catalog.reload()` replays every catalog transform to derive the new
catalog. Each plugin's logic remains composed with the others, so a later
plugin can still modify models added by an earlier one. The catalog updates
without restarting OpenCode.
### Runtime hooks ### Runtime hooks
@@ -236,7 +263,9 @@ export default Plugin.define({
A hook failure fails the operation it intercepts. Keep runtime hooks fast and A hook failure fails the operation it intercepts. Keep runtime hooks fast and
handle expected errors inside the callback. handle expected errors inside the callback.
## Add a tool ## Examples
### Add a tool
Pass a tool declaration to `tools.add`. Define its input with JSON Schema and Pass a tool declaration to `tools.add`. Define its input with JSON Schema and
use an async executor: use an async executor:
@@ -284,22 +313,38 @@ configure registration with `{ group, deferred }`:
The executor receives a second context argument containing `sessionID`, The executor receives a second context argument containing `sessionID`,
`agent`, `assistantMessageID`, and `toolCallID`. `agent`, `assistantMessageID`, and `toolCallID`.
## Types ### Add a command
`Plugin.define` infers the context and callbacks. The package also ```js title=".opencode/plugins/review-command.js"
re-exports the canonical `Agent`, `Command`, `Connection`, `Credential`, import { Plugin } from "@opencode-ai/plugin/v2"
`Integration`, `Model`, `Provider`, `Reference`, and `Skill` schema namespaces.
Import narrower API types from their public subpaths when needed:
```ts export default Plugin.define({
import { Plugin, Model } from "@opencode-ai/plugin/v2" id: "acme.review-command",
import type { Context } from "@opencode-ai/plugin/v2/plugin" setup: async (ctx) => {
import type { AgentDraft } from "@opencode-ai/plugin/v2/agent" await ctx.command.transform((commands) => {
import type { ToolExecuteBeforeEvent } from "@opencode-ai/plugin/v2/tool" commands.update("review", (command) => {
command.description = "Review the current changes"
command.template = "Review the current changes for correctness and missing tests."
})
})
},
})
``` ```
Avoid importing types or runtime values from `@opencode-ai/core` or ### Set the default model
`@opencode-ai/server`; those are private host implementation details.
```js title=".opencode/plugins/default-model.js"
import { Plugin } from "@opencode-ai/plugin/v2"
export default Plugin.define({
id: "acme.default-model",
setup: async (ctx) => {
await ctx.catalog.transform((catalog) => {
catalog.model.default.set("anthropic", "claude-sonnet-4-5")
})
},
})
```
## Publish a package ## Publish a package
@@ -313,7 +358,7 @@ manifest is:
"type": "module", "type": "module",
"exports": "./src/index.ts", "exports": "./src/index.ts",
"dependencies": { "dependencies": {
"@opencode-ai/plugin": "1.17.18" "@opencode-ai/plugin": "next"
} }
} }
``` ```
@@ -325,7 +370,7 @@ change.
## Verify loading ## Verify loading
List active plugin IDs for the current Location through the V2 API: List active plugin IDs through the V2 API:
```sh ```sh
opencode2 api get /api/plugin opencode2 api get /api/plugin
@@ -338,12 +383,12 @@ being resolved.
## Effect ## Effect
Plugins built with Effect use the `@opencode-ai/plugin/v2/effect` entrypoint. OpenCode provides a first-class Effect API for plugins through the
Install `effect` alongside the plugin package and export an `effect` function `@opencode-ai/plugin/v2/effect` entrypoint. Install `effect` alongside the
instead of `setup`: plugin package and export an `effect` function instead of `setup`:
```sh ```sh
bun add @opencode-ai/plugin effect bun add @opencode-ai/plugin@next effect
``` ```
```ts title=".opencode/plugins/reviewer-effect.ts" ```ts title=".opencode/plugins/reviewer-effect.ts"