From da283f38846cb585ef101a040fa15ef91fb8fe96 Mon Sep 17 00:00:00 2001 From: Shrey Pandya Date: Thu, 13 Aug 2026 01:44:57 +0000 Subject: [PATCH 1/2] fix(cli): align Functions with SDK --- .changeset/warm-functions-parity.md | 5 + packages/cli/README.md | 4 + packages/cli/skills/browse/SKILL.md | 2 +- packages/cli/src/commands/functions/dev.ts | 7 ++ packages/cli/src/commands/functions/invoke.ts | 2 + .../cli/src/commands/functions/publish.ts | 7 ++ packages/cli/src/lib/functions/dev.ts | 46 ++++++--- packages/cli/src/lib/functions/init.ts | 19 ++-- packages/cli/src/lib/functions/publish.ts | 20 +++- packages/cli/src/lib/functions/shared.ts | 32 +++++++ .../cli/tests/cli-functions-contract.test.ts | 96 +++++++++++++++++-- 11 files changed, 209 insertions(+), 31 deletions(-) create mode 100644 .changeset/warm-functions-parity.md diff --git a/.changeset/warm-functions-parity.md b/.changeset/warm-functions-parity.md new file mode 100644 index 000000000..ece5c36ef --- /dev/null +++ b/.changeset/warm-functions-parity.md @@ -0,0 +1,5 @@ +--- +"browse": patch +--- + +Align `browse functions` with the Browserbase Functions SDK by forwarding explicit project IDs during local development and publishing, matching local invocation context and error payloads, and accepting the SDK's `--api-url` option. diff --git a/packages/cli/README.md b/packages/cli/README.md index 0e694c43c..0878b1975 100644 --- a/packages/cli/README.md +++ b/packages/cli/README.md @@ -256,6 +256,10 @@ browse functions invoke --params '{"url":"https://example.com"}' browse functions invoke --check-status ``` +Set both `BROWSERBASE_API_KEY` and `BROWSERBASE_PROJECT_ID` for `functions dev` and +`functions publish`. `functions invoke` only requires `BROWSERBASE_API_KEY`. You can also pass +`--api-key` and `--project-id` explicitly; `--api-url` is accepted as an alias for `--base-url`. + ## Templates Discover and scaffold ready-to-run Browserbase example projects. diff --git a/packages/cli/skills/browse/SKILL.md b/packages/cli/skills/browse/SKILL.md index 9ddf70137..1126ad960 100644 --- a/packages/cli/skills/browse/SKILL.md +++ b/packages/cli/skills/browse/SKILL.md @@ -277,7 +277,7 @@ browse functions invoke --params '{"url":"https://example.com"}' browse functions invoke --check-status ``` -Functions commands use `BROWSERBASE_API_KEY`. Generated projects import `defineFn` from `@browserbasehq/sdk-functions`. +Functions commands use `BROWSERBASE_API_KEY`; local development and publishing also require `BROWSERBASE_PROJECT_ID`. Generated projects import `defineFn` from `@browserbasehq/sdk-functions`. ## Templates diff --git a/packages/cli/src/commands/functions/dev.ts b/packages/cli/src/commands/functions/dev.ts index 19a6983ca..4cdfa0145 100644 --- a/packages/cli/src/commands/functions/dev.ts +++ b/packages/cli/src/commands/functions/dev.ts @@ -24,6 +24,8 @@ export default class FunctionsDev extends BrowseCommand { helpValue: "", }), "base-url": Flags.string({ + aliases: ["api-url"], + char: "u", description: "Override the Browserbase API base URL.", helpValue: "", }), @@ -37,6 +39,10 @@ export default class FunctionsDev extends BrowseCommand { description: "Port to listen on.", helpValue: "", }), + "project-id": Flags.string({ + description: "Browserbase project ID used for local browser sessions.", + helpValue: "", + }), verbose: Flags.boolean({ description: "Print verbose runtime logs.", }), @@ -50,6 +56,7 @@ export default class FunctionsDev extends BrowseCommand { entrypoint: args.entrypoint, host: flags.host, port: flags.port, + projectId: flags["project-id"], verbose: flags.verbose ?? false, }); } diff --git a/packages/cli/src/commands/functions/invoke.ts b/packages/cli/src/commands/functions/invoke.ts index ade22822a..f45e32fec 100644 --- a/packages/cli/src/commands/functions/invoke.ts +++ b/packages/cli/src/commands/functions/invoke.ts @@ -26,6 +26,8 @@ export default class FunctionsInvoke extends BrowseCommand { helpValue: "", }), "base-url": Flags.string({ + aliases: ["api-url"], + char: "u", description: "Override the Browserbase API base URL.", helpValue: "", }), diff --git a/packages/cli/src/commands/functions/publish.ts b/packages/cli/src/commands/functions/publish.ts index ceea0f365..97d9112fe 100644 --- a/packages/cli/src/commands/functions/publish.ts +++ b/packages/cli/src/commands/functions/publish.ts @@ -24,12 +24,18 @@ export default class FunctionsPublish extends BrowseCommand { helpValue: "", }), "base-url": Flags.string({ + aliases: ["api-url"], + char: "u", description: "Override the Browserbase API base URL.", helpValue: "", }), "dry-run": Flags.boolean({ description: "Show what would be published without uploading.", }), + "project-id": Flags.string({ + description: "Browserbase project ID to publish into.", + helpValue: "", + }), }; async run(): Promise { @@ -39,6 +45,7 @@ export default class FunctionsPublish extends BrowseCommand { baseUrl: flags["base-url"], dryRun: flags["dry-run"] ?? false, entrypoint: args.entrypoint, + projectId: flags["project-id"], }); } } diff --git a/packages/cli/src/lib/functions/dev.ts b/packages/cli/src/lib/functions/dev.ts index 95aac905e..e1811625d 100644 --- a/packages/cli/src/lib/functions/dev.ts +++ b/packages/cli/src/lib/functions/dev.ts @@ -10,8 +10,8 @@ import { fail } from "../errors.js"; import { functionsRequest, resolveEntrypoint, - resolveFunctionsApiConfig, - type FunctionsApiConfig, + resolveFunctionsProjectConfig, + type FunctionsProjectConfig, } from "./shared.js"; const DEFAULT_RUNTIME_STARTUP_TIMEOUT_MS = 10_000; @@ -22,10 +22,15 @@ export interface StartFunctionsDevServerOptions { entrypoint: string; host: string; port: number; + projectId?: string; verbose: boolean; } interface InvocationContext { + invocation: { + id: string; + region: "local"; + }; session: { id: string; connectUrl: string; @@ -105,7 +110,13 @@ class InvocationBridge { sendJson( this.invokeConnection.response, 500, - { error: payload }, + { + error: { + message: payload.errorMessage, + stackTrace: payload.stackTrace, + type: payload.errorType, + }, + }, this.invokeConnection.corsHeaders, ); try { @@ -139,7 +150,7 @@ class InvocationBridge { "content-type": "application/json", "Lambda-Runtime-Aws-Request-Id": requestId, "Lambda-Runtime-Deadline-Ms": String(Date.now() + 300_000), - "Lambda-Runtime-Invoked-Function-Arn": `arn:aws:lambda:local:function:${functionName}`, + "Lambda-Runtime-Invoked-Function-Arn": `arn:aws:lambda:us-east-1:000000000000:function:${functionName}`, }); this.nextConnection.response.end( JSON.stringify({ @@ -171,7 +182,7 @@ class InvocationBridge { } class BrowserSessionManager { - constructor(private readonly config: FunctionsApiConfig) {} + constructor(private readonly config: FunctionsProjectConfig) {} async createSession( sessionConfig: Record = {}, @@ -181,7 +192,10 @@ class BrowserSessionManager { headers: { "content-type": "application/json", }, - body: JSON.stringify(sessionConfig), + body: JSON.stringify({ + projectId: this.config.projectId, + ...sessionConfig, + }), }); const session = (await response.json()) as { id?: string; @@ -202,7 +216,10 @@ class BrowserSessionManager { headers: { "content-type": "application/json", }, - body: JSON.stringify({ status: "REQUEST_RELEASE" }), + body: JSON.stringify({ + projectId: this.config.projectId, + status: "REQUEST_RELEASE", + }), }); } } @@ -330,7 +347,7 @@ export async function startFunctionsDevServer( fail("Port must be an integer between 1 and 65535."); } - const config = resolveFunctionsApiConfig(options); + const config = resolveFunctionsProjectConfig(options); const runtimeApi = `${options.host}:${options.port}`; const bridge = new InvocationBridge(); const sessionManager = new BrowserSessionManager(config); @@ -538,7 +555,13 @@ async function routeRequest( const accepted = bridge.triggerInvocation( functionName, params, - { session }, + { + invocation: { + id: randomUUID(), + region: "local", + }, + session, + }, corsHeaders, response, ); @@ -606,11 +629,12 @@ async function routeRequest( ); return; } - const completed = await bridge.completeWithError(requestId, { + const runtimeError = { errorMessage: payload?.errorMessage || "Unknown runtime error", errorType: payload?.errorType || "RuntimeError", stackTrace: Array.isArray(payload?.stackTrace) ? payload.stackTrace : [], - }); + }; + const completed = await bridge.completeWithError(requestId, runtimeError); sendJson( response, completed ? 202 : 400, diff --git a/packages/cli/src/lib/functions/init.ts b/packages/cli/src/lib/functions/init.ts index e772fc5d1..bf24dcb60 100644 --- a/packages/cli/src/lib/functions/init.ts +++ b/packages/cli/src/lib/functions/init.ts @@ -6,9 +6,10 @@ import { join, resolve } from "node:path"; import { fail } from "../errors.js"; const envTemplate = `# Browserbase Configuration -# Get your API key from https://browserbase.com/settings +# Get your API key and project ID from https://browserbase.com/settings BROWSERBASE_API_KEY=your_api_key_here +BROWSERBASE_PROJECT_ID=your_project_id_here `; const gitignoreTemplate = `node_modules/ @@ -68,7 +69,7 @@ export async function initFunctionsProject({ ); } - ensureCommand(packageManager); + const packageManagerVersion = ensureCommand(packageManager); const projectRoot = resolve(projectName); if (existsSync(projectRoot)) { @@ -79,7 +80,9 @@ export async function initFunctionsProject({ const packageJson = { name: projectName, + version: "1.0.0", private: true, + packageManager: `${packageManager}@${packageManagerVersion}`, type: "module", scripts: { dev: "browse functions dev index.ts", @@ -98,7 +101,7 @@ export async function initFunctionsProject({ runPackageManager( packageManager, - [...install, "@browserbasehq/sdk-functions", "playwright-core"], + [...install, "@browserbasehq/sdk-functions", "playwright-core", "zod"], projectRoot, ); runPackageManager(packageManager, [...installDev, "typescript", "@types/node"], projectRoot); @@ -118,7 +121,7 @@ export async function initFunctionsProject({ projectRoot, nextSteps: [ `cd ${projectName}`, - "Edit .env with your Browserbase API key", + "Edit .env with your Browserbase API key and project ID", packageManager === "pnpm" ? "pnpm dev" : "npm run dev", packageManager === "pnpm" ? "pnpm run deploy" : "npm run deploy", ], @@ -129,11 +132,15 @@ export async function initFunctionsProject({ ); } -function ensureCommand(command: string): void { - const result = spawnSync(command, ["--version"], { stdio: "ignore" }); +function ensureCommand(command: string): string { + const result = spawnSync(command, ["--version"], { + encoding: "utf8", + stdio: ["ignore", "pipe", "ignore"], + }); if (result.error || result.status !== 0) { fail(`${command} is required but was not found on PATH.`); } + return result.stdout.trim(); } function runPackageManager(packageManager: "npm" | "pnpm", args: string[], cwd: string): void { diff --git a/packages/cli/src/lib/functions/publish.ts b/packages/cli/src/lib/functions/publish.ts index 5ed5ed560..11ebd2e99 100644 --- a/packages/cli/src/lib/functions/publish.ts +++ b/packages/cli/src/lib/functions/publish.ts @@ -21,7 +21,7 @@ import { functionsRequest, pollUntil, resolveEntrypoint, - resolveFunctionsApiConfig, + resolveFunctionsProjectConfig, } from "./shared.js"; export interface PublishFunctionOptions { @@ -29,6 +29,7 @@ export interface PublishFunctionOptions { baseUrl?: string; dryRun: boolean; entrypoint: string; + projectId?: string; } interface BuildUploadResponse { @@ -67,9 +68,11 @@ const defaultIgnorePatterns = [ ".browserbase/", ]; +const maxArchiveSizeBytes = 50 * 1024 * 1024; + export async function publishFunction(options: PublishFunctionOptions): Promise { const entrypoint = await resolveEntrypoint(options.entrypoint); - const config = resolveFunctionsApiConfig(options); + const config = resolveFunctionsProjectConfig(options); const entrypointPath = relative(process.cwd(), entrypoint); if (options.dryRun) { @@ -82,6 +85,7 @@ export async function publishFunction(options: PublishFunctionOptions): Promise< dryRun: true, entrypoint: entrypointPath, files: entries, + projectId: config.projectId, }, null, 2, @@ -92,8 +96,18 @@ export async function publishFunction(options: PublishFunctionOptions): Promise< const { archivePath } = await createArchive(process.cwd()); try { + const archiveStats = await stat(archivePath); + if (archiveStats.size > maxArchiveSizeBytes) { + fail( + `Functions archive is ${(archiveStats.size / 1024 / 1024).toFixed(2)} MB; the maximum is 50 MB. Add files to .gitignore to reduce its size.`, + ); + } + const formData = new FormData(); - formData.append("metadata", JSON.stringify({ entrypoint: entrypointPath })); + formData.append( + "metadata", + JSON.stringify({ entrypoint: entrypointPath, projectId: config.projectId }), + ); formData.append( "archive", new Blob([await readFile(archivePath)], { type: "application/gzip" }), diff --git a/packages/cli/src/lib/functions/shared.ts b/packages/cli/src/lib/functions/shared.ts index 624d4c4c1..96ac0261c 100644 --- a/packages/cli/src/lib/functions/shared.ts +++ b/packages/cli/src/lib/functions/shared.ts @@ -12,6 +12,10 @@ export interface FunctionsApiConfig { baseUrl: string; } +export interface FunctionsProjectConfig extends FunctionsApiConfig { + projectId: string; +} + export interface PollOptions { done: (value: T) => boolean; intervalMs?: number; @@ -32,6 +36,34 @@ export function resolveFunctionsApiConfig(args: { }; } +export function resolveFunctionsProjectConfig(args: { + apiKey?: string; + baseUrl?: string; + projectId?: string; +}): FunctionsProjectConfig { + const apiConfig = resolveFunctionsApiConfig(args); + const projectId = args.projectId ?? process.env.BROWSERBASE_PROJECT_ID; + if (!projectId) { + fail( + [ + "Missing Browserbase project ID. Functions dev and publish need an explicit project.", + "Set BROWSERBASE_PROJECT_ID or pass --project-id.", + "Find your project ID at https://browserbase.com/settings.", + ].join("\n"), + 1, + { + resultCode: "missing_project_id", + requestHadHttpResponse: false, + }, + ); + } + + return { + ...apiConfig, + projectId, + }; +} + export async function functionsRequest( config: FunctionsApiConfig, path: string, diff --git a/packages/cli/tests/cli-functions-contract.test.ts b/packages/cli/tests/cli-functions-contract.test.ts index 75fabf1bd..ede065335 100644 --- a/packages/cli/tests/cli-functions-contract.test.ts +++ b/packages/cli/tests/cli-functions-contract.test.ts @@ -66,7 +66,17 @@ describe("functions API contracts", () => { }, async ({ baseUrl, requests }) => { const result = await runCli( - ["functions", "publish", "index.ts", "--api-key", "test-key", "--base-url", baseUrl], + [ + "functions", + "publish", + "index.ts", + "--api-key", + "test-key", + "--project-id", + "test-project", + "--api-url", + baseUrl, + ], { cwd }, ); @@ -78,6 +88,7 @@ describe("functions API contracts", () => { expectRequest(requests[0], "POST", "/v1/functions/builds", "test-key"); expect(requests[0]?.headers["content-type"]).toContain("multipart/form-data"); expect(requests[0]?.bodyText).toContain('"entrypoint":"index.ts"'); + expect(requests[0]?.bodyText).toContain('"projectId":"test-project"'); expectRequest(requests[1], "GET", "/v1/functions/builds/build_123", "test-key"); }, ); @@ -95,7 +106,16 @@ describe("functions API contracts", () => { await writeFile(join(cwd, ".browserbase", "functions", "manifests", "local.json"), "{}"); const result = await runCli( - ["functions", "publish", "index.ts", "--dry-run", "--api-key", "test-key"], + [ + "functions", + "publish", + "index.ts", + "--dry-run", + "--api-key", + "test-key", + "--project-id", + "test-project", + ], { cwd, env: { @@ -109,9 +129,11 @@ describe("functions API contracts", () => { dryRun: boolean; entrypoint: string; files: string[]; + projectId: string; }; expect(output.dryRun).toBe(true); expect(output.entrypoint).toBe("index.ts"); + expect(output.projectId).toBe("test-project"); expect(output.files).toContain("index.ts"); expect(output.files).toContain("package.json"); expect(output.files.some((file) => file.startsWith(".browserbase/"))).toBe(false); @@ -134,7 +156,17 @@ describe("functions API contracts", () => { }, async ({ baseUrl }) => { const result = await runCli( - ["functions", "publish", "index.ts", "--api-key", "test-key", "--base-url", baseUrl], + [ + "functions", + "publish", + "index.ts", + "--api-key", + "test-key", + "--project-id", + "test-project", + "--base-url", + baseUrl, + ], { cwd }, ); @@ -147,6 +179,22 @@ describe("functions API contracts", () => { ); }); + it("requires a project ID before publishing", async () => { + const cwd = await createFunctionFixture("functions-missing-project-"); + const result = await runCli( + ["functions", "publish", "index.ts", "--dry-run", "--api-key", "test-key"], + { + cwd, + env: { + BROWSERBASE_PROJECT_ID: "", + }, + }, + ); + + expect(result.exitCode).toBe(1); + expect(result.stderr).toContain("Missing Browserbase project ID"); + }); + it("invokes a deployed Function and polls invocation status", async () => { await withServer( async (request, response) => { @@ -180,7 +228,7 @@ describe("functions API contracts", () => { '{"url":"https://example.com"}', "--api-key", "test-key", - "--base-url", + "--api-url", baseUrl, ]); @@ -263,9 +311,17 @@ describe("functions scaffolding and local dev", () => { const entrypoint = await readFile(join(cwd, "demo-function", "index.ts"), "utf8"); expect(entrypoint).toContain('import { defineFn } from "@browserbasehq/sdk-functions";'); - expect(await readFile(join(cwd, "demo-function", ".env"), "utf8")).toContain( - "BROWSERBASE_API_KEY=", - ); + const packageJson = JSON.parse( + await readFile(join(cwd, "demo-function", "package.json"), "utf8"), + ) as { + packageManager?: string; + version?: string; + }; + expect(packageJson.packageManager).toBe("pnpm@10.0.0"); + expect(packageJson.version).toBe("1.0.0"); + const envFile = await readFile(join(cwd, "demo-function", ".env"), "utf8"); + expect(envFile).toContain("BROWSERBASE_API_KEY="); + expect(envFile).toContain("BROWSERBASE_PROJECT_ID="); }); it("runs a local dev server and invokes a function", async () => { @@ -305,6 +361,8 @@ describe("functions scaffolding and local dev", () => { String(port), "--api-key", "test-key", + "--project-id", + "test-project", "--base-url", baseUrl, ], @@ -352,12 +410,21 @@ describe("functions scaffolding and local dev", () => { await expect(invokeResponse.json()).resolves.toMatchObject({ ok: true, params: { answer: 42 }, + invocation: { + id: expect.any(String), + region: "local", + }, sessionId: "sess_123", }); await waitForRequests(requests, 2); expectRequest(requests[0], "POST", "/v1/sessions", "test-key"); + expect(requests[0]?.jsonBody).toMatchObject({ projectId: "test-project" }); expectRequest(requests[1], "POST", "/v1/sessions/sess_123", "test-key"); + expect(requests[1]?.jsonBody).toMatchObject({ + projectId: "test-project", + status: "REQUEST_RELEASE", + }); }, ); }, 30_000); @@ -398,6 +465,7 @@ describe("functions scaffolding and local dev", () => { cwd, env: { ...process.env, + BROWSERBASE_PROJECT_ID: "test-project", NODE_ENV: "test", }, stdio: ["ignore", "pipe", "pipe"], @@ -471,6 +539,7 @@ describe("functions scaffolding and local dev", () => { cwd, env: { ...process.env, + BROWSERBASE_PROJECT_ID: "test-project", BROWSERBASE_FUNCTIONS_DEV_STARTUP_TIMEOUT_MS: "0", NODE_ENV: "test", }, @@ -519,6 +588,7 @@ describe("functions scaffolding and local dev", () => { cwd, env: { ...process.env, + BROWSERBASE_PROJECT_ID: "test-project", NODE_ENV: "test", }, stdio: ["ignore", "pipe", "pipe"], @@ -591,6 +661,7 @@ describe("functions scaffolding and local dev", () => { cwd, env: { ...process.env, + BROWSERBASE_PROJECT_ID: "test-project", NODE_ENV: "test", }, stdio: ["ignore", "pipe", "pipe"], @@ -666,6 +737,7 @@ describe("functions scaffolding and local dev", () => { cwd, env: { ...process.env, + BROWSERBASE_PROJECT_ID: "test-project", NODE_ENV: "test", }, stdio: ["ignore", "pipe", "pipe"], @@ -680,7 +752,8 @@ describe("functions scaffolding and local dev", () => { expect(first.headers.get("access-control-allow-origin")).toBeNull(); await expect(first.json()).resolves.toMatchObject({ error: { - errorMessage: expect.stringContaining("Invalid runtime response payload"), + message: expect.stringContaining("Invalid runtime response payload"), + type: "RuntimeResponseError", }, }); await waitForFileText(runtimeStatusLog, "400\n"); @@ -689,7 +762,8 @@ describe("functions scaffolding and local dev", () => { expect(second.status).toBe(500); await expect(second.json()).resolves.toMatchObject({ error: { - errorMessage: expect.stringContaining("Invalid runtime response payload"), + message: expect.stringContaining("Invalid runtime response payload"), + type: "RuntimeResponseError", }, }); await waitForFileText(runtimeStatusLog, "400\n400\n"); @@ -725,6 +799,7 @@ describe("functions scaffolding and local dev", () => { cwd, env: { ...process.env, + BROWSERBASE_PROJECT_ID: "test-project", NODE_ENV: "test", }, stdio: ["ignore", "pipe", "pipe"], @@ -767,7 +842,7 @@ async function createTempDir(prefix: string): Promise { async function createFakePackageManagerBin( name = "pnpm", - contents = "#!/bin/sh\nexit 0\n", + contents = "#!/bin/sh\necho 10.0.0\n", ): Promise { const directory = await createTempDir("functions-fake-bin-"); const scriptPath = join(directory, name); @@ -820,6 +895,7 @@ while (true) { headers: { "content-type": "application/json" }, body: JSON.stringify({ ok: true, + invocation: event.context.invocation, params: event.params, sessionId: event.context.session.id, }), From 9e8a971337d774088f189f100116065b68909332 Mon Sep 17 00:00:00 2001 From: Shrey Pandya Date: Thu, 13 Aug 2026 05:09:35 +0000 Subject: [PATCH 2/2] fix(cli): infer Functions project from API key --- .changeset/warm-functions-parity.md | 2 +- packages/cli/README.md | 6 ++--- packages/cli/skills/browse/SKILL.md | 2 +- packages/cli/src/lib/functions/init.ts | 5 ++--- packages/cli/src/lib/functions/shared.ts | 22 ++----------------- .../cli/tests/cli-functions-contract.test.ts | 8 +++---- 6 files changed, 13 insertions(+), 32 deletions(-) diff --git a/.changeset/warm-functions-parity.md b/.changeset/warm-functions-parity.md index ece5c36ef..a29162f9d 100644 --- a/.changeset/warm-functions-parity.md +++ b/.changeset/warm-functions-parity.md @@ -2,4 +2,4 @@ "browse": patch --- -Align `browse functions` with the Browserbase Functions SDK by forwarding explicit project IDs during local development and publishing, matching local invocation context and error payloads, and accepting the SDK's `--api-url` option. +Align `browse functions` with the Browserbase Functions SDK by supporting optional project overrides, matching local invocation context and error payloads, and accepting the SDK's `--api-url` option. diff --git a/packages/cli/README.md b/packages/cli/README.md index 0878b1975..a1daeea56 100644 --- a/packages/cli/README.md +++ b/packages/cli/README.md @@ -256,9 +256,9 @@ browse functions invoke --params '{"url":"https://example.com"}' browse functions invoke --check-status ``` -Set both `BROWSERBASE_API_KEY` and `BROWSERBASE_PROJECT_ID` for `functions dev` and -`functions publish`. `functions invoke` only requires `BROWSERBASE_API_KEY`. You can also pass -`--api-key` and `--project-id` explicitly; `--api-url` is accepted as an alias for `--base-url`. +Set `BROWSERBASE_API_KEY` for Functions commands. Browserbase infers the project +from the key; `--project-id` remains available as an explicit override, and +`--api-url` is accepted as an alias for `--base-url`. ## Templates diff --git a/packages/cli/skills/browse/SKILL.md b/packages/cli/skills/browse/SKILL.md index 1126ad960..2f7d05771 100644 --- a/packages/cli/skills/browse/SKILL.md +++ b/packages/cli/skills/browse/SKILL.md @@ -277,7 +277,7 @@ browse functions invoke --params '{"url":"https://example.com"}' browse functions invoke --check-status ``` -Functions commands use `BROWSERBASE_API_KEY`; local development and publishing also require `BROWSERBASE_PROJECT_ID`. Generated projects import `defineFn` from `@browserbasehq/sdk-functions`. +Functions commands use `BROWSERBASE_API_KEY`, and Browserbase infers the project from the key. Generated projects import `defineFn` from `@browserbasehq/sdk-functions`. ## Templates diff --git a/packages/cli/src/lib/functions/init.ts b/packages/cli/src/lib/functions/init.ts index bf24dcb60..70eb4f0d2 100644 --- a/packages/cli/src/lib/functions/init.ts +++ b/packages/cli/src/lib/functions/init.ts @@ -6,10 +6,9 @@ import { join, resolve } from "node:path"; import { fail } from "../errors.js"; const envTemplate = `# Browserbase Configuration -# Get your API key and project ID from https://browserbase.com/settings +# Get your API key from https://browserbase.com/settings BROWSERBASE_API_KEY=your_api_key_here -BROWSERBASE_PROJECT_ID=your_project_id_here `; const gitignoreTemplate = `node_modules/ @@ -121,7 +120,7 @@ export async function initFunctionsProject({ projectRoot, nextSteps: [ `cd ${projectName}`, - "Edit .env with your Browserbase API key and project ID", + "Edit .env with your Browserbase API key", packageManager === "pnpm" ? "pnpm dev" : "npm run dev", packageManager === "pnpm" ? "pnpm run deploy" : "npm run deploy", ], diff --git a/packages/cli/src/lib/functions/shared.ts b/packages/cli/src/lib/functions/shared.ts index 96ac0261c..caa53a2fe 100644 --- a/packages/cli/src/lib/functions/shared.ts +++ b/packages/cli/src/lib/functions/shared.ts @@ -13,7 +13,7 @@ export interface FunctionsApiConfig { } export interface FunctionsProjectConfig extends FunctionsApiConfig { - projectId: string; + projectId?: string; } export interface PollOptions { @@ -43,25 +43,7 @@ export function resolveFunctionsProjectConfig(args: { }): FunctionsProjectConfig { const apiConfig = resolveFunctionsApiConfig(args); const projectId = args.projectId ?? process.env.BROWSERBASE_PROJECT_ID; - if (!projectId) { - fail( - [ - "Missing Browserbase project ID. Functions dev and publish need an explicit project.", - "Set BROWSERBASE_PROJECT_ID or pass --project-id.", - "Find your project ID at https://browserbase.com/settings.", - ].join("\n"), - 1, - { - resultCode: "missing_project_id", - requestHadHttpResponse: false, - }, - ); - } - - return { - ...apiConfig, - projectId, - }; + return projectId ? { ...apiConfig, projectId } : apiConfig; } export async function functionsRequest( diff --git a/packages/cli/tests/cli-functions-contract.test.ts b/packages/cli/tests/cli-functions-contract.test.ts index ede065335..d963dad88 100644 --- a/packages/cli/tests/cli-functions-contract.test.ts +++ b/packages/cli/tests/cli-functions-contract.test.ts @@ -179,7 +179,7 @@ describe("functions API contracts", () => { ); }); - it("requires a project ID before publishing", async () => { + it("infers the project when no project ID is provided", async () => { const cwd = await createFunctionFixture("functions-missing-project-"); const result = await runCli( ["functions", "publish", "index.ts", "--dry-run", "--api-key", "test-key"], @@ -191,8 +191,8 @@ describe("functions API contracts", () => { }, ); - expect(result.exitCode).toBe(1); - expect(result.stderr).toContain("Missing Browserbase project ID"); + expect(result.exitCode).toBe(0); + expect(JSON.parse(result.stdout)).not.toHaveProperty("projectId"); }); it("invokes a deployed Function and polls invocation status", async () => { @@ -321,7 +321,7 @@ describe("functions scaffolding and local dev", () => { expect(packageJson.version).toBe("1.0.0"); const envFile = await readFile(join(cwd, "demo-function", ".env"), "utf8"); expect(envFile).toContain("BROWSERBASE_API_KEY="); - expect(envFile).toContain("BROWSERBASE_PROJECT_ID="); + expect(envFile).not.toContain("BROWSERBASE_PROJECT_ID="); }); it("runs a local dev server and invokes a function", async () => {