diff --git a/src/plugin/tool-registry.ts b/src/plugin/tool-registry.ts index 7236ddc489..861d82ff95 100644 --- a/src/plugin/tool-registry.ts +++ b/src/plugin/tool-registry.ts @@ -11,7 +11,6 @@ import { createBackgroundTools, createCallOmoAgent, createLookAt, - createSkillTool, createSkillMcpTool, createSlashcommandTool, createGrepTools, @@ -87,14 +86,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, @@ -105,6 +96,9 @@ export function createToolRegistry(args: { const slashcommandTool = createSlashcommandTool({ commands, skills: skillContext.mergedSkills, + mcpManager: managers.skillMcpManager, + getSessionID: getSessionIDForMcp, + gitMasterConfig: pluginConfig.git_master, }) const taskSystemEnabled = pluginConfig.experimental?.task_system ?? false @@ -127,7 +121,6 @@ export function createToolRegistry(args: { call_omo_agent: callOmoAgent, ...(lookAt ? { look_at: lookAt } : {}), task: delegateTask, - skill: skillTool, skill_mcp: skillMcpTool, slashcommand: slashcommandTool, interactive_bash, diff --git a/src/tools/index.ts b/src/tools/index.ts index a38a6c74ce..f109522010 100644 --- a/src/tools/index.ts +++ b/src/tools/index.ts @@ -19,7 +19,6 @@ 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-mcp/tools.ts b/src/tools/skill-mcp/tools.ts index 96dddaa753..7769027445 100644 --- a/src/tools/skill-mcp/tools.ts +++ b/src/tools/skill-mcp/tools.ts @@ -137,7 +137,7 @@ export function createSkillMcpTool(options: SkillMcpToolOptions): ToolDefinition `Available MCP servers in loaded skills:\n` + formatAvailableMcps(skills) + `\n\n` + - `Hint: Load the skill first using the 'skill' tool, then call skill_mcp.`, + `Hint: Load the skill first using the 'slashcommand' tool, then call skill_mcp.`, ) } diff --git a/src/tools/slashcommand/skill-formatter.ts b/src/tools/slashcommand/skill-formatter.ts new file mode 100644 index 0000000000..9396c99c45 --- /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 fb1227f3c5..2c4a4b4bf3 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 @@ -76,6 +77,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 2cacdd014c..172b282c88 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 }