fix(plugin): select plugins by id

This commit is contained in:
Dax Raad 2026-07-10 00:44:43 -04:00
commit fbb0fdf88a
8 changed files with 149 additions and 152 deletions

91
packages/docs/build/client.mdx vendored Normal file
View file

@ -0,0 +1,91 @@
---
title: "Client"
description: "Connect an application to the OpenCode HTTP API."
---
`@opencode-ai/client` is the generated TypeScript client for the OpenCode HTTP
API. Use it when your application connects to an OpenCode server over the
network. Its types and methods are generated from the same contract as the
[API reference](/api).
<Warning>
The V2 API and client are beta. Method names, inputs, and outputs may change
before the stable release.
</Warning>
## Install
```sh
bun add @opencode-ai/client
```
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 with the server URL, then call methods grouped by API resource:
```ts
import { OpenCode } from "@opencode-ai/client/promise"
const client = OpenCode.make({
baseUrl: "http://localhost:4096",
})
const session = await client.session.create({
location: { directory: "/workspace" },
})
await client.session.prompt({
sessionID: session.id,
text: "Review the current changes",
})
```
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.
Streaming endpoints return async iterables:
```ts
for await (const event of client.event.subscribe()) {
console.log(event.type)
}
```
## Effect client
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.
```ts
import { AbsolutePath, Location, OpenCode } from "@opencode-ai/client/effect"
import { Effect } from "effect"
import { FetchHttpClient } from "effect/unstable/http"
const program = Effect.gen(function* () {
const client = yield* OpenCode.make({ baseUrl: "http://localhost:4096" })
const session = yield* client.session.create({
location: Location.Ref.make({
directory: AbsolutePath.make("/workspace"),
}),
})
return yield* client.session.get({ sessionID: session.id })
})
const session = await Effect.runPromise(
program.pipe(Effect.provide(FetchHttpClient.layer)),
)
```
Streaming operations, including `client.event.subscribe()` and
`client.session.log(...)`, return Effect `Stream` values.

374
packages/docs/build/plugins.mdx vendored Normal file
View file

@ -0,0 +1,374 @@
---
title: "Plugins"
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.
<Warning>
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.
</Warning>
## Load plugins
Plugins can be loaded from npm packages, explicit local paths, or config
directories. Each module must have one default export containing a unique
plugin `id` and a `setup` function.
### Configuration
Add ordered entries to the `plugins` field in `opencode.json(c)`:
```jsonc title="opencode.jsonc"
{
"$schema": "https://opencode.ai/config.json",
"plugins": [
"opencode-acme-plugin@1.2.0",
"@acme/opencode-plugin",
"./plugins/local.ts",
{
"package": "./plugins/reviewer.ts",
"options": {
"agent": "reviewer",
"strict": true
}
}
]
}
```
A string is either a package specifier or a local path. Local paths must start
with `./` or `../` and resolve relative to the configuration file containing
the entry. Absolute paths and `file://` URLs are also supported. Both scoped
packages and versioned package specifiers are supported.
Use the object form to pass JSON configuration to the plugin. OpenCode passes
`options` unchanged as `ctx.options`; omitted options become an empty object.
The plugin owns validation and defaults for its options.
See [Config](/config#locations) for configuration locations and precedence.
Entries from all applicable files are processed from lowest to highest
precedence rather than replacing the entire array.
### Local discovery
OpenCode automatically scans this directory in every discovered OpenCode config
directory:
```text
.opencode/plugins/
```
The equivalent global directory is `~/.config/opencode/plugins/`. Direct `.ts`
and `.js` children are loaded. An immediate child directory is also loaded as a
package when OpenCode can resolve a string `exports`, `module`, or `main`
entrypoint, or an `index.ts` or `index.js` file.
A `plugins/` directory beside a project-root `opencode.json` is not discovered
automatically. Put it under `.opencode/`, or add its file explicitly with a
relative config entry.
### Enable and disable
A string beginning with `-` disables plugins by their exported `id`. `*`
matches every ID, and a suffix of `.*` matches an ID prefix. Directives are
applied in order:
```jsonc title="opencode.jsonc"
{
"plugins": [
"./plugins/reviewer.ts",
"-acme.reviewer",
"-opencode.provider.*",
"opencode.provider.openai"
]
}
```
Package specifiers and local paths locate plugin modules; they are not disable
selectors. Use the `id` from the plugin's default export to disable it. A later
ID entry re-enables a loaded or built-in plugin. Explicit config directives run
after local auto-discovery, so they can disable discovered plugins by ID.
User plugins are activated in configured order between OpenCode's internal
plugin phases. Hooks run sequentially in registration order, and later hooks
observe earlier mutations. Do not depend on the internal phase ordering while
the API is beta.
### Installation and dependencies
OpenCode installs bare package entries and their production dependencies into
an isolated cache. Package installation does not run lifecycle scripts.
Published packages should expose their plugin entrypoint and include every
runtime import in `dependencies`.
Local files and local package directories are imported directly. OpenCode does
**not** install their dependencies. Install dependencies in a `package.json`
visible from the plugin file, for example:
```sh
cd .opencode
bun add @opencode-ai/plugin
```
Match the plugin package version to the OpenCode release you target.
Configuration and discovered plugin files under watched config directories are
reloaded when they change. Reloading replaces the active plugin generation and
releases its scoped registrations. Restart OpenCode after changing an npm
package version or a local dependency when no watched file changed.
## Create a plugin
Export the result of `Plugin.define` as the module default:
```ts title=".opencode/plugins/reviewer.ts"
import { Plugin } from "@opencode-ai/plugin/v2"
export default Plugin.define({
id: "acme.reviewer",
setup: async (ctx) => {
const description =
typeof ctx.options.description === "string"
? ctx.options.description
: "Reviews code for regressions"
await ctx.agent.transform((agents) => {
agents.update("reviewer", (agent) => {
agent.description = description
agent.mode = "subagent"
})
})
},
})
```
`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.
## Context
Context methods return Promises. Read and action methods use the same inputs
and location-aware responses as the V2 client APIs.
| Capability | Available operations |
| --- | --- |
| `ctx.agent` | `list`, `transform`, `reload` |
| `ctx.catalog.provider` | `list`, `get` |
| `ctx.catalog.model` | `list`, `default` |
| `ctx.catalog` | `transform`, `reload` |
| `ctx.command` | `list`, `transform`, `reload` |
| `ctx.integration` | `list`, `get`, `connect`, `attempt`, `transform`, `reload`, and connection lookup/resolution |
| `ctx.plugin` | `list` currently active plugin IDs |
| `ctx.reference` | `list`, `transform`, `reload` |
| `ctx.session` | `create`, `get`, `prompt`, `command`, `interrupt`, and `hook` |
| `ctx.skill` | `list`, `transform`, `reload` |
| `ctx.tool` | `transform` and `hook` |
| `ctx.aisdk` | `hook` |
| `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 | Draft operations |
| --- | --- |
| `agent.transform` | `list`, `get`, `default`, `update`, `remove` |
| `catalog.transform` | Provider `list`, `get`, `update`, `remove`; model `get`, `update`, `remove`; default model `get`, `set` |
| `command.transform` | `list`, `get`, `update`, `remove` |
| `integration.transform` | Integration `list`, `get`, `update`, `remove`; method `list`, `update`, `remove` |
| `reference.transform` | `add`, `remove`, `list` |
| `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.
### Runtime hooks
Runtime hooks intercept live operations. Their event objects expose specific
mutable fields:
| Hook | Mutable fields |
| --- | --- |
| `ctx.aisdk.hook("sdk", callback)` | `sdk`, after inspecting `model`, `package`, and `options` |
| `ctx.aisdk.hook("language", callback)` | `language`, after inspecting `model`, `sdk`, and `options` |
| `ctx.session.hook("request", callback)` | `system`, `messages`, and the `tools` record immediately before model dispatch |
| `ctx.tool.hook("execute.before", callback)` | `input`, before the selected tool executes |
| `ctx.tool.hook("execute.after", callback)` | `result`, `output`, and `outputPaths`, after execution settles |
For example, remove a tool from selected model requests and normalize another
tool's input:
```ts title=".opencode/plugins/guards.ts"
import { Plugin } from "@opencode-ai/plugin/v2"
export default Plugin.define({
id: "acme.guards",
setup: async (ctx) => {
await ctx.session.hook("request", (event) => {
delete event.tools.write
})
await ctx.tool.hook("execute.before", (event) => {
if (event.tool !== "lookup" || typeof event.input !== "object" || event.input === null) return
event.input = { ...event.input, source: "plugin" }
})
},
})
```
A hook failure fails the operation it intercepts. Keep runtime hooks fast and
handle expected errors inside the callback.
## Add a tool
Pass a tool declaration to `tools.add`. Define its input with JSON Schema and
use an async executor:
```js title=".opencode/plugins/greeting.js"
import { Plugin } from "@opencode-ai/plugin/v2"
export default Plugin.define({
id: "acme.greeting",
setup: async (ctx) => {
await ctx.tool.transform((tools) => {
tools.add({
name: "greeting",
description: "Create a greeting",
jsonSchema: {
type: "object",
properties: {
name: { type: "string" },
},
required: ["name"],
additionalProperties: false,
},
execute: async ({ name }) => {
const text = `Hello, ${name}!`
return {
structured: { greeting: text },
content: [{ type: "text", text }],
}
},
})
})
},
})
```
Unsupported characters in tool and group names are normalized to underscores.
The resulting exposed key must begin with a letter and contain at most 64
letters, digits, underscores, or hyphens. Set `options` on the declaration to
configure registration with `{ group, deferred }`:
- `group` prefixes and groups the exposed tool name.
- `deferred: true` makes the tool available through the deferred `execute`
tool instead of exposing it directly.
The executor receives a second context argument containing `sessionID`,
`agent`, `assistantMessageID`, and `toolCallID`.
## Types
`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:
```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"
```
Avoid importing types or runtime values from `@opencode-ai/core` or
`@opencode-ai/server`; those are private host implementation details.
## Publish a package
A package plugin uses the same default export as a local plugin. A minimal
manifest is:
```json title="package.json"
{
"name": "opencode-acme-plugin",
"version": "1.0.0",
"type": "module",
"exports": "./src/index.ts",
"dependencies": {
"@opencode-ai/plugin": "1.17.18"
}
}
```
Use versions compatible with the OpenCode release you target and test the
installed package, not only a workspace-linked copy. Because the plugin API is
beta, publish compatible plugin updates when V2 entrypoints or contracts
change.
## Verify loading
List active plugin IDs for the current Location through the V2 API:
```sh
opencode2 api get /api/plugin
```
If a plugin is absent, check the server log described in
[Troubleshooting](/troubleshooting#read-logs). Invalid modules and setup failures are
logged; one failing package does not prevent unrelated valid packages from
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`:
```sh
bun add @opencode-ai/plugin effect
```
```ts title=".opencode/plugins/reviewer-effect.ts"
import { Plugin } from "@opencode-ai/plugin/v2/effect"
import { Effect } from "effect"
export default Plugin.define({
id: "acme.reviewer-effect",
effect: (ctx) =>
Effect.gen(function* () {
yield* ctx.agent.transform((agents) => {
agents.update("reviewer", (agent) => {
agent.description = "Reviews code for regressions"
agent.mode = "subagent"
})
})
}),
})
```
Context operations return Effects. The plugin effect is scoped, so finalizers,
fibers, and registrations are released when the plugin reloads or unloads.
OpenCode does not expose its private Core services to the plugin; use the
capabilities on `ctx`.
Typed tools can use `Schema` from `effect` and the contracts exported from
`@opencode-ai/plugin/v2/effect/tool`. Their executors return an Effect and may
fail with the typed tool failure channel.

75
packages/docs/build/sdk.mdx vendored Normal file
View file

@ -0,0 +1,75 @@
---
title: "SDK"
description: "Embed an OpenCode host in an Effect application."
---
`@opencode-ai/sdk-next` is the Effect-native SDK for applications that need to
host OpenCode in-process. Unlike the [network client](/build/client), it assembles the
OpenCode server and routes API calls through its HTTP router in memory. It opens
no HTTP listener and adds no network hop between the client and server.
<Warning>
The V2 SDK is beta and currently private to the OpenCode workspace. It is not
published for external installation yet, and its package name and API may
change before release.
</Warning>
## Create a host
`OpenCode.create()` creates a scoped host. Closing its Effect Scope releases
the router, location services, fibers, and scoped plugin registrations.
```ts
import {
AbsolutePath,
Location,
OpenCode,
} from "@opencode-ai/sdk-next"
import { Effect } from "effect"
const program = Effect.scoped(
Effect.gen(function* () {
const opencode = yield* OpenCode.create()
const session = yield* opencode.sessions.create({
location: Location.Ref.make({
directory: AbsolutePath.make("/workspace"),
}),
})
return yield* opencode.sessions.get({ sessionID: session.id })
}),
)
const session = await Effect.runPromise(program)
```
The embedded host uses the same routes, middleware, codecs, errors, and schema
values as `@opencode-ai/client/effect`. It exposes the full generated client and
adds the convenience aliases `sessions` and `events` for the session and event
groups.
## Use as a service
Use `OpenCode.layer` when the host should be provided through Effect dependency
injection:
```ts
import { OpenCode } from "@opencode-ai/sdk-next"
import { Effect } from "effect"
const program = Effect.gen(function* () {
const opencode = yield* OpenCode.Service
return yield* opencode.sessions.active()
})
const active = await Effect.runPromise(
program.pipe(Effect.provide(OpenCode.layer)),
)
```
## Register plugins
Call `opencode.plugin(...)` to register an embedded V2 plugin. Embedded plugins
use the same discovery and location-scoped activation path as configured
plugins. The SDK also exports `Tool` for plugin-defined tools. See the
[Plugins guide](/build/plugins) for the plugin shape and available hooks.