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" },
+ })