From 118813a412b17f7809fa3db2a87a64cc2d91ad4c Mon Sep 17 00:00:00 2001 From: Gustavo Madeira Santana Date: Fri, 24 Apr 2026 20:23:25 -0400 Subject: [PATCH] fix(plugins): expose channel CLI metadata in discovery --- extensions/matrix/index.test.ts | 31 +++++++ src/plugin-sdk/channel-entry-contract.ts | 4 + src/plugin-sdk/core.ts | 4 + src/plugins/loader.cli-metadata.test.ts | 102 +++++++++++++++++++++++ src/plugins/loader.test-fixtures.ts | 4 + 5 files changed, 145 insertions(+) diff --git a/extensions/matrix/index.test.ts b/extensions/matrix/index.test.ts index f1a9cc70833..c2327759679 100644 --- a/extensions/matrix/index.test.ts +++ b/extensions/matrix/index.test.ts @@ -69,6 +69,37 @@ describe("matrix plugin", () => { expect(entry.setChannelRuntime).toEqual(expect.any(Function)); }); + it("registers CLI metadata during discovery registration", () => { + const registerChannel = vi.fn(); + const registerCli = vi.fn(); + const registerGatewayMethod = vi.fn(); + const api = createTestPluginApi({ + id: "matrix", + name: "Matrix", + source: "test", + config: {}, + runtime: {} as never, + registrationMode: "discovery", + registerChannel, + registerCli, + registerGatewayMethod, + }); + + entry.register(api); + + expect(registerChannel).toHaveBeenCalledTimes(1); + expect(registerCli).toHaveBeenCalledWith(expect.any(Function), { + descriptors: [ + { + name: "matrix", + description: "Manage Matrix accounts, verification, devices, and profile state", + hasSubcommands: true, + }, + ], + }); + expect(registerGatewayMethod).not.toHaveBeenCalled(); + }); + it("registers subagent lifecycle hooks during full runtime registration", () => { const on = vi.fn(); const registerGatewayMethod = vi.fn(); diff --git a/src/plugin-sdk/channel-entry-contract.ts b/src/plugin-sdk/channel-entry-contract.ts index 5f77bd4d9d9..1d795e91db1 100644 --- a/src/plugin-sdk/channel-entry-contract.ts +++ b/src/plugin-sdk/channel-entry-contract.ts @@ -488,6 +488,10 @@ export function defineBundledChannelEntry({ profile("bundled-register:registerChannel", () => api.registerChannel({ plugin: channelPlugin as ChannelPlugin }), ); + if (api.registrationMode === "discovery") { + profile("bundled-register:registerCliMetadata", () => registerCliMetadata?.(api)); + return; + } if (api.registrationMode !== "full") { return; } diff --git a/src/plugin-sdk/core.ts b/src/plugin-sdk/core.ts index 61000fb1285..f1feeb8efb9 100644 --- a/src/plugin-sdk/core.ts +++ b/src/plugin-sdk/core.ts @@ -505,6 +505,10 @@ export function defineChannelPluginEntry({ } setRuntime?.(api.runtime); api.registerChannel({ plugin: plugin as ChannelPlugin }); + if (api.registrationMode === "discovery") { + registerCliMetadata?.(api); + return; + } if (api.registrationMode !== "full") { return; } diff --git a/src/plugins/loader.cli-metadata.test.ts b/src/plugins/loader.cli-metadata.test.ts index c13a705a730..ade86de32bd 100644 --- a/src/plugins/loader.cli-metadata.test.ts +++ b/src/plugins/loader.cli-metadata.test.ts @@ -549,6 +549,108 @@ module.exports = { ); }); + it("collects channel CLI metadata during discovery plugin loads", () => { + useNoBundledPlugins(); + const pluginDir = makeTempDir(); + const modeMarker = path.join(pluginDir, "registration-mode.txt"); + const fullMarker = path.join(pluginDir, "full-loaded.txt"); + + fs.writeFileSync( + path.join(pluginDir, "package.json"), + JSON.stringify( + { + name: "@openclaw/discovery-cli-metadata-channel", + openclaw: { extensions: ["./index.cjs"] }, + }, + null, + 2, + ), + "utf-8", + ); + fs.writeFileSync( + path.join(pluginDir, "openclaw.plugin.json"), + JSON.stringify( + { + id: "discovery-cli-metadata-channel", + configSchema: EMPTY_PLUGIN_SCHEMA, + channels: ["discovery-cli-metadata-channel"], + }, + null, + 2, + ), + "utf-8", + ); + fs.writeFileSync( + path.join(pluginDir, "index.cjs"), + `${inlineChannelPluginEntryFactorySource()} +module.exports = { + ...defineChannelPluginEntry({ + id: "discovery-cli-metadata-channel", + name: "Discovery CLI Metadata Channel", + description: "discovery cli metadata channel", + plugin: { + id: "discovery-cli-metadata-channel", + meta: { + id: "discovery-cli-metadata-channel", + label: "Discovery CLI Metadata Channel", + selectionLabel: "Discovery CLI Metadata Channel", + docsPath: "/channels/discovery-cli-metadata-channel", + blurb: "discovery cli metadata channel", + }, + capabilities: { chatTypes: ["direct"] }, + config: { + listAccountIds: () => [], + resolveAccount: () => ({ accountId: "default" }), + }, + outbound: { deliveryMode: "direct" }, + }, + registerCliMetadata(api) { + require("node:fs").writeFileSync( + ${JSON.stringify(modeMarker)}, + String(api.registrationMode), + "utf-8", + ); + api.registerCli(() => {}, { + descriptors: [ + { + name: "discovery-cli-metadata-channel", + description: "Discovery-load channel CLI metadata", + hasSubcommands: true, + }, + ], + }); + }, + registerFull() { + require("node:fs").writeFileSync(${JSON.stringify(fullMarker)}, "loaded", "utf-8"); + }, + }), +};`, + "utf-8", + ); + + const registry = loadOpenClawPlugins({ + activate: false, + cache: false, + config: { + plugins: { + load: { paths: [pluginDir] }, + allow: ["discovery-cli-metadata-channel"], + entries: { + "discovery-cli-metadata-channel": { + enabled: true, + }, + }, + }, + }, + }); + + expect(fs.readFileSync(modeMarker, "utf-8")).toBe("discovery"); + expect(fs.existsSync(fullMarker)).toBe(false); + expect(registry.cliRegistrars.flatMap((entry) => entry.commands)).toContain( + "discovery-cli-metadata-channel", + ); + }); + it("rejects async plugin registration when collecting CLI metadata", async () => { useNoBundledPlugins(); const plugin = writePlugin({ diff --git a/src/plugins/loader.test-fixtures.ts b/src/plugins/loader.test-fixtures.ts index 49f53dbd507..27204781988 100644 --- a/src/plugins/loader.test-fixtures.ts +++ b/src/plugins/loader.test-fixtures.ts @@ -57,6 +57,10 @@ export function inlineChannelPluginEntryFactorySource(): string { } options.setRuntime?.(api.runtime); api.registerChannel({ plugin: options.plugin }); + if (api.registrationMode === "discovery") { + options.registerCliMetadata?.(api); + return; + } if (api.registrationMode !== "full") { return; }