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
}