76 lines
3.0 KiB
TypeScript
76 lines
3.0 KiB
TypeScript
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<string>(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 }
|