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:
@@ -0,0 +1,138 @@
|
||||
// Promise server plugin — template
|
||||
//
|
||||
// Docs: https://opencode.ai/v2/docs/build/plugins
|
||||
// Config: https://opencode.ai/v2/docs/plugins
|
||||
//
|
||||
// Copy this file, keep only the sections you need, and delete the rest.
|
||||
// Transcribed from the doc page's own examples. Where a field name is uncertain,
|
||||
// re-read the page above before inventing it.
|
||||
//
|
||||
// ---------------------------------------------------------------------------
|
||||
// PREREQUISITE: the package is not bundled with the CLI. Install it at the
|
||||
// version matching your OpenCode release, then restart the service (the running
|
||||
// service caches module resolution):
|
||||
//
|
||||
// cd ~/.config/opencode
|
||||
// npm install @opencode/plugin@$(opencode --version | sed 's/^v//')
|
||||
// opencode service restart
|
||||
//
|
||||
// A stale `@opencode-ai/plugin` (the V1-era name) in node_modules is a leftover
|
||||
// dependency, not what the host provides — ignore its export list.
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
// Model.Info.default(providerID, Model.ID.make("id")) builds a model definition.
|
||||
import { Model, Plugin, Provider } from "@opencode/plugin"
|
||||
|
||||
export default Plugin.define({
|
||||
// Give this a name that will not collide with another plugin.
|
||||
id: "example",
|
||||
|
||||
// Runs when the plugin loads. May return a cleanup function.
|
||||
async setup(ctx) {
|
||||
// ---------------------------------------------------------------- storage
|
||||
// Durable JSON scoped to the plugin. Storage returns values directly.
|
||||
await ctx.storage.set("settings", { strict: true })
|
||||
// ctx.storage.get("settings") -> the value
|
||||
// ctx.storage.scan({ prefix: "cache/", limit: 100 }) -> { entries, next }
|
||||
// ctx.storage.remove("settings")
|
||||
|
||||
// ---------------------------------------------------------------- options
|
||||
// Untyped. Narrow before use.
|
||||
const strict = ctx.options.strict === true
|
||||
|
||||
// ------------------------------------------------------------- transform
|
||||
// The callback is SYNCHRONOUS. Do not return a Promise or an Effect from it.
|
||||
await ctx.model.transform((editor) => {
|
||||
editor
|
||||
.list()
|
||||
// Number() and String(): the shipped types brand these money and id
|
||||
// fields (Money.USDPerMillionTokens, Model.ID), so `>` and `remove`
|
||||
// do not typecheck against the raw values. The docs show the unbranded
|
||||
// form; widen here.
|
||||
.filter((model) => model.cost.some((tier) => Number(tier.output) > 20))
|
||||
.forEach((model) => {
|
||||
editor.remove(String(model.providerID), String(model.id))
|
||||
})
|
||||
})
|
||||
|
||||
// ------------------------------------------------------------- a tool
|
||||
// Registration can be disposed to remove just this transform.
|
||||
const registration = await ctx.tool.transform((editor) => {
|
||||
editor.namespace({ name: "example", description: "Example tools" })
|
||||
|
||||
editor.add({
|
||||
name: "greeting",
|
||||
description: "Create a greeting",
|
||||
// Raw JSON Schema in the Promise flavor, so `input` arrives as `unknown`
|
||||
// in TypeScript. Narrow it once at the top of execute.
|
||||
input: {
|
||||
type: "object",
|
||||
properties: { name: { type: "string" } },
|
||||
required: ["name"],
|
||||
additionalProperties: false,
|
||||
},
|
||||
options: { namespace: "example", codemode: true },
|
||||
execute: async (rawInput, context) => {
|
||||
const { name } = rawInput as { name: string }
|
||||
// Pass the signal so stopping the session stops the work.
|
||||
await context.progress({ status: "greeting" })
|
||||
return { content: `Hello ${name}!` }
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
// --------------------------------------------------------------- a hook
|
||||
// "context" runs for the agent loop only. Register each kind you need.
|
||||
const hook = await ctx.session.hook("context", (event) => {
|
||||
event.system.push({ type: "text", text: "Keep the review focused on correctness." })
|
||||
delete event.tools.write
|
||||
event.options.temperature = 0.2
|
||||
})
|
||||
|
||||
// ------------------------------------------------------------- provider
|
||||
// Optional: contribute a provider and its models.
|
||||
if (strict) {
|
||||
const providerID = Provider.ID.make("acme")
|
||||
const models = [{ ...Model.Info.default(providerID, Model.ID.make("reasoner")), name: "Acme Reasoner" }]
|
||||
await ctx.provider.transform((editor) => {
|
||||
editor.add({
|
||||
info: {
|
||||
...Provider.Info.empty(providerID),
|
||||
name: "Acme",
|
||||
activation: "enabled",
|
||||
package: "@opencode/ai/providers/openai-compatible",
|
||||
settings: { baseURL: "http://127.0.0.1:8000/v1" },
|
||||
},
|
||||
models,
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
// --------------------------------------------------------------- events
|
||||
// Must abort during cleanup.
|
||||
const controller = new AbortController()
|
||||
void (async () => {
|
||||
for await (const event of ctx.event.subscribe({ signal: controller.signal })) {
|
||||
console.log(event.type)
|
||||
}
|
||||
})()
|
||||
|
||||
// ------------------------------------------------- refresh from a source
|
||||
// Captured inputs are NOT watched. Load first, then reload() on change.
|
||||
// const source = { models: await loadModels() }
|
||||
// const timer = setInterval(() => {
|
||||
// void (async () => {
|
||||
// source.models = await loadModels()
|
||||
// await ctx.model.reload()
|
||||
// })().catch(console.error)
|
||||
// }, 60_000)
|
||||
|
||||
// Return a cleanup function for anything you own. Returning nothing is fine.
|
||||
return () => {
|
||||
controller.abort()
|
||||
void registration.dispose()
|
||||
void hook.dispose()
|
||||
// clearInterval(timer)
|
||||
}
|
||||
},
|
||||
})
|
||||
Reference in New Issue
Block a user