Przeglądaj źródła

feat(client): add root promise entrypoint

Dax Raad 1 miesiąc temu
rodzic
commit
6a85f0d3db

+ 1 - 0
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",

+ 1 - 1
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([])

+ 68 - 14
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
+
+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.
 
-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.
+```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),
+  ),
+)
+```

+ 86 - 41
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.
 
 <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.
+  may change before the stable release. Use the `/v2` exports described on this
+  page.
 </Warning>
 
 ## 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"