import type { Command } from "commander"; import { danger } from "../globals.js"; import { defaultRuntime } from "../runtime.js"; import { shortenHomePath } from "../utils.js"; import { callBrowserRequest, type BrowserParentOpts } from "./browser-cli-shared.js"; import { runCommandWithRuntime } from "./cli-utils.js"; const BROWSER_DEBUG_TIMEOUT_MS = 20000; type BrowserRequestParams = Parameters[1]; type DebugContext = { parent: BrowserParentOpts; profile?: string; }; function runBrowserDebug(action: () => Promise) { return runCommandWithRuntime(defaultRuntime, action, (err) => { defaultRuntime.error(danger(String(err))); defaultRuntime.exit(1); }); } async function withDebugContext( cmd: Command, parentOpts: (cmd: Command) => BrowserParentOpts, action: (context: DebugContext) => Promise, ) { const parent = parentOpts(cmd); await runBrowserDebug(() => action({ parent, profile: parent.browserProfile, }), ); } function printJsonResult(parent: BrowserParentOpts, result: unknown): boolean { if (!parent.json) { return false; } defaultRuntime.log(JSON.stringify(result, null, 2)); return true; } async function callDebugRequest( parent: BrowserParentOpts, params: BrowserRequestParams, ): Promise { return callBrowserRequest(parent, params, { timeoutMs: BROWSER_DEBUG_TIMEOUT_MS }); } function resolveProfileQuery(profile?: string) { return profile ? { profile } : undefined; } function resolveDebugQuery(params: { targetId?: unknown; clear?: unknown; profile?: string; filter?: unknown; }) { return { targetId: typeof params.targetId === "string" ? params.targetId.trim() || undefined : undefined, filter: typeof params.filter === "string" ? params.filter.trim() || undefined : undefined, clear: Boolean(params.clear), profile: params.profile, }; } export function registerBrowserDebugCommands( browser: Command, parentOpts: (cmd: Command) => BrowserParentOpts, ) { browser .command("highlight") .description("Highlight an element by ref") .argument("", "Ref id from snapshot") .option("--target-id ", "CDP target id (or unique prefix)") .action(async (ref: string, opts, cmd) => { await withDebugContext(cmd, parentOpts, async ({ parent, profile }) => { const result = await callDebugRequest(parent, { method: "POST", path: "/highlight", query: resolveProfileQuery(profile), body: { ref: ref.trim(), targetId: opts.targetId?.trim() || undefined, }, }); if (printJsonResult(parent, result)) { return; } defaultRuntime.log(`highlighted ${ref.trim()}`); }); }); browser .command("errors") .description("Get recent page errors") .option("--clear", "Clear stored errors after reading", false) .option("--target-id ", "CDP target id (or unique prefix)") .action(async (opts, cmd) => { await withDebugContext(cmd, parentOpts, async ({ parent, profile }) => { const result = await callDebugRequest<{ errors: Array<{ timestamp: string; name?: string; message: string }>; }>(parent, { method: "GET", path: "/errors", query: resolveDebugQuery({ targetId: opts.targetId, clear: opts.clear, profile, }), }); if (printJsonResult(parent, result)) { return; } if (!result.errors.length) { defaultRuntime.log("No page errors."); return; } defaultRuntime.log( result.errors .map((e) => `${e.timestamp} ${e.name ? `${e.name}: ` : ""}${e.message}`) .join("\n"), ); }); }); browser .command("requests") .description("Get recent network requests (best-effort)") .option("--filter ", "Only show URLs that contain this substring") .option("--clear", "Clear stored requests after reading", false) .option("--target-id ", "CDP target id (or unique prefix)") .action(async (opts, cmd) => { await withDebugContext(cmd, parentOpts, async ({ parent, profile }) => { const result = await callDebugRequest<{ requests: Array<{ timestamp: string; method: string; status?: number; ok?: boolean; url: string; failureText?: string; }>; }>(parent, { method: "GET", path: "/requests", query: resolveDebugQuery({ targetId: opts.targetId, filter: opts.filter, clear: opts.clear, profile, }), }); if (printJsonResult(parent, result)) { return; } if (!result.requests.length) { defaultRuntime.log("No requests recorded."); return; } defaultRuntime.log( result.requests .map((r) => { const status = typeof r.status === "number" ? ` ${r.status}` : ""; const ok = r.ok === true ? " ok" : r.ok === false ? " fail" : ""; const fail = r.failureText ? ` (${r.failureText})` : ""; return `${r.timestamp} ${r.method}${status}${ok} ${r.url}${fail}`; }) .join("\n"), ); }); }); const trace = browser.command("trace").description("Record a Playwright trace"); trace .command("start") .description("Start trace recording") .option("--target-id ", "CDP target id (or unique prefix)") .option("--no-screenshots", "Disable screenshots") .option("--no-snapshots", "Disable snapshots") .option("--sources", "Include sources (bigger traces)", false) .action(async (opts, cmd) => { await withDebugContext(cmd, parentOpts, async ({ parent, profile }) => { const result = await callDebugRequest(parent, { method: "POST", path: "/trace/start", query: resolveProfileQuery(profile), body: { targetId: opts.targetId?.trim() || undefined, screenshots: Boolean(opts.screenshots), snapshots: Boolean(opts.snapshots), sources: Boolean(opts.sources), }, }); if (printJsonResult(parent, result)) { return; } defaultRuntime.log("trace started"); }); }); trace .command("stop") .description("Stop trace recording and write a .zip") .option( "--out ", "Output path within openclaw temp dir (e.g. trace.zip or /tmp/openclaw/trace.zip)", ) .option("--target-id ", "CDP target id (or unique prefix)") .action(async (opts, cmd) => { await withDebugContext(cmd, parentOpts, async ({ parent, profile }) => { const result = await callDebugRequest<{ path: string }>(parent, { method: "POST", path: "/trace/stop", query: resolveProfileQuery(profile), body: { targetId: opts.targetId?.trim() || undefined, path: opts.out?.trim() || undefined, }, }); if (printJsonResult(parent, result)) { return; } defaultRuntime.log(`TRACE:${shortenHomePath(result.path)}`); }); }); }