From f56897259e31801795c611e9d5ed448b74da0f33 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Mon, 27 Apr 2026 20:51:19 +0100 Subject: [PATCH] fix(cli): keep route-first json stdout clean --- CHANGELOG.md | 1 + docs/cli/models.md | 4 ++++ src/cli/route.test.ts | 19 +++++++++++++++++-- src/cli/route.ts | 1 - 4 files changed, 22 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a18b4fe8d74..9f25cdf4743 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,7 @@ Docs: https://docs.openclaw.ai ### Fixes - Channels/Telegram: skip the optional webhook-info API call during polling-mode status checks and startup bot-label probes so long-polling setups avoid an unnecessary Telegram round trip. Carries forward #72990. Thanks @danielgruneberg. +- CLI/models: keep route-first `models status --json` stdout reserved for the JSON payload by routing auth-profile and startup diagnostics to stderr. Fixes #72962. Thanks @vishutdhar. - Sessions: ignore future-dated session activity timestamps during reset freshness checks and cap future `updatedAt` values at the merge boundary so clock-skewed messages cannot keep stale sessions alive forever. Fixes #72989. Thanks @martingarramon. - Plugins/CLI: allow managed plugin installs when the active extensions root is a symlink to a real state directory, while keeping nested target symlinks blocked and suppressing misleading hook-pack fallback errors for install-boundary failures. Fixes #72946. Thanks @mayank6136. - Gateway/startup: keep hot Gateway boot paths on leaf config imports and add max-RSS reporting to the gateway startup bench so low-memory startup regressions are visible before release. Thanks @vincentkoc. diff --git a/docs/cli/models.md b/docs/cli/models.md index 24555720353..bc0422e6296 100644 --- a/docs/cli/models.md +++ b/docs/cli/models.md @@ -111,6 +111,10 @@ Options: - `--probe-max-tokens ` - `--agent ` (configured agent id; overrides `OPENCLAW_AGENT_DIR`/`PI_CODING_AGENT_DIR`) +`--json` keeps stdout reserved for the JSON payload. Auth-profile, provider, +and startup diagnostics are routed to stderr so scripts can pipe stdout directly +into tools such as `jq`. + Probe status buckets: - `ok` diff --git a/src/cli/route.test.ts b/src/cli/route.test.ts index 596681dcbad..a75ef4280b7 100644 --- a/src/cli/route.test.ts +++ b/src/cli/route.test.ts @@ -93,7 +93,7 @@ describe("tryRouteCli", () => { expect(ensurePluginRegistryLoadedMock).toHaveBeenCalledWith({ scope: "channels" }); }); - it("routes logs to stderr during plugin loading in --json mode and restores after", async () => { + it("keeps logs routed to stderr for routed --json commands", async () => { findRoutedCommandMock.mockReturnValue({ loadPlugins: true, run: runRouteMock, @@ -110,7 +110,22 @@ describe("tryRouteCli", () => { expect(ensurePluginRegistryLoadedMock).toHaveBeenCalled(); expect(captured[0]).toBe(true); - expect(loggingState.forceConsoleToStderr).toBe(false); + expect(loggingState.forceConsoleToStderr).toBe(true); + }); + + it("routes command-run logs to stderr for config-guard-skipping --json routes", async () => { + const captured: boolean[] = []; + runRouteMock.mockImplementationOnce(async () => { + captured.push(loggingState.forceConsoleToStderr); + return true; + }); + + await expect(tryRouteCli(["node", "openclaw", "models", "status", "--json"])).resolves.toBe( + true, + ); + + expect(ensureConfigReadyMock).not.toHaveBeenCalled(); + expect(captured).toEqual([true]); }); it("does not route logs to stderr during plugin loading without --json", async () => { diff --git a/src/cli/route.ts b/src/cli/route.ts index 74355154948..e7e9a4eb34f 100644 --- a/src/cli/route.ts +++ b/src/cli/route.ts @@ -23,7 +23,6 @@ async function prepareRoutedCommand(params: { const { VERSION } = await import("../version.js"); await applyCliExecutionStartupPresentation({ argv: params.argv, - routeLogsToStderrOnSuppress: false, startupPolicy, showBanner: process.stdout.isTTY && !startupPolicy.suppressDoctorStdout, version: VERSION,