From 360955a7c8ef9754e27241c1dfab3adfc91dfdd8 Mon Sep 17 00:00:00 2001 From: Frank Yang Date: Fri, 10 Apr 2026 15:35:05 +0800 Subject: [PATCH] fix: preserve commands.list metadata (#64147) Merged via squash. Reviewed-by: @frankekn --- CHANGELOG.md | 1 + src/auto-reply/commands-registry.test.ts | 3 ++ src/auto-reply/commands-registry.ts | 1 + src/gateway/server-methods/commands.test.ts | 25 ++++++++- src/gateway/server-methods/commands.ts | 56 +++++++++++---------- 5 files changed, 58 insertions(+), 28 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index bbb7d26f85c..a9b276fe631 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -69,6 +69,7 @@ Docs: https://docs.openclaw.ai - UI/compaction: keep the compaction indicator in a retry-pending state until the run actually finishes, so the UI does not show `Context compacted` before compaction actually finishes. (#55132) Thanks @mpz4life. - Cron/tool schemas: keep cron tool schemas strict-model-friendly while still preserving `failureAlert=false`, nullable `agentId`/`sessionKey`, and flattened add/update recovery for the newly exposed cron job fields. (#55043) Thanks @brunolorente. - Git metadata: read commit ids from packed refs as well as loose refs so version and status metadata stay accurate after repository maintenance. (#63943) +- Gateway: keep `commands.list` skill entries categorized under tools and include provider-aware plugin `nativeName` metadata even when `scope=text`, so remote clients can group skills correctly and map text-surface plugin commands back to native aliases. ## 2026.4.9 diff --git a/src/auto-reply/commands-registry.test.ts b/src/auto-reply/commands-registry.test.ts index 653c06eed32..989c835f959 100644 --- a/src/auto-reply/commands-registry.test.ts +++ b/src/auto-reply/commands-registry.test.ts @@ -100,6 +100,9 @@ describe("commands registry", () => { { skillCommands }, ); expect(commands.find((spec) => spec.nativeName === "demo_skill")).toBeTruthy(); + expect(commands.find((spec) => spec.nativeName === "demo_skill")).toMatchObject({ + category: "tools", + }); const native = listNativeCommandSpecsForConfig( { commands: { config: false, plugins: false, debug: false, native: true } }, diff --git a/src/auto-reply/commands-registry.ts b/src/auto-reply/commands-registry.ts index d767f708c45..7ea6ea4f5ad 100644 --- a/src/auto-reply/commands-registry.ts +++ b/src/auto-reply/commands-registry.ts @@ -88,6 +88,7 @@ function buildSkillCommandDefinitions(skillCommands?: SkillCommandSpec[]): ChatC acceptsArgs: true, argsParsing: "none", scope: "both", + category: "tools", })); } diff --git a/src/gateway/server-methods/commands.test.ts b/src/gateway/server-methods/commands.test.ts index b0da86990bd..f47a54eaa71 100644 --- a/src/gateway/server-methods/commands.test.ts +++ b/src/gateway/server-methods/commands.test.ts @@ -299,12 +299,35 @@ describe("commands.list handler", () => { it("keeps plugin text commands visible for scope=text even without native provider support", () => { const { payload } = callHandler({ provider: "whatsapp", scope: "text" }); const { commands } = payload as { - commands: Array<{ name: string; source: string; textAliases?: string[] }>; + commands: Array<{ + name: string; + source: string; + textAliases?: string[]; + nativeName?: string; + }>; }; expect(commands.find((c) => c.source === "plugin")).toMatchObject({ name: "tts", textAliases: ["/tts"], }); + expect(commands.find((c) => c.source === "plugin")?.nativeName).toBeUndefined(); + }); + + it("keeps plugin text names while exposing provider-native aliases for scope=text", () => { + const { payload } = callHandler({ provider: "discord", scope: "text" }); + const { commands } = payload as { + commands: Array<{ + name: string; + source: string; + textAliases?: string[]; + nativeName?: string; + }>; + }; + expect(commands.find((c) => c.source === "plugin")).toMatchObject({ + name: "tts", + nativeName: "discord_tts", + textAliases: ["/tts"], + }); }); it("returns provider-specific plugin command names", () => { diff --git a/src/gateway/server-methods/commands.ts b/src/gateway/server-methods/commands.ts index 76aafcc3aa4..641c3515e54 100644 --- a/src/gateway/server-methods/commands.ts +++ b/src/gateway/server-methods/commands.ts @@ -120,6 +120,34 @@ function mapCommand( }; } +function buildPluginCommandEntries(params: { + provider?: string; + nameSurface: CommandNameSurface; +}): CommandEntry[] { + const pluginTextSpecs = listPluginCommands(); + const pluginNativeSpecs = getPluginCommandSpecs(params.provider); + const entries: CommandEntry[] = []; + + for (const [index, textSpec] of pluginTextSpecs.entries()) { + const nativeSpec = pluginNativeSpecs[index]; + const nativeName = nativeSpec?.name; + entries.push({ + name: params.nameSurface === "text" ? textSpec.name : (nativeName ?? textSpec.name), + ...(nativeName ? { nativeName } : {}), + textAliases: [`/${textSpec.name}`], + description: textSpec.description, + source: "plugin", + scope: "both", + acceptsArgs: textSpec.acceptsArgs, + }); + } + + if (params.nameSurface === "native") { + return entries.filter((entry) => entry.nativeName); + } + return entries; +} + export function buildCommandsListResult(params: { cfg: ReturnType; agentId: string; @@ -153,33 +181,7 @@ export function buildCommandsListResult(params: { ); } - if (nameSurface === "text") { - for (const spec of listPluginCommands()) { - commands.push({ - name: spec.name, - textAliases: [`/${spec.name}`], - description: spec.description, - source: "plugin", - scope: "both", - acceptsArgs: spec.acceptsArgs, - }); - } - } else { - const pluginTextSpecs = listPluginCommands(); - const pluginSpecs = getPluginCommandSpecs(provider); - for (const [index, spec] of pluginSpecs.entries()) { - const textName = pluginTextSpecs[index]?.name ?? spec.name; - commands.push({ - name: spec.name, - nativeName: spec.name, - textAliases: [`/${textName}`], - description: spec.description, - source: "plugin", - scope: "both", - acceptsArgs: spec.acceptsArgs, - }); - } - } + commands.push(...buildPluginCommandEntries({ provider, nameSurface })); return { commands }; }