feat: implement skill tool for loading and discovering skills
- Add skill tool types: SkillArgs, SkillInfo, SkillLoadOptions interfaces - Implement createSkillTool() factory function with configurable discovery options - Add parseSkillInfo() helper to convert LoadedSkill to user-facing SkillInfo format - Add formatSkillsXml() helper to generate available skills XML for tool description - Support opencodeOnly option to filter Claude Code paths from discovery - Tool loads and parses skill frontmatter, returns skill content with base directory - Export skill tool singleton instance for default usage 🤖 GENERATED WITH ASSISTANCE OF [OhMyOpenCode](https://github.com/code-yeongyu/oh-my-opencode)
This commit is contained in:
8
src/tools/skill/constants.ts
Normal file
8
src/tools/skill/constants.ts
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
export const TOOL_NAME = "skill" as const
|
||||||
|
|
||||||
|
export const TOOL_DESCRIPTION_NO_SKILLS = "Load a skill to get detailed instructions for a specific task. No skills are currently available."
|
||||||
|
|
||||||
|
export const TOOL_DESCRIPTION_PREFIX = `Load a skill to get detailed instructions for a specific task.
|
||||||
|
|
||||||
|
Skills provide specialized knowledge and step-by-step guidance.
|
||||||
|
Use this when a task matches an available skill's description.`
|
||||||
3
src/tools/skill/index.ts
Normal file
3
src/tools/skill/index.ts
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
export * from "./constants"
|
||||||
|
export * from "./types"
|
||||||
|
export { skill, createSkillTool } from "./tools"
|
||||||
79
src/tools/skill/tools.ts
Normal file
79
src/tools/skill/tools.ts
Normal file
@@ -0,0 +1,79 @@
|
|||||||
|
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, getSkillByName, 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.name}</name>`,
|
||||||
|
` <description>${skill.description}</description>`,
|
||||||
|
]
|
||||||
|
if (skill.compatibility) {
|
||||||
|
lines.push(` <compatibility>${skill.compatibility}</compatibility>`)
|
||||||
|
}
|
||||||
|
lines.push(" </skill>")
|
||||||
|
return lines.join("\n")
|
||||||
|
}).join("\n")
|
||||||
|
|
||||||
|
return `\n\n<available_skills>\n${skillsXml}\n</available_skills>`
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createSkillTool(options: SkillLoadOptions = {}): ToolDefinition {
|
||||||
|
const 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 = getSkillByName(args.name, { includeClaudeCodePaths: !options.opencodeOnly })
|
||||||
|
|
||||||
|
if (!skill) {
|
||||||
|
const available = skills.map(s => s.name).join(", ")
|
||||||
|
throw new Error(`Skill "${args.name}" not found. Available skills: ${available || "none"}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
const content = readFileSync(skill.path, "utf-8")
|
||||||
|
const { body } = parseFrontmatter(content)
|
||||||
|
const dir = dirname(skill.path)
|
||||||
|
|
||||||
|
const output = [
|
||||||
|
`## Skill: ${skill.name}`,
|
||||||
|
"",
|
||||||
|
`**Base directory**: ${dir}`,
|
||||||
|
"",
|
||||||
|
body.trim(),
|
||||||
|
].join("\n")
|
||||||
|
|
||||||
|
return output
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export const skill = createSkillTool()
|
||||||
19
src/tools/skill/types.ts
Normal file
19
src/tools/skill/types.ts
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
export interface SkillArgs {
|
||||||
|
name: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SkillInfo {
|
||||||
|
name: string
|
||||||
|
description: string
|
||||||
|
location: string
|
||||||
|
scope: "opencode-project" | "project" | "opencode" | "user"
|
||||||
|
license?: string
|
||||||
|
compatibility?: string
|
||||||
|
metadata?: Record<string, string>
|
||||||
|
allowedTools?: string[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SkillLoadOptions {
|
||||||
|
/** When true, only load from OpenCode paths (.opencode/skill/, ~/.config/opencode/skill/) */
|
||||||
|
opencodeOnly?: boolean
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user