browse 0.8.4 → 0.8.5
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 +4 -0
- package/dist/hooks/command-not-found.js +45 -0
- package/dist/lib/command-suggestions.js +112 -0
- package/dist/lib/driver/command-cli.js +7 -2
- 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/flags.js +13 -0
- package/dist/lib/driver/mode.js +60 -3
- package/dist/lib/driver/remote.disabled.js +15 -0
- package/dist/lib/driver/remote.js +35 -0
- package/dist/lib/driver/session-manager.js +90 -9
- package/dist/lib/skills/install.js +11 -5
- package/dist/lib/telemetry.js +20 -0
- package/oclif.manifest.json +727 -1
- package/package.json +6 -3
- package/skills/browse/SKILL.md +95 -20
|
@@ -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);
|
|
@@ -128,23 +156,23 @@ export class DriverSessionManager {
|
|
|
128
156
|
if (!page) {
|
|
129
157
|
throw new Error(`Target ${target.targetId} was not found in the attached browser.`);
|
|
130
158
|
}
|
|
131
|
-
this.
|
|
159
|
+
this.activateIfNeeded(page);
|
|
132
160
|
this.selectedTargetId = page.targetId();
|
|
133
161
|
return page;
|
|
134
162
|
}
|
|
135
163
|
const existingPage = this.activePageIfPresent() ?? this.context.pages()[0];
|
|
136
164
|
if (existingPage) {
|
|
137
|
-
this.
|
|
165
|
+
this.activateIfNeeded(existingPage);
|
|
138
166
|
this.selectedTargetId = existingPage.targetId();
|
|
139
167
|
return existingPage;
|
|
140
168
|
}
|
|
141
169
|
if (options.createIfMissing) {
|
|
142
170
|
const page = await this.context.newPage();
|
|
143
|
-
this.
|
|
171
|
+
this.activateIfNeeded(page);
|
|
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 {
|
|
@@ -154,6 +182,21 @@ export class DriverSessionManager {
|
|
|
154
182
|
return undefined;
|
|
155
183
|
}
|
|
156
184
|
}
|
|
185
|
+
/**
|
|
186
|
+
* Mark `page` active only when it isn't already the active page.
|
|
187
|
+
*
|
|
188
|
+
* `setActivePage` ends in a CDP `Target.activateTarget`, which on macOS
|
|
189
|
+
* raises the whole Chrome app to the OS foreground and steals keyboard focus.
|
|
190
|
+
* The daemon resolves the active page on every subcommand, so calling this
|
|
191
|
+
* unconditionally yanks focus away from the user's editor/terminal on each
|
|
192
|
+
* command in headed local mode. Skipping the redundant re-activation keeps a
|
|
193
|
+
* headed session usable alongside a coding agent.
|
|
194
|
+
*/
|
|
195
|
+
activateIfNeeded(page) {
|
|
196
|
+
if (page !== this.activePageIfPresent()) {
|
|
197
|
+
this.context?.setActivePage(page);
|
|
198
|
+
}
|
|
199
|
+
}
|
|
157
200
|
async ensureInitialized() {
|
|
158
201
|
if (this.stagehand && this.context)
|
|
159
202
|
return;
|
|
@@ -168,19 +211,32 @@ export class DriverSessionManager {
|
|
|
168
211
|
this.initPromise = this.initialize()
|
|
169
212
|
.then(() => {
|
|
170
213
|
this.initFailure = null;
|
|
214
|
+
this.consecutiveInitFailures = 0;
|
|
171
215
|
})
|
|
172
|
-
.catch((error) => {
|
|
216
|
+
.catch(async (error) => {
|
|
217
|
+
this.consecutiveInitFailures += 1;
|
|
218
|
+
const failure = await this.markRepeatedInitFailure(error);
|
|
173
219
|
this.initFailure = {
|
|
174
|
-
error,
|
|
175
|
-
retryAt: Date.now() +
|
|
220
|
+
error: failure,
|
|
221
|
+
retryAt: Date.now() + initFailureBackoffMs(this.consecutiveInitFailures),
|
|
176
222
|
};
|
|
177
|
-
throw
|
|
223
|
+
throw failure;
|
|
178
224
|
})
|
|
179
225
|
.finally(() => {
|
|
180
226
|
this.initPromise = null;
|
|
181
227
|
});
|
|
182
228
|
return this.initPromise;
|
|
183
229
|
}
|
|
230
|
+
async markRepeatedInitFailure(error) {
|
|
231
|
+
if (this.consecutiveInitFailures < 3 || !(error instanceof Error)) {
|
|
232
|
+
return error;
|
|
233
|
+
}
|
|
234
|
+
const hint = (await getRemote()).driverInitHints().repeatedInitFailure;
|
|
235
|
+
if (!error.message.includes(hint)) {
|
|
236
|
+
error.message += hint;
|
|
237
|
+
}
|
|
238
|
+
return error;
|
|
239
|
+
}
|
|
184
240
|
async initialize() {
|
|
185
241
|
const resolvedTarget = await this.resolveTarget();
|
|
186
242
|
const options = await this.stagehandOptions(resolvedTarget);
|
|
@@ -190,7 +246,7 @@ export class DriverSessionManager {
|
|
|
190
246
|
}
|
|
191
247
|
catch (error) {
|
|
192
248
|
await stagehand.close().catch(() => undefined);
|
|
193
|
-
throw error;
|
|
249
|
+
throw await describeInitError(error, resolvedTarget);
|
|
194
250
|
}
|
|
195
251
|
this.stagehand = stagehand;
|
|
196
252
|
this.context = stagehand.context;
|
|
@@ -213,6 +269,10 @@ export class DriverSessionManager {
|
|
|
213
269
|
disablePino: true,
|
|
214
270
|
env: "LOCAL",
|
|
215
271
|
localBrowserLaunchOptions: {
|
|
272
|
+
...(target.chromeArgs?.length ? { args: target.chromeArgs } : {}),
|
|
273
|
+
...(target.ignoreDefaultArgs !== undefined
|
|
274
|
+
? { ignoreDefaultArgs: target.ignoreDefaultArgs }
|
|
275
|
+
: {}),
|
|
216
276
|
headless: target.headless,
|
|
217
277
|
},
|
|
218
278
|
verbose: 0,
|
|
@@ -239,3 +299,24 @@ async function safeTitle(page) {
|
|
|
239
299
|
return "";
|
|
240
300
|
}
|
|
241
301
|
}
|
|
302
|
+
/**
|
|
303
|
+
* Turn raw `stagehand.init()` failures into typed, actionable errors. Remote
|
|
304
|
+
* failures are classified by the remote capability (401/403/etc.); a missing
|
|
305
|
+
* local Chrome gets install/escape-hatch guidance. Anything else is rethrown
|
|
306
|
+
* unchanged.
|
|
307
|
+
*/
|
|
308
|
+
async function describeInitError(error, target) {
|
|
309
|
+
if (error instanceof DriverError)
|
|
310
|
+
return error;
|
|
311
|
+
if (target.kind === "remote") {
|
|
312
|
+
const { code, httpStatus, message } = (await getRemote()).classifyRemoteInitError(error);
|
|
313
|
+
return new DriverError(message, { cause: error, code, httpStatus });
|
|
314
|
+
}
|
|
315
|
+
if (target.kind === "managed-local" && isChromeNotFoundError(error)) {
|
|
316
|
+
return new DriverError((await getRemote()).driverInitHints().chromeNotFound, {
|
|
317
|
+
cause: error,
|
|
318
|
+
code: "no_chrome_found",
|
|
319
|
+
});
|
|
320
|
+
}
|
|
321
|
+
return error;
|
|
322
|
+
}
|
|
@@ -5,6 +5,7 @@ import { homedir } from "node:os";
|
|
|
5
5
|
import { delimiter, dirname, join } from "node:path";
|
|
6
6
|
import { fileURLToPath } from "node:url";
|
|
7
7
|
import { fail } from "../errors.js";
|
|
8
|
+
import { setRunTelemetryCompletion } from "../run-telemetry.js";
|
|
8
9
|
import { defaultSkillsApiBaseUrl, isRecord, responseDetail } from "./shared.js";
|
|
9
10
|
const defaultBlobBaseUrl = "https://gh0lfhlmyzhg6tww.public.blob.vercel-storage.com";
|
|
10
11
|
const generatedSkillSuffixPattern = /-[A-Za-z0-9]{6}$/;
|
|
@@ -46,14 +47,18 @@ export function isBlobSkillId(skillId) {
|
|
|
46
47
|
return generatedSkillSuffixPattern.test(skillId.task);
|
|
47
48
|
}
|
|
48
49
|
export async function installSkill(rawSkillId) {
|
|
49
|
-
const
|
|
50
|
+
const skill = parseSkillId(rawSkillId);
|
|
51
|
+
// Attach only the validated, catalog-public id to telemetry — never the raw
|
|
52
|
+
// argument, which can contain arbitrary user input. Setting it right after
|
|
53
|
+
// parsing covers success and every downstream failure path.
|
|
54
|
+
setRunTelemetryCompletion({ skillId: skill.id });
|
|
50
55
|
const npxPath = await findExecutable("npx");
|
|
51
56
|
if (!npxPath) {
|
|
52
57
|
fail("`npx` is not installed. Install Node.js from https://nodejs.org, then rerun `browse skills add`.", 1, { resultCode: "npx_missing" });
|
|
53
58
|
}
|
|
54
|
-
const files = await fetchSkillFiles(
|
|
59
|
+
const files = await fetchSkillFiles(skill);
|
|
55
60
|
if (files.status === "found") {
|
|
56
|
-
const result = await downloadBlobSkill(
|
|
61
|
+
const result = await downloadBlobSkill(skill, files.files);
|
|
57
62
|
process.stdout.write(`Downloaded ${result.fileCount} skill file${result.fileCount === 1 ? "" : "s"} to ${result.installPath}\n`);
|
|
58
63
|
await runSkillsInstall(npxPath, [
|
|
59
64
|
"--yes",
|
|
@@ -64,7 +69,7 @@ export async function installSkill(rawSkillId) {
|
|
|
64
69
|
return;
|
|
65
70
|
}
|
|
66
71
|
if (files.status === "not_found") {
|
|
67
|
-
fail(`Skill "${
|
|
72
|
+
fail(`Skill "${skill.id}" not found in the catalog. Run \`browse skills find ${skill.domain}\` to discover available skills, or \`browse skills list\` to browse them.`, 1, { resultCode: "skill_not_found" });
|
|
68
73
|
}
|
|
69
74
|
// Suffix-shaped (generated) ids, or a temporarily unavailable catalog, may
|
|
70
75
|
// still resolve through the browse.sh GitHub repo.
|
|
@@ -74,10 +79,11 @@ export async function installSkill(rawSkillId) {
|
|
|
74
79
|
"add",
|
|
75
80
|
"browserbase/browse.sh",
|
|
76
81
|
"--skill",
|
|
77
|
-
|
|
82
|
+
skill.id,
|
|
78
83
|
]);
|
|
79
84
|
}
|
|
80
85
|
export async function installBundledCliSkill() {
|
|
86
|
+
setRunTelemetryCompletion({ skillId: "bundled/browse" });
|
|
81
87
|
const npxPath = await findExecutable("npx");
|
|
82
88
|
if (!npxPath) {
|
|
83
89
|
fail("`npx` is not installed. Install Node.js from https://nodejs.org, then rerun `browse skills install`.", 1, { resultCode: "npx_missing" });
|
package/dist/lib/telemetry.js
CHANGED
|
@@ -52,6 +52,25 @@ export async function captureCommandCompleted(cliVersion, error) {
|
|
|
52
52
|
completionCapture,
|
|
53
53
|
]);
|
|
54
54
|
}
|
|
55
|
+
/**
|
|
56
|
+
* Captures a `cli.command_not_found` event. Privacy: only the sanitized
|
|
57
|
+
* attempted command id and the computed suggestion are sent — never raw argv,
|
|
58
|
+
* which can contain URLs, selectors, or secrets.
|
|
59
|
+
*/
|
|
60
|
+
export async function captureCommandNotFound(cliVersion, attemptedCommand, suggestedCommand) {
|
|
61
|
+
await getCliTelemetry(cliVersion)
|
|
62
|
+
.capture("cli.command_not_found", {
|
|
63
|
+
attempted_command: attemptedCommand
|
|
64
|
+
? resolveCommandPath(attemptedCommand)
|
|
65
|
+
: null,
|
|
66
|
+
suggested_command: suggestedCommand
|
|
67
|
+
? resolveCommandPath(suggestedCommand)
|
|
68
|
+
: null,
|
|
69
|
+
})
|
|
70
|
+
.catch(() => {
|
|
71
|
+
// Best-effort telemetry should never affect CLI behavior.
|
|
72
|
+
});
|
|
73
|
+
}
|
|
55
74
|
function createCliTelemetry(options) {
|
|
56
75
|
const env = options.env ?? process.env;
|
|
57
76
|
const transport = resolveTransportConfig(env);
|
|
@@ -147,6 +166,7 @@ function commandCompletedProperties(invocation, completion) {
|
|
|
147
166
|
request_had_http_response: failureTelemetry?.requestHadHttpResponse ??
|
|
148
167
|
runTelemetry.requestHadHttpResponse ??
|
|
149
168
|
null,
|
|
169
|
+
skill_id: runTelemetry.skillId ?? null,
|
|
150
170
|
};
|
|
151
171
|
}
|
|
152
172
|
function fallbackResultCode(success, errorType) {
|