diff --git a/package.json b/package.json index 9ee2b8e82bd..473a4fcfefe 100644 --- a/package.json +++ b/package.json @@ -430,10 +430,6 @@ "types": "./dist/plugin-sdk/image-generation.d.ts", "default": "./dist/plugin-sdk/image-generation.js" }, - "./plugin-sdk/image-generation-runtime": { - "types": "./dist/plugin-sdk/image-generation-runtime.d.ts", - "default": "./dist/plugin-sdk/image-generation-runtime.js" - }, "./plugin-sdk/reply-history": { "types": "./dist/plugin-sdk/reply-history.d.ts", "default": "./dist/plugin-sdk/reply-history.js" diff --git a/scripts/lib/plugin-sdk-entrypoints.json b/scripts/lib/plugin-sdk-entrypoints.json index d67f48733f5..72de88ed3ca 100644 --- a/scripts/lib/plugin-sdk-entrypoints.json +++ b/scripts/lib/plugin-sdk-entrypoints.json @@ -97,7 +97,6 @@ "provider-usage", "provider-web-search", "image-generation", - "image-generation-runtime", "reply-history", "media-understanding", "google", diff --git a/src/auto-reply/reply/commands-plugins.test.ts b/src/auto-reply/reply/commands-plugins.test.ts index e5764574d29..1bf3feb772b 100644 --- a/src/auto-reply/reply/commands-plugins.test.ts +++ b/src/auto-reply/reply/commands-plugins.test.ts @@ -62,6 +62,20 @@ describe("handleCommands /plugins", () => { expect(showResult.reply?.text).toContain('"id": "superpowers"'); expect(showResult.reply?.text).toContain('"bundleFormat": "claude"'); expect(showResult.reply?.text).toContain('"shape":'); + + const inspectAllParams = buildCommandTestParams( + "/plugins inspect all", + buildCfg(), + undefined, + { + workspaceDir, + }, + ); + inspectAllParams.command.senderIsOwner = true; + const inspectAllResult = await handleCommands(inspectAllParams); + expect(inspectAllResult.reply?.text).toContain("```json"); + expect(inspectAllResult.reply?.text).toContain('"plugin"'); + expect(inspectAllResult.reply?.text).toContain('"superpowers"'); }); }); diff --git a/src/auto-reply/reply/commands-plugins.ts b/src/auto-reply/reply/commands-plugins.ts index 197786479e8..1adbf57e717 100644 --- a/src/auto-reply/reply/commands-plugins.ts +++ b/src/auto-reply/reply/commands-plugins.ts @@ -7,6 +7,7 @@ import type { OpenClawConfig } from "../../config/config.js"; import type { PluginInstallRecord } from "../../config/types.plugins.js"; import type { PluginRecord } from "../../plugins/registry.js"; import { + buildAllPluginInspectReports, buildPluginInspectReport, buildPluginStatusReport, type PluginStatusReport, @@ -48,6 +49,22 @@ function buildPluginInspectJson(params: { }; } +function buildAllPluginInspectJson(params: { + config: OpenClawConfig; + report: PluginStatusReport; +}): Array<{ + inspect: ReturnType[number]; + install: PluginInstallRecord | null; +}> { + return buildAllPluginInspectReports({ + config: params.config, + report: params.report, + }).map((inspect) => ({ + inspect, + install: params.config.plugins?.installs?.[inspect.plugin.id] ?? null, + })); +} + function formatPluginLabel(plugin: PluginRecord): string { if (!plugin.name || plugin.name === plugin.id) { return plugin.id; @@ -164,6 +181,14 @@ export const handlePluginsCommand: CommandHandler = async (params, allowTextComm reply: { text: formatPluginsList(loaded.report) }, }; } + if (pluginsCommand.name.toLowerCase() === "all") { + return { + shouldContinue: false, + reply: { + text: renderJsonBlock("🔌 Plugins", buildAllPluginInspectJson(loaded)), + }, + }; + } const payload = buildPluginInspectJson({ id: pluginsCommand.name, config: loaded.config, diff --git a/src/cli/plugins-cli.ts b/src/cli/plugins-cli.ts index a73defc736b..c91f65c04c7 100644 --- a/src/cli/plugins-cli.ts +++ b/src/cli/plugins-cli.ts @@ -20,7 +20,11 @@ import { import type { PluginRecord } from "../plugins/registry.js"; import { applyExclusiveSlotSelection } from "../plugins/slots.js"; import { resolvePluginSourceRoots, formatPluginSourceForTable } from "../plugins/source-display.js"; -import { buildPluginInspectReport, buildPluginStatusReport } from "../plugins/status.js"; +import { + buildAllPluginInspectReports, + buildPluginInspectReport, + buildPluginStatusReport, +} from "../plugins/status.js"; import { resolveUninstallDirectoryTarget, uninstallPlugin } from "../plugins/uninstall.js"; import { updateNpmInstalledPlugins } from "../plugins/update.js"; import { defaultRuntime } from "../runtime.js"; @@ -45,6 +49,7 @@ export type PluginsListOptions = { export type PluginInspectOptions = { json?: boolean; + all?: boolean; }; export type PluginUpdateOptions = { @@ -141,6 +146,37 @@ function formatInspectSection(title: string, lines: string[]): string[] { return ["", `${theme.muted(`${title}:`)}`, ...lines]; } +function formatCapabilityKinds( + capabilities: Array<{ + kind: string; + }>, +): string { + if (capabilities.length === 0) { + return "-"; + } + return capabilities.map((entry) => entry.kind).join(", "); +} + +function formatHookSummary(params: { + usesLegacyBeforeAgentStart: boolean; + typedHookCount: number; + customHookCount: number; +}): string { + const parts: string[] = []; + if (params.usesLegacyBeforeAgentStart) { + parts.push("before_agent_start"); + } + const nonLegacyTypedHookCount = + params.typedHookCount - (params.usesLegacyBeforeAgentStart ? 1 : 0); + if (nonLegacyTypedHookCount > 0) { + parts.push(`${nonLegacyTypedHookCount} typed`); + } + if (params.customHookCount > 0) { + parts.push(`${params.customHookCount} custom`); + } + return parts.length > 0 ? parts.join(", ") : "-"; +} + function formatInstallLines(install: PluginInstallRecord | undefined): string[] { if (!install) { return []; @@ -576,11 +612,74 @@ export function registerPluginsCli(program: Command) { .command("inspect") .alias("info") .description("Inspect plugin details") - .argument("", "Plugin id") + .argument("[id]", "Plugin id") + .option("--all", "Inspect all plugins") .option("--json", "Print JSON") - .action((id: string, opts: PluginInspectOptions) => { + .action((id: string | undefined, opts: PluginInspectOptions) => { const cfg = loadConfig(); const report = buildPluginStatusReport({ config: cfg }); + if (opts.all) { + if (id) { + defaultRuntime.error("Pass either a plugin id or --all, not both."); + process.exit(1); + } + const inspectAll = buildAllPluginInspectReports({ + config: cfg, + report, + }); + const inspectAllWithInstall = inspectAll.map((inspect) => ({ + ...inspect, + install: cfg.plugins?.installs?.[inspect.plugin.id], + })); + + if (opts.json) { + defaultRuntime.log(JSON.stringify(inspectAllWithInstall, null, 2)); + return; + } + + const tableWidth = getTerminalTableWidth(); + const rows = inspectAll.map((inspect) => ({ + Name: inspect.plugin.name || inspect.plugin.id, + ID: + inspect.plugin.name && inspect.plugin.name !== inspect.plugin.id + ? inspect.plugin.id + : "", + Status: + inspect.plugin.status === "loaded" + ? theme.success("loaded") + : inspect.plugin.status === "disabled" + ? theme.warn("disabled") + : theme.error("error"), + Shape: inspect.shape, + Capabilities: formatCapabilityKinds(inspect.capabilities), + Hooks: formatHookSummary({ + usesLegacyBeforeAgentStart: inspect.usesLegacyBeforeAgentStart, + typedHookCount: inspect.typedHooks.length, + customHookCount: inspect.customHooks.length, + }), + })); + defaultRuntime.log( + renderTable({ + width: tableWidth, + columns: [ + { key: "Name", header: "Name", minWidth: 14, flex: true }, + { key: "ID", header: "ID", minWidth: 10, flex: true }, + { key: "Status", header: "Status", minWidth: 10 }, + { key: "Shape", header: "Shape", minWidth: 18 }, + { key: "Capabilities", header: "Capabilities", minWidth: 28, flex: true }, + { key: "Hooks", header: "Hooks", minWidth: 20, flex: true }, + ], + rows, + }).trimEnd(), + ); + return; + } + + if (!id) { + defaultRuntime.error("Provide a plugin id or use --all."); + process.exit(1); + } + const inspect = buildPluginInspectReport({ id, config: cfg, diff --git a/src/plugins/status.test.ts b/src/plugins/status.test.ts index b6e7aff30c0..d16db23da4b 100644 --- a/src/plugins/status.test.ts +++ b/src/plugins/status.test.ts @@ -4,6 +4,7 @@ const loadConfigMock = vi.fn(); const loadOpenClawPluginsMock = vi.fn(); let buildPluginStatusReport: typeof import("./status.js").buildPluginStatusReport; let buildPluginInspectReport: typeof import("./status.js").buildPluginInspectReport; +let buildAllPluginInspectReports: typeof import("./status.js").buildAllPluginInspectReports; vi.mock("../config/config.js", () => ({ loadConfig: () => loadConfigMock(), @@ -47,7 +48,8 @@ describe("buildPluginStatusReport", () => { services: [], commands: [], }); - ({ buildPluginInspectReport, buildPluginStatusReport } = await import("./status.js")); + ({ buildAllPluginInspectReports, buildPluginInspectReport, buildPluginStatusReport } = + await import("./status.js")); }); it("forwards an explicit env to plugin loading", () => { @@ -156,4 +158,103 @@ describe("buildPluginStatusReport", () => { { level: "warn", pluginId: "google", message: "watch this seam" }, ]); }); + + it("builds inspect reports for every loaded plugin", () => { + loadOpenClawPluginsMock.mockReturnValue({ + plugins: [ + { + id: "lca", + name: "LCA", + description: "Legacy hook plugin", + source: "/tmp/lca/index.ts", + origin: "workspace", + enabled: true, + status: "loaded", + toolNames: [], + hookNames: [], + channelIds: [], + providerIds: [], + speechProviderIds: [], + mediaUnderstandingProviderIds: [], + imageGenerationProviderIds: [], + webSearchProviderIds: [], + gatewayMethods: [], + cliCommands: [], + services: [], + commands: [], + httpRoutes: 0, + hookCount: 1, + configSchema: false, + }, + { + id: "microsoft", + name: "Microsoft", + description: "Hybrid capability plugin", + source: "/tmp/microsoft/index.ts", + origin: "bundled", + enabled: true, + status: "loaded", + toolNames: [], + hookNames: [], + channelIds: [], + providerIds: ["microsoft"], + speechProviderIds: [], + mediaUnderstandingProviderIds: [], + imageGenerationProviderIds: [], + webSearchProviderIds: ["microsoft"], + gatewayMethods: [], + cliCommands: [], + services: [], + commands: [], + httpRoutes: 0, + hookCount: 0, + configSchema: false, + }, + ], + diagnostics: [], + channels: [], + channelSetups: [], + providers: [], + speechProviders: [], + mediaUnderstandingProviders: [], + imageGenerationProviders: [], + webSearchProviders: [], + tools: [], + hooks: [ + { + pluginId: "lca", + events: ["message"], + entry: { + hook: { + name: "legacy", + handler: () => undefined, + }, + }, + }, + ], + typedHooks: [ + { + pluginId: "lca", + hookName: "before_agent_start", + handler: () => undefined, + source: "/tmp/lca/index.ts", + }, + ], + httpRoutes: [], + gatewayHandlers: {}, + cliRegistrars: [], + services: [], + commands: [], + }); + + const inspect = buildAllPluginInspectReports(); + + expect(inspect.map((entry) => entry.plugin.id)).toEqual(["lca", "microsoft"]); + expect(inspect.map((entry) => entry.shape)).toEqual(["hook-only", "hybrid-capability"]); + expect(inspect[0]?.usesLegacyBeforeAgentStart).toBe(true); + expect(inspect[1]?.capabilities.map((entry) => entry.kind)).toEqual([ + "text-inference", + "web-search", + ]); + }); }); diff --git a/src/plugins/status.ts b/src/plugins/status.ts index b85d9e1cd24..09a75e02516 100644 --- a/src/plugins/status.ts +++ b/src/plugins/status.ts @@ -212,3 +212,29 @@ export function buildPluginInspectReport(params: { usesLegacyBeforeAgentStart: typedHooks.some((entry) => entry.name === "before_agent_start"), }; } + +export function buildAllPluginInspectReports(params?: { + config?: ReturnType; + workspaceDir?: string; + env?: NodeJS.ProcessEnv; + report?: PluginStatusReport; +}): PluginInspectReport[] { + const config = params?.config ?? loadConfig(); + const report = + params?.report ?? + buildPluginStatusReport({ + config, + workspaceDir: params?.workspaceDir, + env: params?.env, + }); + + return report.plugins + .map((plugin) => + buildPluginInspectReport({ + id: plugin.id, + config, + report, + }), + ) + .filter((entry): entry is PluginInspectReport => entry !== null); +}