Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 3 additions & 10 deletions src/plugin/tool-registry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@ import {
createBackgroundTools,
createCallOmoAgent,
createLookAt,
createSkillTool,
createSkillMcpTool,
createSlashcommandTool,
createGrepTools,
Expand Down Expand Up @@ -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,
Expand All @@ -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
Expand All @@ -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,
Expand Down
1 change: 0 additions & 1 deletion src/tools/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
2 changes: 1 addition & 1 deletion src/tools/skill-mcp/tools.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.`,
)
}

Expand Down
131 changes: 131 additions & 0 deletions src/tools/slashcommand/skill-formatter.ts
Original file line number Diff line number Diff line change
@@ -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<string> {
if (skill.lazyContent) {
const fullTemplate = await skill.lazyContent.load()
const templateMatch = fullTemplate.match(/<skill-instruction>([\s\S]*?)<\/skill-instruction>/)
return templateMatch ? templateMatch[1].trim() : fullTemplate
}

if (skill.path) {
return extractSkillTemplate(skill)
}

const templateMatch = skill.definition.template?.match(/<skill-instruction>([\s\S]*?)<\/skill-instruction>/)
return templateMatch ? templateMatch[1].trim() : skill.definition.template || ""
}

export async function formatMcpCapabilities(
skill: LoadedSkill,
manager: SkillMcpManager,
sessionID: string
): Promise<string | null> {
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<string> {
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")
}
13 changes: 13 additions & 0 deletions src/tools/slashcommand/slashcommand-tool.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)
}

Expand Down
8 changes: 8 additions & 0 deletions src/tools/slashcommand/types.ts
Original file line number Diff line number Diff line change
@@ -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"

Expand All @@ -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
}