Files
oc/skills/opencode-plugins/templates/rpc-implementation-effect.ts
T
xiaoshuai 15a1bbcee0 Version OpenCode skills and plugins
Repository sits at the OpenCode config root rather than inside skills/ or
plugins/. OpenCode scans exactly those two subdirectories, and a .git
directory inside either one is picked up as a plugin and fails to load:

  loading plugin id=.../plugins/.git entrypoint=.../plugins/.git/index
  failed to load plugin ... AggregateError: 2 errors building .../plugins/.git/index

The watcher.ignore config option does not help: it only suppresses filesystem
update events, not plugin discovery, and the "-" disable prefix only applies to
plugins listed in opencode.json(c), not auto-discovered ones.

.gitignore admits only skills/ and plugins/ (plus itself). Personal config
(opencode.jsonc, cli.json, service.json) and node_modules stay local.

skills/opencode-plugins  V2-only plugin authoring reference and 7 templates,
                         transcribed from the V2 docs with the URL on every file
plugins/searxng-search   searxng_search tool over a self-hosted SearXNG JSON API
2026-10-08 13:12:10 +08:00

75 lines
2.7 KiB
TypeScript

// RPC implementation (Effect flavor) — template
//
// Docs: https://opencode.ai/v2/docs/build/plugins/effect/rpc/
//
// Needs templates/rpc-effect.ts copied next to this file as src/rpc.ts, so the
// doc's "./rpc.js" specifier resolves. Rename it rpc.ts on save; the contract
// shape is identical, only the schema dialect differs.
//
// Registration goes inside the plugin `effect`, NOT `setup`.
//
// The docs use "@opencode/plugin" and "@opencode/plugin/effect". Install the
// package at your OpenCode version and restart the service first — see the
// PREREQUISITE block in promise-plugin.ts.
import { Plugin } from "@opencode/plugin/effect"
import { Effect, Stream } 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, {
// Input arrives already decoded — destructure it, do not cast.
search: ({ query }, context) =>
findText(query).pipe(
Effect.flatMap((text) =>
text
? Effect.succeed({ text })
: Effect.fail(context.error("not_found", "Result not found", { query })),
),
),
})
// The same plugin can call its own RPC.
const acme = ctx.rpc(Acme)
const result = yield* acme.search({ query: "hello" })
yield* registration.events.emit("updated", { itemID: "item-1", text: result.text })
// Subscriptions are a Stream; forkScoped handles interruption.
// "Subscriptions are live only and close when Stream consumption stops."
yield* acme.events.subscribe("updated").pipe(
Stream.runForEach((event) => Effect.logInfo(event.data.text)),
Effect.forkScoped,
)
// context.signal is NOT documented on the Effect page. Use interruption.
}).pipe(Effect.orDie),
})
// Calling over HTTP uses the Effect client and needs an HTTP client layer:
//
// import { OpenCode } from "@opencode/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)),
// )
//
// Another Effect plugin calls it with ctx.rpc(Acme). events.on and the
// prefixed event type are documented only on the Promise page — check before
// relying on either here.
declare function findText(query: string): Effect.Effect<string | undefined>