import { tool } from "@opencode-ai/plugin"; import { SkillStore } from "../../src/skills/store.js"; import { getRuntimeSessionContext, type RuntimeSessionContext, } from "../../src/runtime/sessionContext.js"; type ToolContextReader = { read(sessionId: string): RuntimeSessionContext | null; }; const runtimeContextReader: ToolContextReader = { read: getRuntimeSessionContext, }; export const createSkillManagerTool = ( skillStore = new SkillStore(), toolContextStore: ToolContextReader = runtimeContextReader, initializePromise: Promise = Promise.resolve(), ) => tool({ description: "维护已验证、可复用、非敏感的 workflow 或方法模式。支持 list、write_skill、remove_skill、append_pattern、remove_pattern、write_reference、remove_reference、write_script、remove_script。", args: { action: tool.schema .enum([ "list", "write_skill", "remove_skill", "append_pattern", "remove_pattern", "write_reference", "remove_reference", "write_script", "remove_script", ]) .describe("Skill maintenance operation."), reason: tool.schema .string() .describe( "Why this skill maintenance action is justified for future reuse.", ), skill_path: tool.schema .string() .describe( "Target skill directory path relative to .opencode/skills. Use 'workflow' for the workflow index, or '__root__' for the root skills index.", ), pattern: tool.schema .string() .optional() .describe("Pattern text used by append_pattern."), target_id: tool.schema .string() .optional() .describe("Stable learned pattern id used by remove_pattern."), file_path: tool.schema .string() .optional() .describe( "Asset file path. For references use references/*.md; for scripts use scripts/*.py.", ), content: tool.schema .string() .optional() .describe( "Content used by write_skill, write_reference, or write_script.", ), }, async execute(args, context) { await initializePromise; const sessionContext = toolContextStore.read(context.sessionID); if (!sessionContext) { throw new Error(`session context not found for ${context.sessionID}`); } if ( sessionContext.allowLearningWrite === false && args.action !== "list" ) { return JSON.stringify({ ok: true, kind: "skill", decision: "rejected", detail: "skill writes are disabled for this session", }); } if (args.action === "list") { const result = await skillStore.list(args.skill_path); if (!result) { return JSON.stringify({ ok: true, kind: "skill", decision: "rejected", detail: "invalid skill_path; expected a relative path under .opencode/skills", }); } return JSON.stringify({ ok: true, kind: "skill", decision: "accepted", detail: "skill listed", ...result, }); } const result = args.action === "write_skill" ? await skillStore.writeSkill(args.skill_path, args.content ?? "") : args.action === "remove_skill" ? await skillStore.removeSkill(args.skill_path) : args.action === "append_pattern" ? await skillStore.appendPattern( args.skill_path, args.pattern ?? "", ) : args.action === "remove_pattern" ? await skillStore.removePattern( args.skill_path, args.target_id ?? "", ) : args.action === "write_reference" ? await skillStore.writeReference( args.skill_path, args.file_path ?? "", args.content ?? "", ) : args.action === "remove_reference" ? await skillStore.removeReference( args.skill_path, args.file_path ?? "", ) : args.action === "write_script" ? await skillStore.writeScript( args.skill_path, args.file_path ?? "", args.content ?? "", ) : await skillStore.removeScript( args.skill_path, args.file_path ?? "", ); return JSON.stringify({ ok: true, kind: "skill", decision: result.changed ? "accepted" : "rejected", detail: result.detail, target: result.target, }); }, }); export default createSkillManagerTool();