import type { Command } from "commander"; import type { BrowserCreateProfileResult, BrowserDeleteProfileResult, BrowserResetProfileResult, BrowserStatus, BrowserTab, ProfileStatus, } from "../browser/client.js"; import { danger, info } 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"; function runBrowserCommand(action: () => Promise) { return runCommandWithRuntime(defaultRuntime, action, (err) => { defaultRuntime.error(danger(String(err))); defaultRuntime.exit(1); }); } export function registerBrowserManageCommands( browser: Command, parentOpts: (cmd: Command) => BrowserParentOpts, ) { browser .command("status") .description("Show browser status") .action(async (_opts, cmd) => { const parent = parentOpts(cmd); await runBrowserCommand(async () => { const status = await callBrowserRequest( parent, { method: "GET", path: "/", query: parent?.browserProfile ? { profile: parent.browserProfile } : undefined, }, { timeoutMs: 1500, }, ); if (parent?.json) { defaultRuntime.log(JSON.stringify(status, null, 2)); return; } const detectedPath = status.detectedExecutablePath ?? status.executablePath; const detectedDisplay = detectedPath ? shortenHomePath(detectedPath) : "auto"; defaultRuntime.log( [ `profile: ${status.profile ?? "clawd"}`, `enabled: ${status.enabled}`, `running: ${status.running}`, `cdpPort: ${status.cdpPort}`, `cdpUrl: ${status.cdpUrl ?? `http://127.0.0.1:${status.cdpPort}`}`, `browser: ${status.chosenBrowser ?? "unknown"}`, `detectedBrowser: ${status.detectedBrowser ?? "unknown"}`, `detectedPath: ${detectedDisplay}`, `profileColor: ${status.color}`, ...(status.detectError ? [`detectError: ${status.detectError}`] : []), ].join("\n"), ); }); }); browser .command("start") .description("Start the browser (no-op if already running)") .action(async (_opts, cmd) => { const parent = parentOpts(cmd); const profile = parent?.browserProfile; await runBrowserCommand(async () => { await callBrowserRequest( parent, { method: "POST", path: "/start", query: profile ? { profile } : undefined, }, { timeoutMs: 15000 }, ); const status = await callBrowserRequest( parent, { method: "GET", path: "/", query: profile ? { profile } : undefined, }, { timeoutMs: 1500 }, ); if (parent?.json) { defaultRuntime.log(JSON.stringify(status, null, 2)); return; } const name = status.profile ?? "clawd"; defaultRuntime.log(info(`🦞 browser [${name}] running: ${status.running}`)); }); }); browser .command("stop") .description("Stop the browser (best-effort)") .action(async (_opts, cmd) => { const parent = parentOpts(cmd); const profile = parent?.browserProfile; await runBrowserCommand(async () => { await callBrowserRequest( parent, { method: "POST", path: "/stop", query: profile ? { profile } : undefined, }, { timeoutMs: 15000 }, ); const status = await callBrowserRequest( parent, { method: "GET", path: "/", query: profile ? { profile } : undefined, }, { timeoutMs: 1500 }, ); if (parent?.json) { defaultRuntime.log(JSON.stringify(status, null, 2)); return; } const name = status.profile ?? "clawd"; defaultRuntime.log(info(`🦞 browser [${name}] running: ${status.running}`)); }); }); browser .command("reset-profile") .description("Reset browser profile (moves it to Trash)") .action(async (_opts, cmd) => { const parent = parentOpts(cmd); const profile = parent?.browserProfile; await runBrowserCommand(async () => { const result = await callBrowserRequest( parent, { method: "POST", path: "/reset-profile", query: profile ? { profile } : undefined, }, { timeoutMs: 20000 }, ); if (parent?.json) { defaultRuntime.log(JSON.stringify(result, null, 2)); return; } if (!result.moved) { defaultRuntime.log(info(`🦞 browser profile already missing.`)); return; } const dest = result.to ?? result.from; defaultRuntime.log(info(`🦞 browser profile moved to Trash (${dest})`)); }); }); browser .command("tabs") .description("List open tabs") .action(async (_opts, cmd) => { const parent = parentOpts(cmd); const profile = parent?.browserProfile; await runBrowserCommand(async () => { const result = await callBrowserRequest<{ running: boolean; tabs: BrowserTab[] }>( parent, { method: "GET", path: "/tabs", query: profile ? { profile } : undefined, }, { timeoutMs: 3000 }, ); const tabs = result.tabs ?? []; if (parent?.json) { defaultRuntime.log(JSON.stringify({ tabs }, null, 2)); return; } if (tabs.length === 0) { defaultRuntime.log("No tabs (browser closed or no targets)."); return; } defaultRuntime.log( tabs .map( (t, i) => `${i + 1}. ${t.title || "(untitled)"}\n ${t.url}\n id: ${t.targetId}`, ) .join("\n"), ); }); }); const tab = browser .command("tab") .description("Tab shortcuts (index-based)") .action(async (_opts, cmd) => { const parent = parentOpts(cmd); const profile = parent?.browserProfile; await runBrowserCommand(async () => { const result = await callBrowserRequest<{ ok: true; tabs: BrowserTab[] }>( parent, { method: "POST", path: "/tabs/action", query: profile ? { profile } : undefined, body: { action: "list", }, }, { timeoutMs: 10_000 }, ); const tabs = result.tabs ?? []; if (parent?.json) { defaultRuntime.log(JSON.stringify({ tabs }, null, 2)); return; } if (tabs.length === 0) { defaultRuntime.log("No tabs (browser closed or no targets)."); return; } defaultRuntime.log( tabs .map( (t, i) => `${i + 1}. ${t.title || "(untitled)"}\n ${t.url}\n id: ${t.targetId}`, ) .join("\n"), ); }); }); tab .command("new") .description("Open a new tab (about:blank)") .action(async (_opts, cmd) => { const parent = parentOpts(cmd); const profile = parent?.browserProfile; await runBrowserCommand(async () => { const result = await callBrowserRequest( parent, { method: "POST", path: "/tabs/action", query: profile ? { profile } : undefined, body: { action: "new" }, }, { timeoutMs: 10_000 }, ); if (parent?.json) { defaultRuntime.log(JSON.stringify(result, null, 2)); return; } defaultRuntime.log("opened new tab"); }); }); tab .command("select") .description("Focus tab by index (1-based)") .argument("", "Tab index (1-based)", (v: string) => Number(v)) .action(async (index: number, _opts, cmd) => { const parent = parentOpts(cmd); const profile = parent?.browserProfile; if (!Number.isFinite(index) || index < 1) { defaultRuntime.error(danger("index must be a positive number")); defaultRuntime.exit(1); return; } await runBrowserCommand(async () => { const result = await callBrowserRequest( parent, { method: "POST", path: "/tabs/action", query: profile ? { profile } : undefined, body: { action: "select", index: Math.floor(index) - 1 }, }, { timeoutMs: 10_000 }, ); if (parent?.json) { defaultRuntime.log(JSON.stringify(result, null, 2)); return; } defaultRuntime.log(`selected tab ${Math.floor(index)}`); }); }); tab .command("close") .description("Close tab by index (1-based); default: first tab") .argument("[index]", "Tab index (1-based)", (v: string) => Number(v)) .action(async (index: number | undefined, _opts, cmd) => { const parent = parentOpts(cmd); const profile = parent?.browserProfile; const idx = typeof index === "number" && Number.isFinite(index) ? Math.floor(index) - 1 : undefined; if (typeof idx === "number" && idx < 0) { defaultRuntime.error(danger("index must be >= 1")); defaultRuntime.exit(1); return; } await runBrowserCommand(async () => { const result = await callBrowserRequest( parent, { method: "POST", path: "/tabs/action", query: profile ? { profile } : undefined, body: { action: "close", index: idx }, }, { timeoutMs: 10_000 }, ); if (parent?.json) { defaultRuntime.log(JSON.stringify(result, null, 2)); return; } defaultRuntime.log("closed tab"); }); }); browser .command("open") .description("Open a URL in a new tab") .argument("", "URL to open") .action(async (url: string, _opts, cmd) => { const parent = parentOpts(cmd); const profile = parent?.browserProfile; await runBrowserCommand(async () => { const tab = await callBrowserRequest( parent, { method: "POST", path: "/tabs/open", query: profile ? { profile } : undefined, body: { url }, }, { timeoutMs: 15000 }, ); if (parent?.json) { defaultRuntime.log(JSON.stringify(tab, null, 2)); return; } defaultRuntime.log(`opened: ${tab.url}\nid: ${tab.targetId}`); }); }); browser .command("focus") .description("Focus a tab by target id (or unique prefix)") .argument("", "Target id or unique prefix") .action(async (targetId: string, _opts, cmd) => { const parent = parentOpts(cmd); const profile = parent?.browserProfile; await runBrowserCommand(async () => { await callBrowserRequest( parent, { method: "POST", path: "/tabs/focus", query: profile ? { profile } : undefined, body: { targetId }, }, { timeoutMs: 5000 }, ); if (parent?.json) { defaultRuntime.log(JSON.stringify({ ok: true }, null, 2)); return; } defaultRuntime.log(`focused tab ${targetId}`); }); }); browser .command("close") .description("Close a tab (target id optional)") .argument("[targetId]", "Target id or unique prefix (optional)") .action(async (targetId: string | undefined, _opts, cmd) => { const parent = parentOpts(cmd); const profile = parent?.browserProfile; await runBrowserCommand(async () => { if (targetId?.trim()) { await callBrowserRequest( parent, { method: "DELETE", path: `/tabs/${encodeURIComponent(targetId.trim())}`, query: profile ? { profile } : undefined, }, { timeoutMs: 5000 }, ); } else { await callBrowserRequest( parent, { method: "POST", path: "/act", query: profile ? { profile } : undefined, body: { kind: "close" }, }, { timeoutMs: 20000 }, ); } if (parent?.json) { defaultRuntime.log(JSON.stringify({ ok: true }, null, 2)); return; } defaultRuntime.log("closed tab"); }); }); // Profile management commands browser .command("profiles") .description("List all browser profiles") .action(async (_opts, cmd) => { const parent = parentOpts(cmd); await runBrowserCommand(async () => { const result = await callBrowserRequest<{ profiles: ProfileStatus[] }>( parent, { method: "GET", path: "/profiles", }, { timeoutMs: 3000 }, ); const profiles = result.profiles ?? []; if (parent?.json) { defaultRuntime.log(JSON.stringify({ profiles }, null, 2)); return; } if (profiles.length === 0) { defaultRuntime.log("No profiles configured."); return; } defaultRuntime.log( profiles .map((p) => { const status = p.running ? "running" : "stopped"; const tabs = p.running ? ` (${p.tabCount} tabs)` : ""; const def = p.isDefault ? " [default]" : ""; const loc = p.isRemote ? `cdpUrl: ${p.cdpUrl}` : `port: ${p.cdpPort}`; const remote = p.isRemote ? " [remote]" : ""; return `${p.name}: ${status}${tabs}${def}${remote}\n ${loc}, color: ${p.color}`; }) .join("\n"), ); }); }); browser .command("create-profile") .description("Create a new browser profile") .requiredOption("--name ", "Profile name (lowercase, numbers, hyphens)") .option("--color ", "Profile color (hex format, e.g. #0066CC)") .option("--cdp-url ", "CDP URL for remote Chrome (http/https)") .option("--driver ", "Profile driver (clawd|extension). Default: clawd") .action( async (opts: { name: string; color?: string; cdpUrl?: string; driver?: string }, cmd) => { const parent = parentOpts(cmd); await runBrowserCommand(async () => { const result = await callBrowserRequest( parent, { method: "POST", path: "/profiles/create", body: { name: opts.name, color: opts.color, cdpUrl: opts.cdpUrl, driver: opts.driver === "extension" ? "extension" : undefined, }, }, { timeoutMs: 10_000 }, ); if (parent?.json) { defaultRuntime.log(JSON.stringify(result, null, 2)); return; } const loc = result.isRemote ? ` cdpUrl: ${result.cdpUrl}` : ` port: ${result.cdpPort}`; defaultRuntime.log( info( `🦞 Created profile "${result.profile}"\n${loc}\n color: ${result.color}${ opts.driver === "extension" ? "\n driver: extension" : "" }`, ), ); }); }, ); browser .command("delete-profile") .description("Delete a browser profile") .requiredOption("--name ", "Profile name to delete") .action(async (opts: { name: string }, cmd) => { const parent = parentOpts(cmd); await runBrowserCommand(async () => { const result = await callBrowserRequest( parent, { method: "DELETE", path: `/profiles/${encodeURIComponent(opts.name)}`, }, { timeoutMs: 20_000 }, ); if (parent?.json) { defaultRuntime.log(JSON.stringify(result, null, 2)); return; } const msg = result.deleted ? `🦞 Deleted profile "${result.profile}" (user data removed)` : `🦞 Deleted profile "${result.profile}" (no user data found)`; defaultRuntime.log(info(msg)); }); }); }