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 };
|
|
};
|