// 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) } }, })