feat(browser): add openclaw browser batch CLI subcommand (#111457)

* feat(browser): add `openclaw browser batch` CLI subcommand

Expose the existing `act:batch` runtime through a dedicated CLI subcommand so
users and scripts can run nested act requests in one call without going through
the agent tool.

- `--actions <json>` for inline JSON, `--actions-file <path>` for file input,
  `--actions-file -` for stdin (1MB cap to bound a runaway pipe)
- `--continue` sets `stopOnError=false`; default keeps the runtime fail-fast
  (stop on first error) behavior, matching the existing batch contract
- `--target-id` forwards to the batch body
- Outer request timeout uses `resolveBrowserActExecutionBudgetMs` so the batch
  budget covers nested actions
- Docs (`docs/tools/browser-control.md` + bundled `browser-automation` skill)
  document the batch CLI, ref lifecycle, targetId conflict handling, and error
  summary format

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(browser): fail batch CLI on action errors

* fix(browser): keep batch stdin reader private

* docs(browser): refresh batch heading map

* test(browser): assert batch JSON via runtime mock

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Mason Huang <masonxhuang@icloud.com>
Co-authored-by: Mason Huang <masonxhuang@proton.me>
This commit is contained in:
FMLS
2026-07-24 11:33:45 +08:00
committed by GitHub
parent edc344834b
commit b88eeddeed
11 changed files with 487 additions and 1 deletions
+10
View File
@@ -197,6 +197,16 @@ Managed Chrome profiles save ordinary click-triggered downloads into the OpenCla
When an action opens a modal dialog, the action response returns `blockedByDialog` with `browserState.dialogs.pending`; pass `--dialog-id` to answer it directly. Dialogs handled outside OpenClaw appear under `browserState.dialogs.recent`.
Batch actions:
```bash
openclaw browser batch --actions '[{"kind":"wait","timeMs":500},{"kind":"click","ref":"12"},{"kind":"type","ref":"23","text":"hello"}]'
openclaw browser batch --actions-file plan.json
openclaw browser batch --actions-file - --continue
```
`openclaw browser batch` sends a `kind="batch"` `/act` request with nested `BrowserActRequest` actions (`wait`, `click`, `type`, `evaluate`, ...) — not `open`/`navigate`/`snapshot`/`screenshot`, which are CLI subcommands, not `/act` kinds. `--continue` sets `stopOnError=false` (default stops on first error); `--target-id` scopes the whole batch to one tab. A failed nested action makes the command exit nonzero; use `--json` to retain the ordered `results` response. See [Browser batch CLI](/tools/browser-control#browser-batch-cli) for the full contract (ref lifecycle, target id conflicts, error summary). `batch` is not supported on `profile="user"` / existing-session profiles.
## State and storage
Viewport + emulation:
+1
View File
@@ -9714,6 +9714,7 @@ Do not edit it by hand; run `pnpm docs:map:gen`.
- H2: How it works (internal)
- H2: CLI quick reference
- H2: Snapshots and refs
- H2: Browser batch CLI
- H2: Wait power-ups
- H2: Debug workflows
- H2: JSON output
+35
View File
@@ -343,6 +343,41 @@ Ref behavior:
Playwright's `aria-ref` selector. Run a fresh snapshot on the same tab when
that happens.
## Browser batch CLI
`openclaw browser batch` runs an array of nested `/act` actions in one `/act`
call (the same `kind="batch"` runtime reached through the agent tool), so CLI
users and scripts can combine actions like `wait`, `click`, `type`, and
`evaluate` into a single replayable plan without per-action round trips. Each
entry in `actions[]` is a `BrowserActRequest` — the closed union the `/act`
route accepts (`click`, `clickCoords`, `type`, `press`, `hover`,
`scrollIntoView`, `drag`, `select`, `fill`, `resize`, `wait`, `evaluate`,
`close`, `batch`) — not arbitrary `openclaw browser` subcommands. `batch` is
not supported on `profile="user"` and other existing-session (chrome-mcp)
profiles; send actions individually there.
- CLI: `openclaw browser batch --actions '<json>'`, `openclaw browser batch
--actions-file plan.json`, or `openclaw browser batch --actions-file -` to
read the JSON array from stdin. `--continue` sets `stopOnError=false`; the
default is to stop on first error. `--target-id` scopes the whole batch to
one tab.
- Ref lifecycle: refs come from a `snapshot` run before the batch (snapshot is
not a nested action). A nested action that changes page state — such as a
`click` that triggers navigation, or an `evaluate` that mutates the DOM — can
invalidate earlier refs for the rest of the batch. Put state-changing actions
first, or split into a follow-up batch after re-snapshotting. Navigation and
re-snapshotting happen outside the batch (`openclaw browser navigate` /
`snapshot`), since `open`, `navigate`, and `snapshot` are not `/act` kinds.
- Target id conflicts: a nested action may omit `targetId` or repeat the
request-level `targetId`; an explicit nested `targetId` that resolves to a
different tab is rejected with `ACT_TARGET_ID_MISMATCH` before any action
runs. Batched actions share the request's tab by design.
- Error summary: the response is `{ "results": [{ "ok": true }, { "ok": false,
"error": "<message>" }, ...] }`, one entry per action in order. When
`stopOnError` is the default, the array ends at the first failure; with
`--continue` it covers every action. Any failed entry makes the CLI exit
nonzero; pass `--json` to preserve the full ordered response for scripts.
## Wait power-ups
You can wait on more than just time/text:
@@ -34,6 +34,15 @@ Use this skill when you need the `browser` tool for anything beyond a single pag
- If the page needs login, permission, captcha, 2FA, camera/microphone approval, or another manual step, stop and tell the user exactly what is needed.
- Do not claim the browser is not logged in just because the current page shows a permission or onboarding dialog. Inspect the visible UI first.
## Browser batch CLI
`openclaw browser batch` runs an array of nested `/act` actions in one `/act` call (the same `kind="batch"` runtime reached through the agent tool), so CLI users and scripts can combine actions like `wait`, `click`, `type`, and `evaluate` into a single replayable plan without per-action round trips. Each entry in `actions[]` is a `BrowserActRequest` — the closed union the `/act` route accepts — not arbitrary `openclaw browser` subcommands. `batch` is not supported on `profile="user"` and other existing-session (chrome-mcp) profiles; send actions individually there.
- CLI: `openclaw browser batch --actions '<json>'`, `--actions-file plan.json`, or `--actions-file -` for stdin. `--continue` sets `stopOnError=false`; default stops on first error.
- Ref lifecycle: refs come from a `snapshot` run before the batch (snapshot is not a nested action). A nested action that changes page state — such as a `click` that triggers navigation, or an `evaluate` that mutates the DOM — can invalidate earlier refs for the rest of the batch; put state-changing actions first, or split into a follow-up batch after re-snapshotting. Navigation and re-snapshotting happen outside the batch, since `open`, `navigate`, and `snapshot` are not `/act` kinds.
- Target id: nested actions share the request's tab; an explicit nested `targetId` that resolves to a different tab is rejected with `ACT_TARGET_ID_MISMATCH`.
- Response: `{ "results": [{ "ok": true } | { "ok": false, "error": "..." }, ...] }` in order; with default `stopOnError` the array ends at the first failure. Any failed entry exits nonzero; use `--json` to preserve the full response in scripts.
## Tab Hygiene
Before creating a tab for a named task, list tabs and reuse an existing matching label or URL when it is still usable.
@@ -0,0 +1,58 @@
// Browser tests cover agent.act.normalize batch contract behavior.
// Locks the documented `openclaw browser batch` examples against the real
// /act normalizer so a doc example the route rejects cannot slip back in.
import { describe, expect, it } from "vitest";
import { normalizeActRequest } from "./agent.act.normalize.js";
// The documented example shipped in `browser-cli-examples.ts`, `docs/cli/browser.md`,
// and `docs/tools/browser-control.md`. Every entry must be a real BrowserActRequest
// kind or the /act normalizer rejects it before dispatch.
const DOCUMENTED_BATCH_ACTIONS = [
{ kind: "wait", timeMs: 500 },
{ kind: "click", ref: "12" },
{ kind: "type", ref: "23", text: "hello" },
];
describe("normalizeActRequest batch contract", () => {
it("accepts the documented batch example through the real /act normalizer", () => {
const normalized = normalizeActRequest({
kind: "batch",
actions: DOCUMENTED_BATCH_ACTIONS,
});
expect(normalized).toMatchObject({
kind: "batch",
actions: [
{ kind: "wait", timeMs: 500 },
{ kind: "click", ref: "12" },
{ kind: "type", ref: "23", text: "hello" },
],
});
});
it("rejects open/navigate/snapshot/screenshot as nested batch actions", () => {
// These are CLI subcommands, not BrowserActRequest kinds; the route normalizer
// must reject them so documented examples cannot describe a non-reproducible workflow.
for (const kind of ["open", "navigate", "snapshot", "screenshot"]) {
expect(() =>
normalizeActRequest({
kind: "batch",
actions: [{ kind, url: "https://example.com" }],
}),
).toThrow("kind is required");
}
});
it("forwards --continue as stopOnError=false and --target-id on the outer batch", () => {
const normalized = normalizeActRequest({
kind: "batch",
actions: DOCUMENTED_BATCH_ACTIONS,
targetId: "tab-1",
stopOnError: false,
});
expect(normalized).toMatchObject({
kind: "batch",
targetId: "tab-1",
stopOnError: false,
});
});
});
@@ -0,0 +1,239 @@
// Browser tests cover register.batch plugin behavior.
import { Command } from "commander";
import { beforeEach, describe, expect, it, vi } from "vitest";
import * as browserCliSharedModule from "../browser-cli-shared.js";
import {
createBrowserProgram,
getBrowserCliRuntime,
getBrowserCliRuntimeCapture,
} from "../browser-cli.test-support.js";
import * as cliCoreApiModule from "../core-api.js";
import * as batchSharedModule from "./shared.js";
const mocks = vi.hoisted(() => ({
callBrowserRequest: vi.fn<
(
opts?: unknown,
req?: unknown,
extra?: { timeoutMs?: number },
) => Promise<Record<string, unknown>>
>(async () => ({ results: [{ ok: true }] })),
readActionsPayload: vi.fn(async () => ""),
}));
vi.spyOn(browserCliSharedModule, "callBrowserRequest").mockImplementation(mocks.callBrowserRequest);
vi.spyOn(batchSharedModule, "readActionsPayload").mockImplementation(mocks.readActionsPayload);
const browserCliRuntime = getBrowserCliRuntime();
vi.spyOn(cliCoreApiModule.defaultRuntime, "log").mockImplementation(browserCliRuntime.log);
vi.spyOn(cliCoreApiModule.defaultRuntime, "writeJson").mockImplementation(
browserCliRuntime.writeJson,
);
vi.spyOn(cliCoreApiModule.defaultRuntime, "error").mockImplementation(browserCliRuntime.error);
vi.spyOn(cliCoreApiModule.defaultRuntime, "exit").mockImplementation(browserCliRuntime.exit);
const { registerBrowserActionInputCommands } = await import("./register.js");
function createActionInputProgram(): Command {
const { program, browser, parentOpts } = createBrowserProgram();
registerBrowserActionInputCommands(browser, parentOpts);
return program;
}
function getLastActionBody(): Record<string, unknown> | undefined {
return (mocks.callBrowserRequest.mock.calls.at(-1)?.[1] as { body?: Record<string, unknown> })
?.body;
}
const SAMPLE_ACTIONS = [
{ kind: "open", url: "https://example.com" },
{ kind: "click", ref: "12" },
];
describe("browser action input batch command", () => {
beforeEach(() => {
mocks.callBrowserRequest.mockClear();
mocks.readActionsPayload.mockClear();
getBrowserCliRuntimeCapture().resetRuntimeCapture();
});
it("sends normalized batch body with inline actions and target id", async () => {
mocks.readActionsPayload.mockResolvedValueOnce(JSON.stringify(SAMPLE_ACTIONS));
const program = createActionInputProgram();
await program.parseAsync(
["browser", "batch", "--actions", JSON.stringify(SAMPLE_ACTIONS), "--target-id", "tab-1"],
{ from: "user" },
);
expect(getLastActionBody()).toMatchObject({
kind: "batch",
actions: SAMPLE_ACTIONS,
targetId: "tab-1",
});
});
it("omits stopOnError by default so the route applies its fail-fast default", async () => {
mocks.readActionsPayload.mockResolvedValueOnce(JSON.stringify(SAMPLE_ACTIONS));
const program = createActionInputProgram();
await program.parseAsync(["browser", "batch", "--actions", JSON.stringify(SAMPLE_ACTIONS)], {
from: "user",
});
const body = getLastActionBody();
expect(body).toMatchObject({ kind: "batch" });
expect(body).not.toHaveProperty("stopOnError");
});
it("sets stopOnError=false when --continue is passed", async () => {
mocks.readActionsPayload.mockResolvedValueOnce(JSON.stringify(SAMPLE_ACTIONS));
const program = createActionInputProgram();
await program.parseAsync(
["browser", "batch", "--actions", JSON.stringify(SAMPLE_ACTIONS), "--continue"],
{ from: "user" },
);
expect(getLastActionBody()).toMatchObject({ kind: "batch", stopOnError: false });
});
it("reports a failed batch action and exits nonzero in text mode", async () => {
mocks.readActionsPayload.mockResolvedValueOnce(JSON.stringify(SAMPLE_ACTIONS));
mocks.callBrowserRequest.mockResolvedValueOnce({
results: [{ ok: true }, { ok: false, error: "ref is stale" }],
});
const program = createActionInputProgram();
await expect(
program.parseAsync(["browser", "batch", "--actions", JSON.stringify(SAMPLE_ACTIONS)], {
from: "user",
}),
).rejects.toThrow("__exit__:1");
expect(getBrowserCliRuntimeCapture().runtimeErrors.join("\n")).toContain(
"batch failed: action 2: ref is stale",
);
});
it("preserves failed batch results in JSON mode before exiting nonzero", async () => {
const result = { results: [{ ok: false, error: "ref is stale" }] };
mocks.readActionsPayload.mockResolvedValueOnce(JSON.stringify(SAMPLE_ACTIONS));
mocks.callBrowserRequest.mockResolvedValueOnce(result);
const program = createActionInputProgram();
await expect(
program.parseAsync(
["browser", "--json", "batch", "--actions", JSON.stringify(SAMPLE_ACTIONS)],
{ from: "user" },
),
).rejects.toThrow("__exit__:1");
expect(getBrowserCliRuntimeCapture().defaultRuntime.writeJson).toHaveBeenCalledWith(result);
});
it("reads actions from a file via --actions-file", async () => {
mocks.readActionsPayload.mockResolvedValueOnce(JSON.stringify(SAMPLE_ACTIONS));
const program = createActionInputProgram();
await program.parseAsync(
["browser", "batch", "--actions-file", "/tmp/openclaw/batch-actions.json"],
{ from: "user" },
);
expect(mocks.readActionsPayload).toHaveBeenCalledWith({
actions: undefined,
actionsFile: "/tmp/openclaw/batch-actions.json",
});
expect(getLastActionBody()).toMatchObject({ kind: "batch", actions: SAMPLE_ACTIONS });
});
it("reads actions from stdin when --actions-file is -", async () => {
mocks.readActionsPayload.mockResolvedValueOnce(JSON.stringify(SAMPLE_ACTIONS));
const program = createActionInputProgram();
await program.parseAsync(["browser", "batch", "--actions-file", "-"], { from: "user" });
expect(mocks.readActionsPayload).toHaveBeenCalledWith({
actions: undefined,
actionsFile: "-",
});
expect(getLastActionBody()).toMatchObject({ kind: "batch", actions: SAMPLE_ACTIONS });
});
it("rejects malformed actions JSON before dispatch", async () => {
mocks.readActionsPayload.mockResolvedValueOnce("NOT JSON {{{");
const program = createActionInputProgram();
await expect(
program.parseAsync(["browser", "batch", "--actions", "NOT JSON {{{"], { from: "user" }),
).rejects.toThrow("__exit__:1");
expect(getBrowserCliRuntimeCapture().runtimeErrors.join("\n")).toContain(
"actions must be valid JSON",
);
expect(mocks.callBrowserRequest).not.toHaveBeenCalled();
});
it("rejects non-array actions before dispatch", async () => {
mocks.readActionsPayload.mockResolvedValueOnce(JSON.stringify({ kind: "click" }));
const program = createActionInputProgram();
await expect(
program.parseAsync(["browser", "batch", "--actions", JSON.stringify({ kind: "click" })], {
from: "user",
}),
).rejects.toThrow("__exit__:1");
expect(getBrowserCliRuntimeCapture().runtimeErrors.join("\n")).toContain(
"actions must be a JSON array",
);
expect(mocks.callBrowserRequest).not.toHaveBeenCalled();
});
it("rejects empty actions before dispatch", async () => {
mocks.readActionsPayload.mockResolvedValueOnce("[]");
const program = createActionInputProgram();
await expect(
program.parseAsync(["browser", "batch", "--actions", "[]"], { from: "user" }),
).rejects.toThrow("__exit__:1");
expect(getBrowserCliRuntimeCapture().runtimeErrors.join("\n")).toContain(
"actions must contain at least one entry",
);
expect(mocks.callBrowserRequest).not.toHaveBeenCalled();
});
it("requires actions from --actions, --actions-file, or stdin", async () => {
const program = createActionInputProgram();
await expect(program.parseAsync(["browser", "batch"], { from: "user" })).rejects.toThrow(
"__exit__:1",
);
expect(getBrowserCliRuntimeCapture().runtimeErrors.join("\n")).toContain(
"Provide --actions, --actions-file, or --actions-file -",
);
expect(mocks.callBrowserRequest).not.toHaveBeenCalled();
});
it("budgets the outer request from the batch execution budget", async () => {
mocks.readActionsPayload.mockResolvedValueOnce(
JSON.stringify([
{ kind: "wait", timeMs: 5000 },
{ kind: "wait", timeMs: 5000 },
]),
);
const program = createActionInputProgram();
await program.parseAsync(
["browser", "batch", "--actions", JSON.stringify([{ kind: "wait", timeMs: 5000 }])],
{ from: "user" },
);
const options = mocks.callBrowserRequest.mock.calls.at(-1)?.[2] as
| { timeoutMs?: number }
| undefined;
expect(options?.timeoutMs).toBeGreaterThan(10_000);
});
});
@@ -0,0 +1,96 @@
/**
* Browser CLI batch command: runs nested act requests in one /act call.
*/
import type { Command } from "commander";
import { normalizeOptionalString } from "openclaw/plugin-sdk/string-coerce-runtime";
import { resolveBrowserActExecutionBudgetMs } from "../../browser/act-policy.js";
import type { BrowserActRequest } from "../../browser/client-actions.types.js";
import { BROWSER_TAB_REFERENCE_HELP, type BrowserParentOpts } from "../browser-cli-shared.js";
import { danger, defaultRuntime } from "../core-api.js";
import {
callBrowserAct,
logBrowserActionResult,
readActionsPayload,
resolveBrowserActionContext,
} from "./shared.js";
/** Registers the Browser CLI batch command. */
export function registerBrowserBatchCommands(
browser: Command,
parentOpts: (cmd: Command) => BrowserParentOpts,
) {
browser
.command("batch")
.description("Run a batch of browser actions in one call (default: stop on first error)")
.option("--actions <json>", "JSON array of act requests")
.option("--actions-file <path>", "Read JSON array from a file (- for stdin)")
.option("--continue", "Continue through all actions instead of stopping on first error")
.option("--target-id <id>", BROWSER_TAB_REFERENCE_HELP)
.action(async (opts, cmd) => {
const { parent, profile } = resolveBrowserActionContext(cmd, parentOpts);
if (!opts.actions && !opts.actionsFile) {
defaultRuntime.error(danger("Provide --actions, --actions-file, or --actions-file -"));
defaultRuntime.exit(1);
return;
}
let actions: unknown[];
let result: { results?: Array<{ ok: boolean; error?: string }> };
try {
const payload = await readActionsPayload({
actions: opts.actions,
actionsFile: opts.actionsFile,
});
if (!payload.trim()) {
throw new Error("actions are required");
}
let parsed: unknown;
try {
parsed = JSON.parse(payload);
} catch (cause) {
throw new Error("actions must be valid JSON", { cause });
}
if (!Array.isArray(parsed)) {
throw new Error("actions must be a JSON array");
}
if (!parsed.length) {
throw new Error("actions must contain at least one entry");
}
actions = parsed;
const targetId = normalizeOptionalString(opts.targetId);
const body: Record<string, unknown> = {
kind: "batch",
actions,
...(targetId ? { targetId } : {}),
...(opts.continue ? { stopOnError: false } : {}),
};
const request = body as unknown as BrowserActRequest;
result = await callBrowserAct<{
results?: Array<{ ok: boolean; error?: string }>;
}>({
parent,
profile,
body,
timeoutMs: resolveBrowserActExecutionBudgetMs(request),
});
} catch (err) {
defaultRuntime.error(danger(String(err)));
defaultRuntime.exit(1);
return;
}
const failures = (result.results ?? []).flatMap((entry, index) =>
entry.ok ? [] : [`action ${index + 1}: ${entry.error ?? "failed"}`],
);
// /act represents recoverable child errors in a successful response.
// Surface them as a command failure so text-mode scripts do not report a false success.
if (failures.length) {
if (parent?.json) {
defaultRuntime.writeJson(result);
} else {
defaultRuntime.error(danger(`batch failed: ${failures.join("; ")}`));
}
defaultRuntime.exit(1);
return;
}
logBrowserActionResult(parent, result, `batch ran ${actions.length} action(s)`);
});
}
@@ -3,12 +3,13 @@
*/
import type { Command } from "commander";
import type { BrowserParentOpts } from "../browser-cli-shared.js";
import { registerBrowserBatchCommands } from "./register.batch.js";
import { registerBrowserElementCommands } from "./register.element.js";
import { registerBrowserFilesAndDownloadsCommands } from "./register.files-downloads.js";
import { registerBrowserFormWaitEvalCommands } from "./register.form-wait-eval.js";
import { registerBrowserNavigationCommands } from "./register.navigation.js";
/** Registers navigation, element, file/download, form, wait, and evaluate commands. */
/** Registers navigation, element, file/download, form, wait, evaluate, and batch commands. */
export function registerBrowserActionInputCommands(
browser: Command,
parentOpts: (cmd: Command) => BrowserParentOpts,
@@ -17,4 +18,5 @@ export function registerBrowserActionInputCommands(
registerBrowserElementCommands(browser, parentOpts);
registerBrowserFilesAndDownloadsCommands(browser, parentOpts);
registerBrowserFormWaitEvalCommands(browser, parentOpts);
registerBrowserBatchCommands(browser, parentOpts);
}
@@ -125,3 +125,35 @@ export async function readFields(opts: {
throw new Error(`fields[${index}].value must be string, number, boolean, or null`);
});
}
/** Cap on batch action JSON read from stdin; keeps a runaway pipe from filling memory. */
const ACTIONS_STDIN_MAX_BYTES = 1_000_000;
/** Reads stdin to a UTF-8 string, throwing once the byte cap is exceeded. */
async function readStdinText(
stream: NodeJS.ReadableStream = process.stdin,
maxBytes = ACTIONS_STDIN_MAX_BYTES,
): Promise<string> {
const chunks: Buffer[] = [];
let total = 0;
for await (const chunk of stream) {
const buf = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
total += buf.length;
if (total > maxBytes) {
throw new Error(`actions stdin exceeds ${maxBytes} bytes.`);
}
chunks.push(buf);
}
return Buffer.concat(chunks).toString("utf8");
}
/** Reads raw batch actions JSON from inline text, a file path, or stdin (`-`). */
export async function readActionsPayload(opts: {
actions?: string;
actionsFile?: string;
}): Promise<string> {
if (opts.actionsFile) {
return opts.actionsFile === "-" ? await readStdinText() : await readFile(opts.actionsFile);
}
return opts.actions ?? "";
}
@@ -40,4 +40,7 @@ export const browserActionExamples = [
"openclaw browser evaluate --fn 'const title = document.title; return title;'",
"openclaw browser console --level error",
"openclaw browser pdf",
"openclaw browser batch --actions-file plan.json",
'openclaw browser batch --actions \'[{"kind":"wait","timeMs":500},{"kind":"click","ref":"12"},{"kind":"type","ref":"23","text":"hello"}]\'',
"openclaw browser batch --actions-file plan.json --continue",
];
@@ -108,6 +108,7 @@ const browserCommandGroupDefinitions: readonly BrowserCommandGroupDefinition[] =
command("fill", "Fill a form with JSON field descriptors"),
command("wait", "Wait for time, selector, URL, load state, or JS conditions"),
command("evaluate", "Evaluate a function against the page or a ref"),
command("batch", "Run a batch of browser actions in one call"),
],
register: async (args) => {
const module = await import("./browser-cli-actions-input.js");