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:
11
src/index.ts
11
src/index.ts
@@ -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) => {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -1,2 +1,2 @@
|
||||
export * from "./types"
|
||||
export { slashcommand } from "./tools"
|
||||
export { slashcommand, createSlashcommandTool, discoverCommandsSync } from "./tools"
|
||||
|
||||
@@ -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,39 +138,62 @@ 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>`
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
// Pre-warm the cache immediately
|
||||
buildDescription()
|
||||
|
||||
return tool({
|
||||
get description() {
|
||||
return cachedDescription ?? TOOL_DESCRIPTION_PREFIX
|
||||
},
|
||||
|
||||
args: {
|
||||
@@ -182,12 +205,7 @@ export const slashcommand: ToolDefinition = tool({
|
||||
},
|
||||
|
||||
async execute(args) {
|
||||
const commands = discoverCommandsSync()
|
||||
const skills = await discoverAllSkills()
|
||||
const allItems = [
|
||||
...commands,
|
||||
...skills.map(skillToCommandInfo),
|
||||
]
|
||||
const allItems = await getAllItems()
|
||||
|
||||
if (!args.command) {
|
||||
return formatCommandList(allItems) + "\n\nProvide a command or skill name to execute."
|
||||
@@ -222,3 +240,7 @@ export const slashcommand: ToolDefinition = tool({
|
||||
)
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
// Default instance for backward compatibility (lazy loading)
|
||||
export const slashcommand = createSlashcommandTool()
|
||||
|
||||
@@ -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[]
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user