fix(browser): lazy-load browser CLI runtime

Co-authored-by: pandego <7780875+pandego@users.noreply.github.com>
Co-authored-by: Tianworld <3580442280@qq.com>
This commit is contained in:
Peter Steinberger
2026-04-25 11:39:32 +01:00
parent 27aae62d99
commit 776d2ab65d
11 changed files with 514 additions and 50 deletions

View File

@@ -37,6 +37,7 @@ Docs: https://docs.openclaw.ai
- Telegram: remove the startup persisted-offset `getUpdates` preflight so polling restarts do not self-conflict before the runner starts. Fixes #69304. (#69779) Thanks @chinar-amrutkar.
- Telegram: keep the polling stall watchdog active even when grammY reports the runner as not running while its task is still pending, so a rebuilt transport cannot leave `getUpdates` silent until a manual gateway restart. Fixes #69064. Thanks @LDLoeb.
- Browser/Playwright: ignore benign already-handled route races during guarded navigation so browser-page tasks no longer fail when Playwright tears down a route mid-flight. (#68708) Thanks @Steady-ai.
- Browser/CLI: lazy-load browser command groups and plugin runtime services so `openclaw browser --help` can render without loading the full browser automation stack. Fixes #65400. (#65460, #66640) Thanks @pandego and @Tianworld.
- Browser/downloads: seed managed Chrome profiles with OpenClaw download prefs and capture unmanaged click-triggered downloads under the guarded downloads directory, while explicit download waiters still own their target file. (#64558) Thanks @Pearcekieser.
- Browser/Chrome: stop passing redundant `--disable-setuid-sandbox` when `browser.noSandbox` is enabled; `--no-sandbox` remains the effective sandbox opt-out. (#67939) Thanks @sebykrueger.
- Browser/client: stop telling agents to permanently avoid the browser after transient timeout or cancellation failures; keep the no-retry hint for persistent unavailable/rate-limit cases. (#46505) Thanks @jriff.

View File

@@ -1,2 +1,2 @@
3a217ac0157fb46f42d455f1509b70a6d4ca3c41d6da00ac412b642875e4c9ef plugin-sdk-api-baseline.json
d962b39c50017ef8e8545d9a3902a4f37f19d338256d8c96c9f860c7a120b687 plugin-sdk-api-baseline.jsonl
d5bad55d588ecafab1298a2a79578ce13becced8bc33d2b8543161ab528feca4 plugin-sdk-api-baseline.json
373ded33d5ecc61229de5179827182f0c6f805a804e1f0666cf2da68301153be plugin-sdk-api-baseline.jsonl

View File

@@ -152,7 +152,7 @@ For the plugin authoring guide, see [Plugin SDK overview](/plugins/sdk-overview)
| `plugin-sdk/hook-runtime` | Shared webhook/internal hook pipeline helpers |
| `plugin-sdk/lazy-runtime` | Lazy runtime import/binding helpers such as `createLazyRuntimeModule`, `createLazyRuntimeMethod`, and `createLazyRuntimeSurface` |
| `plugin-sdk/process-runtime` | Process exec helpers |
| `plugin-sdk/cli-runtime` | CLI formatting, wait, and version helpers |
| `plugin-sdk/cli-runtime` | CLI formatting, wait, version, argument-invocation, and lazy command-group helpers |
| `plugin-sdk/gateway-runtime` | Gateway client and channel-status patch helpers |
| `plugin-sdk/config-runtime` | Config load/write helpers and plugin-config lookup helpers |
| `plugin-sdk/telegram-command-config` | Telegram command-name/description normalization and duplicate/conflict checks, even when the bundled Telegram contract surface is unavailable |

View File

@@ -7,7 +7,7 @@ export default definePluginEntry({
register(api) {
api.registerCli(
async ({ program }) => {
const { registerBrowserCli } = await import("./runtime-api.js");
const { registerBrowserCli } = await import("./src/cli/browser-cli.js");
registerBrowserCli(program);
},
{ commands: ["browser"] },

View File

@@ -19,10 +19,12 @@ const runtimeApiMocks = vi.hoisted(() => ({
name: "browser",
description: "browser",
parameters: { type: "object", properties: {} },
execute: vi.fn(),
execute: vi.fn(async () => ({ type: "json", value: { ok: true } })),
})),
collectBrowserSecurityAuditFindings: vi.fn(() => []),
handleBrowserGatewayRequest: vi.fn(),
registerBrowserCli: vi.fn(),
runBrowserProxyCommand: vi.fn(async () => "ok"),
}));
vi.mock("./register.runtime.js", async () => {
@@ -30,13 +32,18 @@ vi.mock("./register.runtime.js", async () => {
await vi.importActual<typeof import("./register.runtime.js")>("./register.runtime.js");
return {
...actual,
collectBrowserSecurityAuditFindings: runtimeApiMocks.collectBrowserSecurityAuditFindings,
createBrowserPluginService: runtimeApiMocks.createBrowserPluginService,
createBrowserTool: runtimeApiMocks.createBrowserTool,
handleBrowserGatewayRequest: runtimeApiMocks.handleBrowserGatewayRequest,
registerBrowserCli: runtimeApiMocks.registerBrowserCli,
runBrowserProxyCommand: runtimeApiMocks.runBrowserProxyCommand,
};
});
vi.mock("./src/cli/browser-cli.js", () => ({
registerBrowserCli: runtimeApiMocks.registerBrowserCli,
}));
function createApi() {
const registerCli = vi.fn();
const registerGatewayMethod = vi.fn();
@@ -94,23 +101,29 @@ describe("browser plugin", () => {
expect(fs.readFileSync(skillPath, "utf8")).toContain("name: browser-automation");
});
it("forwards per-session browser options into the tool factory", async () => {
it("keeps browser tool registration synchronous while loading runtime on execute", async () => {
const { api, registerTool } = createApi();
registerBrowserPlugin(api);
const tool = registerTool.mock.calls[0]?.[0];
if (typeof tool !== "function") {
const factory = registerTool.mock.calls[0]?.[0];
if (typeof factory !== "function") {
throw new Error("expected browser plugin to register a tool factory");
}
tool({
const tool = factory({
sessionKey: "agent:main:webchat:direct:123",
browser: {
sandboxBridgeUrl: "http://127.0.0.1:9999",
allowHostControl: true,
},
});
if (!tool || Array.isArray(tool)) {
throw new Error("expected browser plugin to return a single tool");
}
expect(tool.name).toBe("browser");
expect(runtimeApiMocks.createBrowserTool).not.toHaveBeenCalled();
await tool.execute("call-1", { action: "status" });
expect(runtimeApiMocks.createBrowserTool).toHaveBeenCalledWith({
sandboxBridgeUrl: "http://127.0.0.1:9999",
allowHostControl: true,
@@ -118,15 +131,61 @@ describe("browser plugin", () => {
});
});
it("registers browser.request as an admin gateway method", () => {
it("registers CLI descriptors and lazy-loads the lightweight browser CLI", async () => {
const { api, registerCli } = createApi();
registerBrowserPlugin(api);
expect(registerCli).toHaveBeenCalledWith(
expect.any(Function),
expect.objectContaining({
commands: ["browser"],
descriptors: [
{
name: "browser",
description: "Manage OpenClaw's dedicated browser (Chrome/Chromium)",
hasSubcommands: true,
},
],
}),
);
const registrar = registerCli.mock.calls[0]?.[0];
await registrar({ program: {} as never });
expect(runtimeApiMocks.registerBrowserCli).toHaveBeenCalledWith({});
});
it("registers browser.request as an admin gateway method and lazy-loads handler", async () => {
const { api, registerGatewayMethod } = createApi();
registerBrowserPlugin(api);
expect(registerGatewayMethod).toHaveBeenCalledWith(
"browser.request",
runtimeApiMocks.handleBrowserGatewayRequest,
{ scope: "operator.admin" },
);
expect(registerGatewayMethod).toHaveBeenCalledWith("browser.request", expect.any(Function), {
scope: "operator.admin",
});
const handler = registerGatewayMethod.mock.calls[0]?.[1];
await handler({ method: "browser.request" });
expect(runtimeApiMocks.handleBrowserGatewayRequest).toHaveBeenCalledWith({
method: "browser.request",
});
});
it("lazy-loads node host and audit runtime handlers", async () => {
await expect(browserPluginNodeHostCommands[0]?.handle("{}")).resolves.toBe("ok");
expect(runtimeApiMocks.runBrowserProxyCommand).toHaveBeenCalledWith("{}");
await expect(browserSecurityAuditCollectors[0]?.({} as never)).resolves.toEqual([]);
expect(runtimeApiMocks.collectBrowserSecurityAuditFindings).toHaveBeenCalled();
});
it("lazy-loads the browser service on start", async () => {
const { api, registerService } = createApi();
registerBrowserPlugin(api);
const service = registerService.mock.calls[0]?.[0];
expect(service).toMatchObject({ id: "browser-control" });
expect(runtimeApiMocks.createBrowserPluginService).not.toHaveBeenCalled();
await service.start({ config: {}, stateDir: "/tmp/openclaw", logger: { warn: vi.fn() } });
expect(runtimeApiMocks.createBrowserPluginService).toHaveBeenCalledOnce();
});
it("declares setup auto-enable reasons for browser config surfaces", () => {

View File

@@ -1,17 +1,52 @@
import type {
AnyAgentTool,
OpenClawPluginApi,
OpenClawPluginNodeHostCommand,
OpenClawPluginSecurityAuditCollector,
OpenClawPluginService,
OpenClawPluginToolContext,
OpenClawPluginToolFactory,
} from "openclaw/plugin-sdk/plugin-entry";
import {
collectBrowserSecurityAuditFindings,
createBrowserPluginService,
createBrowserTool,
handleBrowserGatewayRequest,
registerBrowserCli,
runBrowserProxyCommand,
} from "./register.runtime.js";
import { BrowserToolSchema } from "./src/browser-tool.schema.js";
const BROWSER_CLI_DESCRIPTOR = {
name: "browser",
description: "Manage OpenClaw's dedicated browser (Chrome/Chromium)",
hasSubcommands: true,
};
function createLazyBrowserTool(opts?: {
sandboxBridgeUrl?: string;
allowHostControl?: boolean;
agentSessionKey?: string;
}): AnyAgentTool {
const targetDefault = opts?.sandboxBridgeUrl ? "sandbox" : "host";
const hostHint =
opts?.allowHostControl === false ? "Host target blocked by policy." : "Host target allowed.";
return {
label: "Browser",
name: "browser",
description: [
"Control the browser via OpenClaw's browser control server (status/start/stop/profiles/tabs/open/snapshot/screenshot/actions).",
"Browser choice: omit profile by default for the isolated OpenClaw-managed browser (`openclaw`).",
'For the logged-in user browser, use profile="user". A supported Chromium-based browser (v144+) must be running on the selected host or browser node. Use only when existing logins/cookies matter and the user is present.',
'For profile="user" or other existing-session profiles, omit timeoutMs on act:type, evaluate, hover, scrollIntoView, drag, select, and fill; that driver rejects per-call timeout overrides for those actions.',
'When a node-hosted browser proxy is available, the tool may auto-route to it. Pin a node with node=<id|name> or target="node".',
"When using refs from snapshot (e.g. e12), keep the same tab: prefer passing targetId from the snapshot response into subsequent actions (act/click/type/etc). For tab operations, targetId also accepts tabId handles (t1) and labels from action=tabs.",
"For multi-step browser work, login checks, stale refs, duplicate tabs, or Google Meet flows, use the bundled browser-automation skill when it is available.",
'For stable, self-resolving refs across calls, use snapshot with refs="aria" (Playwright aria-ref ids). Default refs="role" are role+name-based.',
"Use snapshot+act for UI automation. Avoid act:wait by default; use only in exceptional cases when no reliable UI state exists.",
`target selects browser location (sandbox|host|node). Default: ${targetDefault}.`,
hostHint,
].join(" "),
parameters: BrowserToolSchema,
execute: async (toolCallId, args, signal, onUpdate) => {
const { createBrowserTool } = await import("./register.runtime.js");
const tool = createBrowserTool(opts);
return await tool.execute(toolCallId, args, signal, onUpdate);
},
};
}
export const browserPluginReload = { restartPrefixes: ["browser"] };
@@ -19,22 +54,67 @@ export const browserPluginNodeHostCommands: OpenClawPluginNodeHostCommand[] = [
{
command: "browser.proxy",
cap: "browser",
handle: runBrowserProxyCommand,
handle: async (paramsJSON) => {
const { runBrowserProxyCommand } = await import("./register.runtime.js");
return await runBrowserProxyCommand(paramsJSON);
},
},
];
export const browserSecurityAuditCollectors = [collectBrowserSecurityAuditFindings];
export const browserSecurityAuditCollectors: OpenClawPluginSecurityAuditCollector[] = [
async (ctx) => {
const { collectBrowserSecurityAuditFindings } = await import("./register.runtime.js");
return collectBrowserSecurityAuditFindings(ctx);
},
];
function createLazyBrowserPluginService(): OpenClawPluginService {
let service: OpenClawPluginService | null = null;
const loadService = async () => {
if (!service) {
const { createBrowserPluginService } = await import("./register.runtime.js");
service = createBrowserPluginService();
}
return service;
};
return {
id: "browser-control",
start: async (ctx) => {
const loaded = await loadService();
await loaded.start(ctx);
},
stop: async (ctx) => {
if (!service?.stop) {
return;
}
await service.stop(ctx);
},
};
}
export function registerBrowserPlugin(api: OpenClawPluginApi) {
api.registerTool(((ctx: OpenClawPluginToolContext) =>
createBrowserTool({
createLazyBrowserTool({
sandboxBridgeUrl: ctx.browser?.sandboxBridgeUrl,
allowHostControl: ctx.browser?.allowHostControl,
agentSessionKey: ctx.sessionKey,
})) as OpenClawPluginToolFactory);
api.registerCli(({ program }) => registerBrowserCli(program), { commands: ["browser"] });
api.registerGatewayMethod("browser.request", handleBrowserGatewayRequest, {
scope: "operator.admin",
});
api.registerService(createBrowserPluginService());
api.registerCli(
async ({ program }) => {
const { registerBrowserCli } = await import("./src/cli/browser-cli.js");
registerBrowserCli(program);
},
{ commands: ["browser"], descriptors: [BROWSER_CLI_DESCRIPTOR] },
);
api.registerGatewayMethod(
"browser.request",
async (opts) => {
const { handleBrowserGatewayRequest } = await import("./register.runtime.js");
return await handleBrowserGatewayRequest(opts);
},
{
scope: "operator.admin",
},
);
api.registerService(createLazyBrowserPluginService());
}

View File

@@ -1,5 +1,4 @@
export { createBrowserTool } from "./src/browser-tool.js";
export { registerBrowserCli } from "./src/cli/browser-cli.js";
export { handleBrowserGatewayRequest } from "./src/gateway/browser-request.js";
export { runBrowserProxyCommand } from "./src/node-host/invoke-browser.js";
export { createBrowserPluginService } from "./src/plugin-service.js";

View File

@@ -0,0 +1,99 @@
import { Command } from "commander";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
const manageMocks = vi.hoisted(() => {
const statusAction = vi.fn();
const registerBrowserManageCommands = vi.fn((browser: Command) => {
browser.command("status").description("Show browser status").action(statusAction);
});
return { registerBrowserManageCommands, statusAction };
});
const inspectMocks = vi.hoisted(() => ({
registerBrowserInspectCommands: vi.fn(),
}));
const actionInputMocks = vi.hoisted(() => ({
registerBrowserActionInputCommands: vi.fn(),
}));
const actionObserveMocks = vi.hoisted(() => ({
registerBrowserActionObserveCommands: vi.fn(),
}));
const debugMocks = vi.hoisted(() => ({
registerBrowserDebugCommands: vi.fn(),
}));
const stateMocks = vi.hoisted(() => ({
registerBrowserStateCommands: vi.fn(),
}));
vi.mock("./browser-cli-manage.js", () => manageMocks);
vi.mock("./browser-cli-inspect.js", () => inspectMocks);
vi.mock("./browser-cli-actions-input.js", () => actionInputMocks);
vi.mock("./browser-cli-actions-observe.js", () => actionObserveMocks);
vi.mock("./browser-cli-debug.js", () => debugMocks);
vi.mock("./browser-cli-state.js", () => stateMocks);
const { registerBrowserCli } = await import("./browser-cli.js");
describe("registerBrowserCli lazy browser subcommands", () => {
beforeEach(() => {
vi.unstubAllEnvs();
manageMocks.registerBrowserManageCommands.mockClear();
manageMocks.statusAction.mockClear();
inspectMocks.registerBrowserInspectCommands.mockClear();
actionInputMocks.registerBrowserActionInputCommands.mockClear();
actionObserveMocks.registerBrowserActionObserveCommands.mockClear();
debugMocks.registerBrowserDebugCommands.mockClear();
stateMocks.registerBrowserStateCommands.mockClear();
});
afterEach(() => {
vi.unstubAllEnvs();
});
it("registers browser placeholders without loading handlers for help", () => {
const program = new Command();
program.name("openclaw");
registerBrowserCli(program, ["node", "openclaw", "browser", "--help"]);
const browser = program.commands.find((command) => command.name() === "browser");
expect(browser?.commands.map((command) => command.name())).toContain("status");
expect(browser?.commands.map((command) => command.name())).toContain("snapshot");
expect(browser?.commands.map((command) => command.name())).toContain("doctor");
expect(manageMocks.registerBrowserManageCommands).not.toHaveBeenCalled();
expect(inspectMocks.registerBrowserInspectCommands).not.toHaveBeenCalled();
expect(actionInputMocks.registerBrowserActionInputCommands).not.toHaveBeenCalled();
});
it("registers only the requested browser group before dispatch", async () => {
const program = new Command();
program.name("openclaw");
registerBrowserCli(program, ["node", "openclaw", "browser", "status"]);
const browser = program.commands.find((command) => command.name() === "browser");
expect(browser?.commands.map((command) => command.name())).toEqual(["status"]);
await program.parseAsync(["browser", "status"], { from: "user" });
expect(manageMocks.registerBrowserManageCommands).toHaveBeenCalledTimes(1);
expect(inspectMocks.registerBrowserInspectCommands).not.toHaveBeenCalled();
expect(manageMocks.statusAction).toHaveBeenCalledTimes(1);
});
it("can eagerly register all browser groups for compatibility", async () => {
vi.stubEnv("OPENCLAW_DISABLE_LAZY_SUBCOMMANDS", "1");
const program = new Command();
program.name("openclaw");
registerBrowserCli(program, ["node", "openclaw", "browser", "--help"]);
await vi.waitFor(() =>
expect(manageMocks.registerBrowserManageCommands).toHaveBeenCalledTimes(1),
);
expect(inspectMocks.registerBrowserInspectCommands).toHaveBeenCalledTimes(1);
expect(actionInputMocks.registerBrowserActionInputCommands).toHaveBeenCalledTimes(1);
expect(actionObserveMocks.registerBrowserActionObserveCommands).toHaveBeenCalledTimes(1);
expect(debugMocks.registerBrowserDebugCommands).toHaveBeenCalledTimes(1);
expect(stateMocks.registerBrowserStateCommands).toHaveBeenCalledTimes(1);
});
});

View File

@@ -1,12 +1,15 @@
import type { Command } from "commander";
import { registerBrowserActionInputCommands } from "./browser-cli-actions-input.js";
import { registerBrowserActionObserveCommands } from "./browser-cli-actions-observe.js";
import { registerBrowserDebugCommands } from "./browser-cli-debug.js";
import {
buildCommandGroupEntries,
registerCommandGroups,
resolveCliArgvInvocation,
shouldEagerRegisterSubcommands,
type CommandGroupEntry,
type CommandGroupDescriptorSpec,
type NamedCommandDescriptor,
} from "openclaw/plugin-sdk/cli-runtime";
import { browserActionExamples, browserCoreExamples } from "./browser-cli-examples.js";
import { registerBrowserInspectCommands } from "./browser-cli-inspect.js";
import { registerBrowserManageCommands } from "./browser-cli-manage.js";
import type { BrowserParentOpts } from "./browser-cli-shared.js";
import { registerBrowserStateCommands } from "./browser-cli-state.js";
import {
addGatewayClientOptions,
danger,
@@ -17,7 +20,211 @@ import {
theme,
} from "./core-api.js";
export function registerBrowserCli(program: Command) {
type BrowserCommandDescriptor = NamedCommandDescriptor;
type BrowserCommandRegistrar = (args: {
browser: Command;
parentOpts: (cmd: Command) => BrowserParentOpts;
}) => Promise<void> | void;
const browserCommandDescriptors: readonly BrowserCommandDescriptor[] = [
{ name: "status", description: "Show browser status", hasSubcommands: false },
{
name: "start",
description: "Start the browser (no-op if already running)",
hasSubcommands: false,
},
{ name: "stop", description: "Stop the browser (best-effort)", hasSubcommands: false },
{
name: "reset-profile",
description: "Reset browser profile (moves it to Trash)",
hasSubcommands: false,
},
{ name: "tabs", description: "List open tabs", hasSubcommands: false },
{ name: "tab", description: "Tab shortcuts (index-based)", hasSubcommands: true },
{ name: "open", description: "Open a URL in a new tab", hasSubcommands: false },
{
name: "focus",
description: "Focus a tab by target id (or unique prefix)",
hasSubcommands: false,
},
{ name: "close", description: "Close a tab (target id optional)", hasSubcommands: false },
{ name: "profiles", description: "List all browser profiles", hasSubcommands: false },
{ name: "create-profile", description: "Create a new browser profile", hasSubcommands: false },
{ name: "delete-profile", description: "Delete a browser profile", hasSubcommands: false },
{ name: "screenshot", description: "Capture a screenshot (MEDIA:<path>)", hasSubcommands: false },
{
name: "snapshot",
description: "Capture a snapshot (default: ai; aria is the accessibility tree)",
hasSubcommands: false,
},
{ name: "navigate", description: "Navigate the current tab to a URL", hasSubcommands: false },
{ name: "resize", description: "Resize the viewport", hasSubcommands: false },
{ name: "click", description: "Click an element by ref from snapshot", hasSubcommands: false },
{ name: "click-coords", description: "Click viewport coordinates", hasSubcommands: false },
{ name: "type", description: "Type into an element by ref from snapshot", hasSubcommands: false },
{ name: "press", description: "Press a key", hasSubcommands: false },
{ name: "hover", description: "Hover an element by ai ref", hasSubcommands: false },
{
name: "scrollintoview",
description: "Scroll an element into view by ref from snapshot",
hasSubcommands: false,
},
{ name: "drag", description: "Drag from one ref to another", hasSubcommands: false },
{ name: "select", description: "Select option(s) in a select element", hasSubcommands: false },
{
name: "upload",
description: "Arm file upload for the next file chooser",
hasSubcommands: false,
},
{
name: "waitfordownload",
description: "Wait for the next download (and save it)",
hasSubcommands: false,
},
{
name: "download",
description: "Click a ref and save the resulting download",
hasSubcommands: false,
},
{
name: "dialog",
description: "Arm the next modal dialog (alert/confirm/prompt)",
hasSubcommands: false,
},
{ name: "fill", description: "Fill a form with JSON field descriptors", hasSubcommands: false },
{
name: "wait",
description: "Wait for time, selector, URL, load state, or JS conditions",
hasSubcommands: false,
},
{
name: "evaluate",
description: "Evaluate a function against the page or a ref",
hasSubcommands: false,
},
{ name: "console", description: "Get recent console messages", hasSubcommands: false },
{ name: "pdf", description: "Save page as PDF", hasSubcommands: false },
{
name: "responsebody",
description: "Wait for a network response and return its body",
hasSubcommands: false,
},
{ name: "highlight", description: "Highlight an element by ref", hasSubcommands: false },
{ name: "errors", description: "Get recent page errors", hasSubcommands: false },
{
name: "requests",
description: "Get recent network requests (best-effort)",
hasSubcommands: false,
},
{ name: "doctor", description: "Diagnose browser readiness", hasSubcommands: false },
{ name: "trace", description: "Record a Playwright trace", hasSubcommands: true },
{ name: "cookies", description: "Read/write cookies", hasSubcommands: true },
{ name: "storage", description: "Read/write localStorage/sessionStorage", hasSubcommands: true },
{ name: "set", description: "Browser environment settings", hasSubcommands: true },
];
const browserCommandGroupSpecs: readonly CommandGroupDescriptorSpec<BrowserCommandRegistrar>[] = [
{
commandNames: [
"status",
"start",
"stop",
"reset-profile",
"tabs",
"tab",
"open",
"focus",
"close",
"profiles",
"create-profile",
"delete-profile",
],
register: async (args) => {
const module = await import("./browser-cli-manage.js");
module.registerBrowserManageCommands(args.browser, args.parentOpts);
},
},
{
commandNames: ["screenshot", "snapshot"],
register: async (args) => {
const module = await import("./browser-cli-inspect.js");
module.registerBrowserInspectCommands(args.browser, args.parentOpts);
},
},
{
commandNames: [
"navigate",
"resize",
"click",
"click-coords",
"type",
"press",
"hover",
"scrollintoview",
"drag",
"select",
"upload",
"waitfordownload",
"download",
"dialog",
"fill",
"wait",
"evaluate",
],
register: async (args) => {
const module = await import("./browser-cli-actions-input.js");
module.registerBrowserActionInputCommands(args.browser, args.parentOpts);
},
},
{
commandNames: ["console", "pdf", "responsebody"],
register: async (args) => {
const module = await import("./browser-cli-actions-observe.js");
module.registerBrowserActionObserveCommands(args.browser, args.parentOpts);
},
},
{
commandNames: ["highlight", "errors", "requests", "doctor", "trace"],
register: async (args) => {
const module = await import("./browser-cli-debug.js");
module.registerBrowserDebugCommands(args.browser, args.parentOpts);
},
},
{
commandNames: ["cookies", "storage", "set"],
register: async (args) => {
const module = await import("./browser-cli-state.js");
module.registerBrowserStateCommands(args.browser, args.parentOpts);
},
},
];
function buildBrowserCommandGroups(params: {
browser: Command;
parentOpts: (cmd: Command) => BrowserParentOpts;
}): CommandGroupEntry[] {
return buildCommandGroupEntries(
browserCommandDescriptors,
browserCommandGroupSpecs,
(register) => async () => await register(params),
);
}
function registerLazyBrowserCommands(
browser: Command,
parentOpts: (cmd: Command) => BrowserParentOpts,
argv: string[],
) {
const { primary, commandPath } = resolveCliArgvInvocation(argv);
const subcommand = primary === "browser" ? (commandPath[1] ?? null) : null;
registerCommandGroups(browser, buildBrowserCommandGroups({ browser, parentOpts }), {
eager: shouldEagerRegisterSubcommands(),
primary: subcommand,
registerPrimaryOnly: subcommand !== null,
});
}
export function registerBrowserCli(program: Command, argv: string[] = process.argv) {
const browser = program
.command("browser")
.description("Manage OpenClaw's dedicated browser (Chrome/Chromium)")
@@ -46,10 +253,5 @@ export function registerBrowserCli(program: Command) {
const parentOpts = (cmd: Command) => cmd.parent?.opts?.() as BrowserParentOpts;
registerBrowserManageCommands(browser, parentOpts);
registerBrowserInspectCommands(browser, parentOpts);
registerBrowserActionInputCommands(browser, parentOpts);
registerBrowserActionObserveCommands(browser, parentOpts);
registerBrowserDebugCommands(browser, parentOpts);
registerBrowserStateCommands(browser, parentOpts);
registerLazyBrowserCommands(browser, parentOpts, argv);
}

View File

@@ -1130,14 +1130,14 @@
"types": "./dist/plugin-sdk/provider-usage.d.ts",
"default": "./dist/plugin-sdk/provider-usage.js"
},
"./plugin-sdk/web-content-extractor": {
"types": "./dist/plugin-sdk/web-content-extractor.d.ts",
"default": "./dist/plugin-sdk/web-content-extractor.js"
},
"./plugin-sdk/document-extractor": {
"types": "./dist/plugin-sdk/document-extractor.d.ts",
"default": "./dist/plugin-sdk/document-extractor.js"
},
"./plugin-sdk/web-content-extractor": {
"types": "./dist/plugin-sdk/web-content-extractor.d.ts",
"default": "./dist/plugin-sdk/web-content-extractor.js"
},
"./plugin-sdk/provider-web-fetch-contract": {
"types": "./dist/plugin-sdk/provider-web-fetch-contract.d.ts",
"default": "./dist/plugin-sdk/provider-web-fetch-contract.js"

View File

@@ -1,7 +1,31 @@
// Public CLI/output helpers for plugins that share terminal-facing command behavior.
export * from "../cli/command-format.js";
export {
buildCommandGroupEntries,
defineImportedCommandGroupSpec,
defineImportedCommandGroupSpecs,
defineImportedProgramCommandGroupSpec,
defineImportedProgramCommandGroupSpecs,
resolveCommandGroupEntries,
type CommandGroupDescriptorSpec,
type ImportedCommandGroupDefinition,
type ImportedProgramCommandGroupDefinition,
type NamedCommandDescriptor,
} from "../cli/program/command-group-descriptors.js";
export {
findCommandGroupEntry,
getCommandGroupNames,
registerCommandGroupByName,
registerCommandGroups,
registerLazyCommandGroup,
removeCommandGroupNames,
type CommandGroupEntry,
type CommandGroupPlaceholder,
} from "../cli/program/register-command-groups.js";
export * from "../cli/parse-duration.js";
export { resolveCliArgvInvocation, type CliArgvInvocation } from "../cli/argv-invocation.js";
export { shouldEagerRegisterSubcommands } from "../cli/command-registration-policy.js";
export * from "../cli/wait.js";
export { stylePromptTitle } from "../terminal/prompt-style.js";
export * from "../version.js";