import { dirname } from "node:path"
import { readFileSync } from "node:fs"
import { tool, type ToolDefinition } from "@opencode-ai/plugin"
import { TOOL_DESCRIPTION_NO_SKILLS, TOOL_DESCRIPTION_PREFIX } from "./constants"
import type { SkillArgs, SkillInfo, SkillLoadOptions } from "./types"
import { discoverSkills, type LoadedSkill } from "../../features/opencode-skill-loader"
import { parseFrontmatter } from "../../shared/frontmatter"
function loadedSkillToInfo(skill: LoadedSkill): SkillInfo {
return {
name: skill.name,
description: skill.definition.description || "",
location: skill.path,
scope: skill.scope,
license: skill.license,
compatibility: skill.compatibility,
metadata: skill.metadata,
allowedTools: skill.allowedTools,
}
}
function formatSkillsXml(skills: SkillInfo[]): string {
if (skills.length === 0) return ""
const skillsXml = skills.map(skill => {
const lines = [
" ",
` ${skill.name}`,
` ${skill.description}`,
]
if (skill.compatibility) {
lines.push(` ${skill.compatibility}`)
}
lines.push(" ")
return lines.join("\n")
}).join("\n")
return `\n\n\n${skillsXml}\n`
}
function extractSkillBody(skill: LoadedSkill): string {
if (skill.path) {
const content = readFileSync(skill.path, "utf-8")
const { body } = parseFrontmatter(content)
return body.trim()
}
const templateMatch = skill.definition.template?.match(/([\s\S]*?)<\/skill-instruction>/)
return templateMatch ? templateMatch[1].trim() : skill.definition.template || ""
}
export function createSkillTool(options: SkillLoadOptions = {}): ToolDefinition {
const skills = options.skills ?? discoverSkills({ includeClaudeCodePaths: !options.opencodeOnly })
const skillInfos = skills.map(loadedSkillToInfo)
const description = skillInfos.length === 0
? TOOL_DESCRIPTION_NO_SKILLS
: TOOL_DESCRIPTION_PREFIX + formatSkillsXml(skillInfos)
return tool({
description,
args: {
name: tool.schema.string().describe("The skill identifier from available_skills (e.g., 'code-review')"),
},
async execute(args: SkillArgs) {
const skill = options.skills
? skills.find(s => s.name === args.name)
: skills.find(s => s.name === args.name)
if (!skill) {
const available = skills.map(s => s.name).join(", ")
throw new Error(`Skill "${args.name}" not found. Available skills: ${available || "none"}`)
}
const body = extractSkillBody(skill)
const dir = skill.path ? dirname(skill.path) : skill.resolvedPath || process.cwd()
return [
`## Skill: ${skill.name}`,
"",
`**Base directory**: ${dir}`,
"",
body,
].join("\n")
},
})
}
export const skill = createSkillTool()