browse 0.9.5 → 0.9.6
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 +9 -0
- package/bin/run.js +27 -1
- package/dist/commands/cloud/sessions/logs.js +31 -4
- package/dist/lib/cloud/reduce-logs.js +107 -0
- package/dist/lib/driver/daemon/client.js +41 -0
- package/oclif.manifest.json +484 -467
- package/package.json +2 -2
- package/skills/browse/SKILL.md +5 -0
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
|
|
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 = [
|
|
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 = {
|
|
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
|
-
|
|
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,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
|
+
}
|