refactor: route browser control via gateway/node

This commit is contained in:
Peter Steinberger
2026-01-27 03:23:42 +00:00
parent b151b8d196
commit e7fdccce39
91 changed files with 1909 additions and 1608 deletions

View File

@@ -1,9 +1,8 @@
import type { Command } from "commander";
import { browserAct } from "../../browser/client-actions.js";
import { danger } from "../../globals.js";
import { defaultRuntime } from "../../runtime.js";
import type { BrowserParentOpts } from "../browser-cli-shared.js";
import { requireRef, resolveBrowserActionContext } from "./shared.js";
import { callBrowserAct, requireRef, resolveBrowserActionContext } from "./shared.js";
export function registerBrowserElementCommands(
browser: Command,
@@ -18,7 +17,7 @@ export function registerBrowserElementCommands(
.option("--button <left|right|middle>", "Mouse button to use")
.option("--modifiers <list>", "Comma-separated modifiers (Shift,Alt,Meta)")
.action(async (ref: string | undefined, opts, cmd) => {
const { parent, baseUrl, profile } = resolveBrowserActionContext(cmd, parentOpts);
const { parent, profile } = resolveBrowserActionContext(cmd, parentOpts);
const refValue = requireRef(ref);
if (!refValue) return;
const modifiers = opts.modifiers
@@ -28,9 +27,10 @@ export function registerBrowserElementCommands(
.filter(Boolean)
: undefined;
try {
const result = await browserAct(
baseUrl,
{
const result = await callBrowserAct({
parent,
profile,
body: {
kind: "click",
ref: refValue,
targetId: opts.targetId?.trim() || undefined,
@@ -38,8 +38,7 @@ export function registerBrowserElementCommands(
button: opts.button?.trim() || undefined,
modifiers,
},
{ profile },
);
});
if (parent?.json) {
defaultRuntime.log(JSON.stringify(result, null, 2));
return;
@@ -61,13 +60,14 @@ export function registerBrowserElementCommands(
.option("--slowly", "Type slowly (human-like)", false)
.option("--target-id <id>", "CDP target id (or unique prefix)")
.action(async (ref: string | undefined, text: string, opts, cmd) => {
const { parent, baseUrl, profile } = resolveBrowserActionContext(cmd, parentOpts);
const { parent, profile } = resolveBrowserActionContext(cmd, parentOpts);
const refValue = requireRef(ref);
if (!refValue) return;
try {
const result = await browserAct(
baseUrl,
{
const result = await callBrowserAct({
parent,
profile,
body: {
kind: "type",
ref: refValue,
text,
@@ -75,8 +75,7 @@ export function registerBrowserElementCommands(
slowly: Boolean(opts.slowly),
targetId: opts.targetId?.trim() || undefined,
},
{ profile },
);
});
if (parent?.json) {
defaultRuntime.log(JSON.stringify(result, null, 2));
return;
@@ -94,13 +93,13 @@ export function registerBrowserElementCommands(
.argument("<key>", "Key to press (e.g. Enter)")
.option("--target-id <id>", "CDP target id (or unique prefix)")
.action(async (key: string, opts, cmd) => {
const { parent, baseUrl, profile } = resolveBrowserActionContext(cmd, parentOpts);
const { parent, profile } = resolveBrowserActionContext(cmd, parentOpts);
try {
const result = await browserAct(
baseUrl,
{ kind: "press", key, targetId: opts.targetId?.trim() || undefined },
{ profile },
);
const result = await callBrowserAct({
parent,
profile,
body: { kind: "press", key, targetId: opts.targetId?.trim() || undefined },
});
if (parent?.json) {
defaultRuntime.log(JSON.stringify(result, null, 2));
return;
@@ -118,13 +117,13 @@ export function registerBrowserElementCommands(
.argument("<ref>", "Ref id from snapshot")
.option("--target-id <id>", "CDP target id (or unique prefix)")
.action(async (ref: string, opts, cmd) => {
const { parent, baseUrl, profile } = resolveBrowserActionContext(cmd, parentOpts);
const { parent, profile } = resolveBrowserActionContext(cmd, parentOpts);
try {
const result = await browserAct(
baseUrl,
{ kind: "hover", ref, targetId: opts.targetId?.trim() || undefined },
{ profile },
);
const result = await callBrowserAct({
parent,
profile,
body: { kind: "hover", ref, targetId: opts.targetId?.trim() || undefined },
});
if (parent?.json) {
defaultRuntime.log(JSON.stringify(result, null, 2));
return;
@@ -145,20 +144,21 @@ export function registerBrowserElementCommands(
Number(v),
)
.action(async (ref: string | undefined, opts, cmd) => {
const { parent, baseUrl, profile } = resolveBrowserActionContext(cmd, parentOpts);
const { parent, profile } = resolveBrowserActionContext(cmd, parentOpts);
const refValue = requireRef(ref);
if (!refValue) return;
try {
const result = await browserAct(
baseUrl,
{
const result = await callBrowserAct({
parent,
profile,
body: {
kind: "scrollIntoView",
ref: refValue,
targetId: opts.targetId?.trim() || undefined,
timeoutMs: Number.isFinite(opts.timeoutMs) ? opts.timeoutMs : undefined,
},
{ profile },
);
timeoutMs: Number.isFinite(opts.timeoutMs) ? opts.timeoutMs : undefined,
});
if (parent?.json) {
defaultRuntime.log(JSON.stringify(result, null, 2));
return;
@@ -177,18 +177,18 @@ export function registerBrowserElementCommands(
.argument("<endRef>", "End ref id")
.option("--target-id <id>", "CDP target id (or unique prefix)")
.action(async (startRef: string, endRef: string, opts, cmd) => {
const { parent, baseUrl, profile } = resolveBrowserActionContext(cmd, parentOpts);
const { parent, profile } = resolveBrowserActionContext(cmd, parentOpts);
try {
const result = await browserAct(
baseUrl,
{
const result = await callBrowserAct({
parent,
profile,
body: {
kind: "drag",
startRef,
endRef,
targetId: opts.targetId?.trim() || undefined,
},
{ profile },
);
});
if (parent?.json) {
defaultRuntime.log(JSON.stringify(result, null, 2));
return;
@@ -207,18 +207,18 @@ export function registerBrowserElementCommands(
.argument("<values...>", "Option values to select")
.option("--target-id <id>", "CDP target id (or unique prefix)")
.action(async (ref: string, values: string[], opts, cmd) => {
const { parent, baseUrl, profile } = resolveBrowserActionContext(cmd, parentOpts);
const { parent, profile } = resolveBrowserActionContext(cmd, parentOpts);
try {
const result = await browserAct(
baseUrl,
{
const result = await callBrowserAct({
parent,
profile,
body: {
kind: "select",
ref,
values,
targetId: opts.targetId?.trim() || undefined,
},
{ profile },
);
});
if (parent?.json) {
defaultRuntime.log(JSON.stringify(result, null, 2));
return;

View File

@@ -1,13 +1,7 @@
import type { Command } from "commander";
import {
browserArmDialog,
browserArmFileChooser,
browserDownload,
browserWaitForDownload,
} from "../../browser/client-actions.js";
import { danger } from "../../globals.js";
import { defaultRuntime } from "../../runtime.js";
import type { BrowserParentOpts } from "../browser-cli-shared.js";
import { callBrowserRequest, type BrowserParentOpts } from "../browser-cli-shared.js";
import { resolveBrowserActionContext } from "./shared.js";
import { shortenHomePath } from "../../utils.js";
@@ -29,17 +23,26 @@ export function registerBrowserFilesAndDownloadsCommands(
(v: string) => Number(v),
)
.action(async (paths: string[], opts, cmd) => {
const { parent, baseUrl, profile } = resolveBrowserActionContext(cmd, parentOpts);
const { parent, profile } = resolveBrowserActionContext(cmd, parentOpts);
try {
const result = await browserArmFileChooser(baseUrl, {
paths,
ref: opts.ref?.trim() || undefined,
inputRef: opts.inputRef?.trim() || undefined,
element: opts.element?.trim() || undefined,
targetId: opts.targetId?.trim() || undefined,
timeoutMs: Number.isFinite(opts.timeoutMs) ? opts.timeoutMs : undefined,
profile,
});
const timeoutMs = Number.isFinite(opts.timeoutMs) ? opts.timeoutMs : undefined;
const result = await callBrowserRequest(
parent,
{
method: "POST",
path: "/hooks/file-chooser",
query: profile ? { profile } : undefined,
body: {
paths,
ref: opts.ref?.trim() || undefined,
inputRef: opts.inputRef?.trim() || undefined,
element: opts.element?.trim() || undefined,
targetId: opts.targetId?.trim() || undefined,
timeoutMs,
},
},
{ timeoutMs: timeoutMs ?? 20000 },
);
if (parent?.json) {
defaultRuntime.log(JSON.stringify(result, null, 2));
return;
@@ -62,14 +65,23 @@ export function registerBrowserFilesAndDownloadsCommands(
(v: string) => Number(v),
)
.action(async (outPath: string | undefined, opts, cmd) => {
const { parent, baseUrl, profile } = resolveBrowserActionContext(cmd, parentOpts);
const { parent, profile } = resolveBrowserActionContext(cmd, parentOpts);
try {
const result = await browserWaitForDownload(baseUrl, {
path: outPath?.trim() || undefined,
targetId: opts.targetId?.trim() || undefined,
timeoutMs: Number.isFinite(opts.timeoutMs) ? opts.timeoutMs : undefined,
profile,
});
const timeoutMs = Number.isFinite(opts.timeoutMs) ? opts.timeoutMs : undefined;
const result = await callBrowserRequest(
parent,
{
method: "POST",
path: "/wait/download",
query: profile ? { profile } : undefined,
body: {
path: outPath?.trim() || undefined,
targetId: opts.targetId?.trim() || undefined,
timeoutMs,
},
},
{ timeoutMs: timeoutMs ?? 20000 },
);
if (parent?.json) {
defaultRuntime.log(JSON.stringify(result, null, 2));
return;
@@ -93,15 +105,24 @@ export function registerBrowserFilesAndDownloadsCommands(
(v: string) => Number(v),
)
.action(async (ref: string, outPath: string, opts, cmd) => {
const { parent, baseUrl, profile } = resolveBrowserActionContext(cmd, parentOpts);
const { parent, profile } = resolveBrowserActionContext(cmd, parentOpts);
try {
const result = await browserDownload(baseUrl, {
ref,
path: outPath,
targetId: opts.targetId?.trim() || undefined,
timeoutMs: Number.isFinite(opts.timeoutMs) ? opts.timeoutMs : undefined,
profile,
});
const timeoutMs = Number.isFinite(opts.timeoutMs) ? opts.timeoutMs : undefined;
const result = await callBrowserRequest(
parent,
{
method: "POST",
path: "/download",
query: profile ? { profile } : undefined,
body: {
ref,
path: outPath,
targetId: opts.targetId?.trim() || undefined,
timeoutMs,
},
},
{ timeoutMs: timeoutMs ?? 20000 },
);
if (parent?.json) {
defaultRuntime.log(JSON.stringify(result, null, 2));
return;
@@ -126,7 +147,7 @@ export function registerBrowserFilesAndDownloadsCommands(
(v: string) => Number(v),
)
.action(async (opts, cmd) => {
const { parent, baseUrl, profile } = resolveBrowserActionContext(cmd, parentOpts);
const { parent, profile } = resolveBrowserActionContext(cmd, parentOpts);
const accept = opts.accept ? true : opts.dismiss ? false : undefined;
if (accept === undefined) {
defaultRuntime.error(danger("Specify --accept or --dismiss"));
@@ -134,13 +155,22 @@ export function registerBrowserFilesAndDownloadsCommands(
return;
}
try {
const result = await browserArmDialog(baseUrl, {
accept,
promptText: opts.prompt?.trim() || undefined,
targetId: opts.targetId?.trim() || undefined,
timeoutMs: Number.isFinite(opts.timeoutMs) ? opts.timeoutMs : undefined,
profile,
});
const timeoutMs = Number.isFinite(opts.timeoutMs) ? opts.timeoutMs : undefined;
const result = await callBrowserRequest(
parent,
{
method: "POST",
path: "/hooks/dialog",
query: profile ? { profile } : undefined,
body: {
accept,
promptText: opts.prompt?.trim() || undefined,
targetId: opts.targetId?.trim() || undefined,
timeoutMs,
},
},
{ timeoutMs: timeoutMs ?? 20000 },
);
if (parent?.json) {
defaultRuntime.log(JSON.stringify(result, null, 2));
return;

View File

@@ -1,9 +1,8 @@
import type { Command } from "commander";
import { browserAct } from "../../browser/client-actions.js";
import { danger } from "../../globals.js";
import { defaultRuntime } from "../../runtime.js";
import type { BrowserParentOpts } from "../browser-cli-shared.js";
import { readFields, resolveBrowserActionContext } from "./shared.js";
import { callBrowserAct, readFields, resolveBrowserActionContext } from "./shared.js";
export function registerBrowserFormWaitEvalCommands(
browser: Command,
@@ -16,21 +15,21 @@ export function registerBrowserFormWaitEvalCommands(
.option("--fields-file <path>", "Read JSON array from a file")
.option("--target-id <id>", "CDP target id (or unique prefix)")
.action(async (opts, cmd) => {
const { parent, baseUrl, profile } = resolveBrowserActionContext(cmd, parentOpts);
const { parent, profile } = resolveBrowserActionContext(cmd, parentOpts);
try {
const fields = await readFields({
fields: opts.fields,
fieldsFile: opts.fieldsFile,
});
const result = await browserAct(
baseUrl,
{
const result = await callBrowserAct({
parent,
profile,
body: {
kind: "fill",
fields,
targetId: opts.targetId?.trim() || undefined,
},
{ profile },
);
});
if (parent?.json) {
defaultRuntime.log(JSON.stringify(result, null, 2));
return;
@@ -59,16 +58,18 @@ export function registerBrowserFormWaitEvalCommands(
)
.option("--target-id <id>", "CDP target id (or unique prefix)")
.action(async (selector: string | undefined, opts, cmd) => {
const { parent, baseUrl, profile } = resolveBrowserActionContext(cmd, parentOpts);
const { parent, profile } = resolveBrowserActionContext(cmd, parentOpts);
try {
const sel = selector?.trim() || undefined;
const load =
opts.load === "load" || opts.load === "domcontentloaded" || opts.load === "networkidle"
? (opts.load as "load" | "domcontentloaded" | "networkidle")
: undefined;
const result = await browserAct(
baseUrl,
{
const timeoutMs = Number.isFinite(opts.timeoutMs) ? opts.timeoutMs : undefined;
const result = await callBrowserAct({
parent,
profile,
body: {
kind: "wait",
timeMs: Number.isFinite(opts.time) ? opts.time : undefined,
text: opts.text?.trim() || undefined,
@@ -78,10 +79,10 @@ export function registerBrowserFormWaitEvalCommands(
loadState: load,
fn: opts.fn?.trim() || undefined,
targetId: opts.targetId?.trim() || undefined,
timeoutMs: Number.isFinite(opts.timeoutMs) ? opts.timeoutMs : undefined,
timeoutMs,
},
{ profile },
);
timeoutMs,
});
if (parent?.json) {
defaultRuntime.log(JSON.stringify(result, null, 2));
return;
@@ -100,23 +101,23 @@ export function registerBrowserFormWaitEvalCommands(
.option("--ref <id>", "Ref from snapshot")
.option("--target-id <id>", "CDP target id (or unique prefix)")
.action(async (opts, cmd) => {
const { parent, baseUrl, profile } = resolveBrowserActionContext(cmd, parentOpts);
const { parent, profile } = resolveBrowserActionContext(cmd, parentOpts);
if (!opts.fn) {
defaultRuntime.error(danger("Missing --fn"));
defaultRuntime.exit(1);
return;
}
try {
const result = await browserAct(
baseUrl,
{
const result = await callBrowserAct({
parent,
profile,
body: {
kind: "evaluate",
fn: opts.fn,
ref: opts.ref?.trim() || undefined,
targetId: opts.targetId?.trim() || undefined,
},
{ profile },
);
});
if (parent?.json) {
defaultRuntime.log(JSON.stringify(result, null, 2));
return;

View File

@@ -1,8 +1,7 @@
import type { Command } from "commander";
import { browserAct, browserNavigate } from "../../browser/client-actions.js";
import { danger } from "../../globals.js";
import { defaultRuntime } from "../../runtime.js";
import type { BrowserParentOpts } from "../browser-cli-shared.js";
import { callBrowserRequest, type BrowserParentOpts } from "../browser-cli-shared.js";
import { requireRef, resolveBrowserActionContext } from "./shared.js";
export function registerBrowserNavigationCommands(
@@ -15,13 +14,21 @@ export function registerBrowserNavigationCommands(
.argument("<url>", "URL to navigate to")
.option("--target-id <id>", "CDP target id (or unique prefix)")
.action(async (url: string, opts, cmd) => {
const { parent, baseUrl, profile } = resolveBrowserActionContext(cmd, parentOpts);
const { parent, profile } = resolveBrowserActionContext(cmd, parentOpts);
try {
const result = await browserNavigate(baseUrl, {
url,
targetId: opts.targetId?.trim() || undefined,
profile,
});
const result = await callBrowserRequest<{ url?: string }>(
parent,
{
method: "POST",
path: "/navigate",
query: profile ? { profile } : undefined,
body: {
url,
targetId: opts.targetId?.trim() || undefined,
},
},
{ timeoutMs: 20000 },
);
if (parent?.json) {
defaultRuntime.log(JSON.stringify(result, null, 2));
return;
@@ -40,22 +47,27 @@ export function registerBrowserNavigationCommands(
.argument("<height>", "Viewport height", (v: string) => Number(v))
.option("--target-id <id>", "CDP target id (or unique prefix)")
.action(async (width: number, height: number, opts, cmd) => {
const { parent, baseUrl, profile } = resolveBrowserActionContext(cmd, parentOpts);
const { parent, profile } = resolveBrowserActionContext(cmd, parentOpts);
if (!Number.isFinite(width) || !Number.isFinite(height)) {
defaultRuntime.error(danger("width and height must be numbers"));
defaultRuntime.exit(1);
return;
}
try {
const result = await browserAct(
baseUrl,
const result = await callBrowserRequest(
parent,
{
kind: "resize",
width,
height,
targetId: opts.targetId?.trim() || undefined,
method: "POST",
path: "/act",
query: profile ? { profile } : undefined,
body: {
kind: "resize",
width,
height,
targetId: opts.targetId?.trim() || undefined,
},
},
{ profile },
{ timeoutMs: 20000 },
);
if (parent?.json) {
defaultRuntime.log(JSON.stringify(result, null, 2));

View File

@@ -1,13 +1,11 @@
import type { Command } from "commander";
import { resolveBrowserControlUrl } from "../../browser/client.js";
import type { BrowserFormField } from "../../browser/client-actions-core.js";
import { danger } from "../../globals.js";
import { defaultRuntime } from "../../runtime.js";
import type { BrowserParentOpts } from "../browser-cli-shared.js";
import { callBrowserRequest, type BrowserParentOpts } from "../browser-cli-shared.js";
export type BrowserActionContext = {
parent: BrowserParentOpts;
baseUrl: string;
profile: string | undefined;
};
@@ -16,9 +14,26 @@ export function resolveBrowserActionContext(
parentOpts: (cmd: Command) => BrowserParentOpts,
): BrowserActionContext {
const parent = parentOpts(cmd);
const baseUrl = resolveBrowserControlUrl(parent?.url);
const profile = parent?.browserProfile;
return { parent, baseUrl, profile };
return { parent, profile };
}
export async function callBrowserAct<T = unknown>(params: {
parent: BrowserParentOpts;
profile?: string;
body: Record<string, unknown>;
timeoutMs?: number;
}): Promise<T> {
return await callBrowserRequest<T>(
params.parent,
{
method: "POST",
path: "/act",
query: params.profile ? { profile: params.profile } : undefined,
body: params.body,
},
{ timeoutMs: params.timeoutMs ?? 20000 },
);
}
export function requireRef(ref: string | undefined) {

View File

@@ -1,13 +1,7 @@
import type { Command } from "commander";
import { resolveBrowserControlUrl } from "../browser/client.js";
import {
browserConsoleMessages,
browserPdfSave,
browserResponseBody,
} from "../browser/client-actions.js";
import { danger } from "../globals.js";
import { defaultRuntime } from "../runtime.js";
import type { BrowserParentOpts } from "./browser-cli-shared.js";
import { callBrowserRequest, type BrowserParentOpts } from "./browser-cli-shared.js";
import { runCommandWithRuntime } from "./cli-utils.js";
import { shortenHomePath } from "../utils.js";
@@ -29,14 +23,21 @@ export function registerBrowserActionObserveCommands(
.option("--target-id <id>", "CDP target id (or unique prefix)")
.action(async (opts, cmd) => {
const parent = parentOpts(cmd);
const baseUrl = resolveBrowserControlUrl(parent?.url);
const profile = parent?.browserProfile;
await runBrowserObserve(async () => {
const result = await browserConsoleMessages(baseUrl, {
level: opts.level?.trim() || undefined,
targetId: opts.targetId?.trim() || undefined,
profile,
});
const result = await callBrowserRequest<{ messages: unknown[] }>(
parent,
{
method: "GET",
path: "/console",
query: {
level: opts.level?.trim() || undefined,
targetId: opts.targetId?.trim() || undefined,
profile,
},
},
{ timeoutMs: 20000 },
);
if (parent?.json) {
defaultRuntime.log(JSON.stringify(result, null, 2));
return;
@@ -51,13 +52,18 @@ export function registerBrowserActionObserveCommands(
.option("--target-id <id>", "CDP target id (or unique prefix)")
.action(async (opts, cmd) => {
const parent = parentOpts(cmd);
const baseUrl = resolveBrowserControlUrl(parent?.url);
const profile = parent?.browserProfile;
await runBrowserObserve(async () => {
const result = await browserPdfSave(baseUrl, {
targetId: opts.targetId?.trim() || undefined,
profile,
});
const result = await callBrowserRequest<{ path: string }>(
parent,
{
method: "POST",
path: "/pdf",
query: profile ? { profile } : undefined,
body: { targetId: opts.targetId?.trim() || undefined },
},
{ timeoutMs: 20000 },
);
if (parent?.json) {
defaultRuntime.log(JSON.stringify(result, null, 2));
return;
@@ -81,16 +87,25 @@ export function registerBrowserActionObserveCommands(
)
.action(async (url: string, opts, cmd) => {
const parent = parentOpts(cmd);
const baseUrl = resolveBrowserControlUrl(parent?.url);
const profile = parent?.browserProfile;
await runBrowserObserve(async () => {
const result = await browserResponseBody(baseUrl, {
url,
targetId: opts.targetId?.trim() || undefined,
timeoutMs: Number.isFinite(opts.timeoutMs) ? opts.timeoutMs : undefined,
maxChars: Number.isFinite(opts.maxChars) ? opts.maxChars : undefined,
profile,
});
const timeoutMs = Number.isFinite(opts.timeoutMs) ? opts.timeoutMs : undefined;
const maxChars = Number.isFinite(opts.maxChars) ? opts.maxChars : undefined;
const result = await callBrowserRequest<{ response: { body: string } }>(
parent,
{
method: "POST",
path: "/response/body",
query: profile ? { profile } : undefined,
body: {
url,
targetId: opts.targetId?.trim() || undefined,
timeoutMs,
maxChars,
},
},
{ timeoutMs: timeoutMs ?? 20000 },
);
if (parent?.json) {
defaultRuntime.log(JSON.stringify(result, null, 2));
return;

View File

@@ -1,16 +1,8 @@
import type { Command } from "commander";
import { resolveBrowserControlUrl } from "../browser/client.js";
import {
browserHighlight,
browserPageErrors,
browserRequests,
browserTraceStart,
browserTraceStop,
} from "../browser/client-actions.js";
import { danger } from "../globals.js";
import { defaultRuntime } from "../runtime.js";
import type { BrowserParentOpts } from "./browser-cli-shared.js";
import { callBrowserRequest, type BrowserParentOpts } from "./browser-cli-shared.js";
import { runCommandWithRuntime } from "./cli-utils.js";
import { shortenHomePath } from "../utils.js";
@@ -32,14 +24,21 @@ export function registerBrowserDebugCommands(
.option("--target-id <id>", "CDP target id (or unique prefix)")
.action(async (ref: string, opts, cmd) => {
const parent = parentOpts(cmd);
const baseUrl = resolveBrowserControlUrl(parent?.url);
const profile = parent?.browserProfile;
await runBrowserDebug(async () => {
const result = await browserHighlight(baseUrl, {
ref: ref.trim(),
targetId: opts.targetId?.trim() || undefined,
profile,
});
const result = await callBrowserRequest(
parent,
{
method: "POST",
path: "/highlight",
query: profile ? { profile } : undefined,
body: {
ref: ref.trim(),
targetId: opts.targetId?.trim() || undefined,
},
},
{ timeoutMs: 20000 },
);
if (parent?.json) {
defaultRuntime.log(JSON.stringify(result, null, 2));
return;
@@ -55,14 +54,23 @@ export function registerBrowserDebugCommands(
.option("--target-id <id>", "CDP target id (or unique prefix)")
.action(async (opts, cmd) => {
const parent = parentOpts(cmd);
const baseUrl = resolveBrowserControlUrl(parent?.url);
const profile = parent?.browserProfile;
await runBrowserDebug(async () => {
const result = await browserPageErrors(baseUrl, {
targetId: opts.targetId?.trim() || undefined,
clear: Boolean(opts.clear),
profile,
});
const result = await callBrowserRequest<{
errors: Array<{ timestamp: string; name?: string; message: string }>;
}>(
parent,
{
method: "GET",
path: "/errors",
query: {
targetId: opts.targetId?.trim() || undefined,
clear: Boolean(opts.clear),
profile,
},
},
{ timeoutMs: 20000 },
);
if (parent?.json) {
defaultRuntime.log(JSON.stringify(result, null, 2));
return;
@@ -87,15 +95,31 @@ export function registerBrowserDebugCommands(
.option("--target-id <id>", "CDP target id (or unique prefix)")
.action(async (opts, cmd) => {
const parent = parentOpts(cmd);
const baseUrl = resolveBrowserControlUrl(parent?.url);
const profile = parent?.browserProfile;
await runBrowserDebug(async () => {
const result = await browserRequests(baseUrl, {
targetId: opts.targetId?.trim() || undefined,
filter: opts.filter?.trim() || undefined,
clear: Boolean(opts.clear),
profile,
});
const result = await callBrowserRequest<{
requests: Array<{
timestamp: string;
method: string;
status?: number;
ok?: boolean;
url: string;
failureText?: string;
}>;
}>(
parent,
{
method: "GET",
path: "/requests",
query: {
targetId: opts.targetId?.trim() || undefined,
filter: opts.filter?.trim() || undefined,
clear: Boolean(opts.clear),
profile,
},
},
{ timeoutMs: 20000 },
);
if (parent?.json) {
defaultRuntime.log(JSON.stringify(result, null, 2));
return;
@@ -128,16 +152,23 @@ export function registerBrowserDebugCommands(
.option("--sources", "Include sources (bigger traces)", false)
.action(async (opts, cmd) => {
const parent = parentOpts(cmd);
const baseUrl = resolveBrowserControlUrl(parent?.url);
const profile = parent?.browserProfile;
await runBrowserDebug(async () => {
const result = await browserTraceStart(baseUrl, {
targetId: opts.targetId?.trim() || undefined,
screenshots: Boolean(opts.screenshots),
snapshots: Boolean(opts.snapshots),
sources: Boolean(opts.sources),
profile,
});
const result = await callBrowserRequest(
parent,
{
method: "POST",
path: "/trace/start",
query: profile ? { profile } : undefined,
body: {
targetId: opts.targetId?.trim() || undefined,
screenshots: Boolean(opts.screenshots),
snapshots: Boolean(opts.snapshots),
sources: Boolean(opts.sources),
},
},
{ timeoutMs: 20000 },
);
if (parent?.json) {
defaultRuntime.log(JSON.stringify(result, null, 2));
return;
@@ -153,14 +184,21 @@ export function registerBrowserDebugCommands(
.option("--target-id <id>", "CDP target id (or unique prefix)")
.action(async (opts, cmd) => {
const parent = parentOpts(cmd);
const baseUrl = resolveBrowserControlUrl(parent?.url);
const profile = parent?.browserProfile;
await runBrowserDebug(async () => {
const result = await browserTraceStop(baseUrl, {
targetId: opts.targetId?.trim() || undefined,
path: opts.out?.trim() || undefined,
profile,
});
const result = await callBrowserRequest<{ path: string }>(
parent,
{
method: "POST",
path: "/trace/stop",
query: profile ? { profile } : undefined,
body: {
targetId: opts.targetId?.trim() || undefined,
path: opts.out?.trim() || undefined,
},
},
{ timeoutMs: 20000 },
);
if (parent?.json) {
defaultRuntime.log(JSON.stringify(result, null, 2));
return;

View File

@@ -1,12 +1,11 @@
import type { Command } from "commander";
import { browserSnapshot, resolveBrowserControlUrl } from "../browser/client.js";
import { browserScreenshotAction } from "../browser/client-actions.js";
import type { SnapshotResult } from "../browser/client.js";
import { loadConfig } from "../config/config.js";
import { danger } from "../globals.js";
import { defaultRuntime } from "../runtime.js";
import { shortenHomePath } from "../utils.js";
import type { BrowserParentOpts } from "./browser-cli-shared.js";
import { callBrowserRequest, type BrowserParentOpts } from "./browser-cli-shared.js";
export function registerBrowserInspectCommands(
browser: Command,
@@ -22,17 +21,24 @@ export function registerBrowserInspectCommands(
.option("--type <png|jpeg>", "Output type (default: png)", "png")
.action(async (targetId: string | undefined, opts, cmd) => {
const parent = parentOpts(cmd);
const baseUrl = resolveBrowserControlUrl(parent?.url);
const profile = parent?.browserProfile;
try {
const result = await browserScreenshotAction(baseUrl, {
targetId: targetId?.trim() || undefined,
fullPage: Boolean(opts.fullPage),
ref: opts.ref?.trim() || undefined,
element: opts.element?.trim() || undefined,
type: opts.type === "jpeg" ? "jpeg" : "png",
profile,
});
const result = await callBrowserRequest<{ path: string }>(
parent,
{
method: "POST",
path: "/screenshot",
query: profile ? { profile } : undefined,
body: {
targetId: targetId?.trim() || undefined,
fullPage: Boolean(opts.fullPage),
ref: opts.ref?.trim() || undefined,
element: opts.element?.trim() || undefined,
type: opts.type === "jpeg" ? "jpeg" : "png",
},
},
{ timeoutMs: 20000 },
);
if (parent?.json) {
defaultRuntime.log(JSON.stringify(result, null, 2));
return;
@@ -61,7 +67,6 @@ export function registerBrowserInspectCommands(
.option("--out <path>", "Write snapshot to a file")
.action(async (opts, cmd) => {
const parent = parentOpts(cmd);
const baseUrl = resolveBrowserControlUrl(parent?.url);
const profile = parent?.browserProfile;
const format = opts.format === "aria" ? "aria" : "ai";
const configMode =
@@ -70,19 +75,28 @@ export function registerBrowserInspectCommands(
: undefined;
const mode = opts.efficient === true || opts.mode === "efficient" ? "efficient" : configMode;
try {
const result = await browserSnapshot(baseUrl, {
const query: Record<string, string | number | boolean | undefined> = {
format,
targetId: opts.targetId?.trim() || undefined,
limit: Number.isFinite(opts.limit) ? opts.limit : undefined,
interactive: Boolean(opts.interactive) || undefined,
compact: Boolean(opts.compact) || undefined,
interactive: opts.interactive ? true : undefined,
compact: opts.compact ? true : undefined,
depth: Number.isFinite(opts.depth) ? opts.depth : undefined,
selector: opts.selector?.trim() || undefined,
frame: opts.frame?.trim() || undefined,
labels: Boolean(opts.labels) || undefined,
labels: opts.labels ? true : undefined,
mode,
profile,
});
};
const result = await callBrowserRequest<SnapshotResult>(
parent,
{
method: "GET",
path: "/snapshot",
query,
},
{ timeoutMs: 20000 },
);
if (opts.out) {
const fs = await import("node:fs/promises");

View File

@@ -1,25 +1,16 @@
import type { Command } from "commander";
import type { BrowserTab } from "../browser/client.js";
import {
browserCloseTab,
browserCreateProfile,
browserDeleteProfile,
browserFocusTab,
browserOpenTab,
browserProfiles,
browserResetProfile,
browserStart,
browserStatus,
browserStop,
browserTabAction,
browserTabs,
resolveBrowserControlUrl,
import type {
BrowserCreateProfileResult,
BrowserDeleteProfileResult,
BrowserResetProfileResult,
BrowserStatus,
BrowserTab,
ProfileStatus,
} from "../browser/client.js";
import { browserAct } from "../browser/client-actions-core.js";
import { danger, info } from "../globals.js";
import { defaultRuntime } from "../runtime.js";
import { shortenHomePath } from "../utils.js";
import type { BrowserParentOpts } from "./browser-cli-shared.js";
import { callBrowserRequest, type BrowserParentOpts } from "./browser-cli-shared.js";
import { runCommandWithRuntime } from "./cli-utils.js";
function runBrowserCommand(action: () => Promise<void>) {
@@ -38,11 +29,18 @@ export function registerBrowserManageCommands(
.description("Show browser status")
.action(async (_opts, cmd) => {
const parent = parentOpts(cmd);
const baseUrl = resolveBrowserControlUrl(parent?.url);
await runBrowserCommand(async () => {
const status = await browserStatus(baseUrl, {
profile: parent?.browserProfile,
});
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;
@@ -54,7 +52,6 @@ export function registerBrowserManageCommands(
`profile: ${status.profile ?? "clawd"}`,
`enabled: ${status.enabled}`,
`running: ${status.running}`,
`controlUrl: ${status.controlUrl}`,
`cdpPort: ${status.cdpPort}`,
`cdpUrl: ${status.cdpUrl ?? `http://127.0.0.1:${status.cdpPort}`}`,
`browser: ${status.chosenBrowser ?? "unknown"}`,
@@ -72,11 +69,26 @@ export function registerBrowserManageCommands(
.description("Start the browser (no-op if already running)")
.action(async (_opts, cmd) => {
const parent = parentOpts(cmd);
const baseUrl = resolveBrowserControlUrl(parent?.url);
const profile = parent?.browserProfile;
await runBrowserCommand(async () => {
await browserStart(baseUrl, { profile });
const status = await browserStatus(baseUrl, { profile });
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;
@@ -91,11 +103,26 @@ export function registerBrowserManageCommands(
.description("Stop the browser (best-effort)")
.action(async (_opts, cmd) => {
const parent = parentOpts(cmd);
const baseUrl = resolveBrowserControlUrl(parent?.url);
const profile = parent?.browserProfile;
await runBrowserCommand(async () => {
await browserStop(baseUrl, { profile });
const status = await browserStatus(baseUrl, { profile });
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;
@@ -110,10 +137,17 @@ export function registerBrowserManageCommands(
.description("Reset browser profile (moves it to Trash)")
.action(async (_opts, cmd) => {
const parent = parentOpts(cmd);
const baseUrl = resolveBrowserControlUrl(parent?.url);
const profile = parent?.browserProfile;
await runBrowserCommand(async () => {
const result = await browserResetProfile(baseUrl, { profile });
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;
@@ -132,10 +166,18 @@ export function registerBrowserManageCommands(
.description("List open tabs")
.action(async (_opts, cmd) => {
const parent = parentOpts(cmd);
const baseUrl = resolveBrowserControlUrl(parent?.url);
const profile = parent?.browserProfile;
await runBrowserCommand(async () => {
const tabs = await browserTabs(baseUrl, { profile });
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;
@@ -159,13 +201,20 @@ export function registerBrowserManageCommands(
.description("Tab shortcuts (index-based)")
.action(async (_opts, cmd) => {
const parent = parentOpts(cmd);
const baseUrl = resolveBrowserControlUrl(parent?.url);
const profile = parent?.browserProfile;
await runBrowserCommand(async () => {
const result = (await browserTabAction(baseUrl, {
action: "list",
profile,
})) as { ok: true; tabs: BrowserTab[] };
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));
@@ -190,13 +239,18 @@ export function registerBrowserManageCommands(
.description("Open a new tab (about:blank)")
.action(async (_opts, cmd) => {
const parent = parentOpts(cmd);
const baseUrl = resolveBrowserControlUrl(parent?.url);
const profile = parent?.browserProfile;
await runBrowserCommand(async () => {
const result = await browserTabAction(baseUrl, {
action: "new",
profile,
});
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;
@@ -211,7 +265,6 @@ export function registerBrowserManageCommands(
.argument("<index>", "Tab index (1-based)", (v: string) => Number(v))
.action(async (index: number, _opts, cmd) => {
const parent = parentOpts(cmd);
const baseUrl = resolveBrowserControlUrl(parent?.url);
const profile = parent?.browserProfile;
if (!Number.isFinite(index) || index < 1) {
defaultRuntime.error(danger("index must be a positive number"));
@@ -219,11 +272,16 @@ export function registerBrowserManageCommands(
return;
}
await runBrowserCommand(async () => {
const result = await browserTabAction(baseUrl, {
action: "select",
index: Math.floor(index) - 1,
profile,
});
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;
@@ -238,7 +296,6 @@ export function registerBrowserManageCommands(
.argument("[index]", "Tab index (1-based)", (v: string) => Number(v))
.action(async (index: number | undefined, _opts, cmd) => {
const parent = parentOpts(cmd);
const baseUrl = resolveBrowserControlUrl(parent?.url);
const profile = parent?.browserProfile;
const idx =
typeof index === "number" && Number.isFinite(index) ? Math.floor(index) - 1 : undefined;
@@ -248,11 +305,16 @@ export function registerBrowserManageCommands(
return;
}
await runBrowserCommand(async () => {
const result = await browserTabAction(baseUrl, {
action: "close",
index: idx,
profile,
});
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;
@@ -267,10 +329,18 @@ export function registerBrowserManageCommands(
.argument("<url>", "URL to open")
.action(async (url: string, _opts, cmd) => {
const parent = parentOpts(cmd);
const baseUrl = resolveBrowserControlUrl(parent?.url);
const profile = parent?.browserProfile;
await runBrowserCommand(async () => {
const tab = await browserOpenTab(baseUrl, url, { profile });
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;
@@ -285,10 +355,18 @@ export function registerBrowserManageCommands(
.argument("<targetId>", "Target id or unique prefix")
.action(async (targetId: string, _opts, cmd) => {
const parent = parentOpts(cmd);
const baseUrl = resolveBrowserControlUrl(parent?.url);
const profile = parent?.browserProfile;
await runBrowserCommand(async () => {
await browserFocusTab(baseUrl, targetId, { profile });
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;
@@ -303,13 +381,29 @@ export function registerBrowserManageCommands(
.argument("[targetId]", "Target id or unique prefix (optional)")
.action(async (targetId: string | undefined, _opts, cmd) => {
const parent = parentOpts(cmd);
const baseUrl = resolveBrowserControlUrl(parent?.url);
const profile = parent?.browserProfile;
await runBrowserCommand(async () => {
if (targetId?.trim()) {
await browserCloseTab(baseUrl, targetId.trim(), { profile });
await callBrowserRequest(
parent,
{
method: "DELETE",
path: `/tabs/${encodeURIComponent(targetId.trim())}`,
query: profile ? { profile } : undefined,
},
{ timeoutMs: 5000 },
);
} else {
await browserAct(baseUrl, { kind: "close" }, { profile });
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));
@@ -325,9 +419,16 @@ export function registerBrowserManageCommands(
.description("List all browser profiles")
.action(async (_opts, cmd) => {
const parent = parentOpts(cmd);
const baseUrl = resolveBrowserControlUrl(parent?.url);
await runBrowserCommand(async () => {
const profiles = await browserProfiles(baseUrl);
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;
@@ -361,14 +462,21 @@ export function registerBrowserManageCommands(
.action(
async (opts: { name: string; color?: string; cdpUrl?: string; driver?: string }, cmd) => {
const parent = parentOpts(cmd);
const baseUrl = resolveBrowserControlUrl(parent?.url);
await runBrowserCommand(async () => {
const result = await browserCreateProfile(baseUrl, {
name: opts.name,
color: opts.color,
cdpUrl: opts.cdpUrl,
driver: opts.driver === "extension" ? "extension" : undefined,
});
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;
@@ -391,9 +499,15 @@ export function registerBrowserManageCommands(
.requiredOption("--name <name>", "Profile name to delete")
.action(async (opts: { name: string }, cmd) => {
const parent = parentOpts(cmd);
const baseUrl = resolveBrowserControlUrl(parent?.url);
await runBrowserCommand(async () => {
const result = await browserDeleteProfile(baseUrl, opts.name);
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;

View File

@@ -1,121 +0,0 @@
import type { Command } from "commander";
import { loadConfig } from "../config/config.js";
import { danger, info } from "../globals.js";
import { defaultRuntime } from "../runtime.js";
import { resolveBrowserConfig, resolveProfile } from "../browser/config.js";
import { startBrowserBridgeServer, stopBrowserBridgeServer } from "../browser/bridge-server.js";
import { ensureChromeExtensionRelayServer } from "../browser/extension-relay.js";
function isLoopbackBindHost(host: string) {
const h = host.trim().toLowerCase();
return h === "localhost" || h === "127.0.0.1" || h === "::1" || h === "[::1]";
}
function parsePort(raw: unknown): number | null {
const v = typeof raw === "string" ? raw.trim() : "";
if (!v) return null;
const n = Number.parseInt(v, 10);
if (!Number.isFinite(n) || n < 0 || n > 65535) return null;
return n;
}
export function registerBrowserServeCommands(
browser: Command,
_parentOpts: (cmd: Command) => unknown,
) {
browser
.command("serve")
.description("Run a standalone browser control server (for remote gateways)")
.option("--bind <host>", "Bind host (default: 127.0.0.1)")
.option("--port <port>", "Bind port (default: from browser.controlUrl)")
.option(
"--token <token>",
"Require Authorization: Bearer <token> (required when binding non-loopback)",
)
.action(async (opts: { bind?: string; port?: string; token?: string }) => {
const cfg = loadConfig();
const resolved = resolveBrowserConfig(cfg.browser);
if (!resolved.enabled) {
defaultRuntime.error(
danger("Browser control is disabled. Set browser.enabled=true and try again."),
);
defaultRuntime.exit(1);
}
const host = (opts.bind ?? "127.0.0.1").trim();
const port = parsePort(opts.port) ?? resolved.controlPort;
const envToken = process.env.CLAWDBOT_BROWSER_CONTROL_TOKEN?.trim();
const authToken = (opts.token ?? envToken ?? resolved.controlToken)?.trim();
if (!isLoopbackBindHost(host) && !authToken) {
defaultRuntime.error(
danger(
`Refusing to bind browser control on ${host} without --token (or CLAWDBOT_BROWSER_CONTROL_TOKEN, or browser.controlToken).`,
),
);
defaultRuntime.exit(1);
}
const bridge = await startBrowserBridgeServer({
resolved,
host,
port,
...(authToken ? { authToken } : {}),
});
// If any profile uses the Chrome extension relay, start the local relay server eagerly
// so the extension can connect before the first browser action.
for (const name of Object.keys(resolved.profiles)) {
const profile = resolveProfile(resolved, name);
if (!profile || profile.driver !== "extension") continue;
await ensureChromeExtensionRelayServer({ cdpUrl: profile.cdpUrl }).catch((err) => {
defaultRuntime.error(
danger(`Chrome extension relay init failed for profile "${name}": ${String(err)}`),
);
});
}
defaultRuntime.log(
info(
[
`🦞 Browser control listening on ${bridge.baseUrl}/`,
authToken ? "Auth: Bearer token required." : "Auth: off (loopback only).",
"",
"Paste on the Gateway (clawdbot.json):",
JSON.stringify(
{
browser: {
enabled: true,
controlUrl: bridge.baseUrl,
...(authToken ? { controlToken: authToken } : {}),
},
},
null,
2,
),
...(authToken
? [
"",
"Or use env on the Gateway (instead of controlToken in config):",
`export CLAWDBOT_BROWSER_CONTROL_TOKEN=${JSON.stringify(authToken)}`,
]
: []),
].join("\n"),
),
);
let shuttingDown = false;
const shutdown = async (signal: string) => {
if (shuttingDown) return;
shuttingDown = true;
defaultRuntime.log(info(`Shutting down (${signal})...`));
await stopBrowserBridgeServer(bridge.server).catch(() => {});
process.exit(0);
};
process.once("SIGINT", () => void shutdown("SIGINT"));
process.once("SIGTERM", () => void shutdown("SIGTERM"));
await new Promise(() => {});
});
}

View File

@@ -1,5 +1,58 @@
export type BrowserParentOpts = {
url?: string;
import type { GatewayRpcOpts } from "./gateway-rpc.js";
import { callGatewayFromCli } from "./gateway-rpc.js";
export type BrowserParentOpts = GatewayRpcOpts & {
json?: boolean;
browserProfile?: string;
};
type BrowserRequestParams = {
method: "GET" | "POST" | "DELETE";
path: string;
query?: Record<string, string | number | boolean | undefined>;
body?: unknown;
};
function normalizeQuery(query: BrowserRequestParams["query"]): Record<string, string> | undefined {
if (!query) return undefined;
const out: Record<string, string> = {};
for (const [key, value] of Object.entries(query)) {
if (value === undefined) continue;
out[key] = String(value);
}
return Object.keys(out).length ? out : undefined;
}
export async function callBrowserRequest<T>(
opts: BrowserParentOpts,
params: BrowserRequestParams,
extra?: { timeoutMs?: number; progress?: boolean },
): Promise<T> {
const resolvedTimeoutMs =
typeof extra?.timeoutMs === "number" && Number.isFinite(extra.timeoutMs)
? Math.max(1, Math.floor(extra.timeoutMs))
: typeof opts.timeout === "string"
? Number.parseInt(opts.timeout, 10)
: undefined;
const resolvedTimeout =
typeof resolvedTimeoutMs === "number" && Number.isFinite(resolvedTimeoutMs)
? resolvedTimeoutMs
: undefined;
const timeout = typeof resolvedTimeout === "number" ? String(resolvedTimeout) : opts.timeout;
const payload = await callGatewayFromCli(
"browser.request",
{ ...opts, timeout },
{
method: params.method,
path: params.path,
query: normalizeQuery(params.query),
body: params.body,
timeoutMs: resolvedTimeout,
},
{ progress: extra?.progress },
);
if (payload === undefined) {
throw new Error("Unexpected browser.request response");
}
return payload as T;
}

View File

@@ -1,17 +1,8 @@
import type { Command } from "commander";
import { resolveBrowserControlUrl } from "../browser/client.js";
import {
browserCookies,
browserCookiesClear,
browserCookiesSet,
browserStorageClear,
browserStorageGet,
browserStorageSet,
} from "../browser/client-actions.js";
import { danger } from "../globals.js";
import { defaultRuntime } from "../runtime.js";
import type { BrowserParentOpts } from "./browser-cli-shared.js";
import { callBrowserRequest, type BrowserParentOpts } from "./browser-cli-shared.js";
export function registerBrowserCookiesAndStorageCommands(
browser: Command,
@@ -23,13 +14,20 @@ export function registerBrowserCookiesAndStorageCommands(
.option("--target-id <id>", "CDP target id (or unique prefix)")
.action(async (opts, cmd) => {
const parent = parentOpts(cmd);
const baseUrl = resolveBrowserControlUrl(parent?.url);
const profile = parent?.browserProfile;
try {
const result = await browserCookies(baseUrl, {
targetId: opts.targetId?.trim() || undefined,
profile,
});
const result = await callBrowserRequest<{ cookies?: unknown[] }>(
parent,
{
method: "GET",
path: "/cookies",
query: {
targetId: opts.targetId?.trim() || undefined,
profile,
},
},
{ timeoutMs: 20000 },
);
if (parent?.json) {
defaultRuntime.log(JSON.stringify(result, null, 2));
return;
@@ -50,14 +48,21 @@ export function registerBrowserCookiesAndStorageCommands(
.option("--target-id <id>", "CDP target id (or unique prefix)")
.action(async (name: string, value: string, opts, cmd) => {
const parent = parentOpts(cmd);
const baseUrl = resolveBrowserControlUrl(parent?.url);
const profile = parent?.browserProfile;
try {
const result = await browserCookiesSet(baseUrl, {
targetId: opts.targetId?.trim() || undefined,
cookie: { name, value, url: opts.url },
profile,
});
const result = await callBrowserRequest(
parent,
{
method: "POST",
path: "/cookies/set",
query: profile ? { profile } : undefined,
body: {
targetId: opts.targetId?.trim() || undefined,
cookie: { name, value, url: opts.url },
},
},
{ timeoutMs: 20000 },
);
if (parent?.json) {
defaultRuntime.log(JSON.stringify(result, null, 2));
return;
@@ -75,13 +80,20 @@ export function registerBrowserCookiesAndStorageCommands(
.option("--target-id <id>", "CDP target id (or unique prefix)")
.action(async (opts, cmd) => {
const parent = parentOpts(cmd);
const baseUrl = resolveBrowserControlUrl(parent?.url);
const profile = parent?.browserProfile;
try {
const result = await browserCookiesClear(baseUrl, {
targetId: opts.targetId?.trim() || undefined,
profile,
});
const result = await callBrowserRequest(
parent,
{
method: "POST",
path: "/cookies/clear",
query: profile ? { profile } : undefined,
body: {
targetId: opts.targetId?.trim() || undefined,
},
},
{ timeoutMs: 20000 },
);
if (parent?.json) {
defaultRuntime.log(JSON.stringify(result, null, 2));
return;
@@ -105,15 +117,21 @@ export function registerBrowserCookiesAndStorageCommands(
.option("--target-id <id>", "CDP target id (or unique prefix)")
.action(async (key: string | undefined, opts, cmd2) => {
const parent = parentOpts(cmd2);
const baseUrl = resolveBrowserControlUrl(parent?.url);
const profile = parent?.browserProfile;
try {
const result = await browserStorageGet(baseUrl, {
kind,
key: key?.trim() || undefined,
targetId: opts.targetId?.trim() || undefined,
profile,
});
const result = await callBrowserRequest<{ values?: Record<string, string> }>(
parent,
{
method: "GET",
path: `/storage/${kind}`,
query: {
key: key?.trim() || undefined,
targetId: opts.targetId?.trim() || undefined,
profile,
},
},
{ timeoutMs: 20000 },
);
if (parent?.json) {
defaultRuntime.log(JSON.stringify(result, null, 2));
return;
@@ -133,16 +151,22 @@ export function registerBrowserCookiesAndStorageCommands(
.option("--target-id <id>", "CDP target id (or unique prefix)")
.action(async (key: string, value: string, opts, cmd2) => {
const parent = parentOpts(cmd2);
const baseUrl = resolveBrowserControlUrl(parent?.url);
const profile = parent?.browserProfile;
try {
const result = await browserStorageSet(baseUrl, {
kind,
key,
value,
targetId: opts.targetId?.trim() || undefined,
profile,
});
const result = await callBrowserRequest(
parent,
{
method: "POST",
path: `/storage/${kind}/set`,
query: profile ? { profile } : undefined,
body: {
key,
value,
targetId: opts.targetId?.trim() || undefined,
},
},
{ timeoutMs: 20000 },
);
if (parent?.json) {
defaultRuntime.log(JSON.stringify(result, null, 2));
return;
@@ -160,14 +184,20 @@ export function registerBrowserCookiesAndStorageCommands(
.option("--target-id <id>", "CDP target id (or unique prefix)")
.action(async (opts, cmd2) => {
const parent = parentOpts(cmd2);
const baseUrl = resolveBrowserControlUrl(parent?.url);
const profile = parent?.browserProfile;
try {
const result = await browserStorageClear(baseUrl, {
kind,
targetId: opts.targetId?.trim() || undefined,
profile,
});
const result = await callBrowserRequest(
parent,
{
method: "POST",
path: `/storage/${kind}/clear`,
query: profile ? { profile } : undefined,
body: {
targetId: opts.targetId?.trim() || undefined,
},
},
{ timeoutMs: 20000 },
);
if (parent?.json) {
defaultRuntime.log(JSON.stringify(result, null, 2));
return;

View File

@@ -1,21 +1,9 @@
import type { Command } from "commander";
import { resolveBrowserControlUrl } from "../browser/client.js";
import {
browserSetDevice,
browserSetGeolocation,
browserSetHeaders,
browserSetHttpCredentials,
browserSetLocale,
browserSetMedia,
browserSetOffline,
browserSetTimezone,
} from "../browser/client-actions.js";
import { browserAct } from "../browser/client-actions-core.js";
import { danger } from "../globals.js";
import { defaultRuntime } from "../runtime.js";
import { parseBooleanValue } from "../utils/boolean.js";
import type { BrowserParentOpts } from "./browser-cli-shared.js";
import { callBrowserRequest, type BrowserParentOpts } from "./browser-cli-shared.js";
import { registerBrowserCookiesAndStorageCommands } from "./browser-cli-state.cookies-storage.js";
import { runCommandWithRuntime } from "./cli-utils.js";
@@ -47,7 +35,6 @@ export function registerBrowserStateCommands(
.option("--target-id <id>", "CDP target id (or unique prefix)")
.action(async (width: number, height: number, opts, cmd) => {
const parent = parentOpts(cmd);
const baseUrl = resolveBrowserControlUrl(parent?.url);
const profile = parent?.browserProfile;
if (!Number.isFinite(width) || !Number.isFinite(height)) {
defaultRuntime.error(danger("width and height must be numbers"));
@@ -55,15 +42,20 @@ export function registerBrowserStateCommands(
return;
}
await runBrowserCommand(async () => {
const result = await browserAct(
baseUrl,
const result = await callBrowserRequest(
parent,
{
kind: "resize",
width,
height,
targetId: opts.targetId?.trim() || undefined,
method: "POST",
path: "/act",
query: profile ? { profile } : undefined,
body: {
kind: "resize",
width,
height,
targetId: opts.targetId?.trim() || undefined,
},
},
{ profile },
{ timeoutMs: 20000 },
);
if (parent?.json) {
defaultRuntime.log(JSON.stringify(result, null, 2));
@@ -80,7 +72,6 @@ export function registerBrowserStateCommands(
.option("--target-id <id>", "CDP target id (or unique prefix)")
.action(async (value: string, opts, cmd) => {
const parent = parentOpts(cmd);
const baseUrl = resolveBrowserControlUrl(parent?.url);
const profile = parent?.browserProfile;
const offline = parseOnOff(value);
if (offline === null) {
@@ -89,11 +80,19 @@ export function registerBrowserStateCommands(
return;
}
await runBrowserCommand(async () => {
const result = await browserSetOffline(baseUrl, {
offline,
targetId: opts.targetId?.trim() || undefined,
profile,
});
const result = await callBrowserRequest(
parent,
{
method: "POST",
path: "/set/offline",
query: profile ? { profile } : undefined,
body: {
offline,
targetId: opts.targetId?.trim() || undefined,
},
},
{ timeoutMs: 20000 },
);
if (parent?.json) {
defaultRuntime.log(JSON.stringify(result, null, 2));
return;
@@ -109,7 +108,6 @@ export function registerBrowserStateCommands(
.option("--target-id <id>", "CDP target id (or unique prefix)")
.action(async (opts, cmd) => {
const parent = parentOpts(cmd);
const baseUrl = resolveBrowserControlUrl(parent?.url);
const profile = parent?.browserProfile;
await runBrowserCommand(async () => {
const parsed = JSON.parse(String(opts.json)) as unknown;
@@ -120,11 +118,19 @@ export function registerBrowserStateCommands(
for (const [k, v] of Object.entries(parsed as Record<string, unknown>)) {
if (typeof v === "string") headers[k] = v;
}
const result = await browserSetHeaders(baseUrl, {
headers,
targetId: opts.targetId?.trim() || undefined,
profile,
});
const result = await callBrowserRequest(
parent,
{
method: "POST",
path: "/set/headers",
query: profile ? { profile } : undefined,
body: {
headers,
targetId: opts.targetId?.trim() || undefined,
},
},
{ timeoutMs: 20000 },
);
if (parent?.json) {
defaultRuntime.log(JSON.stringify(result, null, 2));
return;
@@ -142,16 +148,23 @@ export function registerBrowserStateCommands(
.option("--target-id <id>", "CDP target id (or unique prefix)")
.action(async (username: string | undefined, password: string | undefined, opts, cmd) => {
const parent = parentOpts(cmd);
const baseUrl = resolveBrowserControlUrl(parent?.url);
const profile = parent?.browserProfile;
await runBrowserCommand(async () => {
const result = await browserSetHttpCredentials(baseUrl, {
username: username?.trim() || undefined,
password,
clear: Boolean(opts.clear),
targetId: opts.targetId?.trim() || undefined,
profile,
});
const result = await callBrowserRequest(
parent,
{
method: "POST",
path: "/set/credentials",
query: profile ? { profile } : undefined,
body: {
username: username?.trim() || undefined,
password,
clear: Boolean(opts.clear),
targetId: opts.targetId?.trim() || undefined,
},
},
{ timeoutMs: 20000 },
);
if (parent?.json) {
defaultRuntime.log(JSON.stringify(result, null, 2));
return;
@@ -171,18 +184,25 @@ export function registerBrowserStateCommands(
.option("--target-id <id>", "CDP target id (or unique prefix)")
.action(async (latitude: number | undefined, longitude: number | undefined, opts, cmd) => {
const parent = parentOpts(cmd);
const baseUrl = resolveBrowserControlUrl(parent?.url);
const profile = parent?.browserProfile;
await runBrowserCommand(async () => {
const result = await browserSetGeolocation(baseUrl, {
latitude: Number.isFinite(latitude) ? latitude : undefined,
longitude: Number.isFinite(longitude) ? longitude : undefined,
accuracy: Number.isFinite(opts.accuracy) ? opts.accuracy : undefined,
origin: opts.origin?.trim() || undefined,
clear: Boolean(opts.clear),
targetId: opts.targetId?.trim() || undefined,
profile,
});
const result = await callBrowserRequest(
parent,
{
method: "POST",
path: "/set/geolocation",
query: profile ? { profile } : undefined,
body: {
latitude: Number.isFinite(latitude) ? latitude : undefined,
longitude: Number.isFinite(longitude) ? longitude : undefined,
accuracy: Number.isFinite(opts.accuracy) ? opts.accuracy : undefined,
origin: opts.origin?.trim() || undefined,
clear: Boolean(opts.clear),
targetId: opts.targetId?.trim() || undefined,
},
},
{ timeoutMs: 20000 },
);
if (parent?.json) {
defaultRuntime.log(JSON.stringify(result, null, 2));
return;
@@ -198,7 +218,6 @@ export function registerBrowserStateCommands(
.option("--target-id <id>", "CDP target id (or unique prefix)")
.action(async (value: string, opts, cmd) => {
const parent = parentOpts(cmd);
const baseUrl = resolveBrowserControlUrl(parent?.url);
const profile = parent?.browserProfile;
const v = value.trim().toLowerCase();
const colorScheme =
@@ -209,11 +228,19 @@ export function registerBrowserStateCommands(
return;
}
await runBrowserCommand(async () => {
const result = await browserSetMedia(baseUrl, {
colorScheme,
targetId: opts.targetId?.trim() || undefined,
profile,
});
const result = await callBrowserRequest(
parent,
{
method: "POST",
path: "/set/media",
query: profile ? { profile } : undefined,
body: {
colorScheme,
targetId: opts.targetId?.trim() || undefined,
},
},
{ timeoutMs: 20000 },
);
if (parent?.json) {
defaultRuntime.log(JSON.stringify(result, null, 2));
return;
@@ -229,14 +256,21 @@ export function registerBrowserStateCommands(
.option("--target-id <id>", "CDP target id (or unique prefix)")
.action(async (timezoneId: string, opts, cmd) => {
const parent = parentOpts(cmd);
const baseUrl = resolveBrowserControlUrl(parent?.url);
const profile = parent?.browserProfile;
await runBrowserCommand(async () => {
const result = await browserSetTimezone(baseUrl, {
timezoneId,
targetId: opts.targetId?.trim() || undefined,
profile,
});
const result = await callBrowserRequest(
parent,
{
method: "POST",
path: "/set/timezone",
query: profile ? { profile } : undefined,
body: {
timezoneId,
targetId: opts.targetId?.trim() || undefined,
},
},
{ timeoutMs: 20000 },
);
if (parent?.json) {
defaultRuntime.log(JSON.stringify(result, null, 2));
return;
@@ -252,14 +286,21 @@ export function registerBrowserStateCommands(
.option("--target-id <id>", "CDP target id (or unique prefix)")
.action(async (locale: string, opts, cmd) => {
const parent = parentOpts(cmd);
const baseUrl = resolveBrowserControlUrl(parent?.url);
const profile = parent?.browserProfile;
await runBrowserCommand(async () => {
const result = await browserSetLocale(baseUrl, {
locale,
targetId: opts.targetId?.trim() || undefined,
profile,
});
const result = await callBrowserRequest(
parent,
{
method: "POST",
path: "/set/locale",
query: profile ? { profile } : undefined,
body: {
locale,
targetId: opts.targetId?.trim() || undefined,
},
},
{ timeoutMs: 20000 },
);
if (parent?.json) {
defaultRuntime.log(JSON.stringify(result, null, 2));
return;
@@ -275,14 +316,21 @@ export function registerBrowserStateCommands(
.option("--target-id <id>", "CDP target id (or unique prefix)")
.action(async (name: string, opts, cmd) => {
const parent = parentOpts(cmd);
const baseUrl = resolveBrowserControlUrl(parent?.url);
const profile = parent?.browserProfile;
await runBrowserCommand(async () => {
const result = await browserSetDevice(baseUrl, {
name,
targetId: opts.targetId?.trim() || undefined,
profile,
});
const result = await callBrowserRequest(
parent,
{
method: "POST",
path: "/set/device",
query: profile ? { profile } : undefined,
body: {
name,
targetId: opts.targetId?.trim() || undefined,
},
},
{ timeoutMs: 20000 },
);
if (parent?.json) {
defaultRuntime.log(JSON.stringify(result, null, 2));
return;

View File

@@ -13,15 +13,14 @@ import { browserActionExamples, browserCoreExamples } from "./browser-cli-exampl
import { registerBrowserExtensionCommands } from "./browser-cli-extension.js";
import { registerBrowserInspectCommands } from "./browser-cli-inspect.js";
import { registerBrowserManageCommands } from "./browser-cli-manage.js";
import { registerBrowserServeCommands } from "./browser-cli-serve.js";
import type { BrowserParentOpts } from "./browser-cli-shared.js";
import { registerBrowserStateCommands } from "./browser-cli-state.js";
import { addGatewayClientOptions } from "./gateway-rpc.js";
export function registerBrowserCli(program: Command) {
const browser = program
.command("browser")
.description("Manage clawd's dedicated browser (Chrome/Chromium)")
.option("--url <url>", "Override browser control URL (default from ~/.clawdbot/clawdbot.json)")
.option("--browser-profile <name>", "Browser profile name (default from config)")
.option("--json", "Output machine-readable JSON", false)
.addHelpText(
@@ -43,11 +42,12 @@ export function registerBrowserCli(program: Command) {
defaultRuntime.exit(1);
});
addGatewayClientOptions(browser);
const parentOpts = (cmd: Command) => cmd.parent?.opts?.() as BrowserParentOpts;
registerBrowserManageCommands(browser, parentOpts);
registerBrowserExtensionCommands(browser, parentOpts);
registerBrowserServeCommands(browser, parentOpts);
registerBrowserInspectCommands(browser, parentOpts);
registerBrowserActionInputCommands(browser, parentOpts);
registerBrowserActionObserveCommands(browser, parentOpts);