From 0dee4377b8481a9037d182588e72f9a86eb6f297 Mon Sep 17 00:00:00 2001 From: Gershom Rogers Date: Sat, 21 Feb 2026 10:05:50 -0500 Subject: [PATCH] feat(dispatch): wire marketplace plugin commands into slash command dispatch MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Connect the existing plugin loader infrastructure to both slash command dispatch paths (executor and slashcommand tool), enabling namespaced commands like /daplug:run-prompt to resolve and execute. - Add plugin discovery to executor.ts discoverAllCommands() - Add plugin discovery to command-discovery.ts discoverCommandsSync() - Add "plugin" to CommandScope type - Remove blanket colon-rejection error (replaced with standard not-found) - Update slash command regex to accept namespaced commands - Thread claude_code.plugins config toggle through dispatch chain - Add unit tests for plugin command discovery and dispatch Closes #2019 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude Co-Authored-By: Codex --- src/create-hooks.ts | 1 + src/hooks/auto-slash-command/constants.ts | 2 +- src/hooks/auto-slash-command/detector.test.ts | 13 ++ src/hooks/auto-slash-command/executor.test.ts | 168 ++++++++++++++++++ src/hooks/auto-slash-command/executor.ts | 43 ++++- src/hooks/auto-slash-command/hook.ts | 4 + src/plugin/hooks/create-skill-hooks.ts | 18 +- src/plugin/tool-registry.ts | 5 +- src/tools/skill/tools.test.ts | 2 +- src/tools/skill/tools.ts | 8 +- src/tools/skill/types.ts | 4 + .../slashcommand/command-discovery.test.ts | 160 +++++++++++++++++ src/tools/slashcommand/command-discovery.ts | 45 ++++- src/tools/slashcommand/types.ts | 2 +- 14 files changed, 461 insertions(+), 14 deletions(-) create mode 100644 src/hooks/auto-slash-command/executor.test.ts create mode 100644 src/tools/slashcommand/command-discovery.test.ts diff --git a/src/create-hooks.ts b/src/create-hooks.ts index 9972551e8..121b0f53e 100644 --- a/src/create-hooks.ts +++ b/src/create-hooks.ts @@ -51,6 +51,7 @@ export function createHooks(args: { const skill = createSkillHooks({ ctx, + pluginConfig, isHookEnabled, safeHookEnabled, mergedSkills, diff --git a/src/hooks/auto-slash-command/constants.ts b/src/hooks/auto-slash-command/constants.ts index de2a49a7a..a8bdac19e 100644 --- a/src/hooks/auto-slash-command/constants.ts +++ b/src/hooks/auto-slash-command/constants.ts @@ -3,7 +3,7 @@ export const HOOK_NAME = "auto-slash-command" as const export const AUTO_SLASH_COMMAND_TAG_OPEN = "" export const AUTO_SLASH_COMMAND_TAG_CLOSE = "" -export const SLASH_COMMAND_PATTERN = /^\/([a-zA-Z][\w-]*)\s*(.*)/ +export const SLASH_COMMAND_PATTERN = /^\/([a-zA-Z@][\w:@/-]*)\s*(.*)/ export const EXCLUDED_COMMANDS = new Set([ "ralph-loop", diff --git a/src/hooks/auto-slash-command/detector.test.ts b/src/hooks/auto-slash-command/detector.test.ts index ce87c2d9c..36eb8bc6d 100644 --- a/src/hooks/auto-slash-command/detector.test.ts +++ b/src/hooks/auto-slash-command/detector.test.ts @@ -102,6 +102,19 @@ After` expect(result?.args).toBe("project") }) + it("should parse namespaced marketplace commands", () => { + // given a namespaced command + const text = "/daplug:run-prompt build bridge" + + // when parsing + const result = parseSlashCommand(text) + + // then should keep full namespaced command + expect(result).not.toBeNull() + expect(result?.command).toBe("daplug:run-prompt") + expect(result?.args).toBe("build bridge") + }) + it("should return null for non-slash text", () => { // given text without slash const text = "regular text" diff --git a/src/hooks/auto-slash-command/executor.test.ts b/src/hooks/auto-slash-command/executor.test.ts new file mode 100644 index 000000000..979215fae --- /dev/null +++ b/src/hooks/auto-slash-command/executor.test.ts @@ -0,0 +1,168 @@ +import { afterEach, beforeEach, describe, expect, it } from "bun:test" +import { mkdtempSync, mkdirSync, rmSync, writeFileSync } from "node:fs" +import { tmpdir } from "node:os" +import { join } from "node:path" +import { executeSlashCommand } from "./executor" + +const ENV_KEYS = [ + "CLAUDE_CONFIG_DIR", + "CLAUDE_PLUGINS_HOME", + "CLAUDE_SETTINGS_PATH", + "OPENCODE_CONFIG_DIR", +] as const + +type EnvKey = (typeof ENV_KEYS)[number] +type EnvSnapshot = Record + +function writePluginFixture(baseDir: string): void { + const claudeConfigDir = join(baseDir, "claude-config") + const pluginsHome = join(claudeConfigDir, "plugins") + const settingsPath = join(claudeConfigDir, "settings.json") + const opencodeConfigDir = join(baseDir, "opencode-config") + const pluginInstallPath = join(baseDir, "installed-plugins", "daplug") + const pluginKey = "daplug@1.0.0" + + mkdirSync(join(pluginInstallPath, ".claude-plugin"), { recursive: true }) + mkdirSync(join(pluginInstallPath, "commands"), { recursive: true }) + + writeFileSync( + join(pluginInstallPath, ".claude-plugin", "plugin.json"), + JSON.stringify({ name: "daplug", version: "1.0.0" }, null, 2), + ) + writeFileSync( + join(pluginInstallPath, "commands", "run-prompt.md"), + `--- +description: Run prompt from daplug +--- +Execute daplug prompt flow. +`, + ) + + mkdirSync(pluginsHome, { recursive: true }) + writeFileSync( + join(pluginsHome, "installed_plugins.json"), + JSON.stringify( + { + version: 2, + plugins: { + [pluginKey]: [ + { + scope: "user", + installPath: pluginInstallPath, + version: "1.0.0", + installedAt: "2026-01-01T00:00:00.000Z", + lastUpdated: "2026-01-01T00:00:00.000Z", + }, + ], + }, + }, + null, + 2, + ), + ) + + mkdirSync(claudeConfigDir, { recursive: true }) + writeFileSync( + settingsPath, + JSON.stringify( + { + enabledPlugins: { + [pluginKey]: true, + }, + }, + null, + 2, + ), + ) + mkdirSync(opencodeConfigDir, { recursive: true }) + + process.env.CLAUDE_CONFIG_DIR = claudeConfigDir + process.env.CLAUDE_PLUGINS_HOME = pluginsHome + process.env.CLAUDE_SETTINGS_PATH = settingsPath + process.env.OPENCODE_CONFIG_DIR = opencodeConfigDir +} + +describe("auto-slash command executor plugin dispatch", () => { + let tempDir = "" + let envSnapshot: EnvSnapshot + + beforeEach(() => { + tempDir = mkdtempSync(join(tmpdir(), "omo-executor-plugin-test-")) + envSnapshot = { + CLAUDE_CONFIG_DIR: process.env.CLAUDE_CONFIG_DIR, + CLAUDE_PLUGINS_HOME: process.env.CLAUDE_PLUGINS_HOME, + CLAUDE_SETTINGS_PATH: process.env.CLAUDE_SETTINGS_PATH, + OPENCODE_CONFIG_DIR: process.env.OPENCODE_CONFIG_DIR, + } + writePluginFixture(tempDir) + }) + + afterEach(() => { + for (const key of ENV_KEYS) { + const previousValue = envSnapshot[key] + if (previousValue === undefined) { + delete process.env[key] + } else { + process.env[key] = previousValue + } + } + rmSync(tempDir, { recursive: true, force: true }) + }) + + it("resolves marketplace plugin commands when plugin loading is enabled", async () => { + const result = await executeSlashCommand( + { + command: "daplug:run-prompt", + args: "ship it", + raw: "/daplug:run-prompt ship it", + }, + { + skills: [], + pluginsEnabled: true, + }, + ) + + expect(result.success).toBe(true) + expect(result.replacementText).toContain("# /daplug:run-prompt Command") + expect(result.replacementText).toContain("**Scope**: plugin") + }) + + it("excludes marketplace commands when plugins are disabled via config toggle", async () => { + const result = await executeSlashCommand( + { + command: "daplug:run-prompt", + args: "", + raw: "/daplug:run-prompt", + }, + { + skills: [], + pluginsEnabled: false, + }, + ) + + expect(result.success).toBe(false) + expect(result.error).toBe( + 'Command "/daplug:run-prompt" not found. Use the skill tool to list available skills and commands.', + ) + }) + + it("returns standard not-found for unknown namespaced commands", async () => { + const result = await executeSlashCommand( + { + command: "daplug:missing", + args: "", + raw: "/daplug:missing", + }, + { + skills: [], + pluginsEnabled: true, + }, + ) + + expect(result.success).toBe(false) + expect(result.error).toBe( + 'Command "/daplug:missing" not found. Use the skill tool to list available skills and commands.', + ) + expect(result.error).not.toContain("Marketplace plugin commands") + }) +}) diff --git a/src/hooks/auto-slash-command/executor.ts b/src/hooks/auto-slash-command/executor.ts index ffa96be8b..f7c906e20 100644 --- a/src/hooks/auto-slash-command/executor.ts +++ b/src/hooks/auto-slash-command/executor.ts @@ -12,10 +12,15 @@ import { loadBuiltinCommands } from "../../features/builtin-commands" import type { CommandFrontmatter } from "../../features/claude-code-command-loader/types" import { isMarkdownFile } from "../../shared/file-utils" import { discoverAllSkills, type LoadedSkill, type LazyContentLoader } from "../../features/opencode-skill-loader" +import { + discoverInstalledPlugins, + loadPluginCommands, + loadPluginSkillsAsCommands, +} from "../../features/claude-code-plugin-loader" import type { ParsedSlashCommand } from "./types" interface CommandScope { - type: "user" | "project" | "opencode" | "opencode-project" | "skill" | "builtin" + type: "user" | "project" | "opencode" | "opencode-project" | "skill" | "builtin" | "plugin" } interface CommandMetadata { @@ -99,6 +104,36 @@ function skillToCommandInfo(skill: LoadedSkill): CommandInfo { export interface ExecutorOptions { skills?: LoadedSkill[] + pluginsEnabled?: boolean + enabledPluginsOverride?: Record +} + +function discoverPluginCommands(options?: ExecutorOptions): CommandInfo[] { + if (options?.pluginsEnabled === false) { + return [] + } + + const { plugins } = discoverInstalledPlugins({ + enabledPluginsOverride: options?.enabledPluginsOverride, + }) + + const pluginDefinitions = { + ...loadPluginCommands(plugins), + ...loadPluginSkillsAsCommands(plugins), + } + + return Object.entries(pluginDefinitions).map(([name, definition]) => ({ + name, + metadata: { + name, + description: definition.description || "", + model: definition.model, + agent: definition.agent, + subtask: definition.subtask, + }, + content: definition.template, + scope: "plugin", + })) } async function discoverAllCommands(options?: ExecutorOptions): Promise { @@ -128,6 +163,7 @@ async function discoverAllCommands(options?: ExecutorOptions): Promise() export interface AutoSlashCommandHookOptions { skills?: LoadedSkill[] + pluginsEnabled?: boolean + enabledPluginsOverride?: Record } export function createAutoSlashCommandHook(options?: AutoSlashCommandHookOptions) { const executorOptions: ExecutorOptions = { skills: options?.skills, + pluginsEnabled: options?.pluginsEnabled, + enabledPluginsOverride: options?.enabledPluginsOverride, } return { diff --git a/src/plugin/hooks/create-skill-hooks.ts b/src/plugin/hooks/create-skill-hooks.ts index 043a0bbbb..b0514d583 100644 --- a/src/plugin/hooks/create-skill-hooks.ts +++ b/src/plugin/hooks/create-skill-hooks.ts @@ -1,5 +1,5 @@ import type { AvailableSkill } from "../../agents/dynamic-agent-prompt-builder" -import type { HookName } from "../../config" +import type { HookName, OhMyOpenCodeConfig } from "../../config" import type { LoadedSkill } from "../../features/opencode-skill-loader/types" import type { PluginContext } from "../types" @@ -13,12 +13,20 @@ export type SkillHooks = { export function createSkillHooks(args: { ctx: PluginContext + pluginConfig: OhMyOpenCodeConfig isHookEnabled: (hookName: HookName) => boolean safeHookEnabled: boolean mergedSkills: LoadedSkill[] availableSkills: AvailableSkill[] }): SkillHooks { - const { ctx, isHookEnabled, safeHookEnabled, mergedSkills, availableSkills } = args + const { + ctx, + pluginConfig, + isHookEnabled, + safeHookEnabled, + mergedSkills, + availableSkills, + } = args const safeHook = (hookName: HookName, factory: () => T): T | null => safeCreateHook(hookName, factory, { enabled: safeHookEnabled }) @@ -30,7 +38,11 @@ export function createSkillHooks(args: { const autoSlashCommand = isHookEnabled("auto-slash-command") ? safeHook("auto-slash-command", () => - createAutoSlashCommandHook({ skills: mergedSkills })) + createAutoSlashCommandHook({ + skills: mergedSkills, + pluginsEnabled: pluginConfig.claude_code?.plugins ?? true, + enabledPluginsOverride: pluginConfig.claude_code?.plugins_override, + })) : null return { categorySkillReminder, autoSlashCommand } diff --git a/src/plugin/tool-registry.ts b/src/plugin/tool-registry.ts index 21d7901f4..80ee0c576 100644 --- a/src/plugin/tool-registry.ts +++ b/src/plugin/tool-registry.ts @@ -94,7 +94,10 @@ export function createToolRegistry(args: { getSessionID: getSessionIDForMcp, }) - const commands = discoverCommandsSync(ctx.directory) + const commands = discoverCommandsSync(ctx.directory, { + pluginsEnabled: pluginConfig.claude_code?.plugins ?? true, + enabledPluginsOverride: pluginConfig.claude_code?.plugins_override, + }) const skillTool = createSkillTool({ commands, skills: skillContext.mergedSkills, diff --git a/src/tools/skill/tools.test.ts b/src/tools/skill/tools.test.ts index d4f2d01f6..e64a20fb4 100644 --- a/src/tools/skill/tools.test.ts +++ b/src/tools/skill/tools.test.ts @@ -464,7 +464,7 @@ describe("skill tool - ordering and priority", () => { const tool = createSkillTool({ skills, commands }) //#then: should include priority info - expect(tool.description).toContain("Priority: project > user > opencode > builtin") + expect(tool.description).toContain("Priority: project > user > opencode > builtin/plugin") expect(tool.description).toContain("Skills listed before commands") }) diff --git a/src/tools/skill/tools.ts b/src/tools/skill/tools.ts index 044776909..4bdfb42a0 100644 --- a/src/tools/skill/tools.ts +++ b/src/tools/skill/tools.ts @@ -16,6 +16,7 @@ const scopePriority: Record = { user: 3, opencode: 2, "opencode-project": 2, + plugin: 1, config: 1, builtin: 1, } @@ -89,7 +90,7 @@ function formatCombinedDescription(skills: SkillInfo[], commands: CommandInfo[]) } if (allItems.length > 0) { - lines.push(`\n\nPriority: project > user > opencode > builtin | Skills listed before commands\nInvoke via: skill(name="item-name") — omit leading slash for commands.\n${allItems.join("\n")}\n`) + lines.push(`\n\nPriority: project > user > opencode > builtin/plugin | Skills listed before commands\nInvoke via: skill(name="item-name") — omit leading slash for commands.\n${allItems.join("\n")}\n`) } return TOOL_DESCRIPTION_PREFIX + lines.join("") @@ -195,7 +196,10 @@ export function createSkillTool(options: SkillLoadOptions = {}): ToolDefinition const getCommands = (): CommandInfo[] => { if (cachedCommands) return cachedCommands - cachedCommands = discoverCommandsSync() + cachedCommands = discoverCommandsSync(undefined, { + pluginsEnabled: options.pluginsEnabled, + enabledPluginsOverride: options.enabledPluginsOverride, + }) return cachedCommands } diff --git a/src/tools/skill/types.ts b/src/tools/skill/types.ts index 4fd48d6c7..579eb69cc 100644 --- a/src/tools/skill/types.ts +++ b/src/tools/skill/types.ts @@ -33,4 +33,8 @@ export interface SkillLoadOptions { /** Git master configuration for watermark/co-author settings */ gitMasterConfig?: GitMasterConfig disabledSkills?: Set + /** Include Claude marketplace plugin commands in discovery (default: true) */ + pluginsEnabled?: boolean + /** Override plugin enablement from Claude settings by plugin key */ + enabledPluginsOverride?: Record } diff --git a/src/tools/slashcommand/command-discovery.test.ts b/src/tools/slashcommand/command-discovery.test.ts new file mode 100644 index 000000000..05e49ab3d --- /dev/null +++ b/src/tools/slashcommand/command-discovery.test.ts @@ -0,0 +1,160 @@ +import { afterEach, beforeEach, describe, expect, it } from "bun:test" +import { mkdtempSync, mkdirSync, rmSync, writeFileSync } from "node:fs" +import { tmpdir } from "node:os" +import { join } from "node:path" +import { discoverCommandsSync } from "./command-discovery" + +const ENV_KEYS = [ + "CLAUDE_CONFIG_DIR", + "CLAUDE_PLUGINS_HOME", + "CLAUDE_SETTINGS_PATH", + "OPENCODE_CONFIG_DIR", +] as const + +type EnvKey = (typeof ENV_KEYS)[number] +type EnvSnapshot = Record + +function writePluginFixture(baseDir: string): { projectDir: string } { + const projectDir = join(baseDir, "project") + const claudeConfigDir = join(baseDir, "claude-config") + const pluginsHome = join(claudeConfigDir, "plugins") + const settingsPath = join(claudeConfigDir, "settings.json") + const opencodeConfigDir = join(baseDir, "opencode-config") + const pluginInstallPath = join(baseDir, "installed-plugins", "daplug") + const pluginKey = "daplug@1.0.0" + + mkdirSync(projectDir, { recursive: true }) + mkdirSync(join(pluginInstallPath, ".claude-plugin"), { recursive: true }) + mkdirSync(join(pluginInstallPath, "commands"), { recursive: true }) + mkdirSync(join(pluginInstallPath, "skills", "plugin-plan"), { recursive: true }) + + writeFileSync( + join(pluginInstallPath, ".claude-plugin", "plugin.json"), + JSON.stringify({ name: "daplug", version: "1.0.0" }, null, 2), + ) + writeFileSync( + join(pluginInstallPath, "commands", "run-prompt.md"), + `--- +description: Run prompt from daplug +--- +Execute daplug prompt flow. +`, + ) + writeFileSync( + join(pluginInstallPath, "skills", "plugin-plan", "SKILL.md"), + `--- +name: plugin-plan +description: Plan work from daplug skill +--- +Build a plan from plugin skill context. +`, + ) + + mkdirSync(pluginsHome, { recursive: true }) + writeFileSync( + join(pluginsHome, "installed_plugins.json"), + JSON.stringify( + { + version: 2, + plugins: { + [pluginKey]: [ + { + scope: "user", + installPath: pluginInstallPath, + version: "1.0.0", + installedAt: "2026-01-01T00:00:00.000Z", + lastUpdated: "2026-01-01T00:00:00.000Z", + }, + ], + }, + }, + null, + 2, + ), + ) + + mkdirSync(claudeConfigDir, { recursive: true }) + writeFileSync( + settingsPath, + JSON.stringify( + { + enabledPlugins: { + [pluginKey]: true, + }, + }, + null, + 2, + ), + ) + mkdirSync(opencodeConfigDir, { recursive: true }) + + process.env.CLAUDE_CONFIG_DIR = claudeConfigDir + process.env.CLAUDE_PLUGINS_HOME = pluginsHome + process.env.CLAUDE_SETTINGS_PATH = settingsPath + process.env.OPENCODE_CONFIG_DIR = opencodeConfigDir + + return { projectDir } +} + +describe("slashcommand command discovery plugin integration", () => { + let tempDir = "" + let projectDir = "" + let envSnapshot: EnvSnapshot + + beforeEach(() => { + tempDir = mkdtempSync(join(tmpdir(), "omo-command-discovery-test-")) + envSnapshot = { + CLAUDE_CONFIG_DIR: process.env.CLAUDE_CONFIG_DIR, + CLAUDE_PLUGINS_HOME: process.env.CLAUDE_PLUGINS_HOME, + CLAUDE_SETTINGS_PATH: process.env.CLAUDE_SETTINGS_PATH, + OPENCODE_CONFIG_DIR: process.env.OPENCODE_CONFIG_DIR, + } + const setup = writePluginFixture(tempDir) + projectDir = setup.projectDir + }) + + afterEach(() => { + for (const key of ENV_KEYS) { + const previousValue = envSnapshot[key] + if (previousValue === undefined) { + delete process.env[key] + } else { + process.env[key] = previousValue + } + } + rmSync(tempDir, { recursive: true, force: true }) + }) + + it("discovers marketplace plugin commands and skills as command items", () => { + const commands = discoverCommandsSync(projectDir, { pluginsEnabled: true }) + const names = commands.map(command => command.name) + + expect(names).toContain("daplug:run-prompt") + expect(names).toContain("daplug:plugin-plan") + + const pluginCommand = commands.find(command => command.name === "daplug:run-prompt") + const pluginSkill = commands.find(command => command.name === "daplug:plugin-plan") + + expect(pluginCommand?.scope).toBe("plugin") + expect(pluginSkill?.scope).toBe("plugin") + }) + + it("omits marketplace plugin commands when plugins are disabled", () => { + const commands = discoverCommandsSync(projectDir, { pluginsEnabled: false }) + const names = commands.map(command => command.name) + + expect(names).not.toContain("daplug:run-prompt") + expect(names).not.toContain("daplug:plugin-plan") + }) + + it("honors plugins_override by disabling overridden plugin keys", () => { + const commands = discoverCommandsSync(projectDir, { + pluginsEnabled: true, + enabledPluginsOverride: { "daplug@1.0.0": false }, + }) + const names = commands.map(command => command.name) + + expect(names).not.toContain("daplug:run-prompt") + expect(names).not.toContain("daplug:plugin-plan") + }) +}) diff --git a/src/tools/slashcommand/command-discovery.ts b/src/tools/slashcommand/command-discovery.ts index d06990036..57182f020 100644 --- a/src/tools/slashcommand/command-discovery.ts +++ b/src/tools/slashcommand/command-discovery.ts @@ -5,8 +5,18 @@ import type { CommandFrontmatter } from "../../features/claude-code-command-load import { isMarkdownFile } from "../../shared/file-utils" import { getClaudeConfigDir } from "../../shared" import { loadBuiltinCommands } from "../../features/builtin-commands" +import { + discoverInstalledPlugins, + loadPluginCommands, + loadPluginSkillsAsCommands, +} from "../../features/claude-code-plugin-loader" import type { CommandInfo, CommandMetadata, CommandScope } from "./types" +export interface CommandDiscoveryOptions { + pluginsEnabled?: boolean + enabledPluginsOverride?: Record +} + function discoverCommandsFromDir(commandsDir: string, scope: CommandScope): CommandInfo[] { if (!existsSync(commandsDir)) return [] @@ -48,7 +58,38 @@ function discoverCommandsFromDir(commandsDir: string, scope: CommandScope): Comm return commands } -export function discoverCommandsSync(directory?: string): CommandInfo[] { +function discoverPluginCommands(options?: CommandDiscoveryOptions): CommandInfo[] { + if (options?.pluginsEnabled === false) { + return [] + } + + const { plugins } = discoverInstalledPlugins({ + enabledPluginsOverride: options?.enabledPluginsOverride, + }) + + const pluginDefinitions = { + ...loadPluginCommands(plugins), + ...loadPluginSkillsAsCommands(plugins), + } + + return Object.entries(pluginDefinitions).map(([name, definition]) => ({ + name, + metadata: { + name, + description: definition.description || "", + model: definition.model, + agent: definition.agent, + subtask: definition.subtask, + }, + content: definition.template, + scope: "plugin", + })) +} + +export function discoverCommandsSync( + directory?: string, + options?: CommandDiscoveryOptions, +): CommandInfo[] { const configDir = getOpenCodeConfigDir({ binary: "opencode" }) const userCommandsDir = join(getClaudeConfigDir(), "commands") const projectCommandsDir = join(directory ?? process.cwd(), ".claude", "commands") @@ -59,6 +100,7 @@ export function discoverCommandsSync(directory?: string): CommandInfo[] { const opencodeGlobalCommands = discoverCommandsFromDir(opencodeGlobalDir, "opencode") const projectCommands = discoverCommandsFromDir(projectCommandsDir, "project") const opencodeProjectCommands = discoverCommandsFromDir(opencodeProjectDir, "opencode-project") + const pluginCommands = discoverPluginCommands(options) const builtinCommandsMap = loadBuiltinCommands() const builtinCommands: CommandInfo[] = Object.values(builtinCommandsMap).map((command) => ({ @@ -81,5 +123,6 @@ export function discoverCommandsSync(directory?: string): CommandInfo[] { ...opencodeProjectCommands, ...opencodeGlobalCommands, ...builtinCommands, + ...pluginCommands, ] } diff --git a/src/tools/slashcommand/types.ts b/src/tools/slashcommand/types.ts index 090e12178..af3935ef3 100644 --- a/src/tools/slashcommand/types.ts +++ b/src/tools/slashcommand/types.ts @@ -1,6 +1,6 @@ import type { LazyContentLoader } from "../../features/opencode-skill-loader" -export type CommandScope = "builtin" | "config" | "user" | "project" | "opencode" | "opencode-project" +export type CommandScope = "builtin" | "config" | "user" | "project" | "opencode" | "opencode-project" | "plugin" export interface CommandMetadata { name: string