/** * OpenCode Skills Plugin (Local Version) * * Implements Anthropic's Agent Skills Specification (v1.0) for OpenCode. * * Modified to: * - Only scan ~/.config/opencode/skills/ directory * - Provide minimal logging (one-line summary) * * Original: https://github.com/malhashemi/opencode-skills */ import { tool } from "@opencode-ai/plugin"; import matter from "gray-matter"; import { Glob } from "bun"; import { join, dirname, basename } from "path"; import { z } from "zod"; import os from "os"; const SkillFrontmatterSchema = z.object({ name: z.string() .regex(/^[a-z0-9-]+$/, "Name must be lowercase alphanumeric with hyphens") .min(1, "Name cannot be empty"), description: z.string() .min(20, "Description must be at least 20 characters for discoverability"), license: z.string().optional(), "allowed-tools": z.array(z.string()).optional(), metadata: z.record(z.string()).optional() }); async function parseSkill(skillPath, baseDir) { try { const content = await Bun.file(skillPath).text(); const { data, content: markdown } = matter(content); let frontmatter; try { frontmatter = SkillFrontmatterSchema.parse(data); } catch (error) { return null; } const skillDir = basename(dirname(skillPath)); if (frontmatter.name !== skillDir) { return null; } return { name: frontmatter.name, fullPath: dirname(skillPath), description: frontmatter.description, allowedTools: frontmatter["allowed-tools"], metadata: frontmatter.metadata, license: frontmatter.license, content: markdown.trim(), path: skillPath }; } catch (error) { return null; } } async function discoverSkills(basePath) { const skills = []; try { const glob = new Glob("**/SKILL.md"); for await (const match of glob.scan({ cwd: basePath, absolute: true })) { const skill = await parseSkill(match, basePath); if (skill) { skills.push(skill); } } } catch (error) { // Directory doesn't exist, return empty array } return skills; } export const SkillsPlugin = async (ctx) => { const xdgConfigHome = process.env.XDG_CONFIG_HOME; const configSkillsPath = xdgConfigHome ? join(xdgConfigHome, "opencode/skills") : join(os.homedir(), ".config/opencode/skills"); const skills = await discoverSkills(configSkillsPath); if (skills.length > 0) { console.log(`Skills loaded: ${skills.map(s => s.name).join(", ")}`); } // Build skill catalog for tool description const skillCatalog = skills.length > 0 ? skills.map(s => `- **${s.name}**: ${s.description}`).join('\n') : 'No skills available.'; // Create single learn_skill tool const tools = { learn_skill: tool({ description: `Load and execute a skill on demand. Skills provide specialized knowledge and workflows for specific tasks. Available skills: ${skillCatalog} Use this tool when you need guidance on these specialized workflows.`, args: { skill_name: tool.schema.string() .describe("The name of the skill to learn (e.g., 'do-job', 'reflect', 'go-pr-review', 'create-skill')") }, async execute(args, toolCtx) { const skill = skills.find(s => s.name === args.skill_name); if (!skill) { const availableSkills = skills.map(s => s.name).join(', '); return `❌ Error: Skill '${args.skill_name}' not found. Available skills: ${availableSkills} Use one of the available skill names exactly as shown above.`; } return `# ⚠️ SKILL EXECUTION INSTRUCTIONS ⚠️ **SKILL NAME:** ${skill.name} **SKILL DIRECTORY:** ${skill.fullPath}/ ## EXECUTION WORKFLOW: **STEP 1: PLAN THE WORK** Before executing this skill, use the \`todowrite\` tool to create a todo list of the main tasks described in the skill content below. - Parse the skill instructions carefully - Identify the key tasks and steps required - Create todos with status "pending" and appropriate priority levels - This helps track progress and ensures nothing is missed **STEP 2: EXECUTE THE SKILL** Follow the skill instructions below, marking todos as "in_progress" when starting a task and "completed" when done. Use \`todowrite\` to update task statuses as you work through them. ## PATH RESOLUTION RULES (READ CAREFULLY): All file paths mentioned below are relative to the SKILL DIRECTORY shown above. **Examples:** - If the skill mentions \`scripts/init_skill.py\`, the full path is: \`${skill.fullPath}/scripts/init_skill.py\` - If the skill mentions \`references/docs.md\`, the full path is: \`${skill.fullPath}/references/docs.md\` - If the skill mentions \`assets/template.html\`, the full path is: \`${skill.fullPath}/assets/template.html\` **IMPORTANT:** Always prepend \`${skill.fullPath}/\` to any relative path mentioned in the skill content below. --- # SKILL CONTENT: ${skill.content} --- **Remember:** 1. All relative paths in the skill content above are relative to: \`${skill.fullPath}/\` 2. Update your todo list as you progress through the skill tasks `; } }) }; return { tool: tools }; }; export const SkillLogger = async () => { return { "tool.execute.before": async (input, output) => { if (input.tool === "learn_skill") { console.log(`Learning skill ${output}`) } }, } }