import { tool, type ToolDefinition } from "@opencode-ai/plugin" import { discoverAllSkills, type LoadedSkill } from "../../features/opencode-skill-loader" import type { CommandInfo, SlashcommandToolOptions } from "./types" 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 let cachedSkills: LoadedSkill[] | null = options.skills ?? null let cachedDescription: string | null = null const getCommands = (): CommandInfo[] => { if (cachedCommands) return cachedCommands cachedCommands = discoverCommandsSync() return cachedCommands } const getSkills = async (): Promise => { if (cachedSkills) return cachedSkills cachedSkills = await discoverAllSkills() return cachedSkills } const getAllItems = async (): Promise => { const commands = getCommands() const skills = await getSkills() return [...commands, ...skills.map(skillToCommandInfo)] } const buildDescription = async (): Promise => { if (cachedDescription) return cachedDescription const commands = getCommands() cachedDescription = buildDescriptionFromItems(commands) return cachedDescription } if (options.commands !== undefined) { cachedDescription = buildDescriptionFromItems(options.commands) } else { void buildDescription() } return tool({ get description() { return cachedDescription ?? TOOL_DESCRIPTION_PREFIX }, args: { command: tool.schema .string() .describe( "The slash command name (without leading slash). E.g., 'publish', 'commit', 'plan'" ), user_message: tool.schema .string() .optional() .describe( "Optional arguments or context to pass to the command. E.g., for '/publish patch', command='publish' user_message='patch'" ), }, async execute(args) { const allItems = await getAllItems() if (!args.command) { return formatCommandList(allItems) + "\n\nProvide a command or skill name to execute." } const commandName = args.command.replace(/^\//, "") const exactMatch = allItems.find( (command) => command.name.toLowerCase() === commandName.toLowerCase() ) 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) } const partialMatches = allItems.filter((command) => command.name.toLowerCase().includes(commandName.toLowerCase()) ) if (partialMatches.length > 0) { const matchList = partialMatches.map((command) => `/${command.name}`).join(", ") return `No exact match for "/${commandName}". Did you mean: ${matchList}?\n\n${formatCommandList(allItems)}` } return commandName.includes(":") ? `Marketplace plugin commands like "/${commandName}" are not supported. Use .claude/commands/ for custom commands.\n\n${formatCommandList(allItems)}` : `Command or skill "/${commandName}" not found.\n\n${formatCommandList(allItems)}\n\nTry a different name.` }, }) } export const slashcommand: ToolDefinition = createSlashcommandTool()