diff --git a/.changeset/tidy-contexts-share.md b/.changeset/tidy-contexts-share.md new file mode 100644 index 0000000000..8079ca9fe5 --- /dev/null +++ b/.changeset/tidy-contexts-share.md @@ -0,0 +1,5 @@ +--- +"browse": patch +--- + +store Context names in Browserbase while retaining local name-to-ID lookup compatibility and preserving legacy aliases diff --git a/packages/cli/README.md b/packages/cli/README.md index 58e8b5b142..7c0a2bc345 100644 --- a/packages/cli/README.md +++ b/packages/cli/README.md @@ -227,7 +227,7 @@ browse cloud sessions downloads get # --output ./downloads.zip browse cloud sessions uploads create ./file.pdf # Contexts -browse cloud contexts create +browse cloud contexts create --name github # name is stored in Browserbase browse cloud contexts get browse cloud contexts update # refresh the upload URL browse cloud contexts delete @@ -242,6 +242,12 @@ browse cloud fetch # markdown by default browse cloud search ``` +Names cached by earlier Browse versions remain local aliases and continue to +resolve to their saved Context IDs. They do not need to match the Context's +Browserbase-managed name. `contexts create --name` never overwrites an existing +local alias; use `contexts add --force` only after explicitly +reconciling a legacy mapping. + `browse cloud fetch` returns markdown-formatted page content by default. Use `--format raw` for the original response body, or `--format json --schema ` for structured extraction. ## Functions diff --git a/packages/cli/package.json b/packages/cli/package.json index d8abad3635..f5c0503954 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -102,7 +102,7 @@ "prepublishOnly": "pnpm build" }, "dependencies": { - "@browserbasehq/sdk": "^2.14.0", + "@browserbasehq/sdk": "^2.17.0", "@browserbasehq/stagehand": "workspace:*", "@oclif/core": "^4.11.0", "@vercel/detect-agent": "^1.2.3", diff --git a/packages/cli/skills/browse/SKILL.md b/packages/cli/skills/browse/SKILL.md index 9ddf701379..4cea63cd96 100644 --- a/packages/cli/skills/browse/SKILL.md +++ b/packages/cli/skills/browse/SKILL.md @@ -249,17 +249,22 @@ For remote sessions with context persistence: browse cloud sessions create --context-id --persist ``` -Contexts persist cookies and local storage (logins) across sessions. Name a -context once with `--name` to save a local alias, then reuse the name anywhere a -context ID is accepted instead of memorizing the ID: +Contexts persist cookies and local storage (logins) across sessions. `--name` +stores the name on the Browserbase Context and caches its returned ID on this +device, so the name can also be reused anywhere the CLI accepts a context ID: ```bash -browse cloud contexts create --name github # saves github -> ctx_... -browse cloud contexts add github # name a context you already have +browse cloud contexts create --name github # server-owned name + local ID cache +browse cloud contexts add github # add a local alias for an existing ID browse cloud sessions create --context-id github --persist -browse cloud contexts list # show saved names +browse cloud contexts list # show this device's cached names/aliases ``` +Names saved by earlier CLI versions remain valid local aliases even when they +do not match the Browserbase-managed Context name. `contexts create --name` +will not overwrite one of those mappings. Reconcile deliberately with +`contexts add --force` when needed. + Use `--verified` when the task needs Browserbase Verified browser mode. To drive a Verified/proxied session directly, prefer `browse open --remote --verified --proxies` over create-then-attach — it keeps the session identity so `browse status`/`browse doctor` can report it. Use `browse cloud sessions create` for session options the driver flags don't cover (region, keep-alive, contexts, full `--stdin` body). Use `browse cloud fetch` when the user needs a simple HTTP fetch without browser interaction. It returns markdown-formatted page content by default; pass `--format raw` for the original response body or `--format json --schema ` for structured extraction. Use `browse cloud search` when the user asks for web search results. diff --git a/packages/cli/src/commands/cloud/contexts/create.ts b/packages/cli/src/commands/cloud/contexts/create.ts index 524986e525..702e91c9c3 100644 --- a/packages/cli/src/commands/cloud/contexts/create.ts +++ b/packages/cli/src/commands/cloud/contexts/create.ts @@ -18,7 +18,7 @@ import { BrowseCommand } from "../../../base.js"; export default class ContextsCreate extends BrowseCommand { static override description = - "Create a Browserbase context. Pass --name to save a local alias you can reuse instead of the context ID."; + "Create a Browserbase context. Pass --name to store a project-scoped name in Browserbase and cache its ID locally."; static override examples = [ "browse cloud contexts create", "browse cloud contexts create --name github", @@ -30,7 +30,7 @@ export default class ContextsCreate extends BrowseCommand { ...apiCommonFlags, name: Flags.string({ description: - "Save a local alias for the new context so you can reuse it by name.", + "Set the Context name in Browserbase and cache its ID for local name lookup.", helpValue: "", }), body: Flags.string({ @@ -50,9 +50,13 @@ export default class ContextsCreate extends BrowseCommand { if (!isValidContextName(name)) { fail(`Invalid context name "${name}". ${contextNameRequirement()}`); } - if (await getContextAlias(name)) { + const existingAlias = await getContextAlias(name); + if (existingAlias) { fail( - `A context named "${name}" already exists locally. Choose another name or remove it with "browse cloud contexts delete ${name}".`, + `A context named "${name}" already exists locally and maps to ${existingAlias.id}. ` + + "Existing local aliases are preserved because they may predate Browserbase-managed Context names. " + + "Choose another name, or reconcile the alias explicitly with " + + "`browse cloud contexts add --force`.", ); } } @@ -60,7 +64,12 @@ export default class ContextsCreate extends BrowseCommand { await withBrowserbaseApi("contexts", async () => { const client = createBrowserbaseClient(toApiOptions(flags)); const body = await resolveBody({ body: flags.body, stdin: flags.stdin }); - const context = await client.contexts.create(body); + // Browserbase owns name uniqueness and canonical storage. The explicit + // flag takes precedence over a name supplied through --body/--stdin, + // matching the merge behavior of other cloud command flags. + const context = await client.contexts.create( + name === undefined ? body : { ...body, name }, + ); if (name !== undefined && context.id) { await saveContextAlias(name, { diff --git a/packages/cli/src/commands/cloud/contexts/list.ts b/packages/cli/src/commands/cloud/contexts/list.ts index d2a9ec73be..2d05fb2707 100644 --- a/packages/cli/src/commands/cloud/contexts/list.ts +++ b/packages/cli/src/commands/cloud/contexts/list.ts @@ -14,7 +14,7 @@ import { export default class ContextsList extends BrowseCommand { static override description = - "List Browserbase contexts you have saved locally with a name."; + "List Context name-to-ID mappings cached on this device."; static override examples = [ "browse cloud contexts list", "browse cloud contexts list --json", @@ -37,7 +37,7 @@ export default class ContextsList extends BrowseCommand { if (contexts.length === 0) { console.log( - "No saved contexts. Create one with: browse cloud contexts create --name ", + "No cached contexts. Create one with: browse cloud contexts create --name ", ); return; } @@ -54,7 +54,7 @@ function outputContextsTable( contexts, [ { - header: "Name", + header: "Local name", maxWidth: 24, value: (context) => context.name, }, diff --git a/packages/cli/src/lib/cloud/contexts-store.ts b/packages/cli/src/lib/cloud/contexts-store.ts index 7a8b862639..8641926cd2 100644 --- a/packages/cli/src/lib/cloud/contexts-store.ts +++ b/packages/cli/src/lib/cloud/contexts-store.ts @@ -5,15 +5,15 @@ import { dirname, join } from "node:path"; import { resolveConfigDir } from "../identity.js"; /** - * Local name -> Browserbase context-id map. + * Local cache of Browserbase context names -> context ids. * - * Browserbase contexts are identified only by an opaque id and the platform has - * no server-side list endpoint, so to give contexts memorable names (e.g. - * `github`, `gmail`) we keep a small map on the local device. It lives next to - * the CLI's other state at `(XDG_CONFIG_HOME||~/.config)/browserbase/contexts.json` - * (honoring `BROWSERBASE_CONFIG_DIR`). This is purely a client-side convenience: - * the ids it stores are the same ids the API already returns, and a missing or - * corrupt file degrades to "no saved contexts" rather than an error. + * Browserbase stores an optional, project-scoped name on each Context. The + * public API still identifies Contexts and session persistence by opaque id and + * does not expose list or lookup-by-name endpoints, so the CLI caches the names + * it creates on this device. It lives next to the CLI's other state at + * `(XDG_CONFIG_HOME||~/.config)/browserbase/contexts.json` (honoring + * `BROWSERBASE_CONFIG_DIR`). A missing or corrupt cache degrades to "no cached + * contexts" rather than an error; Browserbase remains authoritative for names. */ const STORE_VERSION = 1; diff --git a/packages/cli/tests/contexts-named.test.ts b/packages/cli/tests/contexts-named.test.ts index 51ddccc8c6..257cc9896c 100644 --- a/packages/cli/tests/contexts-named.test.ts +++ b/packages/cli/tests/contexts-named.test.ts @@ -29,8 +29,8 @@ function pathOf(request: CapturedRequest): string { /** * Drives the real built CLI through the full named-context lifecycle against a - * fake Browserbase server, proving the local name->id map is written on create - * and resolved by list / get / sessions-create / delete. + * fake Browserbase server, proving Browserbase receives the name and the local + * lookup cache resolves its returned id for list / get / sessions-create / delete. */ describe("named contexts (end to end through the CLI)", () => { it("creates by name, resolves the name everywhere, and prunes on delete", async () => { @@ -70,7 +70,8 @@ describe("named contexts (end to end through the CLI)", () => { const storePath = join(configDir, "contexts.json"); try { - // 1. create --name writes the local alias and echoes the name back. + // 1. create --name sends the server-owned name, caches the returned id, + // and echoes the name back. const created = await runCli( ["cloud", "contexts", "create", "--name", "github"], { env }, @@ -83,6 +84,10 @@ describe("named contexts (end to end through the CLI)", () => { expect(JSON.parse(await readFile(storePath, "utf8"))).toMatchObject({ contexts: { github: { id: CONTEXT_ID } }, }); + const createRequest = server.requests.find( + (r) => r.method === "POST" && pathOf(r) === "/v1/contexts", + ); + expect(createRequest?.jsonBody).toMatchObject({ name: "github" }); // 2. list --json surfaces the saved alias. const listed = await runCli(["cloud", "contexts", "list", "--json"], { @@ -231,6 +236,79 @@ describe("named contexts (end to end through the CLI)", () => { } }); + it("preserves a legacy local alias when its Browserbase-managed name differs", async () => { + const legacyId = "00000000-0000-4000-8000-0000000000cc"; + const server = await startFakeBrowserbaseServer((request, response) => { + if ( + request.method === "GET" && + pathOf(request) === `/v1/contexts/${legacyId}` + ) { + jsonResponse(response, 200, { + id: legacyId, + name: "managed-name", + status: "ready", + }); + return; + } + if (request.method === "POST" && pathOf(request) === "/v1/contexts") { + jsonResponse(response, 200, { id: "ctx_should_not_be_created" }); + return; + } + jsonResponse(response, 200, {}); + }); + const env = { + BROWSERBASE_CONFIG_DIR: configDir, + BROWSERBASE_API_KEY: "test-key", + BROWSERBASE_BASE_URL: server.baseUrl, + }; + const storePath = join(configDir, "contexts.json"); + + try { + // A pre-managed-name CLI install may already have an arbitrary local + // alias. It remains a valid lookup even when the API reports another + // Browserbase-owned name for that Context. + const added = await runCli( + ["cloud", "contexts", "add", "legacy-login", legacyId], + { env }, + ); + expect(added.exitCode).toBe(0); + + const got = await runCli(["cloud", "contexts", "get", "legacy-login"], { + env, + }); + expect(got.exitCode).toBe(0); + expect(JSON.parse(got.stdout)).toMatchObject({ + id: legacyId, + name: "managed-name", + }); + + // Creating a new managed Context under the same local name must fail + // before the API call instead of silently repointing the legacy alias. + const duplicate = await runCli( + ["cloud", "contexts", "create", "--name", "legacy-login"], + { + env, + }, + ); + expect(duplicate.exitCode).not.toBe(0); + expect(duplicate.stderr).toContain("already exists locally"); + expect(duplicate.stderr).toContain( + "may predate Browserbase-managed Context names", + ); + expect( + server.requests.some( + (request) => + request.method === "POST" && pathOf(request) === "/v1/contexts", + ), + ).toBe(false); + expect(JSON.parse(await readFile(storePath, "utf8"))).toMatchObject({ + contexts: { "legacy-login": { id: legacyId } }, + }); + } finally { + await server.close(); + } + }); + it("passes an unrecognized raw id through to the API (raw-id compatibility)", async () => { const rawId = "legacy-id-not-a-uuid-9000"; const server = await startFakeBrowserbaseServer((request, response) => { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 9d109b617f..d3f083ca43 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -240,8 +240,8 @@ importers: packages/cli: dependencies: '@browserbasehq/sdk': - specifier: ^2.14.0 - version: 2.16.0 + specifier: ^2.17.0 + version: 2.19.0 '@browserbasehq/stagehand': specifier: workspace:* version: link:../sdk-ts @@ -1431,6 +1431,9 @@ packages: '@browserbasehq/sdk@2.16.0': resolution: {integrity: sha512-mPAuLRU9jWR7o0KJi9+gQnOBDUSIkoKbbFv4HjrA+80qWVcFacrNPlZmf4mguQnfZ0oP2t5c3ws6yuFyAX9vpA==} + '@browserbasehq/sdk@2.19.0': + resolution: {integrity: sha512-Fiehnkt0q9ItPnkVyi16fQLNtzUMi5KsAFYLzLcDXKWQV6kXhBNVHOa+v9BqqzSxHIS2Nbzk4E1XRKvphLh1eA==} + '@browserbasehq/stagehand@3.7.1': resolution: {integrity: sha512-vAuYSZWIhh3d76BxwppNVE3dB0ztEBLBi85G6TWulZNiebdWptNoANOMuprOB/cw5dE+80b/ZZQo4G33Pc9i6w==} engines: {node: ^20.19.0 || >=22.12.0} @@ -9584,6 +9587,18 @@ snapshots: transitivePeerDependencies: - encoding + '@browserbasehq/sdk@2.19.0': + dependencies: + '@types/node': 18.19.130 + '@types/node-fetch': 2.6.13 + abort-controller: 3.0.0 + agentkeepalive: 4.6.0 + form-data-encoder: 1.7.2 + formdata-node: 4.4.1 + node-fetch: 2.7.0 + transitivePeerDependencies: + - encoding + '@browserbasehq/stagehand@3.7.1(playwright-core@1.56.1)(zod@4.4.3)': dependencies: '@ai-sdk/provider': 2.0.3