From 6cebd265298eded9d3ac0db071310895548a4631 Mon Sep 17 00:00:00 2001 From: James Groat Date: Tue, 6 Jan 2026 09:54:31 -0700 Subject: [PATCH 1/2] fix(browser-cli): rename --profile to --browser-profile to avoid conflict with global --profile flag --- docs/browser.md | 2 +- src/agents/tools/browser-tool.ts | 33 ++++++----- src/cli/browser-cli-actions-input.ts | 26 ++++----- src/cli/browser-cli-actions-observe.ts | 4 +- src/cli/browser-cli-inspect.ts | 4 +- src/cli/browser-cli-manage.ts | 16 +++--- src/cli/browser-cli-shared.ts | 2 +- src/cli/browser-cli.test.ts | 79 ++++++++++++++++++++++++++ src/cli/browser-cli.ts | 2 +- 9 files changed, 127 insertions(+), 41 deletions(-) create mode 100644 src/cli/browser-cli.test.ts diff --git a/docs/browser.md b/docs/browser.md index 5684d7c17..bc1502b7a 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", From cdd0cb6089d72433b11e5f52c522670b6fcc928b Mon Sep 17 00:00:00 2001 From: James Groat Date: Tue, 6 Jan 2026 11:04:33 -0700 Subject: [PATCH 2/2] fix(browser): add profile param to tabs routes and browser-tool - tabs.ts now uses getProfileContext like other routes - browser-tool threads profile param through all actions - add tests for profile query param on /tabs endpoints - update docs with browser tool profile parameter --- docs/browser.md | 16 +++++++++ src/agents/tools/browser-tool.ts | 10 ++++-- src/browser/routes/tabs.ts | 56 ++++++++++++++++++++++---------- src/browser/server.test.ts | 55 +++++++++++++++++++++++++++++++ src/cli/browser-cli.test.ts | 11 +++++-- src/cli/browser-cli.ts | 5 ++- 6 files changed, 130 insertions(+), 23 deletions(-) diff --git a/docs/browser.md b/docs/browser.md index bc1502b7a..1a460a55f 100644 --- a/docs/browser.md +++ b/docs/browser.md @@ -189,10 +189,26 @@ All existing endpoints accept optional `?profile=` query parameter: - `GET /?profile=work` — status for work profile - `POST /start?profile=work` — start work profile browser - `GET /tabs?profile=work` — list tabs for work profile +- `POST /tabs/open?profile=work` — open tab in work profile - etc. When `profile` is omitted, uses `browser.defaultProfile` (defaults to "clawd"). +### Agent browser tool + +The `browser` tool accepts an optional `profile` parameter for all actions: + +```json +{ + "action": "open", + "targetUrl": "https://example.com", + "profile": "work" +} +``` + +This routes the operation to the specified profile's browser instance. Omitting +`profile` uses the default profile. + ### Profile naming rules - Lowercase alphanumeric characters and hyphens only diff --git a/src/agents/tools/browser-tool.ts b/src/agents/tools/browser-tool.ts index cfae25603..8681e3abb 100644 --- a/src/agents/tools/browser-tool.ts +++ b/src/agents/tools/browser-tool.ts @@ -182,7 +182,9 @@ export function createBrowserTool(opts?: { const targetUrl = readStringParam(params, "targetUrl", { required: true, }); - return jsonResult(await browserOpenTab(baseUrl, targetUrl, { profile })); + return jsonResult( + await browserOpenTab(baseUrl, targetUrl, { profile }), + ); } case "focus": { const targetId = readStringParam(params, "targetId", { @@ -250,7 +252,11 @@ export function createBrowserTool(opts?: { }); const targetId = readStringParam(params, "targetId"); return jsonResult( - await browserNavigate(baseUrl, { url: targetUrl, targetId, profile }), + await browserNavigate(baseUrl, { + url: targetUrl, + targetId, + profile, + }), ); } case "console": { diff --git a/src/browser/routes/tabs.ts b/src/browser/routes/tabs.ts index ef786afa6..509395414 100644 --- a/src/browser/routes/tabs.ts +++ b/src/browser/routes/tabs.ts @@ -1,18 +1,26 @@ import type express from "express"; import type { BrowserRouteContext } from "../server-context.js"; -import { jsonError, toNumber, toStringOrEmpty } from "./utils.js"; +import { + getProfileContext, + jsonError, + toNumber, + toStringOrEmpty, +} from "./utils.js"; export function registerBrowserTabRoutes( app: express.Express, ctx: BrowserRouteContext, ) { - app.get("/tabs", async (_req, res) => { + app.get("/tabs", async (req, res) => { + const profileCtx = getProfileContext(req, ctx); + if ("error" in profileCtx) + return jsonError(res, profileCtx.status, profileCtx.error); try { - const reachable = await ctx.isReachable(300); + const reachable = await profileCtx.isReachable(300); if (!reachable) return res.json({ running: false, tabs: [] as unknown[] }); - const tabs = await ctx.listTabs(); + const tabs = await profileCtx.listTabs(); res.json({ running: true, tabs }); } catch (err) { jsonError(res, 500, String(err)); @@ -20,11 +28,14 @@ export function registerBrowserTabRoutes( }); app.post("/tabs/open", async (req, res) => { + const profileCtx = getProfileContext(req, ctx); + if ("error" in profileCtx) + return jsonError(res, profileCtx.status, profileCtx.error); const url = toStringOrEmpty((req.body as { url?: unknown })?.url); if (!url) return jsonError(res, 400, "url is required"); try { - await ctx.ensureBrowserAvailable(); - const tab = await ctx.openTab(url); + await profileCtx.ensureBrowserAvailable(); + const tab = await profileCtx.openTab(url); res.json(tab); } catch (err) { jsonError(res, 500, String(err)); @@ -32,14 +43,17 @@ export function registerBrowserTabRoutes( }); app.post("/tabs/focus", async (req, res) => { + const profileCtx = getProfileContext(req, ctx); + if ("error" in profileCtx) + return jsonError(res, profileCtx.status, profileCtx.error); const targetId = toStringOrEmpty( (req.body as { targetId?: unknown })?.targetId, ); if (!targetId) return jsonError(res, 400, "targetId is required"); try { - if (!(await ctx.isReachable(300))) + if (!(await profileCtx.isReachable(300))) return jsonError(res, 409, "browser not running"); - await ctx.focusTab(targetId); + await profileCtx.focusTab(targetId); res.json({ ok: true }); } catch (err) { const mapped = ctx.mapTabError(err); @@ -49,12 +63,15 @@ export function registerBrowserTabRoutes( }); app.delete("/tabs/:targetId", async (req, res) => { + const profileCtx = getProfileContext(req, ctx); + if ("error" in profileCtx) + return jsonError(res, profileCtx.status, profileCtx.error); const targetId = toStringOrEmpty(req.params.targetId); if (!targetId) return jsonError(res, 400, "targetId is required"); try { - if (!(await ctx.isReachable(300))) + if (!(await profileCtx.isReachable(300))) return jsonError(res, 409, "browser not running"); - await ctx.closeTab(targetId); + await profileCtx.closeTab(targetId); res.json({ ok: true }); } catch (err) { const mapped = ctx.mapTabError(err); @@ -64,37 +81,40 @@ export function registerBrowserTabRoutes( }); app.post("/tabs/action", async (req, res) => { + const profileCtx = getProfileContext(req, ctx); + if ("error" in profileCtx) + return jsonError(res, profileCtx.status, profileCtx.error); const action = toStringOrEmpty((req.body as { action?: unknown })?.action); const index = toNumber((req.body as { index?: unknown })?.index); try { if (action === "list") { - const reachable = await ctx.isReachable(300); + const reachable = await profileCtx.isReachable(300); if (!reachable) return res.json({ ok: true, tabs: [] as unknown[] }); - const tabs = await ctx.listTabs(); + const tabs = await profileCtx.listTabs(); return res.json({ ok: true, tabs }); } if (action === "new") { - await ctx.ensureBrowserAvailable(); - const tab = await ctx.openTab("about:blank"); + await profileCtx.ensureBrowserAvailable(); + const tab = await profileCtx.openTab("about:blank"); return res.json({ ok: true, tab }); } if (action === "close") { - const tabs = await ctx.listTabs(); + const tabs = await profileCtx.listTabs(); const target = typeof index === "number" ? tabs[index] : tabs.at(0); if (!target) return jsonError(res, 404, "tab not found"); - await ctx.closeTab(target.targetId); + await profileCtx.closeTab(target.targetId); return res.json({ ok: true, targetId: target.targetId }); } if (action === "select") { if (typeof index !== "number") return jsonError(res, 400, "index is required"); - const tabs = await ctx.listTabs(); + const tabs = await profileCtx.listTabs(); const target = tabs[index]; if (!target) return jsonError(res, 404, "tab not found"); - await ctx.focusTab(target.targetId); + await profileCtx.focusTab(target.targetId); return res.json({ ok: true, targetId: target.targetId }); } diff --git a/src/browser/server.test.ts b/src/browser/server.test.ts index d1318771b..4124c956c 100644 --- a/src/browser/server.test.ts +++ b/src/browser/server.test.ts @@ -894,6 +894,61 @@ describe("backward compatibility (profile parameter)", () => { // Should at least have the default clawd profile expect(result.profiles.some((p) => p.name === "clawd")).toBe(true); }); + + it("GET /tabs?profile=clawd returns tabs for specified profile", async () => { + const { startBrowserControlServerFromConfig } = await import("./server.js"); + await startBrowserControlServerFromConfig(); + const base = `http://127.0.0.1:${testPort}`; + + await realFetch(`${base}/start`, { method: "POST" }); + + const result = (await realFetch(`${base}/tabs?profile=clawd`).then((r) => + r.json(), + )) as { running: boolean; tabs: unknown[] }; + expect(result.running).toBe(true); + expect(Array.isArray(result.tabs)).toBe(true); + }); + + it("POST /tabs/open?profile=clawd opens tab in specified profile", async () => { + const { startBrowserControlServerFromConfig } = await import("./server.js"); + await startBrowserControlServerFromConfig(); + const base = `http://127.0.0.1:${testPort}`; + + await realFetch(`${base}/start`, { method: "POST" }); + + const result = (await realFetch(`${base}/tabs/open?profile=clawd`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ url: "https://example.com" }), + }).then((r) => r.json())) as { targetId?: string }; + expect(result.targetId).toBe("newtab1"); + }); + + it("GET /tabs?profile=unknown returns 404", async () => { + const { startBrowserControlServerFromConfig } = await import("./server.js"); + await startBrowserControlServerFromConfig(); + const base = `http://127.0.0.1:${testPort}`; + + const result = await realFetch(`${base}/tabs?profile=unknown`); + expect(result.status).toBe(404); + const body = (await result.json()) as { error: string }; + expect(body.error).toContain("not found"); + }); + + it("POST /tabs/open?profile=unknown returns 404", async () => { + const { startBrowserControlServerFromConfig } = await import("./server.js"); + await startBrowserControlServerFromConfig(); + const base = `http://127.0.0.1:${testPort}`; + + const result = await realFetch(`${base}/tabs/open?profile=unknown`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ url: "https://example.com" }), + }); + expect(result.status).toBe(404); + const body = (await result.json()) as { error: string }; + expect(body.error).toContain("not found"); + }); }); describe("profile CRUD endpoints", () => { diff --git a/src/cli/browser-cli.test.ts b/src/cli/browser-cli.test.ts index bae4f2175..6dc5a0a48 100644 --- a/src/cli/browser-cli.test.ts +++ b/src/cli/browser-cli.test.ts @@ -1,5 +1,5 @@ -import { describe, expect, it } from "vitest"; import { Command } from "commander"; +import { describe, expect, it } from "vitest"; describe("browser CLI --browser-profile flag", () => { it("parses --browser-profile from parent command options", () => { @@ -17,7 +17,14 @@ describe("browser CLI --browser-profile flag", () => { capturedProfile = parent?.browserProfile; }); - program.parse(["node", "test", "browser", "--browser-profile", "onasset", "status"]); + program.parse([ + "node", + "test", + "browser", + "--browser-profile", + "onasset", + "status", + ]); expect(capturedProfile).toBe("onasset"); }); diff --git a/src/cli/browser-cli.ts b/src/cli/browser-cli.ts index c38809428..88d6022f5 100644 --- a/src/cli/browser-cli.ts +++ b/src/cli/browser-cli.ts @@ -20,7 +20,10 @@ export function registerBrowserCli(program: Command) { "--url ", "Override browser control URL (default from ~/.clawdbot/clawdbot.json)", ) - .option("--browser-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",