import { existsSync, readFileSync } from "node:fs" import path from "node:path" import { Plugin, Skill } from "@opencode/plugin" import { blockedPattern, guardrailError, isShellTool, resolveOptions } from "./guardrails.ts" import { parseFrontmatter } from "./frontmatter.ts" import { loadManifest, packageRoot, packageVersion, skillFile } from "./manifest.ts" const AGENT_IDS = ["pocock", "pocock-worker", "pocock-reviewer"] as const function installedAgentVersion(projectDir: string): string | undefined { const file = path.join(projectDir, ".opencode", "agents", ".pocock-version") if (!existsSync(file)) return undefined return readFileSync(file, "utf8").trim() } export default Plugin.define({ id: "haylan.pocock", async setup(ctx) { // Fails loudly on bad options or a broken install instead of silently shipping no skills. const options = resolveOptions(ctx.options) const entries = loadManifest() await ctx.skill.transform((editor) => { for (const entry of entries) { const file = skillFile(entry) // File-based skill loading strips frontmatter from content (core skill.ts uses // markdown.content); match that so injected skills read identically. const content = parseFrontmatter(readFileSync(file, "utf8")).body.trim() // Brand types (Skill.ID/Name/AbsolutePath) are nominal-only; runtime values are plain // strings, asserted by test/plugin.test.ts and the serve-based smoke run. editor.add({ id: entry.id, name: entry.name, description: entry.description, autoinvoke: entry.autoinvoke, path: file, content, } as unknown as Skill.Info) } }) if (options.guardrails) { await ctx.tool.hook("execute.before", (event) => { if (!isShellTool(event.tool)) return const command = (event.input as { command?: unknown } | undefined)?.command if (typeof command !== "string") return const pattern = blockedPattern(command) if (pattern) throw new Error(`pocock guardrails: ${guardrailError(command, pattern)}`) }) } // Agents cannot be registered by plugins in v2 (AgentEditor has no add), so guide the user // to the one required copy step instead of failing or writing into their repo. let warned = false await ctx.agent.transform((editor) => { if (warned) return warned = true const ids = new Set(editor.list().map((agent) => String(agent.id))) const have = installedAgentVersion(ctx.location.directory) const want = packageVersion() const initCmd = "npx @haylan/opencode-pocock init" if (!AGENT_IDS.some((id) => ids.has(id))) { console.warn( `pocock: skills loaded, but the pocock agents are not installed — run: ${initCmd}`, ) } else if (have !== want) { console.warn( `pocock: installed agents are ${have ? `from v${have}` : "unversioned"}, package is v${want} — refresh: ${initCmd}`, ) } }) }, }) export { packageRoot }