diff --git a/src/agents/atlas/prompt-section-builder.ts b/src/agents/atlas/prompt-section-builder.ts index 19462165..50f6312d 100644 --- a/src/agents/atlas/prompt-section-builder.ts +++ b/src/agents/atlas/prompt-section-builder.ts @@ -6,7 +6,7 @@ */ import type { CategoryConfig } from "../../config/schema" -import { formatCustomSkillsBlock, type AvailableAgent, type AvailableSkill } from "../dynamic-agent-prompt-builder" +import type { AvailableAgent, AvailableSkill } from "../dynamic-agent-prompt-builder" import { CATEGORY_DESCRIPTIONS } from "../../tools/delegate-task/constants" import { mergeCategories } from "../../shared/merge-categories" import { truncateDescription } from "../../shared/truncate-description" @@ -58,43 +58,16 @@ export function buildSkillsSection(skills: AvailableSkill[]): string { const builtinSkills = skills.filter((s) => s.location === "plugin") const customSkills = skills.filter((s) => s.location !== "plugin") - const builtinRows = builtinSkills.map((s) => { - const shortDesc = truncateDescription(s.description) - return `- **\`${s.name}\`** — ${shortDesc}` - }) - - const customRows = customSkills.map((s) => { - const shortDesc = truncateDescription(s.description) - const source = s.location === "project" ? "project" : "user" - return `- **\`${s.name}\`** (${source}): ${shortDesc}` - }) - - const customSkillBlock = formatCustomSkillsBlock(customRows, customSkills, "**") - - let skillsTable: string - - if (customSkills.length > 0 && builtinSkills.length > 0) { - skillsTable = `**Built-in Skills:** - -${builtinRows.join("\n")} - -${customSkillBlock}` - } else if (customSkills.length > 0) { - skillsTable = customSkillBlock - } else { - skillsTable = `${builtinRows.join("\n")}` - } - return ` #### 3.2.2: Skill Selection (PREPEND TO PROMPT) -**Skills are specialized instructions that guide subagent behavior. Consider them alongside category selection.** - -${skillsTable} +**Use the \`Category + Skills Delegation System\` section below as the single source of truth for skill details.** +- Built-in skills available: ${builtinSkills.length} +- User-installed skills available: ${customSkills.length} **MANDATORY: Evaluate ALL skills (built-in AND user-installed) for relevance to your task.** -Read each skill's description and ask: "Does this skill's domain overlap with my task?" +Read each skill's description in the section below and ask: "Does this skill's domain overlap with my task?" - If YES: INCLUDE in load_skills=[...] - If NO: You MUST justify why in your pre-delegation declaration diff --git a/src/agents/dynamic-agent-prompt-builder.test.ts b/src/agents/dynamic-agent-prompt-builder.test.ts index 952c8912..fcc99bad 100644 --- a/src/agents/dynamic-agent-prompt-builder.test.ts +++ b/src/agents/dynamic-agent-prompt-builder.test.ts @@ -43,16 +43,16 @@ describe("buildCategorySkillsDelegationGuide", () => { expect(result).toContain("HIGH PRIORITY") }) - it("should include custom skill names in CRITICAL warning", () => { + it("should list custom skills and keep CRITICAL warning", () => { //#given: custom skills installed const allSkills = [...builtinSkills, ...customUserSkills] //#when: building the delegation guide const result = buildCategorySkillsDelegationGuide(categories, allSkills) - //#then: should mention custom skills by name in the warning - expect(result).toContain('"react-19"') - expect(result).toContain('"tailwind-4"') + //#then: should mention custom skills by name and include warning + expect(result).toContain("`react-19`") + expect(result).toContain("`tailwind-4`") expect(result).toContain("CRITICAL") }) @@ -180,8 +180,8 @@ describe("formatCustomSkillsBlock", () => { //#then: contains all expected elements expect(result).toContain("User-Installed Skills (HIGH PRIORITY)") expect(result).toContain("CRITICAL") - expect(result).toContain('"react-19"') - expect(result).toContain('"tailwind-4"') + expect(result).toContain("`react-19`") + expect(result).toContain("`tailwind-4`") expect(result).toContain("| user |") expect(result).toContain("| project |") }) diff --git a/src/agents/dynamic-agent-prompt-builder.ts b/src/agents/dynamic-agent-prompt-builder.ts index abb1297f..c192054f 100644 --- a/src/agents/dynamic-agent-prompt-builder.ts +++ b/src/agents/dynamic-agent-prompt-builder.ts @@ -35,7 +35,7 @@ export function categorizeTools(toolNames: string[]): AvailableTool[] { category = "search" } else if (name.startsWith("session_")) { category = "session" - } else if (name === "slashcommand") { + } else if (name === "skill") { category = "command" } return { name, category } @@ -167,7 +167,6 @@ export function formatCustomSkillsBlock( customSkills: AvailableSkill[], headerLevel: "####" | "**" = "####" ): string { - const customSkillNames = customSkills.map((s) => `"${s.name}"`).join(", ") const header = headerLevel === "####" ? `#### User-Installed Skills (HIGH PRIORITY)` : `**User-Installed Skills (HIGH PRIORITY):**` @@ -180,7 +179,7 @@ Subagents are STATELESS — they lose all custom knowledge unless you pass these ${customRows.join("\n")} > **CRITICAL**: Ignoring user-installed skills when they match the task domain is a failure. -> The user installed ${customSkillNames} for a reason — USE THEM when the task overlaps with their domain.` +> The user installed custom skills for a reason — USE THEM when the task overlaps with their domain.` } export function buildCategorySkillsDelegationGuide(categories: AvailableCategory[], skills: AvailableSkill[]): string { diff --git a/src/hooks/auto-slash-command/executor.ts b/src/hooks/auto-slash-command/executor.ts index 185ca479..ffa96be8 100644 --- a/src/hooks/auto-slash-command/executor.ts +++ b/src/hooks/auto-slash-command/executor.ts @@ -202,7 +202,9 @@ export async function executeSlashCommand(parsed: ParsedSlashCommand, options?: if (!command) { return { success: false, - error: parsed.command.includes(":") ? `Marketplace plugin commands like "/${parsed.command}" are not supported. Use .claude/commands/ for custom commands.` : `Command "/${parsed.command}" not found. Use the slashcommand tool to list available commands.`, + error: parsed.command.includes(":") + ? `Marketplace plugin commands like "/${parsed.command}" are not supported. Use .claude/commands/ for custom commands.` + : `Command "/${parsed.command}" not found. Use the skill tool to list available skills and commands.`, } } diff --git a/src/plugin/tool-execute-before.ts b/src/plugin/tool-execute-before.ts index 70d023b4..09ae1681 100644 --- a/src/plugin/tool-execute-before.ts +++ b/src/plugin/tool-execute-before.ts @@ -43,13 +43,13 @@ export function createToolExecuteBeforeHandler(args: { } } - if (hooks.ralphLoop && input.tool === "slashcommand") { - const rawCommand = typeof output.args.command === "string" ? output.args.command : undefined - const command = rawCommand?.replace(/^\//, "").toLowerCase() + if (hooks.ralphLoop && input.tool === "skill") { + const rawName = typeof output.args.name === "string" ? output.args.name : undefined + const command = rawName?.replace(/^\//, "").toLowerCase() const sessionID = input.sessionID || getMainSessionID() if (command === "ralph-loop" && sessionID) { - const rawArgs = rawCommand?.replace(/^\/?(ralph-loop)\s*/i, "") || "" + const rawArgs = rawName?.replace(/^\/?(ralph-loop)\s*/i, "") || "" const taskMatch = rawArgs.match(/^["'](.+?)["']/) const prompt = taskMatch?.[1] || @@ -66,7 +66,7 @@ export function createToolExecuteBeforeHandler(args: { } else if (command === "cancel-ralph" && sessionID) { hooks.ralphLoop.cancelLoop(sessionID) } else if (command === "ulw-loop" && sessionID) { - const rawArgs = rawCommand?.replace(/^\/?(ulw-loop)\s*/i, "") || "" + const rawArgs = rawName?.replace(/^\/?(ulw-loop)\s*/i, "") || "" const taskMatch = rawArgs.match(/^["'](.+?)["']/) const prompt = taskMatch?.[1] || @@ -84,9 +84,9 @@ export function createToolExecuteBeforeHandler(args: { } } - if (input.tool === "slashcommand") { - const rawCommand = typeof output.args.command === "string" ? output.args.command : undefined - const command = rawCommand?.replace(/^\//, "").toLowerCase() + if (input.tool === "skill") { + const rawName = typeof output.args.name === "string" ? output.args.name : undefined + const command = rawName?.replace(/^\//, "").toLowerCase() const sessionID = input.sessionID || getMainSessionID() if (command === "stop-continuation" && sessionID) { diff --git a/src/plugin/tool-registry.ts b/src/plugin/tool-registry.ts index 01c6fe3e..590e28c7 100644 --- a/src/plugin/tool-registry.ts +++ b/src/plugin/tool-registry.ts @@ -11,9 +11,8 @@ import { createBackgroundTools, createCallOmoAgent, createLookAt, - createSkillTool, createSkillMcpTool, - createSlashcommandTool, + createSkillTool, createGrepTools, createGlobTools, createAstGrepTools, @@ -89,14 +88,6 @@ export function createToolRegistry(args: { const getSessionIDForMcp = (): string => getMainSessionID() || "" - const skillTool = createSkillTool({ - skills: skillContext.mergedSkills, - mcpManager: managers.skillMcpManager, - getSessionID: getSessionIDForMcp, - gitMasterConfig: pluginConfig.git_master, - disabledSkills: skillContext.disabledSkills, - }) - const skillMcpTool = createSkillMcpTool({ manager: managers.skillMcpManager, getLoadedSkills: () => skillContext.mergedSkills, @@ -104,9 +95,12 @@ export function createToolRegistry(args: { }) const commands = discoverCommandsSync(ctx.directory) - const slashcommandTool = createSlashcommandTool({ + const skillTool = createSkillTool({ commands, skills: skillContext.mergedSkills, + mcpManager: managers.skillMcpManager, + getSessionID: getSessionIDForMcp, + gitMasterConfig: pluginConfig.git_master, }) const taskSystemEnabled = pluginConfig.experimental?.task_system ?? false @@ -134,9 +128,8 @@ export function createToolRegistry(args: { call_omo_agent: callOmoAgent, ...(lookAt ? { look_at: lookAt } : {}), task: delegateTask, - skill: skillTool, skill_mcp: skillMcpTool, - slashcommand: slashcommandTool, + skill: skillTool, interactive_bash, ...taskToolsRecord, ...hashlineToolsRecord, diff --git a/src/tools/index.ts b/src/tools/index.ts index 0f2b8a1c..9d9bd9c0 100644 --- a/src/tools/index.ts +++ b/src/tools/index.ts @@ -13,13 +13,13 @@ export { lspManager } export { createAstGrepTools } from "./ast-grep" export { createGrepTools } from "./grep" export { createGlobTools } from "./glob" -export { createSlashcommandTool, discoverCommandsSync } from "./slashcommand" +export { createSkillTool } from "./skill" +export { discoverCommandsSync } from "./slashcommand" export { createSessionManagerTools } from "./session-manager" export { sessionExists } from "./session-manager/storage" export { interactive_bash, startBackgroundCheck as startTmuxCheck } from "./interactive-bash" -export { createSkillTool } from "./skill" export { createSkillMcpTool } from "./skill-mcp" import { diff --git a/src/tools/skill/constants.ts b/src/tools/skill/constants.ts index 538dc098..735772d8 100644 --- a/src/tools/skill/constants.ts +++ b/src/tools/skill/constants.ts @@ -1,8 +1,14 @@ 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_NO_SKILLS = "Load a skill or execute a slash command 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. +export const TOOL_DESCRIPTION_PREFIX = `Load a skill or execute a slash command 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.` +Skills and commands provide specialized knowledge and step-by-step guidance. +Use this when a task matches an available skill's or command's description. + +**How to use:** +- Call with a skill name: name='code-review' +- Call with a command name (without leading slash): name='publish' +- The tool will return detailed instructions with your context applied. +` diff --git a/src/tools/skill/tools.ts b/src/tools/skill/tools.ts index 006a07b4..151190cc 100644 --- a/src/tools/skill/tools.ts +++ b/src/tools/skill/tools.ts @@ -7,6 +7,9 @@ import { getAllSkills, extractSkillTemplate } from "../../features/opencode-skil import { injectGitMasterConfig } from "../../features/opencode-skill-loader/skill-content" import type { SkillMcpManager, SkillMcpClientInfo, SkillMcpServerContext } from "../../features/skill-mcp-manager" import type { Tool, Resource, Prompt } from "@modelcontextprotocol/sdk/types.js" +import { discoverCommandsSync } from "../slashcommand/command-discovery" +import type { CommandInfo } from "../slashcommand/types" +import { formatLoadedCommand } from "../slashcommand/command-output-formatter" function loadedSkillToInfo(skill: LoadedSkill): SkillInfo { return { @@ -21,23 +24,38 @@ function loadedSkillToInfo(skill: LoadedSkill): SkillInfo { } } -function formatSkillsXml(skills: SkillInfo[]): string { - if (skills.length === 0) return "" +function formatCombinedDescription(skills: SkillInfo[], commands: CommandInfo[]): string { + const lines: string[] = [] - 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") + if (skills.length === 0 && commands.length === 0) { + return TOOL_DESCRIPTION_NO_SKILLS + } - return `\n\n\n${skillsXml}\n` + if (skills.length > 0) { + const skillsXml = skills.map(skill => { + const parts = [ + " ", + ` ${skill.name}`, + ` ${skill.description}`, + ] + if (skill.compatibility) { + parts.push(` ${skill.compatibility}`) + } + parts.push(" ") + return parts.join("\n") + }).join("\n") + lines.push(`\n\n${skillsXml}\n`) + } + + if (commands.length > 0) { + const commandLines = commands.map(cmd => { + const hint = cmd.metadata.argumentHint ? ` ${cmd.metadata.argumentHint}` : "" + return ` - /${cmd.name}${hint}: ${cmd.metadata.description || "(no description)"} (${cmd.scope})` + }).join("\n") + lines.push(`\n\n${commandLines}\n`) + } + + return TOOL_DESCRIPTION_PREFIX + lines.join("") } async function extractSkillBody(skill: LoadedSkill): Promise { @@ -128,6 +146,7 @@ async function formatMcpCapabilities( export function createSkillTool(options: SkillLoadOptions = {}): ToolDefinition { let cachedSkills: LoadedSkill[] | null = null + let cachedCommands: CommandInfo[] | null = options.commands ?? null let cachedDescription: string | null = null const getSkills = async (): Promise => { @@ -137,23 +156,30 @@ export function createSkillTool(options: SkillLoadOptions = {}): ToolDefinition return cachedSkills } - const getDescription = async (): Promise => { + const getCommands = (): CommandInfo[] => { + if (cachedCommands) return cachedCommands + cachedCommands = discoverCommandsSync() + return cachedCommands + } + + const buildDescription = async (): Promise => { if (cachedDescription) return cachedDescription const skills = await getSkills() + const commands = getCommands() const skillInfos = skills.map(loadedSkillToInfo) - cachedDescription = skillInfos.length === 0 - ? TOOL_DESCRIPTION_NO_SKILLS - : TOOL_DESCRIPTION_PREFIX + formatSkillsXml(skillInfos) + cachedDescription = formatCombinedDescription(skillInfos, commands) return cachedDescription } - if (options.skills) { + // Eagerly build description when callers pre-provide skills/commands. + if (options.skills !== undefined) { const skillInfos = options.skills.map(loadedSkillToInfo) - cachedDescription = skillInfos.length === 0 - ? TOOL_DESCRIPTION_NO_SKILLS - : TOOL_DESCRIPTION_PREFIX + formatSkillsXml(skillInfos) + const commandsForDescription = options.commands ?? [] + cachedDescription = formatCombinedDescription(skillInfos, commandsForDescription) + } else if (options.commands !== undefined) { + cachedDescription = formatCombinedDescription([], options.commands) } else { - getDescription() + void buildDescription() } return tool({ @@ -161,49 +187,79 @@ export function createSkillTool(options: SkillLoadOptions = {}): ToolDefinition return cachedDescription ?? TOOL_DESCRIPTION_PREFIX }, args: { - name: tool.schema.string().describe("The skill identifier from available_skills (e.g., 'code-review')"), + name: tool.schema.string().describe("The skill or command name (e.g., 'code-review' or 'publish'). Use without leading slash for commands."), }, async execute(args: SkillArgs, ctx?: { agent?: string }) { const skills = await getSkills() - const skill = skills.find(s => s.name === args.name) + const commands = getCommands() - if (!skill) { - const available = skills.map(s => s.name).join(", ") - throw new Error(`Skill "${args.name}" not found. Available skills: ${available || "none"}`) + const requestedName = args.name.replace(/^\//, "") + + // Check skills first (exact match, case-insensitive) + const matchedSkill = skills.find(s => s.name.toLowerCase() === requestedName.toLowerCase()) + + if (matchedSkill) { + if (matchedSkill.definition.agent && (!ctx?.agent || matchedSkill.definition.agent !== ctx.agent)) { + throw new Error(`Skill "${matchedSkill.name}" is restricted to agent "${matchedSkill.definition.agent}"`) + } + + let body = await extractSkillBody(matchedSkill) + + if (matchedSkill.name === "git-master") { + body = injectGitMasterConfig(body, options.gitMasterConfig) + } + + const dir = matchedSkill.path ? dirname(matchedSkill.path) : matchedSkill.resolvedPath || process.cwd() + + const output = [ + `## Skill: ${matchedSkill.name}`, + "", + `**Base directory**: ${dir}`, + "", + body, + ] + + if (options.mcpManager && options.getSessionID && matchedSkill.mcpConfig) { + const mcpInfo = await formatMcpCapabilities( + matchedSkill, + options.mcpManager, + options.getSessionID() + ) + if (mcpInfo) { + output.push(mcpInfo) + } + } + + return output.join("\n") } - if (skill.definition.agent && (!ctx?.agent || skill.definition.agent !== ctx.agent)) { - throw new Error(`Skill "${args.name}" is restricted to agent "${skill.definition.agent}"`) + // Check commands (exact match, case-insensitive) + const matchedCommand = commands.find(c => c.name.toLowerCase() === requestedName.toLowerCase()) + + if (matchedCommand) { + return await formatLoadedCommand(matchedCommand) } - let body = await extractSkillBody(skill) - - if (args.name === "git-master") { - body = injectGitMasterConfig(body, options.gitMasterConfig) - } - - const dir = skill.path ? dirname(skill.path) : skill.resolvedPath || process.cwd() - - const output = [ - `## Skill: ${skill.name}`, - "", - `**Base directory**: ${dir}`, - "", - body, + // No match found — provide helpful error with partial matches + const allNames = [ + ...skills.map(s => s.name), + ...commands.map(c => `/${c.name}`), ] - if (options.mcpManager && options.getSessionID && skill.mcpConfig) { - const mcpInfo = await formatMcpCapabilities( - skill, - options.mcpManager, - options.getSessionID() + const partialMatches = allNames.filter(n => + n.toLowerCase().includes(requestedName.toLowerCase()) + ) + + if (partialMatches.length > 0) { + throw new Error( + `Skill or command "${args.name}" not found. Did you mean: ${partialMatches.join(", ")}?` ) - if (mcpInfo) { - output.push(mcpInfo) - } } - return output.join("\n") + const available = allNames.join(", ") + throw new Error( + `Skill or command "${args.name}" not found. Available: ${available || "none"}` + ) }, }) } diff --git a/src/tools/skill/types.ts b/src/tools/skill/types.ts index 3babfeef..13551ba9 100644 --- a/src/tools/skill/types.ts +++ b/src/tools/skill/types.ts @@ -1,6 +1,7 @@ import type { SkillScope, LoadedSkill } from "../../features/opencode-skill-loader/types" import type { SkillMcpManager } from "../../features/skill-mcp-manager" import type { GitMasterConfig } from "../../config/schema" +import type { CommandInfo } from "../slashcommand/types" export interface SkillArgs { name: string @@ -22,6 +23,8 @@ export interface SkillLoadOptions { opencodeOnly?: boolean /** Pre-merged skills to use instead of discovering */ skills?: LoadedSkill[] + /** Pre-discovered commands to use instead of discovering */ + commands?: CommandInfo[] /** MCP manager for querying skill-embedded MCP servers */ mcpManager?: SkillMcpManager /** Session ID getter for MCP client identification */ diff --git a/src/tools/slashcommand/skill-formatter.ts b/src/tools/slashcommand/skill-formatter.ts new file mode 100644 index 00000000..9396c99c --- /dev/null +++ b/src/tools/slashcommand/skill-formatter.ts @@ -0,0 +1,131 @@ +import { dirname } from "node:path" +import type { LoadedSkill } from "../../features/opencode-skill-loader" +import { extractSkillTemplate } from "../../features/opencode-skill-loader/skill-content" +import { injectGitMasterConfig as injectGitMasterConfigOriginal } from "../../features/opencode-skill-loader/skill-content" +import type { SkillMcpManager, SkillMcpClientInfo, SkillMcpServerContext } from "../../features/skill-mcp-manager" +import type { Tool, Resource, Prompt } from "@modelcontextprotocol/sdk/types.js" +import type { GitMasterConfig } from "../../config/schema/git-master" + +export async function extractSkillBody(skill: LoadedSkill): Promise { + if (skill.lazyContent) { + const fullTemplate = await skill.lazyContent.load() + const templateMatch = fullTemplate.match(/([\s\S]*?)<\/skill-instruction>/) + return templateMatch ? templateMatch[1].trim() : fullTemplate + } + + if (skill.path) { + return extractSkillTemplate(skill) + } + + const templateMatch = skill.definition.template?.match(/([\s\S]*?)<\/skill-instruction>/) + return templateMatch ? templateMatch[1].trim() : skill.definition.template || "" +} + +export async function formatMcpCapabilities( + skill: LoadedSkill, + manager: SkillMcpManager, + sessionID: string +): Promise { + if (!skill.mcpConfig || Object.keys(skill.mcpConfig).length === 0) { + return null + } + + const sections: string[] = ["", "## Available MCP Servers", ""] + + for (const [serverName, config] of Object.entries(skill.mcpConfig)) { + const info: SkillMcpClientInfo = { + serverName, + skillName: skill.name, + sessionID, + } + const context: SkillMcpServerContext = { + config, + skillName: skill.name, + } + + sections.push(`### ${serverName}`) + sections.push("") + + try { + const [tools, resources, prompts] = await Promise.all([ + manager.listTools(info, context).catch(() => []), + manager.listResources(info, context).catch(() => []), + manager.listPrompts(info, context).catch(() => []), + ]) + + if (tools.length > 0) { + sections.push("**Tools:**") + sections.push("") + for (const t of tools as Tool[]) { + sections.push(`#### \`${t.name}\``) + if (t.description) { + sections.push(t.description) + } + sections.push("") + sections.push("**inputSchema:**") + sections.push("```json") + sections.push(JSON.stringify(t.inputSchema, null, 2)) + sections.push("```") + sections.push("") + } + } + if (resources.length > 0) { + sections.push(`**Resources**: ${resources.map((r: Resource) => r.uri).join(", ")}`) + } + if (prompts.length > 0) { + sections.push(`**Prompts**: ${prompts.map((p: Prompt) => p.name).join(", ")}`) + } + + if (tools.length === 0 && resources.length === 0 && prompts.length === 0) { + sections.push("*No capabilities discovered*") + } + } catch (error) { + const errorMessage = error instanceof Error ? error.message : String(error) + sections.push(`*Failed to connect: ${errorMessage.split("\n")[0]}*`) + } + + sections.push("") + sections.push(`Use \`skill_mcp\` tool with \`mcp_name="${serverName}"\` to invoke.`) + sections.push("") + } + + return sections.join("\n") +} + +export { injectGitMasterConfigOriginal as injectGitMasterConfig } + +export async function formatSkillOutput( + skill: LoadedSkill, + mcpManager?: SkillMcpManager, + getSessionID?: () => string, + gitMasterConfig?: GitMasterConfig +): Promise { + let body = await extractSkillBody(skill) + + if (skill.name === "git-master") { + body = injectGitMasterConfigOriginal(body, gitMasterConfig) + } + + const dir = skill.path ? dirname(skill.path) : skill.resolvedPath || process.cwd() + + const output = [ + `## Skill: ${skill.name}`, + "", + `**Base directory**: ${dir}`, + "", + body, + ] + + if (mcpManager && getSessionID && skill.mcpConfig) { + const mcpInfo = await formatMcpCapabilities( + skill, + mcpManager, + getSessionID() + ) + if (mcpInfo) { + output.push(mcpInfo) + } + } + + return output.join("\n") +} diff --git a/src/tools/slashcommand/slashcommand-tool.ts b/src/tools/slashcommand/slashcommand-tool.ts index 3de86a4a..798c2281 100644 --- a/src/tools/slashcommand/slashcommand-tool.ts +++ b/src/tools/slashcommand/slashcommand-tool.ts @@ -5,6 +5,7 @@ import { discoverCommandsSync } from "./command-discovery" import { buildDescriptionFromItems, TOOL_DESCRIPTION_PREFIX } from "./slashcommand-description" import { formatCommandList, formatLoadedCommand } from "./command-output-formatter" import { skillToCommandInfo } from "./skill-command-converter" +import { formatSkillOutput } from "./skill-formatter" export function createSlashcommandTool(options: SlashcommandToolOptions = {}): ToolDefinition { let cachedCommands: CommandInfo[] | null = options.commands ?? null @@ -75,6 +76,18 @@ export function createSlashcommandTool(options: SlashcommandToolOptions = {}): T ) if (exactMatch) { + const skills = await getSkills() + const matchedSkill = skills.find(s => s.name === exactMatch.name) + + if (matchedSkill) { + return await formatSkillOutput( + matchedSkill, + options.mcpManager, + options.getSessionID, + options.gitMasterConfig + ) + } + return await formatLoadedCommand(exactMatch, args.user_message) } diff --git a/src/tools/slashcommand/types.ts b/src/tools/slashcommand/types.ts index 2cacdd01..172b282c 100644 --- a/src/tools/slashcommand/types.ts +++ b/src/tools/slashcommand/types.ts @@ -1,4 +1,6 @@ import type { LoadedSkill, LazyContentLoader } from "../../features/opencode-skill-loader" +import type { SkillMcpManager } from "../../features/skill-mcp-manager" +import type { GitMasterConfig } from "../../config/schema/git-master" export type CommandScope = "builtin" | "config" | "user" | "project" | "opencode" | "opencode-project" @@ -25,4 +27,10 @@ export interface SlashcommandToolOptions { commands?: CommandInfo[] /** Pre-loaded skills (skip discovery if provided) */ skills?: LoadedSkill[] + /** MCP manager for skill MCP capabilities */ + mcpManager?: SkillMcpManager + /** Function to get current session ID */ + getSessionID?: () => string + /** Git master configuration */ + gitMasterConfig?: GitMasterConfig }