melbot / src /cli /browser-cli-manage.ts
amos-fernandes's picture
Upload 4501 files
3a65265 verified
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<void>) {
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<BrowserStatus>(
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<BrowserStatus>(
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<BrowserStatus>(
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<BrowserResetProfileResult>(
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("<index>", "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>", "URL to open")
.action(async (url: string, _opts, cmd) => {
const parent = parentOpts(cmd);
const profile = parent?.browserProfile;
await runBrowserCommand(async () => {
const tab = await callBrowserRequest<BrowserTab>(
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("<targetId>", "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 <name>", "Profile name (lowercase, numbers, hyphens)")
.option("--color <hex>", "Profile color (hex format, e.g. #0066CC)")
.option("--cdp-url <url>", "CDP URL for remote Chrome (http/https)")
.option("--driver <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<BrowserCreateProfileResult>(
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 <name>", "Profile name to delete")
.action(async (opts: { name: string }, cmd) => {
const parent = parentOpts(cmd);
await runBrowserCommand(async () => {
const result = await callBrowserRequest<BrowserDeleteProfileResult>(
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));
});
});
}