161 lines
		
	
	
		
			4.9 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
			
		
		
	
	
			161 lines
		
	
	
		
			4.9 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
/**
 | 
						|
 * 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, relative, sep } 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()
 | 
						|
});
 | 
						|
 | 
						|
function generateToolName(skillPath, baseDir) {
 | 
						|
    const rel = relative(baseDir, skillPath);
 | 
						|
    const dirPath = dirname(rel);
 | 
						|
    const components = dirPath.split(sep).filter(c => c !== ".");
 | 
						|
    return "skills_" + components.join("_").replace(/-/g, "_");
 | 
						|
}
 | 
						|
 | 
						|
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;
 | 
						|
        }
 | 
						|
        
 | 
						|
        const toolName = generateToolName(skillPath, baseDir);
 | 
						|
        
 | 
						|
        return {
 | 
						|
            name: frontmatter.name,
 | 
						|
            fullPath: dirname(skillPath),
 | 
						|
            toolName,
 | 
						|
            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(", ")}`);
 | 
						|
    }
 | 
						|
    
 | 
						|
    const tools = {};
 | 
						|
    for (const skill of skills) {
 | 
						|
        tools[skill.toolName] = tool({
 | 
						|
            description: skill.description,
 | 
						|
            args: {},
 | 
						|
            async execute(args, toolCtx) {
 | 
						|
                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 };
 | 
						|
};
 |