diff --git a/packages/core/src/plugin/skill/opencode.md b/packages/core/src/plugin/skill/opencode.md index 1ed6bb2ffc2..a5a7dcfa61f 100644 --- a/packages/core/src/plugin/skill/opencode.md +++ b/packages/core/src/plugin/skill/opencode.md @@ -11,6 +11,9 @@ truth. Follow links from that page when the question needs more detail. Fetch first when you need to discover the relevant documentation page. +A machine-readable documentation index is available at +. + ## Version policy Always answer for OpenCode V2 unless the user explicitly asks about V1, @@ -152,6 +155,8 @@ before answering. Refer to this guide when the user wants to build a plugin. It covers hooks, transforms, tools, plugin context capabilities, and package entrypoints. Plugins can also extend the TUI; for those, fetch the [CLI plugin guide](https://opencode.ai/v2/docs/build/plugins/cli). +For custom methods and events shared with other plugins or clients, fetch the +[RPC guide](https://opencode.ai/v2/docs/build/plugins/rpc). ## [Service](https://opencode.ai/v2/docs/troubleshooting#check-the-background-service) @@ -220,6 +225,16 @@ exposes typed Effects, Streams, and decoded OpenCode schema values. Its `Service` API can discover, start, stop, and authenticate with the local background service from a Node application. +## [SDK](https://opencode.ai/v2/docs/build/sdk) + +For questions about embedding OpenCode directly in an application, fetch the +full [SDK guide](https://opencode.ai/v2/docs/build/sdk) before answering. The SDK +hosts OpenCode in the application without opening an HTTP listener. + +Use the [Effect SDK guide](https://opencode.ai/v2/docs/build/sdk/effect) for +Effect applications. For Cloudflare Durable Objects, use the +[Cloudflare SDK guide](https://opencode.ai/v2/docs/build/sdk/cloudflare). + ## [Troubleshooting](https://opencode.ai/v2/docs/troubleshooting) OpenCode runs a client and a background server. Start by determining whether a diff --git a/packages/www/src/docs/AGENTS.md b/packages/www/src/docs/AGENTS.md index 05b9229213d..18c295262dd 100644 --- a/packages/www/src/docs/AGENTS.md +++ b/packages/www/src/docs/AGENTS.md @@ -14,6 +14,7 @@ - Keep prose sections brief and focused on one idea. Prefer one to three sentences over large paragraphs. - Interleave explanations with concrete code, configuration, command, or output examples so pages do not become walls of text. - Put the relevant example immediately after the text that introduces it, following `content/build/plugins/cli.mdx` as the reference pattern. +- Every subsection that explains syntax, fields, or an API concept must include its own minimal example. A larger example earlier on the page does not count. - Split long explanations with meaningful headings and examples rather than accumulating caveats in one paragraph. - Lead with the common task and working example; place edge cases and supporting details afterward. - Do not stack several prose paragraphs without a visual break. After introducing a concept, use an example, list, table, or task-oriented subheading before covering the next concern. diff --git a/packages/www/src/docs/content/build/client/effect.mdx b/packages/www/src/docs/content/build/client/effect.mdx index c20f2794a4f..0bfa93b223a 100644 --- a/packages/www/src/docs/content/build/client/effect.mdx +++ b/packages/www/src/docs/content/build/client/effect.mdx @@ -104,7 +104,7 @@ RPC error wrapper; reserved `rpc.*` types identify framework failures. RPC events are typed Streams, not callback-style `on` listeners. They receive the RPC's events from all locations, each with required `location` and a normal prefixed type such as `rpc.acme.updated`. This differs from server-plugin handles, -which are fixed to their own location. See [plugin RPC](/build/plugins/rpc) for +which are fixed to their own location. See [Effect plugin RPC](/build/plugins/effect/rpc) for definitions, schemas, registration, and live subscription semantics. ## Local background service diff --git a/packages/www/src/docs/content/build/index.mdx b/packages/www/src/docs/content/build/index.mdx index 8e1221ef756..3f71ce51ae2 100644 --- a/packages/www/src/docs/content/build/index.mdx +++ b/packages/www/src/docs/content/build/index.mdx @@ -6,6 +6,9 @@ OpenCode is used by millions every day. Build on top of it to create your own applications, integrations, and agent experiences without starting from scratch. +We also offer Effect APIs for [plugins](/build/plugins/effect), +[clients](/build/client/effect), and [embedded apps](/build/sdk/effect). + Build plugins that add tools, integrations, commands, agents, and custom behavior while keeping the rest of OpenCode diff --git a/packages/www/src/docs/content/build/plugins/effect.mdx b/packages/www/src/docs/content/build/plugins/effect.mdx index 6d3eee122a6..6b902c3fafd 100644 --- a/packages/www/src/docs/content/build/plugins/effect.mdx +++ b/packages/www/src/docs/content/build/plugins/effect.mdx @@ -631,59 +631,6 @@ interface Context { } ``` -### RPC - -Use the same execution-neutral [`Rpc.define` builder](/build/plugins/rpc). -Effect clients and plugins accept Effect Schema, Standard Schema, or plain JSON -Schema. Promise consumers accept only the portable Standard and JSON formats. - -```ts -import { Plugin } from "@opencode-ai/plugin/effect" -import { Effect } from "effect" -import { Acme } from "./rpc.js" - -export default Plugin.define({ - id: "acme-effect-plugin", - effect: (ctx) => - Effect.gen(function* () { - const registration = yield* ctx.rpc.register(Acme, { - search: ({ query }, context) => - findText(query).pipe( - Effect.flatMap((text) => - text - ? Effect.succeed({ text }) - : Effect.fail(context.error("not_found", "Result not found", { query })), - ), - ), - }) - yield* registration.events.emit("updated", { itemID: "item-1", text: "ready" }) - }).pipe(Effect.orDie), -}) -``` - -Effect handlers use normal interruption. Registrations belong to the plugin -scope; `yield* registration.dispose` removes one explicitly. Later registrations -override earlier ones at the same location, without changing in-flight handlers. - -`ctx.rpc(Acme)` returns a local typed subclient. Its methods return Effects and -`events.subscribe(name)` returns a Stream. Use scoped fibers when listening -during plugin lifetime: - -```ts -const acme = ctx.rpc(Acme) -yield * - acme.events.subscribe("updated").pipe( - Stream.runForEach((event) => Effect.logInfo(event.data.text)), - Effect.forkScoped, - ) -``` - -There is no Effect callback-style `on` API. Subscriptions are location-bound, -live-only, and close when Stream consumption stops. Events use the normal -ephemeral Bus path. Method `errors` maps become typed Effect error channels. Construct one -with `context.error(...)` and fail it with `Effect.fail`; unexpected failures and -transport errors remain separate from the declared method errors. - ### References Read references available at the current location. diff --git a/packages/www/src/docs/content/build/plugins/effect/rpc.mdx b/packages/www/src/docs/content/build/plugins/effect/rpc.mdx new file mode 100644 index 00000000000..d0c0d4e9141 --- /dev/null +++ b/packages/www/src/docs/content/build/plugins/effect/rpc.mdx @@ -0,0 +1,169 @@ +--- +title: "RPC" +--- + +Effect plugins can expose methods and events that return typed Effects and +Streams. + +## Define + +Use `Rpc.define` with Effect Schema to define the RPC. + +```ts title="src/rpc.ts" +import { Rpc } from "@opencode-ai/plugin/rpc" +import { Schema } from "effect" + +export const Acme = Rpc.define({ + id: "acme", + methods: { + search: { + input: Schema.Struct({ query: Schema.String }), + output: Schema.Struct({ text: Schema.String }), + errors: { + not_found: Schema.Struct({ query: Schema.String }), + }, + }, + }, + events: { + updated: { + schema: Schema.Struct({ itemID: Schema.String, text: Schema.String }), + }, + }, +}) +``` + +### Validation + +Effect Schema validates values and infers their TypeScript types. + +```ts +input: Schema.Struct({ query: Schema.String }) +``` + +JSON Schema and Standard Schema are also supported. + +### Input and output + +Use `input` for the method argument and `output` for its return value. Leave +either one out when there is no value. + +```ts +search: { + input: Schema.Struct({ query: Schema.String }), + output: Schema.Struct({ text: Schema.String }), +} +``` + +### Errors + +Add expected failures to `errors`. Each key becomes the error's `type`. + +```ts +errors: { + not_found: Schema.Struct({ query: Schema.String }), +} +``` + +### Events + +Add events to the top-level `events` map. Event data must be an object. + +```ts +events: { + updated: { + schema: Schema.Struct({ itemID: Schema.String, text: Schema.String }), + }, +} +``` + +## Implement + +Register the implementation from the plugin Effect: + +```ts title="src/index.ts" +import { Plugin } from "@opencode-ai/plugin/effect" +import { Effect } from "effect" +import { Acme } from "./rpc.js" + +export default Plugin.define({ + id: "acme-effect-plugin", + effect: (ctx) => + Effect.gen(function* () { + const registration = yield* ctx.rpc.register(Acme, { + search: ({ query }, context) => + findText(query).pipe( + Effect.flatMap((text) => + text + ? Effect.succeed({ text }) + : Effect.fail(context.error("not_found", "Result not found", { query })), + ), + ), + }) + + const acme = ctx.rpc(Acme) + const result = yield* acme.search({ query: "hello" }) + + yield* registration.events.emit("updated", { itemID: "item-1", text: result.text }) + }).pipe(Effect.orDie), +}) +``` + +The error map becomes the error type of each method Effect. Use +`context.error(...)` to create a declared error. + +## Call + +Once the RPC is registered, it can be called over HTTP or from another plugin. + +### HTTP + +Create the Effect client, then create the RPC subclient: + +```ts +import { OpenCode } from "@opencode-ai/client/effect" +import { Effect } from "effect" +import { FetchHttpClient } from "effect/unstable/http" +import { Acme } from "opencode-acme-plugin/rpc" + +const program = Effect.gen(function* () { + const client = yield* OpenCode.make({ baseUrl: "http://localhost:4096" }) + const acme = client.rpc(Acme) + return yield* acme.search({ query: "hello" }) +}) + +const result = await Effect.runPromise(program.pipe(Effect.provide(FetchHttpClient.layer))) +``` + +### Plugin + +Another Effect plugin can create a local subclient from its context: + +```ts +import { Effect } from "effect" + +effect: (ctx) => + Effect.gen(function* () { + const acme = ctx.rpc(Acme) + const result = yield* acme.search({ query: "hello" }) + yield* Effect.logInfo(result.text) + }).pipe(Effect.orDie) +``` + +### Subscribe + +RPC events are Streams. Subscribe by event name and run the Stream in a scoped +fiber: + +```ts +import { Effect, Stream } from "effect" + +const acme = ctx.rpc(Acme) + +yield * + acme.events.subscribe("updated").pipe( + Stream.runForEach((event) => Effect.logInfo(event.data.text)), + Effect.forkScoped, + ) +``` + +Subscriptions are live only and close when Stream consumption stops. diff --git a/packages/www/src/docs/content/build/plugins/rpc.mdx b/packages/www/src/docs/content/build/plugins/rpc.mdx index 4522877c9e6..0a7e01791fd 100644 --- a/packages/www/src/docs/content/build/plugins/rpc.mdx +++ b/packages/www/src/docs/content/build/plugins/rpc.mdx @@ -2,79 +2,129 @@ title: "RPC" --- -Expose typed methods and custom events through a shared RPC definition. Start by -putting the contract in a browser-safe module, separate from plugin setup and -server code. +Plugins can expose custom methods and events that run on the server and can be +called by other plugins or clients. -## Define the contract +## Define -Use `Rpc.define` to declare the RPC ID, methods, errors, and events. The builder -is synchronous and independent of Promise or Effect execution. +Use `Rpc.define` to list the RPC's methods, errors, and events. ```ts title="src/rpc.ts" import { Rpc } from "@opencode-ai/plugin/rpc" -import { z } from "zod" export const Acme = Rpc.define({ id: "acme", methods: { search: { - input: z.object({ query: z.string() }), - output: z.object({ text: z.string() }), + input: { + type: "object", + properties: { query: { type: "string" } }, + required: ["query"], + additionalProperties: false, + }, + output: { + type: "object", + properties: { text: { type: "string" } }, + required: ["text"], + additionalProperties: false, + }, errors: { - not_found: z.object({ query: z.string() }), + not_found: { + type: "object", + properties: { query: { type: "string" } }, + required: ["query"], + additionalProperties: false, + }, }, }, }, events: { updated: { - schema: z.object({ itemID: z.string(), text: z.string() }), + schema: { + type: "object", + properties: { + itemID: { type: "string" }, + text: { type: "string" }, + }, + required: ["itemID", "text"], + additionalProperties: false, + }, }, }, }) ``` -## Choose schemas +### Validation -Use Standard Schema, such as Zod, when Promise and Effect consumers share the -contract. It validates at runtime and infers TypeScript types. +An RPC definition describes the shapes of input and output of methods, events +and errors. It supports two schema formats: -Other schema formats have narrower tradeoffs: +- JSON Schema, simple and requires no dependencies. +- Any Standard Schema compliant validator + - Zod + - Valibot + - ArkType -- Plain JSON Schema validates at runtime, but its inferred TypeScript value is `unknown`. -- Plain JSON Schema uses Draft 2020-12 through Effect's JSON Schema importer and decoder. -- Use Standard Schema when you need another JSON Schema dialect or parser. -- Effect Schema is supported only by Effect plugins and clients. +### Input and output -Every method declares `input` and `output`. Omit either to represent no value. -An empty event payload is an object instead: +Each method can define an `input` schema for its argument and an `output` schema +for its return value. Leave either one out if the method does not accept or +return a value. ```ts -events: { - refreshed: { schema: z.object({}) }, +search: { + input: { + type: "object", + properties: { query: { type: "string" } }, + required: ["query"], + }, + output: { + type: "object", + properties: { text: { type: "string" } }, + required: ["text"], + }, } ``` -Event schemas must produce JSON objects. Scalars, arrays, `null`, and `undefined` -are invalid event data. Plain JSON Schema events are checked when emitted even -though their payload type remains `unknown`. +JSON Schema values are `unknown` in TypeScript, so narrow them before use. +Standard Schema infers the input and output types. -## Declare errors +### Errors Add an `errors` map to a method for expected failures. Each key becomes the -literal error `type`, and its schema validates and transforms the error `data`. +error's `type`, and its schema defines the error's `data`. ```ts errors: { - not_found: z.object({ query: z.string() }), + not_found: { + type: "object", + properties: { query: { type: "string" } }, + required: ["query"], + additionalProperties: false, + }, } ``` -Names beginning with `rpc.` are reserved for framework failures. Contract schemas -own parsing, transformations, and Effect encoding; RPC does not perform another -generic JSON validation pass. +Error names beginning with `rpc.` are reserved by OpenCode. -## Register the implementation +### Events + +Add events to the top-level `events` map. Each event has a schema for the data +sent to subscribers. + +Event data must be an object. Use an empty object schema when there is no data: + +```ts +events: { + refreshed: { + schema: { type: "object", additionalProperties: false }, + }, +} +``` + +Scalars, arrays, `null`, and `undefined` are not valid event data. + +## Implement Register the implementation inside `setup`: @@ -86,42 +136,70 @@ export default Plugin.define({ id: "acme-plugin", async setup(ctx) { const registration = await ctx.rpc.register(Acme, { - search: async ({ query }, context) => { + search: async (input, context) => { + const { query } = input as { query: string } const text = await findText(query, { signal: context.signal }) if (!text) return context.error("not_found", "Result not found", { query }) return { text } }, }) + const acme = ctx.rpc(Acme) + const result = await acme.search({ query: "hello" }) + await registration.events.emit("updated", { itemID: "item-1", text: "ready" }) }, }) ``` -Promise handlers receive `signal` and a typed `error(type, message, data)` -constructor in their second argument. Return or throw a constructed error to -reject callers with `{ type, message, data? }`. +After registering the RPC, the same plugin can call it through `ctx.rpc(Acme)`. -Registration follows these rules: +The second argument includes `signal` for cancellation and `context.error(...)` +for declared errors. You can return or throw the error. -- RPC IDs are independent of plugin IDs, and one plugin can implement several RPCs. -- A later registration overrides an earlier registration at the same location. -- Disposal or plugin unload removes only that registration and reveals the previous one. -- In-flight calls retain the handler with which they started. +One plugin can register more than one RPC. Disposing the registration removes it. -## Call from another plugin +## Call -Other server plugins can obtain a handle without implementing the RPC: +Once the RPC is registered, it can be called over HTTP or from another plugin. + +### HTTP + +Create an OpenCode client, then use `client.rpc` to create a subclient for the +RPC: ```ts -const acme = ctx.rpc(Acme) +import { OpenCode } from "@opencode-ai/client" +import { Acme } from "opencode-acme-plugin/rpc" + +const client = OpenCode.make({ + baseUrl: "http://localhost:4096", +}) + +const acme = client.rpc(Acme) const result = await acme.search({ query: "hello" }) ``` -The handle is available immediately, and each call finds the current registration. -Calls stay within the server plugin's location and cannot override it. +### Plugin -## Subscribe to events +Plugins already have an OpenCode client. For example, a TUI plugin can create +the same RPC subclient from `context.client`: + +```ts title="src/tui.ts" +import { Plugin } from "@opencode-ai/plugin/tui" +import { Acme } from "opencode-acme-plugin/rpc" + +export default Plugin.define({ + id: "acme-tui", + async setup(context) { + const acme = context.client.rpc(Acme) + const result = await acme.search({ query: "hello" }) + console.log(result) + }, +}) +``` + +### Subscribe Use `events.on` for a callback and unsubscribe when the listener is no longer needed: @@ -142,20 +220,12 @@ for await (const event of acme.events.subscribe("updated")) { } ``` -Event subscriptions have these semantics: +Event subscriptions are live only, so disconnected subscribers miss events. -- Event keys use local names such as `updated` when subscribing. -- Delivered types are prefixed, such as `rpc.acme.updated`. -- Each event includes `id`, `created`, direct `data`, required `location`, and optional `metadata`. -- Events use the normal ephemeral Bus path and are live-only; disconnected subscribers miss events. -- Plugin unload closes its subscriptions. There is no plugin event log or replay API yet. -- The method name `events` is reserved for the subclient event API. +- Subscribe with the local name, such as `updated`. +- The received type is prefixed, such as `rpc.acme.updated`. +- Each event includes `data` and `location`. +- Plugin unload closes its subscriptions. -## Connect external clients - -External [clients](/build/client#plugin-rpc) use `client.rpc(Acme)` and receive -that RPC's events across all locations. The native `/api/event` stream and -typed subclients observe the same direct `rpc..` envelope. - -Importing the contract or constructing a handle does not load its server -implementation. Configure the plugin on the server separately. +External clients receive events from every location, so check `event.location` +when needed. The server plugin still needs to be configured and running. diff --git a/packages/www/src/docs/content/build/sdk/index.mdx b/packages/www/src/docs/content/build/sdk/index.mdx index 13b2d6beb82..6b39ecebcc6 100644 --- a/packages/www/src/docs/content/build/sdk/index.mdx +++ b/packages/www/src/docs/content/build/sdk/index.mdx @@ -7,6 +7,8 @@ title: "Overview" calls through its HTTP router in memory. It opens no HTTP listener and adds no network hop between the client and server. +For Cloudflare Durable Objects, see the [Cloudflare guide](/build/sdk/cloudflare). + The V2 SDK is beta. Install the current preview with `bun add @opencode-ai/sdk@dev`; its API may change before a stable release. diff --git a/packages/www/src/docs/lib/llms.ts b/packages/www/src/docs/lib/llms.ts new file mode 100644 index 00000000000..6d63e883439 --- /dev/null +++ b/packages/www/src/docs/lib/llms.ts @@ -0,0 +1,25 @@ +import { docsSections } from "./navigation" + +export function renderLlmsTxt(site: URL) { + const base = new URL(import.meta.env.BASE_URL, site) + const sections = docsSections.flatMap((section) => [ + `## ${section.title}`, + "", + ...section.groups.flatMap((group) => [ + ...(group.title && group.title !== section.title ? [`### ${group.title}`, ""] : []), + ...group.items.map((item) => { + const path = item.slug === "index" ? "" : `${item.slug.replace(/\/index$/, "")}/` + return `- [${item.title}](${new URL(`docs/${path}`, base)})` + }), + "", + ]), + ]) + + return [ + "# OpenCode V2 Documentation", + "", + "> Official documentation for using, configuring, and building with OpenCode V2.", + "", + ...sections, + ].join("\n") +} diff --git a/packages/www/src/docs/lib/navigation.ts b/packages/www/src/docs/lib/navigation.ts index b91704f1d34..7ccdfec9a76 100644 --- a/packages/www/src/docs/lib/navigation.ts +++ b/packages/www/src/docs/lib/navigation.ts @@ -95,25 +95,29 @@ export const docsSections: DocsSection[] = [ items: [ { title: "Overview", slug: "build/plugins" }, { title: "RPC", slug: "build/plugins/rpc" }, - { title: "Effect", slug: "build/plugins/effect" }, { title: "CLI", slug: "build/plugins/cli" }, ], }, { title: "Client", - items: [ - { title: "JavaScript", slug: "build/client" }, - { title: "Effect", slug: "build/client/effect" }, - ], + items: [{ title: "JavaScript", slug: "build/client" }], }, { title: "SDK", items: [ { title: "Overview", slug: "build/sdk" }, - { title: "Effect", slug: "build/sdk/effect" }, { title: "Cloudflare", slug: "build/sdk/cloudflare" }, ], }, + { + title: "Effect", + items: [ + { title: "Plugins", slug: "build/plugins/effect" }, + { title: "RPC", slug: "build/plugins/effect/rpc" }, + { title: "Client", slug: "build/client/effect" }, + { title: "SDK", slug: "build/sdk/effect" }, + ], + }, ], }, { diff --git a/packages/www/src/pages/llms.txt.ts b/packages/www/src/pages/llms.txt.ts new file mode 100644 index 00000000000..b16bc5c77a2 --- /dev/null +++ b/packages/www/src/pages/llms.txt.ts @@ -0,0 +1,9 @@ +import type { APIRoute } from "astro" +import { renderLlmsTxt } from "../docs/lib/llms" + +export const prerender = true + +export const GET: APIRoute = ({ site }) => + new Response(renderLlmsTxt(site ?? new URL("https://opencode.ai")), { + headers: { "Content-Type": "text/plain; charset=utf-8" }, + })