browser-pilot-cli 0.3.0 → 0.5.0

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/cli.js CHANGED
@@ -1,12 +1,12 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  // src/cli.ts
4
- import { Command } from "commander";
5
- import { writeFileSync as writeFileSync2, readFileSync as readFileSync4, existsSync as existsSync4 } from "fs";
6
- import { resolve as resolvePath } from "path";
4
+ import { Command, CommanderError } from "commander";
5
+ import { writeFileSync as writeFileSync2, readFileSync as readFileSync4, existsSync as existsSync4, mkdirSync as mkdirSync2 } from "fs";
6
+ import { basename as basename2, isAbsolute as isAbsolute3, resolve as resolvePath } from "path";
7
7
 
8
8
  // src/version.ts
9
- var BROWSER_PILOT_VERSION = "0.3.0";
9
+ var BROWSER_PILOT_VERSION = "0.5.0";
10
10
 
11
11
  // src/protocol/errors.ts
12
12
  var ERROR_CODES = [
@@ -28,6 +28,7 @@ var ERROR_CODES = [
28
28
  "action_not_verified",
29
29
  "command_cancelled",
30
30
  "command_expired",
31
+ "wait_timeout",
31
32
  "unknown_outcome",
32
33
  "artifact_not_found",
33
34
  "artifact_expired",
@@ -73,23 +74,6 @@ function invalidArgument(message, field, rpcCode = -32602) {
73
74
  rpcCode
74
75
  });
75
76
  }
76
- function asBrowserPilotError(error) {
77
- if (error instanceof BrowserPilotError) return error;
78
- if (error instanceof Error && typeof error.code === "string" && ERROR_CODES.includes(error.code)) {
79
- const stable = error;
80
- return new BrowserPilotError(stable.code, error.message, {
81
- retryable: stable.retryable,
82
- context: stable.context,
83
- remediation: stable.remediation,
84
- rpcCode: stable.rpcCode,
85
- cause: error
86
- });
87
- }
88
- return new BrowserPilotError("internal_error", "Internal Browser Pilot error", {
89
- retryable: false,
90
- cause: error
91
- });
92
- }
93
77
  function browserPilotErrorFromJsonRpc(error) {
94
78
  const data = error.data;
95
79
  if (!data || typeof data !== "object" || Array.isArray(data)) {
@@ -110,154 +94,445 @@ function browserPilotErrorFromJsonRpc(error) {
110
94
  });
111
95
  }
112
96
 
113
- // src/session.ts
114
- import { spawn } from "child_process";
115
-
116
- // src/client.ts
117
- import http from "http";
118
- import { existsSync as existsSync2, readFileSync as readFileSync2 } from "fs";
119
-
120
- // src/broker-locator.ts
121
- import { createHash as createHash2, randomUUID } from "crypto";
122
- import {
123
- chmodSync,
124
- closeSync,
125
- existsSync,
126
- lstatSync,
127
- mkdirSync,
128
- openSync,
129
- readFileSync,
130
- renameSync,
131
- realpathSync,
132
- statSync,
133
- unlinkSync,
134
- writeFileSync,
135
- writeSync
136
- } from "fs";
137
- import { basename, dirname, isAbsolute as isAbsolute2, join as join2 } from "path";
138
-
139
- // src/paths.ts
97
+ // src/chrome.ts
140
98
  import { createHash } from "crypto";
141
- import { homedir, platform, tmpdir, userInfo } from "os";
142
- import { isAbsolute, join, win32 } from "path";
143
- var MAX_PORTABLE_UNIX_SOCKET_BYTES = 96;
144
- function identityDigest(value) {
145
- return createHash("sha256").update(value).digest("hex").slice(0, 16);
146
- }
147
- function resolveBrowserPilotPaths(options = {}) {
148
- const os = options.platform ?? platform();
149
- const home = options.homeDir ?? homedir();
150
- const env = options.env ?? process.env;
151
- if (os === "win32") {
152
- const localAppData = env.LOCALAPPDATA ?? win32.join(home, "AppData", "Local");
153
- const stateDir2 = env.BROWSER_PILOT_HOME ?? win32.join(localAppData, "Browser Pilot");
154
- if (!win32.isAbsolute(stateDir2)) throw new Error("BROWSER_PILOT_HOME must be absolute");
155
- const username = options.username ?? (() => {
156
- try {
157
- return userInfo().username;
158
- } catch {
159
- return home;
160
- }
161
- })();
162
- const identity = identityDigest(
163
- `${username.toLowerCase()}\0${home.toLowerCase()}\0${stateDir2.toLowerCase()}`
164
- );
99
+ import { execFileSync } from "child_process";
100
+ import { existsSync, lstatSync, readFileSync } from "fs";
101
+ import { connect as connectTcp } from "net";
102
+ import { delimiter, join } from "path";
103
+ import { homedir, platform } from "os";
104
+ import WebSocket from "ws";
105
+ function readChromeInfo(browser, dataDir) {
106
+ const portFile = join(dataDir, "DevToolsActivePort");
107
+ if (!existsSync(portFile)) return null;
108
+ try {
109
+ const lines = readFileSync(portFile, "utf-8").trim().split(/\r?\n/);
110
+ if (lines.length < 2) return null;
111
+ const port = Number.parseInt(lines[0], 10);
112
+ const wsPath = lines[1];
113
+ if (!Number.isSafeInteger(port) || port < 1 || port > 65535 || !wsPath.startsWith("/")) return null;
165
114
  return {
166
- stateDir: stateDir2,
167
- runtimeDir: stateDir2,
168
- endpoint: `\\\\.\\pipe\\browser-pilot-${identity}`,
169
- transport: "windows_pipe",
170
- locatorFile: win32.join(stateDir2, "broker-locator.json"),
171
- pidFile: win32.join(stateDir2, "daemon.pid"),
172
- startupLockFile: win32.join(stateDir2, "startup.lock"),
173
- versionHistoryFile: win32.join(stateDir2, "broker-versions.json"),
174
- artifactDir: win32.join(stateDir2, "artifacts"),
175
- downloadDir: win32.join(stateDir2, "downloads")
115
+ port,
116
+ wsPath,
117
+ wsUrl: `ws://127.0.0.1:${port}${wsPath}`,
118
+ browser,
119
+ dataDir
176
120
  };
121
+ } catch {
122
+ return null;
177
123
  }
178
- const stateDir = env.BROWSER_PILOT_HOME ?? join(home, ".browser-pilot");
179
- if (!isAbsolute(stateDir)) throw new Error("BROWSER_PILOT_HOME must be absolute");
180
- const defaultEndpoint = join(stateDir, "daemon.sock");
181
- const uid = options.uid ?? process.getuid?.() ?? 0;
182
- const useShortRuntimePath = Buffer.byteLength(defaultEndpoint) > MAX_PORTABLE_UNIX_SOCKET_BYTES;
183
- let runtimeDir = useShortRuntimePath ? join(options.tempDir ?? tmpdir(), `browser-pilot-${uid}-${identityDigest(stateDir)}`) : stateDir;
184
- let endpoint = join(runtimeDir, "daemon.sock");
185
- if (Buffer.byteLength(endpoint) > MAX_PORTABLE_UNIX_SOCKET_BYTES) {
186
- runtimeDir = join("/tmp", `browser-pilot-${uid}-${identityDigest(stateDir)}`);
187
- endpoint = join(runtimeDir, "daemon.sock");
188
- }
189
- if (Buffer.byteLength(endpoint) > MAX_PORTABLE_UNIX_SOCKET_BYTES) {
190
- throw new Error("Browser Pilot cannot resolve a portable Unix socket path");
191
- }
192
- if (!isAbsolute(endpoint)) throw new Error("Browser Pilot endpoint must be absolute");
193
- return {
194
- stateDir,
195
- runtimeDir,
196
- endpoint,
197
- transport: "unix_socket",
198
- locatorFile: join(stateDir, "broker-locator.json"),
199
- pidFile: join(stateDir, "daemon.pid"),
200
- startupLockFile: join(stateDir, "startup.lock"),
201
- versionHistoryFile: join(stateDir, "broker-versions.json"),
202
- artifactDir: join(stateDir, "artifacts"),
203
- downloadDir: join(stateDir, "downloads")
204
- };
205
124
  }
206
- var BROWSER_PILOT_PATHS = resolveBrowserPilotPaths();
207
- var STATE_DIR = BROWSER_PILOT_PATHS.stateDir;
208
- var RUNTIME_DIR = BROWSER_PILOT_PATHS.runtimeDir;
209
- var SOCKET_PATH = BROWSER_PILOT_PATHS.endpoint;
210
- var BROKER_TRANSPORT = BROWSER_PILOT_PATHS.transport;
211
- var LOCATOR_FILE = BROWSER_PILOT_PATHS.locatorFile;
212
- var PID_FILE = BROWSER_PILOT_PATHS.pidFile;
213
- var STARTUP_LOCK_FILE = BROWSER_PILOT_PATHS.startupLockFile;
214
- var ARTIFACT_DIR = BROWSER_PILOT_PATHS.artifactDir;
215
- var DOWNLOAD_DIR = BROWSER_PILOT_PATHS.downloadDir;
216
-
217
- // src/broker-locator.ts
218
- var PRIVATE_DIRECTORY_MODE = 448;
219
- var PRIVATE_FILE_MODE = 384;
220
- var MAX_METADATA_BYTES = 16 * 1024;
221
- function currentUid() {
222
- return process.getuid?.();
125
+ function executablePaths(env) {
126
+ return (env.PATH ?? "").split(delimiter).filter(Boolean);
223
127
  }
224
- function validateOwnedPath(path, expectDirectory) {
225
- const stats = lstatSync(path);
226
- if (stats.isSymbolicLink()) throw new Error(`Browser Pilot path must not be a symbolic link: ${path}`);
227
- if (expectDirectory ? !stats.isDirectory() : !stats.isFile()) {
228
- throw new Error(`Browser Pilot path has an unexpected type: ${path}`);
128
+ function linuxInstallPaths(env, names) {
129
+ return executablePaths(env).flatMap((directory) => names.map((name) => join(directory, name)));
130
+ }
131
+ function supportedBrowserProfiles(options = {}) {
132
+ const home = options.homeDir ?? homedir();
133
+ const os = options.platform ?? platform();
134
+ const env = options.env ?? process.env;
135
+ if (os === "darwin") {
136
+ const base = join(home, "Library", "Application Support");
137
+ const applications = ["/Applications", join(home, "Applications")];
138
+ const appPaths = (name) => applications.map((directory) => join(directory, name));
139
+ return [
140
+ {
141
+ key: "chrome-stable",
142
+ product: "Chrome",
143
+ channel: "stable",
144
+ dataDir: join(base, "Google", "Chrome"),
145
+ installPaths: appPaths("Google Chrome.app"),
146
+ executableNames: ["Google Chrome"]
147
+ },
148
+ {
149
+ key: "chrome-beta",
150
+ product: "Chrome",
151
+ channel: "beta",
152
+ dataDir: join(base, "Google", "Chrome Beta"),
153
+ installPaths: appPaths("Google Chrome Beta.app"),
154
+ executableNames: ["Google Chrome Beta"]
155
+ },
156
+ {
157
+ key: "chrome-canary",
158
+ product: "Chrome",
159
+ channel: "canary",
160
+ dataDir: join(base, "Google", "Chrome Canary"),
161
+ installPaths: appPaths("Google Chrome Canary.app"),
162
+ executableNames: ["Google Chrome Canary"]
163
+ },
164
+ {
165
+ key: "brave-stable",
166
+ product: "Brave",
167
+ channel: "stable",
168
+ dataDir: join(base, "BraveSoftware", "Brave-Browser"),
169
+ installPaths: appPaths("Brave Browser.app"),
170
+ executableNames: ["Brave Browser"]
171
+ },
172
+ {
173
+ key: "edge-stable",
174
+ product: "Edge",
175
+ channel: "stable",
176
+ dataDir: join(base, "Microsoft Edge"),
177
+ installPaths: appPaths("Microsoft Edge.app"),
178
+ executableNames: ["Microsoft Edge"]
179
+ },
180
+ {
181
+ key: "chromium-stable",
182
+ product: "Chromium",
183
+ channel: "stable",
184
+ dataDir: join(base, "Chromium"),
185
+ installPaths: appPaths("Chromium.app"),
186
+ executableNames: ["Chromium"]
187
+ }
188
+ ];
229
189
  }
230
- const uid = currentUid();
231
- if (uid !== void 0 && stats.uid !== uid) {
232
- throw new Error(`Browser Pilot path is not owned by the current user: ${path}`);
190
+ if (os === "linux") {
191
+ return [
192
+ {
193
+ key: "chrome-stable",
194
+ product: "Chrome",
195
+ channel: "stable",
196
+ dataDir: join(home, ".config", "google-chrome"),
197
+ installPaths: linuxInstallPaths(env, ["google-chrome", "google-chrome-stable"]),
198
+ executableNames: ["google-chrome", "google-chrome-stable"]
199
+ },
200
+ {
201
+ key: "chrome-beta",
202
+ product: "Chrome",
203
+ channel: "beta",
204
+ dataDir: join(home, ".config", "google-chrome-beta"),
205
+ installPaths: linuxInstallPaths(env, ["google-chrome-beta"]),
206
+ executableNames: ["google-chrome-beta"]
207
+ },
208
+ {
209
+ key: "chromium-stable",
210
+ product: "Chromium",
211
+ channel: "stable",
212
+ dataDir: join(home, ".config", "chromium"),
213
+ installPaths: linuxInstallPaths(env, ["chromium", "chromium-browser"]),
214
+ executableNames: ["chromium", "chromium-browser"]
215
+ },
216
+ {
217
+ key: "brave-stable",
218
+ product: "Brave",
219
+ channel: "stable",
220
+ dataDir: join(home, ".config", "BraveSoftware", "Brave-Browser"),
221
+ installPaths: linuxInstallPaths(env, ["brave-browser", "brave-browser-stable"]),
222
+ executableNames: ["brave-browser", "brave-browser-stable"]
223
+ },
224
+ {
225
+ key: "edge-stable",
226
+ product: "Edge",
227
+ channel: "stable",
228
+ dataDir: join(home, ".config", "microsoft-edge"),
229
+ installPaths: linuxInstallPaths(env, ["microsoft-edge", "microsoft-edge-stable"]),
230
+ executableNames: ["microsoft-edge", "microsoft-edge-stable"]
231
+ }
232
+ ];
233
233
  }
234
- if (!expectDirectory && process.platform !== "win32" && (stats.mode & 63) !== 0) {
235
- throw new Error(`Browser Pilot metadata is accessible by another OS user: ${path}`);
234
+ if (os === "win32") {
235
+ const local = env.LOCALAPPDATA ?? join(home, "AppData", "Local");
236
+ const programFiles = [env.PROGRAMFILES, env["PROGRAMFILES(X86)"]].filter((value) => Boolean(value));
237
+ return [
238
+ {
239
+ key: "chrome-stable",
240
+ product: "Chrome",
241
+ channel: "stable",
242
+ dataDir: join(local, "Google", "Chrome", "User Data"),
243
+ installPaths: [join(local, "Google", "Chrome", "Application", "chrome.exe"), ...programFiles.map((path) => join(path, "Google", "Chrome", "Application", "chrome.exe"))],
244
+ executableNames: ["chrome.exe"]
245
+ },
246
+ {
247
+ key: "brave-stable",
248
+ product: "Brave",
249
+ channel: "stable",
250
+ dataDir: join(local, "BraveSoftware", "Brave-Browser", "User Data"),
251
+ installPaths: [join(local, "BraveSoftware", "Brave-Browser", "Application", "brave.exe"), ...programFiles.map((path) => join(path, "BraveSoftware", "Brave-Browser", "Application", "brave.exe"))],
252
+ executableNames: ["brave.exe"]
253
+ },
254
+ {
255
+ key: "edge-stable",
256
+ product: "Edge",
257
+ channel: "stable",
258
+ dataDir: join(local, "Microsoft", "Edge", "User Data"),
259
+ installPaths: [join(local, "Microsoft", "Edge", "Application", "msedge.exe"), ...programFiles.map((path) => join(path, "Microsoft", "Edge", "Application", "msedge.exe"))],
260
+ executableNames: ["msedge.exe"]
261
+ }
262
+ ];
236
263
  }
264
+ return [];
237
265
  }
238
- function ensurePrivateDirectorySync(path) {
239
- if (!existsSync(path)) mkdirSync(path, { recursive: true, mode: PRIVATE_DIRECTORY_MODE });
240
- validateOwnedPath(path, true);
241
- if (process.platform !== "win32") chmodSync(path, PRIVATE_DIRECTORY_MODE);
242
- }
243
- function ensureBrokerDirectoriesSync(paths = BROWSER_PILOT_PATHS) {
244
- ensurePrivateDirectorySync(paths.stateDir);
245
- if (paths.runtimeDir !== paths.stateDir) ensurePrivateDirectorySync(paths.runtimeDir);
246
- }
247
- function parseJsonFile(path) {
248
- validateOwnedPath(path, false);
249
- const stats = statSync(path);
250
- if (stats.size < 2 || stats.size > MAX_METADATA_BYTES) throw new Error("Broker metadata has an invalid size");
251
- return JSON.parse(readFileSync(path, "utf8"));
266
+ function stableBrowserId(definition, os) {
267
+ const digest = createHash("sha256").update(`${os}\0${definition.key}\0${definition.dataDir}`).digest("base64url").slice(0, 20);
268
+ return `browser:${definition.key}:${digest}`;
252
269
  }
253
- function isSafePid(value) {
254
- return Number.isSafeInteger(value) && Number(value) > 0;
270
+ function pathExistsWithoutFollowingSymlink(path) {
271
+ try {
272
+ lstatSync(path);
273
+ return true;
274
+ } catch {
275
+ return false;
276
+ }
255
277
  }
256
- function isSafeTimestamp(value) {
257
- return typeof value === "number" && Number.isSafeInteger(value) && value > 0;
278
+ function installed(definition) {
279
+ return pathExistsWithoutFollowingSymlink(definition.dataDir) || definition.installPaths.some(pathExistsWithoutFollowingSymlink);
258
280
  }
259
- function validExecutable(value) {
260
- if (!value || typeof value !== "object" || Array.isArray(value)) return false;
281
+ function readRunningCommands(os) {
282
+ try {
283
+ if (os === "win32") {
284
+ return execFileSync("tasklist.exe", ["/FO", "CSV", "/NH"], {
285
+ encoding: "utf8",
286
+ timeout: 2e3,
287
+ windowsHide: true
288
+ }).split(/\r?\n/).filter(Boolean);
289
+ }
290
+ return execFileSync("ps", ["-axo", "command="], {
291
+ encoding: "utf8",
292
+ timeout: 2e3
293
+ }).split(/\r?\n/).filter(Boolean);
294
+ } catch {
295
+ return null;
296
+ }
297
+ }
298
+ function browserProcessState(definition, commands) {
299
+ for (const lockName of ["SingletonLock", "SingletonSocket", "lockfile"]) {
300
+ if (pathExistsWithoutFollowingSymlink(join(definition.dataDir, lockName))) return "running";
301
+ }
302
+ if (commands === null) return "unknown";
303
+ const names = definition.executableNames.map((name) => name.toLowerCase());
304
+ return commands.some((command) => {
305
+ const normalized = command.toLowerCase();
306
+ return names.some((name) => {
307
+ const macExecutable = `/${name}.app/contents/macos/${name}`;
308
+ if (normalized.includes(macExecutable)) return true;
309
+ const escaped = name.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
310
+ return new RegExp(`(^|[/\\\\"'])${escaped}(?=$|[\\s"',])`, "i").test(normalized);
311
+ });
312
+ }) ? "running" : "not_running";
313
+ }
314
+ function remediation(state, endpointAvailable = false) {
315
+ switch (state) {
316
+ case "not_running":
317
+ return {
318
+ code: "start_browser",
319
+ message: "Start this browser profile, then enable remote debugging from chrome://inspect/#remote-debugging.",
320
+ actionRequired: true
321
+ };
322
+ case "remote_debugging_disabled":
323
+ return {
324
+ code: "enable_remote_debugging",
325
+ message: "Open chrome://inspect/#remote-debugging in this browser and turn on remote debugging.",
326
+ actionRequired: true
327
+ };
328
+ case "authorization_required":
329
+ return {
330
+ code: "authorize_remote_debugging",
331
+ message: "Approve the browser's remote debugging authorization prompt.",
332
+ actionRequired: true
333
+ };
334
+ case "disconnected":
335
+ if (endpointAvailable) {
336
+ return {
337
+ code: "connect_browser",
338
+ message: "Run an explicit browser connect operation to request remote debugging authorization.",
339
+ actionRequired: true
340
+ };
341
+ }
342
+ return {
343
+ code: "restart_remote_debugging",
344
+ message: "The recorded remote debugging endpoint is stale. Restart this browser profile and enable remote debugging again.",
345
+ actionRequired: true
346
+ };
347
+ case "ready":
348
+ return void 0;
349
+ }
350
+ }
351
+ async function discoverBrowserCandidates(options = {}) {
352
+ const os = options.platform ?? platform();
353
+ const definitions = options.profiles ?? supportedBrowserProfiles(options);
354
+ const runningCommands = options.runningCommands === void 0 ? readRunningCommands(os) : options.runningCommands;
355
+ const results = [];
356
+ for (const definition of definitions) {
357
+ if (!installed(definition)) continue;
358
+ const endpoint = readChromeInfo(definition.product, definition.dataDir);
359
+ let processState = browserProcessState(definition, runningCommands);
360
+ let remoteDebuggingState = "disabled";
361
+ let authorizationState = "not_applicable";
362
+ let state;
363
+ if (!endpoint) {
364
+ state = processState === "running" ? "remote_debugging_disabled" : "not_running";
365
+ } else if (processState === "not_running") {
366
+ remoteDebuggingState = "stale";
367
+ authorizationState = "unknown";
368
+ state = "disconnected";
369
+ } else {
370
+ processState = "running";
371
+ remoteDebuggingState = "enabled";
372
+ authorizationState = "unknown";
373
+ state = "disconnected";
374
+ }
375
+ const candidate = {
376
+ id: stableBrowserId(definition, os),
377
+ product: definition.product,
378
+ channel: definition.channel,
379
+ userDataRoot: definition.dataDir,
380
+ processState,
381
+ remoteDebuggingState,
382
+ authorizationState,
383
+ state,
384
+ ...remediation(state, endpoint !== null && remoteDebuggingState === "enabled") ? { remediation: remediation(state, endpoint !== null && remoteDebuggingState === "enabled") } : {}
385
+ };
386
+ results.push({ candidate, dataDir: definition.dataDir, ...endpoint ? { endpoint } : {} });
387
+ }
388
+ return results;
389
+ }
390
+
391
+ // src/client.ts
392
+ import http from "http";
393
+ import { existsSync as existsSync3, readFileSync as readFileSync3 } from "fs";
394
+
395
+ // src/broker-locator.ts
396
+ import { createHash as createHash3, randomUUID } from "crypto";
397
+ import {
398
+ chmodSync,
399
+ closeSync,
400
+ existsSync as existsSync2,
401
+ lstatSync as lstatSync2,
402
+ mkdirSync,
403
+ openSync,
404
+ readFileSync as readFileSync2,
405
+ renameSync,
406
+ realpathSync,
407
+ statSync,
408
+ unlinkSync,
409
+ writeFileSync,
410
+ writeSync
411
+ } from "fs";
412
+ import { basename, dirname, isAbsolute as isAbsolute2, join as join3 } from "path";
413
+
414
+ // src/paths.ts
415
+ import { createHash as createHash2 } from "crypto";
416
+ import { homedir as homedir2, platform as platform2, tmpdir, userInfo } from "os";
417
+ import { isAbsolute, join as join2, win32 } from "path";
418
+ var MAX_PORTABLE_UNIX_SOCKET_BYTES = 96;
419
+ function identityDigest(value) {
420
+ return createHash2("sha256").update(value).digest("hex").slice(0, 16);
421
+ }
422
+ function resolveBrowserPilotPaths(options = {}) {
423
+ const os = options.platform ?? platform2();
424
+ const home = options.homeDir ?? homedir2();
425
+ const env = options.env ?? process.env;
426
+ if (os === "win32") {
427
+ const localAppData = env.LOCALAPPDATA ?? win32.join(home, "AppData", "Local");
428
+ const stateDir2 = env.BROWSER_PILOT_HOME ?? win32.join(localAppData, "Browser Pilot");
429
+ if (!win32.isAbsolute(stateDir2)) throw new Error("BROWSER_PILOT_HOME must be absolute");
430
+ const username = options.username ?? (() => {
431
+ try {
432
+ return userInfo().username;
433
+ } catch {
434
+ return home;
435
+ }
436
+ })();
437
+ const identity = identityDigest(
438
+ `${username.toLowerCase()}\0${home.toLowerCase()}\0${stateDir2.toLowerCase()}`
439
+ );
440
+ return {
441
+ stateDir: stateDir2,
442
+ runtimeDir: stateDir2,
443
+ endpoint: `\\\\.\\pipe\\browser-pilot-${identity}`,
444
+ transport: "windows_pipe",
445
+ locatorFile: win32.join(stateDir2, "broker-locator.json"),
446
+ pidFile: win32.join(stateDir2, "daemon.pid"),
447
+ startupLockFile: win32.join(stateDir2, "startup.lock"),
448
+ versionHistoryFile: win32.join(stateDir2, "broker-versions.json"),
449
+ artifactDir: win32.join(stateDir2, "artifacts"),
450
+ downloadDir: win32.join(stateDir2, "downloads")
451
+ };
452
+ }
453
+ const stateDir = env.BROWSER_PILOT_HOME ?? join2(home, ".browser-pilot");
454
+ if (!isAbsolute(stateDir)) throw new Error("BROWSER_PILOT_HOME must be absolute");
455
+ const defaultEndpoint = join2(stateDir, "daemon.sock");
456
+ const uid = options.uid ?? process.getuid?.() ?? 0;
457
+ const useShortRuntimePath = Buffer.byteLength(defaultEndpoint) > MAX_PORTABLE_UNIX_SOCKET_BYTES;
458
+ let runtimeDir = useShortRuntimePath ? join2(options.tempDir ?? tmpdir(), `browser-pilot-${uid}-${identityDigest(stateDir)}`) : stateDir;
459
+ let endpoint = join2(runtimeDir, "daemon.sock");
460
+ if (Buffer.byteLength(endpoint) > MAX_PORTABLE_UNIX_SOCKET_BYTES) {
461
+ runtimeDir = join2("/tmp", `browser-pilot-${uid}-${identityDigest(stateDir)}`);
462
+ endpoint = join2(runtimeDir, "daemon.sock");
463
+ }
464
+ if (Buffer.byteLength(endpoint) > MAX_PORTABLE_UNIX_SOCKET_BYTES) {
465
+ throw new Error("Browser Pilot cannot resolve a portable Unix socket path");
466
+ }
467
+ if (!isAbsolute(endpoint)) throw new Error("Browser Pilot endpoint must be absolute");
468
+ return {
469
+ stateDir,
470
+ runtimeDir,
471
+ endpoint,
472
+ transport: "unix_socket",
473
+ locatorFile: join2(stateDir, "broker-locator.json"),
474
+ pidFile: join2(stateDir, "daemon.pid"),
475
+ startupLockFile: join2(stateDir, "startup.lock"),
476
+ versionHistoryFile: join2(stateDir, "broker-versions.json"),
477
+ artifactDir: join2(stateDir, "artifacts"),
478
+ downloadDir: join2(stateDir, "downloads")
479
+ };
480
+ }
481
+ var BROWSER_PILOT_PATHS = resolveBrowserPilotPaths();
482
+ var STATE_DIR = BROWSER_PILOT_PATHS.stateDir;
483
+ var RUNTIME_DIR = BROWSER_PILOT_PATHS.runtimeDir;
484
+ var SOCKET_PATH = BROWSER_PILOT_PATHS.endpoint;
485
+ var BROKER_TRANSPORT = BROWSER_PILOT_PATHS.transport;
486
+ var LOCATOR_FILE = BROWSER_PILOT_PATHS.locatorFile;
487
+ var PID_FILE = BROWSER_PILOT_PATHS.pidFile;
488
+ var STARTUP_LOCK_FILE = BROWSER_PILOT_PATHS.startupLockFile;
489
+ var ARTIFACT_DIR = BROWSER_PILOT_PATHS.artifactDir;
490
+ var DOWNLOAD_DIR = BROWSER_PILOT_PATHS.downloadDir;
491
+
492
+ // src/broker-locator.ts
493
+ var PRIVATE_DIRECTORY_MODE = 448;
494
+ var PRIVATE_FILE_MODE = 384;
495
+ var MAX_METADATA_BYTES = 16 * 1024;
496
+ function currentUid() {
497
+ return process.getuid?.();
498
+ }
499
+ function validateOwnedPath(path, expectDirectory) {
500
+ const stats = lstatSync2(path);
501
+ if (stats.isSymbolicLink()) throw new Error(`Browser Pilot path must not be a symbolic link: ${path}`);
502
+ if (expectDirectory ? !stats.isDirectory() : !stats.isFile()) {
503
+ throw new Error(`Browser Pilot path has an unexpected type: ${path}`);
504
+ }
505
+ const uid = currentUid();
506
+ if (uid !== void 0 && stats.uid !== uid) {
507
+ throw new Error(`Browser Pilot path is not owned by the current user: ${path}`);
508
+ }
509
+ if (!expectDirectory && process.platform !== "win32" && (stats.mode & 63) !== 0) {
510
+ throw new Error(`Browser Pilot metadata is accessible by another OS user: ${path}`);
511
+ }
512
+ }
513
+ function ensurePrivateDirectorySync(path) {
514
+ if (!existsSync2(path)) mkdirSync(path, { recursive: true, mode: PRIVATE_DIRECTORY_MODE });
515
+ validateOwnedPath(path, true);
516
+ if (process.platform !== "win32") chmodSync(path, PRIVATE_DIRECTORY_MODE);
517
+ }
518
+ function ensureBrokerDirectoriesSync(paths = BROWSER_PILOT_PATHS) {
519
+ ensurePrivateDirectorySync(paths.stateDir);
520
+ if (paths.runtimeDir !== paths.stateDir) ensurePrivateDirectorySync(paths.runtimeDir);
521
+ }
522
+ function parseJsonFile(path) {
523
+ validateOwnedPath(path, false);
524
+ const stats = statSync(path);
525
+ if (stats.size < 2 || stats.size > MAX_METADATA_BYTES) throw new Error("Broker metadata has an invalid size");
526
+ return JSON.parse(readFileSync2(path, "utf8"));
527
+ }
528
+ function isSafePid(value) {
529
+ return Number.isSafeInteger(value) && Number(value) > 0;
530
+ }
531
+ function isSafeTimestamp(value) {
532
+ return typeof value === "number" && Number.isSafeInteger(value) && value > 0;
533
+ }
534
+ function validExecutable(value) {
535
+ if (!value || typeof value !== "object" || Array.isArray(value)) return false;
261
536
  const record = value;
262
537
  return typeof record.version === "string" && record.version.length > 0 && record.version.length <= 128 && typeof record.path === "string" && record.path.length > 0 && record.path.length <= 4096 && isAbsolute2(record.path) && typeof record.identity === "string" && /^executable:[a-f0-9]{64}$/.test(record.identity);
263
538
  }
@@ -283,14 +558,14 @@ function validLocator(value, paths) {
283
558
  function createExecutableMetadataSync(version, path) {
284
559
  if (!version || version.length > 128) throw new Error("Invalid Browser Pilot executable version");
285
560
  const resolvedPath = realpathSync(path);
286
- const digest = createHash2("sha256").update(`${resolvedPath}\0${version}\0`);
561
+ const digest = createHash3("sha256").update(`${resolvedPath}\0${version}\0`);
287
562
  for (const artifactPath of [
288
563
  resolvedPath,
289
- join2(dirname(resolvedPath), "daemon.js"),
290
- join2(dirname(resolvedPath), "managed-target-janitor.js")
564
+ join3(dirname(resolvedPath), "daemon.js"),
565
+ join3(dirname(resolvedPath), "managed-target-janitor.js")
291
566
  ]) {
292
- if (!existsSync(artifactPath)) continue;
293
- digest.update(basename(artifactPath)).update("\0").update(readFileSync(artifactPath)).update("\0");
567
+ if (!existsSync2(artifactPath)) continue;
568
+ digest.update(basename(artifactPath)).update("\0").update(readFileSync2(artifactPath)).update("\0");
294
569
  }
295
570
  return {
296
571
  version,
@@ -299,7 +574,7 @@ function createExecutableMetadataSync(version, path) {
299
574
  };
300
575
  }
301
576
  function readBrokerLocatorSync(paths = BROWSER_PILOT_PATHS) {
302
- if (!existsSync(paths.locatorFile)) return void 0;
577
+ if (!existsSync2(paths.locatorFile)) return void 0;
303
578
  try {
304
579
  const value = parseJsonFile(paths.locatorFile);
305
580
  return validLocator(value, paths) ? value : void 0;
@@ -310,7 +585,7 @@ function readBrokerLocatorSync(paths = BROWSER_PILOT_PATHS) {
310
585
  function readBrokerPidSync(paths = BROWSER_PILOT_PATHS) {
311
586
  const locator = readBrokerLocatorSync(paths);
312
587
  if (locator) return locator.pid;
313
- if (!existsSync(paths.pidFile)) return void 0;
588
+ if (!existsSync2(paths.pidFile)) return void 0;
314
589
  try {
315
590
  const value = parseJsonFile(paths.pidFile);
316
591
  if (isSafePid(value)) return value;
@@ -321,7 +596,7 @@ function readBrokerPidSync(paths = BROWSER_PILOT_PATHS) {
321
596
  } catch {
322
597
  try {
323
598
  validateOwnedPath(paths.pidFile, false);
324
- const pid = Number.parseInt(readFileSync(paths.pidFile, "utf8").trim(), 10);
599
+ const pid = Number.parseInt(readFileSync2(paths.pidFile, "utf8").trim(), 10);
325
600
  return isSafePid(pid) ? pid : void 0;
326
601
  } catch {
327
602
  }
@@ -329,7 +604,7 @@ function readBrokerPidSync(paths = BROWSER_PILOT_PATHS) {
329
604
  return void 0;
330
605
  }
331
606
  function readBrokerStartingSync(paths = BROWSER_PILOT_PATHS) {
332
- if (!existsSync(paths.pidFile) || existsSync(paths.locatorFile)) return void 0;
607
+ if (!existsSync2(paths.pidFile) || existsSync2(paths.locatorFile)) return void 0;
333
608
  try {
334
609
  const value = parseJsonFile(paths.pidFile);
335
610
  if (!value || typeof value !== "object" || Array.isArray(value)) return void 0;
@@ -350,7 +625,7 @@ function processIsAlive(pid) {
350
625
  }
351
626
  }
352
627
  function readStartupLock(path) {
353
- if (!existsSync(path)) return void 0;
628
+ if (!existsSync2(path)) return void 0;
354
629
  try {
355
630
  const value = parseJsonFile(path);
356
631
  if (!value || typeof value !== "object" || Array.isArray(value)) return void 0;
@@ -426,7 +701,7 @@ async function acquireBrokerStartupLock(options = {}) {
426
701
  stale = !alive(owner.pid);
427
702
  } else {
428
703
  try {
429
- stale = now() - lstatSync(paths.startupLockFile).mtimeMs > staleAfterMs;
704
+ stale = now() - lstatSync2(paths.startupLockFile).mtimeMs > staleAfterMs;
430
705
  } catch {
431
706
  stale = true;
432
707
  }
@@ -453,14 +728,60 @@ function removeStaleBrokerFilesSync(paths = BROWSER_PILOT_PATHS) {
453
728
  }
454
729
  }
455
730
 
731
+ // src/services/broker-request-timeout.ts
732
+ var MIN_BROKER_REQUEST_TIMEOUT_MS = 6e4;
733
+ var BROKER_REQUEST_TIMEOUT_MARGIN_MS = 5e3;
734
+ function isRecord(value) {
735
+ return typeof value === "object" && value !== null && !Array.isArray(value);
736
+ }
737
+ function brokerRequestTimeoutMs(method, params) {
738
+ if (method !== "tools/call" || !isRecord(params)) return MIN_BROKER_REQUEST_TIMEOUT_MS;
739
+ const deadlineMs = params.deadlineMs;
740
+ if (!Number.isSafeInteger(deadlineMs) || Number(deadlineMs) < 1) {
741
+ return MIN_BROKER_REQUEST_TIMEOUT_MS;
742
+ }
743
+ return Math.max(
744
+ MIN_BROKER_REQUEST_TIMEOUT_MS,
745
+ Number(deadlineMs) + BROKER_REQUEST_TIMEOUT_MARGIN_MS
746
+ );
747
+ }
748
+ function brokerRequestTimeoutError(method, params) {
749
+ if (method !== "tools/call" || !isRecord(params)) {
750
+ return new BrowserPilotError(
751
+ "browser_disconnected",
752
+ "Timed out waiting for the Browser Pilot Broker",
753
+ { retryable: true }
754
+ );
755
+ }
756
+ const commandId = typeof params.commandId === "string" ? params.commandId : void 0;
757
+ const toolName = typeof params.name === "string" ? params.name : void 0;
758
+ return new BrowserPilotError(
759
+ "unknown_outcome",
760
+ "Timed out waiting for the Browser Pilot command result",
761
+ {
762
+ retryable: true,
763
+ context: {
764
+ ...commandId ? { commandId } : {},
765
+ ...toolName ? { method: toolName } : {}
766
+ },
767
+ remediation: {
768
+ code: "inspect_command_before_retry",
769
+ message: commandId ? `Run bp command ${commandId} and inspect browser state before retrying.` : "Inspect recent commands and browser state before retrying.",
770
+ actionRequired: true
771
+ }
772
+ }
773
+ );
774
+ }
775
+
456
776
  // src/client.ts
457
777
  var BROKER_SHUTDOWN_TIMEOUT_MS = 15e3;
778
+ var BROKER_RPC_VERSION = 2;
458
779
  function isDaemonRunning() {
459
780
  const locator = readBrokerLocatorSync();
460
781
  if (locator) return processIsAlive(locator.pid);
461
- if (!existsSync2(BROWSER_PILOT_PATHS.pidFile)) return false;
782
+ if (!existsSync3(BROWSER_PILOT_PATHS.pidFile)) return false;
462
783
  try {
463
- const raw = readFileSync2(BROWSER_PILOT_PATHS.pidFile, "utf-8").trim();
784
+ const raw = readFileSync3(BROWSER_PILOT_PATHS.pidFile, "utf-8").trim();
464
785
  const parsed = raw.startsWith("{") ? JSON.parse(raw) : Number.parseInt(raw, 10);
465
786
  const pid = typeof parsed === "number" ? parsed : parsed && typeof parsed === "object" && !Array.isArray(parsed) ? Number(parsed.pid) : Number.NaN;
466
787
  return processIsAlive(pid);
@@ -473,7 +794,7 @@ var DaemonClient = class {
473
794
  constructor() {
474
795
  this.endpoint = readBrokerLocatorSync()?.endpoint ?? BROWSER_PILOT_PATHS.endpoint;
475
796
  }
476
- request(path, body, signal) {
797
+ request(path, body, signal, timeoutMs = MIN_BROKER_REQUEST_TIMEOUT_MS) {
477
798
  return new Promise((resolve, reject) => {
478
799
  const req = http.request(
479
800
  {
@@ -481,7 +802,7 @@ var DaemonClient = class {
481
802
  path,
482
803
  method: body !== void 0 ? "POST" : "GET",
483
804
  headers: body !== void 0 ? { "Content-Type": "application/json" } : {},
484
- timeout: 6e4,
805
+ timeout: timeoutMs,
485
806
  signal
486
807
  },
487
808
  (res) => {
@@ -507,7 +828,13 @@ var DaemonClient = class {
507
828
  });
508
829
  req.on("timeout", () => {
509
830
  req.destroy();
510
- reject(new Error("Daemon request timeout"));
831
+ reject(
832
+ path === "/broker/rpc" && body && typeof body.method === "string" ? brokerRequestTimeoutError(body.method, body.params) : new BrowserPilotError(
833
+ "browser_disconnected",
834
+ "Timed out waiting for the Browser Pilot Broker",
835
+ { retryable: true }
836
+ )
837
+ );
511
838
  });
512
839
  if (body !== void 0) req.write(JSON.stringify(body));
513
840
  req.end();
@@ -553,22 +880,12 @@ var DaemonClient = class {
553
880
  }
554
881
  );
555
882
  }
556
- async brokerCall(bridgeSessionId, method, params) {
883
+ async brokerCall(clientSessionId, method, params) {
557
884
  return this.request("/broker/rpc", {
558
- bridgeSessionId,
885
+ clientSessionId,
559
886
  method,
560
887
  ...params !== void 0 ? { params } : {}
561
- });
562
- }
563
- async brokerDisconnect(bridgeSessionId) {
564
- await this.request("/broker/disconnect", { bridgeSessionId });
565
- }
566
- async brokerNextNotification(bridgeSessionId, waitMs, signal) {
567
- const result = await this.request("/broker/events/next", {
568
- bridgeSessionId,
569
- waitMs
570
- }, signal);
571
- return result.notification;
888
+ }, void 0, brokerRequestTimeoutMs(method, params));
572
889
  }
573
890
  async discoveredTargets() {
574
891
  const res = await this.request("/discovered");
@@ -598,648 +915,42 @@ var DaemonClient = class {
598
915
  }
599
916
  async netRequests(opts) {
600
917
  const p = new URLSearchParams();
601
- if (opts?.limit) p.set("limit", String(opts.limit));
602
- if (opts?.url) p.set("url", opts.url);
603
- if (opts?.method) p.set("method", opts.method);
604
- if (opts?.status) p.set("status", opts.status);
605
- if (opts?.type) p.set("type", opts.type);
606
- if (opts?.after) p.set("after", String(opts.after));
607
- const qs = p.toString();
608
- return this.request(`/net/requests${qs ? "?" + qs : ""}`);
609
- }
610
- async netRequestDetail(id) {
611
- return this.request(`/net/request/${id}`);
612
- }
613
- async netBody(id) {
614
- return this.request(`/net/body/${id}`);
615
- }
616
- async netClear() {
617
- await this.request("/net/clear", {});
618
- }
619
- async netAddRule(rule) {
620
- return this.request("/net/rules", rule);
621
- }
622
- async netRules() {
623
- return this.request("/net/rules");
624
- }
625
- async netRemoveRule(id) {
626
- await this.request("/net/rules/remove", id !== void 0 ? { id } : { all: true });
627
- }
628
- close() {
629
- }
630
- };
631
-
632
- // src/chrome.ts
633
- import { createHash as createHash3 } from "crypto";
634
- import { execFileSync } from "child_process";
635
- import { existsSync as existsSync3, lstatSync as lstatSync2, readFileSync as readFileSync3 } from "fs";
636
- import { connect as connectTcp } from "net";
637
- import { delimiter, join as join3 } from "path";
638
- import { homedir as homedir2, platform as platform2 } from "os";
639
- import WebSocket from "ws";
640
- function readChromeInfo(browser, dataDir) {
641
- const portFile = join3(dataDir, "DevToolsActivePort");
642
- if (!existsSync3(portFile)) return null;
643
- try {
644
- const lines = readFileSync3(portFile, "utf-8").trim().split(/\r?\n/);
645
- if (lines.length < 2) return null;
646
- const port = Number.parseInt(lines[0], 10);
647
- const wsPath = lines[1];
648
- if (!Number.isSafeInteger(port) || port < 1 || port > 65535 || !wsPath.startsWith("/")) return null;
649
- return {
650
- port,
651
- wsPath,
652
- wsUrl: `ws://127.0.0.1:${port}${wsPath}`,
653
- browser,
654
- dataDir
655
- };
656
- } catch {
657
- return null;
658
- }
659
- }
660
- function executablePaths(env) {
661
- return (env.PATH ?? "").split(delimiter).filter(Boolean);
662
- }
663
- function linuxInstallPaths(env, names) {
664
- return executablePaths(env).flatMap((directory) => names.map((name) => join3(directory, name)));
665
- }
666
- function supportedBrowserProfiles(options = {}) {
667
- const home = options.homeDir ?? homedir2();
668
- const os = options.platform ?? platform2();
669
- const env = options.env ?? process.env;
670
- if (os === "darwin") {
671
- const base = join3(home, "Library", "Application Support");
672
- const applications = ["/Applications", join3(home, "Applications")];
673
- const appPaths = (name) => applications.map((directory) => join3(directory, name));
674
- return [
675
- {
676
- key: "chrome-stable",
677
- product: "Chrome",
678
- channel: "stable",
679
- dataDir: join3(base, "Google", "Chrome"),
680
- installPaths: appPaths("Google Chrome.app"),
681
- executableNames: ["Google Chrome"]
682
- },
683
- {
684
- key: "chrome-beta",
685
- product: "Chrome",
686
- channel: "beta",
687
- dataDir: join3(base, "Google", "Chrome Beta"),
688
- installPaths: appPaths("Google Chrome Beta.app"),
689
- executableNames: ["Google Chrome Beta"]
690
- },
691
- {
692
- key: "chrome-canary",
693
- product: "Chrome",
694
- channel: "canary",
695
- dataDir: join3(base, "Google", "Chrome Canary"),
696
- installPaths: appPaths("Google Chrome Canary.app"),
697
- executableNames: ["Google Chrome Canary"]
698
- },
699
- {
700
- key: "brave-stable",
701
- product: "Brave",
702
- channel: "stable",
703
- dataDir: join3(base, "BraveSoftware", "Brave-Browser"),
704
- installPaths: appPaths("Brave Browser.app"),
705
- executableNames: ["Brave Browser"]
706
- },
707
- {
708
- key: "edge-stable",
709
- product: "Edge",
710
- channel: "stable",
711
- dataDir: join3(base, "Microsoft Edge"),
712
- installPaths: appPaths("Microsoft Edge.app"),
713
- executableNames: ["Microsoft Edge"]
714
- },
715
- {
716
- key: "chromium-stable",
717
- product: "Chromium",
718
- channel: "stable",
719
- dataDir: join3(base, "Chromium"),
720
- installPaths: appPaths("Chromium.app"),
721
- executableNames: ["Chromium"]
722
- }
723
- ];
724
- }
725
- if (os === "linux") {
726
- return [
727
- {
728
- key: "chrome-stable",
729
- product: "Chrome",
730
- channel: "stable",
731
- dataDir: join3(home, ".config", "google-chrome"),
732
- installPaths: linuxInstallPaths(env, ["google-chrome", "google-chrome-stable"]),
733
- executableNames: ["google-chrome", "google-chrome-stable"]
734
- },
735
- {
736
- key: "chrome-beta",
737
- product: "Chrome",
738
- channel: "beta",
739
- dataDir: join3(home, ".config", "google-chrome-beta"),
740
- installPaths: linuxInstallPaths(env, ["google-chrome-beta"]),
741
- executableNames: ["google-chrome-beta"]
742
- },
743
- {
744
- key: "chromium-stable",
745
- product: "Chromium",
746
- channel: "stable",
747
- dataDir: join3(home, ".config", "chromium"),
748
- installPaths: linuxInstallPaths(env, ["chromium", "chromium-browser"]),
749
- executableNames: ["chromium", "chromium-browser"]
750
- },
751
- {
752
- key: "brave-stable",
753
- product: "Brave",
754
- channel: "stable",
755
- dataDir: join3(home, ".config", "BraveSoftware", "Brave-Browser"),
756
- installPaths: linuxInstallPaths(env, ["brave-browser", "brave-browser-stable"]),
757
- executableNames: ["brave-browser", "brave-browser-stable"]
758
- },
759
- {
760
- key: "edge-stable",
761
- product: "Edge",
762
- channel: "stable",
763
- dataDir: join3(home, ".config", "microsoft-edge"),
764
- installPaths: linuxInstallPaths(env, ["microsoft-edge", "microsoft-edge-stable"]),
765
- executableNames: ["microsoft-edge", "microsoft-edge-stable"]
766
- }
767
- ];
768
- }
769
- if (os === "win32") {
770
- const local = env.LOCALAPPDATA ?? join3(home, "AppData", "Local");
771
- const programFiles = [env.PROGRAMFILES, env["PROGRAMFILES(X86)"]].filter((value) => Boolean(value));
772
- return [
773
- {
774
- key: "chrome-stable",
775
- product: "Chrome",
776
- channel: "stable",
777
- dataDir: join3(local, "Google", "Chrome", "User Data"),
778
- installPaths: [join3(local, "Google", "Chrome", "Application", "chrome.exe"), ...programFiles.map((path) => join3(path, "Google", "Chrome", "Application", "chrome.exe"))],
779
- executableNames: ["chrome.exe"]
780
- },
781
- {
782
- key: "brave-stable",
783
- product: "Brave",
784
- channel: "stable",
785
- dataDir: join3(local, "BraveSoftware", "Brave-Browser", "User Data"),
786
- installPaths: [join3(local, "BraveSoftware", "Brave-Browser", "Application", "brave.exe"), ...programFiles.map((path) => join3(path, "BraveSoftware", "Brave-Browser", "Application", "brave.exe"))],
787
- executableNames: ["brave.exe"]
788
- },
789
- {
790
- key: "edge-stable",
791
- product: "Edge",
792
- channel: "stable",
793
- dataDir: join3(local, "Microsoft", "Edge", "User Data"),
794
- installPaths: [join3(local, "Microsoft", "Edge", "Application", "msedge.exe"), ...programFiles.map((path) => join3(path, "Microsoft", "Edge", "Application", "msedge.exe"))],
795
- executableNames: ["msedge.exe"]
796
- }
797
- ];
798
- }
799
- return [];
800
- }
801
- function stableBrowserId(definition, os) {
802
- const digest = createHash3("sha256").update(`${os}\0${definition.key}\0${definition.dataDir}`).digest("base64url").slice(0, 20);
803
- return `browser:${definition.key}:${digest}`;
804
- }
805
- function pathExistsWithoutFollowingSymlink(path) {
806
- try {
807
- lstatSync2(path);
808
- return true;
809
- } catch {
810
- return false;
811
- }
812
- }
813
- function installed(definition) {
814
- return pathExistsWithoutFollowingSymlink(definition.dataDir) || definition.installPaths.some(pathExistsWithoutFollowingSymlink);
815
- }
816
- function readRunningCommands(os) {
817
- try {
818
- if (os === "win32") {
819
- return execFileSync("tasklist.exe", ["/FO", "CSV", "/NH"], {
820
- encoding: "utf8",
821
- timeout: 2e3,
822
- windowsHide: true
823
- }).split(/\r?\n/).filter(Boolean);
824
- }
825
- return execFileSync("ps", ["-axo", "command="], {
826
- encoding: "utf8",
827
- timeout: 2e3
828
- }).split(/\r?\n/).filter(Boolean);
829
- } catch {
830
- return null;
831
- }
832
- }
833
- function browserProcessState(definition, commands) {
834
- for (const lockName of ["SingletonLock", "SingletonSocket", "lockfile"]) {
835
- if (pathExistsWithoutFollowingSymlink(join3(definition.dataDir, lockName))) return "running";
836
- }
837
- if (commands === null) return "unknown";
838
- const names = definition.executableNames.map((name) => name.toLowerCase());
839
- return commands.some((command) => {
840
- const normalized = command.toLowerCase();
841
- return names.some((name) => {
842
- const macExecutable = `/${name}.app/contents/macos/${name}`;
843
- if (normalized.includes(macExecutable)) return true;
844
- const escaped = name.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
845
- return new RegExp(`(^|[/\\\\"'])${escaped}(?=$|[\\s"',])`, "i").test(normalized);
846
- });
847
- }) ? "running" : "not_running";
848
- }
849
- function remediation(state, endpointAvailable = false) {
850
- switch (state) {
851
- case "not_running":
852
- return {
853
- code: "start_browser",
854
- message: "Start this browser profile, then enable remote debugging from chrome://inspect/#remote-debugging.",
855
- actionRequired: true
856
- };
857
- case "remote_debugging_disabled":
858
- return {
859
- code: "enable_remote_debugging",
860
- message: "Open chrome://inspect/#remote-debugging in this browser and turn on remote debugging.",
861
- actionRequired: true
862
- };
863
- case "authorization_required":
864
- return {
865
- code: "authorize_remote_debugging",
866
- message: "Approve the browser's remote debugging authorization prompt.",
867
- actionRequired: true
868
- };
869
- case "disconnected":
870
- if (endpointAvailable) {
871
- return {
872
- code: "connect_browser",
873
- message: "Run an explicit browser connect operation to request remote debugging authorization.",
874
- actionRequired: true
875
- };
876
- }
877
- return {
878
- code: "restart_remote_debugging",
879
- message: "The recorded remote debugging endpoint is stale. Restart this browser profile and enable remote debugging again.",
880
- actionRequired: true
881
- };
882
- case "ready":
883
- return void 0;
884
- }
885
- }
886
- async function discoverBrowserCandidates(options = {}) {
887
- const os = options.platform ?? platform2();
888
- const definitions = options.profiles ?? supportedBrowserProfiles(options);
889
- const runningCommands = options.runningCommands === void 0 ? readRunningCommands(os) : options.runningCommands;
890
- const results = [];
891
- for (const definition of definitions) {
892
- if (!installed(definition)) continue;
893
- const endpoint = readChromeInfo(definition.product, definition.dataDir);
894
- let processState = browserProcessState(definition, runningCommands);
895
- let remoteDebuggingState = "disabled";
896
- let authorizationState = "not_applicable";
897
- let state;
898
- if (!endpoint) {
899
- state = processState === "running" ? "remote_debugging_disabled" : "not_running";
900
- } else if (processState === "not_running") {
901
- remoteDebuggingState = "stale";
902
- authorizationState = "unknown";
903
- state = "disconnected";
904
- } else {
905
- processState = "running";
906
- remoteDebuggingState = "enabled";
907
- authorizationState = "unknown";
908
- state = "disconnected";
909
- }
910
- const candidate = {
911
- id: stableBrowserId(definition, os),
912
- product: definition.product,
913
- channel: definition.channel,
914
- profile: definition.dataDir,
915
- processState,
916
- remoteDebuggingState,
917
- authorizationState,
918
- state,
919
- ...remediation(state, endpoint !== null && remoteDebuggingState === "enabled") ? { remediation: remediation(state, endpoint !== null && remoteDebuggingState === "enabled") } : {}
920
- };
921
- results.push({ candidate, dataDir: definition.dataDir, ...endpoint ? { endpoint } : {} });
922
- }
923
- return results;
924
- }
925
-
926
- // src/page-scripts.ts
927
- var CLICK_TARGET_STATE_FUNCTION = `function() {
928
- const target = this;
929
- if (!target || target.nodeType !== 1 || !target.isConnected) {
930
- return {connected:false, kind:'other', focused:false};
931
- }
932
- const composedParent = node => {
933
- if (node && node.parentElement) return node.parentElement;
934
- const root = node && typeof node.getRootNode === 'function' ? node.getRootNode() : null;
935
- return root && root.host ? root.host : null;
936
- };
937
- const composedContains = (container, node) => {
938
- for (let current = node; current; current = composedParent(current)) {
939
- if (current === container) return true;
940
- }
941
- return false;
942
- };
943
- const isShadowHostOf = (node, possibleHost) => {
944
- let root = node && typeof node.getRootNode === 'function' ? node.getRootNode() : null;
945
- while (root && root.host) {
946
- if (root.host === possibleHost) return true;
947
- root = typeof root.host.getRootNode === 'function' ? root.host.getRootNode() : null;
948
- }
949
- return false;
950
- };
951
- const token = (value, mixed) => {
952
- const normalized = String(value || '').trim().toLowerCase();
953
- if (normalized === 'true') return true;
954
- if (normalized === 'false') return false;
955
- return mixed && normalized === 'mixed' ? 'mixed' : undefined;
956
- };
957
- const tag = String(target.tagName || '').toLowerCase();
958
- const role = String(target.getAttribute && target.getAttribute('role') || '').trim().toLowerCase();
959
- const inputType = tag === 'input' ? String(target.type || '').toLowerCase() : '';
960
- let kind = 'other';
961
- if (inputType === 'checkbox' || role === 'checkbox' || role === 'menuitemcheckbox') kind = 'checkbox';
962
- else if (inputType === 'radio' || role === 'radio' || role === 'menuitemradio') kind = 'radio';
963
- else if (role === 'switch') kind = 'switch';
964
- else if (tag === 'option' || role === 'option') kind = 'option';
965
- else if (tag === 'select' || role === 'listbox' || role === 'combobox') kind = 'select';
966
- else if (
967
- tag === 'button' || tag === 'a' || tag === 'input' || tag === 'textarea' ||
968
- role === 'button' || role === 'link' || role === 'tab' || role === 'menuitem'
969
- ) kind = 'control';
970
-
971
- const state = {connected:true, kind, focused:false};
972
- if ((kind === 'checkbox' || kind === 'radio') && tag === 'input') {
973
- state.checked = !!target.checked;
974
- } else if (kind === 'checkbox' || kind === 'radio' || kind === 'switch') {
975
- const checked = token(target.getAttribute('aria-checked'), true);
976
- if (checked !== undefined) state.checked = checked;
977
- }
978
- if (kind === 'option' && tag === 'option') state.selected = !!target.selected;
979
- else {
980
- const selected = token(target.getAttribute('aria-selected'), false);
981
- if (selected !== undefined) state.selected = selected === true;
982
- }
983
- const pressed = token(target.getAttribute('aria-pressed'), true);
984
- if (pressed !== undefined) state.pressed = pressed;
985
- const expanded = token(target.getAttribute('aria-expanded'), false);
986
- if (expanded !== undefined) state.expanded = expanded === true;
987
-
988
- let active = document.activeElement;
989
- for (let depth = 0; depth < 32 && active && active.shadowRoot && active.shadowRoot.activeElement; depth += 1) {
990
- active = active.shadowRoot.activeElement;
991
- }
992
- state.focused = active === target || composedContains(target, active) || isShadowHostOf(target, active);
993
- return state;
994
- }`;
995
- var GET_POINTER_TARGET_STATE = `function() {
996
- const blocked = (reason, obstruction) => ({
997
- status: 'blocked',
998
- reason,
999
- ...(obstruction ? {obstruction} : {}),
1000
- });
1001
- const composedParent = node => {
1002
- if (node && node.parentElement) return node.parentElement;
1003
- const root = node && typeof node.getRootNode === 'function' ? node.getRootNode() : null;
1004
- return root && root.host ? root.host : null;
1005
- };
1006
- const composedContains = (container, node) => {
1007
- for (let current = node; current; current = composedParent(current)) {
1008
- if (current === container) return true;
1009
- }
1010
- return false;
1011
- };
1012
- const isShadowHostOf = (node, possibleHost) => {
1013
- let root = node && typeof node.getRootNode === 'function' ? node.getRootNode() : null;
1014
- while (root && root.host) {
1015
- if (root.host === possibleHost) return true;
1016
- root = typeof root.host.getRootNode === 'function' ? root.host.getRootNode() : null;
1017
- }
1018
- return false;
1019
- };
1020
- const describes = node => {
1021
- if (!node || typeof node.tagName !== 'string') return undefined;
1022
- const tagName = node.tagName.toLowerCase().slice(0, 40);
1023
- const rawRole = typeof node.getAttribute === 'function' ? node.getAttribute('role') : null;
1024
- const role = typeof rawRole === 'string' ? rawRole.trim().slice(0, 40) : '';
1025
- return role ? {tagName, role} : {tagName};
1026
- };
1027
-
1028
- const target = this;
1029
- if (!target || target.nodeType !== 1 || !target.isConnected) return blocked('detached');
1030
- target.scrollIntoView({block:'center', inline:'center', behavior:'instant'});
1031
- if (!target.isConnected) return blocked('detached');
1032
-
1033
- const style = getComputedStyle(target);
1034
- if (style.display === 'none' || style.visibility === 'hidden' || style.visibility === 'collapse') {
1035
- return blocked('no_layout');
918
+ if (opts?.limit) p.set("limit", String(opts.limit));
919
+ if (opts?.url) p.set("url", opts.url);
920
+ if (opts?.method) p.set("method", opts.method);
921
+ if (opts?.status) p.set("status", opts.status);
922
+ if (opts?.type) p.set("type", opts.type);
923
+ if (opts?.after) p.set("after", String(opts.after));
924
+ const qs = p.toString();
925
+ return this.request(`/net/requests${qs ? "?" + qs : ""}`);
1036
926
  }
1037
-
1038
- for (let current = target; current; current = composedParent(current)) {
1039
- const ariaDisabled = typeof current.getAttribute === 'function'
1040
- ? current.getAttribute('aria-disabled') : null;
1041
- const nativeDisabled = typeof current.matches === 'function' && current.matches(':disabled');
1042
- if (nativeDisabled || current.inert === true || String(ariaDisabled).trim().toLowerCase() === 'true') {
1043
- return blocked('disabled');
1044
- }
927
+ async netRequestDetail(id) {
928
+ return this.request(`/net/request/${id}`);
1045
929
  }
1046
-
1047
- const viewportWidth = Math.max(0, window.innerWidth || document.documentElement.clientWidth || 0);
1048
- const viewportHeight = Math.max(0, window.innerHeight || document.documentElement.clientHeight || 0);
1049
- const rectList = target.getClientRects();
1050
- const rects = [];
1051
- for (let index = 0; index < Math.min(rectList.length, 64); index += 1) {
1052
- const rect = rectList[index];
1053
- if (
1054
- Number.isFinite(rect.left) && Number.isFinite(rect.top) &&
1055
- Number.isFinite(rect.right) && Number.isFinite(rect.bottom) &&
1056
- rect.width > 0 && rect.height > 0
1057
- ) rects.push(rect);
1058
- }
1059
- if (rects.length === 0) return blocked('no_layout');
1060
-
1061
- const visibleRects = rects.map(rect => ({
1062
- left: Math.max(0, rect.left),
1063
- top: Math.max(0, rect.top),
1064
- right: Math.min(viewportWidth, rect.right),
1065
- bottom: Math.min(viewportHeight, rect.bottom),
1066
- })).filter(rect => rect.right > rect.left && rect.bottom > rect.top)
1067
- .sort((a, b) => ((b.right - b.left) * (b.bottom - b.top)) - ((a.right - a.left) * (a.bottom - a.top)))
1068
- .slice(0, 4);
1069
- if (visibleRects.length === 0) return blocked('outside_viewport');
1070
-
1071
- const labels = [];
1072
- if (target.labels && typeof target.labels.length === 'number') {
1073
- for (let index = 0; index < Math.min(target.labels.length, 32); index += 1) {
1074
- labels.push(target.labels[index]);
1075
- }
930
+ async netBody(id) {
931
+ return this.request(`/net/body/${id}`);
1076
932
  }
1077
- for (let current = target; current; current = composedParent(current)) {
1078
- if (String(current.tagName).toLowerCase() === 'label') labels.push(current);
933
+ async netClear() {
934
+ await this.request("/net/clear", {});
1079
935
  }
1080
- const accepts = hit => {
1081
- if (!hit) return false;
1082
- if (hit === target || composedContains(target, hit) || isShadowHostOf(target, hit)) return true;
1083
- return labels.some(label => (
1084
- hit === label || composedContains(label, hit) || isShadowHostOf(label, hit)
1085
- ));
1086
- };
1087
-
1088
- let firstObstruction;
1089
- for (const rect of visibleRects) {
1090
- const width = rect.right - rect.left;
1091
- const height = rect.bottom - rect.top;
1092
- const points = [
1093
- [rect.left + width * 0.5, rect.top + height * 0.5],
1094
- [rect.left + width * 0.25, rect.top + height * 0.25],
1095
- [rect.left + width * 0.75, rect.top + height * 0.25],
1096
- [rect.left + width * 0.25, rect.top + height * 0.75],
1097
- [rect.left + width * 0.75, rect.top + height * 0.75],
1098
- ];
1099
- for (const [x, y] of points) {
1100
- const hit = document.elementsFromPoint(x, y)[0];
1101
- if (accepts(hit)) {
1102
- const readClickTargetState = ${CLICK_TARGET_STATE_FUNCTION};
1103
- return {status:'ready', x, y, targetState:readClickTargetState.call(target)};
1104
- }
1105
- if (!firstObstruction && hit) firstObstruction = describes(hit);
1106
- }
936
+ async netAddRule(rule) {
937
+ return this.request("/net/rules", rule);
1107
938
  }
1108
- return blocked('obscured', firstObstruction);
1109
- }`;
1110
- var EDITABLE_STATE_FUNCTION = `function(target) {
1111
- const unsupported = (reason, inputType) => ({
1112
- kind:'unsupported', value:'', sensitive:false, editable:false,
1113
- ...(inputType ? {inputType} : {}), reason,
1114
- });
1115
- if (!target || target.nodeType !== 1 || !target.isConnected) return unsupported('detached');
1116
-
1117
- const composedParent = node => {
1118
- if (node && node.parentElement) return node.parentElement;
1119
- const root = node && typeof node.getRootNode === 'function' ? node.getRootNode() : null;
1120
- return root && root.host ? root.host : null;
1121
- };
1122
- const hasComposedState = (node, property, attribute) => {
1123
- for (let current = node; current; current = composedParent(current)) {
1124
- if (current[property] === true || current.hasAttribute?.(attribute)) return true;
1125
- }
1126
- return false;
1127
- };
1128
- const ariaTrue = (node, name) => String(node.getAttribute?.(name) || '').trim().toLowerCase() === 'true';
1129
- const blockedReason = node => {
1130
- if (hasComposedState(node, 'inert', 'inert')) return 'inert';
1131
- for (let current = node; current; current = composedParent(current)) {
1132
- if (current.matches?.(':disabled') || current.disabled === true || ariaTrue(current, 'aria-disabled')) {
1133
- return 'disabled';
1134
- }
1135
- }
1136
- for (let current = node; current; current = composedParent(current)) {
1137
- if (current.readOnly === true || ariaTrue(current, 'aria-readonly')) return 'readonly';
1138
- }
1139
- return null;
1140
- };
1141
- const contenteditableRoot = node => {
1142
- let root = node;
1143
- while (root.parentElement && root.parentElement.isContentEditable) root = root.parentElement;
1144
- return root;
1145
- };
1146
-
1147
- if (target instanceof HTMLInputElement) {
1148
- const inputType = String(target.type || 'text').toLowerCase();
1149
- const rangeTextTypes = new Set(['text', 'search', 'tel', 'url', 'password']);
1150
- const selectTextTypes = new Set(['email', 'number']);
1151
- const valueTypes = new Set(['date', 'time', 'datetime-local', 'month', 'week', 'color', 'range']);
1152
- const editMode = rangeTextTypes.has(inputType) || selectTextTypes.has(inputType)
1153
- ? 'text' : valueTypes.has(inputType) ? 'value' : null;
1154
- if (!editMode) return unsupported('unsupported_input_type', inputType);
1155
- const reason = blockedReason(target);
1156
- return {
1157
- kind:'input', value:String(target.value ?? ''), sensitive:inputType === 'password',
1158
- editable:!reason, inputType, editMode,
1159
- ...(editMode === 'text' ? {selectionMode:selectTextTypes.has(inputType) ? 'select' : 'range'} : {}),
1160
- ...(reason ? {reason} : {}),
1161
- };
939
+ async netRules() {
940
+ return this.request("/net/rules");
1162
941
  }
1163
- if (target instanceof HTMLTextAreaElement) {
1164
- const reason = blockedReason(target);
1165
- return {
1166
- kind:'input', value:String(target.value ?? ''), sensitive:false,
1167
- editable:!reason, inputType:'textarea', editMode:'text', selectionMode:'range',
1168
- ...(reason ? {reason} : {}),
1169
- };
942
+ async netRemoveRule(id) {
943
+ await this.request("/net/rules/remove", id !== void 0 ? { id } : { all: true });
1170
944
  }
1171
- if (target.isContentEditable) {
1172
- const root = contenteditableRoot(target);
1173
- const reason = blockedReason(root);
1174
- return {
1175
- kind:'contenteditable', value:String(root.innerText ?? root.textContent ?? ''), sensitive:false,
1176
- editable:!reason, inputType:'contenteditable', editMode:'text', selectionMode:'range',
1177
- ...(reason ? {reason} : {}),
1178
- };
945
+ close() {
1179
946
  }
1180
- return unsupported('unsupported_element');
1181
- }`;
1182
- var PREPARE_EDITABLE_TARGET = `function(clear) {
1183
- const readState = ${EDITABLE_STATE_FUNCTION};
1184
- const state = readState(this);
1185
- if (!state.editable) return state;
947
+ };
1186
948
 
1187
- if (state.kind === 'input') {
1188
- this.focus();
1189
- const focusedState = readState(this);
1190
- if (!focusedState.editable) return focusedState;
1191
- if (focusedState.editMode === 'text') {
1192
- if (focusedState.selectionMode === 'range') {
1193
- const end = String(this.value ?? '').length;
1194
- this.setSelectionRange(clear ? 0 : end, end, 'none');
1195
- } else if (clear) {
1196
- this.select();
1197
- }
1198
- }
1199
- return focusedState;
1200
- }
949
+ // src/compatibility-broker-client.ts
950
+ import { createHash as createHash4, randomUUID as randomUUID2 } from "crypto";
1201
951
 
1202
- let root = this;
1203
- while (root.parentElement && root.parentElement.isContentEditable) root = root.parentElement;
1204
- root.focus();
1205
- const focusedState = readState(root);
1206
- if (!focusedState.editable) return focusedState;
1207
- const range = document.createRange();
1208
- range.selectNodeContents(root);
1209
- if (!clear) range.collapse(false);
1210
- const selection = window.getSelection();
1211
- if (!selection) return {...focusedState, editable:false, reason:'selection_unavailable'};
1212
- selection.removeAllRanges();
1213
- selection.addRange(range);
1214
- return focusedState;
1215
- }`;
1216
- var SET_VALUE_CONTROL = `function(value) {
1217
- const readState = ${EDITABLE_STATE_FUNCTION};
1218
- const state = readState(this);
1219
- if (!state.editable || state.kind !== 'input' || state.editMode !== 'value') return;
1220
- this.focus();
1221
- const beforeInput = new InputEvent('beforeinput', {
1222
- bubbles:true, composed:true, cancelable:true, inputType:'insertReplacementText', data:value,
1223
- });
1224
- if (!this.dispatchEvent(beforeInput)) return;
1225
- const setter = Object.getOwnPropertyDescriptor(HTMLInputElement.prototype, 'value')?.set;
1226
- if (setter) setter.call(this, value); else this.value = value;
1227
- this.dispatchEvent(new InputEvent('input', {
1228
- bubbles:true, composed:true, inputType:'insertReplacementText', data:value,
1229
- }));
1230
- }`;
1231
- var READ_EDITABLE_STATE = `function() {
1232
- const readState = ${EDITABLE_STATE_FUNCTION};
1233
- return readState(this);
1234
- }`;
1235
- var READ_ACTIVE_EDITABLE_STATE = `(() => {
1236
- const readState = ${EDITABLE_STATE_FUNCTION};
1237
- let active = document.activeElement;
1238
- while (active && active.shadowRoot && active.shadowRoot.activeElement) {
1239
- active = active.shadowRoot.activeElement;
1240
- }
1241
- return readState(active);
1242
- })()`;
952
+ // src/session.ts
953
+ import { spawn } from "child_process";
1243
954
 
1244
955
  // src/runtime-layout.ts
1245
956
  import { fileURLToPath } from "url";
@@ -1370,1847 +1081,77 @@ async function connectDaemon(browserFilter) {
1370
1081
  remediation: {
1371
1082
  code: "restart_unresponsive_broker",
1372
1083
  message: "Stop the unresponsive Browser Pilot Broker explicitly, then retry. Live processes are never replaced automatically.",
1373
- actionRequired: true
1374
- }
1375
- });
1376
- }
1377
- removeStaleBrokerFilesSync();
1378
- const candidates = await discoverBrowserCandidates();
1379
- const filter = browserFilter?.toLowerCase();
1380
- const matches = candidates.filter(({ candidate }) => !filter || [candidate.id, candidate.product, candidate.channel].some((value) => value?.toLowerCase().includes(filter)));
1381
- if (browserFilter && matches.length === 0) {
1382
- throw new BrowserPilotError(
1383
- "browser_not_found",
1384
- "No installed supported browser matches the requested selection.",
1385
- {
1386
- remediation: {
1387
- code: "select_supported_browser",
1388
- message: "Run browser discovery and select one of the returned browser IDs or product names.",
1389
- actionRequired: true
1390
- }
1391
- }
1392
- );
1393
- }
1394
- const selected = matches.find((browser) => browser.endpoint !== void 0) ?? matches[0] ?? null;
1395
- return await startDaemon(selected);
1396
- } finally {
1397
- startupLock.release();
1398
- }
1399
- }
1400
-
1401
- // src/bridge/daemon-bridge-backend.ts
1402
- var DaemonBridgeBackend = class {
1403
- constructor(browserFilter) {
1404
- this.browserFilter = browserFilter;
1405
- }
1406
- client;
1407
- async call(bridgeSessionId, method, params) {
1408
- const client = await this.getClient();
1409
- return client.brokerCall(bridgeSessionId, method, params);
1410
- }
1411
- async disconnect(bridgeSessionId) {
1412
- if (!this.client) return;
1413
- await this.client.brokerDisconnect(bridgeSessionId);
1414
- }
1415
- async *notifications(bridgeSessionId, signal) {
1416
- const client = await this.getClient();
1417
- while (!signal.aborted) {
1418
- try {
1419
- const notification = await client.brokerNextNotification(bridgeSessionId, 25e3, signal);
1420
- if (notification) yield notification;
1421
- } catch (error) {
1422
- if (signal.aborted) return;
1423
- throw error;
1424
- }
1425
- }
1426
- }
1427
- async getClient() {
1428
- if (!this.client) this.client = await connectDaemon(this.browserFilter);
1429
- const health = await this.client.healthInfo();
1430
- if (health.brokerProtocol !== 1) {
1431
- throw new BrowserPilotError("protocol_incompatible", "Running Browser Pilot daemon is from an older executable", {
1432
- remediation: {
1433
- code: "use_compatible_executable_or_isolate",
1434
- message: "Use a compatible Browser Pilot executable, or set BROWSER_PILOT_HOME for a deliberately isolated Broker.",
1435
- actionRequired: true
1436
- }
1437
- });
1438
- }
1439
- return this.client;
1440
- }
1441
- };
1442
-
1443
- // src/bridge/stdio-bridge.ts
1444
- import { once } from "events";
1445
- import { randomUUID as randomUUID5 } from "crypto";
1446
-
1447
- // src/protocol/model.ts
1448
- var CAPABILITIES = [
1449
- "browser.discovery",
1450
- "browser.control",
1451
- "workspace.manage",
1452
- "observation.read",
1453
- "action.input",
1454
- "artifact.read",
1455
- "event.read",
1456
- "network.observe",
1457
- "network.modify",
1458
- "auth.manage",
1459
- "cookies.read",
1460
- "developer.eval"
1461
- ];
1462
- var OBSERVATION_TRUNCATION_REASONS = [
1463
- "element_limit",
1464
- "text_limit",
1465
- "depth_limit",
1466
- "byte_limit"
1467
- ];
1468
- var OBSERVATION_V1_LIMITS = {
1469
- defaultElements: 50,
1470
- maxElements: 1e4,
1471
- maxTitleCharacters: 4096,
1472
- maxUrlCharacters: 16384,
1473
- maxElementNameCharacters: 4096,
1474
- maxElementValueCharacters: 65536,
1475
- maxTextCharacters: 1e6,
1476
- maxTreeDepth: 128,
1477
- maxSerializedBytes: 2 * 1024 * 1024,
1478
- ttlMs: 5 * 6e4,
1479
- maxStoredObservations: 2048
1480
- };
1481
- var SENSITIVITIES = [
1482
- "public",
1483
- "browser_data",
1484
- "credential",
1485
- "user_file"
1486
- ];
1487
-
1488
- // src/protocol/validation.ts
1489
- var MAX_ID_LENGTH = 256;
1490
- var MIN_NEGOTIATED_TRANSPORT_BYTES = 64 * 1024;
1491
- var MAX_DECLARED_TRANSPORT_BYTES = 1024 * 1024 * 1024;
1492
- function isRecord(value) {
1493
- return typeof value === "object" && value !== null && !Array.isArray(value);
1494
- }
1495
- function assertOnlyKeys(record, allowed, field = "params") {
1496
- const allowedSet = new Set(allowed);
1497
- const unknown = Object.keys(record).filter((key) => !allowedSet.has(key));
1498
- if (unknown.length > 0) {
1499
- throw invalidArgument(`Unknown ${field} field: ${unknown[0]}`, `${field}.${unknown[0]}`, field === "message" ? -32600 : -32602);
1500
- }
1501
- }
1502
- function isJsonValue(value) {
1503
- if (value === null || ["string", "number", "boolean"].includes(typeof value)) {
1504
- return typeof value !== "number" || Number.isFinite(value);
1505
- }
1506
- if (Array.isArray(value)) return value.every(isJsonValue);
1507
- if (isRecord(value)) return Object.values(value).every(isJsonValue);
1508
- return false;
1509
- }
1510
- function requireString(record, key, options = {}) {
1511
- const value = record[key];
1512
- const maxLength = options.maxLength ?? MAX_ID_LENGTH;
1513
- if (typeof value !== "string" || value.length === 0 || value.length > maxLength) {
1514
- throw invalidArgument(`${key} must be a non-empty string no longer than ${maxLength} characters`, key);
1515
- }
1516
- if (options.pattern && !options.pattern.test(value)) {
1517
- throw invalidArgument(`${key} has an invalid format`, key);
1518
- }
1519
- return value;
1520
- }
1521
- function parseId(value, allowNull) {
1522
- if (allowNull && value === null) return null;
1523
- if (typeof value === "string" && value.length > 0 && value.length <= MAX_ID_LENGTH) return value;
1524
- if (typeof value === "number" && Number.isSafeInteger(value)) return value;
1525
- throw invalidArgument("JSON-RPC id must be a non-empty string or safe integer", "id", -32600);
1526
- }
1527
- function parseParams(value) {
1528
- if (value === void 0) return void 0;
1529
- if (!Array.isArray(value) && !isRecord(value) || !isJsonValue(value)) {
1530
- throw invalidArgument("JSON-RPC params must be a JSON object or array", "params", -32600);
1531
- }
1532
- return value;
1533
- }
1534
- function parseErrorObject(value) {
1535
- if (!isRecord(value)) throw invalidArgument("JSON-RPC error must be an object", "error", -32600);
1536
- if (!Number.isSafeInteger(value.code)) throw invalidArgument("JSON-RPC error.code must be an integer", "error.code", -32600);
1537
- const message = requireString(value, "message", { maxLength: 4096 });
1538
- if (value.data !== void 0 && !isJsonValue(value.data)) {
1539
- throw invalidArgument("JSON-RPC error.data must be valid JSON", "error.data", -32600);
1540
- }
1541
- return { code: value.code, message, ...value.data !== void 0 ? { data: value.data } : {} };
1542
- }
1543
- function parseJsonRpcMessage(line, maxMessageBytes = 1024 * 1024) {
1544
- if (Buffer.byteLength(line, "utf8") > maxMessageBytes) {
1545
- throw new BrowserPilotError("result_too_large", `Protocol message exceeds ${maxMessageBytes} bytes`, {
1546
- context: { maxMessageBytes },
1547
- rpcCode: -32600
1548
- });
1549
- }
1550
- if (line.trim().length === 0) throw invalidArgument("Protocol message is empty", void 0, -32700);
1551
- let parsed;
1552
- try {
1553
- parsed = JSON.parse(line);
1554
- } catch (cause) {
1555
- throw new BrowserPilotError("invalid_argument", "Invalid JSON", { rpcCode: -32700, cause });
1556
- }
1557
- if (!isRecord(parsed) || parsed.jsonrpc !== "2.0") {
1558
- throw invalidArgument("Message must be a JSON-RPC 2.0 object", "jsonrpc", -32600);
1559
- }
1560
- if (typeof parsed.method === "string") {
1561
- assertOnlyKeys(parsed, ["jsonrpc", "id", "method", "params"], "message");
1562
- const method = requireString(parsed, "method", { maxLength: 256 });
1563
- const params = parseParams(parsed.params);
1564
- if (parsed.id === void 0) {
1565
- return { jsonrpc: "2.0", method, ...params !== void 0 ? { params } : {} };
1566
- }
1567
- return {
1568
- jsonrpc: "2.0",
1569
- id: parseId(parsed.id, false),
1570
- method,
1571
- ...params !== void 0 ? { params } : {}
1572
- };
1573
- }
1574
- if (parsed.id === void 0) {
1575
- throw invalidArgument("JSON-RPC response must include id", "id", -32600);
1576
- }
1577
- const id = parseId(parsed.id, true);
1578
- const hasResult = Object.hasOwn(parsed, "result");
1579
- const hasError = Object.hasOwn(parsed, "error");
1580
- if (hasResult === hasError) {
1581
- throw invalidArgument("JSON-RPC response must contain exactly one of result or error", void 0, -32600);
1582
- }
1583
- if (hasResult) {
1584
- assertOnlyKeys(parsed, ["jsonrpc", "id", "result"], "message");
1585
- if (!isJsonValue(parsed.result)) throw invalidArgument("JSON-RPC result must be valid JSON", "result", -32600);
1586
- return { jsonrpc: "2.0", id, result: parsed.result };
1587
- }
1588
- assertOnlyKeys(parsed, ["jsonrpc", "id", "error"], "message");
1589
- return { jsonrpc: "2.0", id, error: parseErrorObject(parsed.error) };
1590
- }
1591
-
1592
- // src/services/broker-runtime.ts
1593
- import { randomUUID as randomUUID4 } from "crypto";
1594
-
1595
- // src/protocol/schema.ts
1596
- function isRecord2(value) {
1597
- return typeof value === "object" && value !== null && !Array.isArray(value);
1598
- }
1599
- function assertSchemaDefinition(schema, path = "$schema") {
1600
- const sensitivity = schema["x-browser-pilot-sensitivity"];
1601
- if (sensitivity) {
1602
- if (sensitivity.length === 0 || new Set(sensitivity).size !== sensitivity.length) {
1603
- throw new Error(`${path}.x-browser-pilot-sensitivity must be non-empty and unique`);
1604
- }
1605
- for (const value of sensitivity) {
1606
- if (!SENSITIVITIES.includes(value)) {
1607
- throw new Error(`${path}.x-browser-pilot-sensitivity contains unknown value ${value}`);
1608
- }
1609
- }
1610
- }
1611
- if (schema.type === "object") {
1612
- const properties = schema.properties ?? {};
1613
- for (const required of schema.required ?? []) {
1614
- if (!Object.hasOwn(properties, required)) {
1615
- throw new Error(`${path}.required references unknown property ${required}`);
1616
- }
1617
- }
1618
- for (const [name, property] of Object.entries(properties)) {
1619
- assertSchemaDefinition(property, `${path}.properties.${name}`);
1620
- }
1621
- if (isRecord2(schema.additionalProperties)) {
1622
- assertSchemaDefinition(schema.additionalProperties, `${path}.additionalProperties`);
1623
- }
1624
- }
1625
- if (schema.type === "array" && schema.items) {
1626
- assertSchemaDefinition(schema.items, `${path}.items`);
1627
- }
1628
- for (const [index, alternative] of (schema.oneOf ?? []).entries()) {
1629
- assertSchemaDefinition(alternative, `${path}.oneOf[${index}]`);
1630
- }
1631
- if (schema.pattern) new RegExp(schema.pattern, "u");
1632
- }
1633
- var stringSchema = (options = {}) => ({ type: "string", ...options });
1634
- var booleanSchema = (options = {}) => ({ type: "boolean", ...options });
1635
- var integerSchema = (options = {}) => ({ type: "integer", ...options });
1636
- var numberSchema = (options = {}) => ({ type: "number", ...options });
1637
- var arraySchema = (items, options = {}) => ({
1638
- type: "array",
1639
- items,
1640
- ...options
1641
- });
1642
- var objectSchema = (properties, required = [], options = {}) => ({
1643
- type: "object",
1644
- properties,
1645
- required,
1646
- additionalProperties: false,
1647
- ...options
1648
- });
1649
-
1650
- // src/protocol/tools.ts
1651
- var emptyInput = objectSchema({});
1652
- function sensitive(schema, ...sensitivity) {
1653
- return { ...schema, "x-browser-pilot-sensitivity": sensitivity };
1654
- }
1655
- var boundedText = sensitive(
1656
- stringSchema({ minLength: 1, maxLength: 1e6 }),
1657
- "browser_data",
1658
- "credential"
1659
- );
1660
- var boundedSelector = sensitive(stringSchema({ minLength: 1, maxLength: 4096 }), "browser_data");
1661
- var boundedUrl = sensitive(stringSchema({ minLength: 1, maxLength: 16384 }), "browser_data");
1662
- var opaqueIdSchema = stringSchema({ minLength: 3, maxLength: 128, pattern: "^[A-Za-z0-9][A-Za-z0-9._:-]+$" });
1663
- var headerSchema = objectSchema({
1664
- name: sensitive(stringSchema({ minLength: 1, maxLength: 256 }), "browser_data"),
1665
- value: sensitive(stringSchema({ maxLength: 8192 }), "browser_data", "credential")
1666
- }, ["name", "value"]);
1667
- var networkRuleSchema = objectSchema({
1668
- ruleId: opaqueIdSchema,
1669
- type: stringSchema({ enum: ["block", "mock", "headers"] }),
1670
- pattern: boundedUrl,
1671
- status: integerSchema({ minimum: 100, maximum: 999 }),
1672
- headers: arraySchema(headerSchema, { maxItems: 256 }),
1673
- bodySize: integerSchema({ minimum: 0 })
1674
- }, ["ruleId", "type", "pattern"]);
1675
- var networkRequestDetailSchema = objectSchema({
1676
- requestId: opaqueIdSchema,
1677
- sequence: integerSchema({ minimum: 1 }),
1678
- method: stringSchema({ maxLength: 32 }),
1679
- url: boundedUrl,
1680
- type: stringSchema({ maxLength: 128 }),
1681
- requestHeaders: arraySchema(headerSchema, { maxItems: 256 }),
1682
- postData: sensitive(stringSchema({ maxLength: 65536 }), "browser_data", "credential"),
1683
- postDataTruncated: booleanSchema(),
1684
- status: integerSchema({ minimum: 100, maximum: 999 }),
1685
- statusText: sensitive(stringSchema({ maxLength: 4096 }), "browser_data"),
1686
- responseHeaders: arraySchema(headerSchema, { maxItems: 256 }),
1687
- mimeType: stringSchema({ maxLength: 256 }),
1688
- size: integerSchema({ minimum: 0 }),
1689
- durationMs: numberSchema({ minimum: 0 }),
1690
- error: sensitive(stringSchema({ maxLength: 4096 }), "browser_data"),
1691
- bodyAvailable: booleanSchema()
1692
- }, [
1693
- "requestId",
1694
- "sequence",
1695
- "method",
1696
- "url",
1697
- "type",
1698
- "requestHeaders",
1699
- "postDataTruncated",
1700
- "bodyAvailable"
1701
- ]);
1702
- var elementSchema = objectSchema({
1703
- ref: integerSchema({ minimum: 1 }),
1704
- role: stringSchema({ minLength: 1, maxLength: 128 }),
1705
- name: sensitive(stringSchema({ maxLength: OBSERVATION_V1_LIMITS.maxElementNameCharacters }), "browser_data"),
1706
- value: sensitive(stringSchema({ maxLength: OBSERVATION_V1_LIMITS.maxElementValueCharacters }), "browser_data", "credential"),
1707
- checked: booleanSchema()
1708
- }, ["ref", "role", "name"]);
1709
- var inputEvidenceSchema = objectSchema({
1710
- action: stringSchema({ enum: ["type", "keyboard"] }),
1711
- status: stringSchema({ enum: ["verified", "mismatch", "unavailable"] }),
1712
- kind: stringSchema({ enum: ["input", "contenteditable", "unsupported"] }),
1713
- sensitive: booleanSchema(),
1714
- beforeLength: integerSchema({ minimum: 0 }),
1715
- expectedLength: integerSchema({ minimum: 0 }),
1716
- afterLength: integerSchema({ minimum: 0 }),
1717
- reason: stringSchema({ enum: ["active_element_not_readable", "value_mismatch"] })
1718
- }, ["action", "status", "kind", "sensitive"]);
1719
- var clickEvidenceSchema = objectSchema({
1720
- action: stringSchema({ const: "click" }),
1721
- status: stringSchema({ enum: ["verified", "mismatch", "unavailable"] }),
1722
- kind: stringSchema({ enum: [
1723
- "checkbox",
1724
- "radio",
1725
- "switch",
1726
- "option",
1727
- "select",
1728
- "control",
1729
- "other",
1730
- "coordinates"
1731
- ] }),
1732
- effects: arraySchema(stringSchema({ enum: [
1733
- "checked_changed",
1734
- "selected_changed",
1735
- "pressed_changed",
1736
- "expanded_changed",
1737
- "focus_changed",
1738
- "navigation",
1739
- "document_changed",
1740
- "dialog_opened",
1741
- "popup_opened"
1742
- ] }), { maxItems: 9, uniqueItems: true }),
1743
- checked: { oneOf: [booleanSchema(), stringSchema({ const: "mixed" })] },
1744
- selected: booleanSchema(),
1745
- pressed: { oneOf: [booleanSchema(), stringSchema({ const: "mixed" })] },
1746
- expanded: booleanSchema(),
1747
- focused: booleanSchema(),
1748
- reason: stringSchema({ enum: [
1749
- "coordinate_target",
1750
- "target_unavailable",
1751
- "expected_state_unchanged",
1752
- "no_observable_effect"
1753
- ] })
1754
- }, ["action", "status", "kind", "effects"]);
1755
- var pressEvidenceSchema = objectSchema({
1756
- action: stringSchema({ const: "press" }),
1757
- status: stringSchema({ enum: ["verified", "unavailable"] }),
1758
- kind: stringSchema({ enum: [
1759
- "input",
1760
- "contenteditable",
1761
- "checkbox",
1762
- "radio",
1763
- "select",
1764
- "control",
1765
- "other"
1766
- ] }),
1767
- effects: arraySchema(stringSchema({ enum: [
1768
- "value_changed",
1769
- "checked_changed",
1770
- "selected_changed",
1771
- "pressed_changed",
1772
- "expanded_changed",
1773
- "focus_changed",
1774
- "navigation",
1775
- "document_changed",
1776
- "dialog_opened",
1777
- "popup_opened"
1778
- ] }), { maxItems: 10, uniqueItems: true }),
1779
- sensitive: booleanSchema(),
1780
- reason: stringSchema({ enum: ["target_unavailable", "no_observable_effect"] })
1781
- }, ["action", "status", "kind", "effects", "sensitive"]);
1782
- var uploadEvidenceSchema = objectSchema({
1783
- action: stringSchema({ const: "upload" }),
1784
- status: stringSchema({ enum: ["verified", "mismatch", "unavailable"] }),
1785
- expectedFileCount: integerSchema({ const: 1 }),
1786
- fileCount: integerSchema({ minimum: 0 }),
1787
- nameMatched: booleanSchema(),
1788
- reason: stringSchema({ enum: [
1789
- "target_unavailable",
1790
- "file_count_mismatch",
1791
- "file_name_mismatch"
1792
- ] })
1793
- }, ["action", "status", "expectedFileCount"]);
1794
- var scrollEvidenceSchema = objectSchema({
1795
- action: stringSchema({ const: "scroll" }),
1796
- status: stringSchema({ enum: ["verified", "mismatch"] }),
1797
- mode: stringSchema({ enum: ["relative", "position", "text"] }),
1798
- target: stringSchema({ enum: ["page", "element", "text"] }),
1799
- moved: booleanSchema(),
1800
- deltaX: numberSchema(),
1801
- deltaY: numberSchema(),
1802
- beforeX: numberSchema({ minimum: 0 }),
1803
- beforeY: numberSchema({ minimum: 0 }),
1804
- afterX: numberSchema({ minimum: 0 }),
1805
- afterY: numberSchema({ minimum: 0 }),
1806
- matchedText: sensitive(stringSchema({ maxLength: 4096 }), "browser_data"),
1807
- reason: stringSchema({ enum: ["at_boundary", "text_not_found", "text_not_revealed"] })
1808
- }, [
1809
- "action",
1810
- "status",
1811
- "mode",
1812
- "target",
1813
- "moved",
1814
- "deltaX",
1815
- "deltaY",
1816
- "beforeX",
1817
- "beforeY",
1818
- "afterX",
1819
- "afterY"
1820
- ]);
1821
- var dropdownOptionSchema = objectSchema({
1822
- index: integerSchema({ minimum: 1, maximum: 500 }),
1823
- label: sensitive(stringSchema({ maxLength: 4096 }), "browser_data"),
1824
- value: sensitive(stringSchema({ maxLength: 4096 }), "browser_data"),
1825
- selected: booleanSchema(),
1826
- disabled: booleanSchema()
1827
- }, ["index", "label", "value", "selected", "disabled"]);
1828
- var selectEvidenceSchema = objectSchema({
1829
- action: stringSchema({ const: "select" }),
1830
- status: stringSchema({ enum: ["verified", "mismatch", "unavailable"] }),
1831
- kind: stringSchema({ enum: ["native", "aria"] }),
1832
- selected: arraySchema(dropdownOptionSchema, { maxItems: 500 }),
1833
- reason: stringSchema({ enum: ["option_not_found", "selection_mismatch", "selection_not_exposed"] })
1834
- }, ["action", "status", "kind", "selected"]);
1835
- var artifactSchema = objectSchema({
1836
- id: opaqueIdSchema,
1837
- workspaceId: opaqueIdSchema,
1838
- kind: stringSchema({ enum: ["screenshot", "screenshot_preview", "pdf", "download", "upload_input"] }),
1839
- mimeType: stringSchema({ minLength: 1, maxLength: 256 }),
1840
- byteSize: integerSchema({ minimum: 0 }),
1841
- fileName: stringSchema({ minLength: 1, maxLength: 4096 }),
1842
- width: integerSchema({ minimum: 1 }),
1843
- height: integerSchema({ minimum: 1 }),
1844
- sensitivity: stringSchema({ enum: ["public", "browser_data", "credential", "user_file"] }),
1845
- createdAt: integerSchema({ minimum: 0 }),
1846
- expiresAt: integerSchema({ minimum: 0 }),
1847
- retained: booleanSchema(),
1848
- previewOf: opaqueIdSchema
1849
- }, ["id", "workspaceId", "kind", "mimeType", "byteSize", "sensitivity", "createdAt", "expiresAt", "retained"]);
1850
- var hintRefsSchema = arraySchema(integerSchema({ minimum: 1 }), {
1851
- maxItems: 32,
1852
- uniqueItems: true
1853
- });
1854
- var agentHintSchema = {
1855
- oneOf: [
1856
- objectSchema({
1857
- code: stringSchema({ const: "autocomplete" }),
1858
- source: stringSchema({ const: "observation" }),
1859
- confidence: stringSchema({ enum: ["strong", "possible"] }),
1860
- recommendedAction: stringSchema({ const: "observe_then_select" }),
1861
- refs: hintRefsSchema
1862
- }, ["code", "source", "confidence", "recommendedAction", "refs"]),
1863
- objectSchema({
1864
- code: stringSchema({ const: "modal_overlay" }),
1865
- source: stringSchema({ const: "observation" }),
1866
- confidence: stringSchema({ enum: ["strong", "possible"] }),
1867
- recommendedAction: stringSchema({ const: "resolve_overlay_first" }),
1868
- blocking: booleanSchema(),
1869
- refs: hintRefsSchema
1870
- }, ["code", "source", "confidence", "recommendedAction", "blocking", "refs"]),
1871
- objectSchema({
1872
- code: stringSchema({ const: "filter_controls" }),
1873
- source: stringSchema({ const: "observation" }),
1874
- confidence: stringSchema({ const: "strong" }),
1875
- recommendedAction: stringSchema({ const: "review_refinement_controls" }),
1876
- refs: hintRefsSchema
1877
- }, ["code", "source", "confidence", "recommendedAction", "refs"]),
1878
- objectSchema({
1879
- code: stringSchema({ const: "access_blocked" }),
1880
- source: stringSchema({ const: "network" }),
1881
- confidence: stringSchema({ const: "strong" }),
1882
- recommendedAction: stringSchema({ const: "avoid_same_navigation_retry" }),
1883
- status: integerSchema({ enum: [403, 429] })
1884
- }, ["code", "source", "confidence", "recommendedAction", "status"]),
1885
- objectSchema({
1886
- code: stringSchema({ const: "authentication_surface" }),
1887
- source: stringSchema({ const: "observation" }),
1888
- confidence: stringSchema({ const: "strong" }),
1889
- recommendedAction: stringSchema({ const: "inspect_authentication_state" }),
1890
- state: stringSchema({ enum: ["present", "entered", "left"] })
1891
- }, ["code", "source", "confidence", "recommendedAction", "state"]),
1892
- objectSchema({
1893
- code: stringSchema({ const: "download" }),
1894
- source: stringSchema({ const: "download" }),
1895
- confidence: stringSchema({ const: "strong" }),
1896
- recommendedAction: stringSchema({ const: "wait_for_download" }),
1897
- state: stringSchema({ const: "started" })
1898
- }, ["code", "source", "confidence", "recommendedAction", "state"]),
1899
- objectSchema({
1900
- code: stringSchema({ const: "download" }),
1901
- source: stringSchema({ const: "download" }),
1902
- confidence: stringSchema({ const: "strong" }),
1903
- recommendedAction: stringSchema({ const: "inspect_download_artifact" }),
1904
- state: stringSchema({ const: "completed" }),
1905
- artifactId: opaqueIdSchema
1906
- }, ["code", "source", "confidence", "recommendedAction", "state", "artifactId"]),
1907
- objectSchema({
1908
- code: stringSchema({ const: "download" }),
1909
- source: stringSchema({ const: "download" }),
1910
- confidence: stringSchema({ const: "strong" }),
1911
- recommendedAction: stringSchema({ const: "inspect_download_failure" }),
1912
- state: stringSchema({ enum: ["failed", "cancelled"] }),
1913
- reason: stringSchema({ minLength: 1, maxLength: 128 })
1914
- }, ["code", "source", "confidence", "recommendedAction", "state", "reason"]),
1915
- objectSchema({
1916
- code: stringSchema({ const: "repeated_action" }),
1917
- source: stringSchema({ const: "watchdog" }),
1918
- confidence: stringSchema({ const: "strong" }),
1919
- recommendedAction: stringSchema({ const: "change_strategy" }),
1920
- streak: integerSchema({ minimum: 1, maximum: 1e6 }),
1921
- reason: stringSchema({ minLength: 1, maxLength: 128 })
1922
- }, ["code", "source", "confidence", "recommendedAction", "streak", "reason"])
1923
- ]
1924
- };
1925
- function resultSchema(context, properties = {}, required = []) {
1926
- const contextProperties = {};
1927
- const contextRequired = [];
1928
- if (context === "workspace" || context === "target") {
1929
- contextProperties.workspaceId = opaqueIdSchema;
1930
- contextProperties.leaseId = opaqueIdSchema;
1931
- contextRequired.push("workspaceId", "leaseId");
1932
- }
1933
- if (context === "target") {
1934
- contextProperties.targetId = opaqueIdSchema;
1935
- contextProperties.profileContextId = opaqueIdSchema;
1936
- contextProperties.url = boundedUrl;
1937
- contextRequired.push("targetId", "url");
1938
- }
1939
- return objectSchema({ ...contextProperties, ...properties }, [...contextRequired, ...required]);
1940
- }
1941
- var observationOutput = resultSchema("target", {
1942
- observationId: opaqueIdSchema,
1943
- title: sensitive(stringSchema({ maxLength: OBSERVATION_V1_LIMITS.maxTitleCharacters }), "browser_data"),
1944
- page: objectSchema({
1945
- viewportWidth: integerSchema({ minimum: 0 }),
1946
- viewportHeight: integerSchema({ minimum: 0 }),
1947
- documentWidth: integerSchema({ minimum: 0 }),
1948
- documentHeight: integerSchema({ minimum: 0 }),
1949
- scrollX: integerSchema({ minimum: 0 }),
1950
- scrollY: integerSchema({ minimum: 0 }),
1951
- pixelsAbove: integerSchema({ minimum: 0 }),
1952
- pixelsBelow: integerSchema({ minimum: 0 }),
1953
- pixelsLeft: integerSchema({ minimum: 0 }),
1954
- pixelsRight: integerSchema({ minimum: 0 }),
1955
- scrollPercentX: numberSchema({ minimum: 0, maximum: 100 }),
1956
- scrollPercentY: numberSchema({ minimum: 0, maximum: 100 })
1957
- }, [
1958
- "viewportWidth",
1959
- "viewportHeight",
1960
- "documentWidth",
1961
- "documentHeight",
1962
- "scrollX",
1963
- "scrollY",
1964
- "pixelsAbove",
1965
- "pixelsBelow",
1966
- "pixelsLeft",
1967
- "pixelsRight",
1968
- "scrollPercentX",
1969
- "scrollPercentY"
1970
- ]),
1971
- elements: arraySchema(elementSchema, { maxItems: OBSERVATION_V1_LIMITS.maxElements }),
1972
- truncated: booleanSchema(),
1973
- truncationReasons: arraySchema(stringSchema({
1974
- enum: [...OBSERVATION_TRUNCATION_REASONS]
1975
- }), { uniqueItems: true }),
1976
- hints: arraySchema(agentHintSchema, { maxItems: 16 }),
1977
- evidence: { oneOf: [
1978
- inputEvidenceSchema,
1979
- clickEvidenceSchema,
1980
- pressEvidenceSchema,
1981
- uploadEvidenceSchema,
1982
- scrollEvidenceSchema,
1983
- selectEvidenceSchema
1984
- ] }
1985
- }, ["observationId", "title", "elements", "truncated", "truncationReasons"]);
1986
- var artifactOutput = resultSchema("target", {
1987
- artifact: artifactSchema,
1988
- preview: artifactSchema,
1989
- annotationCount: integerSchema({ minimum: 0, maximum: 200 })
1990
- }, ["artifact"]);
1991
- var elementAddressSchema = {
1992
- oneOf: [
1993
- objectSchema({
1994
- observationId: opaqueIdSchema,
1995
- ref: integerSchema({ minimum: 1 })
1996
- }, ["observationId", "ref"]),
1997
- objectSchema({ selector: boundedSelector }, ["selector"])
1998
- ]
1999
- };
2000
- var dropdownChoiceSchema = {
2001
- oneOf: [
2002
- objectSchema({ by: stringSchema({ const: "index" }), index: integerSchema({ minimum: 1, maximum: 500 }) }, ["by", "index"]),
2003
- objectSchema({
2004
- by: stringSchema({ const: "label" }),
2005
- label: sensitive(stringSchema({ minLength: 1, maxLength: 4096 }), "browser_data"),
2006
- exact: booleanSchema()
2007
- }, ["by", "label"]),
2008
- objectSchema({
2009
- by: stringSchema({ const: "value" }),
2010
- value: sensitive(stringSchema({ minLength: 1, maxLength: 4096 }), "browser_data"),
2011
- exact: booleanSchema()
2012
- }, ["by", "value"])
2013
- ]
2014
- };
2015
- var profileRepresentativeTabSchema = objectSchema({
2016
- targetId: opaqueIdSchema,
2017
- title: sensitive(stringSchema({ maxLength: 4096 }), "browser_data"),
2018
- url: boundedUrl
2019
- }, ["targetId", "title", "url"]);
2020
- var profileContextSchema = objectSchema({
2021
- profileContextId: opaqueIdSchema,
2022
- label: stringSchema({ minLength: 1, maxLength: 128 }),
2023
- displayName: sensitive(stringSchema({ minLength: 1, maxLength: 4096 }), "browser_data"),
2024
- tabCount: integerSchema({ minimum: 0 }),
2025
- eligibleTabCount: integerSchema({ minimum: 0 }),
2026
- selected: booleanSchema(),
2027
- representativeTabs: arraySchema(profileRepresentativeTabSchema, { maxItems: 3 })
2028
- }, [
2029
- "profileContextId",
2030
- "label",
2031
- "tabCount",
2032
- "eligibleTabCount",
2033
- "selected",
2034
- "representativeTabs"
2035
- ]);
2036
- function tool(definition) {
2037
- return definition;
2038
- }
2039
- var TOOL_DEFINITIONS = [
2040
- tool({
2041
- name: "browser.discover",
2042
- title: "Discover browsers",
2043
- description: "List supported local browsers and structured setup or authorization state.",
2044
- context: "connection",
2045
- inputSchema: objectSchema({ browser: stringSchema({ minLength: 1, maxLength: 128 }) }),
2046
- outputSchema: resultSchema("connection", {
2047
- browsers: arraySchema(objectSchema({
2048
- id: opaqueIdSchema,
2049
- product: stringSchema({ minLength: 1, maxLength: 128 }),
2050
- channel: stringSchema({ maxLength: 128 }),
2051
- profile: sensitive(stringSchema({ maxLength: 4096 }), "browser_data"),
2052
- processState: stringSchema({ enum: ["running", "not_running", "unknown"] }),
2053
- remoteDebuggingState: stringSchema({ enum: ["enabled", "disabled", "stale"] }),
2054
- authorizationState: stringSchema({ enum: ["authorized", "required", "not_applicable", "unknown"] }),
2055
- state: stringSchema({ enum: ["ready", "not_running", "remote_debugging_disabled", "authorization_required", "disconnected"] }),
2056
- remediation: objectSchema({
2057
- code: stringSchema({ minLength: 1, maxLength: 128 }),
2058
- message: stringSchema({ minLength: 1, maxLength: 4096 }),
2059
- actionRequired: booleanSchema()
2060
- }, ["code", "message", "actionRequired"])
2061
- }, [
2062
- "id",
2063
- "product",
2064
- "processState",
2065
- "remoteDebuggingState",
2066
- "authorizationState",
2067
- "state"
2068
- ]))
2069
- }, ["browsers"]),
2070
- requiredCapabilities: ["browser.discovery"],
2071
- mutating: false,
2072
- idempotency: "read_only",
2073
- cancellation: "not_applicable",
2074
- sensitivity: { input: [], output: ["browser_data"] },
2075
- artifactKinds: []
2076
- }),
2077
- tool({
2078
- name: "browser.connect",
2079
- title: "Connect browser",
2080
- description: "Connect a Workspace to a selected authorized browser instance.",
2081
- context: "workspace",
2082
- inputSchema: objectSchema({ browserId: opaqueIdSchema }, ["browserId"]),
2083
- outputSchema: resultSchema("workspace", {
2084
- browserInstanceId: opaqueIdSchema,
2085
- connectionGeneration: integerSchema({ minimum: 1 }),
2086
- state: stringSchema({ const: "connected" })
2087
- }, ["browserInstanceId", "connectionGeneration", "state"]),
2088
- requiredCapabilities: ["browser.control"],
2089
- mutating: true,
2090
- idempotency: "idempotent",
2091
- cancellation: "before_dispatch",
2092
- sensitivity: { input: [], output: ["browser_data"] },
2093
- artifactKinds: []
2094
- }),
2095
- tool({
2096
- name: "browser.profiles.list",
2097
- title: "List browser Profiles",
2098
- description: "Passively list live Profile contexts and bounded representative tabs.",
2099
- context: "workspace",
2100
- inputSchema: emptyInput,
2101
- outputSchema: resultSchema("workspace", {
2102
- profiles: arraySchema(profileContextSchema, { maxItems: 128 })
2103
- }, ["profiles"]),
2104
- requiredCapabilities: ["browser.control"],
2105
- mutating: false,
2106
- idempotency: "read_only",
2107
- cancellation: "not_applicable",
2108
- sensitivity: { input: [], output: ["browser_data"] },
2109
- artifactKinds: []
2110
- }),
2111
- tool({
2112
- name: "browser.profiles.select",
2113
- title: "Select browser Profile",
2114
- description: "Select one current Profile context for new managed targets in this Workspace.",
2115
- context: "workspace",
2116
- inputSchema: objectSchema({ profileContextId: opaqueIdSchema }, ["profileContextId"]),
2117
- outputSchema: resultSchema("workspace", {
2118
- profileContextId: opaqueIdSchema,
2119
- label: stringSchema({ minLength: 1, maxLength: 128 }),
2120
- displayName: sensitive(stringSchema({ minLength: 1, maxLength: 4096 }), "browser_data")
2121
- }, ["profileContextId", "label"]),
2122
- requiredCapabilities: ["browser.control"],
2123
- mutating: true,
2124
- idempotency: "idempotent",
2125
- cancellation: "before_dispatch",
2126
- sensitivity: { input: [], output: ["browser_data"] },
2127
- artifactKinds: []
2128
- }),
2129
- tool({
2130
- name: "browser.open",
2131
- title: "Open URL",
2132
- description: "Navigate an authorized controlled target or create a new target in the ManagedTabSet.",
2133
- context: "workspace",
2134
- inputSchema: objectSchema({
2135
- url: boundedUrl,
2136
- targetId: opaqueIdSchema,
2137
- newTarget: booleanSchema(),
2138
- profileContextId: opaqueIdSchema,
2139
- observationLimit: integerSchema({ minimum: 1, maximum: OBSERVATION_V1_LIMITS.maxElements })
2140
- }, ["url"]),
2141
- outputSchema: observationOutput,
2142
- requiredCapabilities: ["browser.control", "observation.read"],
2143
- mutating: true,
2144
- idempotency: "non_idempotent",
2145
- cancellation: "best_effort",
2146
- sensitivity: { input: ["browser_data"], output: ["browser_data", "credential"] },
2147
- artifactKinds: []
2148
- }),
2149
- tool({
2150
- name: "browser.observe",
2151
- title: "Observe page",
2152
- description: "Create an immutable bounded Observation with numbered refs.",
2153
- context: "target",
2154
- inputSchema: objectSchema({
2155
- limit: integerSchema({ minimum: 1, maximum: OBSERVATION_V1_LIMITS.maxElements })
2156
- }),
2157
- outputSchema: observationOutput,
2158
- requiredCapabilities: ["observation.read"],
2159
- mutating: false,
2160
- idempotency: "read_only",
2161
- cancellation: "best_effort",
2162
- sensitivity: { input: [], output: ["browser_data", "credential"] },
2163
- artifactKinds: []
2164
- }),
2165
- tool({
2166
- name: "browser.observation.latest",
2167
- title: "Get latest Observation",
2168
- description: "Return the latest live Observation descriptor for a controlled target.",
2169
- context: "target",
2170
- inputSchema: emptyInput,
2171
- outputSchema: resultSchema("target", {
2172
- observationId: opaqueIdSchema,
2173
- createdAt: integerSchema({ minimum: 0 }),
2174
- expiresAt: integerSchema({ minimum: 0 }),
2175
- elementCount: integerSchema({ minimum: 0, maximum: OBSERVATION_V1_LIMITS.maxElements })
2176
- }, ["observationId", "createdAt", "expiresAt", "elementCount"]),
2177
- requiredCapabilities: ["observation.read"],
2178
- mutating: false,
2179
- idempotency: "read_only",
2180
- cancellation: "not_applicable",
2181
- sensitivity: { input: [], output: ["browser_data"] },
2182
- artifactKinds: []
2183
- }),
2184
- tool({
2185
- name: "browser.locate",
2186
- title: "Locate element",
2187
- description: "Return viewport coordinates for a CSS selector in the selected frame.",
2188
- context: "target",
2189
- inputSchema: objectSchema({ selector: boundedSelector }, ["selector"]),
2190
- outputSchema: resultSchema("target", {
2191
- x: numberSchema(),
2192
- y: numberSchema(),
2193
- top: numberSchema(),
2194
- left: numberSchema(),
2195
- width: numberSchema({ minimum: 0 }),
2196
- height: numberSchema({ minimum: 0 })
2197
- }, ["x", "y", "top", "left", "width", "height"]),
2198
- requiredCapabilities: ["observation.read"],
2199
- mutating: false,
2200
- idempotency: "read_only",
2201
- cancellation: "best_effort",
2202
- sensitivity: { input: ["browser_data"], output: ["browser_data"] },
2203
- artifactKinds: []
2204
- }),
2205
- tool({
2206
- name: "browser.read",
2207
- title: "Read page text",
2208
- description: "Read bounded text from the page or a selector.",
2209
- context: "target",
2210
- inputSchema: objectSchema({
2211
- selector: boundedSelector,
2212
- limit: integerSchema({ minimum: 1, maximum: 1e6 })
2213
- }),
2214
- outputSchema: resultSchema("target", {
2215
- title: sensitive(stringSchema({ maxLength: 4096 }), "browser_data"),
2216
- text: sensitive(stringSchema({ maxLength: 1e6 }), "browser_data"),
2217
- length: integerSchema({ minimum: 0 }),
2218
- truncated: booleanSchema()
2219
- }, ["title", "text", "length", "truncated"]),
2220
- requiredCapabilities: ["observation.read"],
2221
- mutating: false,
2222
- idempotency: "read_only",
2223
- cancellation: "best_effort",
2224
- sensitivity: { input: ["browser_data"], output: ["browser_data"] },
2225
- artifactKinds: []
2226
- }),
2227
- tool({
2228
- name: "browser.search",
2229
- title: "Search page text",
2230
- description: "Find bounded visible text matches without returning the entire page.",
2231
- context: "target",
2232
- inputSchema: objectSchema({
2233
- query: sensitive(stringSchema({ minLength: 1, maxLength: 4096 }), "browser_data"),
2234
- selector: boundedSelector,
2235
- caseSensitive: booleanSchema(),
2236
- wholeWord: booleanSchema(),
2237
- limit: integerSchema({ minimum: 1, maximum: 200 })
2238
- }, ["query"]),
2239
- outputSchema: resultSchema("target", {
2240
- title: sensitive(stringSchema({ maxLength: 4096 }), "browser_data"),
2241
- totalMatches: integerSchema({ minimum: 0 }),
2242
- matches: arraySchema(objectSchema({
2243
- index: integerSchema({ minimum: 1 }),
2244
- text: sensitive(stringSchema({ maxLength: 4096 }), "browser_data"),
2245
- context: sensitive(stringSchema({ maxLength: 500 }), "browser_data"),
2246
- tagName: stringSchema({ maxLength: 64 }),
2247
- visible: booleanSchema(),
2248
- x: numberSchema(),
2249
- y: numberSchema(),
2250
- width: numberSchema({ minimum: 0 }),
2251
- height: numberSchema({ minimum: 0 })
2252
- }, ["index", "text", "context", "tagName", "visible", "x", "y", "width", "height"]), { maxItems: 200 }),
2253
- truncated: booleanSchema()
2254
- }, ["title", "totalMatches", "matches", "truncated"]),
2255
- requiredCapabilities: ["observation.read"],
2256
- mutating: false,
2257
- idempotency: "read_only",
2258
- cancellation: "best_effort",
2259
- sensitivity: { input: ["browser_data"], output: ["browser_data"] },
2260
- artifactKinds: []
2261
- }),
2262
- tool({
2263
- name: "browser.elements.find",
2264
- title: "Find DOM elements",
2265
- description: "Run a bounded CSS query and return safe element metadata without exposing DOM handles.",
2266
- context: "target",
2267
- inputSchema: objectSchema({
2268
- selector: boundedSelector,
2269
- limit: integerSchema({ minimum: 1, maximum: 200 }),
2270
- attributeNames: arraySchema(
2271
- sensitive(stringSchema({ minLength: 1, maxLength: 128, pattern: "^[A-Za-z_:][A-Za-z0-9_.:-]*$" }), "browser_data"),
2272
- { maxItems: 20, uniqueItems: true }
2273
- ),
2274
- pierceShadow: booleanSchema()
2275
- }, ["selector"]),
2276
- outputSchema: resultSchema("target", {
2277
- title: sensitive(stringSchema({ maxLength: 4096 }), "browser_data"),
2278
- totalMatches: integerSchema({ minimum: 0 }),
2279
- elements: arraySchema(objectSchema({
2280
- index: integerSchema({ minimum: 1 }),
2281
- tagName: stringSchema({ maxLength: 64 }),
2282
- role: stringSchema({ maxLength: 128 }),
2283
- name: sensitive(stringSchema({ maxLength: 4096 }), "browser_data"),
2284
- text: sensitive(stringSchema({ maxLength: 500 }), "browser_data"),
2285
- visible: booleanSchema(),
2286
- enabled: booleanSchema(),
2287
- x: numberSchema(),
2288
- y: numberSchema(),
2289
- width: numberSchema({ minimum: 0 }),
2290
- height: numberSchema({ minimum: 0 }),
2291
- attributes: arraySchema(objectSchema({
2292
- name: sensitive(stringSchema({ maxLength: 128 }), "browser_data"),
2293
- value: sensitive(stringSchema({ maxLength: 2048 }), "browser_data", "credential")
2294
- }, ["name", "value"]), { maxItems: 20 })
2295
- }, [
2296
- "index",
2297
- "tagName",
2298
- "role",
2299
- "name",
2300
- "text",
2301
- "visible",
2302
- "enabled",
2303
- "x",
2304
- "y",
2305
- "width",
2306
- "height",
2307
- "attributes"
2308
- ]), { maxItems: 200 }),
2309
- truncated: booleanSchema()
2310
- }, ["title", "totalMatches", "elements", "truncated"]),
2311
- requiredCapabilities: ["observation.read"],
2312
- mutating: false,
2313
- idempotency: "read_only",
2314
- cancellation: "best_effort",
2315
- sensitivity: { input: ["browser_data"], output: ["browser_data", "credential"] },
2316
- artifactKinds: []
2317
- }),
2318
- tool({
2319
- name: "browser.scroll",
2320
- title: "Scroll page or element",
2321
- description: "Scroll the page, a ref, a CSS-selected container, or visible text and return a fresh Observation.",
2322
- context: "target",
2323
- inputSchema: objectSchema({
2324
- target: elementAddressSchema,
2325
- direction: stringSchema({ enum: ["up", "down", "left", "right"] }),
2326
- amount: numberSchema({ minimum: 0.01, maximum: 1e5 }),
2327
- unit: stringSchema({ enum: ["pixels", "viewport"] }),
2328
- position: stringSchema({ enum: ["start", "end"] }),
2329
- text: sensitive(stringSchema({ minLength: 1, maxLength: 4096 }), "browser_data"),
2330
- exact: booleanSchema(),
2331
- observationLimit: integerSchema({ minimum: 1, maximum: OBSERVATION_V1_LIMITS.maxElements })
2332
- }),
2333
- outputSchema: observationOutput,
2334
- requiredCapabilities: ["action.input", "observation.read"],
2335
- mutating: true,
2336
- idempotency: "non_idempotent",
2337
- cancellation: "best_effort",
2338
- sensitivity: { input: ["browser_data"], output: ["browser_data", "credential"] },
2339
- artifactKinds: []
2340
- }),
2341
- tool({
2342
- name: "browser.dropdown.options",
2343
- title: "List dropdown options",
2344
- description: "Enumerate bounded native or currently exposed ARIA dropdown options.",
2345
- context: "target",
2346
- inputSchema: objectSchema({ target: elementAddressSchema }, ["target"]),
2347
- outputSchema: resultSchema("target", {
2348
- kind: stringSchema({ enum: ["native", "aria"] }),
2349
- expanded: booleanSchema(),
2350
- multiple: booleanSchema(),
2351
- requiresOpen: booleanSchema(),
2352
- options: arraySchema(dropdownOptionSchema, { maxItems: 500 }),
2353
- truncated: booleanSchema()
2354
- }, ["kind", "expanded", "multiple", "requiresOpen", "options", "truncated"]),
2355
- requiredCapabilities: ["observation.read"],
2356
- mutating: false,
2357
- idempotency: "read_only",
2358
- cancellation: "best_effort",
2359
- sensitivity: { input: ["browser_data"], output: ["browser_data"] },
2360
- artifactKinds: []
2361
- }),
2362
- tool({
2363
- name: "browser.dropdown.select",
2364
- title: "Select dropdown option",
2365
- description: "Select and verify a native or ARIA dropdown option, opening custom controls when needed.",
2366
- context: "target",
2367
- inputSchema: objectSchema({
2368
- target: elementAddressSchema,
2369
- choice: dropdownChoiceSchema,
2370
- observationLimit: integerSchema({ minimum: 1, maximum: OBSERVATION_V1_LIMITS.maxElements })
2371
- }, ["target", "choice"]),
2372
- outputSchema: observationOutput,
2373
- requiredCapabilities: ["action.input", "observation.read"],
2374
- mutating: true,
2375
- idempotency: "non_idempotent",
2376
- cancellation: "best_effort",
2377
- sensitivity: { input: ["browser_data"], output: ["browser_data", "credential"] },
2378
- artifactKinds: []
2379
- }),
2380
- tool({
2381
- name: "browser.click",
2382
- title: "Click element",
2383
- description: "Click an Observation ref or viewport coordinates after interactability checks.",
2384
- context: "target",
2385
- inputSchema: objectSchema({
2386
- target: {
2387
- oneOf: [
2388
- objectSchema({ observationId: opaqueIdSchema, ref: integerSchema({ minimum: 1 }) }, ["observationId", "ref"]),
2389
- objectSchema({ x: numberSchema(), y: numberSchema() }, ["x", "y"])
2390
- ]
2391
- },
2392
- button: stringSchema({ enum: ["left", "right"] }),
2393
- clickCount: integerSchema({ minimum: 1, maximum: 2 }),
2394
- observationLimit: integerSchema({ minimum: 1, maximum: OBSERVATION_V1_LIMITS.maxElements })
2395
- }, ["target"]),
2396
- outputSchema: observationOutput,
2397
- requiredCapabilities: ["action.input", "observation.read"],
2398
- mutating: true,
2399
- idempotency: "non_idempotent",
2400
- cancellation: "best_effort",
2401
- sensitivity: { input: ["browser_data"], output: ["browser_data", "credential"] },
2402
- artifactKinds: []
2403
- }),
2404
- tool({
2405
- name: "browser.type",
2406
- title: "Type into element",
2407
- description: "Enter text into an Observation ref and read back the effective value.",
2408
- context: "target",
2409
- inputSchema: objectSchema({
2410
- observationId: opaqueIdSchema,
2411
- ref: integerSchema({ minimum: 1 }),
2412
- text: boundedText,
2413
- clear: booleanSchema(),
2414
- submit: booleanSchema(),
2415
- verification: stringSchema({ enum: ["report", "require_exact"] }),
2416
- observationLimit: integerSchema({ minimum: 1, maximum: OBSERVATION_V1_LIMITS.maxElements })
2417
- }, ["observationId", "ref", "text"]),
2418
- outputSchema: observationOutput,
2419
- requiredCapabilities: ["action.input", "observation.read"],
2420
- mutating: true,
2421
- idempotency: "non_idempotent",
2422
- cancellation: "best_effort",
2423
- sensitivity: { input: ["browser_data", "credential"], output: ["browser_data", "credential"] },
2424
- artifactKinds: []
2425
- }),
2426
- tool({
2427
- name: "browser.keyboard",
2428
- title: "Type with keyboard",
2429
- description: "Dispatch keyboard input to the currently focused page control.",
2430
- context: "target",
2431
- inputSchema: objectSchema({
2432
- text: boundedText,
2433
- clear: booleanSchema(),
2434
- submit: booleanSchema(),
2435
- delayMs: integerSchema({ minimum: 0, maximum: 6e4 }),
2436
- focusSelector: boundedSelector,
2437
- verification: stringSchema({ enum: ["report", "require_exact"] }),
2438
- observationLimit: integerSchema({ minimum: 1, maximum: OBSERVATION_V1_LIMITS.maxElements })
2439
- }, ["text"]),
2440
- outputSchema: observationOutput,
2441
- requiredCapabilities: ["action.input", "observation.read"],
2442
- mutating: true,
2443
- idempotency: "non_idempotent",
2444
- cancellation: "best_effort",
2445
- sensitivity: { input: ["browser_data", "credential"], output: ["browser_data", "credential"] },
2446
- artifactKinds: []
2447
- }),
2448
- tool({
2449
- name: "browser.press",
2450
- title: "Press key",
2451
- description: "Dispatch a key or key combination and report bounded observable effects.",
2452
- context: "target",
2453
- inputSchema: objectSchema({
2454
- key: stringSchema({ minLength: 1, maxLength: 128 }),
2455
- observationLimit: integerSchema({ minimum: 1, maximum: OBSERVATION_V1_LIMITS.maxElements })
2456
- }, ["key"]),
2457
- outputSchema: observationOutput,
2458
- requiredCapabilities: ["action.input", "observation.read"],
2459
- mutating: true,
2460
- idempotency: "non_idempotent",
2461
- cancellation: "best_effort",
2462
- sensitivity: { input: ["browser_data"], output: ["browser_data", "credential"] },
2463
- artifactKinds: []
2464
- }),
2465
- tool({
2466
- name: "browser.capture",
2467
- title: "Capture screenshot",
2468
- description: "Capture the viewport, full page, or a selected element as protected Artifacts.",
2469
- context: "target",
2470
- inputSchema: objectSchema({
2471
- fullPage: booleanSchema(),
2472
- selector: boundedSelector,
2473
- includeOriginal: booleanSchema(),
2474
- annotations: objectSchema({
2475
- observationId: opaqueIdSchema,
2476
- refs: arraySchema(integerSchema({ minimum: 1 }), { maxItems: 200, uniqueItems: true })
2477
- }, ["observationId"])
2478
- }),
2479
- outputSchema: artifactOutput,
2480
- requiredCapabilities: ["artifact.read"],
2481
- mutating: false,
2482
- idempotency: "idempotent",
2483
- cancellation: "best_effort",
2484
- sensitivity: { input: ["browser_data"], output: ["browser_data"] },
2485
- artifactKinds: ["screenshot", "screenshot_preview"]
2486
- }),
2487
- tool({
2488
- name: "browser.pdf",
2489
- title: "Capture PDF",
2490
- description: "Print the controlled target to a protected PDF Artifact.",
2491
- context: "target",
2492
- inputSchema: objectSchema({ landscape: booleanSchema() }),
2493
- outputSchema: artifactOutput,
2494
- requiredCapabilities: ["artifact.read"],
2495
- mutating: false,
2496
- idempotency: "idempotent",
2497
- cancellation: "best_effort",
2498
- sensitivity: { input: [], output: ["browser_data"] },
2499
- artifactKinds: ["pdf"]
2500
- }),
2501
- tool({
2502
- name: "browser.upload",
2503
- title: "Upload file",
2504
- description: "Assign a client-authorized local file and verify the browser selection.",
2505
- context: "target",
2506
- inputSchema: {
2507
- oneOf: [
2508
- objectSchema({
2509
- artifactId: sensitive(opaqueIdSchema, "user_file"),
2510
- observationId: opaqueIdSchema,
2511
- ref: integerSchema({ minimum: 1 }),
2512
- observationLimit: integerSchema({ minimum: 1, maximum: OBSERVATION_V1_LIMITS.maxElements })
2513
- }, ["artifactId", "observationId", "ref"]),
2514
- objectSchema({
2515
- artifactId: sensitive(opaqueIdSchema, "user_file"),
2516
- inputIndex: integerSchema({ minimum: 1 }),
2517
- observationLimit: integerSchema({ minimum: 1, maximum: OBSERVATION_V1_LIMITS.maxElements })
2518
- }, ["artifactId", "inputIndex"]),
2519
- objectSchema({
2520
- artifactId: sensitive(opaqueIdSchema, "user_file"),
2521
- observationLimit: integerSchema({ minimum: 1, maximum: OBSERVATION_V1_LIMITS.maxElements })
2522
- }, ["artifactId"])
2523
- ]
2524
- },
2525
- outputSchema: observationOutput,
2526
- requiredCapabilities: ["action.input", "artifact.read", "observation.read"],
2527
- mutating: true,
2528
- idempotency: "non_idempotent",
2529
- cancellation: "best_effort",
2530
- sensitivity: { input: ["user_file"], output: ["browser_data", "credential"] },
2531
- artifactKinds: []
2532
- }),
2533
- tool({
2534
- name: "browser.tabs.list",
2535
- title: "List browser tabs",
2536
- description: "List ManagedTabSet targets and all eligible user tabs in the connected browser.",
2537
- context: "workspace",
2538
- inputSchema: objectSchema({
2539
- scope: stringSchema({ enum: ["all", "managed_only", "user_tabs"] })
2540
- }),
2541
- outputSchema: resultSchema("workspace", {
2542
- targets: arraySchema(objectSchema({
2543
- targetId: opaqueIdSchema,
2544
- profileContextId: opaqueIdSchema,
2545
- title: sensitive(stringSchema({ maxLength: 4096 }), "browser_data"),
2546
- url: boundedUrl,
2547
- active: booleanSchema(),
2548
- origin: stringSchema({ enum: ["managed", "managed_popup", "user_tab"] }),
2549
- managedTabSetId: opaqueIdSchema,
2550
- controlState: stringSchema({ enum: ["available", "controlled", "busy"] })
2551
- }, ["targetId", "title", "url", "active", "origin", "controlState"]))
2552
- }, ["targets"]),
2553
- requiredCapabilities: ["browser.control"],
2554
- mutating: false,
2555
- idempotency: "read_only",
2556
- cancellation: "not_applicable",
2557
- sensitivity: { input: [], output: ["browser_data"] },
2558
- artifactKinds: []
2559
- }),
2560
- tool({
2561
- name: "browser.tabs.switch",
2562
- title: "Switch tab",
2563
- description: "Set a managed or user tab as the active controlled target for the current Lease.",
2564
- context: "workspace",
2565
- inputSchema: objectSchema({ targetId: opaqueIdSchema }, ["targetId"]),
2566
- outputSchema: resultSchema("target"),
2567
- requiredCapabilities: ["browser.control"],
2568
- mutating: true,
2569
- idempotency: "idempotent",
2570
- cancellation: "before_dispatch",
2571
- sensitivity: { input: ["browser_data"], output: ["browser_data"] },
2572
- artifactKinds: []
2573
- }),
2574
- tool({
2575
- name: "browser.tabs.close",
2576
- title: "Close tab",
2577
- description: "Close one explicitly addressed managed or user tab.",
2578
- context: "target",
2579
- inputSchema: emptyInput,
2580
- outputSchema: resultSchema("workspace", { closedTargetId: opaqueIdSchema }, ["closedTargetId"]),
2581
- requiredCapabilities: ["browser.control"],
2582
- mutating: true,
2583
- idempotency: "idempotent",
2584
- cancellation: "before_dispatch",
2585
- sensitivity: { input: [], output: ["browser_data"] },
2586
- artifactKinds: []
2587
- }),
2588
- tool({
2589
- name: "browser.tabs.release",
2590
- title: "Release tab control",
2591
- description: "Relinquish this Lease control of one tab without closing it.",
2592
- context: "target",
2593
- inputSchema: emptyInput,
2594
- outputSchema: resultSchema("target", { released: booleanSchema() }, ["released"]),
2595
- requiredCapabilities: ["browser.control"],
2596
- mutating: true,
2597
- idempotency: "idempotent",
2598
- cancellation: "before_dispatch",
2599
- sensitivity: { input: [], output: ["browser_data"] },
2600
- artifactKinds: []
2601
- }),
2602
- tool({
2603
- name: "browser.frames.list",
2604
- title: "List frames",
2605
- description: "List frames and CDP sessions belonging to the controlled target.",
2606
- context: "target",
2607
- inputSchema: emptyInput,
2608
- outputSchema: resultSchema("target", {
2609
- frames: arraySchema(objectSchema({
2610
- frameId: opaqueIdSchema,
2611
- parentFrameId: opaqueIdSchema,
2612
- url: boundedUrl,
2613
- name: sensitive(stringSchema({ maxLength: 4096 }), "browser_data")
2614
- }, ["frameId", "url", "name"]))
2615
- }, ["frames"]),
2616
- requiredCapabilities: ["observation.read"],
2617
- mutating: false,
2618
- idempotency: "read_only",
2619
- cancellation: "not_applicable",
2620
- sensitivity: { input: [], output: ["browser_data"] },
2621
- artifactKinds: []
2622
- }),
2623
- tool({
2624
- name: "browser.frames.switch",
2625
- title: "Switch frame",
2626
- description: "Set the active frame for the current Lease after target-access validation.",
2627
- context: "target",
2628
- inputSchema: {
2629
- oneOf: [
2630
- objectSchema({ frameId: opaqueIdSchema }, ["frameId"]),
2631
- objectSchema({ top: booleanSchema({ const: true }) }, ["top"])
2632
- ]
2633
- },
2634
- outputSchema: resultSchema("target", { frameId: opaqueIdSchema }, ["frameId"]),
2635
- requiredCapabilities: ["browser.control"],
2636
- mutating: true,
2637
- idempotency: "idempotent",
2638
- cancellation: "before_dispatch",
2639
- sensitivity: { input: ["browser_data"], output: ["browser_data"] },
2640
- artifactKinds: []
2641
- }),
2642
- tool({
2643
- name: "browser.dialogs.list",
2644
- title: "List dialogs",
2645
- description: "List pending dialogs for controlled targets without auto-accepting them.",
2646
- context: "workspace",
2647
- inputSchema: emptyInput,
2648
- outputSchema: resultSchema("workspace", {
2649
- dialogs: arraySchema(objectSchema({
2650
- dialogId: opaqueIdSchema,
2651
- targetId: opaqueIdSchema,
2652
- type: stringSchema({ enum: ["alert", "confirm", "prompt", "beforeunload"] }),
2653
- message: sensitive(stringSchema({ maxLength: 65536 }), "browser_data")
2654
- }, ["dialogId", "targetId", "type", "message"]))
2655
- }, ["dialogs"]),
2656
- requiredCapabilities: ["event.read"],
2657
- mutating: false,
2658
- idempotency: "read_only",
2659
- cancellation: "not_applicable",
2660
- sensitivity: { input: [], output: ["browser_data"] },
2661
- artifactKinds: []
2662
- }),
2663
- tool({
2664
- name: "browser.dialogs.respond",
2665
- title: "Respond to dialog",
2666
- description: "Explicitly accept or dismiss a pending dialog on a controlled target.",
2667
- context: "target",
2668
- inputSchema: objectSchema({
2669
- dialogId: opaqueIdSchema,
2670
- action: stringSchema({ enum: ["accept", "dismiss"] }),
2671
- promptText: sensitive(stringSchema({ maxLength: 65536 }), "browser_data", "credential")
2672
- }, ["dialogId", "action"]),
2673
- outputSchema: resultSchema("target", { dialogId: opaqueIdSchema, action: stringSchema({ enum: ["accept", "dismiss"] }) }, ["dialogId", "action"]),
2674
- requiredCapabilities: ["action.input"],
2675
- mutating: true,
2676
- idempotency: "idempotent",
2677
- cancellation: "before_dispatch",
2678
- sensitivity: { input: ["browser_data", "credential"], output: ["browser_data"] },
2679
- artifactKinds: []
2680
- }),
2681
- tool({
2682
- name: "browser.auth.set",
2683
- title: "Set HTTP auth",
2684
- description: "Set Workspace-scoped HTTP authentication credentials.",
2685
- context: "workspace",
2686
- inputSchema: objectSchema({
2687
- username: sensitive(stringSchema({ minLength: 1, maxLength: 4096 }), "credential"),
2688
- password: sensitive(stringSchema({ maxLength: 65536 }), "credential")
2689
- }, ["username", "password"]),
2690
- outputSchema: resultSchema("workspace", { configured: booleanSchema({ const: true }) }, ["configured"]),
2691
- requiredCapabilities: ["auth.manage"],
2692
- mutating: true,
2693
- idempotency: "idempotent",
2694
- cancellation: "before_dispatch",
2695
- sensitivity: { input: ["credential"], output: [] },
2696
- artifactKinds: []
2697
- }),
2698
- tool({
2699
- name: "browser.auth.clear",
2700
- title: "Clear HTTP auth",
2701
- description: "Remove Workspace-scoped HTTP authentication credentials.",
2702
- context: "workspace",
2703
- inputSchema: emptyInput,
2704
- outputSchema: resultSchema("workspace", { configured: booleanSchema({ const: false }) }, ["configured"]),
2705
- requiredCapabilities: ["auth.manage"],
2706
- mutating: true,
2707
- idempotency: "idempotent",
2708
- cancellation: "before_dispatch",
2709
- sensitivity: { input: [], output: [] },
2710
- artifactKinds: []
2711
- }),
2712
- tool({
2713
- name: "browser.cookies.list",
2714
- title: "List cookies",
2715
- description: "List cookies available to a controlled target with sensitivity metadata.",
2716
- context: "target",
2717
- inputSchema: objectSchema({
2718
- domain: sensitive(stringSchema({ minLength: 1, maxLength: 2048 }), "browser_data")
2719
- }),
2720
- outputSchema: resultSchema("target", {
2721
- cookies: arraySchema(objectSchema({
2722
- name: sensitive(stringSchema({ maxLength: 4096 }), "browser_data"),
2723
- value: sensitive(stringSchema({ maxLength: 1e6 }), "credential"),
2724
- domain: sensitive(stringSchema({ maxLength: 2048 }), "browser_data"),
2725
- path: sensitive(stringSchema({ maxLength: 4096 }), "browser_data"),
2726
- httpOnly: booleanSchema(),
2727
- secure: booleanSchema(),
2728
- expires: numberSchema()
2729
- }, ["name", "value", "domain", "path", "httpOnly", "secure", "expires"]))
2730
- }, ["cookies"]),
2731
- requiredCapabilities: ["cookies.read"],
2732
- mutating: false,
2733
- idempotency: "read_only",
2734
- cancellation: "best_effort",
2735
- sensitivity: { input: ["browser_data"], output: ["browser_data", "credential"] },
2736
- artifactKinds: []
2737
- }),
2738
- tool({
2739
- name: "browser.network.requests",
2740
- title: "List network requests",
2741
- description: "List bounded Workspace-scoped network request metadata.",
2742
- context: "workspace",
2743
- inputSchema: objectSchema({
2744
- limit: integerSchema({ minimum: 1, maximum: 1e3 }),
2745
- after: integerSchema({ minimum: 0 }),
2746
- url: sensitive(stringSchema({ maxLength: 16384 }), "browser_data"),
2747
- method: stringSchema({ maxLength: 32 }),
2748
- status: stringSchema({ maxLength: 16 }),
2749
- type: arraySchema(stringSchema({ minLength: 1, maxLength: 128 }), { uniqueItems: true })
2750
- }),
2751
- outputSchema: resultSchema("workspace", {
2752
- requests: arraySchema(objectSchema({
2753
- requestId: opaqueIdSchema,
2754
- sequence: integerSchema({ minimum: 1 }),
2755
- method: stringSchema({ maxLength: 32 }),
2756
- url: boundedUrl,
2757
- status: integerSchema({ minimum: 100, maximum: 999 }),
2758
- type: stringSchema({ maxLength: 128 }),
2759
- size: integerSchema({ minimum: 0 }),
2760
- durationMs: numberSchema({ minimum: 0 }),
2761
- error: sensitive(stringSchema({ maxLength: 4096 }), "browser_data")
2762
- }, ["requestId", "sequence", "method", "url", "type"])),
2763
- nextCursor: integerSchema({ minimum: 0 }),
2764
- truncated: booleanSchema()
2765
- }, ["requests", "nextCursor", "truncated"]),
2766
- requiredCapabilities: ["network.observe"],
2767
- mutating: false,
2768
- idempotency: "read_only",
2769
- cancellation: "best_effort",
2770
- sensitivity: { input: ["browser_data"], output: ["browser_data"] },
2771
- artifactKinds: []
2772
- }),
2773
- tool({
2774
- name: "browser.network.request",
2775
- title: "Read network request",
2776
- description: "Read bounded details for one Workspace-scoped request.",
2777
- context: "workspace",
2778
- inputSchema: objectSchema({ requestId: opaqueIdSchema, includeBody: booleanSchema() }, ["requestId"]),
2779
- outputSchema: resultSchema("workspace", {
2780
- request: networkRequestDetailSchema,
2781
- body: sensitive(stringSchema({ maxLength: 1e6 }), "browser_data", "credential"),
2782
- bodyEncoding: stringSchema({ enum: ["utf8", "base64"] }),
2783
- mimeType: stringSchema({ maxLength: 256 }),
2784
- bodyTruncated: booleanSchema()
2785
- }, ["request", "bodyTruncated"]),
2786
- requiredCapabilities: ["network.observe"],
2787
- mutating: false,
2788
- idempotency: "read_only",
2789
- cancellation: "best_effort",
2790
- sensitivity: { input: ["browser_data"], output: ["browser_data", "credential"] },
2791
- artifactKinds: []
2792
- }),
2793
- tool({
2794
- name: "browser.network.clear",
2795
- title: "Clear network journal",
2796
- description: "Clear captured request metadata for the current Workspace.",
2797
- context: "workspace",
2798
- inputSchema: emptyInput,
2799
- outputSchema: resultSchema("workspace", { cleared: booleanSchema({ const: true }) }, ["cleared"]),
2800
- requiredCapabilities: ["network.observe"],
2801
- mutating: true,
2802
- idempotency: "idempotent",
2803
- cancellation: "before_dispatch",
2804
- sensitivity: { input: [], output: [] },
2805
- artifactKinds: []
2806
- }),
2807
- tool({
2808
- name: "browser.network.rules.list",
2809
- title: "List network rules",
2810
- description: "List interception rules owned by the current Workspace.",
2811
- context: "workspace",
2812
- inputSchema: emptyInput,
2813
- outputSchema: resultSchema("workspace", {
2814
- rules: arraySchema(networkRuleSchema)
2815
- }, ["rules"]),
2816
- requiredCapabilities: ["network.observe"],
2817
- mutating: false,
2818
- idempotency: "read_only",
2819
- cancellation: "not_applicable",
2820
- sensitivity: { input: [], output: ["browser_data", "credential"] },
2821
- artifactKinds: []
2822
- }),
2823
- tool({
2824
- name: "browser.network.rules.add",
2825
- title: "Add network rule",
2826
- description: "Add a Workspace-scoped block, mock, or header rule.",
2827
- context: "workspace",
2828
- inputSchema: {
2829
- oneOf: [
2830
- objectSchema({ type: stringSchema({ const: "block" }), pattern: boundedUrl }, ["type", "pattern"]),
2831
- objectSchema({
2832
- type: stringSchema({ const: "mock" }),
2833
- pattern: boundedUrl,
2834
- status: integerSchema({ minimum: 100, maximum: 999 }),
2835
- headers: arraySchema(headerSchema, { maxItems: 256 }),
2836
- body: sensitive(stringSchema({ maxLength: 1e6 }), "browser_data", "credential")
2837
- }, ["type", "pattern"]),
2838
- objectSchema({
2839
- type: stringSchema({ const: "headers" }),
2840
- pattern: boundedUrl,
2841
- headers: arraySchema(headerSchema, { minItems: 1, maxItems: 256 })
2842
- }, ["type", "pattern", "headers"])
2843
- ]
2844
- },
2845
- outputSchema: resultSchema("workspace", { ruleId: opaqueIdSchema }, ["ruleId"]),
2846
- requiredCapabilities: ["network.modify"],
2847
- mutating: true,
2848
- idempotency: "non_idempotent",
2849
- cancellation: "before_dispatch",
2850
- sensitivity: { input: ["browser_data", "credential"], output: ["browser_data"] },
2851
- artifactKinds: []
2852
- }),
2853
- tool({
2854
- name: "browser.network.rules.remove",
2855
- title: "Remove network rule",
2856
- description: "Remove one owned interception rule or all rules in the Workspace.",
2857
- context: "workspace",
2858
- inputSchema: {
2859
- oneOf: [
2860
- objectSchema({ ruleId: opaqueIdSchema }, ["ruleId"]),
2861
- objectSchema({ all: booleanSchema({ const: true }) }, ["all"])
2862
- ]
2863
- },
2864
- outputSchema: resultSchema("workspace", { removed: integerSchema({ minimum: 0 }) }, ["removed"]),
2865
- requiredCapabilities: ["network.modify"],
2866
- mutating: true,
2867
- idempotency: "idempotent",
2868
- cancellation: "before_dispatch",
2869
- sensitivity: { input: ["browser_data"], output: [] },
2870
- artifactKinds: []
2871
- }),
2872
- tool({
2873
- name: "browser.eval",
2874
- title: "Evaluate JavaScript",
2875
- description: "Evaluate JavaScript in a controlled target when the Agent host exposes developer capability.",
2876
- context: "target",
2877
- inputSchema: objectSchema({ expression: boundedText, awaitPromise: booleanSchema() }, ["expression"]),
2878
- outputSchema: resultSchema("target", {
2879
- value: sensitive({}, "browser_data", "credential"),
2880
- truncated: booleanSchema()
2881
- }, ["value", "truncated"]),
2882
- requiredCapabilities: ["developer.eval"],
2883
- mutating: true,
2884
- idempotency: "non_idempotent",
2885
- cancellation: "best_effort",
2886
- sensitivity: { input: ["browser_data", "credential"], output: ["browser_data", "credential"] },
2887
- artifactKinds: []
2888
- })
2889
- ];
2890
- var toolsByName = new Map(TOOL_DEFINITIONS.map((definition) => [definition.name, definition]));
2891
- function schemaSensitivities(schema, result = /* @__PURE__ */ new Set()) {
2892
- for (const value of schema["x-browser-pilot-sensitivity"] ?? []) result.add(value);
2893
- for (const property of Object.values(schema.properties ?? {})) schemaSensitivities(property, result);
2894
- for (const alternative of schema.oneOf ?? []) schemaSensitivities(alternative, result);
2895
- if (schema.items) schemaSensitivities(schema.items, result);
2896
- if (schema.additionalProperties && typeof schema.additionalProperties === "object") {
2897
- schemaSensitivities(schema.additionalProperties, result);
2898
- }
2899
- return result;
2900
- }
2901
- function assertToolManifest(definitions = TOOL_DEFINITIONS) {
2902
- const names = /* @__PURE__ */ new Set();
2903
- const knownCapabilities = new Set(CAPABILITIES);
2904
- for (const definition of definitions) {
2905
- if (!/^[a-z][a-z0-9]*(?:[._][a-z][a-z0-9]*)+$/.test(definition.name)) {
2906
- throw new Error(`Invalid tool name: ${definition.name}`);
2907
- }
2908
- if (names.has(definition.name)) throw new Error(`Duplicate tool name: ${definition.name}`);
2909
- names.add(definition.name);
2910
- for (const capability of definition.requiredCapabilities) {
2911
- if (!knownCapabilities.has(capability)) {
2912
- throw new Error(`Tool ${definition.name} references unknown capability ${capability}`);
2913
- }
2914
- }
2915
- if (!definition.mutating && definition.idempotency === "non_idempotent") {
2916
- throw new Error(`Read-only tool ${definition.name} cannot be non-idempotent`);
2917
- }
2918
- if (definition.mutating && definition.cancellation === "not_applicable") {
2919
- throw new Error(`Mutating tool ${definition.name} must declare cancellation semantics`);
2920
- }
2921
- for (const direction of ["input", "output"]) {
2922
- const sensitivity = definition.sensitivity[direction];
2923
- if (new Set(sensitivity).size !== sensitivity.length) {
2924
- throw new Error(`Tool ${definition.name} has duplicate ${direction} sensitivity`);
2925
- }
2926
- for (const value of sensitivity) {
2927
- if (!SENSITIVITIES.includes(value)) {
2928
- throw new Error(`Tool ${definition.name} references unknown sensitivity ${value}`);
2929
- }
2930
- }
2931
- for (const value of schemaSensitivities(
2932
- direction === "input" ? definition.inputSchema : definition.outputSchema
2933
- )) {
2934
- if (!SENSITIVITIES.includes(value)) {
2935
- throw new Error(
2936
- `Tool ${definition.name} ${direction} schema contains unknown sensitivity ${value}`
2937
- );
2938
- }
2939
- if (!sensitivity.includes(value)) {
2940
- throw new Error(
2941
- `Tool ${definition.name} ${direction} schema marks ${value} without declaring it`
2942
- );
2943
- }
2944
- }
2945
- }
2946
- assertSchemaDefinition(definition.inputSchema, `${definition.name}.inputSchema`);
2947
- assertSchemaDefinition(definition.outputSchema, `${definition.name}.outputSchema`);
2948
- }
2949
- }
2950
- assertToolManifest();
2951
-
2952
- // src/services/command-runtime.ts
2953
- import { randomUUID as randomUUID2 } from "crypto";
2954
-
2955
- // src/services/event-journal.ts
2956
- import { randomUUID as randomUUID3 } from "crypto";
2957
-
2958
- // src/services/broker-runtime.ts
2959
- var DEFAULT_PROTOCOL_LIMITS = {
2960
- maxMessageBytes: 1024 * 1024,
2961
- maxResultBytes: 4 * 1024 * 1024,
2962
- maxArtifactBytes: 100 * 1024 * 1024,
2963
- eventJournalSize: 1e3
2964
- };
2965
-
2966
- // src/bridge/stdio-bridge.ts
2967
- function isIncomingCall(message) {
2968
- return "method" in message;
2969
- }
2970
- function isOutOfBandControl(message) {
2971
- if (message.method === "commands/get" || message.method === "commands/cancel") return true;
2972
- if (message.method !== "tools/call" || !message.params || typeof message.params !== "object") return false;
2973
- if (Array.isArray(message.params)) return false;
2974
- const name = message.params.name;
2975
- return name === "browser.dialogs.list" || name === "browser.dialogs.respond";
2976
- }
2977
- function errorResponse(id, error) {
2978
- const stable = asBrowserPilotError(error);
2979
- return { jsonrpc: "2.0", id, error: stable.toJsonRpcError() };
2980
- }
2981
- async function writeLine(output, value, maxResultBytes) {
2982
- let serialized = JSON.stringify(value);
2983
- if (Buffer.byteLength(serialized, "utf8") > maxResultBytes) {
2984
- if (!("id" in value)) return;
2985
- serialized = JSON.stringify(errorResponse(value.id, new BrowserPilotError(
2986
- "result_too_large",
2987
- `Protocol result exceeds ${maxResultBytes} bytes`,
2988
- { context: { maxResultBytes } }
2989
- )));
2990
- }
2991
- if (output.destroyed || !output.writable) throw new Error("Protocol output is closed");
2992
- if (!output.write(`${serialized}
2993
- `)) await once(output, "drain");
2994
- }
2995
- function negotiatedTransportLimits(value, ceilings) {
2996
- if (!value || typeof value !== "object" || Array.isArray(value)) return void 0;
2997
- const limits = value.limits;
2998
- if (!limits || typeof limits !== "object" || Array.isArray(limits)) return void 0;
2999
- if (!Number.isSafeInteger(limits.maxMessageBytes) || !Number.isSafeInteger(limits.maxResultBytes) || limits.maxMessageBytes <= 0 || limits.maxResultBytes <= 0) return void 0;
3000
- return {
3001
- maxMessageBytes: Math.min(limits.maxMessageBytes, ceilings.maxMessageBytes),
3002
- maxResultBytes: Math.min(limits.maxResultBytes, ceilings.maxResultBytes)
3003
- };
3004
- }
3005
- function decodeLine(bytes) {
3006
- try {
3007
- return new TextDecoder("utf-8", { fatal: true }).decode(bytes);
3008
- } catch (cause) {
3009
- throw new BrowserPilotError("invalid_argument", "Protocol message is not valid UTF-8", {
3010
- rpcCode: -32700,
3011
- cause
3012
- });
3013
- }
3014
- }
3015
- async function runStdioBridge(options) {
3016
- const bridgeSessionId = options.bridgeSessionId ?? `bridge:${randomUUID5()}`;
3017
- const ceilings = {
3018
- maxMessageBytes: options.maxMessageBytes ?? DEFAULT_PROTOCOL_LIMITS.maxMessageBytes,
3019
- maxResultBytes: options.maxResultBytes ?? DEFAULT_PROTOCOL_LIMITS.maxResultBytes
3020
- };
3021
- let maxMessageBytes = ceilings.maxMessageBytes;
3022
- let maxResultBytes = ceilings.maxResultBytes;
3023
- const maxPendingCalls = options.maxPendingCalls ?? 256;
3024
- const maxOutOfBandCalls = options.maxOutOfBandCalls ?? 16;
3025
- if (!Number.isSafeInteger(maxPendingCalls) || maxPendingCalls <= 0 || !Number.isSafeInteger(maxOutOfBandCalls) || maxOutOfBandCalls <= 0) throw new Error("Bridge in-flight call limits must be positive integers");
3026
- let pending = Buffer.alloc(0);
3027
- let reason = "eof";
3028
- let exitCode = 0;
3029
- let stopped = false;
3030
- let framingFailed = false;
3031
- let outputTail = Promise.resolve();
3032
- let normalTail = Promise.resolve();
3033
- let initialization;
3034
- let notificationController;
3035
- let notificationTask;
3036
- const inFlight = /* @__PURE__ */ new Set();
3037
- let pendingCalls = 0;
3038
- let outOfBandCalls = 0;
3039
- const queueWrite = (value) => {
3040
- const write = outputTail.then(() => writeLine(options.output, value, maxResultBytes));
3041
- outputTail = write.catch(() => {
3042
- });
3043
- return write;
3044
- };
3045
- const failFraming = async (error) => {
3046
- exitCode = 1;
3047
- reason = "protocol_error";
3048
- stopped = true;
3049
- framingFailed = true;
3050
- await queueWrite(errorResponse(null, error));
3051
- };
3052
- const track = (task, outOfBand) => {
3053
- inFlight.add(task);
3054
- if (outOfBand) outOfBandCalls += 1;
3055
- else pendingCalls += 1;
3056
- const finished = () => {
3057
- inFlight.delete(task);
3058
- if (outOfBand) outOfBandCalls -= 1;
3059
- else pendingCalls -= 1;
3060
- };
3061
- void task.then(finished, finished);
3062
- };
3063
- const startNotifications = () => {
3064
- if (notificationTask || !options.backend.notifications) return;
3065
- notificationController = new AbortController();
3066
- notificationTask = (async () => {
3067
- for await (const notification of options.backend.notifications(
3068
- bridgeSessionId,
3069
- notificationController.signal
3070
- )) {
3071
- if (notificationController.signal.aborted || stopped) break;
3072
- await queueWrite(notification);
3073
- }
3074
- })().catch(() => {
3075
- if (notificationController?.signal.aborted) return;
3076
- stopped = true;
3077
- reason = "output_closed";
3078
- exitCode = 1;
3079
- options.input.destroy();
3080
- });
3081
- };
3082
- const processLine = async (rawLine) => {
3083
- const line = rawLine.at(-1) === 13 ? rawLine.subarray(0, -1) : rawLine;
3084
- let message;
3085
- try {
3086
- message = parseJsonRpcMessage(decodeLine(line), maxMessageBytes);
3087
- } catch (error) {
3088
- await failFraming(error);
3089
- return;
3090
- }
3091
- if (!isIncomingCall(message)) {
3092
- await failFraming(invalidArgument("Bridge accepts JSON-RPC requests and notifications only", void 0, -32600));
3093
- return;
3094
- }
3095
- const requestId = "id" in message ? message.id : void 0;
3096
- const outOfBand = isOutOfBandControl(message);
3097
- const saturated = outOfBand ? outOfBandCalls >= maxOutOfBandCalls : pendingCalls >= maxPendingCalls;
3098
- if (saturated) {
3099
- if (requestId !== void 0) {
3100
- await queueWrite(errorResponse(requestId, new BrowserPilotError(
3101
- "result_too_large",
3102
- `Bridge ${outOfBand ? "control" : "request"} queue is full`,
3103
- {
3104
- retryable: true,
3105
- context: {
3106
- limit: outOfBand ? maxOutOfBandCalls : maxPendingCalls,
3107
- queue: outOfBand ? "control" : "request"
3108
- }
3109
- }
3110
- )));
3111
- }
3112
- return;
3113
- }
3114
- const dispatch = async () => {
3115
- try {
3116
- const result = await options.backend.call(bridgeSessionId, message.method, message.params);
3117
- if (requestId !== void 0 && reason !== "protocol_error") {
3118
- await queueWrite({ jsonrpc: "2.0", id: requestId, result });
3119
- }
3120
- if (message.method === "initialize" && reason !== "protocol_error") {
3121
- const negotiated = negotiatedTransportLimits(result, ceilings);
3122
- if (negotiated) {
3123
- maxMessageBytes = negotiated.maxMessageBytes;
3124
- maxResultBytes = negotiated.maxResultBytes;
3125
- }
3126
- startNotifications();
3127
- }
3128
- } catch (error) {
3129
- if (requestId !== void 0 && reason !== "protocol_error") {
3130
- try {
3131
- await queueWrite(errorResponse(requestId, error));
3132
- } catch {
3133
- stopped = true;
3134
- reason = "output_closed";
3135
- exitCode = 1;
3136
- }
3137
- }
3138
- }
3139
- };
3140
- const task = outOfBand ? (initialization ?? Promise.resolve()).then(dispatch) : normalTail.then(dispatch);
3141
- if (!outOfBand) normalTail = task.catch(() => {
3142
- });
3143
- if (message.method === "initialize") initialization = task.catch(() => {
3144
- });
3145
- track(task, outOfBand);
3146
- if (message.method === "shutdown") {
3147
- stopped = true;
3148
- reason = "shutdown";
1084
+ actionRequired: true
1085
+ }
1086
+ });
3149
1087
  }
3150
- };
3151
- try {
3152
- for await (const value of options.input) {
3153
- const chunk = Buffer.isBuffer(value) ? value : Buffer.from(value);
3154
- let offset = 0;
3155
- while (!stopped && offset < chunk.length) {
3156
- const newline = chunk.indexOf(10, offset);
3157
- if (newline === -1) {
3158
- const remaining = chunk.subarray(offset);
3159
- if (pending.length + remaining.length > maxMessageBytes) {
3160
- await failFraming(new BrowserPilotError(
3161
- "result_too_large",
3162
- `Protocol message exceeds ${maxMessageBytes} bytes`,
3163
- { context: { maxMessageBytes }, rpcCode: -32600 }
3164
- ));
3165
- break;
1088
+ removeStaleBrokerFilesSync();
1089
+ const candidates = await discoverBrowserCandidates();
1090
+ const filter = browserFilter?.toLowerCase();
1091
+ const matches = candidates.filter(({ candidate }) => !filter || [candidate.id, candidate.product, candidate.channel].some((value) => value?.toLowerCase().includes(filter)));
1092
+ if (browserFilter && matches.length === 0) {
1093
+ throw new BrowserPilotError(
1094
+ "browser_not_found",
1095
+ "No installed supported browser matches the requested selection.",
1096
+ {
1097
+ remediation: {
1098
+ code: "select_supported_browser",
1099
+ message: "Run browser discovery and select one of the returned browser IDs or product names.",
1100
+ actionRequired: true
3166
1101
  }
3167
- pending = pending.length === 0 ? Buffer.from(remaining) : Buffer.concat([pending, remaining]);
3168
- offset = chunk.length;
3169
- continue;
3170
1102
  }
3171
- const part = chunk.subarray(offset, newline);
3172
- if (pending.length + part.length > maxMessageBytes) {
3173
- await failFraming(new BrowserPilotError(
3174
- "result_too_large",
3175
- `Protocol message exceeds ${maxMessageBytes} bytes`,
3176
- { context: { maxMessageBytes }, rpcCode: -32600 }
3177
- ));
3178
- break;
3179
- }
3180
- const line = pending.length === 0 ? part : Buffer.concat([pending, part]);
3181
- pending = Buffer.alloc(0);
3182
- offset = newline + 1;
3183
- await processLine(line);
3184
- if (initialization) await initialization;
3185
- }
3186
- if (stopped) break;
3187
- }
3188
- if (!stopped && pending.length > 0) await processLine(pending);
3189
- notificationController?.abort();
3190
- await notificationTask;
3191
- if (!framingFailed) await Promise.allSettled([...inFlight]);
3192
- await outputTail;
3193
- } catch {
3194
- if (!stopped) {
3195
- reason = "output_closed";
3196
- exitCode = 1;
1103
+ );
3197
1104
  }
1105
+ const selected = matches.find((browser) => browser.endpoint !== void 0) ?? matches[0] ?? null;
1106
+ return await startDaemon(selected);
3198
1107
  } finally {
3199
- notificationController?.abort();
3200
- await notificationTask;
3201
- try {
3202
- await options.backend.disconnect(bridgeSessionId);
3203
- } catch {
3204
- }
1108
+ startupLock.release();
3205
1109
  }
3206
- return { exitCode, reason };
3207
1110
  }
3208
1111
 
1112
+ // src/protocol/model.ts
1113
+ var CAPABILITIES = [
1114
+ "browser.discovery",
1115
+ "browser.control",
1116
+ "workspace.manage",
1117
+ "observation.read",
1118
+ "action.input",
1119
+ "artifact.read",
1120
+ "event.read",
1121
+ "network.observe",
1122
+ "network.modify",
1123
+ "auth.manage",
1124
+ "cookies.read",
1125
+ "developer.eval"
1126
+ ];
1127
+ var OBSERVATION_V1_LIMITS = {
1128
+ defaultElements: 50,
1129
+ maxElements: 1e4,
1130
+ maxTitleCharacters: 4096,
1131
+ maxUrlCharacters: 16384,
1132
+ maxElementNameCharacters: 4096,
1133
+ maxElementValueCharacters: 65536,
1134
+ maxTextCharacters: 1e6,
1135
+ maxTreeDepth: 128,
1136
+ maxSerializedBytes: 2 * 1024 * 1024,
1137
+ ttlMs: 5 * 6e4,
1138
+ maxStoredObservations: 2048
1139
+ };
1140
+
3209
1141
  // src/compatibility-broker-client.ts
3210
- import { randomUUID as randomUUID6 } from "crypto";
3211
- var BRIDGE_SESSION_ID = "bridge:browser-pilot-cli";
3212
1142
  var CLIENT_KEY = "browser-pilot-cli";
3213
1143
  var LEASE_TTL_MS = 5 * 6e4;
1144
+ function compatibilityIdentity(clientKey, executableVersion) {
1145
+ const principalDigest = createHash4("sha256").update(clientKey).digest("base64url").slice(0, 24);
1146
+ const sessionDigest = createHash4("sha256").update(clientKey).update("\0").update(executableVersion).digest("base64url").slice(0, 24);
1147
+ return {
1148
+ clientSessionId: `client:browser-pilot-cli:${sessionDigest}`,
1149
+ instanceId: `local:browser-pilot-cli:${principalDigest}`
1150
+ };
1151
+ }
1152
+ function isSelected(target) {
1153
+ return target.selected ?? target.active === true;
1154
+ }
3214
1155
  function asRecord(value, label) {
3215
1156
  if (!value || typeof value !== "object" || Array.isArray(value)) {
3216
1157
  throw new BrowserPilotError("internal_error", `${label} returned an invalid result`);
@@ -3226,55 +1167,93 @@ function commandResult(value, method) {
3226
1167
  return asRecord(outcome.result, `${method} result`);
3227
1168
  }
3228
1169
  var CompatibilityBrokerClient = class _CompatibilityBrokerClient {
3229
- constructor(transport, initialized, workspace, lease) {
1170
+ constructor(transport, clientSessionId, initialized, workspace, lease, invocation) {
3230
1171
  this.transport = transport;
1172
+ this.clientSessionId = clientSessionId;
3231
1173
  this.initialized = initialized;
3232
1174
  this.workspace = workspace;
3233
1175
  this.lease = lease;
1176
+ this.invocation = invocation;
3234
1177
  }
3235
1178
  commandSequence = 0;
3236
- static async create(transport, executableVersion) {
3237
- const initialized = asRecord(await transport.brokerCall(BRIDGE_SESSION_ID, "initialize", {
1179
+ static async create(transport, executableVersion, clientKey = CLIENT_KEY, invocation = {}) {
1180
+ const identity = compatibilityIdentity(clientKey, executableVersion);
1181
+ const initialized = asRecord(await transport.brokerCall(identity.clientSessionId, "initialize", {
3238
1182
  client: {
3239
1183
  id: "org.browser-pilot.cli",
3240
1184
  name: "Browser Pilot CLI",
3241
1185
  version: executableVersion,
3242
- instanceId: "local:one-shot"
1186
+ instanceId: identity.instanceId
3243
1187
  },
3244
- protocol: { min: { major: 1, minor: 1 }, max: { major: 1, minor: 2 } },
3245
- requestedCapabilities: [...CAPABILITIES],
3246
- launchMode: "one-shot"
1188
+ protocol: { min: { major: 1, minor: 1 }, max: { major: 1, minor: 3 } },
1189
+ requestedCapabilities: [...CAPABILITIES]
3247
1190
  }), "initialize");
3248
- if (initialized.executableVersion !== executableVersion) {
3249
- throw new BrowserPilotError("protocol_incompatible", "Running Browser Pilot daemon is from another executable version", {
3250
- remediation: {
3251
- code: "use_matching_executable_or_isolate",
3252
- message: "Use the matching Browser Pilot installation, or set BROWSER_PILOT_HOME for a deliberately isolated Broker.",
3253
- actionRequired: true
3254
- }
3255
- });
3256
- }
3257
- const created = asRecord(await transport.brokerCall(BRIDGE_SESSION_ID, "workspaces/create", {
3258
- clientKey: CLIENT_KEY
1191
+ const created = asRecord(await transport.brokerCall(identity.clientSessionId, "workspaces/create", {
1192
+ clientKey
3259
1193
  }), "workspaces/create");
3260
- const leased = asRecord(await transport.brokerCall(BRIDGE_SESSION_ID, "leases/create", {
1194
+ const leased = asRecord(await transport.brokerCall(identity.clientSessionId, "leases/create", {
3261
1195
  workspaceId: created.workspace.id,
3262
- clientKey: CLIENT_KEY,
1196
+ clientKey,
3263
1197
  ttlMs: LEASE_TTL_MS
3264
1198
  }), "leases/create");
3265
- return new _CompatibilityBrokerClient(transport, initialized, created.workspace, leased.lease);
1199
+ return new _CompatibilityBrokerClient(
1200
+ transport,
1201
+ identity.clientSessionId,
1202
+ initialized,
1203
+ created.workspace,
1204
+ leased.lease,
1205
+ invocation
1206
+ );
3266
1207
  }
3267
1208
  async callTool(name, args = {}, targetId) {
3268
1209
  this.commandSequence += 1;
3269
- return commandResult(await this.transport.brokerCall(BRIDGE_SESSION_ID, "tools/call", {
1210
+ if (this.invocation.signal?.aborted) {
1211
+ throw new BrowserPilotError("command_cancelled", "Command was cancelled before dispatch");
1212
+ }
1213
+ const commandId = this.commandId(this.commandSequence);
1214
+ const request = this.transport.brokerCall(this.clientSessionId, "tools/call", {
3270
1215
  name,
3271
1216
  arguments: args,
3272
1217
  workspaceId: this.workspace.id,
3273
1218
  leaseId: this.lease.id,
3274
1219
  ...targetId ? { targetId } : {},
3275
- commandId: `command:cli-${process.pid}-${this.commandSequence}-${randomUUID6()}`,
3276
- deadlineMs: 6e4
3277
- }), name);
1220
+ commandId,
1221
+ ...this.invocation.requestId ? { idempotencyKey: `cli-request:${this.invocation.requestId}:${this.commandSequence}` } : {},
1222
+ deadlineMs: this.invocation.deadlineMs ?? 6e4
1223
+ });
1224
+ const cancel = () => {
1225
+ void this.transport.brokerCall(this.clientSessionId, "commands/cancel", {
1226
+ commandId,
1227
+ workspaceId: this.workspace.id
1228
+ }).catch(() => {
1229
+ });
1230
+ };
1231
+ this.invocation.signal?.addEventListener("abort", cancel, { once: true });
1232
+ try {
1233
+ return commandResult(await request, name);
1234
+ } finally {
1235
+ this.invocation.signal?.removeEventListener("abort", cancel);
1236
+ }
1237
+ }
1238
+ async listCommands(limit = 20, statuses) {
1239
+ const result = asRecord(await this.transport.brokerCall(this.clientSessionId, "commands/list", {
1240
+ workspaceId: this.workspace.id,
1241
+ limit,
1242
+ ...statuses ? { statuses: [...statuses] } : {}
1243
+ }), "commands/list");
1244
+ return result.commands;
1245
+ }
1246
+ async getCommand(commandId) {
1247
+ return asRecord(await this.transport.brokerCall(this.clientSessionId, "commands/get", {
1248
+ workspaceId: this.workspace.id,
1249
+ commandId
1250
+ }), "commands/get");
1251
+ }
1252
+ async cancelCommand(commandId) {
1253
+ return asRecord(await this.transport.brokerCall(this.clientSessionId, "commands/cancel", {
1254
+ workspaceId: this.workspace.id,
1255
+ commandId
1256
+ }), "commands/cancel");
3278
1257
  }
3279
1258
  async connectBrowser(browserId) {
3280
1259
  const selected = browserId ? this.initialized.browsers.find((candidate) => candidate.id === browserId) : this.initialized.browsers.find((candidate) => candidate.state === "ready") ?? this.initialized.browsers[0];
@@ -3304,6 +1283,16 @@ var CompatibilityBrokerClient = class _CompatibilityBrokerClient {
3304
1283
  }
3305
1284
  return result.profiles;
3306
1285
  }
1286
+ async identifyProfiles(profileContextId, refresh = false) {
1287
+ const result = await this.callTool("browser.profiles.identify", {
1288
+ ...profileContextId ? { profileContextId } : {},
1289
+ ...refresh ? { refresh: true } : {}
1290
+ });
1291
+ if (!Array.isArray(result.profiles)) {
1292
+ throw new BrowserPilotError("internal_error", "browser.profiles.identify returned invalid Profiles");
1293
+ }
1294
+ return result.profiles;
1295
+ }
3307
1296
  async selectProfile(profileContextId) {
3308
1297
  const result = await this.callTool("browser.profiles.select", { profileContextId });
3309
1298
  const profiles = await this.listProfiles();
@@ -3317,11 +1306,11 @@ var CompatibilityBrokerClient = class _CompatibilityBrokerClient {
3317
1306
  }
3318
1307
  async ensureTarget() {
3319
1308
  const targets = await this.listTabs("all");
3320
- const selected = targets.find((target) => target.active) ?? targets.find((target) => target.origin !== "user_tab");
1309
+ const selected = targets.find(isSelected) ?? targets.find((target) => target.origin !== "user_tab");
3321
1310
  if (selected) {
3322
- if (!selected.active) {
1311
+ if (!isSelected(selected)) {
3323
1312
  await this.callTool("browser.tabs.switch", { targetId: selected.targetId });
3324
- selected.active = true;
1313
+ selected.selected = true;
3325
1314
  }
3326
1315
  return selected;
3327
1316
  }
@@ -3329,11 +1318,11 @@ var CompatibilityBrokerClient = class _CompatibilityBrokerClient {
3329
1318
  }
3330
1319
  async ensureManagedTarget() {
3331
1320
  const targets = await this.listTabs("managed_only");
3332
- const selected = targets.find((target) => target.active) ?? targets[0];
1321
+ const selected = targets.find(isSelected) ?? targets[0];
3333
1322
  if (selected) {
3334
- if (!selected.active) {
1323
+ if (!isSelected(selected)) {
3335
1324
  await this.callTool("browser.tabs.switch", { targetId: selected.targetId });
3336
- selected.active = true;
1325
+ selected.selected = true;
3337
1326
  }
3338
1327
  return selected;
3339
1328
  }
@@ -3350,7 +1339,7 @@ var CompatibilityBrokerClient = class _CompatibilityBrokerClient {
3350
1339
  profileContextId: opened.profileContextId,
3351
1340
  title: String(opened.title ?? ""),
3352
1341
  url: String(opened.url),
3353
- active: true,
1342
+ selected: true,
3354
1343
  origin: "managed",
3355
1344
  controlState: "controlled"
3356
1345
  };
@@ -3363,7 +1352,7 @@ var CompatibilityBrokerClient = class _CompatibilityBrokerClient {
3363
1352
  return result.observationId;
3364
1353
  }
3365
1354
  async importArtifact(path, mimeType) {
3366
- const result = asRecord(await this.transport.brokerCall(BRIDGE_SESSION_ID, "artifacts/import", {
1355
+ const result = asRecord(await this.transport.brokerCall(this.clientSessionId, "artifacts/import", {
3367
1356
  workspaceId: this.workspace.id,
3368
1357
  leaseId: this.lease.id,
3369
1358
  path,
@@ -3371,8 +1360,19 @@ var CompatibilityBrokerClient = class _CompatibilityBrokerClient {
3371
1360
  }), "artifacts/import");
3372
1361
  return asRecord(result.artifact, "imported Artifact");
3373
1362
  }
1363
+ async listArtifacts(kinds) {
1364
+ const result = asRecord(await this.transport.brokerCall(this.clientSessionId, "artifacts/list", {
1365
+ workspaceId: this.workspace.id,
1366
+ leaseId: this.lease.id,
1367
+ ...kinds ? { kinds: [...kinds] } : {}
1368
+ }), "artifacts/list");
1369
+ if (!Array.isArray(result.artifacts)) {
1370
+ throw new BrowserPilotError("internal_error", "artifacts/list returned invalid Artifacts");
1371
+ }
1372
+ return result.artifacts;
1373
+ }
3374
1374
  async exportArtifact(artifactId, path) {
3375
- await this.transport.brokerCall(BRIDGE_SESSION_ID, "artifacts/export", {
1375
+ await this.transport.brokerCall(this.clientSessionId, "artifacts/export", {
3376
1376
  workspaceId: this.workspace.id,
3377
1377
  leaseId: this.lease.id,
3378
1378
  artifactId,
@@ -3381,113 +1381,88 @@ var CompatibilityBrokerClient = class _CompatibilityBrokerClient {
3381
1381
  });
3382
1382
  }
3383
1383
  async releaseArtifact(artifactId) {
3384
- await this.transport.brokerCall(BRIDGE_SESSION_ID, "artifacts/release", {
1384
+ await this.transport.brokerCall(this.clientSessionId, "artifacts/release", {
3385
1385
  workspaceId: this.workspace.id,
3386
1386
  leaseId: this.lease.id,
3387
1387
  artifactId
3388
1388
  });
3389
1389
  }
3390
1390
  async releaseWorkspace() {
3391
- await this.transport.brokerCall(BRIDGE_SESSION_ID, "workspaces/release", {
1391
+ await this.transport.brokerCall(this.clientSessionId, "workspaces/release", {
3392
1392
  workspaceId: this.workspace.id
3393
1393
  });
3394
1394
  }
1395
+ commandId(sequence) {
1396
+ if (!this.invocation.requestId) {
1397
+ return `command:cli-${process.pid}-${sequence}-${randomUUID2()}`;
1398
+ }
1399
+ const digest = createHash4("sha256").update(this.workspace.clientKey ?? CLIENT_KEY).update("\0").update(this.invocation.requestId).update("\0").update(String(sequence)).digest("base64url").slice(0, 32);
1400
+ return `command:cli-${digest}-${sequence}`;
1401
+ }
3395
1402
  };
3396
1403
  async function validateDaemon(client) {
3397
1404
  const health = await client.healthInfo();
3398
1405
  if (!health.ok) throw new BrowserPilotError("browser_disconnected", "Browser Pilot daemon is unavailable");
3399
- if (health.brokerProtocol !== 1) {
3400
- throw new BrowserPilotError("protocol_incompatible", "Running Browser Pilot daemon is from an older executable", {
3401
- remediation: {
3402
- code: "use_compatible_executable_or_isolate",
3403
- message: "Use a compatible Browser Pilot executable, or set BROWSER_PILOT_HOME for a deliberately isolated Broker.",
3404
- actionRequired: true
3405
- }
3406
- });
3407
- }
3408
- }
3409
- async function validateCompatibilityDaemon(client, executableVersion) {
3410
- await validateDaemon(client);
3411
- const health = await client.healthInfo();
3412
- const requester = createExecutableMetadataSync(
3413
- executableVersion,
3414
- publicExecutablePath(import.meta.url)
3415
- );
3416
- if (health.executableVersion !== void 0 && health.executableVersion !== requester.version || health.executableIdentity !== void 0 && health.executableIdentity !== requester.identity) {
3417
- throw new BrowserPilotError("protocol_incompatible", "Running Browser Pilot daemon is from another executable installation", {
1406
+ if (health.brokerProtocol !== BROKER_RPC_VERSION) {
1407
+ throw new BrowserPilotError("protocol_incompatible", "Running Browser Pilot Broker uses an incompatible private transport", {
3418
1408
  context: {
3419
- brokerExecutableVersion: health.executableVersion,
3420
- requesterExecutableVersion: requester.version
1409
+ brokerRpcVersion: health.brokerProtocol,
1410
+ requiredBrokerRpcVersion: BROKER_RPC_VERSION,
1411
+ serviceVersion: health.serviceVersion,
1412
+ executableVersion: health.executableVersion
3421
1413
  },
3422
1414
  remediation: {
3423
- code: "use_matching_executable_or_isolate",
3424
- message: "Use the matching Browser Pilot installation, or set BROWSER_PILOT_HOME for a deliberately isolated Broker.",
1415
+ code: "stop_incompatible_broker_or_isolate",
1416
+ message: "Stop the running Broker with the Browser Pilot executable that started it, or set BROWSER_PILOT_HOME for a deliberately isolated Broker.",
3425
1417
  actionRequired: true
3426
1418
  }
3427
1419
  });
3428
1420
  }
3429
1421
  }
3430
- async function connectCompatibility(executableVersion, browserFilter) {
1422
+ async function connectCompatibility(executableVersion, browserFilter, clientKey = CLIENT_KEY, invocation = {}) {
3431
1423
  const daemon = await connectDaemon(browserFilter);
3432
- await validateCompatibilityDaemon(daemon, executableVersion);
3433
- return CompatibilityBrokerClient.create(daemon, executableVersion);
1424
+ await validateDaemon(daemon);
1425
+ return CompatibilityBrokerClient.create(daemon, executableVersion, clientKey, invocation);
3434
1426
  }
3435
- async function resumeCompatibility(executableVersion) {
1427
+ async function resumeCompatibility(executableVersion, clientKey = CLIENT_KEY, invocation = {}) {
3436
1428
  if (!isDaemonRunning()) return null;
3437
1429
  const daemon = new DaemonClient();
3438
1430
  try {
3439
- await validateCompatibilityDaemon(daemon, executableVersion);
3440
- return await CompatibilityBrokerClient.create(daemon, executableVersion);
1431
+ await validateDaemon(daemon);
1432
+ return await CompatibilityBrokerClient.create(daemon, executableVersion, clientKey, invocation);
3441
1433
  } catch (error) {
3442
1434
  if (error instanceof BrowserPilotError && error.code === "protocol_incompatible") throw error;
3443
1435
  return null;
3444
1436
  }
3445
1437
  }
3446
- async function withCompatibilityTarget(executableVersion, operation) {
3447
- const client = await resumeCompatibility(executableVersion);
1438
+ async function withCompatibilityTarget(executableVersion, operation, clientKey = CLIENT_KEY, invocation = {}) {
1439
+ const client = await resumeCompatibility(executableVersion, clientKey, invocation);
3448
1440
  if (!client) throw new Error("Not connected");
3449
1441
  const target = await client.ensureTarget();
3450
1442
  return operation(client, target);
3451
1443
  }
3452
- async function shutdownCompatibility(executableVersion) {
1444
+ async function shutdownCompatibility(executableVersion, clientKey = CLIENT_KEY) {
3453
1445
  if (!isDaemonRunning()) return;
3454
1446
  const daemon = new DaemonClient();
3455
1447
  await validateDaemon(daemon);
3456
- const health = await daemon.healthInfo();
3457
- if (!health.ok || !health.brokerProcessIdentity || !health.executableVersion || !health.executableIdentity) {
3458
- throw new BrowserPilotError("protocol_incompatible", "Running Browser Pilot Broker does not support protected shutdown", {
3459
- remediation: {
3460
- code: "use_matching_executable_or_isolate",
3461
- message: "Use the Browser Pilot installation that started the Broker, or set BROWSER_PILOT_HOME for a deliberately isolated Broker.",
3462
- actionRequired: true
3463
- }
3464
- });
3465
- }
3466
- const requester = createExecutableMetadataSync(
3467
- executableVersion,
3468
- publicExecutablePath(import.meta.url)
3469
- );
3470
- if (requester.version !== health.executableVersion || requester.identity !== health.executableIdentity) {
3471
- throw new BrowserPilotError("protocol_incompatible", "This Browser Pilot installation does not own the running Broker", {
3472
- context: {
3473
- brokerExecutableVersion: health.executableVersion,
3474
- requesterExecutableVersion: requester.version
3475
- },
3476
- remediation: {
3477
- code: "use_matching_executable_or_isolate",
3478
- message: "Use the matching Browser Pilot installation, or set BROWSER_PILOT_HOME for a deliberately isolated Broker.",
3479
- actionRequired: true
3480
- }
3481
- });
3482
- }
3483
1448
  try {
3484
- const client = await CompatibilityBrokerClient.create(daemon, executableVersion);
1449
+ const client = await CompatibilityBrokerClient.create(daemon, executableVersion, clientKey);
3485
1450
  await client.releaseWorkspace();
3486
1451
  } catch (error) {
3487
1452
  if (!(error instanceof BrowserPilotError) || error.code !== "browser_not_found") throw error;
3488
1453
  }
1454
+ const afterRelease = await daemon.healthInfo();
1455
+ if ((afterRelease.clients?.activeLeases ?? 0) > 0) {
1456
+ return;
1457
+ }
1458
+ if (!afterRelease.ok || !afterRelease.brokerProcessIdentity || !afterRelease.executableVersion || !afterRelease.executableIdentity) return;
1459
+ const requester = createExecutableMetadataSync(
1460
+ executableVersion,
1461
+ publicExecutablePath(import.meta.url)
1462
+ );
1463
+ if (requester.version !== afterRelease.executableVersion || requester.identity !== afterRelease.executableIdentity) return;
3489
1464
  await daemon.shutdown({
3490
- brokerProcessIdentity: health.brokerProcessIdentity,
1465
+ brokerProcessIdentity: afterRelease.brokerProcessIdentity,
3491
1466
  executableVersion: requester.version,
3492
1467
  executableIdentity: requester.identity
3493
1468
  });
@@ -3495,7 +1470,8 @@ async function shutdownCompatibility(executableVersion) {
3495
1470
 
3496
1471
  // src/cli.ts
3497
1472
  var program = new Command();
3498
- program.name("bp").description("Control your browser from the command line").version(BROWSER_PILOT_VERSION).option("--human", "force human-readable output (default when TTY)").addHelpText("after", `
1473
+ program.exitOverride();
1474
+ program.name("bp").description("Control your browser from the command line").version(BROWSER_PILOT_VERSION).option("--human", "force human-readable output (default when TTY)").option("--client-key <key>", "stable namespace for Agent browser state").option("--request-id <id>", "stable host request ID for safe retry recovery").option("--timeout <ms>", "browser command deadline in milliseconds", "60000").addHelpText("after", `
3499
1475
  Workflow:
3500
1476
  bp connect # one-time setup (click Allow in Chrome)
3501
1477
  bp profiles # list live Chrome Profile contexts
@@ -3525,7 +1501,7 @@ Output:
3525
1501
  JSON by default when piped (for LLM/script use).
3526
1502
  Human-readable when run in a terminal (TTY). Force with --human.
3527
1503
  Actions return: {"ok":true, "title":"...", "url":"...", "elements":[...]}
3528
- Errors return: {"ok":false, "error":"...", "hint":"..."}
1504
+ Errors return: {"ok":false, "error":"...", "code":"...", "retryable":false}
3529
1505
 
3530
1506
  Canvas editors (Google Docs, Sheets, Figma):
3531
1507
  bp keyboard "text" --click ".editor" # click to focus, then type
@@ -3550,24 +1526,43 @@ Eval (escape hatch for operations without a dedicated command):
3550
1526
  bp eval "JSON.stringify(localStorage)" # read storage
3551
1527
  echo 'complex js here' | bp eval # stdin for complex JS
3552
1528
  `);
1529
+ var cliAbortController = new AbortController();
1530
+ var receivedSignal;
1531
+ for (const signal of ["SIGINT", "SIGTERM"]) {
1532
+ process.once(signal, () => {
1533
+ receivedSignal = signal;
1534
+ cliAbortController.abort();
1535
+ const fallback = setTimeout(() => process.exit(signal === "SIGINT" ? 130 : 143), 2e3);
1536
+ fallback.unref();
1537
+ });
1538
+ }
3553
1539
  function useJson() {
3554
- if (program.opts().human) return false;
1540
+ if (process.argv.slice(2).includes("--human") || program.opts().human) return false;
3555
1541
  return !process.stdout.isTTY;
3556
1542
  }
1543
+ program.configureOutput({
1544
+ writeErr: (value) => {
1545
+ if (!useJson()) process.stderr.write(value);
1546
+ }
1547
+ });
3557
1548
  function emit(data, human) {
3558
1549
  if (useJson()) console.log(JSON.stringify(data));
3559
1550
  else if (human) console.log(human);
3560
1551
  }
3561
1552
  function fail(error, hint, details) {
1553
+ const stable = details ?? new BrowserPilotError("internal_error", error);
3562
1554
  if (useJson()) console.log(JSON.stringify({
3563
1555
  ok: false,
3564
1556
  error,
1557
+ code: stable.code,
1558
+ retryable: stable.retryable,
3565
1559
  ...hint ? { hint } : {},
3566
- ...details ? details.toData() : {}
1560
+ ...stable.context ? { context: stable.context } : {},
1561
+ ...stable.remediation ? { remediation: stable.remediation } : {}
3567
1562
  }));
3568
1563
  else console.error(`\u2717 ${error}${hint ? `
3569
1564
  hint: ${hint}` : ""}`);
3570
- process.exit(1);
1565
+ process.exit(receivedSignal === "SIGINT" ? 130 : receivedSignal === "SIGTERM" ? 143 : 1);
3571
1566
  }
3572
1567
  function observationElements(result) {
3573
1568
  return Array.isArray(result.elements) ? result.elements : [];
@@ -3617,13 +1612,17 @@ function emitObservation(result) {
3617
1612
  function action(fn) {
3618
1613
  return (...args) => fn(...args).catch((err) => {
3619
1614
  const msg = err instanceof Error ? err.message : String(err);
3620
- if (err instanceof BrowserPilotError && err.code === "stale_ref") {
3621
- fail(msg, "Run 'bp snapshot' to refresh element refs.", err);
1615
+ const stable = err instanceof BrowserPilotError ? err : new BrowserPilotError("internal_error", msg, { cause: err });
1616
+ if (stable.code === "stale_ref") {
1617
+ fail(msg, "Run 'bp snapshot' to refresh element refs.", stable);
1618
+ }
1619
+ if (stable.code === "browser_disconnected") {
1620
+ fail(msg, "Run 'bp connect' first.", stable);
3622
1621
  }
3623
- if (msg.includes("not found") && msg.includes("Ref")) fail(msg, "Run 'bp snapshot' to refresh element refs.", err instanceof BrowserPilotError ? err : void 0);
3624
- if (msg.includes("Not connected")) fail(msg, "Run 'bp connect' first.", err instanceof BrowserPilotError ? err : void 0);
3625
- if (msg.includes("Page load timeout")) fail(msg, "Page may still be loading. Retry the command after a moment.", err instanceof BrowserPilotError ? err : void 0);
3626
- fail(msg, void 0, err instanceof BrowserPilotError ? err : void 0);
1622
+ if (stable.code === "unknown_outcome") {
1623
+ fail(msg, "Inspect the current tab state before deciding whether to retry.", stable);
1624
+ }
1625
+ fail(msg, void 0, stable);
3627
1626
  });
3628
1627
  }
3629
1628
  function normalizeUrl(url) {
@@ -3631,18 +1630,127 @@ function normalizeUrl(url) {
3631
1630
  return `https://${url}`;
3632
1631
  }
3633
1632
  function parseLimit(raw) {
3634
- const n = parseInt(raw, 10);
3635
- if (isNaN(n) || n < 1) throw new Error("--limit must be a positive number");
1633
+ const n = Number(raw);
1634
+ if (!/^\d+$/.test(raw) || !Number.isSafeInteger(n) || n < 1) {
1635
+ throw invalidArgument("--limit must be a positive integer", "limit");
1636
+ }
3636
1637
  return n;
3637
1638
  }
3638
1639
  function parseRef(raw) {
3639
1640
  const ref = Number(raw);
3640
- if (!Number.isSafeInteger(ref) || ref < 1) {
3641
- throw new Error("Ref must be a positive integer");
1641
+ if (!/^\d+$/.test(raw) || !Number.isSafeInteger(ref) || ref < 1) {
1642
+ throw invalidArgument("Ref must be a positive integer", "ref");
3642
1643
  }
3643
1644
  return ref;
3644
1645
  }
3645
- function requireString2(value, label) {
1646
+ function parseCoordinates(raw) {
1647
+ const parts = raw.split(",");
1648
+ if (parts.length !== 2 || parts.some((part) => part.trim() === "")) {
1649
+ throw invalidArgument("--xy must be x,y (e.g. --xy 400,300)", "xy");
1650
+ }
1651
+ const [x, y] = parts.map((part) => Number(part.trim()));
1652
+ if (!Number.isFinite(x) || !Number.isFinite(y)) {
1653
+ throw invalidArgument("--xy must be x,y (e.g. --xy 400,300)", "xy");
1654
+ }
1655
+ return { x, y };
1656
+ }
1657
+ function cliClientKey() {
1658
+ const value = program.opts().clientKey ?? process.env.BROWSER_PILOT_CLIENT_KEY;
1659
+ if (value === void 0) return "browser-pilot-cli";
1660
+ if (typeof value !== "string" || !/^[A-Za-z0-9][A-Za-z0-9._:-]{2,127}$/.test(value)) {
1661
+ throw new BrowserPilotError(
1662
+ "invalid_argument",
1663
+ "Client key must be 3-128 characters using letters, digits, dot, underscore, colon, or hyphen",
1664
+ { context: { field: "clientKey" } }
1665
+ );
1666
+ }
1667
+ return value;
1668
+ }
1669
+ function cliInvocationOptions() {
1670
+ const requestId = program.opts().requestId ?? process.env.BROWSER_PILOT_REQUEST_ID;
1671
+ if (requestId !== void 0 && (typeof requestId !== "string" || !/^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$/.test(requestId))) {
1672
+ throw invalidArgument(
1673
+ "Request ID must be 1-128 characters using letters, digits, dot, underscore, colon, or hyphen",
1674
+ "requestId"
1675
+ );
1676
+ }
1677
+ const rawTimeout = String(program.opts().timeout ?? "60000");
1678
+ const deadlineMs = Number(rawTimeout);
1679
+ if (!/^\d+$/.test(rawTimeout) || !Number.isSafeInteger(deadlineMs) || deadlineMs < 1 || deadlineMs > 3e5) {
1680
+ throw invalidArgument("--timeout must be an integer from 1 through 300000", "timeout");
1681
+ }
1682
+ return {
1683
+ ...requestId ? { requestId } : {},
1684
+ deadlineMs,
1685
+ signal: cliAbortController.signal
1686
+ };
1687
+ }
1688
+ function cliCommand(command) {
1689
+ return {
1690
+ id: command.id,
1691
+ method: command.method,
1692
+ mutating: command.mutating,
1693
+ status: command.status,
1694
+ acceptedAt: command.acceptedAt,
1695
+ deadlineAt: command.deadlineAt,
1696
+ ...command.targetId ? { targetId: command.targetId } : {},
1697
+ ...command.dispatchedAt !== void 0 ? { dispatchedAt: command.dispatchedAt } : {},
1698
+ ...command.completedAt !== void 0 ? { completedAt: command.completedAt } : {},
1699
+ ...command.cancellationRequested ? { cancellationRequested: true } : {}
1700
+ };
1701
+ }
1702
+ function cliCommandOutcome(outcome) {
1703
+ return {
1704
+ command: cliCommand(outcome.command),
1705
+ ...outcome.result !== void 0 ? { result: outcome.result } : {},
1706
+ ...outcome.error !== void 0 ? { error: outcome.error } : {}
1707
+ };
1708
+ }
1709
+ function outputPath(filename) {
1710
+ if (isAbsolute3(filename)) return resolvePath(filename);
1711
+ const configured = process.env.BROWSER_PILOT_OUTPUT_DIR;
1712
+ if (configured !== void 0) {
1713
+ if (!isAbsolute3(configured)) {
1714
+ throw invalidArgument("BROWSER_PILOT_OUTPUT_DIR must be an absolute path", "outputDir");
1715
+ }
1716
+ mkdirSync2(configured, { recursive: true, mode: 448 });
1717
+ return resolvePath(configured, filename);
1718
+ }
1719
+ return resolvePath(filename);
1720
+ }
1721
+ function artifactFileResult(artifact, file) {
1722
+ return {
1723
+ file,
1724
+ mimeType: artifact.mimeType,
1725
+ sizeBytes: artifact.byteSize,
1726
+ ...artifact.width !== void 0 ? { width: artifact.width } : {},
1727
+ ...artifact.height !== void 0 ? { height: artifact.height } : {}
1728
+ };
1729
+ }
1730
+ function delay2(ms) {
1731
+ if (cliAbortController.signal.aborted) {
1732
+ return Promise.reject(new BrowserPilotError("command_cancelled", "Wait was cancelled"));
1733
+ }
1734
+ return new Promise((resolve, reject) => {
1735
+ const timer = setTimeout(done, ms);
1736
+ function done() {
1737
+ cliAbortController.signal.removeEventListener("abort", cancelled);
1738
+ resolve();
1739
+ }
1740
+ function cancelled() {
1741
+ clearTimeout(timer);
1742
+ cliAbortController.signal.removeEventListener("abort", cancelled);
1743
+ reject(new BrowserPilotError("command_cancelled", "Wait was cancelled"));
1744
+ }
1745
+ cliAbortController.signal.addEventListener("abort", cancelled, { once: true });
1746
+ });
1747
+ }
1748
+ function matchesUrl(url, pattern) {
1749
+ if (!pattern.includes("*")) return url.includes(pattern);
1750
+ const source = pattern.split("*").map((part) => part.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")).join(".*");
1751
+ return new RegExp(`^${source}$`).test(url);
1752
+ }
1753
+ function requireString(value, label) {
3646
1754
  if (typeof value !== "string") throw new BrowserPilotError("internal_error", `${label} is missing`);
3647
1755
  return value;
3648
1756
  }
@@ -3659,12 +1767,21 @@ async function cliElementAddress(client, target, raw) {
3659
1767
  ref: parseRef(raw)
3660
1768
  };
3661
1769
  }
3662
- if (!raw.trim()) throw new Error("Element target must not be empty");
1770
+ if (!raw.trim()) throw invalidArgument("Element target must not be empty", "target");
3663
1771
  return { selector: raw };
3664
1772
  }
3665
1773
  async function requireCompatibility() {
3666
- const client = await resumeCompatibility(BROWSER_PILOT_VERSION);
3667
- if (!client) throw new Error("Not connected");
1774
+ const client = await resumeCompatibility(BROWSER_PILOT_VERSION, cliClientKey(), cliInvocationOptions());
1775
+ if (!client) {
1776
+ throw new BrowserPilotError("browser_disconnected", "Browser Pilot is not connected", {
1777
+ retryable: true,
1778
+ remediation: {
1779
+ code: "connect_browser",
1780
+ message: "Run 'bp connect', complete Chrome authorization if prompted, then retry.",
1781
+ actionRequired: true
1782
+ }
1783
+ });
1784
+ }
3668
1785
  return client;
3669
1786
  }
3670
1787
  async function resolveCliProfile(client, selector) {
@@ -3673,11 +1790,11 @@ async function resolveCliProfile(client, selector) {
3673
1790
  if (exactId) return exactId;
3674
1791
  if (/^\d+$/.test(selector)) {
3675
1792
  const index = Number(selector);
3676
- if (Number.isSafeInteger(index) && profiles[index]) return profiles[index];
3677
- throw invalidCliProfile(`Profile index out of range (0-${Math.max(0, profiles.length - 1)})`, selector);
1793
+ if (Number.isSafeInteger(index) && index >= 1 && profiles[index - 1]) return profiles[index - 1];
1794
+ throw invalidCliProfile(`Profile index out of range (1-${profiles.length})`, selector);
3678
1795
  }
3679
1796
  const normalized = selector.trim().toLocaleLowerCase();
3680
- const matches = profiles.filter((profile) => profile.label.toLocaleLowerCase() === normalized || profile.displayName?.toLocaleLowerCase() === normalized);
1797
+ const matches = profiles.filter((profile) => profile.label.toLocaleLowerCase() === normalized || profile.displayName?.toLocaleLowerCase() === normalized || profile.profileName?.toLocaleLowerCase() === normalized || profile.accountName?.toLocaleLowerCase() === normalized || profile.accountEmail?.toLocaleLowerCase() === normalized);
3681
1798
  if (matches.length === 1) return matches[0];
3682
1799
  if (matches.length > 1) {
3683
1800
  throw invalidCliProfile("Profile selector is ambiguous; use its index or Profile context ID", selector);
@@ -3691,10 +1808,16 @@ function invalidCliProfile(message, selector) {
3691
1808
  }
3692
1809
  function cliProfile(profile, index) {
3693
1810
  return {
3694
- index,
1811
+ index: index + 1,
3695
1812
  profileContextId: profile.profileContextId,
3696
1813
  label: profile.label,
3697
1814
  ...profile.displayName ? { displayName: profile.displayName } : {},
1815
+ ...profile.identityStatus ? { identityStatus: profile.identityStatus } : {},
1816
+ ...profile.profileName ? { profileName: profile.profileName } : {},
1817
+ ...profile.accountName ? { accountName: profile.accountName } : {},
1818
+ ...profile.accountEmail ? { accountEmail: profile.accountEmail } : {},
1819
+ ...profile.profileDirectory ? { profileDirectory: profile.profileDirectory } : {},
1820
+ ...profile.identityErrorCode ? { identityErrorCode: profile.identityErrorCode } : {},
3698
1821
  tabCount: profile.tabCount,
3699
1822
  eligibleTabCount: profile.eligibleTabCount,
3700
1823
  selected: profile.selected,
@@ -3702,7 +1825,7 @@ function cliProfile(profile, index) {
3702
1825
  };
3703
1826
  }
3704
1827
  function withCliTarget(operation) {
3705
- return withCompatibilityTarget(BROWSER_PILOT_VERSION, operation);
1828
+ return withCompatibilityTarget(BROWSER_PILOT_VERSION, operation, cliClientKey(), cliInvocationOptions());
3706
1829
  }
3707
1830
  function readStdin() {
3708
1831
  if (process.stdin.isTTY) return Promise.resolve("");
@@ -3714,9 +1837,212 @@ function readStdin() {
3714
1837
  process.stdin.on("end", () => resolve(d.trim()));
3715
1838
  });
3716
1839
  }
1840
+ program.command("status").description("Show current Broker, browser, Agent namespace, and recovery state").action(action(async () => {
1841
+ const brokerRunning = isDaemonRunning();
1842
+ const client = await resumeCompatibility(BROWSER_PILOT_VERSION, cliClientKey(), cliInvocationOptions());
1843
+ if (!client) {
1844
+ const browsers = (await discoverBrowserCandidates()).map((discovered) => discovered.candidate);
1845
+ emit({
1846
+ ok: true,
1847
+ service: { state: brokerRunning ? "unavailable" : "stopped", version: BROWSER_PILOT_VERSION },
1848
+ browser: { state: "disconnected" },
1849
+ browsers,
1850
+ session: { state: "unavailable" },
1851
+ recovery: {
1852
+ required: true,
1853
+ code: "browser_disconnected",
1854
+ action: "bp connect"
1855
+ }
1856
+ }, brokerRunning ? "Browser Pilot Broker is running, but the browser session is unavailable." : "Browser Pilot is not connected. Run bp connect.");
1857
+ return;
1858
+ }
1859
+ const [activeCommands, uncertainCommands] = await Promise.all([
1860
+ client.listCommands(20, ["accepted", "dispatched"]),
1861
+ client.listCommands(20, ["unknown_outcome"])
1862
+ ]);
1863
+ let tabs = [];
1864
+ let profiles = [];
1865
+ let browserStateError;
1866
+ try {
1867
+ [tabs, profiles] = await Promise.all([client.listTabs("all"), client.listProfiles()]);
1868
+ } catch (error) {
1869
+ browserStateError = error instanceof BrowserPilotError ? error : new BrowserPilotError("internal_error", error instanceof Error ? error.message : String(error));
1870
+ }
1871
+ const selectedTarget = tabs.find((tab) => tab.selected ?? tab.active === true);
1872
+ const selectedProfile = profiles.find((profile) => profile.selected);
1873
+ const browser = client.initialized.browsers.find((candidate) => candidate.state === "ready") ?? client.initialized.browsers[0];
1874
+ const recovery = browserStateError ? {
1875
+ required: true,
1876
+ code: browserStateError.code,
1877
+ action: browserStateError.remediation?.message ?? "Inspect the browser connection before continuing."
1878
+ } : uncertainCommands.length > 0 ? {
1879
+ required: true,
1880
+ code: "unknown_outcome",
1881
+ action: "Inspect the current tab and the uncertain command before retrying."
1882
+ } : { required: false };
1883
+ emit({
1884
+ ok: true,
1885
+ service: {
1886
+ state: "running",
1887
+ version: client.initialized.serviceVersion
1888
+ },
1889
+ browser: browser ? { id: browser.id, product: browser.product, state: browser.state } : { state: "disconnected" },
1890
+ session: {
1891
+ state: client.lease.state,
1892
+ expiresAt: client.lease.expiresAt,
1893
+ profile: selectedProfile ? cliProfile(selectedProfile, profiles.indexOf(selectedProfile)) : null,
1894
+ target: selectedTarget ? {
1895
+ index: tabs.indexOf(selectedTarget) + 1,
1896
+ title: selectedTarget.title,
1897
+ url: selectedTarget.url,
1898
+ origin: selectedTarget.origin,
1899
+ profileContextId: selectedTarget.profileContextId
1900
+ } : null
1901
+ },
1902
+ commands: {
1903
+ active: activeCommands.map(cliCommand),
1904
+ uncertain: uncertainCommands.map(cliCommand)
1905
+ },
1906
+ recovery
1907
+ }, `Browser Pilot: ${browser?.state ?? "disconnected"}; ${tabs.length} tab(s); ${activeCommands.length} active command(s)`);
1908
+ }));
1909
+ program.command("commands").description("List recent commands for this Agent namespace").option("-l, --limit <n>", "maximum commands to return", "20").option("--status <statuses>", "comma-separated Command statuses").action(action(async (opts) => {
1910
+ const limit = parseLimit(opts.limit);
1911
+ if (limit > 100) throw invalidArgument("--limit must not exceed 100", "limit");
1912
+ const statuses = opts.status === void 0 ? void 0 : String(opts.status).split(",").map((status) => status.trim()).filter(Boolean);
1913
+ const validStatuses = /* @__PURE__ */ new Set([
1914
+ "accepted",
1915
+ "dispatched",
1916
+ "completed",
1917
+ "unknown_outcome",
1918
+ "cancelled",
1919
+ "expired"
1920
+ ]);
1921
+ if (statuses && (statuses.length === 0 || statuses.some((status) => !validStatuses.has(status)))) {
1922
+ throw invalidArgument("--status contains an invalid Command status", "status");
1923
+ }
1924
+ const commands = await (await requireCompatibility()).listCommands(limit, statuses);
1925
+ if (useJson()) {
1926
+ console.log(JSON.stringify({ ok: true, commands: commands.map(cliCommand) }));
1927
+ } else if (commands.length === 0) {
1928
+ console.log("No recent commands.");
1929
+ } else {
1930
+ for (const command of commands) {
1931
+ console.log(`${command.id} ${command.status.padEnd(15)} ${command.method}`);
1932
+ }
1933
+ }
1934
+ }));
1935
+ program.command("command <commandId>").description("Inspect one recent command and its outcome").action(action(async (commandId) => {
1936
+ const outcome = await (await requireCompatibility()).getCommand(commandId);
1937
+ const result = cliCommandOutcome(outcome);
1938
+ if (useJson()) console.log(JSON.stringify({ ok: true, ...result }));
1939
+ else console.log(`${outcome.command.id}: ${outcome.command.status} (${outcome.command.method})`);
1940
+ }));
1941
+ program.command("cancel <commandId>").description("Request cancellation of a running command").action(action(async (commandId) => {
1942
+ const outcome = await (await requireCompatibility()).cancelCommand(commandId);
1943
+ const result = cliCommandOutcome(outcome);
1944
+ if (useJson()) console.log(JSON.stringify({ ok: true, ...result }));
1945
+ else console.log(`${outcome.command.id}: ${outcome.command.status}`);
1946
+ }));
1947
+ program.command("wait").description("Wait for a browser-visible condition").option("--url <pattern>", "wait for the selected tab URL to contain a value or match a * glob").option("--text <text>", "wait for visible page text").option("--selector <selector>", "wait for a CSS selector").option("--dialog", "wait for a pending JavaScript dialog").option("--download", "wait for a completed unexported download").option("--popup", "wait for a managed popup tab").option("--interval <ms>", "poll interval in milliseconds", "250").action(action(async (opts) => {
1948
+ const conditions = [
1949
+ opts.url !== void 0,
1950
+ opts.text !== void 0,
1951
+ opts.selector !== void 0,
1952
+ opts.dialog === true,
1953
+ opts.download === true,
1954
+ opts.popup === true
1955
+ ].filter(Boolean).length;
1956
+ if (conditions !== 1) {
1957
+ throw invalidArgument(
1958
+ "Choose exactly one of --url, --text, --selector, --dialog, --download, or --popup",
1959
+ "condition"
1960
+ );
1961
+ }
1962
+ const intervalMs = Number(opts.interval);
1963
+ if (!/^\d+$/.test(String(opts.interval)) || !Number.isSafeInteger(intervalMs) || intervalMs < 100 || intervalMs > 5e3) {
1964
+ throw invalidArgument("--interval must be an integer from 100 through 5000", "interval");
1965
+ }
1966
+ const waitTimeoutMs = cliInvocationOptions().deadlineMs ?? 6e4;
1967
+ const startedAt = Date.now();
1968
+ const deadlineAt = startedAt + waitTimeoutMs;
1969
+ const client = await requireCompatibility();
1970
+ const needsTarget = opts.url !== void 0 || opts.text !== void 0 || opts.selector !== void 0;
1971
+ const target = needsTarget ? await client.ensureTarget() : void 0;
1972
+ const condition = opts.url !== void 0 ? "url" : opts.text !== void 0 ? "text" : opts.selector !== void 0 ? "selector" : opts.dialog ? "dialog" : opts.download ? "download" : "popup";
1973
+ while (Date.now() <= deadlineAt) {
1974
+ let matched;
1975
+ if (opts.url !== void 0) {
1976
+ const tabs = await client.listTabs("all");
1977
+ const current = tabs.find((tab) => tab.targetId === target.targetId);
1978
+ if (current && matchesUrl(current.url, String(opts.url))) {
1979
+ matched = {
1980
+ title: current.title,
1981
+ url: current.url,
1982
+ origin: current.origin,
1983
+ profileContextId: current.profileContextId
1984
+ };
1985
+ }
1986
+ } else if (opts.text !== void 0) {
1987
+ const result = await client.callTool("browser.search", {
1988
+ query: String(opts.text),
1989
+ limit: 1
1990
+ }, target.targetId);
1991
+ const matches = Array.isArray(result.matches) ? result.matches : [];
1992
+ if (matches.length > 0) matched = { title: result.title, url: result.url, match: matches[0] };
1993
+ } else if (opts.selector !== void 0) {
1994
+ const result = await client.callTool("browser.elements.find", {
1995
+ selector: String(opts.selector),
1996
+ limit: 1
1997
+ }, target.targetId);
1998
+ const elements = Array.isArray(result.elements) ? result.elements : [];
1999
+ if (elements.length > 0) matched = { url: result.url, element: elements[0] };
2000
+ } else if (opts.dialog) {
2001
+ const result = await client.callTool("browser.dialogs.list");
2002
+ const dialogs = Array.isArray(result.dialogs) ? result.dialogs : [];
2003
+ if (dialogs.length > 0) matched = dialogs[0];
2004
+ } else if (opts.download) {
2005
+ const downloads = await client.listArtifacts(["download"]);
2006
+ if (downloads.length > 0) matched = {
2007
+ index: 1,
2008
+ id: downloads[0].id,
2009
+ fileName: downloads[0].fileName ?? null,
2010
+ mimeType: downloads[0].mimeType,
2011
+ sizeBytes: downloads[0].byteSize,
2012
+ createdAt: downloads[0].createdAt,
2013
+ expiresAt: downloads[0].expiresAt
2014
+ };
2015
+ } else {
2016
+ const tabs = await client.listTabs("all");
2017
+ const popup = tabs.find((tab) => tab.origin === "managed_popup");
2018
+ if (popup) {
2019
+ matched = {
2020
+ title: popup.title,
2021
+ url: popup.url,
2022
+ origin: popup.origin,
2023
+ profileContextId: popup.profileContextId
2024
+ };
2025
+ }
2026
+ }
2027
+ if (matched !== void 0) {
2028
+ emit({
2029
+ ok: true,
2030
+ condition,
2031
+ elapsedMs: Date.now() - startedAt,
2032
+ matched
2033
+ }, `Condition satisfied: ${condition}`);
2034
+ return;
2035
+ }
2036
+ await delay2(Math.min(intervalMs, Math.max(0, deadlineAt - Date.now())));
2037
+ }
2038
+ throw new BrowserPilotError("wait_timeout", `Timed out waiting for ${condition}`, {
2039
+ retryable: true,
2040
+ context: { condition, timeoutMs: waitTimeoutMs }
2041
+ });
2042
+ }));
3717
2043
  program.command("browsers").description("List supported local browsers and their setup state").option("-b, --browser <name>", "filter by browser ID, product, or channel").action(action(async (opts) => {
3718
2044
  const filter = typeof opts.browser === "string" ? opts.browser.toLowerCase() : void 0;
3719
- const client = await resumeCompatibility(BROWSER_PILOT_VERSION);
2045
+ const client = await resumeCompatibility(BROWSER_PILOT_VERSION, cliClientKey(), cliInvocationOptions());
3720
2046
  const browsers = client ? await client.listBrowsers(filter) : (await discoverBrowserCandidates()).map((discovered) => discovered.candidate).filter((candidate) => !filter || [candidate.id, candidate.product, candidate.channel].some((value) => value?.toLowerCase().includes(filter)));
3721
2047
  if (useJson()) {
3722
2048
  console.log(JSON.stringify({ ok: true, browsers }));
@@ -3743,7 +2069,7 @@ program.command("connect").description("Connect to Chrome and prepare unambiguou
3743
2069
  console.log(`If prompted, click "Allow" in Chrome's authorization dialog.
3744
2070
  `);
3745
2071
  }
3746
- const client = await connectCompatibility(BROWSER_PILOT_VERSION, opts.browser);
2072
+ const client = await connectCompatibility(BROWSER_PILOT_VERSION, opts.browser, cliClientKey(), cliInvocationOptions());
3747
2073
  await client.connectBrowser();
3748
2074
  const profiles = await client.listProfiles();
3749
2075
  if (profiles.length <= 1) await client.ensureManagedTarget();
@@ -3753,7 +2079,7 @@ program.command("connect").description("Connect to Chrome and prepare unambiguou
3753
2079
  emit(
3754
2080
  { ok: true, browser, profileSelectionRequired: true, profiles: listed },
3755
2081
  `\u2713 Connected to ${browser}
3756
- Multiple Chrome Profiles are open. Run 'bp profiles', then 'bp profile <index>'.`
2082
+ Multiple Chrome Profiles are open. Run 'bp profiles --identify', then 'bp profile <index>'.`
3757
2083
  );
3758
2084
  return;
3759
2085
  }
@@ -3765,31 +2091,14 @@ Multiple Chrome Profiles are open. Run 'bp profiles', then 'bp profile <index>'.
3765
2091
  Ready! Try: bp open https://example.com`
3766
2092
  );
3767
2093
  }));
3768
- program.command("bridge").description("Run the Agent-neutral JSON-RPC bridge").option("--stdio", "use newline-delimited JSON-RPC over stdin/stdout").option("-b, --browser <name>", "prefer a browser ID, product, or channel when starting the Broker").action((opts) => {
3769
- if (!opts.stdio) {
3770
- process.stderr.write("bridge currently requires --stdio\n");
3771
- process.exitCode = 2;
3772
- return;
3773
- }
3774
- void runStdioBridge({
3775
- input: process.stdin,
3776
- output: process.stdout,
3777
- backend: new DaemonBridgeBackend(opts.browser)
3778
- }).then((result) => {
3779
- process.exitCode = result.exitCode;
3780
- }).catch((error) => {
3781
- const message = error instanceof Error ? error.message : String(error);
3782
- process.stderr.write(`Bridge error: ${message}
3783
- `);
3784
- process.exitCode = 1;
3785
- });
3786
- });
3787
2094
  program.command("disconnect").description("Release CLI browser state and stop an otherwise unused daemon").action(action(async () => {
3788
- await shutdownCompatibility(BROWSER_PILOT_VERSION);
2095
+ await shutdownCompatibility(BROWSER_PILOT_VERSION, cliClientKey());
3789
2096
  emit({ ok: true }, "\u2713 Disconnected");
3790
2097
  }));
3791
- program.command("profiles").description("List live Chrome Profile contexts").action(action(async () => {
3792
- const profiles = (await (await requireCompatibility()).listProfiles()).map(cliProfile);
2098
+ program.command("profiles").description("List live Chrome Profile contexts").option("--identify", "explicitly identify Profiles using temporary visible Chrome pages").option("--refresh", "repeat Profile identification instead of using cached results").action(action(async (opts) => {
2099
+ const client = await requireCompatibility();
2100
+ const identified = opts.identify || opts.refresh ? await client.identifyProfiles(void 0, opts.refresh === true) : await client.listProfiles();
2101
+ const profiles = identified.map(cliProfile);
3793
2102
  if (useJson()) {
3794
2103
  console.log(JSON.stringify({ ok: true, profiles }));
3795
2104
  return;
@@ -3799,8 +2108,10 @@ program.command("profiles").description("List live Chrome Profile contexts").act
3799
2108
  return;
3800
2109
  }
3801
2110
  for (const profile of profiles) {
3802
- const name = profile.displayName ? ` (${profile.displayName})` : "";
3803
- console.log(`${profile.selected ? "*" : " "} ${profile.index} ${profile.label}${name} ${profile.tabCount} tab(s)`);
2111
+ const name = profile.profileName ?? profile.displayName;
2112
+ const account = profile.accountEmail ?? profile.accountName;
2113
+ const identity = name ? ` ${name}${account ? ` (${account})` : ""} [${profile.label}]` : ` ${profile.label}${profile.identityStatus === "unavailable" ? " (identity unavailable)" : ""}`;
2114
+ console.log(`${profile.selected ? "*" : " "} ${profile.index}${identity} ${profile.tabCount} tab(s)`);
3804
2115
  }
3805
2116
  }));
3806
2117
  program.command("profile <selector>").description("Select a Chrome Profile context for new managed tabs").action(action(async (selector) => {
@@ -3812,15 +2123,21 @@ program.command("profile <selector>").description("Select a Chrome Profile conte
3812
2123
  ok: true,
3813
2124
  profileContextId: selected.profileContextId,
3814
2125
  label: selected.label,
3815
- ...selected.displayName ? { displayName: selected.displayName } : {}
2126
+ ...selected.displayName ? { displayName: selected.displayName } : {},
2127
+ ...selected.identityStatus ? { identityStatus: selected.identityStatus } : {},
2128
+ ...selected.profileName ? { profileName: selected.profileName } : {},
2129
+ ...selected.accountName ? { accountName: selected.accountName } : {},
2130
+ ...selected.accountEmail ? { accountEmail: selected.accountEmail } : {},
2131
+ ...selected.profileDirectory ? { profileDirectory: selected.profileDirectory } : {},
2132
+ ...selected.identityErrorCode ? { identityErrorCode: selected.identityErrorCode } : {}
3816
2133
  },
3817
- `\u2713 Selected ${selected.displayName ?? selected.label}`
2134
+ `\u2713 Selected ${selected.profileName ?? selected.displayName ?? selected.label}`
3818
2135
  );
3819
2136
  }));
3820
- program.command("open <url>").description("Navigate to URL and return page snapshot").option("-n, --new", "open in new tab").option("--profile <selector>", "Profile index, ID, label, or verified display name (requires --new)").option("-l, --limit <n>", "max elements in snapshot", "50").addHelpText("after", "\nExamples:\n bp open https://github.com\n bp open github.com --new\n bp open https://example.com --new --profile 1\n bp open https://example.com --limit 20").action(action(async (url, opts) => {
2137
+ program.command("open <url>").description("Navigate to URL and return page snapshot").option("-n, --new", "open in new tab").option("--profile <selector>", "Profile index, ID, label, verified name, or email (requires --new)").option("-l, --limit <n>", "max elements in snapshot", "50").addHelpText("after", "\nExamples:\n bp open https://github.com\n bp open github.com --new\n bp open https://example.com --new --profile 1\n bp open https://example.com --limit 20").action(action(async (url, opts) => {
3821
2138
  url = normalizeUrl(url);
3822
2139
  const limit = parseLimit(opts.limit);
3823
- if (opts.profile && !opts.new) throw new Error("--profile requires --new");
2140
+ if (opts.profile && !opts.new) throw invalidArgument("--profile requires --new", "profile");
3824
2141
  if (opts.new) {
3825
2142
  const client = await requireCompatibility();
3826
2143
  const profile = opts.profile ? await resolveCliProfile(client, String(opts.profile)) : void 0;
@@ -3854,20 +2171,22 @@ Examples:
3854
2171
  bp click --xy 400,300 --double # double-click at coordinates
3855
2172
  bp click --xy 400,300 --right # right-click (context menu)
3856
2173
  bp click 3 --right # right-click element [3]`).action(action(async (ref, opts) => {
3857
- if (opts.double && opts.right) throw new Error("--double and --right are mutually exclusive");
3858
- if (!ref && !opts.xy) throw new Error("Provide a ref number or --xy coordinates");
2174
+ if (opts.double && opts.right) {
2175
+ throw invalidArgument("--double and --right are mutually exclusive", "button");
2176
+ }
2177
+ if (!ref && !opts.xy) throw invalidArgument("Provide a ref number or --xy coordinates", "target");
2178
+ if (ref && opts.xy) throw invalidArgument("Provide either a ref number or --xy, not both", "target");
3859
2179
  const limit = parseLimit(opts.limit);
2180
+ const coordinates = opts.xy ? parseCoordinates(opts.xy) : void 0;
2181
+ const parsedRef = ref ? parseRef(ref) : void 0;
3860
2182
  await withCliTarget(async (client, controlledTarget) => {
3861
2183
  let target;
3862
- if (opts.xy) {
3863
- const [xStr, yStr] = opts.xy.split(",");
3864
- const x = parseFloat(xStr), y = parseFloat(yStr);
3865
- if (isNaN(x) || isNaN(y)) throw new Error("--xy must be x,y (e.g. --xy 400,300)");
3866
- target = { x, y };
2184
+ if (coordinates) {
2185
+ target = coordinates;
3867
2186
  } else {
3868
2187
  target = {
3869
2188
  observationId: await client.latestObservation(controlledTarget.targetId),
3870
- ref: parseRef(ref)
2189
+ ref: parsedRef
3871
2190
  };
3872
2191
  }
3873
2192
  const result = await client.callTool("browser.click", {
@@ -3906,10 +2225,11 @@ Examples:
3906
2225
  }));
3907
2226
  program.command("type <ref> <text>").description("Type text into element and return page snapshot").option("-c, --clear", "clear field before typing").option("-s, --submit", "press Enter after typing").option("-l, --limit <n>", "max elements in snapshot", "50").addHelpText("after", '\nExamples:\n bp type 2 "hello world"\n bp type 5 "query" --submit\n bp type 3 "new value" --clear').action(action(async (ref, text, opts) => {
3908
2227
  const limit = parseLimit(opts.limit);
2228
+ const parsedRef = parseRef(ref);
3909
2229
  await withCliTarget(async (client, target) => {
3910
2230
  const result = await client.callTool("browser.type", {
3911
2231
  observationId: await client.latestObservation(target.targetId),
3912
- ref: parseRef(ref),
2232
+ ref: parsedRef,
3913
2233
  text,
3914
2234
  ...opts.clear ? { clear: true } : {},
3915
2235
  ...opts.submit ? { submit: true } : {},
@@ -3932,17 +2252,19 @@ Examples:
3932
2252
  bp keyboard "Hello Docs!" --click ".kix-appview-editor"
3933
2253
  bp keyboard "slow typing" --delay 50`).action(action(async (text, opts) => {
3934
2254
  const limit = parseLimit(opts.limit);
3935
- let delay2 = 0;
2255
+ let delay3 = 0;
3936
2256
  if (opts.delay) {
3937
- delay2 = parseInt(opts.delay, 10);
3938
- if (isNaN(delay2) || delay2 < 0) throw new Error("--delay must be a non-negative number");
2257
+ delay3 = Number(opts.delay);
2258
+ if (!/^\d+$/.test(opts.delay) || !Number.isSafeInteger(delay3) || delay3 < 0) {
2259
+ throw invalidArgument("--delay must be a non-negative integer", "delay");
2260
+ }
3939
2261
  }
3940
2262
  await withCliTarget(async (client, target) => {
3941
2263
  const result = await client.callTool("browser.keyboard", {
3942
2264
  text,
3943
2265
  ...opts.clear ? { clear: true } : {},
3944
2266
  ...opts.submit ? { submit: true } : {},
3945
- delayMs: delay2,
2267
+ delayMs: delay3,
3946
2268
  ...opts.click ? { focusSelector: opts.click } : {},
3947
2269
  observationLimit: limit
3948
2270
  }, target.targetId);
@@ -3982,7 +2304,9 @@ Examples:
3982
2304
  echo 'complex js' | bp eval`).action(action(async (expression) => {
3983
2305
  if (!expression) {
3984
2306
  expression = await readStdin();
3985
- if (!expression) throw new Error("No expression. Pass as argument or pipe via stdin.");
2307
+ if (!expression) {
2308
+ throw invalidArgument("No expression. Pass as argument or pipe via stdin.", "expression");
2309
+ }
3986
2310
  }
3987
2311
  await withCliTarget(async (client, target) => {
3988
2312
  const result = await client.callTool("browser.eval", {
@@ -4014,8 +2338,10 @@ When to use which command:
4014
2338
  bp snapshot \u2192 list interactive elements (buttons, links, inputs)
4015
2339
  bp read \u2192 page text content (search results, articles)
4016
2340
  bp eval \u2192 custom extraction (structured data, attributes)`).action(action(async (selector, opts) => {
4017
- const limit = parseInt(opts.limit, 10);
4018
- if (isNaN(limit) || limit < 1) throw new Error("--limit must be a positive integer");
2341
+ const limit = Number(opts.limit);
2342
+ if (!/^\d+$/.test(opts.limit) || !Number.isSafeInteger(limit) || limit < 1) {
2343
+ throw invalidArgument("--limit must be a positive integer", "limit");
2344
+ }
4019
2345
  await withCliTarget(async (client, target) => {
4020
2346
  let data;
4021
2347
  try {
@@ -4096,15 +2422,25 @@ Examples:
4096
2422
  bp scroll down --selector ".results"
4097
2423
  bp scroll --ref 8 --to end
4098
2424
  bp scroll --to-text "Payment details"`).action(action(async (direction, opts) => {
4099
- if (opts.selector && opts.ref) throw new Error("--selector and --ref are mutually exclusive");
2425
+ if (opts.selector && opts.ref) {
2426
+ throw invalidArgument("--selector and --ref are mutually exclusive", "target");
2427
+ }
4100
2428
  const modes = [direction !== void 0, opts.to !== void 0, opts.toText !== void 0].filter(Boolean).length;
4101
- if (modes > 1) throw new Error("Use only one of direction, --to, or --to-text");
2429
+ if (modes > 1) throw invalidArgument("Use only one of direction, --to, or --to-text", "mode");
4102
2430
  const validDirections = /* @__PURE__ */ new Set(["up", "down", "left", "right"]);
4103
- if (direction !== void 0 && !validDirections.has(direction)) throw new Error("direction must be up, down, left, or right");
4104
- if (opts.to !== void 0 && !["start", "end"].includes(opts.to)) throw new Error("--to must be start or end");
4105
- if (!["pixels", "viewport"].includes(opts.unit)) throw new Error("--unit must be pixels or viewport");
2431
+ if (direction !== void 0 && !validDirections.has(direction)) {
2432
+ throw invalidArgument("direction must be up, down, left, or right", "direction");
2433
+ }
2434
+ if (opts.to !== void 0 && !["start", "end"].includes(opts.to)) {
2435
+ throw invalidArgument("--to must be start or end", "to");
2436
+ }
2437
+ if (!["pixels", "viewport"].includes(opts.unit)) {
2438
+ throw invalidArgument("--unit must be pixels or viewport", "unit");
2439
+ }
4106
2440
  const amount = opts.amount === void 0 ? void 0 : Number(opts.amount);
4107
- if (amount !== void 0 && (!Number.isFinite(amount) || amount <= 0)) throw new Error("--amount must be a positive number");
2441
+ if (amount !== void 0 && (!Number.isFinite(amount) || amount <= 0)) {
2442
+ throw invalidArgument("--amount must be a positive number", "amount");
2443
+ }
4108
2444
  const limit = parseLimit(opts.limit);
4109
2445
  await withCliTarget(async (client, target) => {
4110
2446
  const rawTarget = opts.selector ?? opts.ref;
@@ -4141,7 +2477,9 @@ program.command("dropdown <target>").description("List native or ARIA dropdown o
4141
2477
  });
4142
2478
  }));
4143
2479
  program.command("select <target> <option>").description("Select and verify a dropdown option").option("--by <mode>", "match by label, value, or index", "label").option("--contains", "use case-insensitive substring matching").option("-l, --limit <n>", "max elements in returned snapshot", "50").addHelpText("after", '\nExamples:\n bp select 4 "United States"\n bp select 4 us --by value\n bp select 4 3 --by index').action(action(async (rawTarget, rawOption, opts) => {
4144
- if (!["label", "value", "index"].includes(opts.by)) throw new Error("--by must be label, value, or index");
2480
+ if (!["label", "value", "index"].includes(opts.by)) {
2481
+ throw invalidArgument("--by must be label, value, or index", "by");
2482
+ }
4145
2483
  const choice = opts.by === "index" ? { by: "index", index: parseRef(rawOption) } : { by: opts.by, [opts.by]: rawOption, exact: opts.contains !== true };
4146
2484
  const limit = parseLimit(opts.limit);
4147
2485
  await withCliTarget(async (client, target) => {
@@ -4155,9 +2493,11 @@ program.command("select <target> <option>").description("Select and verify a dro
4155
2493
  }));
4156
2494
  program.command("upload <filepath>").description('Upload file (auto-finds <input type="file"> on the page)').option("--nth <n>", "which file input to use if multiple exist", "1").addHelpText("after", "\nAuto-detects file inputs on the page. No ref needed.\n\nExamples:\n bp upload ./photo.jpg\n bp upload /tmp/resume.pdf\n bp upload ./doc.pdf --nth 2 # if multiple file inputs").action(action(async (filepath, opts) => {
4157
2495
  const absPath = resolvePath(filepath);
4158
- if (!existsSync4(absPath)) throw new Error(`File not found: ${absPath}`);
4159
- const inputIndex = parseInt(opts.nth, 10);
4160
- if (isNaN(inputIndex) || inputIndex < 1) throw new Error("--nth must be a positive integer");
2496
+ if (!existsSync4(absPath)) throw invalidArgument(`File not found: ${absPath}`, "filepath");
2497
+ const inputIndex = Number(opts.nth);
2498
+ if (!/^\d+$/.test(opts.nth) || !Number.isSafeInteger(inputIndex) || inputIndex < 1) {
2499
+ throw invalidArgument("--nth must be a positive integer", "nth");
2500
+ }
4161
2501
  await withCliTarget(async (client, target) => {
4162
2502
  const artifact = await client.importArtifact(absPath);
4163
2503
  try {
@@ -4173,9 +2513,46 @@ program.command("upload <filepath>").description('Upload file (auto-finds <input
4173
2513
  }
4174
2514
  });
4175
2515
  }));
2516
+ program.command("downloads").description("List completed downloads available to this Agent namespace").action(action(async () => {
2517
+ const artifacts = await (await requireCompatibility()).listArtifacts(["download"]);
2518
+ const downloads = artifacts.map((artifact, index) => ({
2519
+ index: index + 1,
2520
+ id: artifact.id,
2521
+ fileName: artifact.fileName,
2522
+ mimeType: artifact.mimeType,
2523
+ sizeBytes: artifact.byteSize,
2524
+ createdAt: artifact.createdAt,
2525
+ expiresAt: artifact.expiresAt
2526
+ }));
2527
+ if (useJson()) {
2528
+ console.log(JSON.stringify({ ok: true, downloads }));
2529
+ } else if (downloads.length === 0) {
2530
+ console.log("No completed downloads.");
2531
+ } else {
2532
+ for (const download of downloads) {
2533
+ console.log(`${download.index} ${download.fileName ?? download.id} ${download.sizeBytes} bytes`);
2534
+ }
2535
+ }
2536
+ }));
2537
+ program.command("download <selector> [filename]").description("Export one completed download to a local file").action(action(async (selector, filename) => {
2538
+ const client = await requireCompatibility();
2539
+ const artifacts = await client.listArtifacts(["download"]);
2540
+ const artifact = /^\d+$/.test(selector) ? artifacts[Number(selector) - 1] : artifacts.find((candidate) => candidate.id === selector);
2541
+ if (!artifact) {
2542
+ throw new BrowserPilotError("artifact_not_found", "Download was not found for this Agent namespace", {
2543
+ context: { field: "selector" }
2544
+ });
2545
+ }
2546
+ const fallbackName = artifact.fileName ? basename2(artifact.fileName) : `download-${artifact.createdAt}`;
2547
+ const file = outputPath(filename ?? fallbackName);
2548
+ await client.exportArtifact(artifact.id, file);
2549
+ await client.releaseArtifact(artifact.id).catch(() => {
2550
+ });
2551
+ emit({ ok: true, ...artifactFileResult(artifact, file) }, `\u2713 Download saved to ${file}`);
2552
+ }));
4176
2553
  program.command("screenshot [filename]").description("Capture screenshot").option("-f, --full", "capture full page").option("--selector <sel>", "capture specific element").option("--annotate [refs]", "draw Observation ref boxes; optionally comma-separated refs").addHelpText("after", '\nExamples:\n bp screenshot\n bp screenshot page.png\n bp screenshot --full\n bp screenshot --selector ".chart"\n bp screenshot page.png --annotate\n bp screenshot page.png --annotate 1,3,8').action(action(async (filename, opts) => {
4177
2554
  if (opts.annotate !== void 0 && (opts.full || opts.selector)) {
4178
- throw new Error("--annotate cannot be combined with --full or --selector");
2555
+ throw invalidArgument("--annotate cannot be combined with --full or --selector", "annotate");
4179
2556
  }
4180
2557
  await withCliTarget(async (client, target) => {
4181
2558
  let annotations;
@@ -4194,9 +2571,9 @@ program.command("screenshot [filename]").description("Capture screenshot").optio
4194
2571
  }, target.targetId);
4195
2572
  const artifact = artifactFrom(result);
4196
2573
  const file = filename ?? `screenshot-${(/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-").slice(0, -5)}.png`;
4197
- const outputPath = resolvePath(file);
2574
+ const destination = outputPath(file);
4198
2575
  try {
4199
- await client.exportArtifact(artifact.id, outputPath);
2576
+ await client.exportArtifact(artifact.id, destination);
4200
2577
  } finally {
4201
2578
  await client.releaseArtifact(artifact.id).catch(() => {
4202
2579
  });
@@ -4208,9 +2585,9 @@ program.command("screenshot [filename]").description("Capture screenshot").optio
4208
2585
  }
4209
2586
  emit({
4210
2587
  ok: true,
4211
- file: outputPath,
2588
+ ...artifactFileResult(artifact, destination),
4212
2589
  ...typeof result.annotationCount === "number" ? { annotationCount: result.annotationCount } : {}
4213
- }, `\u2713 Screenshot saved to ${outputPath}`);
2590
+ }, `\u2713 Screenshot saved to ${destination}`);
4214
2591
  });
4215
2592
  }));
4216
2593
  program.command("pdf [filename]").description("Save page as PDF").option("--landscape", "landscape orientation").addHelpText("after", "\nExamples:\n bp pdf\n bp pdf report.pdf\n bp pdf report.pdf --landscape").action(action(async (filename, opts) => {
@@ -4220,14 +2597,14 @@ program.command("pdf [filename]").description("Save page as PDF").option("--land
4220
2597
  }, target.targetId);
4221
2598
  const artifact = artifactFrom(result);
4222
2599
  const file = filename ?? `page-${(/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-").slice(0, -5)}.pdf`;
4223
- const outputPath = resolvePath(file);
2600
+ const destination = outputPath(file);
4224
2601
  try {
4225
- await client.exportArtifact(artifact.id, outputPath);
2602
+ await client.exportArtifact(artifact.id, destination);
4226
2603
  } finally {
4227
2604
  await client.releaseArtifact(artifact.id).catch(() => {
4228
2605
  });
4229
2606
  }
4230
- emit({ ok: true, file: outputPath }, `\u2713 PDF saved to ${outputPath}`);
2607
+ emit({ ok: true, ...artifactFileResult(artifact, destination) }, `\u2713 PDF saved to ${destination}`);
4231
2608
  });
4232
2609
  }));
4233
2610
  program.command("cookies [domain]").description("View cookies (CDP-only, includes HttpOnly)").addHelpText("after", "\nExamples:\n bp cookies\n bp cookies github.com").action(action(async (domain) => {
@@ -4263,12 +2640,15 @@ program.command("frame [target]").description("List frames, or switch to a frame
4263
2640
  }
4264
2641
  }
4265
2642
  } else {
4266
- const idx = parseInt(target, 10);
2643
+ const idx = Number(target);
4267
2644
  if (!Number.isSafeInteger(idx) || idx < 0 || idx >= frames.length) {
4268
- throw new Error(`Frame index out of range (0-${Math.max(0, frames.length - 1)})`);
2645
+ throw invalidArgument(
2646
+ `Frame index out of range (0-${Math.max(0, frames.length - 1)})`,
2647
+ "target"
2648
+ );
4269
2649
  }
4270
2650
  const frame = frames[idx];
4271
- await client.callTool("browser.frames.switch", idx === 0 ? { top: true } : { frameId: requireString2(frame.frameId, "frameId") }, controlledTarget.targetId);
2651
+ await client.callTool("browser.frames.switch", idx === 0 ? { top: true } : { frameId: requireString(frame.frameId, "frameId") }, controlledTarget.targetId);
4272
2652
  emit(
4273
2653
  { ok: true, frame: idx, url: frame.url },
4274
2654
  `\u2713 Switched to frame ${idx}: ${frame.url}`
@@ -4283,7 +2663,7 @@ program.command("auth [username] [password]").description("Set or clear HTTP Bas
4283
2663
  emit({ ok: true }, "\u2713 Auth credentials cleared");
4284
2664
  return;
4285
2665
  }
4286
- if (!password) throw new Error("Usage: bp auth <username> <password>");
2666
+ if (!password) throw invalidArgument("Usage: bp auth <username> <password>", "password");
4287
2667
  await client.callTool("browser.auth.set", { username, password });
4288
2668
  emit({ ok: true }, "\u2713 Auth credentials set (scoped to HTTP 401 challenges)");
4289
2669
  }));
@@ -4302,7 +2682,7 @@ program.command("dialogs").description("List pending JavaScript dialogs").action
4302
2682
  }));
4303
2683
  program.command("dialog <dialogId>").description("Accept or dismiss a pending JavaScript dialog").option("--accept", "accept the dialog").option("--dismiss", "dismiss the dialog").option("--prompt <text>", "text to submit to a prompt dialog").action(action(async (dialogId, opts) => {
4304
2684
  if (Boolean(opts.accept) === Boolean(opts.dismiss)) {
4305
- throw new Error("Choose exactly one of --accept or --dismiss");
2685
+ throw invalidArgument("Choose exactly one of --accept or --dismiss", "action");
4306
2686
  }
4307
2687
  const client = await requireCompatibility();
4308
2688
  const listed = await client.callTool("browser.dialogs.list");
@@ -4316,7 +2696,7 @@ program.command("dialog <dialogId>").description("Accept or dismiss a pending Ja
4316
2696
  dialogId,
4317
2697
  action: opts.accept ? "accept" : "dismiss",
4318
2698
  ...opts.prompt !== void 0 ? { promptText: opts.prompt } : {}
4319
- }, requireString2(dialog.targetId, "dialog targetId"));
2699
+ }, requireString(dialog.targetId, "dialog targetId"));
4320
2700
  emit(
4321
2701
  { ok: true, dialogId: result.dialogId, action: result.action },
4322
2702
  `\u2713 ${result.action === "accept" ? "Accepted" : "Dismissed"} dialog ${result.dialogId}`
@@ -4324,9 +2704,10 @@ program.command("dialog <dialogId>").description("Accept or dismiss a pending Ja
4324
2704
  }));
4325
2705
  program.command("tabs").description("List all controllable browser tabs").action(action(async () => {
4326
2706
  const targets = await (await requireCompatibility()).listTabs("all");
4327
- const tabs = targets.map(({ targetId: _targetId, managedTabSetId: _managedTabSetId, controlState, ...tab }, index) => ({
4328
- index,
2707
+ const tabs = targets.map(({ targetId: _targetId, managedTabSetId: _managedTabSetId, controlState, active, selected, ...tab }, index) => ({
2708
+ index: index + 1,
4329
2709
  ...tab,
2710
+ selected: selected ?? active === true,
4330
2711
  controlState
4331
2712
  }));
4332
2713
  if (useJson()) {
@@ -4334,20 +2715,20 @@ program.command("tabs").description("List all controllable browser tabs").action
4334
2715
  } else if (tabs.length === 0) {
4335
2716
  console.log("No controllable tabs open.");
4336
2717
  } else {
4337
- for (const t of tabs) console.log(`${t.active ? "*" : " "} ${t.index} ${t.url} ${t.title}`);
2718
+ for (const t of tabs) console.log(`${t.selected ? "*" : " "} ${t.index} ${t.url} ${t.title}`);
4338
2719
  }
4339
2720
  }));
4340
- program.command("tab <index>").description("Switch to tab by index").action(action(async (indexStr) => {
2721
+ program.command("tab <index>").description("Select a tab by one-based index").action(action(async (indexStr) => {
4341
2722
  const client = await requireCompatibility();
4342
- const index = parseInt(indexStr, 10);
2723
+ const index = Number(indexStr);
4343
2724
  const targets = await client.listTabs("all");
4344
- if (!Number.isSafeInteger(index) || index < 0 || index >= targets.length) {
4345
- throw new Error(`Tab index out of range (0-${Math.max(0, targets.length - 1)})`);
2725
+ if (!Number.isSafeInteger(index) || index < 1 || index > targets.length) {
2726
+ throw invalidArgument(`Tab index out of range (1-${targets.length})`, "index");
4346
2727
  }
4347
- await client.callTool("browser.tabs.switch", { targetId: targets[index].targetId });
4348
- emit({ ok: true, index }, `\u2713 Switched to tab ${index}`);
2728
+ await client.callTool("browser.tabs.switch", { targetId: targets[index - 1].targetId });
2729
+ emit({ ok: true, index }, `\u2713 Selected tab ${index}`);
4349
2730
  }));
4350
- program.command("close").description("Close current browser tab").option("-a, --all", "close all tabs in the current Pilot window").action(action(async (opts) => {
2731
+ program.command("close").description("Close current browser tab").option("-a, --all", "close all managed tabs in this Agent Workspace").action(action(async (opts) => {
4351
2732
  const client = await requireCompatibility();
4352
2733
  if (opts.all) {
4353
2734
  const managed = await client.listTabs("managed_only");
@@ -4377,7 +2758,7 @@ program.command("close").description("Close current browser tab").option("-a, --
4377
2758
  await client.callTool("browser.tabs.close", {}, target.targetId);
4378
2759
  const remainingTabs = await client.listTabs("all");
4379
2760
  if (remainingTabs.length > 0) {
4380
- if (!remainingTabs.some((tab) => tab.active)) {
2761
+ if (!remainingTabs.some((tab) => tab.selected ?? tab.active === true)) {
4381
2762
  const fallback = remainingTabs.find((tab) => tab.origin !== "user_tab");
4382
2763
  if (fallback) await client.callTool("browser.tabs.switch", { targetId: fallback.targetId });
4383
2764
  }
@@ -4404,7 +2785,7 @@ function cliNetworkRequest(request) {
4404
2785
  }
4405
2786
  async function findNetworkRequest(client, sequence) {
4406
2787
  if (!Number.isSafeInteger(sequence) || sequence < 1) {
4407
- throw new Error("Request ID must be a positive integer");
2788
+ throw invalidArgument("Request ID must be a positive integer", "id");
4408
2789
  }
4409
2790
  const listed = await client.callTool("browser.network.requests", {
4410
2791
  after: sequence - 1,
@@ -4428,14 +2809,18 @@ Examples:
4428
2809
  bp net rules # list active rules
4429
2810
  bp net remove --all # clear rules`).action(action(async (opts) => {
4430
2811
  const client = await requireCompatibility();
4431
- const limit = opts.limit ? parseInt(opts.limit, 10) : 20;
2812
+ const limit = parseLimit(opts.limit ?? "20");
2813
+ const after = opts.after === void 0 ? void 0 : Number(opts.after);
2814
+ if (after !== void 0 && (!/^\d+$/.test(opts.after) || !Number.isSafeInteger(after))) {
2815
+ throw invalidArgument("--after must be a non-negative integer", "after");
2816
+ }
4432
2817
  const result = await client.callTool("browser.network.requests", {
4433
2818
  limit,
4434
2819
  ...opts.url ? { url: opts.url } : {},
4435
2820
  ...opts.method ? { method: opts.method } : {},
4436
2821
  ...opts.status ? { status: opts.status } : {},
4437
2822
  ...opts.type ? { type: String(opts.type).split(",").map((value) => value.trim()).filter(Boolean) } : {},
4438
- ...opts.after ? { after: parseInt(opts.after, 10) } : {}
2823
+ ...after !== void 0 ? { after } : {}
4439
2824
  });
4440
2825
  const requests = networkRequests(result).map(cliNetworkRequest);
4441
2826
  if (useJson()) {
@@ -4459,20 +2844,31 @@ Examples:
4459
2844
  }));
4460
2845
  netCmd.command("show <id>").description("Show full request/response details").option("--save <file>", "save response body to file").action(action(async (idStr, opts) => {
4461
2846
  const client = await requireCompatibility();
4462
- const id = parseInt(idStr, 10);
2847
+ const id = Number(idStr);
4463
2848
  const summary = await findNetworkRequest(client, id);
4464
2849
  const result = await client.callTool("browser.network.request", {
4465
- requestId: requireString2(summary.requestId, "requestId"),
2850
+ requestId: requireString(summary.requestId, "requestId"),
4466
2851
  includeBody: true
4467
2852
  });
4468
2853
  const request = result.request && typeof result.request === "object" && !Array.isArray(result.request) ? result.request : {};
4469
2854
  const responseBody = typeof result.body === "string" ? result.body : void 0;
4470
2855
  if (opts.save) {
4471
- if (responseBody === void 0) throw new Error(`Response body for request #${id} is unavailable`);
4472
- const outputPath = resolvePath(opts.save);
2856
+ if (responseBody === void 0) {
2857
+ throw new BrowserPilotError(
2858
+ "action_not_verified",
2859
+ `Response body for request #${id} is unavailable`,
2860
+ { context: { sequence: id } }
2861
+ );
2862
+ }
2863
+ const destination = outputPath(opts.save);
4473
2864
  const bytes = result.bodyEncoding === "base64" ? Buffer.from(responseBody, "base64") : Buffer.from(responseBody, "utf8");
4474
- writeFileSync2(outputPath, bytes);
4475
- emit({ ok: true, file: outputPath }, `Saved to ${outputPath}`);
2865
+ writeFileSync2(destination, bytes, { mode: 384 });
2866
+ emit({
2867
+ ok: true,
2868
+ file: destination,
2869
+ mimeType: typeof request.mimeType === "string" ? request.mimeType : "application/octet-stream",
2870
+ sizeBytes: bytes.byteLength
2871
+ }, `Saved to ${destination}`);
4476
2872
  return;
4477
2873
  }
4478
2874
  const detail = { id: request.sequence, ...request, responseBody };
@@ -4497,15 +2893,18 @@ netCmd.command("block <pattern>").description("Block requests matching URL patte
4497
2893
  const rule = { id: result.ruleId, type: "block", pattern };
4498
2894
  emit({ ok: true, rule }, `Rule #${rule.id}: blocking "${pattern}"`);
4499
2895
  }));
4500
- netCmd.command("mock <pattern>").description("Mock responses for matching URLs").option("--body <json>", "response body").option("--file <path>", "read body from file").option("--status <code>", "HTTP status", "200").action(action(async (pattern, opts) => {
2896
+ netCmd.command("mock <pattern>").description("Mock responses for matching URLs").option("--body <text>", "response body text").option("--file <path>", "read body from file").option("--status <code>", "HTTP status", "200").action(action(async (pattern, opts) => {
4501
2897
  const client = await requireCompatibility();
4502
2898
  let body = opts.body || "";
4503
2899
  if (opts.file) {
4504
2900
  const filePath = resolvePath(opts.file);
4505
- if (!existsSync4(filePath)) throw new Error(`File not found: ${filePath}`);
2901
+ if (!existsSync4(filePath)) throw invalidArgument(`File not found: ${filePath}`, "file");
4506
2902
  body = readFileSync4(filePath, "utf-8");
4507
2903
  }
4508
- const status = parseInt(opts.status, 10);
2904
+ const status = Number(opts.status);
2905
+ if (!/^\d+$/.test(opts.status) || !Number.isSafeInteger(status) || status < 100 || status > 599) {
2906
+ throw invalidArgument("--status must be an HTTP status from 100 through 599", "status");
2907
+ }
4509
2908
  const result = await client.callTool("browser.network.rules.add", {
4510
2909
  type: "mock",
4511
2910
  pattern,
@@ -4552,10 +2951,32 @@ netCmd.command("remove [ruleId]").description("Remove interception rule(s)").opt
4552
2951
  } else if (ruleId) {
4553
2952
  await client.callTool("browser.network.rules.remove", { ruleId });
4554
2953
  emit({ ok: true }, `Rule #${ruleId} removed`);
4555
- } else throw new Error("Specify a rule ID or use --all");
2954
+ } else throw invalidArgument("Specify a rule ID or use --all", "ruleId");
4556
2955
  }));
4557
2956
  netCmd.command("clear").description("Clear captured request log").action(action(async () => {
4558
2957
  await (await requireCompatibility()).callTool("browser.network.clear");
4559
2958
  emit({ ok: true }, "Request log cleared");
4560
2959
  }));
4561
- program.parse();
2960
+ async function main() {
2961
+ try {
2962
+ await program.parseAsync();
2963
+ } catch (error) {
2964
+ if (error instanceof CommanderError) {
2965
+ if (error.exitCode === 0) {
2966
+ process.exitCode = 0;
2967
+ } else if (useJson()) {
2968
+ const message = error.message.replace(/^error:\s*/i, "");
2969
+ const stable = new BrowserPilotError("invalid_argument", message, {
2970
+ context: { parserCode: error.code }
2971
+ });
2972
+ fail(message, void 0, stable);
2973
+ } else {
2974
+ process.exitCode = error.exitCode;
2975
+ }
2976
+ } else {
2977
+ const message = error instanceof Error ? error.message : String(error);
2978
+ fail(message, void 0, error instanceof BrowserPilotError ? error : void 0);
2979
+ }
2980
+ }
2981
+ }
2982
+ void main();