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
This commit is contained in:
2026-10-08 13:12:10 +08:00
commit 15a1bbcee0
17 changed files with 3494 additions and 0 deletions
@@ -0,0 +1,79 @@
// RPC implementation + calling — template
//
// Docs: https://opencode.ai/v2/docs/build/plugins/rpc/
//
// Two files. The contract is importable without loading the implementation, so
// other plugins and clients can depend on it.
//
// Needs templates/rpc.ts copied next to this file as src/rpc.ts, so the
// doc's "./rpc.js" specifier resolves. An Effect plugin would import the
// Effect-dialect contract instead — see rpc-effect.ts.
//
// 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"
import { Acme } from "./rpc.js"
export default Plugin.define({
id: "acme-plugin",
async setup(ctx) {
const registration = await ctx.rpc.register(Acme, {
search: async (input, context) => {
// JSON Schema input is `unknown` — narrow it.
const { query } = input as { query: string }
// `signal` cancels with the request; `context.error` declares a failure.
const text = await findText(query, { signal: context.signal })
if (!text) return context.error("not_found", "Result not found", { query })
return { text }
},
})
// The same plugin can call its own RPC once registered.
const acme = ctx.rpc(Acme)
// Raw JSON Schema output arrives as `unknown` in the Promise flavor; the
// docs say to narrow it.
const result = (await acme.search({ query: "hello" })) as { text: string }
// Emit to subscribers.
await registration.events.emit("updated", { itemID: "item-1", text: result.text })
// ---------------------------------------------------------------- events
// Subscribe with the LOCAL name. The received `type` is prefixed
// ("rpc.acme.updated"). Every event carries `data` and `location`.
//
// Subscriptions are LIVE ONLY: a disconnected subscriber misses events.
// Plugin unload closes them.
//
// const unsubscribe = acme.events.on("updated", (event) => {
// console.log(event.type, event.location.directory, event.data.text)
// })
// unsubscribe()
//
// for await (const event of acme.events.subscribe("updated")) {
// console.log(event.data.text)
// }
return () => {
void registration.dispose()
}
},
})
// Calling from a client over HTTP:
// import { OpenCode } from "@opencode/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" })
// Calling from another plugin (a TUI plugin uses `context.client`):
// import { Plugin } from "@opencode/plugin/tui"
// const acme = context.client.rpc(Acme)
// const result = await acme.search({ query: "hello" })
declare function findText(query: string, options: { signal: AbortSignal }): Promise<string | undefined>