Skip to content
Merged
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
85 changes: 67 additions & 18 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,12 @@ Add your Browserbase API key to `.env`:
BROWSERBASE_API_KEY=your_api_key_here
```

The starter function uses [Stagehand](https://docs.stagehand.dev), which needs its extension in the browser session. Upload the extension then paste the returned `id` into `extensionId` in `index.ts`:

```sh
browse cloud extensions upload node_modules/@browserbasehq/stagehand/dist/assets/stagehand-extension.zip
```

Start the local development server:

```sh
Expand All @@ -45,6 +51,8 @@ When ready, publish to Browserbase:
pnpm bb publish index.ts
```

Then [attach your API key as a secret](#secrets) to the published function.

## Usage

### Basic Function
Expand All @@ -59,23 +67,38 @@ defineFn("hello-world", async () => {

### Browser Automation

Every function receives a `context` with a managed browser session. Connect to it with Playwright:
Every function receives a `context` with a managed browser session. Connect [Stagehand](https://docs.stagehand.dev) to it by session ID:

```ts
import { defineFn } from "@browserbasehq/sdk-functions";
import { chromium } from "playwright-core";
import { browserbase, Stagehand } from "@browserbasehq/stagehand";
import { z } from "zod/v4";

defineFn("scrape-titles", async (context) => {
const browser = await chromium.connectOverCDP(context.session.connectUrl);
const page = browser.contexts()[0]!.pages()[0]!;

await page.goto("https://news.ycombinator.com");
const titles = await page.$$eval(".titleline > a", (els) =>
els.slice(0, 5).map((el) => el.textContent),
);

return { titles };
});
defineFn(
"scrape-titles",
async (context) => {
const browser = await browserbase.connect({
// The local dev server has no secrets, so fall back to .env.
apiKey:
context.secrets.BROWSERBASE_API_KEY ?? process.env.BROWSERBASE_API_KEY!,
sessionId: context.session.id,
});
// In this example, Stagehand uses the Model Gateway where Browserbase charges for the tokens
const stagehand = await Stagehand.create({ browser });
const page = (await browser.context.activePage())!;

await page.goto("https://news.ycombinator.com");
const { data } = await stagehand.extract(
"Extract the titles of the top 5 stories",
z.object({ titles: z.array(z.string()).max(5) }),
);

await stagehand.close();
return { titles: data.titles };
},
// The ID from `browse cloud extensions upload`. Stagehand needs its extension in the session.
{ sessionConfig: { extensionId: "your-extension-id" } },
);
```

### Parameter Validation
Expand Down Expand Up @@ -106,25 +129,51 @@ Pass `sessionConfig` to customize the browser session (uses the same options as

```ts
import { defineFn } from "@browserbasehq/sdk-functions";
import { chromium } from "playwright-core";
import { browserbase, Stagehand } from "@browserbasehq/stagehand";
import { z } from "zod/v4";

defineFn(
"stealth-scraper",
async (context) => {
const browser = await chromium.connectOverCDP(context.session.connectUrl);
const page = browser.contexts()[0]!.pages()[0]!;
const browser = await browserbase.connect({
apiKey:
context.secrets.BROWSERBASE_API_KEY ?? process.env.BROWSERBASE_API_KEY!,
sessionId: context.session.id,
});
const stagehand = await Stagehand.create({ browser });
const page = (await browser.context.activePage())!;

await page.goto("https://example.com");
return { content: await page.textContent("body") };
const { data } = await stagehand.extract(
"Extract the main text of the page",
z.object({ content: z.string() }),
);

await stagehand.close();
return { content: data.content };
},
{
sessionConfig: {
extensionId: "your-extension-id", // Stagehand's extension
browserSettings: { advancedStealth: true },
},
},
);
```

### Secrets

Keep API keys in encrypted project secrets. Each secret attached to a function is available as `context.secrets[name]`. The Stagehand examples read `BROWSERBASE_API_KEY` this way. They don't need a model API key: without a `model` option, Stagehand uses the [Browserbase Model Gateway](https://docs.stagehand.dev/v4/configuration/models#model-gateway), which picks a model for each call. Browserbase charges for the tokens.

Create the secret and attach it to the published function with the [`browse` CLI](https://www.npmjs.com/package/browse):

```sh
browse cloud secrets create BROWSERBASE_API_KEY --env BROWSERBASE_API_KEY
browse functions secrets attach <functionId> <secretId>
```

Use the Function ID from `builtFunctions[].id` in the publish output. The local development server doesn't pass secrets, so the examples fall back to the values in `.env`.

## CLI Reference

The `bb` CLI is included with the package.
Expand Down Expand Up @@ -187,7 +236,7 @@ Options:

## Configuration

Set your Browserbase API key as an environment variable or in a `.env` file:
Set variables in your environment or in a `.env` file for the CLI and the local development server. Deployed functions read them from [secrets](#secrets) instead.

| Variable | Required | Description |
| --------------------- | -------- | ------------------------ |
Expand Down
110 changes: 90 additions & 20 deletions src/cli/init/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,9 @@ export async function init(options: InitOptions) {
// Step 5: Detect and update package manager
const packageManager = detectPackageManager(options.packageManager);
updatePackageManager(targetDir, packageManager);
if (packageManager === "pnpm") {
createPnpmWorkspaceFile(targetDir);
}

// Step 6: Install dependencies
console.log(chalk.gray("Installing dependencies..."));
Expand Down Expand Up @@ -107,21 +110,37 @@ export async function init(options: InitOptions) {
console.log(chalk.cyan("Next steps:"));
console.log(chalk.gray("1. Navigate to your project:"));
console.log(chalk.white(` cd ${options.projectName}`));
console.log(chalk.gray("2. Add your Browserbase API key to .env"));
console.log(
chalk.gray("2. Add your Browserbase API key and project ID to .env"),
chalk.gray(
"3. Upload the Stagehand extension, then paste its ID into index.ts:",
),
);
console.log(chalk.gray("3. Run your function locally:"));
console.log(
chalk.white(
" browse cloud extensions upload node_modules/@browserbasehq/stagehand/dist/assets/stagehand-extension.zip",
),
);
console.log(chalk.gray("4. Run your function locally:"));
console.log(
chalk.white(
` ${packageManager === "pnpm" ? "pnpm" : "npx"} bb dev index.ts`,
),
);
console.log(chalk.gray("4. When ready, publish your function:"));
console.log(chalk.gray("5. When ready, publish your function:"));
console.log(
chalk.white(
` ${packageManager === "pnpm" ? "pnpm" : "npx"} bb publish index.ts`,
),
);
console.log(
chalk.gray(
"6. Create a BROWSERBASE_API_KEY project secret and attach it to the function:",
),
);
console.log(
chalk.white(" browse functions secrets attach <functionId> <secretId>"),
);
console.log("");
console.log(chalk.gray("Learn more at https://browserbase.com/docs"));
} catch (error) {
Expand Down Expand Up @@ -192,6 +211,9 @@ function updatePackageManager(
// Add "type": "module" to support ES modules
packageJson.type = "module";

// pnpm 11 init adds devEngines.packageManager. npm rejects it, and it conflicts with packageManager.
delete packageJson.devEngines;

writeFileSync(packageJsonPath, JSON.stringify(packageJson, null, 2));
console.log(
chalk.green(`✓ Package manager set to ${packageJson.packageManager}`),
Expand All @@ -208,29 +230,77 @@ function installDependencies(

// Install regular dependencies
console.log(chalk.gray(" Installing @browserbasehq/sdk-functions..."));
execSync(`${installCmd} @browserbasehq/sdk-functions`, {
cwd: targetDir,
stdio: "pipe",
});
runInstallCommand(`${installCmd} @browserbasehq/sdk-functions`, targetDir);

console.log(chalk.gray(" Installing playwright-core..."));
execSync(`${installCmd} playwright-core`, {
cwd: targetDir,
stdio: "pipe",
});
console.log(chalk.gray(" Installing @browserbasehq/stagehand..."));
runInstallCommand(`${installCmd} @browserbasehq/stagehand`, targetDir);

// Match Stagehand's version of zod in order to pass type checks.
const zodVersion = readStagehandZodVersion(targetDir);
console.log(chalk.gray(" Installing zod..."));
execSync(`${installCmd} zod`, {
cwd: targetDir,
stdio: "pipe",
});
runInstallCommand(
`${installCmd} ${zodVersion ? `zod@${zodVersion}` : "zod"}`,
targetDir,
);

// Install dev dependencies
console.log(chalk.gray(" Installing TypeScript and type definitions..."));
execSync(`${installDevCmd} typescript @types/node`, {
cwd: targetDir,
stdio: "pipe",
});
runInstallCommand(`${installDevCmd} typescript @types/node`, targetDir);
}

function readStagehandZodVersion(targetDir: string): string | undefined {
try {
const stagehandPackageJson = JSON.parse(
readFileSync(
join(
targetDir,
"node_modules",
"@browserbasehq",
"stagehand",
"package.json",
),
"utf-8",
),
);
const version = stagehandPackageJson.dependencies?.zod;
return typeof version === "string" ? version : undefined;
} catch {
return undefined;
}
}

function runInstallCommand(command: string, cwd: string) {
try {
execSync(command, { cwd, stdio: "pipe" });
} catch (error) {
// execSync's message has only stderr. The package manager often writes the real error to stdout.
const { stdout, stderr } = error as { stdout?: Buffer; stderr?: Buffer };
const output = [stdout, stderr]
.map((stream) => stream?.toString().trim())
.filter(Boolean)
.join("\n");
throw new Error(
`Command failed: ${command}${output ? `\n${output}` : ""}`,
{
cause: error,
},
);
}
}

function createPnpmWorkspaceFile(targetDir: string) {
const workspacePath = join(targetDir, "pnpm-workspace.yaml");
if (!existsSync(workspacePath)) {
const templatePath = join(
__dirname,
"templates",
"pnpm-workspace.yaml.template",
);
copyFileSync(templatePath, workspacePath);
console.log(chalk.green("✓ pnpm-workspace.yaml file created"));
} else {
console.log(chalk.yellow("✓ pnpm-workspace.yaml file already exists"));
}
}

function createEnvFile(targetDir: string) {
Expand Down
3 changes: 1 addition & 2 deletions src/cli/init/templates/.env.template
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
# Browserbase Configuration
# Get your API key from https://browserbase.com
# Get your API key from https://browserbase.com/settings

# Your Browserbase API key
BROWSERBASE_API_KEY=your_api_key_here
3 changes: 3 additions & 0 deletions src/cli/init/templates/pnpm-workspace.yaml.template
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# pnpm 11 and later fail installs when esbuild's build script is not approved.
allowBuilds:
esbuild: true
Loading
Loading