Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/tidy-contexts-share.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"browse": patch
---

store Context names in Browserbase while retaining local name-to-ID lookup compatibility and preserving legacy aliases
8 changes: 7 additions & 1 deletion packages/cli/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -227,7 +227,7 @@ browse cloud sessions downloads get <session-id> # --output ./downloads.zip
browse cloud sessions uploads create <session-id> ./file.pdf

# Contexts
browse cloud contexts create
browse cloud contexts create --name github # name is stored in Browserbase
browse cloud contexts get <context-id>
browse cloud contexts update <context-id> # refresh the upload URL
browse cloud contexts delete <context-id>
Expand All @@ -242,6 +242,12 @@ browse cloud fetch <url> # markdown by default
browse cloud search <query>
```

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 <name> <context-id> --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 <schema>` for structured extraction.

## Functions
Expand Down
2 changes: 1 addition & 1 deletion packages/cli/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
17 changes: 11 additions & 6 deletions packages/cli/skills/browse/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -249,17 +249,22 @@ For remote sessions with context persistence:
browse cloud sessions create --context-id <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 <context-id> # name a context you already have
browse cloud contexts create --name github # server-owned name + local ID cache
browse cloud contexts add github <context-id> # 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 <name> <context-id> --force` when needed.

Use `--verified` when the task needs Browserbase Verified browser mode. To drive a Verified/proxied session directly, prefer `browse open <url> --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 <schema>` for structured extraction. Use `browse cloud search` when the user asks for web search results.
Expand Down
19 changes: 14 additions & 5 deletions packages/cli/src/commands/cloud/contexts/create.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -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: "<name>",
}),
body: Flags.string({
Expand All @@ -50,17 +50,26 @@ 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 <name> <context-id> --force`.",
);
}
}

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, {
Expand Down
6 changes: 3 additions & 3 deletions packages/cli/src/commands/cloud/contexts/list.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -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 <name>",
"No cached contexts. Create one with: browse cloud contexts create --name <name>",
);
return;
}
Expand All @@ -54,7 +54,7 @@ function outputContextsTable(
contexts,
[
{
header: "Name",
header: "Local name",
maxWidth: 24,
value: (context) => context.name,
},
Expand Down
16 changes: 8 additions & 8 deletions packages/cli/src/lib/cloud/contexts-store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
84 changes: 81 additions & 3 deletions packages/cli/tests/contexts-named.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 () => {
Expand Down Expand Up @@ -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 },
Expand All @@ -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"], {
Expand Down Expand Up @@ -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) => {
Expand Down
19 changes: 17 additions & 2 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading