refactor(slashcommand): split tools.ts into discovery and formatting modules
Extract slash command tool internals: - command-discovery.ts: command finding and listing - command-output-formatter.ts: output formatting - skill-command-converter.ts: skill-to-command conversion - slashcommand-description.ts: tool description generation - slashcommand-tool.ts: core tool definition
This commit is contained in:
85
src/tools/slashcommand/command-discovery.ts
Normal file
85
src/tools/slashcommand/command-discovery.ts
Normal file
@@ -0,0 +1,85 @@
|
||||
import { existsSync, readdirSync, readFileSync } from "fs"
|
||||
import { basename, join } from "path"
|
||||
import { parseFrontmatter, sanitizeModelField, getOpenCodeConfigDir } from "../../shared"
|
||||
import type { CommandFrontmatter } from "../../features/claude-code-command-loader/types"
|
||||
import { isMarkdownFile } from "../../shared/file-utils"
|
||||
import { getClaudeConfigDir } from "../../shared"
|
||||
import { loadBuiltinCommands } from "../../features/builtin-commands"
|
||||
import type { CommandInfo, CommandMetadata, CommandScope } from "./types"
|
||||
|
||||
function discoverCommandsFromDir(commandsDir: string, scope: CommandScope): CommandInfo[] {
|
||||
if (!existsSync(commandsDir)) return []
|
||||
|
||||
const entries = readdirSync(commandsDir, { withFileTypes: true })
|
||||
const commands: CommandInfo[] = []
|
||||
|
||||
for (const entry of entries) {
|
||||
if (!isMarkdownFile(entry)) continue
|
||||
|
||||
const commandPath = join(commandsDir, entry.name)
|
||||
const commandName = basename(entry.name, ".md")
|
||||
|
||||
try {
|
||||
const content = readFileSync(commandPath, "utf-8")
|
||||
const { data, body } = parseFrontmatter<CommandFrontmatter>(content)
|
||||
|
||||
const isOpencodeSource = scope === "opencode" || scope === "opencode-project"
|
||||
const metadata: CommandMetadata = {
|
||||
name: commandName,
|
||||
description: data.description || "",
|
||||
argumentHint: data["argument-hint"],
|
||||
model: sanitizeModelField(data.model, isOpencodeSource ? "opencode" : "claude-code"),
|
||||
agent: data.agent,
|
||||
subtask: Boolean(data.subtask),
|
||||
}
|
||||
|
||||
commands.push({
|
||||
name: commandName,
|
||||
path: commandPath,
|
||||
metadata,
|
||||
content: body,
|
||||
scope,
|
||||
})
|
||||
} catch {
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
return commands
|
||||
}
|
||||
|
||||
export function discoverCommandsSync(): CommandInfo[] {
|
||||
const configDir = getOpenCodeConfigDir({ binary: "opencode" })
|
||||
const userCommandsDir = join(getClaudeConfigDir(), "commands")
|
||||
const projectCommandsDir = join(process.cwd(), ".claude", "commands")
|
||||
const opencodeGlobalDir = join(configDir, "command")
|
||||
const opencodeProjectDir = join(process.cwd(), ".opencode", "command")
|
||||
|
||||
const userCommands = discoverCommandsFromDir(userCommandsDir, "user")
|
||||
const opencodeGlobalCommands = discoverCommandsFromDir(opencodeGlobalDir, "opencode")
|
||||
const projectCommands = discoverCommandsFromDir(projectCommandsDir, "project")
|
||||
const opencodeProjectCommands = discoverCommandsFromDir(opencodeProjectDir, "opencode-project")
|
||||
|
||||
const builtinCommandsMap = loadBuiltinCommands()
|
||||
const builtinCommands: CommandInfo[] = Object.values(builtinCommandsMap).map((command) => ({
|
||||
name: command.name,
|
||||
metadata: {
|
||||
name: command.name,
|
||||
description: command.description || "",
|
||||
argumentHint: command.argumentHint,
|
||||
model: command.model,
|
||||
agent: command.agent,
|
||||
subtask: command.subtask,
|
||||
},
|
||||
content: command.template,
|
||||
scope: "builtin",
|
||||
}))
|
||||
|
||||
return [
|
||||
...builtinCommands,
|
||||
...opencodeProjectCommands,
|
||||
...projectCommands,
|
||||
...opencodeGlobalCommands,
|
||||
...userCommands,
|
||||
]
|
||||
}
|
||||
73
src/tools/slashcommand/command-output-formatter.ts
Normal file
73
src/tools/slashcommand/command-output-formatter.ts
Normal file
@@ -0,0 +1,73 @@
|
||||
import { dirname } from "path"
|
||||
import { resolveCommandsInText, resolveFileReferencesInText } from "../../shared"
|
||||
import type { CommandInfo } from "./types"
|
||||
|
||||
export async function formatLoadedCommand(
|
||||
command: CommandInfo,
|
||||
userMessage?: string
|
||||
): Promise<string> {
|
||||
const sections: string[] = []
|
||||
|
||||
sections.push(`# /${command.name} Command\n`)
|
||||
|
||||
if (command.metadata.description) {
|
||||
sections.push(`**Description**: ${command.metadata.description}\n`)
|
||||
}
|
||||
|
||||
if (command.metadata.argumentHint) {
|
||||
sections.push(`**Usage**: /${command.name} ${command.metadata.argumentHint}\n`)
|
||||
}
|
||||
|
||||
if (userMessage) {
|
||||
sections.push(`**Arguments**: ${userMessage}\n`)
|
||||
}
|
||||
|
||||
if (command.metadata.model) {
|
||||
sections.push(`**Model**: ${command.metadata.model}\n`)
|
||||
}
|
||||
|
||||
if (command.metadata.agent) {
|
||||
sections.push(`**Agent**: ${command.metadata.agent}\n`)
|
||||
}
|
||||
|
||||
if (command.metadata.subtask) {
|
||||
sections.push("**Subtask**: true\n")
|
||||
}
|
||||
|
||||
sections.push(`**Scope**: ${command.scope}\n`)
|
||||
sections.push("---\n")
|
||||
sections.push("## Command Instructions\n")
|
||||
|
||||
let content = command.content || ""
|
||||
if (!content && command.lazyContentLoader) {
|
||||
content = await command.lazyContentLoader.load()
|
||||
}
|
||||
|
||||
const commandDir = command.path ? dirname(command.path) : process.cwd()
|
||||
const withFileReferences = await resolveFileReferencesInText(content, commandDir)
|
||||
const resolvedContent = await resolveCommandsInText(withFileReferences)
|
||||
|
||||
let finalContent = resolvedContent.trim()
|
||||
if (userMessage) {
|
||||
finalContent = finalContent.replace(/\$\{user_message\}/g, userMessage)
|
||||
}
|
||||
|
||||
sections.push(finalContent)
|
||||
return sections.join("\n")
|
||||
}
|
||||
|
||||
export function formatCommandList(items: CommandInfo[]): string {
|
||||
if (items.length === 0) return "No commands or skills found."
|
||||
|
||||
const lines = ["# Available Commands & Skills\n"]
|
||||
|
||||
for (const command of items) {
|
||||
const hint = command.metadata.argumentHint ? ` ${command.metadata.argumentHint}` : ""
|
||||
lines.push(
|
||||
`- **/${command.name}${hint}**: ${command.metadata.description || "(no description)"} (${command.scope})`
|
||||
)
|
||||
}
|
||||
|
||||
lines.push(`\n**Total**: ${items.length} items`)
|
||||
return lines.join("\n")
|
||||
}
|
||||
20
src/tools/slashcommand/skill-command-converter.ts
Normal file
20
src/tools/slashcommand/skill-command-converter.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
import type { LoadedSkill } from "../../features/opencode-skill-loader"
|
||||
import type { CommandInfo } from "./types"
|
||||
|
||||
export function skillToCommandInfo(skill: LoadedSkill): CommandInfo {
|
||||
return {
|
||||
name: skill.name,
|
||||
path: skill.path,
|
||||
metadata: {
|
||||
name: skill.name,
|
||||
description: skill.definition.description || "",
|
||||
argumentHint: skill.definition.argumentHint,
|
||||
model: skill.definition.model,
|
||||
agent: skill.definition.agent,
|
||||
subtask: skill.definition.subtask,
|
||||
},
|
||||
content: skill.definition.template,
|
||||
scope: skill.scope,
|
||||
lazyContentLoader: skill.lazyContent,
|
||||
}
|
||||
}
|
||||
26
src/tools/slashcommand/slashcommand-description.ts
Normal file
26
src/tools/slashcommand/slashcommand-description.ts
Normal file
@@ -0,0 +1,26 @@
|
||||
import type { CommandInfo } from "./types"
|
||||
|
||||
export const TOOL_DESCRIPTION_PREFIX = `Load a skill or execute a command to get detailed instructions for a specific task.
|
||||
|
||||
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 command name only: command='publish'
|
||||
- Call with command and arguments: command='publish' user_message='patch'
|
||||
- The tool will return detailed instructions for the command with your arguments substituted.
|
||||
`
|
||||
|
||||
export function buildDescriptionFromItems(items: CommandInfo[]): string {
|
||||
const commandListForDescription = items
|
||||
.map((command) => {
|
||||
const hint = command.metadata.argumentHint ? ` ${command.metadata.argumentHint}` : ""
|
||||
return `- /${command.name}${hint}: ${command.metadata.description} (${command.scope})`
|
||||
})
|
||||
.join("\n")
|
||||
|
||||
return `${TOOL_DESCRIPTION_PREFIX}
|
||||
<available_skills>
|
||||
${commandListForDescription}
|
||||
</available_skills>`
|
||||
}
|
||||
96
src/tools/slashcommand/slashcommand-tool.ts
Normal file
96
src/tools/slashcommand/slashcommand-tool.ts
Normal file
@@ -0,0 +1,96 @@
|
||||
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"
|
||||
|
||||
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<LoadedSkill[]> => {
|
||||
if (cachedSkills) return cachedSkills
|
||||
cachedSkills = await discoverAllSkills()
|
||||
return cachedSkills
|
||||
}
|
||||
|
||||
const getAllItems = async (): Promise<CommandInfo[]> => {
|
||||
const commands = getCommands()
|
||||
const skills = await getSkills()
|
||||
return [...commands, ...skills.map(skillToCommandInfo)]
|
||||
}
|
||||
|
||||
const buildDescription = async (): Promise<string> => {
|
||||
if (cachedDescription) return cachedDescription
|
||||
const allItems = await getAllItems()
|
||||
cachedDescription = buildDescriptionFromItems(allItems)
|
||||
return cachedDescription
|
||||
}
|
||||
|
||||
if (options.commands !== undefined && options.skills !== undefined) {
|
||||
const allItems = [...options.commands, ...options.skills.map(skillToCommandInfo)]
|
||||
cachedDescription = buildDescriptionFromItems(allItems)
|
||||
} 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) {
|
||||
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 `Command or skill "/${commandName}" not found.\n\n${formatCommandList(allItems)}\n\nTry a different name.`
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
export const slashcommand: ToolDefinition = createSlashcommandTool()
|
||||
@@ -1,294 +1,2 @@
|
||||
import { tool, type ToolDefinition } from "@opencode-ai/plugin"
|
||||
import { existsSync, readdirSync, readFileSync } from "fs"
|
||||
import { join, basename, dirname } from "path"
|
||||
import { parseFrontmatter, resolveCommandsInText, resolveFileReferencesInText, sanitizeModelField, getOpenCodeConfigDir } from "../../shared"
|
||||
import type { CommandFrontmatter } from "../../features/claude-code-command-loader/types"
|
||||
import { isMarkdownFile } from "../../shared/file-utils"
|
||||
import { getClaudeConfigDir } from "../../shared"
|
||||
import { discoverAllSkills, type LoadedSkill } from "../../features/opencode-skill-loader"
|
||||
import { loadBuiltinCommands } from "../../features/builtin-commands"
|
||||
import type { CommandScope, CommandMetadata, CommandInfo, SlashcommandToolOptions } from "./types"
|
||||
|
||||
function discoverCommandsFromDir(commandsDir: string, scope: CommandScope): CommandInfo[] {
|
||||
if (!existsSync(commandsDir)) {
|
||||
return []
|
||||
}
|
||||
|
||||
const entries = readdirSync(commandsDir, { withFileTypes: true })
|
||||
const commands: CommandInfo[] = []
|
||||
|
||||
for (const entry of entries) {
|
||||
if (!isMarkdownFile(entry)) continue
|
||||
|
||||
const commandPath = join(commandsDir, entry.name)
|
||||
const commandName = basename(entry.name, ".md")
|
||||
|
||||
try {
|
||||
const content = readFileSync(commandPath, "utf-8")
|
||||
const { data, body } = parseFrontmatter<CommandFrontmatter>(content)
|
||||
|
||||
const isOpencodeSource = scope === "opencode" || scope === "opencode-project"
|
||||
const metadata: CommandMetadata = {
|
||||
name: commandName,
|
||||
description: data.description || "",
|
||||
argumentHint: data["argument-hint"],
|
||||
model: sanitizeModelField(data.model, isOpencodeSource ? "opencode" : "claude-code"),
|
||||
agent: data.agent,
|
||||
subtask: Boolean(data.subtask),
|
||||
}
|
||||
|
||||
commands.push({
|
||||
name: commandName,
|
||||
path: commandPath,
|
||||
metadata,
|
||||
content: body,
|
||||
scope,
|
||||
})
|
||||
} catch {
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
return commands
|
||||
}
|
||||
|
||||
export function discoverCommandsSync(): CommandInfo[] {
|
||||
const configDir = getOpenCodeConfigDir({ binary: "opencode" })
|
||||
const userCommandsDir = join(getClaudeConfigDir(), "commands")
|
||||
const projectCommandsDir = join(process.cwd(), ".claude", "commands")
|
||||
const opencodeGlobalDir = join(configDir, "command")
|
||||
const opencodeProjectDir = join(process.cwd(), ".opencode", "command")
|
||||
|
||||
const userCommands = discoverCommandsFromDir(userCommandsDir, "user")
|
||||
const opencodeGlobalCommands = discoverCommandsFromDir(opencodeGlobalDir, "opencode")
|
||||
const projectCommands = discoverCommandsFromDir(projectCommandsDir, "project")
|
||||
const opencodeProjectCommands = discoverCommandsFromDir(opencodeProjectDir, "opencode-project")
|
||||
|
||||
const builtinCommandsMap = loadBuiltinCommands()
|
||||
const builtinCommands: CommandInfo[] = Object.values(builtinCommandsMap).map(cmd => ({
|
||||
name: cmd.name,
|
||||
metadata: {
|
||||
name: cmd.name,
|
||||
description: cmd.description || "",
|
||||
argumentHint: cmd.argumentHint,
|
||||
model: cmd.model,
|
||||
agent: cmd.agent,
|
||||
subtask: cmd.subtask
|
||||
},
|
||||
content: cmd.template,
|
||||
scope: "builtin"
|
||||
}))
|
||||
|
||||
return [...builtinCommands, ...opencodeProjectCommands, ...projectCommands, ...opencodeGlobalCommands, ...userCommands]
|
||||
}
|
||||
|
||||
function skillToCommandInfo(skill: LoadedSkill): CommandInfo {
|
||||
return {
|
||||
name: skill.name,
|
||||
path: skill.path,
|
||||
metadata: {
|
||||
name: skill.name,
|
||||
description: skill.definition.description || "",
|
||||
argumentHint: skill.definition.argumentHint,
|
||||
model: skill.definition.model,
|
||||
agent: skill.definition.agent,
|
||||
subtask: skill.definition.subtask,
|
||||
},
|
||||
content: skill.definition.template,
|
||||
scope: skill.scope,
|
||||
lazyContentLoader: skill.lazyContent,
|
||||
}
|
||||
}
|
||||
|
||||
async function formatLoadedCommand(cmd: CommandInfo, userMessage?: string): Promise<string> {
|
||||
const sections: string[] = []
|
||||
|
||||
sections.push(`# /${cmd.name} Command\n`)
|
||||
|
||||
if (cmd.metadata.description) {
|
||||
sections.push(`**Description**: ${cmd.metadata.description}\n`)
|
||||
}
|
||||
|
||||
if (cmd.metadata.argumentHint) {
|
||||
sections.push(`**Usage**: /${cmd.name} ${cmd.metadata.argumentHint}\n`)
|
||||
}
|
||||
|
||||
if (userMessage) {
|
||||
sections.push(`**Arguments**: ${userMessage}\n`)
|
||||
}
|
||||
|
||||
if (cmd.metadata.model) {
|
||||
sections.push(`**Model**: ${cmd.metadata.model}\n`)
|
||||
}
|
||||
|
||||
if (cmd.metadata.agent) {
|
||||
sections.push(`**Agent**: ${cmd.metadata.agent}\n`)
|
||||
}
|
||||
|
||||
if (cmd.metadata.subtask) {
|
||||
sections.push(`**Subtask**: true\n`)
|
||||
}
|
||||
|
||||
sections.push(`**Scope**: ${cmd.scope}\n`)
|
||||
sections.push("---\n")
|
||||
sections.push("## Command Instructions\n")
|
||||
|
||||
let content = cmd.content || ""
|
||||
if (!content && cmd.lazyContentLoader) {
|
||||
content = await cmd.lazyContentLoader.load()
|
||||
}
|
||||
|
||||
const commandDir = cmd.path ? dirname(cmd.path) : process.cwd()
|
||||
const withFileRefs = await resolveFileReferencesInText(content, commandDir)
|
||||
const resolvedContent = await resolveCommandsInText(withFileRefs)
|
||||
|
||||
// Substitute user_message into content if provided
|
||||
let finalContent = resolvedContent.trim()
|
||||
if (userMessage) {
|
||||
finalContent = finalContent.replace(/\$\{user_message\}/g, userMessage)
|
||||
}
|
||||
|
||||
sections.push(finalContent)
|
||||
|
||||
return sections.join("\n")
|
||||
}
|
||||
|
||||
function formatCommandList(items: CommandInfo[]): string {
|
||||
if (items.length === 0) {
|
||||
return "No commands or skills found."
|
||||
}
|
||||
|
||||
const lines = ["# Available Commands & Skills\n"]
|
||||
|
||||
for (const cmd of items) {
|
||||
const hint = cmd.metadata.argumentHint ? ` ${cmd.metadata.argumentHint}` : ""
|
||||
lines.push(
|
||||
`- **/${cmd.name}${hint}**: ${cmd.metadata.description || "(no description)"} (${cmd.scope})`
|
||||
)
|
||||
}
|
||||
|
||||
lines.push(`\n**Total**: ${items.length} items`)
|
||||
return lines.join("\n")
|
||||
}
|
||||
|
||||
const TOOL_DESCRIPTION_PREFIX = `Load a skill or execute a command to get detailed instructions for a specific task.
|
||||
|
||||
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 command name only: command='publish'
|
||||
- Call with command and arguments: command='publish' user_message='patch'
|
||||
- The tool will return detailed instructions for the command with your arguments substituted.
|
||||
`
|
||||
|
||||
function buildDescriptionFromItems(items: CommandInfo[]): string {
|
||||
const commandListForDescription = items
|
||||
.map((cmd) => {
|
||||
const hint = cmd.metadata.argumentHint ? ` ${cmd.metadata.argumentHint}` : ""
|
||||
return `- /${cmd.name}${hint}: ${cmd.metadata.description} (${cmd.scope})`
|
||||
})
|
||||
.join("\n")
|
||||
|
||||
return `${TOOL_DESCRIPTION_PREFIX}
|
||||
<available_skills>
|
||||
${commandListForDescription}
|
||||
</available_skills>`
|
||||
}
|
||||
|
||||
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<LoadedSkill[]> => {
|
||||
if (cachedSkills) return cachedSkills
|
||||
cachedSkills = await discoverAllSkills()
|
||||
return cachedSkills
|
||||
}
|
||||
|
||||
const getAllItems = async (): Promise<CommandInfo[]> => {
|
||||
const commands = getCommands()
|
||||
const skills = await getSkills()
|
||||
return [...commands, ...skills.map(skillToCommandInfo)]
|
||||
}
|
||||
|
||||
const buildDescription = async (): Promise<string> => {
|
||||
if (cachedDescription) return cachedDescription
|
||||
const allItems = await getAllItems()
|
||||
cachedDescription = buildDescriptionFromItems(allItems)
|
||||
return cachedDescription
|
||||
}
|
||||
|
||||
if (options.commands !== undefined && options.skills !== undefined) {
|
||||
const allItems = [...options.commands, ...options.skills.map(skillToCommandInfo)]
|
||||
cachedDescription = buildDescriptionFromItems(allItems)
|
||||
} else {
|
||||
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 cmdName = args.command.replace(/^\//, "")
|
||||
|
||||
const exactMatch = allItems.find(
|
||||
(cmd) => cmd.name.toLowerCase() === cmdName.toLowerCase()
|
||||
)
|
||||
|
||||
if (exactMatch) {
|
||||
return await formatLoadedCommand(exactMatch, args.user_message)
|
||||
}
|
||||
|
||||
const partialMatches = allItems.filter((cmd) =>
|
||||
cmd.name.toLowerCase().includes(cmdName.toLowerCase())
|
||||
)
|
||||
|
||||
if (partialMatches.length > 0) {
|
||||
const matchList = partialMatches.map((cmd) => `/${cmd.name}`).join(", ")
|
||||
return (
|
||||
`No exact match for "/${cmdName}". Did you mean: ${matchList}?\n\n` +
|
||||
formatCommandList(allItems)
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
`Command or skill "/${cmdName}" not found.\n\n` +
|
||||
formatCommandList(allItems) +
|
||||
"\n\nTry a different name."
|
||||
)
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
// Default instance for backward compatibility (lazy loading)
|
||||
export const slashcommand: ToolDefinition = createSlashcommandTool()
|
||||
export { discoverCommandsSync } from "./command-discovery"
|
||||
export { createSlashcommandTool, slashcommand } from "./slashcommand-tool"
|
||||
|
||||
Reference in New Issue
Block a user