browse 0.8.4-alpha-4ee8d99 → 0.8.4-alpha-cffcc91
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/dist/lib/driver/commands/selectors.js +2 -1
- package/dist/lib/driver/daemon/client.js +13 -6
- package/dist/lib/driver/daemon/protocol.js +2 -0
- package/dist/lib/driver/daemon/server.js +14 -3
- package/dist/lib/driver/errors.js +16 -0
- package/dist/lib/driver/remote.disabled.js +15 -0
- package/dist/lib/driver/remote.js +35 -0
- package/dist/lib/driver/session-manager.js +68 -6
- package/package.json +2 -1
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { DriverError } from "../errors.js";
|
|
1
2
|
export function emptyRefMaps() {
|
|
2
3
|
return { urlMap: {}, xpathMap: {} };
|
|
3
4
|
}
|
|
@@ -24,7 +25,7 @@ export function resolveSelector(selector, refMaps) {
|
|
|
24
25
|
return selector;
|
|
25
26
|
const xpath = refMaps.xpathMap[ref];
|
|
26
27
|
if (!xpath) {
|
|
27
|
-
throw new
|
|
28
|
+
throw new DriverError(`Unknown ref "${ref}" - run browse snapshot first to populate refs (have ${Object.keys(refMaps.xpathMap).length} refs).`, { code: "stale_ref" });
|
|
28
29
|
}
|
|
29
30
|
return xpath;
|
|
30
31
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { spawn } from "node:child_process";
|
|
2
2
|
import { promises as fs } from "node:fs";
|
|
3
3
|
import net from "node:net";
|
|
4
|
-
import { fail } from "../../errors.js";
|
|
4
|
+
import { CommandFailure, fail } from "../../errors.js";
|
|
5
5
|
import { targetsCompatible } from "../mode.js";
|
|
6
6
|
import { cleanupDaemonFiles, ensureRuntimeDir, getLockPath, getPidPath, getSocketPath, PRIVATE_FILE_MODE, } from "./paths.js";
|
|
7
7
|
import { isProcessAlive } from "./process.js";
|
|
@@ -15,7 +15,7 @@ export async function ensureDriverDaemon({ session, target, }) {
|
|
|
15
15
|
}
|
|
16
16
|
const locked = await acquireLock(session);
|
|
17
17
|
if (!locked) {
|
|
18
|
-
fail(`Timed out waiting for driver daemon lock for session "${session}"
|
|
18
|
+
fail(`Timed out waiting for driver daemon lock for session "${session}".`, 1, { resultCode: "daemon_lock_timeout" });
|
|
19
19
|
}
|
|
20
20
|
try {
|
|
21
21
|
const afterLock = await tryDriverStatus(session);
|
|
@@ -24,7 +24,7 @@ export async function ensureDriverDaemon({ session, target, }) {
|
|
|
24
24
|
return;
|
|
25
25
|
}
|
|
26
26
|
if (await isDaemonPidAlive(session)) {
|
|
27
|
-
fail(`Driver daemon session "${session}" is running but not responding. Run browse stop --session ${session} --force to clean it up
|
|
27
|
+
fail(`Driver daemon session "${session}" is running but not responding. Run browse stop --session ${session} --force to clean it up.`, 1, { resultCode: "daemon_unresponsive" });
|
|
28
28
|
}
|
|
29
29
|
spawnDaemon(session, target);
|
|
30
30
|
await waitForSocketReady(getSocketPath(session), 30_000);
|
|
@@ -123,7 +123,7 @@ async function sendDriverRequest(session, request) {
|
|
|
123
123
|
return new Error(`Driver daemon session "${session}" closed ${detail}.`);
|
|
124
124
|
};
|
|
125
125
|
const timeout = setTimeout(() => {
|
|
126
|
-
failRequest(new
|
|
126
|
+
failRequest(new CommandFailure(`Timed out waiting for driver daemon session "${session}".`, 1, { resultCode: "daemon_socket_timeout" }));
|
|
127
127
|
}, 35_000);
|
|
128
128
|
socket.on("connect", () => {
|
|
129
129
|
socket.write(`${JSON.stringify(request)}\n`);
|
|
@@ -136,7 +136,12 @@ async function sendDriverRequest(session, request) {
|
|
|
136
136
|
try {
|
|
137
137
|
const response = ResponseSchema.parse(JSON.parse(buffer.slice(0, newline)));
|
|
138
138
|
if (response.type === "error") {
|
|
139
|
-
failRequest(new
|
|
139
|
+
failRequest(new CommandFailure(response.error, 1, {
|
|
140
|
+
...(response.code ? { resultCode: response.code } : {}),
|
|
141
|
+
...(response.httpStatus !== undefined
|
|
142
|
+
? { httpStatus: response.httpStatus }
|
|
143
|
+
: {}),
|
|
144
|
+
}));
|
|
140
145
|
return;
|
|
141
146
|
}
|
|
142
147
|
completeRequest(response.data);
|
|
@@ -161,7 +166,9 @@ async function sendDriverRequest(session, request) {
|
|
|
161
166
|
function spawnDaemon(session, target) {
|
|
162
167
|
const entrypoint = process.argv[1];
|
|
163
168
|
if (!entrypoint) {
|
|
164
|
-
fail("Unable to locate browse CLI entrypoint for daemon startup."
|
|
169
|
+
fail("Unable to locate browse CLI entrypoint for daemon startup.", 1, {
|
|
170
|
+
resultCode: "daemon_spawn_failed",
|
|
171
|
+
});
|
|
165
172
|
}
|
|
166
173
|
const child = spawn(process.execPath, [
|
|
167
174
|
entrypoint,
|
|
@@ -32,7 +32,9 @@ export const SuccessResponseSchema = z.object({
|
|
|
32
32
|
type: z.literal("success"),
|
|
33
33
|
});
|
|
34
34
|
export const ErrorResponseSchema = z.object({
|
|
35
|
+
code: z.string().optional(),
|
|
35
36
|
error: z.string(),
|
|
37
|
+
httpStatus: z.number().optional(),
|
|
36
38
|
id: z.string().optional(),
|
|
37
39
|
type: z.literal("error"),
|
|
38
40
|
});
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import net from "node:net";
|
|
2
2
|
import readline from "node:readline";
|
|
3
|
+
import { DriverError } from "../errors.js";
|
|
3
4
|
import { DriverSessionManager } from "../session-manager.js";
|
|
4
5
|
import { cleanupDaemonFiles, ensureRuntimeDir, getPidPath, getSocketPath, writePrivateFile, } from "./paths.js";
|
|
5
6
|
import { parseRequest, serializeResponse, } from "./protocol.js";
|
|
@@ -83,7 +84,7 @@ async function handleLine(line, socket, manager) {
|
|
|
83
84
|
request = parseRequest(line);
|
|
84
85
|
}
|
|
85
86
|
catch (error) {
|
|
86
|
-
await writeResponse(socket, {
|
|
87
|
+
await writeResponse(socket, { ...formatError(error), type: "error" });
|
|
87
88
|
return false;
|
|
88
89
|
}
|
|
89
90
|
try {
|
|
@@ -124,7 +125,7 @@ async function handleLine(line, socket, manager) {
|
|
|
124
125
|
}
|
|
125
126
|
catch (error) {
|
|
126
127
|
await writeResponse(socket, {
|
|
127
|
-
|
|
128
|
+
...formatError(error),
|
|
128
129
|
id: request.id,
|
|
129
130
|
type: "error",
|
|
130
131
|
});
|
|
@@ -161,5 +162,15 @@ function endSocket(socket) {
|
|
|
161
162
|
});
|
|
162
163
|
}
|
|
163
164
|
function formatError(error) {
|
|
164
|
-
|
|
165
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
166
|
+
if (error instanceof DriverError) {
|
|
167
|
+
return {
|
|
168
|
+
code: error.code,
|
|
169
|
+
error: message,
|
|
170
|
+
...(error.httpStatus !== undefined
|
|
171
|
+
? { httpStatus: error.httpStatus }
|
|
172
|
+
: {}),
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
return { error: message };
|
|
165
176
|
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Typed driver error. The daemon serializes `code`/`httpStatus` into error
|
|
3
|
+
* responses so the client can record a telemetry result code and agents get
|
|
4
|
+
* a stable, machine-readable failure reason alongside the human message.
|
|
5
|
+
*/
|
|
6
|
+
export class DriverError extends Error {
|
|
7
|
+
code;
|
|
8
|
+
httpStatus;
|
|
9
|
+
constructor(message, options) {
|
|
10
|
+
super(message, options.cause === undefined ? {} : { cause: options.cause });
|
|
11
|
+
this.name = "DriverError";
|
|
12
|
+
this.code = options.code;
|
|
13
|
+
if (options.httpStatus !== undefined)
|
|
14
|
+
this.httpStatus = options.httpStatus;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
@@ -13,6 +13,21 @@ export function autoSelectRemoteTarget() {
|
|
|
13
13
|
export function remoteStagehandOptions() {
|
|
14
14
|
throw new Error(DISABLED_MESSAGE);
|
|
15
15
|
}
|
|
16
|
+
export function classifyRemoteInitError(error) {
|
|
17
|
+
// Remote targets cannot be selected in a local-only build, so this is only
|
|
18
|
+
// reachable if a remote error somehow surfaces anyway; preserve it as-is.
|
|
19
|
+
return {
|
|
20
|
+
code: "remote_session_create_failed",
|
|
21
|
+
message: error instanceof Error ? error.message : String(error),
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
export function driverInitHints() {
|
|
25
|
+
// Key-free variants: a local-only artifact must not mention the API key.
|
|
26
|
+
return {
|
|
27
|
+
chromeNotFound: "No Chrome or Chromium found on this machine. Install one (Linux: apt install chromium \u00b7 macOS: brew install --cask google-chrome, or Chromium with CHROME_PATH set) or attach to a running browser with --cdp <port>.",
|
|
28
|
+
repeatedInitFailure: " (failing repeatedly — check your browser setup or run browse doctor)",
|
|
29
|
+
};
|
|
30
|
+
}
|
|
16
31
|
export function remoteDoctorCheck() {
|
|
17
32
|
return { ok: true, message: "remote disabled (local-only build)" };
|
|
18
33
|
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { StatusCodes } from "http-status-codes";
|
|
1
2
|
/**
|
|
2
3
|
* Real Browserbase capability. This is the ONLY module that reads
|
|
3
4
|
* `BROWSERBASE_API_KEY`; it is excluded from `build:local-only` so that
|
|
@@ -25,6 +26,40 @@ export function remoteStagehandOptions() {
|
|
|
25
26
|
verbose: 0,
|
|
26
27
|
};
|
|
27
28
|
}
|
|
29
|
+
/**
|
|
30
|
+
* Map a failed remote `stagehand.init()` to an actionable message and a
|
|
31
|
+
* stable result code. Browserbase SDK errors carry an HTTP `status`.
|
|
32
|
+
*/
|
|
33
|
+
export function classifyRemoteInitError(error) {
|
|
34
|
+
const status = error?.status;
|
|
35
|
+
const httpStatus = typeof status === "number" ? status : undefined;
|
|
36
|
+
const original = error instanceof Error ? error.message : String(error);
|
|
37
|
+
if (httpStatus === StatusCodes.UNAUTHORIZED) {
|
|
38
|
+
return {
|
|
39
|
+
code: "remote_auth_401",
|
|
40
|
+
httpStatus,
|
|
41
|
+
message: "Browserbase rejected your BROWSERBASE_API_KEY (401 Unauthorized). A set key makes browse default to remote mode. Check the key at https://browserbase.com/settings, run without one using --local (browse open <url> --local), or diagnose with browse doctor.",
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
if (httpStatus === StatusCodes.FORBIDDEN) {
|
|
45
|
+
return {
|
|
46
|
+
code: "remote_auth_403",
|
|
47
|
+
httpStatus,
|
|
48
|
+
message: "Browserbase refused this request (403 Forbidden). Your BROWSERBASE_API_KEY may lack access to this project, or your plan may not allow this session type. Check the key at https://browserbase.com/settings, run without one using --local (browse open <url> --local), or diagnose with browse doctor.",
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
return {
|
|
52
|
+
code: "remote_session_create_failed",
|
|
53
|
+
...(httpStatus !== undefined ? { httpStatus } : {}),
|
|
54
|
+
message: `Failed to start a remote (Browserbase) session: ${original}\nRun browse doctor to diagnose remote connectivity.`,
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
export function driverInitHints() {
|
|
58
|
+
return {
|
|
59
|
+
chromeNotFound: "No Chrome or Chromium found on this machine. Install one (Linux: apt install chromium \u00b7 macOS: brew install --cask google-chrome, or Chromium with CHROME_PATH set), attach to a running browser with --cdp <port>, or set BROWSERBASE_API_KEY to use a remote browser.",
|
|
60
|
+
repeatedInitFailure: " (failing repeatedly — fix BROWSERBASE_API_KEY, use --local, or run browse doctor)",
|
|
61
|
+
};
|
|
62
|
+
}
|
|
28
63
|
export function remoteDoctorCheck(env) {
|
|
29
64
|
if (env.BROWSERBASE_API_KEY) {
|
|
30
65
|
return { ok: true, message: "BROWSERBASE_API_KEY is set" };
|
|
@@ -1,14 +1,41 @@
|
|
|
1
1
|
import { Stagehand } from "@browserbasehq/stagehand";
|
|
2
2
|
import { emptyRefMaps, resolveSelector as resolveCachedSelector, } from "./commands/selectors.js";
|
|
3
3
|
import { executeDriverCommand } from "./commands/registry.js";
|
|
4
|
+
import { DriverError } from "./errors.js";
|
|
4
5
|
import { discoverLocalCdp } from "./local-cdp-discovery.js";
|
|
5
6
|
import { NetworkCapture } from "./network-capture.js";
|
|
6
7
|
import { getRemote } from "./remote-binding.js";
|
|
7
8
|
const INIT_FAILURE_RETRY_MS = 5_000;
|
|
9
|
+
const INIT_FAILURE_RETRY_MAX_MS = 60_000;
|
|
10
|
+
// chrome-launcher reports "no Chrome on this machine" with these codes (its
|
|
11
|
+
// LaunchErrorCodes const enum, which can't be imported directly: const enums
|
|
12
|
+
// are erased at build time and isolatedModules forbids cross-module access).
|
|
13
|
+
const CHROME_NOT_FOUND_ERROR_CODES = new Set([
|
|
14
|
+
"ERR_LAUNCHER_NOT_INSTALLED",
|
|
15
|
+
"ERR_LAUNCHER_PATH_NOT_SET",
|
|
16
|
+
]);
|
|
17
|
+
/**
|
|
18
|
+
* Exponential backoff for cached init failures: 5s, 10s, 20s, ... capped at
|
|
19
|
+
* 1 minute. Prevents agents stuck in retry loops from hammering init while
|
|
20
|
+
* still allowing a quick retry after the first failure.
|
|
21
|
+
*/
|
|
22
|
+
export function initFailureBackoffMs(consecutiveFailures) {
|
|
23
|
+
const attempt = Math.max(1, consecutiveFailures);
|
|
24
|
+
return Math.min(INIT_FAILURE_RETRY_MS * 2 ** (attempt - 1), INIT_FAILURE_RETRY_MAX_MS);
|
|
25
|
+
}
|
|
26
|
+
export function isChromeNotFoundError(error) {
|
|
27
|
+
const code = error?.code;
|
|
28
|
+
if (typeof code === "string" && CHROME_NOT_FOUND_ERROR_CODES.has(code)) {
|
|
29
|
+
return true;
|
|
30
|
+
}
|
|
31
|
+
return (error instanceof Error &&
|
|
32
|
+
error.message.includes("No Chrome installations found"));
|
|
33
|
+
}
|
|
8
34
|
export class DriverSessionManager {
|
|
9
35
|
session;
|
|
10
36
|
target;
|
|
11
37
|
network;
|
|
38
|
+
consecutiveInitFailures = 0;
|
|
12
39
|
context = null;
|
|
13
40
|
initFailure = null;
|
|
14
41
|
initPromise = null;
|
|
@@ -79,6 +106,7 @@ export class DriverSessionManager {
|
|
|
79
106
|
this.stagehand = null;
|
|
80
107
|
this.context = null;
|
|
81
108
|
this.initFailure = null;
|
|
109
|
+
this.consecutiveInitFailures = 0;
|
|
82
110
|
await this.network.disable().catch(() => undefined);
|
|
83
111
|
if (stagehand) {
|
|
84
112
|
await stagehand.close().catch(() => undefined);
|
|
@@ -144,7 +172,7 @@ export class DriverSessionManager {
|
|
|
144
172
|
this.selectedTargetId = page.targetId();
|
|
145
173
|
return page;
|
|
146
174
|
}
|
|
147
|
-
throw new
|
|
175
|
+
throw new DriverError(`No active page in session "${this.session}". Run browse open <url> --session ${this.session} or browse tab new <url> --session ${this.session}.`, { code: "no_active_page" });
|
|
148
176
|
}
|
|
149
177
|
activePageIfPresent() {
|
|
150
178
|
try {
|
|
@@ -168,19 +196,32 @@ export class DriverSessionManager {
|
|
|
168
196
|
this.initPromise = this.initialize()
|
|
169
197
|
.then(() => {
|
|
170
198
|
this.initFailure = null;
|
|
199
|
+
this.consecutiveInitFailures = 0;
|
|
171
200
|
})
|
|
172
|
-
.catch((error) => {
|
|
201
|
+
.catch(async (error) => {
|
|
202
|
+
this.consecutiveInitFailures += 1;
|
|
203
|
+
const failure = await this.markRepeatedInitFailure(error);
|
|
173
204
|
this.initFailure = {
|
|
174
|
-
error,
|
|
175
|
-
retryAt: Date.now() +
|
|
205
|
+
error: failure,
|
|
206
|
+
retryAt: Date.now() + initFailureBackoffMs(this.consecutiveInitFailures),
|
|
176
207
|
};
|
|
177
|
-
throw
|
|
208
|
+
throw failure;
|
|
178
209
|
})
|
|
179
210
|
.finally(() => {
|
|
180
211
|
this.initPromise = null;
|
|
181
212
|
});
|
|
182
213
|
return this.initPromise;
|
|
183
214
|
}
|
|
215
|
+
async markRepeatedInitFailure(error) {
|
|
216
|
+
if (this.consecutiveInitFailures < 3 || !(error instanceof Error)) {
|
|
217
|
+
return error;
|
|
218
|
+
}
|
|
219
|
+
const hint = (await getRemote()).driverInitHints().repeatedInitFailure;
|
|
220
|
+
if (!error.message.includes(hint)) {
|
|
221
|
+
error.message += hint;
|
|
222
|
+
}
|
|
223
|
+
return error;
|
|
224
|
+
}
|
|
184
225
|
async initialize() {
|
|
185
226
|
const resolvedTarget = await this.resolveTarget();
|
|
186
227
|
const options = await this.stagehandOptions(resolvedTarget);
|
|
@@ -190,7 +231,7 @@ export class DriverSessionManager {
|
|
|
190
231
|
}
|
|
191
232
|
catch (error) {
|
|
192
233
|
await stagehand.close().catch(() => undefined);
|
|
193
|
-
throw error;
|
|
234
|
+
throw await describeInitError(error, resolvedTarget);
|
|
194
235
|
}
|
|
195
236
|
this.stagehand = stagehand;
|
|
196
237
|
this.context = stagehand.context;
|
|
@@ -243,3 +284,24 @@ async function safeTitle(page) {
|
|
|
243
284
|
return "";
|
|
244
285
|
}
|
|
245
286
|
}
|
|
287
|
+
/**
|
|
288
|
+
* Turn raw `stagehand.init()` failures into typed, actionable errors. Remote
|
|
289
|
+
* failures are classified by the remote capability (401/403/etc.); a missing
|
|
290
|
+
* local Chrome gets install/escape-hatch guidance. Anything else is rethrown
|
|
291
|
+
* unchanged.
|
|
292
|
+
*/
|
|
293
|
+
async function describeInitError(error, target) {
|
|
294
|
+
if (error instanceof DriverError)
|
|
295
|
+
return error;
|
|
296
|
+
if (target.kind === "remote") {
|
|
297
|
+
const { code, httpStatus, message } = (await getRemote()).classifyRemoteInitError(error);
|
|
298
|
+
return new DriverError(message, { cause: error, code, httpStatus });
|
|
299
|
+
}
|
|
300
|
+
if (target.kind === "managed-local" && isChromeNotFoundError(error)) {
|
|
301
|
+
return new DriverError((await getRemote()).driverInitHints().chromeNotFound, {
|
|
302
|
+
cause: error,
|
|
303
|
+
code: "no_chrome_found",
|
|
304
|
+
});
|
|
305
|
+
}
|
|
306
|
+
return error;
|
|
307
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "browse",
|
|
3
|
-
"version": "0.8.4-alpha-
|
|
3
|
+
"version": "0.8.4-alpha-cffcc91",
|
|
4
4
|
"description": "Unified Browserbase CLI for browser automation and cloud APIs.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"private": false,
|
|
@@ -93,6 +93,7 @@
|
|
|
93
93
|
"deepmerge": "^4.3.1",
|
|
94
94
|
"dotenv": "^16.5.0",
|
|
95
95
|
"fastest-levenshtein": "^1.0.16",
|
|
96
|
+
"http-status-codes": "^2.3.0",
|
|
96
97
|
"ignore": "^7.0.5",
|
|
97
98
|
"node-html-markdown": "^1.3.0",
|
|
98
99
|
"semver": "^7.7.4",
|