From 73c0db77506b55545b6584f8aadba0f069b89c18 Mon Sep 17 00:00:00 2001 From: YeonGyu-Kim Date: Tue, 23 Dec 2025 02:12:17 +0900 Subject: [PATCH] feat: remove redundant skill tool - OpenCode handles natively MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit OpenCode has native skill support that automatically scans .claude/skills/ and injects available_skills into system prompt. The agent reads SKILL.md files directly via the Read tool, making our separate skill tool a duplicate. The claude-code-skill-loader feature (which converts skills to slash commands) is intentionally kept - only the redundant skill tool is removed. 🤖 GENERATED WITH ASSISTANCE OF [OhMyOpenCode](https://github.com/code-yeongyu/oh-my-opencode) --- src/tools/index.ts | 2 - src/tools/skill/index.ts | 2 - src/tools/skill/tools.ts | 304 --------------------------------------- src/tools/skill/types.ts | 47 ------ 4 files changed, 355 deletions(-) delete mode 100644 src/tools/skill/index.ts delete mode 100644 src/tools/skill/tools.ts delete mode 100644 src/tools/skill/types.ts diff --git a/src/tools/index.ts b/src/tools/index.ts index b61a006..ccfda72 100644 --- a/src/tools/index.ts +++ b/src/tools/index.ts @@ -20,7 +20,6 @@ import { import { grep } from "./grep" import { glob } from "./glob" import { slashcommand } from "./slashcommand" -import { skill } from "./skill" export { interactive_bash, startBackgroundCheck as startTmuxCheck } from "./interactive-bash" export { getTmuxPath } from "./interactive-bash/utils" @@ -64,5 +63,4 @@ export const builtinTools = { grep, glob, slashcommand, - skill, } diff --git a/src/tools/skill/index.ts b/src/tools/skill/index.ts deleted file mode 100644 index ef82557..0000000 --- a/src/tools/skill/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export * from "./types" -export { skill } from "./tools" diff --git a/src/tools/skill/tools.ts b/src/tools/skill/tools.ts deleted file mode 100644 index 452b0b5..0000000 --- a/src/tools/skill/tools.ts +++ /dev/null @@ -1,304 +0,0 @@ -import { tool } from "@opencode-ai/plugin" -import { existsSync, readdirSync, readFileSync } from "fs" -import { homedir } from "os" -import { join, basename } from "path" -import { z } from "zod/v4" -import { parseFrontmatter, resolveCommandsInText } from "../../shared" -import { resolveSymlink } from "../../shared/file-utils" -import { SkillFrontmatterSchema } from "./types" -import type { SkillScope, SkillMetadata, SkillInfo, LoadedSkill, SkillFrontmatter } from "./types" - -function parseSkillFrontmatter(data: Record): SkillFrontmatter { - return { - name: typeof data.name === "string" ? data.name : "", - description: typeof data.description === "string" ? data.description : "", - license: typeof data.license === "string" ? data.license : undefined, - "allowed-tools": Array.isArray(data["allowed-tools"]) ? data["allowed-tools"] : undefined, - metadata: - typeof data.metadata === "object" && data.metadata !== null - ? (data.metadata as Record) - : undefined, - } -} - -function discoverSkillsFromDir( - skillsDir: string, - scope: SkillScope -): Array<{ name: string; description: string; scope: SkillScope }> { - if (!existsSync(skillsDir)) { - return [] - } - - const entries = readdirSync(skillsDir, { withFileTypes: true }) - const skills: Array<{ name: string; description: string; scope: SkillScope }> = [] - - for (const entry of entries) { - if (entry.name.startsWith(".")) continue - - const skillPath = join(skillsDir, entry.name) - - if (entry.isDirectory() || entry.isSymbolicLink()) { - const resolvedPath = resolveSymlink(skillPath) - - const skillMdPath = join(resolvedPath, "SKILL.md") - if (!existsSync(skillMdPath)) continue - - try { - const content = readFileSync(skillMdPath, "utf-8") - const { data } = parseFrontmatter(content) - - skills.push({ - name: data.name || entry.name, - description: data.description || "", - scope, - }) - } catch { - continue - } - } - } - - return skills -} - -function discoverSkillsSync(): Array<{ name: string; description: string; scope: SkillScope }> { - const userSkillsDir = join(homedir(), ".claude", "skills") - const projectSkillsDir = join(process.cwd(), ".claude", "skills") - - const userSkills = discoverSkillsFromDir(userSkillsDir, "user") - const projectSkills = discoverSkillsFromDir(projectSkillsDir, "project") - - return [...projectSkills, ...userSkills] -} - -const availableSkills = discoverSkillsSync() -const skillListForDescription = availableSkills - .map((s) => `- ${s.name}: ${s.description} (${s.scope})`) - .join("\n") - -async function parseSkillMd(skillPath: string): Promise { - const resolvedPath = resolveSymlink(skillPath) - const skillMdPath = join(resolvedPath, "SKILL.md") - - if (!existsSync(skillMdPath)) { - return null - } - - try { - let content = readFileSync(skillMdPath, "utf-8") - content = await resolveCommandsInText(content) - const { data, body } = parseFrontmatter(content) - - const frontmatter = parseSkillFrontmatter(data) - - const metadata: SkillMetadata = { - name: frontmatter.name || basename(skillPath), - description: frontmatter.description, - license: frontmatter.license, - allowedTools: frontmatter["allowed-tools"], - metadata: frontmatter.metadata, - } - - const referencesDir = join(resolvedPath, "references") - const scriptsDir = join(resolvedPath, "scripts") - const assetsDir = join(resolvedPath, "assets") - - const references = existsSync(referencesDir) - ? readdirSync(referencesDir).filter((f) => !f.startsWith(".")) - : [] - - const scripts = existsSync(scriptsDir) - ? readdirSync(scriptsDir).filter((f) => !f.startsWith(".") && !f.startsWith("__")) - : [] - - const assets = existsSync(assetsDir) - ? readdirSync(assetsDir).filter((f) => !f.startsWith(".")) - : [] - - return { - name: metadata.name, - path: resolvedPath, - basePath: resolvedPath, - metadata, - content: body, - references, - scripts, - assets, - } - } catch { - return null - } -} - -async function discoverSkillsFromDirAsync(skillsDir: string): Promise { - if (!existsSync(skillsDir)) { - return [] - } - - const entries = readdirSync(skillsDir, { withFileTypes: true }) - const skills: SkillInfo[] = [] - - for (const entry of entries) { - if (entry.name.startsWith(".")) continue - - const skillPath = join(skillsDir, entry.name) - - if (entry.isDirectory() || entry.isSymbolicLink()) { - const skillInfo = await parseSkillMd(skillPath) - if (skillInfo) { - skills.push(skillInfo) - } - } - } - - return skills -} - -async function discoverSkills(): Promise { - const userSkillsDir = join(homedir(), ".claude", "skills") - const projectSkillsDir = join(process.cwd(), ".claude", "skills") - - const userSkills = await discoverSkillsFromDirAsync(userSkillsDir) - const projectSkills = await discoverSkillsFromDirAsync(projectSkillsDir) - - return [...projectSkills, ...userSkills] -} - -function findMatchingSkills(skills: SkillInfo[], query: string): SkillInfo[] { - const queryLower = query.toLowerCase() - const queryTerms = queryLower.split(/\s+/).filter(Boolean) - - return skills - .map((skill) => { - let score = 0 - const nameLower = skill.metadata.name.toLowerCase() - const descLower = skill.metadata.description.toLowerCase() - - if (nameLower === queryLower) score += 100 - if (nameLower.includes(queryLower)) score += 50 - - for (const term of queryTerms) { - if (nameLower.includes(term)) score += 20 - if (descLower.includes(term)) score += 10 - } - - return { skill, score } - }) - .filter(({ score }) => score > 0) - .sort((a, b) => b.score - a.score) - .map(({ skill }) => skill) -} - -async function loadSkillWithReferences( - skill: SkillInfo, - includeRefs: boolean -): Promise { - const referencesLoaded: Array<{ path: string; content: string }> = [] - - if (includeRefs && skill.references.length > 0) { - for (const ref of skill.references) { - const refPath = join(skill.path, "references", ref) - try { - let content = readFileSync(refPath, "utf-8") - content = await resolveCommandsInText(content) - referencesLoaded.push({ path: ref, content }) - } catch { - // Skip unreadable references - } - } - } - - return { - name: skill.name, - metadata: skill.metadata, - basePath: skill.basePath, - body: skill.content, - referencesLoaded, - } -} - -function formatSkillList(skills: SkillInfo[]): string { - if (skills.length === 0) { - return "No skills found in ~/.claude/skills/" - } - - const lines = ["# Available Skills\n"] - - for (const skill of skills) { - lines.push(`- **${skill.metadata.name}**: ${skill.metadata.description || "(no description)"}`) - } - - lines.push(`\n**Total**: ${skills.length} skills`) - return lines.join("\n") -} - -function formatLoadedSkills(loadedSkills: LoadedSkill[]): string { - if (loadedSkills.length === 0) { - return "No skills loaded." - } - - const skill = loadedSkills[0] - const sections: string[] = [] - - sections.push(`Base directory for this skill: ${skill.basePath}/`) - sections.push("") - sections.push(skill.body.trim()) - - if (skill.referencesLoaded.length > 0) { - sections.push("\n---\n### Loaded References\n") - for (const ref of skill.referencesLoaded) { - sections.push(`#### ${ref.path}\n`) - sections.push("```") - sections.push(ref.content.trim()) - sections.push("```\n") - } - } - - sections.push(`\n---\n**Launched skill**: ${skill.metadata.name}`) - - return sections.join("\n") -} - -export const skill = tool({ - description: `Execute a skill within the main conversation. - -When you invoke a skill, the skill's prompt will expand and provide detailed instructions on how to complete the task. - -Available Skills: -${skillListForDescription}`, - - args: { - skill: tool.schema - .string() - .describe( - "The skill name or search query to find and load. Can be exact skill name (e.g., 'python-programmer') or keywords (e.g., 'python', 'plan')." - ), - }, - - async execute(args) { - const skills = await discoverSkills() - - if (!args.skill) { - return formatSkillList(skills) + "\n\nProvide a skill name to load." - } - - const matchingSkills = findMatchingSkills(skills, args.skill) - - if (matchingSkills.length === 0) { - return ( - `No skills found matching "${args.skill}".\n\n` + - formatSkillList(skills) + - "\n\nTry a different skill name." - ) - } - - const loadedSkills: LoadedSkill[] = [] - - for (const skillInfo of matchingSkills.slice(0, 3)) { - const loaded = await loadSkillWithReferences(skillInfo, true) - loadedSkills.push(loaded) - } - - return formatLoadedSkills(loadedSkills) - }, -}) diff --git a/src/tools/skill/types.ts b/src/tools/skill/types.ts deleted file mode 100644 index fcf739f..0000000 --- a/src/tools/skill/types.ts +++ /dev/null @@ -1,47 +0,0 @@ -import { z } from "zod/v4" - -export type SkillScope = "user" | "project" - -/** - * Zod schema for skill frontmatter validation - * Following Anthropic Agent Skills Specification v1.0 - */ -export const SkillFrontmatterSchema = z.object({ - name: z - .string() - .regex(/^[a-z0-9-]+$/, "Name must be lowercase alphanumeric with hyphens only") - .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(), z.string()).optional(), -}) - -export type SkillFrontmatter = z.infer - -export interface SkillMetadata { - name: string - description: string - license?: string - allowedTools?: string[] - metadata?: Record -} - -export interface SkillInfo { - name: string - path: string - basePath: string - metadata: SkillMetadata - content: string - references: string[] - scripts: string[] - assets: string[] -} - -export interface LoadedSkill { - name: string - metadata: SkillMetadata - basePath: string - body: string - referencesLoaded: Array<{ path: string; content: string }> -}