browse 0.9.4 → 0.9.5-alpha-04c8ee4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -277,10 +277,19 @@ export BROWSERBASE_API_KEY=bb_live_...
277
277
 
278
278
  Local driver commands (`--local`) work without an API key.
279
279
 
280
+ `browse` also auto-loads a `.env` file from the current directory on startup, and prints a
281
+ one-time deprecation warning to stderr when it actually pulls a variable from `.env` this way.
282
+ This is deprecated because a CLI used across many unrelated projects can silently pick up a
283
+ stale key from the wrong project's `.env`; a future release will disable auto-loading by
284
+ default so `browse` only reads `process.env`. Set `BROWSE_LOAD_DOTENV=0` (or `false`/`no`) to
285
+ opt out of `.env` auto-loading now, ahead of that change, or `BROWSE_LOAD_DOTENV=1` to keep
286
+ auto-loading with no warning.
287
+
280
288
  | Variable | Description |
281
289
  |----------|-------------|
282
290
  | `BROWSERBASE_API_KEY` | Enables `--remote` sessions and all `browse cloud` / `functions` commands |
283
291
  | `BROWSE_SESSION` | Default session name (alternative to `-s, --session`) |
292
+ | `BROWSE_LOAD_DOTENV` | Controls `.env` auto-loading: unset = load + warn once (default, deprecated), `0`/`false`/`no` = skip loading, anything else = load silently |
284
293
 
285
294
  ## Links
286
295
 
package/bin/run.js CHANGED
@@ -1,5 +1,31 @@
1
1
  #!/usr/bin/env node
2
- import "dotenv/config";
2
+ import { config as loadDotenvConfig } from "dotenv";
3
+
4
+ // `BROWSE_LOAD_DOTENV` controls whether `browse` auto-loads a `.env` file
5
+ // from the current working directory. This is deprecated: a future release
6
+ // will flip the default to "off" so `browse` only reads `process.env`,
7
+ // matching how most CLIs used across many unrelated projects behave. Until
8
+ // then we keep loading `.env` by default (unset behaves exactly as before)
9
+ // but warn once when we actually pull a value from it, so users relying on
10
+ // the implicit default get a heads-up before the default changes.
11
+ const dotenvToggle = process.env.BROWSE_LOAD_DOTENV;
12
+ const dotenvOptedOut =
13
+ dotenvToggle !== undefined &&
14
+ ["0", "false", "no"].includes(dotenvToggle.toLowerCase());
15
+
16
+ if (!dotenvOptedOut) {
17
+ const keysBeforeLoad = new Set(Object.keys(process.env));
18
+ const { parsed } = loadDotenvConfig();
19
+ const appliedFromDotenv = Object.keys(parsed ?? {}).filter(
20
+ (key) => !keysBeforeLoad.has(key),
21
+ );
22
+
23
+ if (appliedFromDotenv.length > 0 && dotenvToggle === undefined) {
24
+ console.error(
25
+ `[browse] Loaded ${appliedFromDotenv.join(", ")} from .env. Auto-loading .env is deprecated and will be disabled by default in a future release -- export these variables in your shell instead, or set BROWSE_LOAD_DOTENV=1 to keep this behavior explicitly once that happens. Set BROWSE_LOAD_DOTENV=0 to opt out today. Run \`browse doctor\` to check for conflicts with your shell environment.`,
26
+ );
27
+ }
28
+ }
3
29
 
4
30
  globalThis.oclif = {
5
31
  ...globalThis.oclif,
@@ -1,19 +1,46 @@
1
- import { Args } from "@oclif/core";
1
+ import { Args, Flags } from "@oclif/core";
2
2
  import { createBrowserbaseClient, outputJson, withBrowserbaseApi, } from "../../../lib/cloud/api.js";
3
3
  import { apiCommonFlags, toApiOptions } from "../../../lib/cloud/flags.js";
4
+ import { reduceLogs } from "../../../lib/cloud/reduce-logs.js";
4
5
  import { BrowseCommand } from "../../../base.js";
5
6
  export default class SessionsLogs extends BrowseCommand {
6
7
  static description = "Get Browserbase session logs.";
7
- static examples = ["browse cloud sessions logs <session-id>"];
8
+ static examples = [
9
+ "browse cloud sessions logs <session-id>",
10
+ "browse cloud sessions logs <session-id> --only-errors",
11
+ "browse cloud sessions logs <session-id> --only-errors --failed-requests",
12
+ ];
8
13
  static args = {
9
14
  id: Args.string({ required: true, description: "Session ID." }),
10
15
  };
11
- static flags = { ...apiCommonFlags };
16
+ static flags = {
17
+ ...apiCommonFlags,
18
+ "only-errors": Flags.boolean({
19
+ description: "Return only the high-signal error records (console errors/exceptions, HTTP 4xx/5xx, network failures) instead of the full CDP firehose.",
20
+ default: false,
21
+ }),
22
+ "failed-requests": Flags.boolean({
23
+ description: "With --only-errors, narrow to failed / error-status network requests only.",
24
+ default: false,
25
+ dependsOn: ["only-errors"],
26
+ }),
27
+ };
12
28
  async run() {
13
29
  const { args, flags } = await this.parse(SessionsLogs);
14
30
  await withBrowserbaseApi("sessions", async () => {
15
31
  const client = createBrowserbaseClient(toApiOptions(flags));
16
- outputJson(await client.sessions.logs.list(args.id));
32
+ const raw = await client.sessions.logs.list(args.id);
33
+ if (flags["only-errors"]) {
34
+ const arr = Array.isArray(raw)
35
+ ? raw
36
+ : (raw?.data ?? []);
37
+ outputJson(reduceLogs(arr, {
38
+ failedRequests: flags["failed-requests"],
39
+ }));
40
+ }
41
+ else {
42
+ outputJson(raw);
43
+ }
17
44
  });
18
45
  }
19
46
  }
@@ -0,0 +1,20 @@
1
+ import { readFile } from "node:fs/promises";
2
+ import { join } from "node:path";
3
+ import { BrowseCommand } from "../../base.js";
4
+ import { fail } from "../../lib/errors.js";
5
+ import { bundledCliSkillPath } from "../../lib/skills/install.js";
6
+ export default class SkillsShow extends BrowseCommand {
7
+ static description = "Print the bundled browse skill (usage patterns, workflows, gotchas) to stdout. Run before using browse in an agent.";
8
+ static examples = ["browse skills show"];
9
+ async run() {
10
+ const skillMdPath = join(bundledCliSkillPath(), "SKILL.md");
11
+ let contents;
12
+ try {
13
+ contents = await readFile(skillMdPath, "utf8");
14
+ }
15
+ catch (error) {
16
+ fail(`Could not read the bundled browse skill (SKILL.md): ${error.message}`, 1, { resultCode: "skill_show_missing" });
17
+ }
18
+ this.log(contents);
19
+ }
20
+ }
package/dist/help.js ADDED
@@ -0,0 +1,19 @@
1
+ import { Help } from "@oclif/core";
2
+ // Custom root-help formatter so `browse --help` (and bare `browse`) lead with
3
+ // a short block pointing agents at the bundled skill, ahead of oclif's
4
+ // auto-generated VERSION/USAGE/DESCRIPTION/COMMANDS sections. Only the root
5
+ // help view is customized -- individual `browse <command> --help` views are
6
+ // unaffected.
7
+ const AGENT_HEADER = `Start here (for AI agents):
8
+ browse skills show
9
+
10
+ The bundled skill ships with the CLI (always version-matched) and covers
11
+ workflows, session management, and common failure recovery. Prefer it over
12
+ guessing commands from flag docs alone.
13
+ `;
14
+ export default class BrowseHelp extends Help {
15
+ async showRootHelp() {
16
+ this.log(AGENT_HEADER);
17
+ await super.showRootHelp();
18
+ }
19
+ }
@@ -0,0 +1,107 @@
1
+ // Deterministic reducer for `browse cloud sessions logs --only-errors`.
2
+ // Turns the raw CDP firehose (~hundreds of events per session) into the high-signal error slice:
3
+ // console errors/warnings/asserts, uncaught exceptions, HTTP 4xx/5xx responses, and net-level load
4
+ // failures. No LLM — pure allowlist + severity/status filter + field projection + dedupe + stack trim.
5
+ function paramsOf(e) {
6
+ try {
7
+ const parsed = JSON.parse(e.request?.rawBody ?? "{}");
8
+ return parsed.params ?? {};
9
+ }
10
+ catch {
11
+ return {};
12
+ }
13
+ }
14
+ // Keep the message line + the app (`/src/`) stack frames; drop framework/vendor frames and hosts.
15
+ function trimStack(s) {
16
+ return (s || "")
17
+ .split("\n")
18
+ .filter((l, i) => i === 0 || (/\/src\//.test(l) && !/node_modules|\.vite/.test(l)))
19
+ .slice(0, 4)
20
+ .map((l) => l
21
+ .replace(/https?:\/\/[^/)]+/g, "")
22
+ .replace(/\?[^):]*/, "")
23
+ .trim())
24
+ .join("\n");
25
+ }
26
+ export function reduceLogs(raw, opts = {}) {
27
+ const out = [];
28
+ const seen = new Set();
29
+ const push = (rec) => {
30
+ const k = JSON.stringify(rec);
31
+ if (!seen.has(k)) {
32
+ seen.add(k);
33
+ out.push(rec);
34
+ }
35
+ };
36
+ for (const e of raw) {
37
+ const p = paramsOf(e);
38
+ const m = e.method;
39
+ const responseStatus = p.response?.status;
40
+ let rec = null;
41
+ if (m === "Runtime.consoleAPICalled" &&
42
+ typeof p.type === "string" &&
43
+ ["error", "warning", "assert"].includes(p.type)) {
44
+ const text = (Array.isArray(p.args) ? p.args : [])
45
+ .map((a) => a && typeof a === "object" ? a.description || a.value || "" : "")
46
+ .filter(Boolean)
47
+ .join(" ");
48
+ if (text && !/^%[os]/.test(text))
49
+ rec = {
50
+ kind: `console.${p.type}`,
51
+ domain: "Runtime",
52
+ severity: p.type,
53
+ text: trimStack(text),
54
+ };
55
+ }
56
+ else if (m === "Runtime.exceptionThrown") {
57
+ rec = {
58
+ kind: "exception",
59
+ domain: "Runtime",
60
+ severity: "error",
61
+ text: trimStack(p.exceptionDetails?.exception?.description ??
62
+ p.exceptionDetails?.text ??
63
+ ""),
64
+ };
65
+ }
66
+ else if (m === "Log.entryAdded" &&
67
+ typeof p.entry?.level === "string" &&
68
+ ["error", "warning"].includes(p.entry?.level)) {
69
+ rec = {
70
+ kind: `log.${p.entry.level}`,
71
+ domain: "Log",
72
+ severity: p.entry.level,
73
+ text: p.entry.text,
74
+ url: p.entry.url,
75
+ };
76
+ }
77
+ else if (m === "Network.responseReceived" &&
78
+ typeof responseStatus === "number" &&
79
+ responseStatus >= 400) {
80
+ rec = {
81
+ kind: "network",
82
+ domain: "Network",
83
+ status: responseStatus,
84
+ url: p.response?.url,
85
+ type: p.type,
86
+ };
87
+ }
88
+ else if (m === "Network.loadingFailed" &&
89
+ p.errorText !== "net::ERR_ABORTED") {
90
+ rec = {
91
+ kind: "network.failed",
92
+ domain: "Network",
93
+ error: p.errorText,
94
+ type: p.type,
95
+ };
96
+ }
97
+ else {
98
+ continue; // everything else (byte-chunk / lifecycle events) is noise
99
+ }
100
+ if (!rec)
101
+ continue; // e.g. a console.error whose text was empty / formatting noise
102
+ if (opts.failedRequests && rec.domain !== "Network")
103
+ continue;
104
+ push(rec);
105
+ }
106
+ return out;
107
+ }
@@ -70,6 +70,12 @@ export async function stopDriverDaemon(session, force = false) {
70
70
  });
71
71
  }
72
72
  catch (error) {
73
+ if (!force &&
74
+ error instanceof CommandFailure &&
75
+ error.telemetry.resultCode === "daemon_not_running") {
76
+ await cleanupStoppedDaemonFiles(session);
77
+ return { stopped: false };
78
+ }
73
79
  if (!force)
74
80
  throw error;
75
81
  await cleanupDaemonFiles(session);
@@ -154,6 +160,10 @@ async function sendDriverRequest(session, request) {
154
160
  }
155
161
  });
156
162
  socket.on("error", (error) => {
163
+ if (isDaemonUnavailableError(error)) {
164
+ failRequest(daemonNotRunningError(session, request));
165
+ return;
166
+ }
157
167
  failRequest(error);
158
168
  });
159
169
  socket.on("end", () => {
@@ -219,6 +229,20 @@ async function cleanupStaleDaemonFiles(session) {
219
229
  return;
220
230
  await cleanupDaemonFiles(session, { includeLock: false });
221
231
  }
232
+ async function cleanupStoppedDaemonFiles(session) {
233
+ const locked = await acquireLock(session);
234
+ if (!locked)
235
+ return;
236
+ try {
237
+ const status = await tryDriverStatus(session);
238
+ if (status?.session === session)
239
+ return;
240
+ await cleanupDaemonFiles(session, { includeLock: false });
241
+ }
242
+ finally {
243
+ await releaseLock(session);
244
+ }
245
+ }
222
246
  async function acquireLock(session, timeoutMs = 10_000) {
223
247
  const lockPath = getLockPath(session);
224
248
  const start = Date.now();
@@ -281,3 +305,20 @@ async function removeStaleLock(lockPath) {
281
305
  function requestId() {
282
306
  return `${process.pid}-${Date.now()}-${Math.random().toString(16).slice(2)}`;
283
307
  }
308
+ function isDaemonUnavailableError(error) {
309
+ const code = error.code;
310
+ return code === "ECONNREFUSED" || code === "ENOENT";
311
+ }
312
+ function daemonNotRunningError(session, request) {
313
+ const sessionFlag = session === "default" ? "" : ` --session ${formatCommandArgument(session)}`;
314
+ const startCommand = request.type === "open"
315
+ ? `browse open ${formatCommandArgument(request.url)}${sessionFlag}`
316
+ : `browse open <url>${sessionFlag}`;
317
+ return new CommandFailure(`Driver daemon session "${session}" is not running. Start it with: ${startCommand}`, 1, { resultCode: "daemon_not_running" });
318
+ }
319
+ function formatCommandArgument(value) {
320
+ if (/^[A-Za-z0-9_./:@%+=,-]+$/.test(value))
321
+ return value;
322
+ const escaped = value.replaceAll("'", "'\"'\"'");
323
+ return `'${escaped}'`;
324
+ }
@@ -167,7 +167,7 @@ function localSkillPath(skillId) {
167
167
  const configHome = process.env.XDG_CONFIG_HOME || join(homedir(), ".config");
168
168
  return join(configHome, "browserbase", "skills", skillId.domain, skillId.task);
169
169
  }
170
- function bundledCliSkillPath() {
170
+ export function bundledCliSkillPath() {
171
171
  return join(packageRoot(), "skills", "browse");
172
172
  }
173
173
  function packageRoot() {
@@ -4536,6 +4536,29 @@
4536
4536
  "list.js"
4537
4537
  ]
4538
4538
  },
4539
+ "skills:show": {
4540
+ "aliases": [],
4541
+ "args": {},
4542
+ "description": "Print the bundled browse skill (usage patterns, workflows, gotchas) to stdout. Run before using browse in an agent.",
4543
+ "examples": [
4544
+ "browse skills show"
4545
+ ],
4546
+ "flags": {},
4547
+ "hasDynamicHelp": false,
4548
+ "hiddenAliases": [],
4549
+ "id": "skills:show",
4550
+ "pluginAlias": "browse",
4551
+ "pluginName": "browse",
4552
+ "pluginType": "core",
4553
+ "strict": true,
4554
+ "isESM": true,
4555
+ "relativePath": [
4556
+ "dist",
4557
+ "commands",
4558
+ "skills",
4559
+ "show.js"
4560
+ ]
4561
+ },
4539
4562
  "tab:close": {
4540
4563
  "aliases": [],
4541
4564
  "args": {
@@ -6177,7 +6200,9 @@
6177
6200
  },
6178
6201
  "description": "Get Browserbase session logs.",
6179
6202
  "examples": [
6180
- "browse cloud sessions logs <session-id>"
6203
+ "browse cloud sessions logs <session-id>",
6204
+ "browse cloud sessions logs <session-id> --only-errors",
6205
+ "browse cloud sessions logs <session-id> --only-errors --failed-requests"
6181
6206
  ],
6182
6207
  "flags": {
6183
6208
  "api-key": {
@@ -6195,6 +6220,21 @@
6195
6220
  "helpValue": "<baseUrl>",
6196
6221
  "multiple": false,
6197
6222
  "type": "option"
6223
+ },
6224
+ "only-errors": {
6225
+ "description": "Return only the high-signal error records (console errors/exceptions, HTTP 4xx/5xx, network failures) instead of the full CDP firehose.",
6226
+ "name": "only-errors",
6227
+ "allowNo": false,
6228
+ "type": "boolean"
6229
+ },
6230
+ "failed-requests": {
6231
+ "dependsOn": [
6232
+ "only-errors"
6233
+ ],
6234
+ "description": "With --only-errors, narrow to failed / error-status network requests only.",
6235
+ "name": "failed-requests",
6236
+ "allowNo": false,
6237
+ "type": "boolean"
6198
6238
  }
6199
6239
  },
6200
6240
  "hasDynamicHelp": false,
@@ -6398,5 +6438,5 @@
6398
6438
  ]
6399
6439
  }
6400
6440
  },
6401
- "version": "0.9.4"
6441
+ "version": "0.9.5"
6402
6442
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "browse",
3
- "version": "0.9.4",
3
+ "version": "0.9.5-alpha-04c8ee4",
4
4
  "description": "Unified Browserbase CLI for browser automation and cloud APIs.",
5
5
  "type": "module",
6
6
  "private": false,
@@ -24,6 +24,7 @@
24
24
  "bin": "browse",
25
25
  "dirname": "browse",
26
26
  "commands": "./dist/commands",
27
+ "helpClass": "./dist/help.js",
27
28
  "hooks": {
28
29
  "init": "./dist/hooks/init.js",
29
30
  "prerun": "./dist/hooks/prerun.js",
@@ -100,7 +101,7 @@
100
101
  "tsx": "^4.20.6",
101
102
  "ws": "^8.18.3",
102
103
  "zod": "^4.2.1",
103
- "@browserbasehq/stagehand": "3.6.0"
104
+ "@browserbasehq/stagehand": "3.7.1"
104
105
  },
105
106
  "devDependencies": {
106
107
  "@eslint/js": "^10.0.1",
@@ -65,12 +65,16 @@ browse open https://example.com --cdp ws://127.0.0.1:9222/devtools/browser/<id>
65
65
 
66
66
  Use local mode for development, localhost, trusted sites, and fast iteration. Use `--auto-connect` only when the user explicitly wants to attach to an already-running debuggable Chrome session with existing cookies or login state; use `--local` when no debuggable Chrome is available. Use remote mode when Browserbase credentials are available and the site needs hosted browser infrastructure, Verified browser mode, CAPTCHA solving, proxies, or session persistence.
67
67
 
68
+ `--local` requires Chrome or Chromium already installed on the machine. In containers, CI, and sandboxes with no browser installed, use `--remote` instead of `--local`. If `--local` fails with "No Chrome or Chromium found" and `BROWSERBASE_API_KEY` is set, switch to `--remote` — do not retry `--local`.
69
+
68
70
  For a Verified and/or proxied remote session, add `--verified` and/or `--proxies` to `--remote` — a single command that keeps the Browserbase session identity, so `browse status` and `browse doctor` report the session ID and live-view URL. `--verified` requires a Browserbase Scale plan. These flags only apply to `--remote` and are sticky for the session's lifetime, like `--headed`. Reach for `browse cloud sessions create` + `--cdp` only when you need session options `open` doesn't expose (region, keep-alive, contexts).
69
71
 
70
72
  Choose headed/headless and local/remote mode when starting a session. A running session keeps its mode: passing a conflicting flag such as `--headed` to an already-running headless session fails until you run `browse stop --session <name>` or target a different session.
71
73
 
72
74
  Use named sessions for any non-trivial work, especially when multiple agents or parallel tasks may run at once. Every browser command accepts `--session <name>` (or `-s <name>`); the `BROWSE_SESSION` env var sets the default, and commands without either share the `default` session.
73
75
 
76
+ If `BROWSE_SESSION` is already set in the environment, every command already targets that session — do not pass `--session` or invent a new name. An explicit `--session <name>` always overrides `BROWSE_SESSION` for that command, so only pass it to deliberately target a different session.
77
+
74
78
  ```bash
75
79
  browse open https://example.com --session research --local
76
80
  browse snapshot --session research
@@ -82,6 +86,8 @@ Remote browser and cloud API commands require:
82
86
  export BROWSERBASE_API_KEY=...
83
87
  ```
84
88
 
89
+ The CLI forwards `BROWSERBASE_API_KEY` from the caller on every browser command, even when the daemon is already running. If a remote command first fails because the key was missing, set or export the key and retry the command; do not restart the daemon just to refresh the environment.
90
+
85
91
  ## Browser Automation Workflow
86
92
 
87
93
  Start by opening the page, then inspect state, act, and verify.
@@ -196,6 +202,8 @@ browse stop --force
196
202
 
197
203
  Use `browse doctor` before debugging a broken browser session. Use `browse doctor --json` when another agent or CI needs structured diagnostics.
198
204
 
205
+ `browse stop` is idempotent. If no daemon is running, `{ "stopped": false }` with exit code `0` is successful cleanup, not an error.
206
+
199
207
  If a page command reports that no active page is available, inspect and recover the named session:
200
208
 
201
209
  ```bash
@@ -356,6 +364,7 @@ JSON output includes every match with full descriptions and ignores `--limit`; `
356
364
  ## Troubleshooting
357
365
 
358
366
  - "No active page": run `browse status --session <name>`, then `browse open <url> --session <name>` or `browse tab new <url> --session <name>`; use `browse stop --force` if the daemon is stale.
367
+ - "Driver daemon ... is not running": run the exact `browse open` command printed by the error. Browser commands start the daemon automatically.
359
368
  - Chrome not found: use `--remote` with Browserbase credentials, install Chrome, or attach with `--cdp`.
360
369
  - Action fails: run `browse snapshot` and use a visible ref from the current page state.
361
370
  - 401 Unauthorized on `open`, `get`, or other driver commands: a set `BROWSERBASE_API_KEY` makes `browse` default to remote mode. Fix the key at https://browserbase.com/settings, unset it, or pass `--local` to run a managed local browser (no key needed).