feat(tools): refactor slashcommand to support options and caching

- Extract createSlashcommandTool factory with SlashcommandToolOptions
- Export discoverCommandsSync for external use
- Move description building to lazy evaluation with caching
- Support pre-warming cache with provided commands and skills
- Simplify tool initialization in plugin with new factory approach

This allows the slashcommand tool to be instantiated with custom options
while maintaining backward compatibility through lazy loading.

🤖 Generated with assistance of [OhMyOpenCode](https://github.com/code-yeongyu/oh-my-opencode)
This commit is contained in:
YeonGyu-Kim
2026-01-05 23:45:01 +09:00
parent 898d3e6175
commit 4e5b3566a2
5 changed files with 107 additions and 68 deletions

View File

@@ -53,6 +53,8 @@ import {
createLookAt,
createSkillTool,
createSkillMcpTool,
createSlashcommandTool,
discoverCommandsSync,
sessionExists,
interactive_bash,
startTmuxCheck,
@@ -231,6 +233,12 @@ const OhMyOpenCodePlugin: Plugin = async (ctx) => {
getSessionID: getSessionIDForMcp,
});
const commands = discoverCommandsSync();
const slashcommandTool = createSlashcommandTool({
commands,
skills: mergedSkills,
});
const googleAuthHooks = pluginConfig.google_auth !== false
? await createGoogleAntigravityAuthPlugin(ctx)
: null;
@@ -251,7 +259,8 @@ const OhMyOpenCodePlugin: Plugin = async (ctx) => {
look_at: lookAt,
skill: skillTool,
skill_mcp: skillMcpTool,
interactive_bash, // Always included, handles missing tmux gracefully via getCachedTmuxPath() ?? "tmux"
slashcommand: slashcommandTool,
interactive_bash,
},
"chat.message": async (input, output) => {

View File

@@ -19,7 +19,7 @@ import {
import { grep } from "./grep"
import { glob } from "./glob"
import { slashcommand } from "./slashcommand"
export { createSlashcommandTool, discoverCommandsSync } from "./slashcommand"
import {
session_list,
@@ -73,7 +73,6 @@ export const builtinTools: Record<string, ToolDefinition> = {
ast_grep_replace,
grep,
glob,
slashcommand,
session_list,
session_read,
session_search,

View File

@@ -1,2 +1,2 @@
export * from "./types"
export { slashcommand } from "./tools"
export { slashcommand, createSlashcommandTool, discoverCommandsSync } from "./tools"

View File

@@ -6,7 +6,7 @@ import type { CommandFrontmatter } from "../../features/claude-code-command-load
import { isMarkdownFile } from "../../shared/file-utils"
import { getClaudeConfigDir } from "../../shared"
import { discoverAllSkills, type LoadedSkill } from "../../features/opencode-skill-loader"
import type { CommandScope, CommandMetadata, CommandInfo } from "./types"
import type { CommandScope, CommandMetadata, CommandInfo, SlashcommandToolOptions } from "./types"
function discoverCommandsFromDir(commandsDir: string, scope: CommandScope): CommandInfo[] {
if (!existsSync(commandsDir)) {
@@ -51,7 +51,7 @@ function discoverCommandsFromDir(commandsDir: string, scope: CommandScope): Comm
return commands
}
function discoverCommandsSync(): CommandInfo[] {
export function discoverCommandsSync(): CommandInfo[] {
const { homedir } = require("os")
const userCommandsDir = join(getClaudeConfigDir(), "commands")
const projectCommandsDir = join(process.cwd(), ".claude", "commands")
@@ -138,87 +138,109 @@ function formatCommandList(items: CommandInfo[]): string {
return lines.join("\n")
}
async function buildDescription(): Promise<string> {
const availableCommands = discoverCommandsSync()
const availableSkills = await discoverAllSkills()
const availableItems = [
...availableCommands,
...availableSkills.map(skillToCommandInfo),
]
const commandListForDescription = availableItems
const TOOL_DESCRIPTION_PREFIX = `Load a skill 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.
`
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 `Load a skill 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.
return `${TOOL_DESCRIPTION_PREFIX}
<available_skills>
${commandListForDescription}
</available_skills>`
}
let cachedDescription: string | null = null
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
export const slashcommand: ToolDefinition = tool({
get description() {
if (!cachedDescription) {
cachedDescription = "Loading available commands and skills..."
buildDescription().then(desc => { cachedDescription = desc })
}
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
},
}
args: {
command: tool.schema
.string()
.describe(
"The slash command to execute (without the leading slash). E.g., 'commit', 'plan', 'execute'."
),
},
// Pre-warm the cache immediately
buildDescription()
async execute(args) {
const commands = discoverCommandsSync()
const skills = await discoverAllSkills()
const allItems = [
...commands,
...skills.map(skillToCommandInfo),
]
return tool({
get description() {
return cachedDescription ?? TOOL_DESCRIPTION_PREFIX
},
if (!args.command) {
return formatCommandList(allItems) + "\n\nProvide a command or skill name to execute."
}
args: {
command: tool.schema
.string()
.describe(
"The slash command to execute (without the leading slash). E.g., 'commit', 'plan', 'execute'."
),
},
const cmdName = args.command.replace(/^\//, "")
async execute(args) {
const allItems = await getAllItems()
const exactMatch = allItems.find(
(cmd) => cmd.name.toLowerCase() === cmdName.toLowerCase()
)
if (!args.command) {
return formatCommandList(allItems) + "\n\nProvide a command or skill name to execute."
}
if (exactMatch) {
return await formatLoadedCommand(exactMatch)
}
const cmdName = args.command.replace(/^\//, "")
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)
const exactMatch = allItems.find(
(cmd) => cmd.name.toLowerCase() === cmdName.toLowerCase()
)
}
return (
`Command or skill "/${cmdName}" not found.\n\n` +
formatCommandList(allItems) +
"\n\nTry a different name."
)
},
})
if (exactMatch) {
return await formatLoadedCommand(exactMatch)
}
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 = createSlashcommandTool()

View File

@@ -1,3 +1,5 @@
import type { LoadedSkill } from "../../features/opencode-skill-loader"
export type CommandScope = "builtin" | "config" | "user" | "project" | "opencode" | "opencode-project"
export interface CommandMetadata {
@@ -16,3 +18,10 @@ export interface CommandInfo {
content?: string
scope: CommandScope
}
export interface SlashcommandToolOptions {
/** Pre-loaded commands (skip discovery if provided) */
commands?: CommandInfo[]
/** Pre-loaded skills (skip discovery if provided) */
skills?: LoadedSkill[]
}