diff --git a/packages/client/package.json b/packages/client/package.json index 85b550779e..531e28caaa 100644 --- a/packages/client/package.json +++ b/packages/client/package.json @@ -16,6 +16,7 @@ "dist" ], "exports": { + ".": "./src/promise/index.ts", "./promise": "./src/promise/index.ts", "./promise/api": "./src/promise/api.ts", "./effect": "./src/effect/index.ts", diff --git a/packages/client/test/import-boundaries.test.ts b/packages/client/test/import-boundaries.test.ts index 1a018a032b..5b881edc32 100644 --- a/packages/client/test/import-boundaries.test.ts +++ b/packages/client/test/import-boundaries.test.ts @@ -12,7 +12,7 @@ const server = resolve(import.meta.dir, "../../server") describe("public import boundaries", () => { 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, schema)).toEqual([]) diff --git a/packages/docs/build/client.mdx b/packages/docs/build/client.mdx index 4299b57520..70eed5104f 100644 --- a/packages/docs/build/client.mdx +++ b/packages/docs/build/client.mdx @@ -16,22 +16,15 @@ network. Its types and methods are generated from the same contract as the ## Install ```sh -bun add @opencode-ai/client +bun add @opencode-ai/client@next ``` -The package has two entrypoints: - -- `@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 Create a client with the server URL, then call methods grouped by API resource: ```ts -import { OpenCode } from "@opencode-ai/client/promise" +import { OpenCode } from "@opencode-ai/client" const client = OpenCode.make({ 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 `headers`. You can also supply a custom `fetch` implementation. Each operation accepts request options as its final argument for an `AbortSignal` or 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: ```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 -client uses canonical V2 values such as `Location.Ref` and `Session.ID`, and -returns typed failures in the Effect error channel. +OpenCode provides a first-class Effect client through the +`@opencode-ai/client/effect` entrypoint. It returns typed Effects and Streams +and decodes responses into OpenCode schema values. + +```sh +bun add @opencode-ai/client@next effect +``` + +### Create a client ```ts 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 `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), + ), +) +``` diff --git a/packages/docs/build/plugins.mdx b/packages/docs/build/plugins.mdx index f6c1bccf1b..c697f7a122 100644 --- a/packages/docs/build/plugins.mdx +++ b/packages/docs/build/plugins.mdx @@ -5,12 +5,12 @@ description: "Extend OpenCode with plugins." Plugins extend OpenCode in-process. They can transform agents, models, commands, 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. 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 - this page; the root `@opencode-ai/plugin` API is the legacy API. + may change before the stable release. Use the `/v2` exports described on this + page. ## Load plugins @@ -112,7 +112,7 @@ visible from the plugin file, for example: ```sh cd .opencode -bun add @opencode-ai/plugin +bun add @opencode-ai/plugin@next ``` 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 -long-lived behavior during setup; do not wait there on an infinite event -stream. +`setup` runs each time the plugin is activated. Register long-lived behavior +during setup; do not wait there on an infinite event stream. -## Context +### Context -Context methods return Promises. Read and action methods use the same inputs -and location-aware responses as the V2 client APIs. +The plugin context is essentially an [OpenCode server client](/build/client). +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 | | --- | --- | @@ -173,16 +174,11 @@ and location-aware responses as the V2 client APIs. | `ctx.event` | `subscribe` to the current public server event stream | | `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 -Transforms synchronously edit a draft whenever a stateful domain is built. -Registering or disposing a transform rebuilds the domain from fresh state and -runs all active transforms in order. Call the domain's `reload()` method when -external data captured by a transform changes. +Transform hooks let a plugin modify how OpenCode is configured. Use them to add +or remove definitions, override settings, choose defaults, and provide tools or +other sources. | Transform | Draft operations | | --- | --- | @@ -194,10 +190,41 @@ external data captured by a transform changes. | `skill.transform` | `source`, `list` | | `tool.transform` | `add` | -Hook registrations are owned by the plugin scope. Transform and runtime hook -calls also return a `Registration` with `dispose` for explicit cleanup. Tool -contributions currently remain until the owning plugin scope closes, so prefer -scope cleanup for plugin-wide teardown while this API is beta. +Here's an example that keeps models synced from a remote source: + +```js title=".opencode/plugins/remote-models.js" +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 @@ -236,7 +263,9 @@ export default Plugin.define({ A hook failure fails the operation it intercepts. Keep runtime hooks fast and 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 use an async executor: @@ -284,22 +313,38 @@ configure registration with `{ group, deferred }`: The executor receives a second context argument containing `sessionID`, `agent`, `assistantMessageID`, and `toolCallID`. -## Types +### Add a command -`Plugin.define` infers the context and callbacks. The package also -re-exports the canonical `Agent`, `Command`, `Connection`, `Credential`, -`Integration`, `Model`, `Provider`, `Reference`, and `Skill` schema namespaces. -Import narrower API types from their public subpaths when needed: +```js title=".opencode/plugins/review-command.js" +import { Plugin } from "@opencode-ai/plugin/v2" -```ts -import { Plugin, Model } from "@opencode-ai/plugin/v2" -import type { Context } from "@opencode-ai/plugin/v2/plugin" -import type { AgentDraft } from "@opencode-ai/plugin/v2/agent" -import type { ToolExecuteBeforeEvent } from "@opencode-ai/plugin/v2/tool" +export default Plugin.define({ + id: "acme.review-command", + setup: async (ctx) => { + await ctx.command.transform((commands) => { + 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 -`@opencode-ai/server`; those are private host implementation details. +### Set the default model + +```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 @@ -313,7 +358,7 @@ manifest is: "type": "module", "exports": "./src/index.ts", "dependencies": { - "@opencode-ai/plugin": "1.17.18" + "@opencode-ai/plugin": "next" } } ``` @@ -325,7 +370,7 @@ change. ## Verify loading -List active plugin IDs for the current Location through the V2 API: +List active plugin IDs through the V2 API: ```sh opencode2 api get /api/plugin @@ -338,12 +383,12 @@ being resolved. ## Effect -Plugins built with Effect use the `@opencode-ai/plugin/v2/effect` entrypoint. -Install `effect` alongside the plugin package and export an `effect` function -instead of `setup`: +OpenCode provides a first-class Effect API for plugins through the +`@opencode-ai/plugin/v2/effect` entrypoint. Install `effect` alongside the +plugin package and export an `effect` function instead of `setup`: ```sh -bun add @opencode-ai/plugin effect +bun add @opencode-ai/plugin@next effect ``` ```ts title=".opencode/plugins/reviewer-effect.ts"