From feb7216b0fa2257789ce8b8fa71627aa33c03d9e Mon Sep 17 00:00:00 2001 From: Gustavo Madeira Santana Date: Mon, 2 Mar 2026 19:14:28 -0500 Subject: [PATCH] CLI: fix root-option parsing for routed config paths --- CHANGELOG.md | 1 + src/cli/program/routes.test.ts | 39 +++++++++++++++++++++++++++++++++- src/cli/program/routes.ts | 30 +++++++++++++++++++++++++- 3 files changed, 68 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 168ed898a6d..234b8d1b06a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -29,6 +29,7 @@ Docs: https://docs.openclaw.ai ### Fixes - Gateway/Control UI basePath webhook passthrough: let non-read methods under configured `controlUiBasePath` fall through to plugin routes (instead of returning Control UI 405), restoring webhook handlers behind basePath mounts. (#32311) Thanks @ademczuk. +- CLI/route-first config path parsing: ignore split root-option values when extracting routed `config get/unset` positional args so invocations like `openclaw --log-level debug config get ` and `--profile config unset ` resolve the intended config key. (#32050) thanks @gumadeiras. - Voice-call/Twilio signature verification: retry signature validation across deterministic URL port variants (with/without port) to handle mixed Twilio signing behavior behind reverse proxies and non-standard ports. (#25140) Thanks @drvoss. - Hooks/webhook ACK compatibility: return `200` (instead of `202`) for successful `/hooks/agent` requests so providers that require `200` (for example Forward Email) accept dispatched agent hook deliveries. (#28204) Thanks @Glucksberg. - Voice-call/Twilio external outbound: auto-register webhook-first `outbound-api` calls (initiated outside OpenClaw) so media streams are accepted and call direction metadata stays accurate. (#31181) Thanks @scoootscooob. diff --git a/src/cli/program/routes.test.ts b/src/cli/program/routes.test.ts index eb4b7351c59..bd476e26b94 100644 --- a/src/cli/program/routes.test.ts +++ b/src/cli/program/routes.test.ts @@ -1,7 +1,19 @@ -import { describe, expect, it } from "vitest"; +import { beforeEach, describe, expect, it, vi } from "vitest"; import { findRoutedCommand } from "./routes.js"; +const runConfigGetMock = vi.hoisted(() => vi.fn(async () => {})); +const runConfigUnsetMock = vi.hoisted(() => vi.fn(async () => {})); + +vi.mock("../config-cli.js", () => ({ + runConfigGet: runConfigGetMock, + runConfigUnset: runConfigUnsetMock, +})); + describe("program routes", () => { + beforeEach(() => { + vi.clearAllMocks(); + }); + function expectRoute(path: string[]) { const route = findRoutedCommand(path); expect(route).not.toBeNull(); @@ -58,6 +70,31 @@ describe("program routes", () => { await expectRunFalse(["config", "unset"], ["node", "openclaw", "config", "unset"]); }); + it("passes config get path correctly when root option values precede command", async () => { + const route = expectRoute(["config", "get"]); + await expect( + route?.run([ + "node", + "openclaw", + "--log-level", + "debug", + "config", + "get", + "update.channel", + "--json", + ]), + ).resolves.toBe(true); + expect(runConfigGetMock).toHaveBeenCalledWith({ path: "update.channel", json: true }); + }); + + it("passes config unset path correctly when root option values precede command", async () => { + const route = expectRoute(["config", "unset"]); + await expect( + route?.run(["node", "openclaw", "--profile", "work", "config", "unset", "update.channel"]), + ).resolves.toBe(true); + expect(runConfigUnsetMock).toHaveBeenCalledWith({ path: "update.channel" }); + }); + it("returns false for memory status route when --agent value is missing", async () => { await expectRunFalse(["memory", "status"], ["node", "openclaw", "memory", "status", "--agent"]); }); diff --git a/src/cli/program/routes.ts b/src/cli/program/routes.ts index ccecd8548f5..366c51ae135 100644 --- a/src/cli/program/routes.ts +++ b/src/cli/program/routes.ts @@ -99,16 +99,44 @@ const routeMemoryStatus: RouteSpec = { }, }; +function isValueToken(arg: string | undefined): boolean { + if (!arg || arg === "--") { + return false; + } + if (!arg.startsWith("-")) { + return true; + } + return /^-\d+(?:\.\d+)?$/.test(arg); +} + function getCommandPositionals(argv: string[]): string[] { const out: string[] = []; const args = argv.slice(2); - for (const arg of args) { + let commandStarted = false; + for (let i = 0; i < args.length; i += 1) { + const arg = args[i]; if (!arg || arg === "--") { break; } + if (!commandStarted) { + if (arg.startsWith("--profile=") || arg.startsWith("--log-level=")) { + continue; + } + if (arg === "--dev" || arg === "--no-color") { + continue; + } + if (arg === "--profile" || arg === "--log-level") { + const next = args[i + 1]; + if (isValueToken(next)) { + i += 1; + } + continue; + } + } if (arg.startsWith("-")) { continue; } + commandStarted = true; out.push(arg); } return out;