diff --git a/docs/browser.md b/docs/browser.md index 973c16c37..ab3ae214d 100644 --- a/docs/browser.md +++ b/docs/browser.md @@ -221,7 +221,7 @@ The agent should not assume tabs are ephemeral. It should: ## CLI quick reference (one example each) -All commands accept `--profile ` to target a specific profile (default: `clawd`). +All commands accept `--browser-profile ` to target a specific profile (default: `clawd`). Profile management: - `clawdbot browser profiles` diff --git a/src/agents/tools/browser-tool.ts b/src/agents/tools/browser-tool.ts index 882c65a33..cfae25603 100644 --- a/src/agents/tools/browser-tool.ts +++ b/src/agents/tools/browser-tool.ts @@ -118,6 +118,7 @@ const BrowserToolSchema = Type.Object({ Type.Literal("dialog"), Type.Literal("act"), ]), + profile: Type.Optional(Type.String()), controlUrl: Type.Optional(Type.String()), targetUrl: Type.Optional(Type.String()), targetId: Type.Optional(Type.String()), @@ -161,38 +162,39 @@ export function createBrowserTool(opts?: { const params = args as Record; const action = readStringParam(params, "action", { required: true }); const controlUrl = readStringParam(params, "controlUrl"); + const profile = readStringParam(params, "profile"); const baseUrl = resolveBrowserBaseUrl( controlUrl ?? opts?.defaultControlUrl, ); switch (action) { case "status": - return jsonResult(await browserStatus(baseUrl)); + return jsonResult(await browserStatus(baseUrl, { profile })); case "start": - await browserStart(baseUrl); - return jsonResult(await browserStatus(baseUrl)); + await browserStart(baseUrl, { profile }); + return jsonResult(await browserStatus(baseUrl, { profile })); case "stop": - await browserStop(baseUrl); - return jsonResult(await browserStatus(baseUrl)); + await browserStop(baseUrl, { profile }); + return jsonResult(await browserStatus(baseUrl, { profile })); case "tabs": - return jsonResult({ tabs: await browserTabs(baseUrl) }); + return jsonResult({ tabs: await browserTabs(baseUrl, { profile }) }); case "open": { const targetUrl = readStringParam(params, "targetUrl", { required: true, }); - return jsonResult(await browserOpenTab(baseUrl, targetUrl)); + return jsonResult(await browserOpenTab(baseUrl, targetUrl, { profile })); } case "focus": { const targetId = readStringParam(params, "targetId", { required: true, }); - await browserFocusTab(baseUrl, targetId); + await browserFocusTab(baseUrl, targetId, { profile }); return jsonResult({ ok: true }); } case "close": { const targetId = readStringParam(params, "targetId"); - if (targetId) await browserCloseTab(baseUrl, targetId); - else await browserAct(baseUrl, { kind: "close" }); + if (targetId) await browserCloseTab(baseUrl, targetId, { profile }); + else await browserAct(baseUrl, { kind: "close" }, { profile }); return jsonResult({ ok: true }); } case "snapshot": { @@ -212,6 +214,7 @@ export function createBrowserTool(opts?: { format, targetId, limit, + profile, }); if (snapshot.format === "ai") { return { @@ -233,6 +236,7 @@ export function createBrowserTool(opts?: { ref, element, type, + profile, }); return await imageResultFromFile({ label: "browser:screenshot", @@ -246,7 +250,7 @@ export function createBrowserTool(opts?: { }); const targetId = readStringParam(params, "targetId"); return jsonResult( - await browserNavigate(baseUrl, { url: targetUrl, targetId }), + await browserNavigate(baseUrl, { url: targetUrl, targetId, profile }), ); } case "console": { @@ -257,7 +261,7 @@ export function createBrowserTool(opts?: { ? params.targetId.trim() : undefined; return jsonResult( - await browserConsoleMessages(baseUrl, { level, targetId }), + await browserConsoleMessages(baseUrl, { level, targetId, profile }), ); } case "pdf": { @@ -265,7 +269,7 @@ export function createBrowserTool(opts?: { typeof params.targetId === "string" ? params.targetId.trim() : undefined; - const result = await browserPdfSave(baseUrl, { targetId }); + const result = await browserPdfSave(baseUrl, { targetId, profile }); return { content: [{ type: "text", text: `FILE:${result.path}` }], details: result, @@ -296,6 +300,7 @@ export function createBrowserTool(opts?: { element, targetId, timeoutMs, + profile, }), ); } @@ -320,6 +325,7 @@ export function createBrowserTool(opts?: { promptText, targetId, timeoutMs, + profile, }), ); } @@ -331,6 +337,7 @@ export function createBrowserTool(opts?: { const result = await browserAct( baseUrl, request as Parameters[1], + { profile }, ); return jsonResult(result); } diff --git a/src/cli/browser-cli-actions-input.ts b/src/cli/browser-cli-actions-input.ts index a2c412105..ad19b6648 100644 --- a/src/cli/browser-cli-actions-input.ts +++ b/src/cli/browser-cli-actions-input.ts @@ -64,7 +64,7 @@ export function registerBrowserActionInputCommands( .action(async (url: string, opts, cmd) => { const parent = parentOpts(cmd); const baseUrl = resolveBrowserControlUrl(parent?.url); - const profile = parent?.profile; + const profile = parent?.browserProfile; try { const result = await browserNavigate(baseUrl, { url, @@ -91,7 +91,7 @@ export function registerBrowserActionInputCommands( .action(async (width: number, height: number, opts, cmd) => { const parent = parentOpts(cmd); const baseUrl = resolveBrowserControlUrl(parent?.url); - const profile = parent?.profile; + const profile = parent?.browserProfile; if (!Number.isFinite(width) || !Number.isFinite(height)) { defaultRuntime.error(danger("width and height must be numbers")); defaultRuntime.exit(1); @@ -130,7 +130,7 @@ export function registerBrowserActionInputCommands( .action(async (ref: string | undefined, opts, cmd) => { const parent = parentOpts(cmd); const baseUrl = resolveBrowserControlUrl(parent?.url); - const profile = parent?.profile; + const profile = parent?.browserProfile; const refValue = typeof ref === "string" ? ref.trim() : ""; if (!refValue) { defaultRuntime.error(danger("ref is required")); @@ -179,7 +179,7 @@ export function registerBrowserActionInputCommands( .action(async (ref: string | undefined, text: string, opts, cmd) => { const parent = parentOpts(cmd); const baseUrl = resolveBrowserControlUrl(parent?.url); - const profile = parent?.profile; + const profile = parent?.browserProfile; const refValue = typeof ref === "string" ? ref.trim() : ""; if (!refValue) { defaultRuntime.error(danger("ref is required")); @@ -218,7 +218,7 @@ export function registerBrowserActionInputCommands( .action(async (key: string, opts, cmd) => { const parent = parentOpts(cmd); const baseUrl = resolveBrowserControlUrl(parent?.url); - const profile = parent?.profile; + const profile = parent?.browserProfile; try { const result = await browserAct( baseUrl, @@ -248,7 +248,7 @@ export function registerBrowserActionInputCommands( .action(async (ref: string, opts, cmd) => { const parent = parentOpts(cmd); const baseUrl = resolveBrowserControlUrl(parent?.url); - const profile = parent?.profile; + const profile = parent?.browserProfile; try { const result = await browserAct( baseUrl, @@ -279,7 +279,7 @@ export function registerBrowserActionInputCommands( .action(async (startRef: string, endRef: string, opts, cmd) => { const parent = parentOpts(cmd); const baseUrl = resolveBrowserControlUrl(parent?.url); - const profile = parent?.profile; + const profile = parent?.browserProfile; try { const result = await browserAct( baseUrl, @@ -311,7 +311,7 @@ export function registerBrowserActionInputCommands( .action(async (ref: string, values: string[], opts, cmd) => { const parent = parentOpts(cmd); const baseUrl = resolveBrowserControlUrl(parent?.url); - const profile = parent?.profile; + const profile = parent?.browserProfile; try { const result = await browserAct( baseUrl, @@ -350,7 +350,7 @@ export function registerBrowserActionInputCommands( .action(async (paths: string[], opts, cmd) => { const parent = parentOpts(cmd); const baseUrl = resolveBrowserControlUrl(parent?.url); - const profile = parent?.profile; + const profile = parent?.browserProfile; try { const result = await browserArmFileChooser(baseUrl, { paths, @@ -383,7 +383,7 @@ export function registerBrowserActionInputCommands( .action(async (opts, cmd) => { const parent = parentOpts(cmd); const baseUrl = resolveBrowserControlUrl(parent?.url); - const profile = parent?.profile; + const profile = parent?.browserProfile; try { const fields = await readFields({ fields: opts.fields, @@ -424,7 +424,7 @@ export function registerBrowserActionInputCommands( .action(async (opts, cmd) => { const parent = parentOpts(cmd); const baseUrl = resolveBrowserControlUrl(parent?.url); - const profile = parent?.profile; + const profile = parent?.browserProfile; const accept = opts.accept ? true : opts.dismiss ? false : undefined; if (accept === undefined) { defaultRuntime.error(danger("Specify --accept or --dismiss")); @@ -462,7 +462,7 @@ export function registerBrowserActionInputCommands( .action(async (opts, cmd) => { const parent = parentOpts(cmd); const baseUrl = resolveBrowserControlUrl(parent?.url); - const profile = parent?.profile; + const profile = parent?.browserProfile; try { const result = await browserAct( baseUrl, @@ -495,7 +495,7 @@ export function registerBrowserActionInputCommands( .action(async (opts, cmd) => { const parent = parentOpts(cmd); const baseUrl = resolveBrowserControlUrl(parent?.url); - const profile = parent?.profile; + const profile = parent?.browserProfile; if (!opts.fn) { defaultRuntime.error(danger("Missing --fn")); defaultRuntime.exit(1); diff --git a/src/cli/browser-cli-actions-observe.ts b/src/cli/browser-cli-actions-observe.ts index b39a3347e..0eade88a6 100644 --- a/src/cli/browser-cli-actions-observe.ts +++ b/src/cli/browser-cli-actions-observe.ts @@ -20,7 +20,7 @@ export function registerBrowserActionObserveCommands( .action(async (opts, cmd) => { const parent = parentOpts(cmd); const baseUrl = resolveBrowserControlUrl(parent?.url); - const profile = parent?.profile; + const profile = parent?.browserProfile; try { const result = await browserConsoleMessages(baseUrl, { level: opts.level?.trim() || undefined, @@ -45,7 +45,7 @@ export function registerBrowserActionObserveCommands( .action(async (opts, cmd) => { const parent = parentOpts(cmd); const baseUrl = resolveBrowserControlUrl(parent?.url); - const profile = parent?.profile; + const profile = parent?.browserProfile; try { const result = await browserPdfSave(baseUrl, { targetId: opts.targetId?.trim() || undefined, diff --git a/src/cli/browser-cli-inspect.ts b/src/cli/browser-cli-inspect.ts index 0a78af641..0bc528bd4 100644 --- a/src/cli/browser-cli-inspect.ts +++ b/src/cli/browser-cli-inspect.ts @@ -24,7 +24,7 @@ export function registerBrowserInspectCommands( .action(async (targetId: string | undefined, opts, cmd) => { const parent = parentOpts(cmd); const baseUrl = resolveBrowserControlUrl(parent?.url); - const profile = parent?.profile; + const profile = parent?.browserProfile; try { const result = await browserScreenshotAction(baseUrl, { targetId: targetId?.trim() || undefined, @@ -59,7 +59,7 @@ export function registerBrowserInspectCommands( .action(async (opts, cmd) => { const parent = parentOpts(cmd); const baseUrl = resolveBrowserControlUrl(parent?.url); - const profile = parent?.profile; + const profile = parent?.browserProfile; const format = opts.format === "aria" ? "aria" : "ai"; try { const result = await browserSnapshot(baseUrl, { diff --git a/src/cli/browser-cli-manage.ts b/src/cli/browser-cli-manage.ts index 725b7f987..6164c8c73 100644 --- a/src/cli/browser-cli-manage.ts +++ b/src/cli/browser-cli-manage.ts @@ -31,7 +31,7 @@ export function registerBrowserManageCommands( const baseUrl = resolveBrowserControlUrl(parent?.url); try { const status = await browserStatus(baseUrl, { - profile: parent?.profile, + profile: parent?.browserProfile, }); if (parent?.json) { defaultRuntime.log(JSON.stringify(status, null, 2)); @@ -61,7 +61,7 @@ export function registerBrowserManageCommands( .action(async (_opts, cmd) => { const parent = parentOpts(cmd); const baseUrl = resolveBrowserControlUrl(parent?.url); - const profile = parent?.profile; + const profile = parent?.browserProfile; try { await browserStart(baseUrl, { profile }); const status = await browserStatus(baseUrl, { profile }); @@ -85,7 +85,7 @@ export function registerBrowserManageCommands( .action(async (_opts, cmd) => { const parent = parentOpts(cmd); const baseUrl = resolveBrowserControlUrl(parent?.url); - const profile = parent?.profile; + const profile = parent?.browserProfile; try { await browserStop(baseUrl, { profile }); const status = await browserStatus(baseUrl, { profile }); @@ -109,7 +109,7 @@ export function registerBrowserManageCommands( .action(async (_opts, cmd) => { const parent = parentOpts(cmd); const baseUrl = resolveBrowserControlUrl(parent?.url); - const profile = parent?.profile; + const profile = parent?.browserProfile; try { const result = await browserResetProfile(baseUrl, { profile }); if (parent?.json) { @@ -134,7 +134,7 @@ export function registerBrowserManageCommands( .action(async (_opts, cmd) => { const parent = parentOpts(cmd); const baseUrl = resolveBrowserControlUrl(parent?.url); - const profile = parent?.profile; + const profile = parent?.browserProfile; try { const tabs = await browserTabs(baseUrl, { profile }); if (parent?.json) { @@ -166,7 +166,7 @@ export function registerBrowserManageCommands( .action(async (url: string, _opts, cmd) => { const parent = parentOpts(cmd); const baseUrl = resolveBrowserControlUrl(parent?.url); - const profile = parent?.profile; + const profile = parent?.browserProfile; try { const tab = await browserOpenTab(baseUrl, url, { profile }); if (parent?.json) { @@ -187,7 +187,7 @@ export function registerBrowserManageCommands( .action(async (targetId: string, _opts, cmd) => { const parent = parentOpts(cmd); const baseUrl = resolveBrowserControlUrl(parent?.url); - const profile = parent?.profile; + const profile = parent?.browserProfile; try { await browserFocusTab(baseUrl, targetId, { profile }); if (parent?.json) { @@ -208,7 +208,7 @@ export function registerBrowserManageCommands( .action(async (targetId: string | undefined, _opts, cmd) => { const parent = parentOpts(cmd); const baseUrl = resolveBrowserControlUrl(parent?.url); - const profile = parent?.profile; + const profile = parent?.browserProfile; try { if (targetId?.trim()) { await browserCloseTab(baseUrl, targetId.trim(), { profile }); diff --git a/src/cli/browser-cli-shared.ts b/src/cli/browser-cli-shared.ts index b280052a2..2e110f186 100644 --- a/src/cli/browser-cli-shared.ts +++ b/src/cli/browser-cli-shared.ts @@ -1,5 +1,5 @@ export type BrowserParentOpts = { url?: string; json?: boolean; - profile?: string; + browserProfile?: string; }; diff --git a/src/cli/browser-cli.test.ts b/src/cli/browser-cli.test.ts new file mode 100644 index 000000000..bae4f2175 --- /dev/null +++ b/src/cli/browser-cli.test.ts @@ -0,0 +1,79 @@ +import { describe, expect, it } from "vitest"; +import { Command } from "commander"; + +describe("browser CLI --browser-profile flag", () => { + it("parses --browser-profile from parent command options", () => { + const program = new Command(); + program.name("test"); + + const browser = program + .command("browser") + .option("--browser-profile ", "Browser profile name"); + + let capturedProfile: string | undefined; + + browser.command("status").action((_opts, cmd) => { + const parent = cmd.parent?.opts?.() as { browserProfile?: string }; + capturedProfile = parent?.browserProfile; + }); + + program.parse(["node", "test", "browser", "--browser-profile", "onasset", "status"]); + + expect(capturedProfile).toBe("onasset"); + }); + + it("defaults to undefined when --browser-profile not provided", () => { + const program = new Command(); + program.name("test"); + + const browser = program + .command("browser") + .option("--browser-profile ", "Browser profile name"); + + let capturedProfile: string | undefined = "should-be-undefined"; + + browser.command("status").action((_opts, cmd) => { + const parent = cmd.parent?.opts?.() as { browserProfile?: string }; + capturedProfile = parent?.browserProfile; + }); + + program.parse(["node", "test", "browser", "status"]); + + expect(capturedProfile).toBeUndefined(); + }); + + it("does not conflict with global --profile flag", () => { + // The global --profile flag is handled by entry.js before Commander + // This test verifies --browser-profile is a separate option + const program = new Command(); + program.name("test"); + program.option("--profile ", "Global config profile"); + + const browser = program + .command("browser") + .option("--browser-profile ", "Browser profile name"); + + let globalProfile: string | undefined; + let browserProfile: string | undefined; + + browser.command("status").action((_opts, cmd) => { + const parent = cmd.parent?.opts?.() as { browserProfile?: string }; + browserProfile = parent?.browserProfile; + globalProfile = program.opts().profile; + }); + + program.parse([ + "node", + "test", + "--profile", + "dev", + "browser", + "--browser-profile", + "onasset", + "status", + ]); + + expect(globalProfile).toBe("dev"); + expect(browserProfile).toBe("onasset"); + }); +}); diff --git a/src/cli/browser-cli.ts b/src/cli/browser-cli.ts index e6bd5adfc..c38809428 100644 --- a/src/cli/browser-cli.ts +++ b/src/cli/browser-cli.ts @@ -20,7 +20,7 @@ export function registerBrowserCli(program: Command) { "--url ", "Override browser control URL (default from ~/.clawdbot/clawdbot.json)", ) - .option("--profile ", "Browser profile name (default from config)") + .option("--browser-profile ", "Browser profile name (default from config)") .option("--json", "Output machine-readable JSON", false) .addHelpText( "after",