refactor(cli): separate json payload output from logging

This commit is contained in:
Peter Steinberger
2026-03-22 23:19:14 +00:00
parent 274af0486a
commit 4ee41cc6f3
89 changed files with 710 additions and 693 deletions

View File

@@ -38,7 +38,7 @@ async function runBrowserPostAction<T>(params: {
{ timeoutMs: params.timeoutMs },
);
if (params.parent?.json) {
defaultRuntime.log(JSON.stringify(result, null, 2));
defaultRuntime.writeJson(result);
return;
}
defaultRuntime.log(params.describeSuccess(result));

View File

@@ -116,10 +116,10 @@ export function registerBrowserFormWaitEvalCommands(
},
});
if (parent?.json) {
defaultRuntime.log(JSON.stringify(result, null, 2));
defaultRuntime.writeJson(result);
return;
}
defaultRuntime.log(JSON.stringify(result.result ?? null, null, 2));
defaultRuntime.writeJson(result.result ?? null);
} catch (err) {
defaultRuntime.error(danger(String(err)));
defaultRuntime.exit(1);

View File

@@ -31,7 +31,7 @@ export function registerBrowserNavigationCommands(
{ timeoutMs: 20000 },
);
if (parent?.json) {
defaultRuntime.log(JSON.stringify(result, null, 2));
defaultRuntime.writeJson(result);
return;
}
defaultRuntime.log(`navigated to ${result.url ?? url}`);

View File

@@ -46,7 +46,7 @@ export function logBrowserActionResult(
successMessage: string,
) {
if (parent?.json) {
defaultRuntime.log(JSON.stringify(result, null, 2));
defaultRuntime.writeJson(result);
return;
}
defaultRuntime.log(successMessage);

View File

@@ -39,10 +39,10 @@ export function registerBrowserActionObserveCommands(
{ timeoutMs: 20000 },
);
if (parent?.json) {
defaultRuntime.log(JSON.stringify(result, null, 2));
defaultRuntime.writeJson(result);
return;
}
defaultRuntime.log(JSON.stringify(result.messages, null, 2));
defaultRuntime.writeJson(result.messages);
});
});
@@ -65,7 +65,7 @@ export function registerBrowserActionObserveCommands(
{ timeoutMs: 20000 },
);
if (parent?.json) {
defaultRuntime.log(JSON.stringify(result, null, 2));
defaultRuntime.writeJson(result);
return;
}
defaultRuntime.log(`PDF: ${shortenHomePath(result.path)}`);
@@ -107,7 +107,7 @@ export function registerBrowserActionObserveCommands(
{ timeoutMs: timeoutMs ?? 20000 },
);
if (parent?.json) {
defaultRuntime.log(JSON.stringify(result, null, 2));
defaultRuntime.writeJson(result);
return;
}
defaultRuntime.log(result.response.body);

View File

@@ -39,7 +39,7 @@ function printJsonResult(parent: BrowserParentOpts, result: unknown): boolean {
if (!parent.json) {
return false;
}
defaultRuntime.log(JSON.stringify(result, null, 2));
defaultRuntime.writeJson(result);
return true;
}

View File

@@ -39,7 +39,7 @@ export function registerBrowserInspectCommands(
{ timeoutMs: 20000 },
);
if (parent?.json) {
defaultRuntime.log(JSON.stringify(result, null, 2));
defaultRuntime.writeJson(result);
return;
}
defaultRuntime.log(`MEDIA:${shortenHomePath(result.path)}`);
@@ -106,19 +106,13 @@ export function registerBrowserInspectCommands(
await fs.writeFile(opts.out, payload, "utf8");
}
if (parent?.json) {
defaultRuntime.log(
JSON.stringify(
{
ok: true,
out: opts.out,
...(result.format === "ai" && result.imagePath
? { imagePath: result.imagePath }
: {}),
},
null,
2,
),
);
defaultRuntime.writeJson({
ok: true,
out: opts.out,
...(result.format === "ai" && result.imagePath
? { imagePath: result.imagePath }
: {}),
});
} else {
defaultRuntime.log(shortenHomePath(opts.out));
if (result.format === "ai" && result.imagePath) {
@@ -129,7 +123,7 @@ export function registerBrowserInspectCommands(
}
if (parent?.json) {
defaultRuntime.log(JSON.stringify(result, null, 2));
defaultRuntime.writeJson(result);
return;
}

View File

@@ -25,7 +25,7 @@ function printJsonResult(parent: BrowserParentOpts, payload: unknown): boolean {
if (!parent?.json) {
return false;
}
defaultRuntime.log(JSON.stringify(payload, null, 2));
defaultRuntime.writeJson(payload);
return true;
}
@@ -89,7 +89,7 @@ function runBrowserCommand(action: () => Promise<void>) {
function logBrowserTabs(tabs: BrowserTab[], json?: boolean) {
if (json) {
defaultRuntime.log(JSON.stringify({ tabs }, null, 2));
defaultRuntime.writeJson({ tabs });
return;
}
if (tabs.length === 0) {

View File

@@ -30,7 +30,7 @@ export async function runBrowserResizeWithOutput(params: {
);
if (params.parent?.json) {
defaultRuntime.log(JSON.stringify(result, null, 2));
defaultRuntime.writeJson(result);
return;
}
defaultRuntime.log(params.successMessage);

View File

@@ -36,7 +36,7 @@ async function runMutationRequest(params: {
try {
const result = await callBrowserRequest(params.parent, params.request, { timeoutMs: 20000 });
if (params.parent?.json) {
defaultRuntime.log(JSON.stringify(result, null, 2));
defaultRuntime.writeJson(result);
return;
}
defaultRuntime.log(params.successMessage);
@@ -72,10 +72,10 @@ export function registerBrowserCookiesAndStorageCommands(
{ timeoutMs: 20000 },
);
if (parent?.json) {
defaultRuntime.log(JSON.stringify(result, null, 2));
defaultRuntime.writeJson(result);
return;
}
defaultRuntime.log(JSON.stringify(result.cookies ?? [], null, 2));
defaultRuntime.writeJson(result.cookies ?? []);
} catch (err) {
defaultRuntime.error(danger(String(err)));
defaultRuntime.exit(1);
@@ -165,10 +165,10 @@ export function registerBrowserCookiesAndStorageCommands(
{ timeoutMs: 20000 },
);
if (parent?.json) {
defaultRuntime.log(JSON.stringify(result, null, 2));
defaultRuntime.writeJson(result);
return;
}
defaultRuntime.log(JSON.stringify(result.values ?? {}, null, 2));
defaultRuntime.writeJson(result.values ?? {});
} catch (err) {
defaultRuntime.error(danger(String(err)));
defaultRuntime.exit(1);

View File

@@ -38,7 +38,7 @@ async function runBrowserSetRequest(params: {
{ timeoutMs: 20000 },
);
if (params.parent?.json) {
defaultRuntime.log(JSON.stringify(result, null, 2));
defaultRuntime.writeJson(result);
return;
}
defaultRuntime.log(params.successMessage);
@@ -139,7 +139,7 @@ export function registerBrowserStateCommands(
{ timeoutMs: 20000 },
);
if (parent?.json) {
defaultRuntime.log(JSON.stringify(result, null, 2));
defaultRuntime.writeJson(result);
return;
}
defaultRuntime.log("headers set");

View File

@@ -18,7 +18,7 @@ import {
import { validateConfigObjectRaw } from "../config/validation.js";
import { SecretProviderSchema } from "../config/zod-schema.core.js";
import { danger, info, success } from "../globals.js";
import type { RuntimeEnv } from "../runtime.js";
import { type RuntimeEnv, writeRuntimeJson } from "../runtime.js";
import { defaultRuntime } from "../runtime.js";
import {
formatExecSecretRefIdValidationMessage,
@@ -51,6 +51,7 @@ import {
type ConfigSetOptions,
} from "./config-set-input.js";
import { resolveConfigSetMode } from "./config-set-parser.js";
import { setCommandJsonMode } from "./program/json-mode.js";
type PathSegment = string;
type ConfigSetParseOpts = {
@@ -1072,7 +1073,7 @@ export async function runConfigSet(opts: {
);
}
if (opts.cliOptions.json) {
runtime.log(JSON.stringify(dryRunResult, null, 2));
writeRuntimeJson(runtime, dryRunResult);
} else {
if (!dryRunResult.checks.schema && !dryRunResult.checks.resolvability) {
runtime.log(
@@ -1120,7 +1121,7 @@ export async function runConfigSet(opts: {
opts.cliOptions.json &&
err instanceof ConfigSetDryRunValidationError
) {
runtime.log(JSON.stringify(err.result, null, 2));
writeRuntimeJson(runtime, err.result);
runtime.exit(1);
return;
}
@@ -1142,7 +1143,7 @@ export async function runConfigGet(opts: { path: string; json?: boolean; runtime
return;
}
if (opts.json) {
runtime.log(JSON.stringify(res.value ?? null, null, 2));
writeRuntimeJson(runtime, res.value ?? null);
return;
}
if (
@@ -1153,7 +1154,7 @@ export async function runConfigGet(opts: { path: string; json?: boolean; runtime
runtime.log(String(res.value));
return;
}
runtime.log(JSON.stringify(res.value ?? null, null, 2));
writeRuntimeJson(runtime, res.value ?? null);
} catch (err) {
runtime.error(danger(String(err)));
runtime.exit(1);
@@ -1205,7 +1206,7 @@ export async function runConfigValidate(opts: { json?: boolean; runtime?: Runtim
if (!snapshot.exists) {
if (opts.json) {
runtime.log(JSON.stringify({ valid: false, path: outputPath, error: "file not found" }));
writeRuntimeJson(runtime, { valid: false, path: outputPath, error: "file not found" }, 0);
} else {
runtime.error(danger(`Config file not found: ${shortPath}`));
}
@@ -1217,7 +1218,7 @@ export async function runConfigValidate(opts: { json?: boolean; runtime?: Runtim
const issues = normalizeConfigIssues(snapshot.issues);
if (opts.json) {
runtime.log(JSON.stringify({ valid: false, path: outputPath, issues }, null, 2));
writeRuntimeJson(runtime, { valid: false, path: outputPath, issues });
} else {
runtime.error(danger(`Config invalid at ${shortPath}:`));
for (const line of formatConfigIssueLines(issues, danger("×"), { normalizeRoot: true })) {
@@ -1231,13 +1232,13 @@ export async function runConfigValidate(opts: { json?: boolean; runtime?: Runtim
}
if (opts.json) {
runtime.log(JSON.stringify({ valid: true, path: outputPath }));
writeRuntimeJson(runtime, { valid: true, path: outputPath }, 0);
} else {
runtime.log(success(`Config valid: ${shortPath}`));
}
} catch (err) {
if (opts.json) {
runtime.log(JSON.stringify({ valid: false, path: outputPath, error: String(err) }));
writeRuntimeJson(runtime, { valid: false, path: outputPath, error: String(err) }, 0);
} else {
runtime.error(danger(`Config validation error: ${String(err)}`));
}
@@ -1276,8 +1277,7 @@ export function registerConfigCli(program: Command) {
await runConfigGet({ path, json: Boolean(opts.json) });
});
cmd
.command("set")
setCommandJsonMode(cmd.command("set"), "parse-only")
.description(CONFIG_SET_DESCRIPTION)
.argument("[path]", "Config path (dot or bracket notation)")
.argument("[value]", "Value (JSON/JSON5 or raw string)")

View File

@@ -341,7 +341,7 @@ export function registerCronEditCommand(cron: Command) {
id,
patch,
});
defaultRuntime.log(JSON.stringify(res, null, 2));
defaultRuntime.writeJson(res);
await warnIfCronSchedulerDisabled(opts);
} catch (err) {
defaultRuntime.error(danger(String(err)));

View File

@@ -4,7 +4,7 @@ import { resolveCronStaggerMs } from "../../cron/stagger.js";
import type { CronJob, CronSchedule } from "../../cron/types.js";
import { danger } from "../../globals.js";
import { formatDurationHuman } from "../../infra/format-time/format-duration.ts";
import { defaultRuntime } from "../../runtime.js";
import { defaultRuntime, type RuntimeEnv } from "../../runtime.js";
import { colorize, isRich, theme } from "../../terminal/theme.js";
import type { GatewayRpcOpts } from "../gateway-rpc.js";
import { callGatewayFromCli } from "../gateway-rpc.js";
@@ -13,7 +13,7 @@ export const getCronChannelOptions = () =>
["last", ...listChannelPlugins().map((plugin) => plugin.id)].join("|");
export function printCronJson(value: unknown) {
defaultRuntime.log(JSON.stringify(value, null, 2));
defaultRuntime.writeJson(value);
}
export function handleCronCliError(err: unknown) {
@@ -184,7 +184,7 @@ const formatStatus = (job: CronJob) => {
return job.state.lastStatus ?? "idle";
};
export function printCronList(jobs: CronJob[], runtime = defaultRuntime) {
export function printCronList(jobs: CronJob[], runtime: RuntimeEnv = defaultRuntime) {
if (jobs.length === 0) {
runtime.log("No cron jobs.");
return;

View File

@@ -21,7 +21,7 @@ export type DaemonActionResponse = {
};
export function emitDaemonActionJson(payload: DaemonActionResponse) {
defaultRuntime.log(JSON.stringify(payload, null, 2));
defaultRuntime.writeJson(payload);
}
export function buildDaemonServiceSnapshot(service: GatewayService, loaded: boolean) {

View File

@@ -52,7 +52,7 @@ function sanitizeDaemonStatusForJson(status: DaemonStatus): DaemonStatus {
export function printDaemonStatus(status: DaemonStatus, opts: { json: boolean }) {
if (opts.json) {
const sanitized = sanitizeDaemonStatusForJson(status);
defaultRuntime.log(JSON.stringify(sanitized, null, 2));
defaultRuntime.writeJson(sanitized);
return;
}

View File

@@ -246,7 +246,7 @@ export function registerDevicesCli(program: Command) {
.action(async (opts: DevicesRpcOpts) => {
const list = await listPairingWithFallback(opts);
if (opts.json) {
defaultRuntime.log(JSON.stringify(list, null, 2));
defaultRuntime.writeJson(list);
return;
}
if (list.pending?.length) {
@@ -323,7 +323,7 @@ export function registerDevicesCli(program: Command) {
}
const result = await callGatewayCli("device.pair.remove", opts, { deviceId: trimmed });
if (opts.json) {
defaultRuntime.log(JSON.stringify(result, null, 2));
defaultRuntime.writeJson(result);
return;
}
defaultRuntime.log(`${theme.warn("Removed")} ${theme.command(trimmed)}`);
@@ -366,16 +366,10 @@ export function registerDevicesCli(program: Command) {
}
}
if (opts.json) {
defaultRuntime.log(
JSON.stringify(
{
removedDevices: removedDeviceIds,
rejectedPending: rejectedRequestIds,
},
null,
2,
),
);
defaultRuntime.writeJson({
removedDevices: removedDeviceIds,
rejectedPending: rejectedRequestIds,
});
return;
}
defaultRuntime.log(
@@ -413,7 +407,7 @@ export function registerDevicesCli(program: Command) {
return;
}
if (opts.json) {
defaultRuntime.log(JSON.stringify(result, null, 2));
defaultRuntime.writeJson(result);
return;
}
const deviceId = (result as { device?: { deviceId?: string } })?.device?.deviceId;
@@ -431,7 +425,7 @@ export function registerDevicesCli(program: Command) {
.action(async (requestId: string, opts: DevicesRpcOpts) => {
const result = await callGatewayCli("device.pair.reject", opts, { requestId });
if (opts.json) {
defaultRuntime.log(JSON.stringify(result, null, 2));
defaultRuntime.writeJson(result);
return;
}
const deviceId = (result as { deviceId?: string })?.deviceId;
@@ -456,7 +450,7 @@ export function registerDevicesCli(program: Command) {
role: required.role,
scopes: Array.isArray(opts.scope) ? opts.scope : undefined,
});
defaultRuntime.log(JSON.stringify(result, null, 2));
defaultRuntime.writeJson(result);
}),
);
@@ -475,7 +469,7 @@ export function registerDevicesCli(program: Command) {
deviceId: required.deviceId,
role: required.role,
});
defaultRuntime.log(JSON.stringify(result, null, 2));
defaultRuntime.writeJson(result);
}),
);
}

View File

@@ -160,7 +160,7 @@ export function registerDirectoryCli(program: Command) {
runtime: defaultRuntime,
});
if (params.opts.json) {
defaultRuntime.log(JSON.stringify(result, null, 2));
defaultRuntime.writeJson(result);
return;
}
printDirectoryList({ title: params.title, emptyMessage: params.emptyMessage, entries: result });
@@ -179,7 +179,7 @@ export function registerDirectoryCli(program: Command) {
}
const result = await fn({ cfg, accountId, runtime: defaultRuntime });
if (opts.json) {
defaultRuntime.log(JSON.stringify(result, null, 2));
defaultRuntime.writeJson(result);
return;
}
if (!result) {
@@ -272,7 +272,7 @@ export function registerDirectoryCli(program: Command) {
runtime: defaultRuntime,
});
if (opts.json) {
defaultRuntime.log(JSON.stringify(result, null, 2));
defaultRuntime.writeJson(result);
return;
}
printDirectoryList({

View File

@@ -154,16 +154,10 @@ export function registerDnsCli(program: Command) {
);
defaultRuntime.log("");
defaultRuntime.log(theme.heading("Recommended ~/.openclaw/openclaw.json:"));
defaultRuntime.log(
JSON.stringify(
{
gateway: { bind: "auto" },
discovery: { wideArea: { enabled: true, domain: wideAreaDomain } },
},
null,
2,
),
);
defaultRuntime.writeJson({
gateway: { bind: "auto" },
discovery: { wideArea: { enabled: true, domain: wideAreaDomain } },
});
defaultRuntime.log("");
defaultRuntime.log(theme.heading("Tailscale admin (DNS → Nameservers):"));
defaultRuntime.log(

View File

@@ -135,7 +135,7 @@ async function saveSnapshotTargeted(params: {
? saveSnapshotLocal(params.file)
: await saveSnapshot(params.opts, params.nodeId, params.file, params.baseHash);
if (params.opts.json) {
defaultRuntime.log(JSON.stringify(next));
defaultRuntime.writeJson(next, 0);
return;
}
defaultRuntime.log(theme.muted(`Target: ${params.targetLabel}`));
@@ -365,7 +365,7 @@ export function registerExecApprovalsCli(program: Command) {
try {
const { snapshot, nodeId, source } = await loadSnapshotTarget(opts);
if (opts.json) {
defaultRuntime.log(JSON.stringify(snapshot));
defaultRuntime.writeJson(snapshot, 0);
return;
}

View File

@@ -124,14 +124,14 @@ export function registerGatewayCli(program: Command) {
const params = JSON.parse(String(opts.params ?? "{}"));
const result = await callGatewayCli(method, { ...rpcOpts, config }, params);
if (rpcOpts.json) {
defaultRuntime.log(JSON.stringify(result, null, 2));
defaultRuntime.writeJson(result);
return;
}
const rich = isRich();
defaultRuntime.log(
`${colorize(rich, theme.heading, "Gateway call")}: ${colorize(rich, theme.muted, String(method))}`,
);
defaultRuntime.log(JSON.stringify(result, null, 2));
defaultRuntime.writeJson(result);
}, "Gateway call failed");
}),
);
@@ -148,7 +148,7 @@ export function registerGatewayCli(program: Command) {
const config = await readBestEffortConfig();
const result = await callGatewayCli("usage.cost", { ...rpcOpts, config }, { days });
if (rpcOpts.json) {
defaultRuntime.log(JSON.stringify(result, null, 2));
defaultRuntime.writeJson(result);
return;
}
const rich = isRich();
@@ -170,7 +170,7 @@ export function registerGatewayCli(program: Command) {
const config = await readBestEffortConfig();
const result = await callGatewayCli("health", { ...rpcOpts, config });
if (rpcOpts.json) {
defaultRuntime.log(JSON.stringify(result, null, 2));
defaultRuntime.writeJson(result);
return;
}
const rich = isRich();
@@ -242,18 +242,12 @@ export function registerGatewayCli(program: Command) {
const port = pickGatewayPort(b);
return { ...b, wsUrl: host ? `ws://${host}:${port}` : null };
});
defaultRuntime.log(
JSON.stringify(
{
timeoutMs,
domains,
count: enriched.length,
beacons: enriched,
},
null,
2,
),
);
defaultRuntime.writeJson({
timeoutMs,
domains,
count: enriched.length,
beacons: enriched,
});
return;
}

View File

@@ -20,7 +20,7 @@ import {
waitForActiveTasks,
} from "../../process/command-queue.js";
import { createRestartIterationHook } from "../../process/restart-recovery.js";
import type { defaultRuntime } from "../../runtime.js";
import type { RuntimeEnv } from "../../runtime.js";
const gatewayLog = createSubsystemLogger("gateway");
@@ -28,7 +28,7 @@ type GatewayRunSignalAction = "stop" | "restart";
export async function runGatewayLoop(params: {
start: () => Promise<Awaited<ReturnType<typeof startGatewayServer>>>;
runtime: typeof defaultRuntime;
runtime: RuntimeEnv;
lockPort?: number;
}) {
let lock = await acquireGatewayLock({ port: params.lockPort });

View File

@@ -145,6 +145,14 @@ function exitHooksCliWithError(err: unknown): never {
process.exit(1);
}
function writeHooksOutput(value: string, json: boolean | undefined): void {
if (json) {
defaultRuntime.writeStdout(value);
return;
}
defaultRuntime.log(value);
}
async function runHooksCliAction(action: () => Promise<void> | void): Promise<void> {
try {
await action();
@@ -455,7 +463,7 @@ export function registerHooksCli(program: Command): void {
runHooksCliAction(async () => {
const config = loadConfig();
const report = buildHooksReport(config);
defaultRuntime.log(formatHooksList(report, opts));
writeHooksOutput(formatHooksList(report, opts), opts.json);
}),
);
@@ -467,7 +475,7 @@ export function registerHooksCli(program: Command): void {
runHooksCliAction(async () => {
const config = loadConfig();
const report = buildHooksReport(config);
defaultRuntime.log(formatHookInfo(report, name, opts));
writeHooksOutput(formatHookInfo(report, name, opts), opts.json);
}),
);
@@ -479,7 +487,7 @@ export function registerHooksCli(program: Command): void {
runHooksCliAction(async () => {
const config = loadConfig();
const report = buildHooksReport(config);
defaultRuntime.log(formatHooksCheck(report, opts));
writeHooksOutput(formatHooksCheck(report, opts), opts.json);
}),
);

View File

@@ -14,7 +14,7 @@ function fail(message: string): never {
}
function printJson(value: unknown): void {
defaultRuntime.log(JSON.stringify(value, null, 2));
defaultRuntime.writeJson(value);
}
export function registerMcpCli(program: Command) {

View File

@@ -414,7 +414,7 @@ export async function runMemoryStatus(opts: MemoryCommandOptions) {
}
if (opts.json) {
defaultRuntime.log(JSON.stringify(allResults, null, 2));
defaultRuntime.writeJson(allResults);
return;
}
@@ -723,7 +723,7 @@ export async function runMemorySearch(
return;
}
if (opts.json) {
defaultRuntime.log(JSON.stringify({ results }, null, 2));
defaultRuntime.writeJson({ results });
return;
}
if (results.length === 0) {

View File

@@ -223,7 +223,7 @@ export async function runNodeDaemonStatus(opts: NodeDaemonStatusOptions = {}) {
};
if (json) {
defaultRuntime.log(JSON.stringify(payload, null, 2));
defaultRuntime.writeJson(payload);
return;
}

View File

@@ -66,7 +66,7 @@ export function registerNodesCameraCommands(nodes: Command) {
const devices = Array.isArray(payload.devices) ? payload.devices : [];
if (opts.json) {
defaultRuntime.log(JSON.stringify(devices, null, 2));
defaultRuntime.writeJson(devices);
return;
}
@@ -184,7 +184,7 @@ export function registerNodesCameraCommands(nodes: Command) {
}
if (opts.json) {
defaultRuntime.log(JSON.stringify({ files: results }, null, 2));
defaultRuntime.writeJson({ files: results });
return;
}
defaultRuntime.log(results.map((r) => `MEDIA:${shortenHomePath(r.path)}`).join("\n"));
@@ -241,20 +241,14 @@ export function registerNodesCameraCommands(nodes: Command) {
});
if (opts.json) {
defaultRuntime.log(
JSON.stringify(
{
file: {
facing,
path: filePath,
durationMs: payload.durationMs,
hasAudio: payload.hasAudio,
},
},
null,
2,
),
);
defaultRuntime.writeJson({
file: {
facing,
path: filePath,
durationMs: payload.durationMs,
hasAudio: payload.hasAudio,
},
});
return;
}
defaultRuntime.log(`MEDIA:${shortenHomePath(filePath)}`);

View File

@@ -65,9 +65,7 @@ export function registerNodesCanvasCommands(nodes: Command) {
await writeBase64ToFile(filePath, payload.base64);
if (opts.json) {
defaultRuntime.log(
JSON.stringify({ file: { path: filePath, format: payload.format } }, null, 2),
);
defaultRuntime.writeJson({ file: { path: filePath, format: payload.format } });
return;
}
defaultRuntime.log(`MEDIA:${shortenHomePath(filePath)}`);
@@ -169,7 +167,7 @@ export function registerNodesCanvasCommands(nodes: Command) {
javaScript: js,
});
if (opts.json) {
defaultRuntime.log(JSON.stringify(raw, null, 2));
defaultRuntime.writeJson(raw);
return;
}
const payload =

View File

@@ -333,7 +333,7 @@ export function registerNodesInvokeCommands(nodes: Command) {
}
const result = await callGatewayCli("node.invoke", opts, invokeParams);
defaultRuntime.log(JSON.stringify(result, null, 2));
defaultRuntime.writeJson(result);
});
}),
{ timeoutMs: 30_000 },
@@ -421,7 +421,7 @@ export function registerNodesInvokeCommands(nodes: Command) {
const result = await callGatewayCli("node.invoke", opts, invokeParams);
if (opts.json) {
defaultRuntime.log(JSON.stringify(result, null, 2));
defaultRuntime.writeJson(result);
return;
}

View File

@@ -61,7 +61,7 @@ export function registerNodesLocationCommands(nodes: Command) {
: {};
if (opts.json) {
defaultRuntime.log(JSON.stringify(payload, null, 2));
defaultRuntime.writeJson(payload);
return;
}
@@ -73,7 +73,7 @@ export function registerNodesLocationCommands(nodes: Command) {
defaultRuntime.log(`${lat},${lon}${accText}`);
return;
}
defaultRuntime.log(JSON.stringify(payload));
defaultRuntime.writeJson(payload, 0);
});
}),
{ timeoutMs: 30_000 },

View File

@@ -46,7 +46,7 @@ export function registerNodesNotifyCommand(nodes: Command) {
const result = await callGatewayCli("node.invoke", opts, invokeParams);
if (opts.json) {
defaultRuntime.log(JSON.stringify(result, null, 2));
defaultRuntime.writeJson(result);
return;
}
const { ok } = getNodesTheme();

View File

@@ -17,7 +17,7 @@ export function registerNodesPairingCommands(nodes: Command) {
const result = await callGatewayCli("node.pair.list", opts, {});
const { pending } = parsePairingList(result);
if (opts.json) {
defaultRuntime.log(JSON.stringify(pending, null, 2));
defaultRuntime.writeJson(pending);
return;
}
if (pending.length === 0) {
@@ -50,7 +50,7 @@ export function registerNodesPairingCommands(nodes: Command) {
const result = await callGatewayCli("node.pair.approve", opts, {
requestId,
});
defaultRuntime.log(JSON.stringify(result, null, 2));
defaultRuntime.writeJson(result);
});
}),
);
@@ -65,7 +65,7 @@ export function registerNodesPairingCommands(nodes: Command) {
const result = await callGatewayCli("node.pair.reject", opts, {
requestId,
});
defaultRuntime.log(JSON.stringify(result, null, 2));
defaultRuntime.writeJson(result);
});
}),
);
@@ -90,7 +90,7 @@ export function registerNodesPairingCommands(nodes: Command) {
displayName: name,
});
if (opts.json) {
defaultRuntime.log(JSON.stringify(result, null, 2));
defaultRuntime.writeJson(result);
return;
}
const { ok } = getNodesTheme();

View File

@@ -52,7 +52,7 @@ export function registerNodesPushCommand(nodes: Command) {
const result = await callGatewayCli("push.test", opts, params);
if (opts.json) {
defaultRuntime.log(JSON.stringify(result, null, 2));
defaultRuntime.writeJson(result);
return;
}

View File

@@ -57,21 +57,15 @@ export function registerNodesScreenCommands(nodes: Command) {
const written = await writeScreenRecordToFile(filePath, parsed.base64);
if (opts.json) {
defaultRuntime.log(
JSON.stringify(
{
file: {
path: written.path,
durationMs: parsed.durationMs,
fps: parsed.fps,
screenIndex: parsed.screenIndex,
hasAudio: parsed.hasAudio,
},
},
null,
2,
),
);
defaultRuntime.writeJson({
file: {
path: written.path,
durationMs: parsed.durationMs,
fps: parsed.fps,
screenIndex: parsed.screenIndex,
hasAudio: parsed.hasAudio,
},
});
return;
}
defaultRuntime.log(`MEDIA:${shortenHomePath(written.path)}`);

View File

@@ -147,7 +147,7 @@ export function registerNodesStatusCommands(nodes: Command) {
if (opts.json) {
const ts = typeof obj.ts === "number" ? obj.ts : Date.now();
defaultRuntime.log(JSON.stringify({ ...obj, ts, nodes: filtered }, null, 2));
defaultRuntime.writeJson({ ...obj, ts, nodes: filtered });
return;
}
@@ -223,7 +223,7 @@ export function registerNodesStatusCommands(nodes: Command) {
nodeId,
});
if (opts.json) {
defaultRuntime.log(JSON.stringify(result, null, 2));
defaultRuntime.writeJson(result);
return;
}
@@ -350,9 +350,7 @@ export function registerNodesStatusCommands(nodes: Command) {
);
if (opts.json) {
defaultRuntime.log(
JSON.stringify({ pending: pendingRows, paired: filteredPaired }, null, 2),
);
defaultRuntime.writeJson({ pending: pendingRows, paired: filteredPaired });
return;
}

View File

@@ -80,7 +80,7 @@ export function registerPairingCli(program: Command) {
? await listChannelPairingRequests(channel, process.env, accountId)
: await listChannelPairingRequests(channel);
if (opts.json) {
defaultRuntime.log(JSON.stringify({ channel, requests }, null, 2));
defaultRuntime.writeJson({ channel, requests });
return;
}
if (requests.length === 0) {

View File

@@ -191,7 +191,7 @@ export function registerPluginsCli(program: Command) {
plugins: list,
diagnostics: report.diagnostics,
};
defaultRuntime.log(JSON.stringify(payload, null, 2));
defaultRuntime.writeJson(payload);
return;
}
@@ -298,7 +298,7 @@ export function registerPluginsCli(program: Command) {
}));
if (opts.json) {
defaultRuntime.log(JSON.stringify(inspectAllWithInstall, null, 2));
defaultRuntime.writeJson(inspectAllWithInstall);
return;
}
@@ -367,16 +367,10 @@ export function registerPluginsCli(program: Command) {
const install = cfg.plugins?.installs?.[inspect.plugin.id];
if (opts.json) {
defaultRuntime.log(
JSON.stringify(
{
...inspect,
install,
},
null,
2,
),
);
defaultRuntime.writeJson({
...inspect,
install,
});
return;
}
@@ -760,18 +754,12 @@ export function registerPluginsCli(program: Command) {
}
if (opts.json) {
defaultRuntime.log(
JSON.stringify(
{
source: result.sourceLabel,
name: result.manifest.name,
version: result.manifest.version,
plugins: result.manifest.plugins,
},
null,
2,
),
);
defaultRuntime.writeJson({
source: result.sourceLabel,
name: result.manifest.name,
version: result.manifest.version,
plugins: result.manifest.plugins,
});
return;
}

View File

@@ -0,0 +1,58 @@
import type { Command } from "commander";
import { hasFlag } from "../argv.js";
const jsonModeSymbol = Symbol("openclaw.cli.jsonMode");
type JsonMode = "output" | "parse-only";
type JsonModeCommand = Command & {
[jsonModeSymbol]?: JsonMode;
};
function commandDefinesJsonOption(command: Command): boolean {
return command.options.some((option) => option.long === "--json");
}
function getDeclaredCommandJsonMode(command: Command): JsonMode | null {
for (let current: Command | null = command; current; current = current.parent ?? null) {
const metadata = (current as JsonModeCommand)[jsonModeSymbol];
if (metadata) {
return metadata;
}
if (commandDefinesJsonOption(current)) {
return "output";
}
}
return null;
}
function commandSelectedJsonFlag(command: Command, argv: string[]): boolean {
const commandWithGlobals = command as Command & {
optsWithGlobals?: <T extends Record<string, unknown>>() => T;
};
if (typeof commandWithGlobals.optsWithGlobals === "function") {
const resolved = commandWithGlobals.optsWithGlobals<Record<string, unknown>>().json;
if (resolved === true) {
return true;
}
}
return hasFlag(argv, "--json");
}
export function setCommandJsonMode(command: Command, mode: JsonMode): Command {
(command as JsonModeCommand)[jsonModeSymbol] = mode;
return command;
}
export function getCommandJsonMode(
command: Command,
argv: string[] = process.argv,
): JsonMode | null {
if (!commandSelectedJsonFlag(command, argv)) {
return null;
}
return getDeclaredCommandJsonMode(command);
}
export function isCommandJsonOutputMode(command: Command, argv: string[] = process.argv): boolean {
return getCommandJsonMode(command, argv) === "output";
}

View File

@@ -1,5 +1,6 @@
import { Command } from "commander";
import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
import { setCommandJsonMode } from "./json-mode.js";
const setVerboseMock = vi.fn();
const emitCliBannerMock = vi.fn();
@@ -10,6 +11,8 @@ const routeLogsToStderrMock = vi.fn();
const runtimeMock = {
log: vi.fn(),
error: vi.fn(),
writeStdout: vi.fn(),
writeJson: vi.fn(),
exit: vi.fn(),
};
@@ -100,7 +103,10 @@ describe("registerPreActionHooks", () => {
function buildProgram() {
const program = new Command().name("openclaw");
program.command("status").action(() => {});
program
.command("status")
.option("--json")
.action(() => {});
program
.command("backup")
.command("create")
@@ -109,7 +115,11 @@ describe("registerPreActionHooks", () => {
program.command("doctor").action(() => {});
program.command("completion").action(() => {});
program.command("secrets").action(() => {});
program.command("agents").action(() => {});
program
.command("agents")
.command("list")
.option("--json")
.action(() => {});
program.command("configure").action(() => {});
program.command("onboard").action(() => {});
const channels = program.command("channels");
@@ -125,8 +135,7 @@ describe("registerPreActionHooks", () => {
.option("--json")
.action(() => {});
const config = program.command("config");
config
.command("set")
setCommandJsonMode(config.command("set"), "parse-only")
.argument("<path>")
.argument("<value>")
.option("--json")
@@ -277,8 +286,8 @@ describe("registerPreActionHooks", () => {
it("routes logs to stderr in --json mode so stdout stays clean", async () => {
await runPreAction({
parseArgv: ["agents"],
processArgv: ["node", "openclaw", "agents", "--json"],
parseArgv: ["agents", "list"],
processArgv: ["node", "openclaw", "agents", "list", "--json"],
});
expect(routeLogsToStderrMock).toHaveBeenCalledOnce();
@@ -297,8 +306,8 @@ describe("registerPreActionHooks", () => {
// non-json command should not route
await runPreAction({
parseArgv: ["agents"],
processArgv: ["node", "openclaw", "agents"],
parseArgv: ["agents", "list"],
processArgv: ["node", "openclaw", "agents", "list"],
});
expect(routeLogsToStderrMock).not.toHaveBeenCalled();

View File

@@ -4,14 +4,10 @@ import { isTruthyEnvValue } from "../../infra/env.js";
import { routeLogsToStderr } from "../../logging/console.js";
import type { LogLevel } from "../../logging/levels.js";
import { defaultRuntime } from "../../runtime.js";
import {
getCommandPathWithRootOptions,
getVerboseFlag,
hasFlag,
hasHelpOrVersion,
} from "../argv.js";
import { getCommandPathWithRootOptions, getVerboseFlag, hasHelpOrVersion } from "../argv.js";
import { emitCliBanner } from "../banner.js";
import { resolveCliName } from "../cli-name.js";
import { isCommandJsonOutputMode } from "./json-mode.js";
function setProcessTitleForCommand(actionCommand: Command) {
let current: Command = actionCommand;
@@ -37,7 +33,6 @@ const PLUGIN_REQUIRED_COMMANDS = new Set([
"health",
]);
const CONFIG_GUARD_BYPASS_COMMANDS = new Set(["backup", "doctor", "completion", "secrets"]);
const JSON_PARSE_ONLY_COMMANDS = new Set(["config set"]);
let configGuardModulePromise: Promise<typeof import("./config-guard.js")> | undefined;
let pluginRegistryModulePromise: Promise<typeof import("../plugin-registry.js")> | undefined;
@@ -71,12 +66,12 @@ function resolvePluginRegistryScope(commandPath: string[]): "channels" | "all" {
return commandPath[0] === "status" || commandPath[0] === "health" ? "channels" : "all";
}
function shouldLoadPluginsForCommand(commandPath: string[], argv: string[]): boolean {
function shouldLoadPluginsForCommand(commandPath: string[], jsonOutputMode: boolean): boolean {
const [primary, secondary] = commandPath;
if (!primary || !PLUGIN_REQUIRED_COMMANDS.has(primary)) {
return false;
}
if ((primary === "status" || primary === "health") && hasFlag(argv, "--json")) {
if ((primary === "status" || primary === "health") && jsonOutputMode) {
return false;
}
// Setup wizard and channels add should stay manifest-first and load selected plugins on demand.
@@ -105,17 +100,6 @@ function getCliLogLevel(actionCommand: Command): LogLevel | undefined {
return typeof logLevel === "string" ? (logLevel as LogLevel) : undefined;
}
function isJsonOutputMode(commandPath: string[], argv: string[]): boolean {
if (!hasFlag(argv, "--json")) {
return false;
}
const key = `${commandPath[0] ?? ""} ${commandPath[1] ?? ""}`.trim();
if (JSON_PARSE_ONLY_COMMANDS.has(key)) {
return false;
}
return true;
}
export function registerPreActionHooks(program: Command, programVersion: string) {
program.hook("preAction", async (_thisCommand, actionCommand) => {
setProcessTitleForCommand(actionCommand);
@@ -124,7 +108,8 @@ export function registerPreActionHooks(program: Command, programVersion: string)
return;
}
const commandPath = getCommandPathWithRootOptions(argv, 2);
if (isJsonOutputMode(commandPath, argv)) {
const jsonOutputMode = isCommandJsonOutputMode(actionCommand, argv);
if (jsonOutputMode) {
routeLogsToStderr();
}
const hideBanner =
@@ -147,15 +132,14 @@ export function registerPreActionHooks(program: Command, programVersion: string)
if (shouldBypassConfigGuard(commandPath)) {
return;
}
const suppressDoctorStdout = isJsonOutputMode(commandPath, argv);
const { ensureConfigReady } = await loadConfigGuardModule();
await ensureConfigReady({
runtime: defaultRuntime,
commandPath,
...(suppressDoctorStdout ? { suppressDoctorStdout: true } : {}),
...(jsonOutputMode ? { suppressDoctorStdout: true } : {}),
});
// Load plugins for commands that need channel access
if (shouldLoadPluginsForCommand(commandPath, argv)) {
if (shouldLoadPluginsForCommand(commandPath, jsonOutputMode)) {
const { ensurePluginRegistryLoaded } = await loadPluginRegistryModule();
ensurePluginRegistryLoaded({ scope: resolvePluginRegistryScope(commandPath) });
}

View File

@@ -225,18 +225,12 @@ export function registerQrCli(program: Command) {
}
if (opts.json) {
defaultRuntime.log(
JSON.stringify(
{
setupCode,
gatewayUrl: resolved.payload.url,
auth: resolved.authLabel,
urlSource: resolved.urlSource,
},
null,
2,
),
);
defaultRuntime.writeJson({
setupCode,
gatewayUrl: resolved.payload.url,
auth: resolved.authLabel,
urlSource: resolved.urlSource,
});
return;
}

View File

@@ -64,7 +64,7 @@ export function registerSecretsCli(program: Command) {
expectFinal: false,
});
if (opts.json) {
defaultRuntime.log(JSON.stringify(result, null, 2));
defaultRuntime.writeJson(result);
return;
}
const warningCount = Number(
@@ -97,7 +97,7 @@ export function registerSecretsCli(program: Command) {
allowExec: Boolean(opts.allowExec),
});
if (opts.json) {
defaultRuntime.log(JSON.stringify(report, null, 2));
defaultRuntime.writeJson(report);
} else {
defaultRuntime.log(
`Secrets audit: ${report.status}. plaintext=${report.summary.plaintextCount}, unresolved=${report.summary.unresolvedRefCount}, shadowed=${report.summary.shadowedRefCount}, legacy=${report.summary.legacyResidueCount}.`,
@@ -162,16 +162,10 @@ export function registerSecretsCli(program: Command) {
fs.writeFileSync(opts.planOut, `${JSON.stringify(configured.plan, null, 2)}\n`, "utf8");
}
if (opts.json) {
defaultRuntime.log(
JSON.stringify(
{
plan: configured.plan,
preflight: configured.preflight,
},
null,
2,
),
);
defaultRuntime.writeJson({
plan: configured.plan,
preflight: configured.preflight,
});
} else {
defaultRuntime.log(
`Preflight: changed=${configured.preflight.changed}, files=${configured.preflight.changedFiles.length}, warnings=${configured.preflight.warningCount}.`,
@@ -228,7 +222,7 @@ export function registerSecretsCli(program: Command) {
allowExec: Boolean(opts.allowExec),
});
if (opts.json) {
defaultRuntime.log(JSON.stringify(result, null, 2));
defaultRuntime.writeJson(result);
return;
}
defaultRuntime.log(
@@ -259,7 +253,7 @@ export function registerSecretsCli(program: Command) {
allowExec: Boolean(opts.allowExec),
});
if (opts.json) {
defaultRuntime.log(JSON.stringify(result, null, 2));
defaultRuntime.writeJson(result);
return;
}
if (opts.dryRun) {

View File

@@ -86,14 +86,10 @@ export function registerSecurityCli(program: Command) {
});
if (opts.json) {
defaultRuntime.log(
JSON.stringify(
fixResult
? { fix: fixResult, report, secretDiagnostics }
: { ...report, secretDiagnostics },
null,
2,
),
defaultRuntime.writeJson(
fixResult
? { fix: fixResult, report, secretDiagnostics }
: { ...report, secretDiagnostics },
);
return;
}

View File

@@ -71,7 +71,7 @@ export function registerSkillsCli(program: Command) {
limit: opts.limit,
});
if (opts.json) {
defaultRuntime.log(JSON.stringify({ results }, null, 2));
defaultRuntime.writeJson({ results });
return;
}
if (results.length === 0) {

View File

@@ -28,7 +28,7 @@ async function runSystemGatewayCommand(
try {
const result = await action();
if (opts.json || successText === undefined) {
defaultRuntime.log(JSON.stringify(result, null, 2));
defaultRuntime.writeJson(result);
} else {
defaultRuntime.log(successText);
}

View File

@@ -138,7 +138,7 @@ type PrintResultOptions = UpdateCommandOptions & {
export function printResult(result: UpdateRunResult, opts: PrintResultOptions): void {
if (opts.json) {
defaultRuntime.log(JSON.stringify(result, null, 2));
defaultRuntime.writeJson(result);
return;
}

View File

@@ -70,22 +70,16 @@ export async function updateStatusCommand(opts: UpdateStatusOptions): Promise<vo
const updateLine = formatUpdateOneLiner(update).replace(/^Update:\s*/i, "");
if (opts.json) {
defaultRuntime.log(
JSON.stringify(
{
update,
channel: {
value: channelInfo.channel,
source: channelInfo.source,
label: channelLabel,
config: configChannel,
},
availability: updateAvailability,
},
null,
2,
),
);
defaultRuntime.writeJson({
update,
channel: {
value: channelInfo.channel,
source: channelInfo.source,
label: channelLabel,
config: configChannel,
},
availability: updateAvailability,
});
return;
}

View File

@@ -178,7 +178,7 @@ type UpdateDryRunPreview = {
function printDryRunPreview(preview: UpdateDryRunPreview, jsonMode: boolean): void {
if (jsonMode) {
defaultRuntime.log(JSON.stringify(preview, null, 2));
defaultRuntime.writeJson(preview);
return;
}

View File

@@ -6,7 +6,7 @@ import { withProgress } from "../cli/progress.js";
import { loadConfig } from "../config/config.js";
import { callGateway, randomIdempotencyKey } from "../gateway/call.js";
import { normalizeAgentId } from "../routing/session-key.js";
import type { RuntimeEnv } from "../runtime.js";
import { type RuntimeEnv, writeRuntimeJson } from "../runtime.js";
import {
GATEWAY_CLIENT_MODES,
GATEWAY_CLIENT_NAMES,
@@ -156,7 +156,7 @@ export async function agentViaGatewayCommand(opts: AgentCliOpts, runtime: Runtim
);
if (opts.json) {
runtime.log(JSON.stringify(response, null, 2));
writeRuntimeJson(runtime, response);
return response;
}

View File

@@ -10,7 +10,7 @@ import { resolveAuthStorePath } from "../agents/auth-profiles/paths.js";
import { writeConfigFile } from "../config/config.js";
import { logConfigUpdated } from "../config/logging.js";
import { DEFAULT_AGENT_ID, normalizeAgentId } from "../routing/session-key.js";
import type { RuntimeEnv } from "../runtime.js";
import { type RuntimeEnv, writeRuntimeJson } from "../runtime.js";
import { defaultRuntime } from "../runtime.js";
import { resolveUserPath, shortenHomePath } from "../utils.js";
import { createClackPrompter } from "../wizard/clack-prompter.js";
@@ -153,7 +153,7 @@ export async function agentsAddCommand(
},
};
if (opts.json) {
runtime.log(JSON.stringify(payload, null, 2));
writeRuntimeJson(runtime, payload);
} else {
runtime.log(`Agent: ${agentId}`);
runtime.log(`Workspace: ${shortenHomePath(workspaceDir)}`);
@@ -356,7 +356,7 @@ export async function agentsAddCommand(
agentDir,
};
if (opts.json) {
runtime.log(JSON.stringify(payload, null, 2));
writeRuntimeJson(runtime, payload);
}
await prompter.outro(`Agent "${agentId}" ready.`);
} catch (err) {

View File

@@ -4,7 +4,7 @@ import { writeConfigFile } from "../config/config.js";
import { logConfigUpdated } from "../config/logging.js";
import type { AgentRouteBinding } from "../config/types.js";
import { normalizeAgentId } from "../routing/session-key.js";
import type { RuntimeEnv } from "../runtime.js";
import { type RuntimeEnv, writeRuntimeJson } from "../runtime.js";
import { defaultRuntime } from "../runtime.js";
import {
applyAgentBindings,
@@ -122,7 +122,7 @@ function emitJsonPayload(params: {
if (!params.json) {
return false;
}
params.runtime.log(JSON.stringify(params.payload, null, 2));
writeRuntimeJson(params.runtime, params.payload);
if ((params.conflictCount ?? 0) > 0) {
params.runtime.exit(1);
}
@@ -176,16 +176,13 @@ export async function agentsBindingsCommand(
(binding) => !filterAgentId || normalizeAgentId(binding.agentId) === filterAgentId,
);
if (opts.json) {
runtime.log(
JSON.stringify(
filtered.map((binding) => ({
agentId: normalizeAgentId(binding.agentId),
match: binding.match,
description: describeBinding(binding),
})),
null,
2,
),
writeRuntimeJson(
runtime,
filtered.map((binding) => ({
agentId: normalizeAgentId(binding.agentId),
match: binding.match,
description: describeBinding(binding),
})),
);
return;
}

View File

@@ -3,7 +3,7 @@ import { writeConfigFile } from "../config/config.js";
import { logConfigUpdated } from "../config/logging.js";
import { resolveSessionTranscriptsDirForAgent } from "../config/sessions.js";
import { DEFAULT_AGENT_ID, normalizeAgentId } from "../routing/session-key.js";
import type { RuntimeEnv } from "../runtime.js";
import { type RuntimeEnv, writeRuntimeJson } from "../runtime.js";
import { defaultRuntime } from "../runtime.js";
import { createClackPrompter } from "../wizard/clack-prompter.js";
import { createQuietRuntime, requireValidConfig } from "./agents.command-shared.js";
@@ -81,20 +81,14 @@ export async function agentsDeleteCommand(
await moveToTrash(sessionsDir, quietRuntime);
if (opts.json) {
runtime.log(
JSON.stringify(
{
agentId,
workspace: workspaceDir,
agentDir,
sessionsDir,
removedBindings: result.removedBindings,
removedAllow: result.removedAllow,
},
null,
2,
),
);
writeRuntimeJson(runtime, {
agentId,
workspace: workspaceDir,
agentDir,
sessionsDir,
removedBindings: result.removedBindings,
removedAllow: result.removedAllow,
});
} else {
runtime.log(`Deleted agent: ${agentId}`);
}

View File

@@ -7,7 +7,7 @@ import { writeConfigFile } from "../config/config.js";
import { logConfigUpdated } from "../config/logging.js";
import type { IdentityConfig } from "../config/types.js";
import { normalizeAgentId } from "../routing/session-key.js";
import type { RuntimeEnv } from "../runtime.js";
import { type RuntimeEnv, writeRuntimeJson } from "../runtime.js";
import { defaultRuntime } from "../runtime.js";
import { resolveUserPath, shortenHomePath } from "../utils.js";
import { requireValidConfig } from "./agents.command-shared.js";
@@ -198,18 +198,12 @@ export async function agentsSetIdentityCommand(
await writeConfigFile(nextConfig);
if (opts.json) {
runtime.log(
JSON.stringify(
{
agentId,
identity: nextIdentity,
workspace: workspaceDir ?? null,
identityFile: identityFilePath ?? null,
},
null,
2,
),
);
writeRuntimeJson(runtime, {
agentId,
identity: nextIdentity,
workspace: workspaceDir ?? null,
identityFile: identityFilePath ?? null,
});
return;
}

View File

@@ -2,7 +2,7 @@ import { formatCliCommand } from "../cli/command-format.js";
import { listRouteBindings } from "../config/bindings.js";
import type { AgentRouteBinding } from "../config/types.js";
import { normalizeAgentId } from "../routing/session-key.js";
import type { RuntimeEnv } from "../runtime.js";
import { type RuntimeEnv, writeRuntimeJson } from "../runtime.js";
import { defaultRuntime } from "../runtime.js";
import { shortenHomePath } from "../utils.js";
import { describeBinding } from "./agents.bindings.js";
@@ -122,7 +122,7 @@ export async function agentsListCommand(
}
if (opts.json) {
runtime.log(JSON.stringify(summaries, null, 2));
writeRuntimeJson(runtime, summaries);
return;
}

View File

@@ -1,6 +1,6 @@
import path from "node:path";
import * as tar from "tar";
import type { RuntimeEnv } from "../runtime.js";
import { type RuntimeEnv, writeRuntimeJson } from "../runtime.js";
import { resolveUserPath } from "../utils.js";
const WINDOWS_ABSOLUTE_ARCHIVE_PATH_RE = /^[A-Za-z]:[\\/]/;
@@ -319,6 +319,10 @@ export async function backupVerifyCommand(
entryCount: rawEntries.length,
};
runtime.log(opts.json ? JSON.stringify(result, null, 2) : formatResult(result));
if (opts.json) {
writeRuntimeJson(runtime, result);
} else {
runtime.log(formatResult(result));
}
return result;
}

View File

@@ -4,7 +4,7 @@ import {
type BackupCreateOptions,
type BackupCreateResult,
} from "../infra/backup-create.js";
import type { RuntimeEnv } from "../runtime.js";
import { type RuntimeEnv, writeRuntimeJson } from "../runtime.js";
import { backupVerifyCommand } from "./backup-verify.js";
export type { BackupCreateOptions, BackupCreateResult } from "../infra/backup-create.js";
@@ -23,9 +23,10 @@ export async function backupCreateCommand(
);
result.verified = true;
}
const output = opts.json
? JSON.stringify(result, null, 2)
: formatBackupCreateSummary(result).join("\n");
runtime.log(output);
if (opts.json) {
writeRuntimeJson(runtime, result);
} else {
runtime.log(formatBackupCreateSummary(result).join("\n"));
}
return result;
}

View File

@@ -12,7 +12,7 @@ import type {
} from "../../channels/plugins/types.js";
import { writeConfigFile, type OpenClawConfig } from "../../config/config.js";
import { danger } from "../../globals.js";
import { defaultRuntime, type RuntimeEnv } from "../../runtime.js";
import { defaultRuntime, type RuntimeEnv, writeRuntimeJson } from "../../runtime.js";
import { theme } from "../../terminal/theme.js";
import { resolveInstallableChannelPlugin } from "../channel-setup/channel-plugin-resolution.js";
import { formatChannelAccountLabel, requireValidConfig } from "./shared.js";
@@ -266,7 +266,7 @@ export async function channelsCapabilitiesCommand(
}
if (opts.json) {
runtime.log(JSON.stringify({ channels: reports }, null, 2));
writeRuntimeJson(runtime, { channels: reports });
return;
}

View File

@@ -4,7 +4,7 @@ import { buildChannelAccountSnapshot } from "../../channels/plugins/status.js";
import type { ChannelAccountSnapshot, ChannelPlugin } from "../../channels/plugins/types.js";
import { withProgress } from "../../cli/progress.js";
import { formatUsageReportLines, loadProviderUsageSummary } from "../../infra/provider-usage.js";
import { defaultRuntime, type RuntimeEnv } from "../../runtime.js";
import { defaultRuntime, type RuntimeEnv, writeRuntimeJson } from "../../runtime.js";
import { formatDocsLink } from "../../terminal/links.js";
import { theme } from "../../terminal/theme.js";
import { formatChannelAccountLabel, requireValidConfig } from "./shared.js";
@@ -126,7 +126,7 @@ export async function channelsListCommand(
chat[plugin.id] = plugin.config.listAccountIds(cfg);
}
const payload = { chat, auth: authProfiles, ...(usage ? { usage } : {}) };
runtime.log(JSON.stringify(payload, null, 2));
writeRuntimeJson(runtime, payload);
return;
}

View File

@@ -2,7 +2,7 @@ import fs from "node:fs/promises";
import { listChannelPlugins } from "../../channels/plugins/index.js";
import { getResolvedLoggerSettings } from "../../logging.js";
import { parseLogLine } from "../../logging/parse-log-line.js";
import { defaultRuntime, type RuntimeEnv } from "../../runtime.js";
import { defaultRuntime, type RuntimeEnv, writeRuntimeJson } from "../../runtime.js";
import { theme } from "../../terminal/theme.js";
export type ChannelsLogsOptions = {
@@ -93,7 +93,7 @@ export async function channelsLogsCommand(
const lines = filtered.slice(Math.max(0, filtered.length - limit));
if (opts.json) {
runtime.log(JSON.stringify({ file, channel, lines }, null, 2));
writeRuntimeJson(runtime, { file, channel, lines });
return;
}

View File

@@ -5,7 +5,7 @@ import { getChannelsCommandSecretTargetIds } from "../../cli/command-secret-targ
import { loadConfig, writeConfigFile } from "../../config/config.js";
import { danger } from "../../globals.js";
import { resolveMessageChannelSelection } from "../../infra/outbound/channel-selection.js";
import type { RuntimeEnv } from "../../runtime.js";
import { type RuntimeEnv, writeRuntimeJson } from "../../runtime.js";
import { resolveInstallableChannelPlugin } from "../channel-setup/channel-plugin-resolution.js";
export type ChannelsResolveOptions = {
@@ -166,7 +166,7 @@ export async function channelsResolveCommand(opts: ChannelsResolveOptions, runti
}
if (opts.json) {
runtime.log(JSON.stringify(results, null, 2));
writeRuntimeJson(runtime, results);
return;
}

View File

@@ -16,7 +16,7 @@ import { type OpenClawConfig, readConfigFileSnapshot } from "../../config/config
import { callGateway } from "../../gateway/call.js";
import { collectChannelStatusIssues } from "../../infra/channels-status-issues.js";
import { formatTimeAgo } from "../../infra/format-time/format-relative.ts";
import { defaultRuntime, type RuntimeEnv } from "../../runtime.js";
import { defaultRuntime, type RuntimeEnv, writeRuntimeJson } from "../../runtime.js";
import { formatDocsLink } from "../../terminal/links.js";
import { theme } from "../../terminal/theme.js";
import {
@@ -301,7 +301,7 @@ export async function channelsStatusCommand(
}),
);
if (opts.json) {
runtime.log(JSON.stringify(payload, null, 2));
writeRuntimeJson(runtime, payload);
return;
}
runtime.log(formatGatewayChannelsStatusLines(payload).join("\n"));

View File

@@ -3,7 +3,7 @@ import { readBestEffortConfig, resolveGatewayPort } from "../config/config.js";
import { probeGateway } from "../gateway/probe.js";
import { discoverGatewayBeacons } from "../infra/bonjour-discovery.js";
import { resolveWideAreaDiscoveryDomain } from "../infra/widearea-dns.js";
import type { RuntimeEnv } from "../runtime.js";
import { type RuntimeEnv, writeRuntimeJson } from "../runtime.js";
import { colorize, isRich, theme } from "../terminal/theme.js";
import {
buildNetworkHints,
@@ -264,61 +264,55 @@ export async function gatewayStatusCommand(
}
if (opts.json) {
runtime.log(
JSON.stringify(
{
ok,
degraded,
ts: Date.now(),
durationMs: Date.now() - startedAt,
timeoutMs: overallTimeoutMs,
primaryTargetId: primary?.target.id ?? null,
warnings,
network,
discovery: {
timeoutMs: discoveryTimeoutMs,
count: discovery.length,
beacons: discovery.map((b) => ({
instanceName: b.instanceName,
displayName: b.displayName ?? null,
domain: b.domain ?? null,
host: b.host ?? null,
lanHost: b.lanHost ?? null,
tailnetDns: b.tailnetDns ?? null,
gatewayPort: b.gatewayPort ?? null,
sshPort: b.sshPort ?? null,
wsUrl: (() => {
const host = b.tailnetDns || b.lanHost || b.host;
const port = b.gatewayPort ?? 18789;
return host ? `ws://${host}:${port}` : null;
})(),
})),
},
targets: probed.map((p) => ({
id: p.target.id,
kind: p.target.kind,
url: p.target.url,
active: p.target.active,
tunnel: p.target.tunnel ?? null,
connect: {
ok: isProbeReachable(p.probe),
rpcOk: p.probe.ok,
scopeLimited: isScopeLimitedProbeFailure(p.probe),
latencyMs: p.probe.connectLatencyMs,
error: p.probe.error,
close: p.probe.close,
},
self: p.self,
config: p.configSummary,
health: p.probe.health,
summary: p.probe.status,
presence: p.probe.presence,
})),
writeRuntimeJson(runtime, {
ok,
degraded,
ts: Date.now(),
durationMs: Date.now() - startedAt,
timeoutMs: overallTimeoutMs,
primaryTargetId: primary?.target.id ?? null,
warnings,
network,
discovery: {
timeoutMs: discoveryTimeoutMs,
count: discovery.length,
beacons: discovery.map((b) => ({
instanceName: b.instanceName,
displayName: b.displayName ?? null,
domain: b.domain ?? null,
host: b.host ?? null,
lanHost: b.lanHost ?? null,
tailnetDns: b.tailnetDns ?? null,
gatewayPort: b.gatewayPort ?? null,
sshPort: b.sshPort ?? null,
wsUrl: (() => {
const host = b.tailnetDns || b.lanHost || b.host;
const port = b.gatewayPort ?? 18789;
return host ? `ws://${host}:${port}` : null;
})(),
})),
},
targets: probed.map((p) => ({
id: p.target.id,
kind: p.target.kind,
url: p.target.url,
active: p.target.active,
tunnel: p.target.tunnel ?? null,
connect: {
ok: isProbeReachable(p.probe),
rpcOk: p.probe.ok,
scopeLimited: isScopeLimitedProbeFailure(p.probe),
latencyMs: p.probe.connectLatencyMs,
error: p.probe.error,
close: p.probe.close,
},
null,
2,
),
);
self: p.self,
config: p.configSummary,
health: p.probe.health,
summary: p.probe.status,
presence: p.probe.presence,
})),
});
if (!ok) {
runtime.exit(1);
}

View File

@@ -17,7 +17,7 @@ import {
} from "../infra/heartbeat-summary.js";
import { buildChannelAccountBindings, resolvePreferredAccountId } from "../routing/bindings.js";
import { normalizeAgentId } from "../routing/session-key.js";
import type { RuntimeEnv } from "../runtime.js";
import { type RuntimeEnv, writeRuntimeJson } from "../runtime.js";
import { styleHealthChannelLine } from "../terminal/health-style.js";
import { isRich } from "../terminal/theme.js";
@@ -618,7 +618,7 @@ export async function healthCommand(
const fatal = false;
if (opts.json) {
runtime.log(JSON.stringify(summary, null, 2));
writeRuntimeJson(runtime, summary);
} else {
const debugEnabled = isTruthyEnvValue(process.env.OPENCLAW_DEBUG_HEALTH);
const rich = isRich();

View File

@@ -10,7 +10,7 @@ import { withProgress } from "../cli/progress.js";
import { loadConfig } from "../config/config.js";
import type { OutboundSendDeps } from "../infra/outbound/deliver.js";
import { runMessageAction } from "../infra/outbound/message-action-runner.js";
import type { RuntimeEnv } from "../runtime.js";
import { type RuntimeEnv, writeRuntimeJson } from "../runtime.js";
import { GATEWAY_CLIENT_MODES, GATEWAY_CLIENT_NAMES } from "../utils/message-channel.js";
import { buildMessageCliJson, formatMessageCliText } from "./message-format.js";
@@ -80,7 +80,7 @@ export async function messageCommand(
: await run();
if (json) {
runtime.log(JSON.stringify(buildMessageCliJson(result), null, 2));
writeRuntimeJson(runtime, buildMessageCliJson(result));
return;
}

View File

@@ -1,5 +1,5 @@
import { logConfigUpdated } from "../../config/logging.js";
import type { RuntimeEnv } from "../../runtime.js";
import { type RuntimeEnv, writeRuntimeJson } from "../../runtime.js";
import { loadModelsConfig } from "./load-config.js";
import {
ensureFlagCompatibility,
@@ -27,7 +27,7 @@ export async function modelsAliasesListCommand(
);
if (opts.json) {
runtime.log(JSON.stringify({ aliases }, null, 2));
writeRuntimeJson(runtime, { aliases });
return;
}
if (opts.plain) {

View File

@@ -5,7 +5,7 @@ import {
setAuthProfileOrder,
} from "../../agents/auth-profiles.js";
import { normalizeProviderId } from "../../agents/model-selection.js";
import type { RuntimeEnv } from "../../runtime.js";
import { type RuntimeEnv, writeRuntimeJson } from "../../runtime.js";
import { normalizeStringEntries } from "../../shared/string-normalization.js";
import { shortenHomePath } from "../../utils.js";
import { loadModelsConfig } from "./load-config.js";
@@ -54,19 +54,13 @@ export async function modelsAuthOrderGetCommand(
const order = describeOrder(store, provider);
if (opts.json) {
runtime.log(
JSON.stringify(
{
agentId,
agentDir,
provider,
authStorePath: shortenHomePath(`${agentDir}/auth-profiles.json`),
order: order.length > 0 ? order : null,
},
null,
2,
),
);
writeRuntimeJson(runtime, {
agentId,
agentDir,
provider,
authStorePath: shortenHomePath(`${agentDir}/auth-profiles.json`),
order: order.length > 0 ? order : null,
});
return;
}

View File

@@ -3,7 +3,7 @@ import type { OpenClawConfig } from "../../config/config.js";
import { logConfigUpdated } from "../../config/logging.js";
import { resolveAgentModelFallbackValues, toAgentModelListLike } from "../../config/model-input.js";
import type { AgentModelEntryConfig } from "../../config/types.agent-defaults.js";
import type { RuntimeEnv } from "../../runtime.js";
import { type RuntimeEnv, writeRuntimeJson } from "../../runtime.js";
import { loadModelsConfig } from "./load-config.js";
import {
DEFAULT_PROVIDER,
@@ -50,7 +50,7 @@ export async function listFallbacksCommand(
const fallbacks = getFallbacks(cfg, params.key);
if (opts.json) {
runtime.log(JSON.stringify({ fallbacks }, null, 2));
writeRuntimeJson(runtime, { fallbacks });
return;
}
if (opts.plain) {

View File

@@ -36,7 +36,7 @@ import {
type UsageProviderId,
} from "../../infra/provider-usage.js";
import { getShellEnvAppliedKeys, shouldEnableShellEnvFallback } from "../../infra/shell-env.js";
import type { RuntimeEnv } from "../../runtime.js";
import { type RuntimeEnv, writeRuntimeJson } from "../../runtime.js";
import { getTerminalTableWidth, renderTable } from "../../terminal/table.js";
import { colorize, theme } from "../../terminal/theme.js";
import { shortenHomePath } from "../../utils.js";
@@ -324,49 +324,43 @@ export async function modelsStatusCommand(
})();
if (opts.json) {
runtime.log(
JSON.stringify(
{
configPath,
...(agentId ? { agentId } : {}),
agentDir,
defaultModel: defaultLabel,
resolvedDefault: resolvedLabel,
fallbacks,
imageModel: imageModel || null,
imageFallbacks,
...(agentId
? {
modelConfig: {
defaultSource: agentModelPrimary ? "agent" : "defaults",
fallbacksSource: agentFallbacksOverride !== undefined ? "agent" : "defaults",
},
}
: {}),
aliases,
allowed,
auth: {
storePath: resolveAuthStorePathForDisplay(agentDir),
shellEnvFallback: {
enabled: shellFallbackEnabled,
appliedKeys: applied,
writeRuntimeJson(runtime, {
configPath,
...(agentId ? { agentId } : {}),
agentDir,
defaultModel: defaultLabel,
resolvedDefault: resolvedLabel,
fallbacks,
imageModel: imageModel || null,
imageFallbacks,
...(agentId
? {
modelConfig: {
defaultSource: agentModelPrimary ? "agent" : "defaults",
fallbacksSource: agentFallbacksOverride !== undefined ? "agent" : "defaults",
},
providersWithOAuth: providersWithOauth,
missingProvidersInUse,
providers: providerAuth,
unusableProfiles,
oauth: {
warnAfterMs: authHealth.warnAfterMs,
profiles: authHealth.profiles,
providers: authHealth.providers,
},
probes: probeSummary,
},
}
: {}),
aliases,
allowed,
auth: {
storePath: resolveAuthStorePathForDisplay(agentDir),
shellEnvFallback: {
enabled: shellFallbackEnabled,
appliedKeys: applied,
},
null,
2,
),
);
providersWithOAuth: providersWithOauth,
missingProvidersInUse,
providers: providerAuth,
unusableProfiles,
oauth: {
warnAfterMs: authHealth.warnAfterMs,
profiles: authHealth.profiles,
providers: authHealth.providers,
},
probes: probeSummary,
},
});
if (opts.check) {
runtime.exit(checkStatus);
}

View File

@@ -1,4 +1,4 @@
import type { RuntimeEnv } from "../../runtime.js";
import { type RuntimeEnv, writeRuntimeJson } from "../../runtime.js";
import { colorize, theme } from "../../terminal/theme.js";
import { formatTag, isRich, pad, truncate } from "./list.format.js";
import type { ModelRow } from "./list.types.js";
@@ -16,16 +16,10 @@ export function printModelTable(
opts: { json?: boolean; plain?: boolean } = {},
) {
if (opts.json) {
runtime.log(
JSON.stringify(
{
count: rows.length,
models: rows,
},
null,
2,
),
);
writeRuntimeJson(runtime, {
count: rows.length,
models: rows,
});
return;
}

View File

@@ -4,7 +4,7 @@ import { type ModelScanResult, scanOpenRouterModels } from "../../agents/model-s
import { withProgressTotals } from "../../cli/progress.js";
import { logConfigUpdated } from "../../config/logging.js";
import { toAgentModelListLike } from "../../config/model-input.js";
import type { RuntimeEnv } from "../../runtime.js";
import { type RuntimeEnv, writeRuntimeJson } from "../../runtime.js";
import {
stylePromptHint,
stylePromptMessage,
@@ -217,7 +217,7 @@ export async function modelsScanCommand(
);
printScanTable(sortScanResults(results), runtime);
} else {
runtime.log(JSON.stringify(results, null, 2));
writeRuntimeJson(runtime, results);
}
return;
}
@@ -328,20 +328,14 @@ export async function modelsScanCommand(
});
if (opts.json) {
runtime.log(
JSON.stringify(
{
selected,
selectedImages,
setDefault: Boolean(opts.setDefault),
setImage: Boolean(opts.setImage),
results,
warnings: [],
},
null,
2,
),
);
writeRuntimeJson(runtime, {
selected,
selectedImages,
setDefault: Boolean(opts.setDefault),
setImage: Boolean(opts.setImage),
results,
warnings: [],
});
return;
}

View File

@@ -1,4 +1,4 @@
import type { RuntimeEnv } from "../../../runtime.js";
import { type RuntimeEnv, writeRuntimeJson } from "../../../runtime.js";
import type { OnboardOptions } from "../../onboard-types.js";
export type GatewayHealthFailureDiagnostics = {
@@ -41,24 +41,18 @@ export function logNonInteractiveOnboardingJson(params: {
if (!params.opts.json) {
return;
}
params.runtime.log(
JSON.stringify(
{
ok: true,
mode: params.mode,
workspace: params.workspaceDir,
authChoice: params.authChoice,
gateway: params.gateway,
installDaemon: Boolean(params.installDaemon),
daemonInstall: params.daemonInstall,
daemonRuntime: params.daemonRuntime,
skipSkills: Boolean(params.skipSkills),
skipHealth: Boolean(params.skipHealth),
},
null,
2,
),
);
writeRuntimeJson(params.runtime, {
ok: true,
mode: params.mode,
workspace: params.workspaceDir,
authChoice: params.authChoice,
gateway: params.gateway,
installDaemon: Boolean(params.installDaemon),
daemonInstall: params.daemonInstall,
daemonRuntime: params.daemonRuntime,
skipSkills: Boolean(params.skipSkills),
skipHealth: Boolean(params.skipHealth),
});
}
function formatGatewayRuntimeSummary(
@@ -109,25 +103,19 @@ export function logNonInteractiveOnboardingFailure(params: {
const gatewayRuntime = formatGatewayRuntimeSummary(params.diagnostics);
if (params.opts.json) {
params.runtime.error(
JSON.stringify(
{
ok: false,
mode: params.mode,
phase: params.phase,
message: params.message,
detail: params.detail,
gateway: params.gateway,
installDaemon: Boolean(params.installDaemon),
daemonInstall: params.daemonInstall,
daemonRuntime: params.daemonRuntime,
diagnostics: params.diagnostics,
hints: hints.length > 0 ? hints : undefined,
},
null,
2,
),
);
writeRuntimeJson(params.runtime, {
ok: false,
mode: params.mode,
phase: params.phase,
message: params.message,
detail: params.detail,
gateway: params.gateway,
installDaemon: Boolean(params.installDaemon),
daemonInstall: params.daemonInstall,
daemonRuntime: params.daemonRuntime,
diagnostics: params.diagnostics,
hints: hints.length > 0 ? hints : undefined,
});
return;
}

View File

@@ -2,7 +2,7 @@ import { formatCliCommand } from "../../cli/command-format.js";
import type { OpenClawConfig } from "../../config/config.js";
import { writeConfigFile } from "../../config/config.js";
import { logConfigUpdated } from "../../config/logging.js";
import type { RuntimeEnv } from "../../runtime.js";
import { type RuntimeEnv, writeRuntimeJson } from "../../runtime.js";
import { applyWizardMetadata } from "../onboard-helpers.js";
import type { OnboardOptions } from "../onboard-types.js";
@@ -42,7 +42,7 @@ export async function runNonInteractiveRemoteSetup(params: {
auth: opts.remoteToken ? "token" : "none",
};
if (opts.json) {
runtime.log(JSON.stringify(payload, null, 2));
writeRuntimeJson(runtime, payload);
} else {
runtime.log(`Remote gateway: ${remoteUrl}`);
runtime.log(`Auth: ${payload.auth}`);

View File

@@ -19,7 +19,7 @@ import {
parseAgentSessionKey,
resolveAgentIdFromSessionKey,
} from "../routing/session-key.js";
import type { RuntimeEnv } from "../runtime.js";
import { type RuntimeEnv, writeRuntimeJson } from "../runtime.js";
import { formatDocsLink } from "../terminal/links.js";
import { colorize, isRich, theme } from "../terminal/theme.js";
import { INTERNAL_MESSAGE_CHANNEL } from "../utils/message-channel.js";
@@ -262,7 +262,7 @@ export async function sandboxExplainCommand(
} as const;
if (opts.json) {
runtime.log(`${JSON.stringify(payload, null, 2)}\n`);
writeRuntimeJson(runtime, payload);
return;
}

View File

@@ -7,7 +7,7 @@ import {
type SandboxBrowserInfo,
type SandboxContainerInfo,
} from "../agents/sandbox.js";
import type { RuntimeEnv } from "../runtime.js";
import { type RuntimeEnv, writeRuntimeJson } from "../runtime.js";
import {
displayBrowsers,
displayContainers,
@@ -48,7 +48,7 @@ export async function sandboxListCommand(
const browsers = opts.browser ? await listSandboxBrowsers().catch(() => []) : [];
if (opts.json) {
runtime.log(JSON.stringify({ containers, browsers }, null, 2));
writeRuntimeJson(runtime, { containers, browsers });
return;
}

View File

@@ -12,7 +12,7 @@ import {
type SessionEntry,
type SessionMaintenanceApplyReport,
} from "../config/sessions.js";
import type { RuntimeEnv } from "../runtime.js";
import { type RuntimeEnv, writeRuntimeJson } from "../runtime.js";
import { isRich, theme } from "../terminal/theme.js";
import {
resolveSessionStoreTargetsOrExit,
@@ -325,21 +325,15 @@ export async function sessionsCleanupCommand(opts: SessionsCleanupOptions, runti
if (opts.dryRun) {
if (opts.json) {
if (previewResults.length === 1) {
runtime.log(JSON.stringify(previewResults[0]?.summary ?? {}, null, 2));
writeRuntimeJson(runtime, previewResults[0]?.summary ?? {});
return;
}
runtime.log(
JSON.stringify(
{
allAgents: true,
mode,
dryRun: true,
stores: previewResults.map((result) => result.summary),
},
null,
2,
),
);
writeRuntimeJson(runtime, {
allAgents: true,
mode,
dryRun: true,
stores: previewResults.map((result) => result.summary),
});
return;
}
@@ -436,21 +430,15 @@ export async function sessionsCleanupCommand(opts: SessionsCleanupOptions, runti
if (opts.json) {
if (appliedSummaries.length === 1) {
runtime.log(JSON.stringify(appliedSummaries[0] ?? {}, null, 2));
writeRuntimeJson(runtime, appliedSummaries[0] ?? {});
return;
}
runtime.log(
JSON.stringify(
{
allAgents: true,
mode,
dryRun: false,
stores: appliedSummaries,
},
null,
2,
),
);
writeRuntimeJson(runtime, {
allAgents: true,
mode,
dryRun: false,
stores: appliedSummaries,
});
return;
}

View File

@@ -5,7 +5,7 @@ import { loadSessionStore, resolveFreshSessionTotalTokens } from "../config/sess
import { classifySessionKey } from "../gateway/session-utils.js";
import { info } from "../globals.js";
import { parseAgentSessionKey } from "../routing/session-key.js";
import type { RuntimeEnv } from "../runtime.js";
import { type RuntimeEnv, writeRuntimeJson } from "../runtime.js";
import { isRich, theme } from "../terminal/theme.js";
import { resolveSessionStoreTargetsOrExit } from "./session-store-targets.js";
import {
@@ -142,36 +142,30 @@ export async function sessionsCommand(
if (opts.json) {
const multi = targets.length > 1;
const aggregate = aggregateAgents || multi;
runtime.log(
JSON.stringify(
{
path: aggregate ? null : (targets[0]?.storePath ?? null),
stores: aggregate
? targets.map((target) => ({
agentId: target.agentId,
path: target.storePath,
}))
: undefined,
allAgents: aggregateAgents ? true : undefined,
count: rows.length,
activeMinutes: activeMinutes ?? null,
sessions: rows.map((r) => {
const model = resolveSessionDisplayModel(cfg, r, displayDefaults);
return {
...r,
totalTokens: resolveFreshSessionTotalTokens(r) ?? null,
totalTokensFresh:
typeof r.totalTokens === "number" ? r.totalTokensFresh !== false : false,
contextTokens:
r.contextTokens ?? lookupContextTokens(model) ?? configContextTokens ?? null,
model,
};
}),
},
null,
2,
),
);
writeRuntimeJson(runtime, {
path: aggregate ? null : (targets[0]?.storePath ?? null),
stores: aggregate
? targets.map((target) => ({
agentId: target.agentId,
path: target.storePath,
}))
: undefined,
allAgents: aggregateAgents ? true : undefined,
count: rows.length,
activeMinutes: activeMinutes ?? null,
sessions: rows.map((r) => {
const model = resolveSessionDisplayModel(cfg, r, displayDefaults);
return {
...r,
totalTokens: resolveFreshSessionTotalTokens(r) ?? null,
totalTokensFresh:
typeof r.totalTokens === "number" ? r.totalTokensFresh !== false : false,
contextTokens:
r.contextTokens ?? lookupContextTokens(model) ?? configContextTokens ?? null,
model,
};
}),
});
return;
}

View File

@@ -1,6 +1,6 @@
import type { HeartbeatEventPayload } from "../infra/heartbeat-events.js";
import { normalizeUpdateChannel, resolveUpdateChannelDisplay } from "../infra/update-channels.js";
import type { RuntimeEnv } from "../runtime.js";
import { type RuntimeEnv, writeRuntimeJson } from "../runtime.js";
import { getDaemonStatusSummary, getNodeDaemonStatusSummary } from "./status.daemon.js";
import { scanStatusJsonFast } from "./status.scan.fast-json.js";
@@ -83,36 +83,30 @@ export async function statusJsonCommand(
gitBranch: scan.update.git?.branch ?? null,
});
runtime.log(
JSON.stringify(
{
...scan.summary,
os: scan.osSummary,
update: scan.update,
updateChannel: channelInfo.channel,
updateChannelSource: channelInfo.source,
memory: scan.memory,
memoryPlugin: scan.memoryPlugin,
gateway: {
mode: scan.gatewayMode,
url: scan.gatewayConnection.url,
urlSource: scan.gatewayConnection.urlSource,
misconfigured: scan.remoteUrlMissing,
reachable: scan.gatewayReachable,
connectLatencyMs: scan.gatewayProbe?.connectLatencyMs ?? null,
self: scan.gatewaySelf,
error: scan.gatewayProbe?.error ?? null,
authWarning: scan.gatewayProbeAuthWarning ?? null,
},
gatewayService: daemon,
nodeService: nodeDaemon,
agents: scan.agentStatus,
secretDiagnostics: scan.secretDiagnostics,
...(securityAudit ? { securityAudit } : {}),
...(health || usage || lastHeartbeat ? { health, usage, lastHeartbeat } : {}),
},
null,
2,
),
);
writeRuntimeJson(runtime, {
...scan.summary,
os: scan.osSummary,
update: scan.update,
updateChannel: channelInfo.channel,
updateChannelSource: channelInfo.source,
memory: scan.memory,
memoryPlugin: scan.memoryPlugin,
gateway: {
mode: scan.gatewayMode,
url: scan.gatewayConnection.url,
urlSource: scan.gatewayConnection.urlSource,
misconfigured: scan.remoteUrlMissing,
reachable: scan.gatewayReachable,
connectLatencyMs: scan.gatewayProbe?.connectLatencyMs ?? null,
self: scan.gatewaySelf,
error: scan.gatewayProbe?.error ?? null,
authWarning: scan.gatewayProbeAuthWarning ?? null,
},
gatewayService: daemon,
nodeService: nodeDaemon,
agents: scan.agentStatus,
secretDiagnostics: scan.secretDiagnostics,
...(securityAudit ? { securityAudit } : {}),
...(health || usage || lastHeartbeat ? { health, usage, lastHeartbeat } : {}),
});
}

View File

@@ -17,7 +17,7 @@ import {
formatPluginCompatibilityNotice,
summarizePluginCompatibility,
} from "../plugins/status.js";
import type { RuntimeEnv } from "../runtime.js";
import { type RuntimeEnv, writeRuntimeJson } from "../runtime.js";
import { getTerminalTableWidth, renderTable } from "../terminal/table.js";
import { theme } from "../terminal/theme.js";
import { formatHealthChannelLines, type HealthSummary } from "./health.js";
@@ -196,42 +196,36 @@ export async function statusCommand(
getDaemonStatusSummary(),
getNodeDaemonStatusSummary(),
]);
runtime.log(
JSON.stringify(
{
...summary,
os: osSummary,
update,
updateChannel: channelInfo.channel,
updateChannelSource: channelInfo.source,
memory,
memoryPlugin,
gateway: {
mode: gatewayMode,
url: gatewayConnection.url,
urlSource: gatewayConnection.urlSource,
misconfigured: remoteUrlMissing,
reachable: gatewayReachable,
connectLatencyMs: gatewayProbe?.connectLatencyMs ?? null,
self: gatewaySelf,
error: gatewayProbe?.error ?? null,
authWarning: gatewayProbeAuthWarning ?? null,
},
gatewayService: daemon,
nodeService: nodeDaemon,
agents: agentStatus,
securityAudit,
secretDiagnostics,
pluginCompatibility: {
count: pluginCompatibility.length,
warnings: pluginCompatibility,
},
...(health || usage || lastHeartbeat ? { health, usage, lastHeartbeat } : {}),
},
null,
2,
),
);
writeRuntimeJson(runtime, {
...summary,
os: osSummary,
update,
updateChannel: channelInfo.channel,
updateChannelSource: channelInfo.source,
memory,
memoryPlugin,
gateway: {
mode: gatewayMode,
url: gatewayConnection.url,
urlSource: gatewayConnection.urlSource,
misconfigured: remoteUrlMissing,
reachable: gatewayReachable,
connectLatencyMs: gatewayProbe?.connectLatencyMs ?? null,
self: gatewaySelf,
error: gatewayProbe?.error ?? null,
authWarning: gatewayProbeAuthWarning ?? null,
},
gatewayService: daemon,
nodeService: nodeDaemon,
agents: agentStatus,
securityAudit,
secretDiagnostics,
pluginCompatibility: {
count: pluginCompatibility.length,
warnings: pluginCompatibility,
},
...(health || usage || lastHeartbeat ? { health, usage, lastHeartbeat } : {}),
});
return;
}

View File

@@ -107,7 +107,7 @@ function createManager(options?: {
const log = createSubsystemLogger("gateway/server-channels-test");
const channelLogs = { discord: log } as Record<ChannelId, SubsystemLogger>;
const runtime = runtimeForLogger(log);
const channelRuntimeEnvs = { discord: runtime } as Record<ChannelId, RuntimeEnv>;
const channelRuntimeEnvs = { discord: runtime } as unknown as Record<ChannelId, RuntimeEnv>;
return createChannelManager({
loadConfig: () => options?.loadConfig?.() ?? {},
channelLogs,

View File

@@ -575,7 +575,7 @@ export async function startGatewayServer(
) as Record<ChannelId, ReturnType<typeof createSubsystemLogger>>;
const channelRuntimeEnvs = Object.fromEntries(
Object.entries(channelLogs).map(([id, logger]) => [id, runtimeForLogger(logger)]),
) as Record<ChannelId, RuntimeEnv>;
) as unknown as Record<ChannelId, RuntimeEnv>;
const channelMethods = listChannelPlugins().flatMap((plugin) => plugin.gatewayMethods ?? []);
const gatewayMethods = Array.from(new Set([...baseGatewayMethods, ...channelMethods]));
let pluginServices: PluginServicesHandle | null = null;

View File

@@ -254,7 +254,7 @@ export async function runGmailSetup(opts: GmailSetupOptions) {
};
if (opts.json) {
defaultRuntime.log(JSON.stringify(summary, null, 2));
defaultRuntime.writeJson(summary);
return;
}

View File

@@ -9,6 +9,7 @@ import {
setConsoleTimestampPrefix,
setLoggerOverride,
} from "../logging.js";
import { defaultRuntime } from "../runtime.js";
import { loggingState } from "./state.js";
import {
captureConsoleSnapshot,
@@ -101,7 +102,7 @@ describe("enableConsoleCapture", () => {
expect(warn).toHaveBeenCalledWith("12:34:56 [exec] hello");
});
it("leaves JSON output unchanged when timestamp prefix is enabled", () => {
it("prefixes JSON console output when timestamp prefix is enabled", () => {
setLoggerOverride({ level: "info", file: tempLogPath() });
const log = vi.fn();
console.log = log;
@@ -109,7 +110,24 @@ describe("enableConsoleCapture", () => {
enableConsoleCapture();
const payload = JSON.stringify({ ok: true });
console.log(payload);
expect(log).toHaveBeenCalledWith(payload);
expect(log).toHaveBeenCalledTimes(1);
const firstArg = String(log.mock.calls[0]?.[0] ?? "");
expect(firstArg).toMatch(/^(?:\d{2}:\d{2}:\d{2}|\d{4}-\d{2}-\d{2}T)/);
expect(firstArg.endsWith(` ${payload}`)).toBe(true);
});
it("keeps diagnostics on stderr while runtime JSON stays on stdout", () => {
setLoggerOverride({ level: "info", file: tempLogPath() });
const stdoutWrite = vi.spyOn(process.stdout, "write").mockImplementation(() => true);
const stderrWrite = vi.spyOn(process.stderr, "write").mockImplementation(() => true);
routeLogsToStderr();
enableConsoleCapture();
console.log("diag");
defaultRuntime.writeJson({ ok: true });
expect(stderrWrite).toHaveBeenCalledWith("diag\n");
expect(stdoutWrite).toHaveBeenCalledWith('{\n "ok": true\n}\n');
});
it.each([

View File

@@ -189,19 +189,6 @@ function hasTimestampPrefix(value: string): boolean {
);
}
function isJsonPayload(value: string): boolean {
const trimmed = value.trim();
if (!trimmed.startsWith("{") && !trimmed.startsWith("[")) {
return false;
}
try {
JSON.parse(trimmed);
return true;
} catch {
return false;
}
}
/**
* Route console.* calls through file logging while still emitting to stdout/stderr.
* This keeps user-facing output unchanged but guarantees every console call is captured in log files.
@@ -262,10 +249,7 @@ export function enableConsoleCapture(): void {
}
const trimmed = stripAnsi(formatted).trimStart();
const shouldPrefixTimestamp =
loggingState.consoleTimestampPrefix &&
trimmed.length > 0 &&
!hasTimestampPrefix(trimmed) &&
!isJsonPayload(trimmed);
loggingState.consoleTimestampPrefix && trimmed.length > 0 && !hasTimestampPrefix(trimmed);
const timestamp = shouldPrefixTimestamp
? formatConsoleTimestamp(getConsoleSettings().style)
: "";
@@ -288,9 +272,8 @@ export function enableConsoleCapture(): void {
} catch {
// never block console output on logging failures
}
if (loggingState.forceConsoleToStderr && !isJsonPayload(trimmed)) {
// In --json mode, route diagnostic logs to stderr but let JSON
// payloads (the actual command output) through to stdout via orig().
if (loggingState.forceConsoleToStderr) {
// In --json mode, all console.* writes are diagnostics and should stay off stdout.
try {
const line = timestamp ? `${timestamp} ${formatted}` : formatted;
process.stderr.write(`${line}\n`);

View File

@@ -1,7 +1,7 @@
import { Chalk } from "chalk";
import type { Logger as TsLogger } from "tslog";
import { isVerbose } from "../globals.js";
import { defaultRuntime, type RuntimeEnv } from "../runtime.js";
import { defaultRuntime, type OutputRuntimeEnv, type RuntimeEnv } from "../runtime.js";
import { clearActiveProgressLine } from "../terminal/progress-line.js";
import {
formatConsoleTimestamp,
@@ -404,7 +404,7 @@ export function createSubsystemLogger(subsystem: string): SubsystemLogger {
export function runtimeForLogger(
logger: SubsystemLogger,
exit: RuntimeEnv["exit"] = defaultRuntime.exit,
): RuntimeEnv {
): OutputRuntimeEnv {
const formatArgs = (...args: unknown[]) =>
args
.map((arg) => formatRuntimeArg(arg))
@@ -413,6 +413,10 @@ export function runtimeForLogger(
return {
log: (...args: unknown[]) => logger.info(formatArgs(...args)),
error: (...args: unknown[]) => logger.error(formatArgs(...args)),
writeStdout: (value: string) => logger.info(value),
writeJson: (value: unknown, space = 2) => {
logger.info(JSON.stringify(value, null, space > 0 ? space : undefined));
},
exit,
};
}
@@ -420,6 +424,6 @@ export function runtimeForLogger(
export function createSubsystemRuntime(
subsystem: string,
exit: RuntimeEnv["exit"] = defaultRuntime.exit,
): RuntimeEnv {
): OutputRuntimeEnv {
return runtimeForLogger(createSubsystemLogger(subsystem), exit);
}

View File

@@ -32,8 +32,12 @@ describe("resolveRuntimeEnv", () => {
const resolved = resolveRuntimeEnv({ logger });
resolved.log?.("hello %s", "world");
resolved.error?.("bad %d", 7);
resolved.writeStdout("plain");
resolved.writeJson({ ok: true });
expect(logger.info).toHaveBeenCalledWith("hello world");
expect(logger.error).toHaveBeenCalledWith("bad 7");
expect(logger.info).toHaveBeenCalledWith("plain");
expect(logger.info).toHaveBeenCalledWith('{\n "ok": true\n}');
});
});

View File

@@ -1,6 +1,6 @@
import { format } from "node:util";
import type { RuntimeEnv } from "../runtime.js";
export type { RuntimeEnv } from "../runtime.js";
import type { OutputRuntimeEnv, RuntimeEnv } from "../runtime.js";
export type { OutputRuntimeEnv, RuntimeEnv } from "../runtime.js";
export { createNonExitingRuntime, defaultRuntime } from "../runtime.js";
export {
danger,
@@ -29,7 +29,7 @@ type LoggerLike = {
export function createLoggerBackedRuntime(params: {
logger: LoggerLike;
exitError?: (code: number) => Error;
}): RuntimeEnv {
}): OutputRuntimeEnv {
return {
log: (...args) => {
params.logger.info(format(...args));
@@ -37,6 +37,12 @@ export function createLoggerBackedRuntime(params: {
error: (...args) => {
params.logger.error(format(...args));
},
writeStdout: (value) => {
params.logger.info(value);
},
writeJson: (value, space = 2) => {
params.logger.info(JSON.stringify(value, null, space > 0 ? space : undefined));
},
exit: (code: number): never => {
throw params.exitError?.(code) ?? new Error(`exit ${code}`);
},
@@ -44,22 +50,48 @@ export function createLoggerBackedRuntime(params: {
}
/** Reuse an existing runtime when present, otherwise synthesize one from the provided logger. */
export function resolveRuntimeEnv(params: {
runtime: RuntimeEnv;
logger: LoggerLike;
exitError?: (code: number) => Error;
}): RuntimeEnv;
export function resolveRuntimeEnv(params: {
runtime?: undefined;
logger: LoggerLike;
exitError?: (code: number) => Error;
}): OutputRuntimeEnv;
export function resolveRuntimeEnv(params: {
runtime?: RuntimeEnv;
logger: LoggerLike;
exitError?: (code: number) => Error;
}): RuntimeEnv {
}): RuntimeEnv | OutputRuntimeEnv {
return params.runtime ?? createLoggerBackedRuntime(params);
}
/** Resolve a runtime that treats exit requests as unsupported errors instead of process termination. */
export function resolveRuntimeEnvWithUnavailableExit(params: {
runtime: RuntimeEnv;
logger: LoggerLike;
unavailableMessage?: string;
}): RuntimeEnv;
export function resolveRuntimeEnvWithUnavailableExit(params: {
runtime?: undefined;
logger: LoggerLike;
unavailableMessage?: string;
}): OutputRuntimeEnv;
export function resolveRuntimeEnvWithUnavailableExit(params: {
runtime?: RuntimeEnv;
logger: LoggerLike;
unavailableMessage?: string;
}): RuntimeEnv {
}): RuntimeEnv | OutputRuntimeEnv {
if (params.runtime) {
return resolveRuntimeEnv({
runtime: params.runtime,
logger: params.logger,
exitError: () => new Error(params.unavailableMessage ?? "Runtime exit not available"),
});
}
return resolveRuntimeEnv({
runtime: params.runtime,
logger: params.logger,
exitError: () => new Error(params.unavailableMessage ?? "Runtime exit not available"),
});

View File

@@ -9,7 +9,7 @@ export { removeAckReactionAfterReply, shouldAckReaction } from "../channels/ack-
export type { ChannelAccountSnapshot, ChannelGatewayContext } from "../channels/plugins/types.js";
export type { OpenClawConfig } from "../config/config.js";
export type { PluginRuntime } from "../plugins/runtime/types.js";
export type { RuntimeEnv } from "../runtime.js";
export type { OutputRuntimeEnv, RuntimeEnv } from "../runtime.js";
export type { MockFn } from "../test-utils/vitest-mock-fn.js";
/** Create a tiny Windows `.cmd` shim fixture for plugin tests that spawn CLIs. */

View File

@@ -7,6 +7,11 @@ export type RuntimeEnv = {
exit: (code: number) => void;
};
export type OutputRuntimeEnv = RuntimeEnv & {
writeStdout: (value: string) => void;
writeJson: (value: unknown, space?: number) => void;
};
function shouldEmitRuntimeLog(env: NodeJS.ProcessEnv = process.env): boolean {
if (env.VITEST !== "true") {
return true;
@@ -18,7 +23,49 @@ function shouldEmitRuntimeLog(env: NodeJS.ProcessEnv = process.env): boolean {
return typeof maybeMockedLog.mock === "object";
}
function createRuntimeIo(): Pick<RuntimeEnv, "log" | "error"> {
function shouldEmitRuntimeStdout(env: NodeJS.ProcessEnv = process.env): boolean {
if (env.VITEST !== "true") {
return true;
}
if (env.OPENCLAW_TEST_RUNTIME_LOG === "1") {
return true;
}
const stdout = process.stdout as NodeJS.WriteStream & {
write: {
mock?: unknown;
};
};
return typeof stdout.write.mock === "object";
}
function isPipeClosedError(err: unknown): boolean {
const code = (err as { code?: string })?.code;
return code === "EPIPE" || code === "EIO";
}
function hasRuntimeOutputWriter(
runtime: RuntimeEnv | OutputRuntimeEnv,
): runtime is OutputRuntimeEnv {
return typeof (runtime as Partial<OutputRuntimeEnv>).writeStdout === "function";
}
function writeStdout(value: string): void {
if (!shouldEmitRuntimeStdout()) {
return;
}
clearActiveProgressLine();
const line = value.endsWith("\n") ? value : `${value}\n`;
try {
process.stdout.write(line);
} catch (err) {
if (isPipeClosedError(err)) {
return;
}
throw err;
}
}
function createRuntimeIo(): Pick<OutputRuntimeEnv, "log" | "error" | "writeStdout" | "writeJson"> {
return {
log: (...args: Parameters<typeof console.log>) => {
if (!shouldEmitRuntimeLog()) {
@@ -31,10 +78,14 @@ function createRuntimeIo(): Pick<RuntimeEnv, "log" | "error"> {
clearActiveProgressLine();
console.error(...args);
},
writeStdout,
writeJson: (value: unknown, space = 2) => {
writeStdout(JSON.stringify(value, null, space > 0 ? space : undefined));
},
};
}
export const defaultRuntime: RuntimeEnv = {
export const defaultRuntime: OutputRuntimeEnv = {
...createRuntimeIo(),
exit: (code) => {
restoreTerminalState("runtime exit", { resumeStdinIfPaused: false });
@@ -43,7 +94,7 @@ export const defaultRuntime: RuntimeEnv = {
},
};
export function createNonExitingRuntime(): RuntimeEnv {
export function createNonExitingRuntime(): OutputRuntimeEnv {
return {
...createRuntimeIo(),
exit: (code: number) => {
@@ -51,3 +102,23 @@ export function createNonExitingRuntime(): RuntimeEnv {
},
};
}
export function writeRuntimeStdout(runtime: RuntimeEnv | OutputRuntimeEnv, value: string): void {
if (hasRuntimeOutputWriter(runtime)) {
runtime.writeStdout(value);
return;
}
runtime.log(value);
}
export function writeRuntimeJson(
runtime: RuntimeEnv | OutputRuntimeEnv,
value: unknown,
space = 2,
): void {
if (hasRuntimeOutputWriter(runtime)) {
runtime.writeJson(value, space);
return;
}
runtime.log(JSON.stringify(value, null, space > 0 ? space : undefined));
}