diff --git a/src/commands/status.command.ts b/src/commands/status.command.ts index e06feb42af5..d21ae16f176 100644 --- a/src/commands/status.command.ts +++ b/src/commands/status.command.ts @@ -37,6 +37,21 @@ import { resolveUpdateAvailability, } from "./status.update.js"; +function resolvePairingRecoveryContext(params: { + error?: string | null; + closeReason?: string | null; +}): { requestId: string | null } | null { + const source = [params.error, params.closeReason] + .filter((part) => typeof part === "string" && part.trim().length > 0) + .join(" "); + if (!source || !/pairing required/i.test(source)) { + return null; + } + const requestIdMatch = source.match(/requestId:\s*([^\s)]+)/i); + const requestId = requestIdMatch && requestIdMatch[1] ? requestIdMatch[1].trim() : ""; + return { requestId: requestId || null }; +} + export async function statusCommand( opts: { json?: boolean; @@ -230,6 +245,10 @@ export async function statusCommand( const suffix = self ? ` · ${self}` : ""; return `${gatewayMode} · ${target} · ${reach}${auth}${suffix}`; })(); + const pairingRecovery = resolvePairingRecoveryContext({ + error: gatewayProbe?.error ?? null, + closeReason: gatewayProbe?.close?.reason ?? null, + }); const agentsValue = (() => { const pending = @@ -399,6 +418,20 @@ export async function statusCommand( }).trimEnd(), ); + if (pairingRecovery) { + runtime.log(""); + runtime.log(theme.warn("Gateway pairing approval required.")); + if (pairingRecovery.requestId) { + runtime.log( + theme.muted( + `Recovery: ${formatCliCommand(`openclaw devices approve ${pairingRecovery.requestId}`)}`, + ), + ); + } + runtime.log(theme.muted(`Fallback: ${formatCliCommand("openclaw devices approve --latest")}`)); + runtime.log(theme.muted(`Inspect: ${formatCliCommand("openclaw devices list")}`)); + } + runtime.log(""); runtime.log(theme.heading("Security audit")); const fmtSummary = (value: { critical: number; warn: number; info: number }) => { diff --git a/src/commands/status.test.ts b/src/commands/status.test.ts index 1275c0bea2c..8092469f588 100644 --- a/src/commands/status.test.ts +++ b/src/commands/status.test.ts @@ -479,6 +479,52 @@ describe("statusCommand", () => { expect(logs.join("\n")).toMatch(/WARN/); }); + it("prints requestId-aware recovery guidance when gateway pairing is required", async () => { + mocks.probeGateway.mockResolvedValueOnce({ + ok: false, + url: "ws://127.0.0.1:18789", + connectLatencyMs: null, + error: "connect failed: pairing required (requestId: req-123)", + close: { code: 1008, reason: "pairing required (requestId: req-123)" }, + health: null, + status: null, + presence: null, + configSnapshot: null, + }); + + runtimeLogMock.mockClear(); + await statusCommand({}, runtime as never); + const logs = runtimeLogMock.mock.calls.map((c: unknown[]) => String(c[0])); + const joined = logs.join("\n"); + expect(joined).toContain("Gateway pairing approval required."); + expect(joined).toContain("devices approve req-123"); + expect(joined).toContain("devices approve --latest"); + expect(joined).toContain("devices list"); + }); + + it("prints fallback recovery guidance when pairing requestId is unavailable", async () => { + mocks.probeGateway.mockResolvedValueOnce({ + ok: false, + url: "ws://127.0.0.1:18789", + connectLatencyMs: null, + error: "connect failed: pairing required", + close: { code: 1008, reason: "connect failed" }, + health: null, + status: null, + presence: null, + configSnapshot: null, + }); + + runtimeLogMock.mockClear(); + await statusCommand({}, runtime as never); + const logs = runtimeLogMock.mock.calls.map((c: unknown[]) => String(c[0])); + const joined = logs.join("\n"); + expect(joined).toContain("Gateway pairing approval required."); + expect(joined).not.toContain("devices approve req-"); + expect(joined).toContain("devices approve --latest"); + expect(joined).toContain("devices list"); + }); + it("includes sessions across agents in JSON output", async () => { const originalAgents = mocks.listAgentsForGateway.getMockImplementation(); const originalResolveStorePath = mocks.resolveStorePath.getMockImplementation();