From c401113537dd66dd62f63fb86ad6f0cb50000dfb Mon Sep 17 00:00:00 2001 From: YeonGyu-Kim Date: Tue, 30 Dec 2025 15:15:43 +0900 Subject: [PATCH] feat(skill): add builtin skill infrastructure and improve tool descriptions (#340) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat(skill): add builtin skill types and schemas with priority-based merging support - Add BuiltinSkill interface for programmatic skill definitions - Create builtin-skills module with createBuiltinSkills factory function - Add SkillScope expansion to include 'builtin' and 'config' scopes - Create SkillsConfig and SkillDefinition Zod schemas for config validation - Add merger.ts utility with mergeSkills function for priority-based skill merging - Update skill and command types to support optional paths for builtin/config skills - Priority order: builtin < config < user < opencode < project < opencode-project 🤖 GENERATED WITH ASSISTANCE OF [OhMyOpenCode](https://github.com/code-yeongyu/oh-my-opencode) * feat(skill): integrate programmatic skill discovery and merged skill support - Add discovery functions for Claude and OpenCode skill directories - Add discoverUserClaudeSkills, discoverProjectClaudeSkills functions - Add discoverOpencodeGlobalSkills, discoverOpencodeProjectSkills functions - Update createSkillTool to support pre-merged skills via options - Add extractSkillBody utility to handle both file and programmatic skills - Integrate mergeSkills in plugin initialization to apply priority-based merging - Support optional path/resolvedPath for builtin and config-sourced skills 🤖 GENERATED WITH ASSISTANCE OF [OhMyOpenCode](https://github.com/code-yeongyu/oh-my-opencode) * chore(slashcommand): support optional path for builtin and config command scopes - Update CommandInfo type to make path and content optional properties - Prepare command tool for builtin and config sourced commands - Maintain backward compatibility with file-based command loading 🤖 GENERATED WITH ASSISTANCE OF [OhMyOpenCode](https://github.com/code-yeongyu/oh-my-opencode) * docs(tools): improve tool descriptions for interactive-bash and slashcommand - Added use case clarification to interactive-bash tool description (server processes, long-running tasks, background jobs, interactive CLI tools) - Simplified slashcommand description to emphasize 'loading' skills concept and removed verbose documentation 🤖 Generated with assistance of [OhMyOpenCode](https://github.com/code-yeongyu/oh-my-opencode) * refactor(skill-loader): simplify redundant condition in skill merging logic Remove redundant 'else if (loaded)' condition that was always true since we're already inside the 'if (loaded)' block. Simplify to 'else' for clarity. Addresses code review feedback on PR #340 for the skill infrastructure feature. 🤖 Generated with assistance of OhMyOpenCode (https://github.com/code-yeongyu/oh-my-opencode) --- src/config/schema.ts | 41 +++ src/features/builtin-skills/index.ts | 2 + src/features/builtin-skills/skills.ts | 5 + src/features/builtin-skills/types.ts | 13 + src/features/opencode-skill-loader/index.ts | 1 + src/features/opencode-skill-loader/loader.ts | 20 ++ src/features/opencode-skill-loader/merger.ts | 266 +++++++++++++++++++ src/features/opencode-skill-loader/types.ts | 6 +- src/index.ts | 20 +- src/tools/interactive-bash/constants.ts | 2 + src/tools/skill/tools.ts | 30 ++- src/tools/skill/types.ts | 8 +- src/tools/slashcommand/tools.ts | 42 +-- src/tools/slashcommand/types.ts | 6 +- 14 files changed, 407 insertions(+), 55 deletions(-) create mode 100644 src/features/builtin-skills/index.ts create mode 100644 src/features/builtin-skills/skills.ts create mode 100644 src/features/builtin-skills/types.ts create mode 100644 src/features/opencode-skill-loader/merger.ts diff --git a/src/config/schema.ts b/src/config/schema.ts index aea9af7..05f1936 100644 --- a/src/config/schema.ts +++ b/src/config/schema.ts @@ -175,6 +175,44 @@ export const ExperimentalConfigSchema = z.object({ dcp_for_compaction: z.boolean().optional(), }) +export const SkillSourceSchema = z.union([ + z.string(), + z.object({ + path: z.string(), + recursive: z.boolean().optional(), + glob: z.string().optional(), + }), +]) + +export const SkillDefinitionSchema = z.object({ + description: z.string().optional(), + template: z.string().optional(), + from: z.string().optional(), + model: z.string().optional(), + agent: z.string().optional(), + subtask: z.boolean().optional(), + "argument-hint": z.string().optional(), + license: z.string().optional(), + compatibility: z.string().optional(), + metadata: z.record(z.string(), z.unknown()).optional(), + "allowed-tools": z.array(z.string()).optional(), + disable: z.boolean().optional(), +}) + +export const SkillEntrySchema = z.union([ + z.boolean(), + SkillDefinitionSchema, +]) + +export const SkillsConfigSchema = z.union([ + z.array(z.string()), + z.record(z.string(), SkillEntrySchema).and(z.object({ + sources: z.array(SkillSourceSchema).optional(), + enable: z.array(z.string()).optional(), + disable: z.array(z.string()).optional(), + }).partial()), +]) + export const OhMyOpenCodeConfigSchema = z.object({ $schema: z.string().optional(), disabled_mcps: z.array(McpNameSchema).optional(), @@ -188,6 +226,7 @@ export const OhMyOpenCodeConfigSchema = z.object({ comment_checker: CommentCheckerConfigSchema.optional(), experimental: ExperimentalConfigSchema.optional(), auto_update: z.boolean().optional(), + skills: SkillsConfigSchema.optional(), }) export type OhMyOpenCodeConfig = z.infer @@ -200,5 +239,7 @@ export type SisyphusAgentConfig = z.infer export type CommentCheckerConfig = z.infer export type ExperimentalConfig = z.infer export type DynamicContextPruningConfig = z.infer +export type SkillsConfig = z.infer +export type SkillDefinition = z.infer export { McpNameSchema, type McpName } from "../mcp/types" diff --git a/src/features/builtin-skills/index.ts b/src/features/builtin-skills/index.ts new file mode 100644 index 0000000..7ca1fac --- /dev/null +++ b/src/features/builtin-skills/index.ts @@ -0,0 +1,2 @@ +export * from "./types" +export { createBuiltinSkills } from "./skills" diff --git a/src/features/builtin-skills/skills.ts b/src/features/builtin-skills/skills.ts new file mode 100644 index 0000000..c29d212 --- /dev/null +++ b/src/features/builtin-skills/skills.ts @@ -0,0 +1,5 @@ +import type { BuiltinSkill } from "./types" + +export function createBuiltinSkills(): BuiltinSkill[] { + return [] +} diff --git a/src/features/builtin-skills/types.ts b/src/features/builtin-skills/types.ts new file mode 100644 index 0000000..095e846 --- /dev/null +++ b/src/features/builtin-skills/types.ts @@ -0,0 +1,13 @@ +export interface BuiltinSkill { + name: string + description: string + template: string + license?: string + compatibility?: string + metadata?: Record + allowedTools?: string[] + agent?: string + model?: string + subtask?: boolean + argumentHint?: string +} diff --git a/src/features/opencode-skill-loader/index.ts b/src/features/opencode-skill-loader/index.ts index 644158c..027427a 100644 --- a/src/features/opencode-skill-loader/index.ts +++ b/src/features/opencode-skill-loader/index.ts @@ -1,2 +1,3 @@ export * from "./types" export * from "./loader" +export * from "./merger" diff --git a/src/features/opencode-skill-loader/loader.ts b/src/features/opencode-skill-loader/loader.ts index dd1449a..32ffad4 100644 --- a/src/features/opencode-skill-loader/loader.ts +++ b/src/features/opencode-skill-loader/loader.ts @@ -224,3 +224,23 @@ export function getSkillByName(name: string, options: DiscoverSkillsOptions = {} const skills = discoverSkills(options) return skills.find(s => s.name === name) } + +export function discoverUserClaudeSkills(): LoadedSkill[] { + const userSkillsDir = join(getClaudeConfigDir(), "skills") + return loadSkillsFromDir(userSkillsDir, "user") +} + +export function discoverProjectClaudeSkills(): LoadedSkill[] { + const projectSkillsDir = join(process.cwd(), ".claude", "skills") + return loadSkillsFromDir(projectSkillsDir, "project") +} + +export function discoverOpencodeGlobalSkills(): LoadedSkill[] { + const opencodeSkillsDir = join(homedir(), ".config", "opencode", "skill") + return loadSkillsFromDir(opencodeSkillsDir, "opencode") +} + +export function discoverOpencodeProjectSkills(): LoadedSkill[] { + const opencodeProjectDir = join(process.cwd(), ".opencode", "skill") + return loadSkillsFromDir(opencodeProjectDir, "opencode-project") +} diff --git a/src/features/opencode-skill-loader/merger.ts b/src/features/opencode-skill-loader/merger.ts new file mode 100644 index 0000000..bc166e4 --- /dev/null +++ b/src/features/opencode-skill-loader/merger.ts @@ -0,0 +1,266 @@ +import type { LoadedSkill, SkillScope, SkillMetadata } from "./types" +import type { SkillsConfig, SkillDefinition } from "../../config/schema" +import type { BuiltinSkill } from "../builtin-skills/types" +import type { CommandDefinition } from "../claude-code-command-loader/types" +import { readFileSync, existsSync } from "fs" +import { dirname, resolve, isAbsolute } from "path" +import { homedir } from "os" +import { parseFrontmatter } from "../../shared/frontmatter" +import { sanitizeModelField } from "../../shared/model-sanitizer" +import { deepMerge } from "../../shared/deep-merge" + +const SCOPE_PRIORITY: Record = { + builtin: 1, + config: 2, + user: 3, + opencode: 4, + project: 5, + "opencode-project": 6, +} + +function builtinToLoaded(builtin: BuiltinSkill): LoadedSkill { + const definition: CommandDefinition = { + name: builtin.name, + description: `(builtin - Skill) ${builtin.description}`, + template: builtin.template, + model: builtin.model, + agent: builtin.agent, + subtask: builtin.subtask, + argumentHint: builtin.argumentHint, + } + + return { + name: builtin.name, + definition, + scope: "builtin", + license: builtin.license, + compatibility: builtin.compatibility, + metadata: builtin.metadata as Record | undefined, + allowedTools: builtin.allowedTools, + } +} + +function resolveFilePath(from: string, configDir?: string): string { + let filePath = from + + if (filePath.startsWith("{file:") && filePath.endsWith("}")) { + filePath = filePath.slice(6, -1) + } + + if (filePath.startsWith("~/")) { + return resolve(homedir(), filePath.slice(2)) + } + + if (isAbsolute(filePath)) { + return filePath + } + + const baseDir = configDir || process.cwd() + return resolve(baseDir, filePath) +} + +function loadSkillFromFile(filePath: string): { template: string; metadata: SkillMetadata } | null { + try { + if (!existsSync(filePath)) return null + const content = readFileSync(filePath, "utf-8") + const { data, body } = parseFrontmatter(content) + return { template: body, metadata: data } + } catch { + return null + } +} + +function configEntryToLoaded( + name: string, + entry: SkillDefinition, + configDir?: string +): LoadedSkill | null { + let template = entry.template || "" + let fileMetadata: SkillMetadata = {} + + if (entry.from) { + const filePath = resolveFilePath(entry.from, configDir) + const loaded = loadSkillFromFile(filePath) + if (loaded) { + template = loaded.template + fileMetadata = loaded.metadata + } else { + return null + } + } + + if (!template && !entry.from) { + return null + } + + const description = entry.description || fileMetadata.description || "" + const resolvedPath = entry.from ? dirname(resolveFilePath(entry.from, configDir)) : configDir || process.cwd() + + const wrappedTemplate = ` +Base directory for this skill: ${resolvedPath}/ +File references (@path) in this skill are relative to this directory. + +${template.trim()} + + + +$ARGUMENTS +` + + const definition: CommandDefinition = { + name, + description: `(config - Skill) ${description}`, + template: wrappedTemplate, + model: sanitizeModelField(entry.model || fileMetadata.model, "opencode"), + agent: entry.agent || fileMetadata.agent, + subtask: entry.subtask ?? fileMetadata.subtask, + argumentHint: entry["argument-hint"] || fileMetadata["argument-hint"], + } + + const allowedTools = entry["allowed-tools"] || + (fileMetadata["allowed-tools"] ? fileMetadata["allowed-tools"].split(/\s+/).filter(Boolean) : undefined) + + return { + name, + path: entry.from ? resolveFilePath(entry.from, configDir) : undefined, + resolvedPath, + definition, + scope: "config", + license: entry.license || fileMetadata.license, + compatibility: entry.compatibility || fileMetadata.compatibility, + metadata: entry.metadata as Record | undefined || fileMetadata.metadata, + allowedTools, + } +} + +function normalizeConfig(config: SkillsConfig | undefined): { + sources: Array + enable: string[] + disable: string[] + entries: Record +} { + if (!config) { + return { sources: [], enable: [], disable: [], entries: {} } + } + + if (Array.isArray(config)) { + return { sources: [], enable: config, disable: [], entries: {} } + } + + const { sources = [], enable = [], disable = [], ...entries } = config + return { sources, enable, disable, entries } +} + +function mergeSkillDefinitions(base: LoadedSkill, patch: SkillDefinition): LoadedSkill { + const mergedMetadata = base.metadata || patch.metadata + ? deepMerge(base.metadata || {}, (patch.metadata as Record) || {}) + : undefined + + const mergedTools = base.allowedTools || patch["allowed-tools"] + ? [...(base.allowedTools || []), ...(patch["allowed-tools"] || [])] + : undefined + + const description = patch.description || base.definition.description?.replace(/^\([^)]+\) /, "") + + return { + ...base, + definition: { + ...base.definition, + description: `(${base.scope} - Skill) ${description}`, + model: patch.model || base.definition.model, + agent: patch.agent || base.definition.agent, + subtask: patch.subtask ?? base.definition.subtask, + argumentHint: patch["argument-hint"] || base.definition.argumentHint, + }, + license: patch.license || base.license, + compatibility: patch.compatibility || base.compatibility, + metadata: mergedMetadata as Record | undefined, + allowedTools: mergedTools ? [...new Set(mergedTools)] : undefined, + } +} + +export interface MergeSkillsOptions { + configDir?: string +} + +export function mergeSkills( + builtinSkills: BuiltinSkill[], + config: SkillsConfig | undefined, + userClaudeSkills: LoadedSkill[], + userOpencodeSkills: LoadedSkill[], + projectClaudeSkills: LoadedSkill[], + projectOpencodeSkills: LoadedSkill[], + options: MergeSkillsOptions = {} +): LoadedSkill[] { + const skillMap = new Map() + + for (const builtin of builtinSkills) { + const loaded = builtinToLoaded(builtin) + skillMap.set(loaded.name, loaded) + } + + const normalizedConfig = normalizeConfig(config) + + for (const [name, entry] of Object.entries(normalizedConfig.entries)) { + if (entry === false) continue + if (entry === true) continue + + if (entry.disable) continue + + const loaded = configEntryToLoaded(name, entry, options.configDir) + if (loaded) { + const existing = skillMap.get(name) + if (existing && !entry.template && !entry.from) { + skillMap.set(name, mergeSkillDefinitions(existing, entry)) + } else { + skillMap.set(name, loaded) + } + } + } + + const fileSystemSkills = [ + ...userClaudeSkills, + ...userOpencodeSkills, + ...projectClaudeSkills, + ...projectOpencodeSkills, + ] + + for (const skill of fileSystemSkills) { + const existing = skillMap.get(skill.name) + if (!existing || SCOPE_PRIORITY[skill.scope] > SCOPE_PRIORITY[existing.scope]) { + skillMap.set(skill.name, skill) + } + } + + for (const [name, entry] of Object.entries(normalizedConfig.entries)) { + if (entry === true) continue + if (entry === false) { + skillMap.delete(name) + continue + } + if (entry.disable) { + skillMap.delete(name) + continue + } + + const existing = skillMap.get(name) + if (existing && !entry.template && !entry.from) { + skillMap.set(name, mergeSkillDefinitions(existing, entry)) + } + } + + for (const name of normalizedConfig.disable) { + skillMap.delete(name) + } + + if (normalizedConfig.enable.length > 0) { + const enableSet = new Set(normalizedConfig.enable) + for (const name of skillMap.keys()) { + if (!enableSet.has(name)) { + skillMap.delete(name) + } + } + } + + return Array.from(skillMap.values()) +} diff --git a/src/features/opencode-skill-loader/types.ts b/src/features/opencode-skill-loader/types.ts index 397bb36..f956922 100644 --- a/src/features/opencode-skill-loader/types.ts +++ b/src/features/opencode-skill-loader/types.ts @@ -1,6 +1,6 @@ import type { CommandDefinition } from "../claude-code-command-loader/types" -export type SkillScope = "user" | "project" | "opencode" | "opencode-project" +export type SkillScope = "builtin" | "config" | "user" | "project" | "opencode" | "opencode-project" export interface SkillMetadata { name?: string @@ -17,8 +17,8 @@ export interface SkillMetadata { export interface LoadedSkill { name: string - path: string - resolvedPath: string + path?: string + resolvedPath?: string definition: CommandDefinition scope: SkillScope license?: string diff --git a/src/index.ts b/src/index.ts index d7decfc..d16ee7c 100644 --- a/src/index.ts +++ b/src/index.ts @@ -38,7 +38,13 @@ import { loadProjectSkills, loadOpencodeGlobalSkills, loadOpencodeProjectSkills, + discoverUserClaudeSkills, + discoverProjectClaudeSkills, + discoverOpencodeGlobalSkills, + discoverOpencodeProjectSkills, + mergeSkills, } from "./features/opencode-skill-loader"; +import { createBuiltinSkills } from "./features/builtin-skills"; import { loadUserAgents, @@ -322,9 +328,17 @@ const OhMyOpenCodePlugin: Plugin = async (ctx) => { const callOmoAgent = createCallOmoAgent(ctx, backgroundManager); const lookAt = createLookAt(ctx); - const skillTool = createSkillTool({ - opencodeOnly: pluginConfig.claude_code?.skills === false, - }); + const builtinSkills = createBuiltinSkills(); + const includeClaudeSkills = pluginConfig.claude_code?.skills !== false; + const mergedSkills = mergeSkills( + builtinSkills, + pluginConfig.skills, + includeClaudeSkills ? discoverUserClaudeSkills() : [], + discoverOpencodeGlobalSkills(), + includeClaudeSkills ? discoverProjectClaudeSkills() : [], + discoverOpencodeProjectSkills(), + ); + const skillTool = createSkillTool({ skills: mergedSkills }); const googleAuthHooks = pluginConfig.google_auth !== false ? await createGoogleAntigravityAuthPlugin(ctx) diff --git a/src/tools/interactive-bash/constants.ts b/src/tools/interactive-bash/constants.ts index 83470d5..485846f 100644 --- a/src/tools/interactive-bash/constants.ts +++ b/src/tools/interactive-bash/constants.ts @@ -13,4 +13,6 @@ export const BLOCKED_TMUX_SUBCOMMANDS = [ export const INTERACTIVE_BASH_DESCRIPTION = `Execute tmux commands. Use "omo-{name}" session pattern. +For: server processes, long-running tasks, background jobs, interactive CLI tools. + Blocked (use bash instead): capture-pane, save-buffer, show-buffer, pipe-pane.` diff --git a/src/tools/skill/tools.ts b/src/tools/skill/tools.ts index da6f217..0b4d594 100644 --- a/src/tools/skill/tools.ts +++ b/src/tools/skill/tools.ts @@ -3,7 +3,7 @@ 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 { discoverSkills, type LoadedSkill } from "../../features/opencode-skill-loader" import { parseFrontmatter } from "../../shared/frontmatter" function loadedSkillToInfo(skill: LoadedSkill): SkillInfo { @@ -38,8 +38,19 @@ function formatSkillsXml(skills: SkillInfo[]): string { 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 = discoverSkills({ includeClaudeCodePaths: !options.opencodeOnly }) + const skills = options.skills ?? discoverSkills({ includeClaudeCodePaths: !options.opencodeOnly }) const skillInfos = skills.map(loadedSkillToInfo) const description = skillInfos.length === 0 @@ -52,26 +63,25 @@ export function createSkillTool(options: SkillLoadOptions = {}): ToolDefinition 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 }) + 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 content = readFileSync(skill.path, "utf-8") - const { body } = parseFrontmatter(content) - const dir = dirname(skill.path) + const body = extractSkillBody(skill) + const dir = skill.path ? dirname(skill.path) : skill.resolvedPath || process.cwd() - const output = [ + return [ `## Skill: ${skill.name}`, "", `**Base directory**: ${dir}`, "", - body.trim(), + body, ].join("\n") - - return output }, }) } diff --git a/src/tools/skill/types.ts b/src/tools/skill/types.ts index 9534086..298cbad 100644 --- a/src/tools/skill/types.ts +++ b/src/tools/skill/types.ts @@ -1,3 +1,5 @@ +import type { SkillScope, LoadedSkill } from "../../features/opencode-skill-loader/types" + export interface SkillArgs { name: string } @@ -5,8 +7,8 @@ export interface SkillArgs { export interface SkillInfo { name: string description: string - location: string - scope: "opencode-project" | "project" | "opencode" | "user" + location?: string + scope: SkillScope license?: string compatibility?: string metadata?: Record @@ -16,4 +18,6 @@ export interface SkillInfo { export interface SkillLoadOptions { /** When true, only load from OpenCode paths (.opencode/skill/, ~/.config/opencode/skill/) */ opencodeOnly?: boolean + /** Pre-merged skills to use instead of discovering */ + skills?: LoadedSkill[] } diff --git a/src/tools/slashcommand/tools.ts b/src/tools/slashcommand/tools.ts index 3631aed..d99c14b 100644 --- a/src/tools/slashcommand/tools.ts +++ b/src/tools/slashcommand/tools.ts @@ -124,8 +124,8 @@ async function formatLoadedCommand(cmd: CommandInfo): Promise { sections.push("---\n") sections.push("## Command Instructions\n") - const commandDir = dirname(cmd.path) - const withFileRefs = await resolveFileReferencesInText(cmd.content, commandDir) + const commandDir = cmd.path ? dirname(cmd.path) : process.cwd() + const withFileRefs = await resolveFileReferencesInText(cmd.content || "", commandDir) const resolvedContent = await resolveCommandsInText(withFileRefs) sections.push(resolvedContent.trim()) @@ -151,40 +151,14 @@ function formatCommandList(items: CommandInfo[]): string { } export const slashcommand: ToolDefinition = tool({ - description: `Execute a slash command within the main conversation. + description: `Load a skill to get detailed instructions for a specific task. -When you use this tool, the slash command gets expanded to a full prompt that provides detailed instructions on how to complete the task. +Skills provide specialized knowledge and step-by-step guidance. +Use this when a task matches an available skill's description. -How slash commands work: -- Invoke commands using this tool with the command name (without arguments) -- The command's prompt will expand and provide detailed instructions -- Arguments from user input should be passed separately - -Important: -- Only use commands listed in Available Commands below -- Do not invoke a command that is already running -- **CRITICAL**: When user's message starts with '/' (e.g., "/commit", "/plan"), you MUST immediately invoke this tool with that command. Do NOT attempt to handle the command manually. - -Commands are loaded from (priority order, highest wins): -- .opencode/command/ (opencode-project - OpenCode project-specific commands) -- ./.claude/commands/ (project - Claude Code project-specific commands) -- ~/.config/opencode/command/ (opencode - OpenCode global commands) -- $CLAUDE_CONFIG_DIR/commands/ or ~/.claude/commands/ (user - Claude Code global commands) - -Skills are loaded from (priority order, highest wins): -- .opencode/skill/ (opencode-project - OpenCode project-specific skills) -- ./.claude/skills/ (project - Claude Code project-specific skills) -- ~/.config/opencode/skill/ (opencode - OpenCode global skills) -- $CLAUDE_CONFIG_DIR/skills/ or ~/.claude/skills/ (user - Claude Code global skills) - -Each command/skill is a markdown file with: -- YAML frontmatter: description, argument-hint, model, agent, subtask (optional) -- Markdown body: The command instructions/prompt -- File references: @path/to/file (relative to command file location) -- Shell injection: \`!\`command\`\` (executes and injects output) - -Available Commands: -${commandListForDescription}`, + +${commandListForDescription} +`, args: { command: tool.schema diff --git a/src/tools/slashcommand/types.ts b/src/tools/slashcommand/types.ts index 41142d0..36437e2 100644 --- a/src/tools/slashcommand/types.ts +++ b/src/tools/slashcommand/types.ts @@ -1,4 +1,4 @@ -export type CommandScope = "user" | "project" | "opencode" | "opencode-project" +export type CommandScope = "builtin" | "config" | "user" | "project" | "opencode" | "opencode-project" export interface CommandMetadata { name: string @@ -11,8 +11,8 @@ export interface CommandMetadata { export interface CommandInfo { name: string - path: string + path?: string metadata: CommandMetadata - content: string + content?: string scope: CommandScope }