import { clearSubmittedText } from "./helpers/draft"; import { reset as resetCapabilities } from "../capability"; import { clearPluginRootsAndCaches, resolveActiveProjectRegistryPath, resolveOrDefaultProjectRegistryPath, } from "../discovery/helpers.js"; import { PluginManager } from "../extensibility/plugins"; import { setInstalledPluginEnabled } from "../extensibility/plugins/toggle"; import { getInstalledPluginsRegistryPath, getMarketplacesCacheDir, getMarketplacesRegistryPath, getPluginsCacheDir, MarketplaceManager, } from "../extensibility/plugins/marketplace"; import { MCPCommandController } from "../modes/controllers/mcp-command-controller"; import type { InteractiveModeContext } from "../modes/types"; import { refreshAgentDiscovery } from "../task"; import { createMarketplaceManager } from "./helpers/marketplace-manager"; import { commandConsumed, errorMessage, parseSubcommand, usage } from "./helpers/parse"; import { parseMarketplaceInstallArgs, parsePluginScopeArgs } from "./marketplace-install-parser"; import type { SlashCommandSpec } from "./types"; /** * Reload the interactive session's plugin runtime: invalidate fs/plugin-root * caches, rediscover skills, file slash commands, and task agents, reset the * capability cache, and reconnect MCP servers (rebinding the session's MCP * tools). Shared by `/reload-plugins`'s TUI handler and the `handle`-adapter's * `reloadPlugins` hook so both honor the command's documented reload scope. */ export async function reloadTuiPluginState(ctx: InteractiveModeContext): Promise { const projectPath = await resolveActiveProjectRegistryPath(ctx.sessionManager.getCwd()); clearPluginRootsAndCaches(projectPath ? [projectPath] : undefined); await refreshAgentDiscovery(ctx.sessionManager.getCwd(), ctx.session.effectiveExtensionRoots); await ctx.refreshSkillState(); await ctx.refreshSlashCommandState(); resetCapabilities(); if (ctx.mcpManager) { await new MCPCommandController(ctx).reloadServers(); } } export const BUILTIN_MARKETPLACE_SLASH_COMMANDS: ReadonlyArray = [ { name: "marketplace", icon: "cart", description: "Manage marketplace plugin sources and installed plugins", acpDescription: "Manage plugins from marketplaces", acpInputHint: "", subcommands: [ { name: "add", description: "Add a marketplace source", usage: "" }, { name: "remove", description: "Remove a marketplace source", usage: "" }, { name: "update", description: "Update marketplace catalog(s)", usage: "[name]" }, { name: "list", description: "List configured marketplaces" }, { name: "discover", description: "Browse available plugins", usage: "[marketplace]" }, { name: "install", description: "Install a plugin (interactive browser if no args)", usage: "[--force] [name@marketplace]", }, { name: "uninstall", description: "Uninstall a plugin (selector if no args)", usage: "[name@marketplace]" }, { name: "installed", description: "List installed marketplace plugins" }, { name: "upgrade", description: "Upgrade outdated plugins", usage: "[name@marketplace]" }, { name: "help", description: "Show usage guide" }, ], allowArgs: true, handle: async (command, runtime) => { const { verb, rest } = parseSubcommand(command.args); if (!verb) { try { const manager = await createMarketplaceManager(runtime); const marketplaces = await manager.listMarketplaces(); if (marketplaces.length === 0) { await runtime.output( "No marketplaces configured.\n\nGet started:\n /marketplace add anthropics/claude-plugins-official\n\nThen browse with /marketplace discover", ); } else { const lines = marketplaces.map(m => ` ${m.name} ${m.sourceUri}`); await runtime.output( `Marketplaces:\n${lines.join("\n")}\n\nUse /marketplace discover to browse plugins, or /marketplace help for all commands`, ); } return commandConsumed(); } catch (err) { return usage(`Marketplace error: ${errorMessage(err)}`, runtime); } } if (verb === "help") { await runtime.output( [ "Marketplace commands:", " /marketplace List configured marketplaces", " /marketplace add Add a marketplace (e.g. owner/repo)", " /marketplace remove Remove a marketplace", " /marketplace update [name] Re-fetch catalog(s)", " /marketplace list List configured marketplaces", " /marketplace discover [marketplace] Browse available plugins", " /marketplace install Install a plugin", " /marketplace uninstall Uninstall a plugin", " /marketplace installed List installed plugins", " /marketplace upgrade [name@marketplace] Upgrade plugin(s)", "", "Quick start:", " /marketplace add anthropics/claude-plugins-official", ].join("\n"), ); return commandConsumed(); } if ((verb === "install" || verb === "uninstall") && !rest) { return usage( "Interactive plugin pickers are TUI-only. Pass an explicit name@marketplace argument.", runtime, ); } try { const manager = await createMarketplaceManager(runtime); switch (verb) { case "add": { if (!rest) return usage("Usage: /marketplace add ", runtime); const entry = await manager.addMarketplace(rest); await runtime.output(`Added marketplace: ${entry.name}`); return commandConsumed(); } case "remove": case "rm": { if (!rest) return usage("Usage: /marketplace remove ", runtime); await manager.removeMarketplace(rest); await runtime.output(`Removed marketplace: ${rest}`); return commandConsumed(); } case "update": { if (rest) { await manager.updateMarketplace(rest); await runtime.output(`Updated marketplace: ${rest}`); } else { const results = await manager.updateAllMarketplaces(); await runtime.output(`Updated ${results.length} marketplace(s)`); } return commandConsumed(); } case "list": { const marketplaces = await manager.listMarketplaces(); if (marketplaces.length === 0) { await runtime.output("No marketplaces configured."); } else { const lines = marketplaces.map(m => ` ${m.name} ${m.sourceUri}`); await runtime.output(`Marketplaces:\n${lines.join("\n")}`); } return commandConsumed(); } case "discover": { const plugins = await manager.listAvailablePlugins(rest || undefined); if (plugins.length === 0) { const marketplaces = await manager.listMarketplaces(); await runtime.output( marketplaces.length === 0 ? "No marketplaces configured. Try:\n /marketplace add anthropics/claude-plugins-official" : "No plugins available in configured marketplaces", ); return commandConsumed(); } const lines = ["Available plugins:"]; for (const plugin of plugins) { lines.push(` - ${plugin.name}${plugin.version ? `@${plugin.version}` : ""}`); if (plugin.description) lines.push(` ${plugin.description}`); } await runtime.output(lines.join("\n")); return commandConsumed(); } case "install": { const parsed = parseMarketplaceInstallArgs(rest); if ("error" in parsed) return usage(parsed.error, runtime); const atIndex = parsed.installSpec.lastIndexOf("@"); const pluginName = parsed.installSpec.slice(0, atIndex); const marketplace = parsed.installSpec.slice(atIndex + 1); await manager.installPlugin(pluginName, marketplace, { force: parsed.force, scope: parsed.scope }); await runtime.reloadPlugins(); await runtime.output(`Installed ${pluginName} from ${marketplace}`); return commandConsumed(); } case "uninstall": { const parsed = parsePluginScopeArgs( rest, "Usage: /marketplace uninstall [--scope user|project] ", ); if ("error" in parsed) return usage(parsed.error, runtime); await manager.uninstallPlugin(parsed.pluginId, parsed.scope); await runtime.reloadPlugins(); await runtime.output(`Uninstalled ${parsed.pluginId}`); return commandConsumed(); } case "installed": { const installed = await manager.listInstalledPlugins(); if (installed.length === 0) { await runtime.output("No marketplace plugins installed"); } else { const lines = installed.map( p => ` ${p.id} [${p.scope}]${p.shadowedBy ? " [shadowed]" : ""} (${p.entries.length} entry)`, ); await runtime.output(`Installed plugins:\n${lines.join("\n")}`); } return commandConsumed(); } case "upgrade": { if (rest) { const parsed = parsePluginScopeArgs( rest, "Usage: /marketplace upgrade [--scope user|project] ", ); if ("error" in parsed) return usage(parsed.error, runtime); const result = await manager.upgradePlugin(parsed.pluginId, parsed.scope); await runtime.reloadPlugins(); await runtime.output(`Upgraded ${parsed.pluginId} to ${result.version}`); return commandConsumed(); } const results = await manager.upgradeAllPlugins(); if (results.length === 0) { await runtime.output("All marketplace plugins are up to date"); } else { await runtime.reloadPlugins(); const lines = results.map(r => ` ${r.pluginId}: ${r.from} -> ${r.to}`); await runtime.output(`Upgraded ${results.length} plugin(s):\n${lines.join("\n")}`); } return commandConsumed(); } default: return usage( `Unknown /marketplace subcommand: ${verb}. Use /marketplace help for available commands.`, runtime, ); } } catch (err) { return usage(`Marketplace error: ${errorMessage(err)}`, runtime); } }, handleTui: async (command, runtime) => { clearSubmittedText(runtime); const args = command.args.trim().split(/\s+/); const sub = args[0] || "install"; const rest = args.slice(1).join(" ").trim(); // /marketplace (no args) or /marketplace install (no args) → interactive browser if ((sub === "install" && !rest) || (!args[0] && !command.args.trim())) { try { runtime.ctx.showPluginSelector("install"); } catch (err) { runtime.ctx.showStatus(`Marketplace error: ${err}`); } return; } const mgr = new MarketplaceManager({ marketplacesRegistryPath: getMarketplacesRegistryPath(), installedRegistryPath: getInstalledPluginsRegistryPath(), projectInstalledRegistryPath: await resolveOrDefaultProjectRegistryPath( runtime.ctx.sessionManager.getCwd(), ), marketplacesCacheDir: getMarketplacesCacheDir(), pluginsCacheDir: getPluginsCacheDir(), clearPluginRootsCache: clearPluginRootsAndCaches, }); try { switch (sub) { case "add": { if (!rest) { runtime.ctx.showStatus("Usage: /marketplace add "); return; } const entry = await mgr.addMarketplace(rest); runtime.ctx.showStatus(`Added marketplace: ${entry.name}`); break; } case "remove": case "rm": { if (!rest) { runtime.ctx.showStatus("Usage: /marketplace remove "); return; } await mgr.removeMarketplace(rest); runtime.ctx.showStatus(`Removed marketplace: ${rest}`); break; } case "update": { if (rest) { await mgr.updateMarketplace(rest); runtime.ctx.showStatus(`Updated marketplace: ${rest}`); } else { const results = await mgr.updateAllMarketplaces(); runtime.ctx.showStatus(`Updated ${results.length} marketplace(s)`); } break; } case "discover": { const plugins = await mgr.listAvailablePlugins(rest || undefined); if (plugins.length === 0) { const marketplaces = await mgr.listMarketplaces(); if (marketplaces.length === 0) { runtime.ctx.showStatus( "No marketplaces configured. Try:\n /marketplace add anthropics/claude-plugins-official", ); } else { runtime.ctx.showStatus("No plugins available in configured marketplaces"); } } else { const lines = plugins.map( p => ` ${p.name}${p.version ? `@${p.version}` : ""}${p.description ? ` - ${p.description}` : ""}`, ); runtime.ctx.showStatus(`Available plugins:\n${lines.join("\n")}`); } break; } case "install": { // Parse: /marketplace install [--force] [--scope user|project] name@marketplace const parsed = parseMarketplaceInstallArgs(rest); if ("error" in parsed) { runtime.ctx.showStatus(parsed.error); return; } const atIdx = parsed.installSpec.lastIndexOf("@"); const name = parsed.installSpec.slice(0, atIdx); const marketplace = parsed.installSpec.slice(atIdx + 1); await mgr.installPlugin(name, marketplace, { force: parsed.force, scope: parsed.scope }); runtime.ctx.showStatus(`Installed ${name} from ${marketplace}`); break; } case "uninstall": { if (!rest) { // No args → open interactive uninstall selector runtime.ctx.showPluginSelector("uninstall"); return; } const uninstArgs = parsePluginScopeArgs( rest, "Usage: /marketplace uninstall [--scope user|project] ", ); if ("error" in uninstArgs) { runtime.ctx.showStatus(uninstArgs.error); return; } await mgr.uninstallPlugin(uninstArgs.pluginId, uninstArgs.scope); runtime.ctx.showStatus(`Uninstalled ${uninstArgs.pluginId}`); break; } case "installed": { const installed = await mgr.listInstalledPlugins(); if (installed.length === 0) { runtime.ctx.showStatus("No marketplace plugins installed"); } else { const lines = installed.map( p => ` ${p.id} [${p.scope}]${p.shadowedBy ? " [shadowed]" : ""} (${p.entries.length} entry)`, ); runtime.ctx.showStatus(`Installed plugins:\n${lines.join("\n")}`); } break; } case "upgrade": { if (rest) { const upArgs = parsePluginScopeArgs( rest, "Usage: /marketplace upgrade [--scope user|project] ", ); if ("error" in upArgs) { runtime.ctx.showStatus(upArgs.error); return; } const result = await mgr.upgradePlugin(upArgs.pluginId, upArgs.scope); runtime.ctx.showStatus(`Upgraded ${upArgs.pluginId} to ${result.version}`); } else { const results = await mgr.upgradeAllPlugins(); if (results.length === 0) { runtime.ctx.showStatus("All marketplace plugins are up to date"); } else { const lines = results.map(r => ` ${r.pluginId}: ${r.from} -> ${r.to}`); runtime.ctx.showStatus(`Upgraded ${results.length} plugin(s):\n${lines.join("\n")}`); } } break; } case "help": { runtime.ctx.showStatus( [ "Marketplace commands:", " /marketplace Browse and install plugins", " /marketplace add Add a marketplace (e.g. owner/repo)", " /marketplace remove Remove a marketplace", " /marketplace update [name] Re-fetch catalog(s)", " /marketplace list List configured marketplaces", " /marketplace discover [marketplace] Browse available plugins", " /marketplace install Install a plugin", " /marketplace uninstall Uninstall a plugin", " /marketplace installed List installed plugins", " /marketplace upgrade [name@marketplace] Upgrade plugin(s)", "", "Quick start:", " /marketplace add anthropics/claude-plugins-official", " /marketplace (opens interactive browser)", ].join("\n"), ); break; } default: { const marketplaces = await mgr.listMarketplaces(); if (marketplaces.length === 0) { runtime.ctx.showStatus( "No marketplaces configured.\n\nGet started:\n /marketplace add anthropics/claude-plugins-official\n\nThen browse plugins with /marketplace or /marketplace discover", ); } else { const lines = marketplaces.map(m => ` ${m.name} ${m.sourceUri}`); runtime.ctx.showStatus( `Marketplaces:\n${lines.join("\n")}\n\nUse /marketplace discover to browse plugins, or /marketplace help for all commands`, ); } break; } } } catch (err) { const msg = err instanceof Error ? err.message : String(err); runtime.ctx.showStatus(`Marketplace error: ${msg}`); } }, }, { name: "plugins", aliases: ["plugin"], icon: "package", description: "View and manage installed plugins", acpDescription: "Manage plugins", acpInputHint: "[list|enable|disable]", subcommands: [ { name: "list", description: "List all installed plugins (npm + marketplace)" }, { name: "enable", description: "Enable an installed plugin", usage: "" }, { name: "disable", description: "Disable an installed plugin", usage: "" }, ], allowArgs: true, handle: async (command, runtime) => { const { verb, rest } = parseSubcommand(command.args); try { if (verb === "enable" || verb === "disable") { const parsed = parsePluginScopeArgs(rest, `Usage: /plugins ${verb} [--scope user|project] `); if ("error" in parsed) return usage(parsed.error, runtime); const marketplace = await createMarketplaceManager(runtime); const marketplaceIds = new Set((await marketplace.listInstalledPlugins()).map(plugin => plugin.id)); await setInstalledPluginEnabled(parsed.pluginId, { enabled: verb === "enable", scope: parsed.scope, marketplaceIds, marketplace, npm: new PluginManager(), }); await runtime.reloadPlugins(); await runtime.output(`${verb === "enable" ? "Enabled" : "Disabled"} ${parsed.pluginId}`); return commandConsumed(); } // Default: list const lines: string[] = []; const npmManager = new PluginManager(); const npmPlugins = await npmManager.list(); if (npmPlugins.length > 0) { lines.push("npm plugins:"); for (const plugin of npmPlugins) { const status = plugin.enabled === false ? " (disabled)" : ""; lines.push(` ${plugin.name}@${plugin.version}${status}`); } } const marketplaceManager = await createMarketplaceManager(runtime); const marketplacePlugins = await marketplaceManager.listInstalledPlugins(); if (marketplacePlugins.length > 0) { if (lines.length > 0) lines.push(""); lines.push("marketplace plugins:"); for (const plugin of marketplacePlugins) { const entry = plugin.entries[0]; const status = entry?.enabled === false ? " (disabled)" : ""; const shadowed = plugin.shadowedBy ? " [shadowed]" : ""; lines.push(` ${plugin.id} v${entry?.version ?? "?"}${status} [${plugin.scope}]${shadowed}`); } } await runtime.output(lines.length === 0 ? "No plugins installed" : lines.join("\n")); return commandConsumed(); } catch (err) { return usage(`Plugin error: ${errorMessage(err)}`, runtime); } }, }, { name: "reload-plugins", icon: "restart", description: "Reload all plugins (skills, commands, hooks, tools, agents, MCP)", acpDescription: "Reload all plugins", handle: async (_command, runtime) => { await runtime.reloadPlugins(); await runtime.output("Plugins reloaded."); return commandConsumed(); }, handleTui: async (_command, runtime) => { await reloadTuiPluginState(runtime.ctx); runtime.ctx.showStatus("Plugins reloaded."); clearSubmittedText(runtime); }, }, ];