@orkestrel/browser 0.0.26 → 0.0.28

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.
@@ -1,10 +1,10 @@
1
- import { BROWSER_DEFAULT_TIMEOUT_MS, BROWSER_JOURNEY_FORMAT_VERSION, BROWSER_JOURNEY_NAME_PATTERN, BROWSER_JOURNEY_TOOL_NAMES, BROWSER_RUN_ID_PATTERN, BROWSER_TOOL_COPY, BROWSER_TOOL_NAMES, BrowserConnectionError, BrowserContext, BrowserError, BrowserTransition, CDPClient, createBrowserToolset, createCDPClient, generateBrowserRunId, isBrowserConnectionError, isBrowserError, isCDPConnectionError, isCDPTimeoutError, normalizeBrowserJourneyReason, validateBrowserContextOptions, validateBrowserJourney, validateBrowserRun, validateBrowserStorePage } from "../core/index.js";
2
- import { attempt, isArray, isError, isInstance, isInteger, isNumber, isRecord, isString, parseInteger, parseJSON } from "@orkestrel/contract";
3
1
  import { existsSync, globSync, realpathSync } from "node:fs";
4
2
  import { lstat, mkdir, mkdtemp, open, readFile, readdir, rename, rm, rmdir, unlink, writeFile } from "node:fs/promises";
5
3
  import { tmpdir } from "node:os";
6
4
  import { basename, dirname, join, posix, relative, resolve, sep, win32 } from "node:path";
7
5
  import { spawn, spawnSync } from "node:child_process";
6
+ import { attempt, isArray, isError, isInteger, isJSONValue, isNumber, isRecord, isString, parseInteger, parseJSON } from "@orkestrel/contract";
7
+ import { BROWSER_DEFAULT_TIMEOUT_MS, BROWSER_JOURNEY_FORMAT_VERSION, BROWSER_JOURNEY_NAME_PATTERN, BROWSER_JOURNEY_TOOL_NAMES, BROWSER_RUN_ID_PATTERN, BROWSER_TOOL_COPY, BROWSER_TOOL_CUT_FOOTER, BROWSER_TOOL_LIMIT, BROWSER_TOOL_NAMES, BrowserContext, BrowserError, BrowserTransition, CDPClient, boundBrowserText, createBrowserToolset, createCDPClient, generateBrowserRunId, isBrowserError, normalizeBrowserJourneyReason, validateBrowserContextOptions, validateBrowserJourney, validateBrowserJourneyWriteOptions, validateBrowserRun, validateBrowserStorePage } from "../core/index.js";
8
8
  import { randomBytes, randomUUID } from "node:crypto";
9
9
  import { request } from "node:http";
10
10
  import { request as request$1 } from "node:https";
@@ -226,9 +226,9 @@ var BROWSER_RUN_DIRECTORY = "runs";
226
226
  /** Bounds a file-store listing page by default. */
227
227
  var BROWSER_FILE_STORE_LIMIT = 100;
228
228
  /** Names the refusal when no browser can serve a call. */
229
- var BROWSER_SERVER_UNAVAILABLE = "BROWSER_SERVER_UNAVAILABLE";
229
+ var BROWSER_SERVER_UNAVAILABLE = "SERVER_UNAVAILABLE";
230
230
  /** Names a failed attempt to warm a browser. */
231
- var BROWSER_SERVER_LAUNCH = "BROWSER_SERVER_LAUNCH";
231
+ var BROWSER_SERVER_LAUNCH = "SERVER_LAUNCH";
232
232
  /** Sets the default number of warm browsers to 1. */
233
233
  var BROWSER_SERVER_POOL_SIZE = 1;
234
234
  /** Sets the default context capacity per browser, including the shared holder. */
@@ -242,21 +242,21 @@ var BROWSER_SERVER_RESTARTS = 1;
242
242
  /** Names the browser process record in each profile. */
243
243
  var BROWSER_SERVER_RECORD = "browse.json";
244
244
  /** Names the diagnostic when the warm floor spends its restart bound. */
245
- var BROWSER_SERVER_EXHAUSTED = "BROWSER_SERVER_EXHAUSTED";
245
+ var BROWSER_SERVER_EXHAUSTED = "SERVER_EXHAUSTED";
246
246
  /** Names a failed browser server teardown. */
247
- var BROWSER_SERVER_TEARDOWN = "BROWSER_SERVER_TEARDOWN";
247
+ var BROWSER_SERVER_TEARDOWN = "SERVER_TEARDOWN";
248
248
  /** Names a failed profile sweep. */
249
- var BROWSER_SERVER_SWEEP = "BROWSER_SERVER_SWEEP";
249
+ var BROWSER_SERVER_SWEEP = "SERVER_SWEEP";
250
250
  /** Names a refused browser server option. */
251
- var BROWSER_SERVER_OPTIONS = "BROWSER_SERVER_OPTIONS";
251
+ var BROWSER_SERVER_OPTIONS = "ARGUMENT";
252
252
  /** Names the notice that a browser and its session state were lost. */
253
- var BROWSER_SERVER_CRASH = "BROWSER_SERVER_CRASH";
253
+ var BROWSER_SERVER_CRASH = "SERVER_CRASH";
254
254
  /** Names an interrupted call whose outcome is unknown. */
255
- var BROWSER_SERVER_UNRESOLVED = "BROWSER_SERVER_UNRESOLVED";
255
+ var BROWSER_SERVER_UNRESOLVED = "SERVER_UNRESOLVED";
256
256
  /** Names the refusal when every browser is admitted to a holder. */
257
- var BROWSER_SERVER_BUSY = "BROWSER_SERVER_BUSY";
257
+ var BROWSER_SERVER_BUSY = "SERVER_BUSY";
258
258
  /** Names the refusal for an unknown or ended holder. */
259
- var BROWSER_SERVER_HOLDER = "BROWSER_SERVER_HOLDER";
259
+ var BROWSER_SERVER_HOLDER = "SERVER_HOLDER";
260
260
  /** Defines the holder tools independently of the shared browser's vocabulary. */
261
261
  var BROWSER_SERVER_COPY = Object.freeze({
262
262
  acquire: Object.freeze({
@@ -332,46 +332,6 @@ var BROWSER_SERVER_COPY = Object.freeze({
332
332
  })
333
333
  });
334
334
  //#endregion
335
- //#region src/server/errors.ts
336
- /**
337
- * Reports that an operation requiring an active connection was attempted while disconnected,
338
- * under the code `BROWSER_NOT_CONNECTED_ERROR`.
339
- */
340
- var BrowserNotConnectedError = class extends BrowserError {
341
- constructor(context) {
342
- super("Browser is not connected", "BROWSER_NOT_CONNECTED_ERROR", context);
343
- this.name = "BrowserNotConnectedError";
344
- }
345
- };
346
- /**
347
- * Reports that an operation was attempted after the browser wrapper was destroyed, under the
348
- * code `BROWSER_DESTROYED_ERROR`.
349
- */
350
- var BrowserDestroyedError = class extends BrowserError {
351
- constructor(context) {
352
- super("Browser has been destroyed", "BROWSER_DESTROYED_ERROR", context);
353
- this.name = "BrowserDestroyedError";
354
- }
355
- };
356
- /**
357
- * Narrows an unknown value to a `BrowserNotConnectedError`.
358
- *
359
- * @param value - Value to check
360
- * @returns True if value is a BrowserNotConnectedError instance; false otherwise
361
- */
362
- function isBrowserNotConnectedError(value) {
363
- return isInstance(value, BrowserNotConnectedError);
364
- }
365
- /**
366
- * Narrows an unknown value to a `BrowserDestroyedError`.
367
- *
368
- * @param value - Value to check
369
- * @returns True if value is a BrowserDestroyedError instance; false otherwise
370
- */
371
- function isBrowserDestroyedError(value) {
372
- return isInstance(value, BrowserDestroyedError);
373
- }
374
- //#endregion
375
335
  //#region src/server/helpers.ts
376
336
  /**
377
337
  * Parses positive integer viewport dimensions in WIDTHxHEIGHT form.
@@ -434,15 +394,15 @@ function parseBrowserProfileRecord(text) {
434
394
  * @param url - The lost page's last URL, when known
435
395
  * @returns The coded diagnostic and any operation-lifecycle guidance
436
396
  * @example
437
- * describeBrowserServerLoss('BROWSER_SERVER_UNAVAILABLE', new Error('Browser exited'))
397
+ * describeBrowserServerLoss('SERVER_UNAVAILABLE', new Error('Browser exited'))
438
398
  */
439
399
  function describeBrowserServerLoss(code, cause, url) {
440
400
  const pid = isBrowserError(cause) ? cause.context?.["pid"] : void 0;
441
401
  const message = `${code}: ${(cause === void 0 ? "Browser session lost" : isError(cause) ? cause.message : String(cause)).replace(/\.+$/u, "")}${isNumber(pid) ? ` (pid ${pid})` : ""}.`;
442
- if (code !== "BROWSER_SERVER_CRASH" && code !== "BROWSER_SERVER_UNRESOLVED") return message;
443
- const lost = `Lost the page${url === void 0 ? "" : ` at ${url}`}, its tabs, every element reference, the retained reading, dialogs, holds, an unsaved recording, the active replay, and the isolated context's cookies.`;
402
+ if (code !== "SERVER_CRASH" && code !== "SERVER_UNRESOLVED") return message;
403
+ const lost = `Lost the page${url === void 0 ? "" : ` at ${url}`}, its tabs, every element reference, the last projection, dialogs, holds, an unsaved recording, the active replay, and the isolated context's cookies.`;
444
404
  const next = `The next call acquires a browser that starts at about:blank, or answers ${BROWSER_SERVER_UNAVAILABLE} when none can serve.`;
445
- return code === "BROWSER_SERVER_UNRESOLVED" ? `${message} The outcome is unknown. ${lost} Browse did not repeat the call. ${next}` : `${message} ${lost}`;
405
+ return code === "SERVER_UNRESOLVED" ? `${message} The outcome is unknown. ${lost} Browse did not repeat the call. ${next}` : `${message} ${lost}`;
446
406
  }
447
407
  /**
448
408
  * Formats a process identifier and UUID token as a lock entry name.
@@ -517,16 +477,6 @@ function findSystemBrowsers(options) {
517
477
  return browsers;
518
478
  }
519
479
  /**
520
- * Locates a Chrome/Chromium/Edge executable on this machine — the first entry
521
- * of {@link findSystemBrowsers}.
522
- *
523
- * @param options - Overrides for the candidate sources; see {@link SystemBrowserOptions}
524
- * @returns The first discovered browser, or undefined
525
- */
526
- function findSystemBrowser(options) {
527
- return findSystemBrowsers(options)[0];
528
- }
529
- /**
530
480
  * Classifies an executable path/name into a {@link BrowserEngine} by
531
481
  * case-insensitive hint, checked in the order edge → chromium → chrome.
532
482
  *
@@ -582,7 +532,7 @@ async function removeBrowserProfile(profile) {
582
532
  if (!profile.temporary) return;
583
533
  const path = resolve(profile.path);
584
534
  const temporary = resolve(tmpdir());
585
- if (dirname(path) !== temporary || !basename(path).startsWith("orkestrel-browser-")) throw new BrowserError("Refusing to remove an unsafe browser profile path", void 0, { path });
535
+ if (dirname(path) !== temporary || !basename(path).startsWith("orkestrel-browser-")) throw new BrowserError("ARGUMENT", "Refusing to remove an unsafe browser profile path", { path });
586
536
  await rm(path, {
587
537
  recursive: true,
588
538
  force: true,
@@ -772,18 +722,18 @@ function readBrowserEndpoint(stream, signal) {
772
722
  stream.on("close", () => {
773
723
  const endpoint = found ? void 0 : BROWSER_DEVTOOLS_PATTERN.exec(pending)?.[1];
774
724
  if (endpoint !== void 0) read.resolve(endpoint);
775
- else read.reject(new BrowserConnectionError("Browser closed its standard error before reporting a CDP endpoint"));
725
+ else read.reject(new BrowserError("CONNECTION", "Browser closed its standard error before reporting a CDP endpoint"));
776
726
  });
777
727
  return read.promise;
778
728
  }
779
729
  /**
780
730
  * Fetches the current CDP target list from a browser's `/json/list` endpoint, as a `Result`
781
- * carrying either the targets or a coded `BrowserConnectionError`.
731
+ * carrying either the targets or a coded `BrowserError`.
782
732
  *
783
733
  * @remarks
784
734
  * The endpoint is a network boundary, so an unreachable host, a non-2xx
785
735
  * response, and a body that is not a JSON array each come back as a failed
786
- * `Result` carrying a coded `BrowserConnectionError`. An entry missing a
736
+ * `Result` carrying a coded `BrowserError`. An entry missing a
787
737
  * required string field is skipped rather than failing the whole list.
788
738
  *
789
739
  * @param port - Port the browser exposes its CDP endpoint on
@@ -808,7 +758,7 @@ async function fetchCDPTargets(port, timeout, host = BROWSER_DEFAULT_HOST) {
808
758
  const response = await fetch(url, { signal: controller.signal });
809
759
  if (!response.ok) return {
810
760
  success: false,
811
- error: new BrowserConnectionError("CDP target list request was refused", {
761
+ error: new BrowserError("CONNECTION", "CDP target list request was refused", {
812
762
  url,
813
763
  status: response.status
814
764
  })
@@ -816,7 +766,7 @@ async function fetchCDPTargets(port, timeout, host = BROWSER_DEFAULT_HOST) {
816
766
  const list = await response.json();
817
767
  if (!isArray(list)) return {
818
768
  success: false,
819
- error: new BrowserConnectionError("CDP target list is not a JSON array", { url })
769
+ error: new BrowserError("CONNECTION", "CDP target list is not a JSON array", { url })
820
770
  };
821
771
  const targets = [];
822
772
  for (const entry of list) {
@@ -836,9 +786,9 @@ async function fetchCDPTargets(port, timeout, host = BROWSER_DEFAULT_HOST) {
836
786
  } catch (error) {
837
787
  return {
838
788
  success: false,
839
- error: new BrowserConnectionError("CDP target list is unreachable", {
789
+ error: new BrowserError("CONNECTION", "CDP target list is unreachable", {
840
790
  url,
841
- error
791
+ error: isJSONValue(error) ? error : String(error)
842
792
  })
843
793
  };
844
794
  } finally {
@@ -904,7 +854,7 @@ var WebSocketCDPTransport = class {
904
854
  }
905
855
  async send(data) {
906
856
  const socket = this.#socket;
907
- if (socket === void 0 || socket.readyState !== WEBSOCKET_READY_OPEN) throw new BrowserConnectionError("WebSocket CDP transport is not open", { url: this.#url });
857
+ if (socket === void 0 || socket.readyState !== WEBSOCKET_READY_OPEN) throw new BrowserError("CONNECTION", "WebSocket CDP transport is not open", { url: this.#url });
908
858
  socket.send(data);
909
859
  }
910
860
  async close() {
@@ -912,12 +862,12 @@ var WebSocketCDPTransport = class {
912
862
  }
913
863
  async #start() {
914
864
  const parsed = attempt(() => new URL(this.#url));
915
- if (!parsed.success) throw new BrowserConnectionError(`WebSocket CDP URL is invalid: ${this.#url}`, {
865
+ if (!parsed.success) throw new BrowserError("CONNECTION", `WebSocket CDP URL is invalid: ${this.#url}`, {
916
866
  url: this.#url,
917
- error: parsed.error
867
+ error: isJSONValue(parsed.error) ? parsed.error : String(parsed.error)
918
868
  });
919
869
  const url = parsed.value;
920
- if (url.protocol !== "ws:" && url.protocol !== "wss:") throw new BrowserConnectionError(`WebSocket CDP connection requires a ws: or wss: URL: ${this.#url}`, { url: this.#url });
870
+ if (url.protocol !== "ws:" && url.protocol !== "wss:") throw new BrowserError("CONNECTION", `WebSocket CDP connection requires a ws: or wss: URL: ${this.#url}`, { url: this.#url });
921
871
  const key = randomBytes(16).toString("base64");
922
872
  const deferred = Promise.withResolvers();
923
873
  const options = {
@@ -949,7 +899,7 @@ var WebSocketCDPTransport = class {
949
899
  async #close() {
950
900
  const request = this.#request;
951
901
  if (request !== void 0) {
952
- this.#rejectRequest(request, new BrowserConnectionError(`WebSocket CDP connection to ${this.#url} was closed before it finished connecting`, { url: this.#url }));
902
+ this.#rejectRequest(request, new BrowserError("CONNECTION", `WebSocket CDP connection to ${this.#url} was closed before it finished connecting`, { url: this.#url }));
953
903
  request.destroy();
954
904
  }
955
905
  await this.#starting.pending?.catch(() => void 0);
@@ -978,7 +928,7 @@ var WebSocketCDPTransport = class {
978
928
  const accept = response.headers["sec-websocket-accept"];
979
929
  if (!isString(accept) || accept !== computeWebSocketAccept(key)) {
980
930
  socket.destroy();
981
- this.#rejectRequest(request, new BrowserConnectionError(`WebSocket CDP connection to ${this.#url} failed: Sec-WebSocket-Accept mismatch`, { url: this.#url }));
931
+ this.#rejectRequest(request, new BrowserError("CONNECTION", `WebSocket CDP connection to ${this.#url} failed: Sec-WebSocket-Accept mismatch`, { url: this.#url }));
982
932
  return;
983
933
  }
984
934
  try {
@@ -994,13 +944,13 @@ var WebSocketCDPTransport = class {
994
944
  #decline(request, response) {
995
945
  response.resume();
996
946
  const reason = response.statusCode === void 0 ? "upgrade was declined without a status code" : `upgrade declined with status ${response.statusCode}`;
997
- this.#rejectRequest(request, new BrowserConnectionError(`WebSocket CDP connection to ${this.#url} failed: ${reason}`, { url: this.#url }));
947
+ this.#rejectRequest(request, new BrowserError("CONNECTION", `WebSocket CDP connection to ${this.#url} failed: ${reason}`, { url: this.#url }));
998
948
  }
999
949
  #fail(request, error) {
1000
- this.#rejectRequest(request, new BrowserConnectionError(`WebSocket CDP connection to ${this.#url} failed: ${error.message}`, { url: this.#url }));
950
+ this.#rejectRequest(request, new BrowserError("CONNECTION", `WebSocket CDP connection to ${this.#url} failed: ${error.message}`, { url: this.#url }));
1001
951
  }
1002
952
  #expire(request) {
1003
- this.#rejectRequest(request, new BrowserConnectionError(`WebSocket CDP connection to ${this.#url} timed out after ${this.#timeout}ms`, {
953
+ this.#rejectRequest(request, new BrowserError("CONNECTION", `WebSocket CDP connection to ${this.#url} timed out after ${this.#timeout}ms`, {
1004
954
  url: this.#url,
1005
955
  timeout: this.#timeout
1006
956
  }));
@@ -1072,7 +1022,7 @@ var FileBrowserWriter = class {
1072
1022
  };
1073
1023
  //#endregion
1074
1024
  //#region package.json
1075
- var version = "0.0.26";
1025
+ var version = "0.0.28";
1076
1026
  //#endregion
1077
1027
  //#region src/server/BrowserMCPServer.ts
1078
1028
  /**
@@ -1080,9 +1030,9 @@ var version = "0.0.26";
1080
1030
  * the Model Context Protocol on stdio, and warms Chromium at server start.
1081
1031
  *
1082
1032
  * @remarks
1083
- * The server's own manager holds one dispatcher per name of the vocabulary — `look`, `read`, `plain`,
1084
- * `click`, `type`, `press`, `navigate`, `wait`, `dialog`, `tabs`, `switch`, `record`, `save`,
1085
- * `journeys`, `edit`, `replay`, and `forget` — each carrying the description, parameters, and annotations
1033
+ * The server's own manager holds one dispatcher per name of the vocabulary — `read`, `click`,
1034
+ * `type`, `press`, `navigate`, `wait`, `dialog`, `switch`, `record`, `save`, `journeys`, `edit`,
1035
+ * `replay`, `forget`, and `capture` — each carrying the description, parameters, and annotations
1086
1036
  * `BROWSER_TOOL_COPY` gives, so `tools/list` answers before Chromium starts and with Chromium
1087
1037
  * absent. After the launch, a tool the toolset's manager adds under another name, such as a page
1088
1038
  * tool it adopts, is mirrored as a dispatcher with that tool's definition, and the mirror is
@@ -1119,7 +1069,7 @@ var version = "0.0.26";
1119
1069
  * ```ts
1120
1070
  * import { BrowserMCPServer } from '@orkestrel/browser/server'
1121
1071
  *
1122
- * const server = new BrowserMCPServer({ root: 'tmp/browsers', readonly: true })
1072
+ * const server = new BrowserMCPServer({ root: 'tmp/browsers', journeys: { readonly: true } })
1123
1073
  * await server.start()
1124
1074
  * await server.destroy()
1125
1075
  * ```
@@ -1182,8 +1132,8 @@ var BrowserMCPServer = class {
1182
1132
  constructor(options) {
1183
1133
  const size = options?.pool?.size ?? 1;
1184
1134
  const contexts = options?.pool?.contexts ?? 2;
1185
- if (!isInteger(size) || size < 1 || size > 3) throw new BrowserError(`pool.size must be an integer from 1 through 3`, BROWSER_SERVER_OPTIONS);
1186
- if (!isInteger(contexts) || contexts < 1 || contexts > 4) throw new BrowserError(`pool.contexts must be an integer from 1 through 4`, BROWSER_SERVER_OPTIONS);
1135
+ if (!isInteger(size) || size < 1 || size > 3) throw new BrowserError(BROWSER_SERVER_OPTIONS, `pool.size must be an integer from 1 through 3`, { subject: "server" });
1136
+ if (!isInteger(contexts) || contexts < 1 || contexts > 4) throw new BrowserError(BROWSER_SERVER_OPTIONS, `pool.contexts must be an integer from 1 through 4`, { subject: "server" });
1187
1137
  this.#admission = size * contexts;
1188
1138
  this.#log = options?.log ?? process.stderr;
1189
1139
  this.#reference = this.#issue.bind(this);
@@ -1198,11 +1148,11 @@ var BrowserMCPServer = class {
1198
1148
  restarts: 1
1199
1149
  });
1200
1150
  this.#root = resolve(options?.root ?? "tmp/browsers");
1201
- this.#headless = options?.headless ?? true;
1202
- this.#executable = options?.executable;
1203
- this.#viewport = options?.viewport;
1204
- this.#readonly = options?.readonly ?? false;
1205
- this.#launcher = options?.launch ?? createBrowser;
1151
+ this.#headless = options?.browser?.headless ?? true;
1152
+ this.#executable = options?.browser?.executable;
1153
+ this.#viewport = options?.browser?.viewport;
1154
+ this.#readonly = options?.journeys?.readonly ?? false;
1155
+ this.#launcher = options?.pool?.launch ?? createBrowser;
1206
1156
  this.#input = options?.stdio?.input ?? process.stdin;
1207
1157
  this.#tools = createToolManager();
1208
1158
  const names = [...BROWSER_TOOL_NAMES, ...BROWSER_JOURNEY_TOOL_NAMES];
@@ -1266,7 +1216,7 @@ var BrowserMCPServer = class {
1266
1216
  warming.then(this.#exhaust.bind(this));
1267
1217
  } catch (error) {
1268
1218
  if (this.#closing !== void 0) return;
1269
- if (isBrowserError(error) && error.code === "BROWSER_SERVER_UNAVAILABLE") throw error;
1219
+ if (isBrowserError(error) && error.code === "SERVER_UNAVAILABLE") throw error;
1270
1220
  throw this.#unavailable(isPoolError(error) ? error.cause ?? this.#failure : error);
1271
1221
  }
1272
1222
  }
@@ -1316,12 +1266,16 @@ var BrowserMCPServer = class {
1316
1266
  signal: context.signal,
1317
1267
  ...context.caller === void 0 ? {} : { caller: context.caller }
1318
1268
  });
1319
- if (!result.success || typeof result.value !== "string") return result;
1269
+ if (!result.success) return {
1270
+ ...result,
1271
+ error: boundBrowserText(result.error, BROWSER_TOOL_LIMIT, BROWSER_TOOL_CUT_FOOTER)
1272
+ };
1273
+ if (typeof result.value !== "string") return result;
1320
1274
  return {
1321
1275
  resultType: "complete",
1322
1276
  content: [{
1323
1277
  type: "text",
1324
- text: result.value
1278
+ text: context.call.name === "acquire" || context.call.name === "tools" ? result.value : boundBrowserText(result.value, BROWSER_TOOL_LIMIT, BROWSER_TOOL_CUT_FOOTER)
1325
1279
  }]
1326
1280
  };
1327
1281
  }
@@ -1337,8 +1291,8 @@ var BrowserMCPServer = class {
1337
1291
  context.signal.throwIfAborted();
1338
1292
  if (this.#closing !== void 0) throw this.#ended();
1339
1293
  const purpose = args["purpose"];
1340
- if (!isString(purpose) || purpose.trim().length === 0) throw new BrowserError("purpose must describe the work", "BROWSER_TOOLSET_ARGUMENT");
1341
- if (this.#holders.size >= this.#admission) throw new BrowserError(`${BROWSER_SERVER_BUSY}: ${[...this.#holders.values()].map((holder) => `${holder.id} (${holder.purpose})`).join(", ")}. Call destroy for a holder no longer needed or call the named tools to share the shared browser.`, BROWSER_SERVER_BUSY);
1294
+ if (!isString(purpose) || purpose.trim().length === 0) throw new BrowserError("ARGUMENT", "purpose must describe the work", { subject: "toolset" });
1295
+ if (this.#holders.size >= this.#admission) throw new BrowserError(BROWSER_SERVER_BUSY, `${BROWSER_SERVER_BUSY}: ${[...this.#holders.values()].map((holder) => `${holder.id} (${holder.purpose})`).join(", ")}. Call destroy for a holder no longer needed or call the named tools to share the shared browser.`);
1342
1296
  const holder = {
1343
1297
  id: randomUUID(),
1344
1298
  purpose,
@@ -1365,14 +1319,14 @@ var BrowserMCPServer = class {
1365
1319
  }
1366
1320
  #holder(value) {
1367
1321
  const holder = isString(value) ? this.#holders.get(value) : void 0;
1368
- if (holder === void 0 || holder === this.#shared || holder.abort.signal.aborted) throw new BrowserError(`${BROWSER_SERVER_HOLDER}: unknown or ended holder`, BROWSER_SERVER_HOLDER);
1322
+ if (holder === void 0 || holder === this.#shared || holder.abort.signal.aborted) throw new BrowserError(BROWSER_SERVER_HOLDER, `${BROWSER_SERVER_HOLDER}: unknown or ended holder`);
1369
1323
  return holder;
1370
1324
  }
1371
1325
  async #dispatch(args, context) {
1372
1326
  const holder = this.#holder(args["holder"]);
1373
1327
  const name = args["name"];
1374
1328
  const parameters = args["arguments"];
1375
- if (!isString(name) || !isRecord(parameters)) throw new BrowserError("execute requires name and arguments", "BROWSER_TOOLSET_ARGUMENT");
1329
+ if (!isString(name) || !isRecord(parameters)) throw new BrowserError("ARGUMENT", "execute requires name and arguments", { subject: "toolset" });
1376
1330
  return this.#forward(holder, name, parameters, context);
1377
1331
  }
1378
1332
  async #catalog(args, context) {
@@ -1389,8 +1343,8 @@ var BrowserMCPServer = class {
1389
1343
  return this.#annotate(holder, JSON.stringify(catalog));
1390
1344
  } catch (error) {
1391
1345
  context.signal.throwIfAborted();
1392
- if (holder.abort.signal.aborted) throw new BrowserError(`${BROWSER_SERVER_UNRESOLVED}: the holder ended during the call`, BROWSER_SERVER_UNRESOLVED);
1393
- if (isBrowserError(error)) throw new BrowserError(this.#annotate(holder, `${error.code}: ${error.message}`), error.code);
1346
+ if (holder.abort.signal.aborted) throw new BrowserError(BROWSER_SERVER_UNRESOLVED, `${BROWSER_SERVER_UNRESOLVED}: the holder ended during the call`);
1347
+ if (isBrowserError(error)) throw new BrowserError(error.code, this.#annotate(holder, `${error.code}: ${error.message}`));
1394
1348
  throw error;
1395
1349
  }
1396
1350
  }
@@ -1427,7 +1381,7 @@ var BrowserMCPServer = class {
1427
1381
  context.signal.throwIfAborted();
1428
1382
  const journey = args["journey"];
1429
1383
  if (name !== "replay" && name !== "forget" || !isString(journey)) return this.#perform(holder, name, args, context);
1430
- if (this.#writers.has(journey) || name === "forget" && this.#readers.has(journey)) throw new BrowserError(`BROWSER_JOURNEY_LOCKED: Journey ${journey} is locked; call ${name} again.`, "BROWSER_JOURNEY_LOCKED", { name: journey });
1384
+ if (this.#writers.has(journey) || name === "forget" && this.#readers.has(journey)) throw new BrowserError("STORE_LOCKED", `STORE_LOCKED: Journey ${journey} is locked; call ${name} again.`, { name: journey });
1431
1385
  const reader = Symbol(journey);
1432
1386
  if (name === "forget") this.#writers.add(journey);
1433
1387
  else {
@@ -1453,8 +1407,8 @@ var BrowserMCPServer = class {
1453
1407
  lease = await this.#serve(holder, signal);
1454
1408
  } catch (error) {
1455
1409
  context.signal.throwIfAborted();
1456
- if (holder !== this.#shared && holder.abort.signal.aborted) throw new BrowserError(`${BROWSER_SERVER_UNRESOLVED}: the holder ended during the call`, BROWSER_SERVER_UNRESOLVED);
1457
- if (isBrowserError(error)) throw new BrowserError(this.#annotate(holder, `${error.code}: ${error.message}`), error.code);
1410
+ if (holder !== this.#shared && holder.abort.signal.aborted) throw new BrowserError(BROWSER_SERVER_UNRESOLVED, `${BROWSER_SERVER_UNRESOLVED}: the holder ended during the call`);
1411
+ if (isBrowserError(error)) throw new BrowserError(error.code, this.#annotate(holder, `${error.code}: ${error.message}`));
1458
1412
  throw error;
1459
1413
  }
1460
1414
  let result;
@@ -1479,7 +1433,7 @@ var BrowserMCPServer = class {
1479
1433
  };
1480
1434
  }
1481
1435
  context.signal.throwIfAborted();
1482
- if (holder !== this.#shared && holder.abort.signal.aborted) throw new BrowserError(BROWSER_SERVER_UNRESOLVED + ": the holder ended during the call", BROWSER_SERVER_UNRESOLVED);
1436
+ if (holder !== this.#shared && holder.abort.signal.aborted) throw new BrowserError(BROWSER_SERVER_UNRESOLVED, BROWSER_SERVER_UNRESOLVED + ": the holder ended during the call");
1483
1437
  if (!result.success) {
1484
1438
  if (this.#leases.get(holder) === lease && !context.signal.aborted) try {
1485
1439
  await this.#race(this.#ping(lease.token.value), context.signal);
@@ -1487,9 +1441,9 @@ var BrowserMCPServer = class {
1487
1441
  context.signal.throwIfAborted();
1488
1442
  if (this.#leases.get(holder) !== lease) {
1489
1443
  const loss = this.#departures.get(lease);
1490
- throw new BrowserError(this.#annotate(holder, describeBrowserServerLoss(BROWSER_SERVER_UNRESOLVED, loss?.cause, loss?.url), lease), BROWSER_SERVER_UNRESOLVED);
1444
+ throw new BrowserError(BROWSER_SERVER_UNRESOLVED, this.#annotate(holder, describeBrowserServerLoss(BROWSER_SERVER_UNRESOLVED, loss?.cause, loss?.url), lease));
1491
1445
  }
1492
- throw new BrowserError(this.#annotate(holder, result.error, lease));
1446
+ throw new BrowserError("PROTOCOL", boundBrowserText(this.#annotate(holder, result.error, lease), BROWSER_TOOL_LIMIT, BROWSER_TOOL_CUT_FOOTER));
1493
1447
  }
1494
1448
  return typeof result.value === "string" ? this.#annotate(holder, result.value, lease) : result.value;
1495
1449
  }
@@ -1503,6 +1457,10 @@ var BrowserMCPServer = class {
1503
1457
  }
1504
1458
  return [...notes, value].join("\n");
1505
1459
  }
1460
+ #notice(context) {
1461
+ const entry = [...this.#leases].find(([, lease]) => lease.generation.context === context);
1462
+ return entry === void 0 ? "" : this.#annotate(entry[0], "");
1463
+ }
1506
1464
  async #handshake(options) {
1507
1465
  try {
1508
1466
  await this.#race(this.#starting, options.signal);
@@ -1653,7 +1611,7 @@ var BrowserMCPServer = class {
1653
1611
  if (this.#leases.get(holder) !== lease || this.#holders.get(holder.id) !== holder) return;
1654
1612
  this.#departures.set(lease, {
1655
1613
  cause,
1656
- url: lease.generation.toolset.view.url
1614
+ url: lease.generation.toolset.redact(lease.generation.toolset.view.url)
1657
1615
  });
1658
1616
  if (holder === this.#shared) this.#unmirror();
1659
1617
  this.#leases.delete(holder);
@@ -1795,6 +1753,7 @@ var BrowserMCPServer = class {
1795
1753
  }
1796
1754
  toolset = createBrowserToolset(page, {
1797
1755
  context,
1756
+ notes: this.#notice.bind(this, context),
1798
1757
  journeys: {
1799
1758
  store: createFileBrowserJourneyStore({ root: this.#root }),
1800
1759
  runs: createFileBrowserRunStore({ root: this.#root }),
@@ -1814,7 +1773,7 @@ var BrowserMCPServer = class {
1814
1773
  } catch (error) {
1815
1774
  await toolset?.destroy().catch(this.#fault.bind(this));
1816
1775
  await context?.close().catch((failure) => {
1817
- if (!this.#losses.has(slot) && slot.browser.status === "connected" && this.#closing === void 0 && !isCDPConnectionError(failure)) this.#fault(failure);
1776
+ if (!this.#losses.has(slot) && slot.browser.status === "connected" && this.#closing === void 0 && !(isBrowserError(failure) && failure.code === "DISCONNECTED")) this.#fault(failure);
1818
1777
  });
1819
1778
  await rm(directory, {
1820
1779
  recursive: true,
@@ -1824,7 +1783,8 @@ var BrowserMCPServer = class {
1824
1783
  }
1825
1784
  }
1826
1785
  async #destroyRecord(slot) {
1827
- if (isCDPTimeoutError(this.#losses.get(slot)?.cause) && slot.browser.pid !== void 0) try {
1786
+ const cause = this.#losses.get(slot)?.cause;
1787
+ if (isBrowserError(cause) && cause.code === "TIMEOUT" && slot.browser.pid !== void 0) try {
1828
1788
  process.kill(slot.browser.pid, "SIGKILL");
1829
1789
  } catch (error) {
1830
1790
  if (!isError(error) || !("code" in error) || error.code !== "ESRCH") this.#faults.add(error);
@@ -1849,7 +1809,7 @@ var BrowserMCPServer = class {
1849
1809
  }
1850
1810
  async #warm() {
1851
1811
  if (this.#closing !== void 0) throw this.#ended();
1852
- if (this.#stranded !== void 0) throw new BrowserError(isError(this.#stranded) ? this.#stranded.message : String(this.#stranded), BROWSER_SERVER_TEARDOWN, isBrowserError(this.#stranded) ? this.#stranded.context : void 0);
1812
+ if (this.#stranded !== void 0) throw new BrowserError(BROWSER_SERVER_TEARDOWN, isError(this.#stranded) ? this.#stranded.message : String(this.#stranded), isBrowserError(this.#stranded) ? this.#stranded.context : void 0);
1853
1813
  const profile = join(this.#root, ".profiles", formatBrowserLockEntry(process.pid, randomUUID()));
1854
1814
  let browser;
1855
1815
  let slot;
@@ -1922,7 +1882,7 @@ var BrowserMCPServer = class {
1922
1882
  const record = parseBrowserProfileRecord(await readFile(join(folder, BROWSER_SERVER_RECORD), "utf8"));
1923
1883
  if (record === void 0) continue;
1924
1884
  if (probeProcess(record.pid)) {
1925
- const transport = createCDPTransport({ url: record.endpoint });
1885
+ const transport = createWebSocketCDPTransport({ url: record.endpoint });
1926
1886
  const client = createCDPClient({ transport });
1927
1887
  try {
1928
1888
  this.#abort.signal.throwIfAborted();
@@ -1964,7 +1924,7 @@ var BrowserMCPServer = class {
1964
1924
  }
1965
1925
  #unavailable(cause) {
1966
1926
  const message = describeBrowserServerLoss(BROWSER_SERVER_UNAVAILABLE, cause);
1967
- return new BrowserError(message.slice(BROWSER_SERVER_UNAVAILABLE.length + 2), BROWSER_SERVER_UNAVAILABLE);
1927
+ return new BrowserError(BROWSER_SERVER_UNAVAILABLE, message.slice(BROWSER_SERVER_UNAVAILABLE.length + 2));
1968
1928
  }
1969
1929
  #issue() {
1970
1930
  return `e${++this.#references}`;
@@ -1996,7 +1956,7 @@ var BrowserMCPServer = class {
1996
1956
  tools.emitter.off("clear", mirrored.cleared);
1997
1957
  }
1998
1958
  #ended() {
1999
- return new BrowserError("the browser session ended", "BROWSER_TOOLSET_ENDED");
1959
+ return new BrowserError("CLOSED", "the browser session ended", { subject: "toolset" });
2000
1960
  }
2001
1961
  };
2002
1962
  //#endregion
@@ -2009,31 +1969,32 @@ var BrowserMCPServer = class {
2009
1969
  * const files = new FileBrowserStore({ root: directory })
2010
1970
  * files.validateName('check-ready')
2011
1971
  */
2012
- var FileBrowserStore = class {
1972
+ var FileBrowserStore = class FileBrowserStore {
1973
+ static #locks = /* @__PURE__ */ new Map();
2013
1974
  #root;
2014
1975
  #limit;
2015
1976
  constructor(options) {
2016
1977
  this.#limit = options.limit ?? 100;
2017
- if (!Number.isSafeInteger(this.#limit) || this.#limit < 1) throw new BrowserError("The listing cap must be a positive integer", "BROWSER_JOURNEY_ARGUMENT");
1978
+ if (!Number.isSafeInteger(this.#limit) || this.#limit < 1) throw new BrowserError("ARGUMENT", "The listing cap must be a positive integer", { subject: "store" });
2018
1979
  try {
2019
1980
  this.#root = realpathSync(options.root);
2020
1981
  } catch (error) {
2021
- if (error instanceof Error && "code" in error && error.code === "ENOENT") throw new BrowserError(`Missing root: ${options.root}`, "BROWSER_JOURNEY_PATH", { path: options.root });
1982
+ if (error instanceof Error && "code" in error && error.code === "ENOENT") throw new BrowserError("STORE_PATH", `Missing root: ${options.root}`, { path: options.root });
2022
1983
  throw this.translateError(options.root, error);
2023
1984
  }
2024
1985
  }
2025
1986
  /** Checks a journey name before filesystem access. @param name - Journey name */
2026
1987
  validateName(name) {
2027
- if (!BROWSER_JOURNEY_NAME_PATTERN.test(name)) throw new BrowserError(`Refused journey name: ${name}`, "BROWSER_JOURNEY_PATH");
1988
+ if (!BROWSER_JOURNEY_NAME_PATTERN.test(name)) throw new BrowserError("STORE_PATH", `Refused journey name: ${name}`);
2028
1989
  }
2029
1990
  /** Checks a run id before filesystem access. @param id - Producer id */
2030
1991
  validateId(id) {
2031
- if (!BROWSER_RUN_ID_PATTERN.test(id)) throw new BrowserError(`Refused run id: ${id}`, "BROWSER_JOURNEY_PATH");
1992
+ if (!BROWSER_RUN_ID_PATTERN.test(id)) throw new BrowserError("STORE_PATH", `Refused run id: ${id}`);
2032
1993
  }
2033
1994
  /** Resolves a confined target. @param parts - Relative components @returns Absolute target */
2034
1995
  resolvePath(...parts) {
2035
1996
  const path = resolve(this.#root, ...parts);
2036
- if (path !== this.#root && !path.startsWith(this.#root.endsWith(sep) ? this.#root : this.#root + sep)) throw new BrowserError(`Refused path: ${path}`, "BROWSER_JOURNEY_PATH");
1997
+ if (path !== this.#root && !path.startsWith(this.#root.endsWith(sep) ? this.#root : this.#root + sep)) throw new BrowserError("STORE_PATH", `Refused path: ${path}`);
2037
1998
  return path;
2038
1999
  }
2039
2000
  /**
@@ -2051,8 +2012,8 @@ var FileBrowserStore = class {
2051
2012
  try {
2052
2013
  const status = await lstat(current);
2053
2014
  options?.signal?.throwIfAborted();
2054
- if (status.isSymbolicLink()) throw new BrowserError(`Refused symbolic link: ${current}`, "BROWSER_JOURNEY_PATH");
2055
- if (index < parts.length && !status.isDirectory()) throw new BrowserError(`Not a directory: ${current}`, "BROWSER_JOURNEY_PATH");
2015
+ if (status.isSymbolicLink()) throw new BrowserError("STORE_PATH", `Refused symbolic link: ${current}`);
2016
+ if (index < parts.length && !status.isDirectory()) throw new BrowserError("STORE_PATH", `Not a directory: ${current}`);
2056
2017
  } catch (error) {
2057
2018
  options?.signal?.throwIfAborted();
2058
2019
  if (error instanceof Error && "code" in error && error.code === "ENOENT") return false;
@@ -2066,7 +2027,7 @@ var FileBrowserStore = class {
2066
2027
  /** Creates checked directories one component at a time. @param path - Directory @param options - Cancellation options */
2067
2028
  async createDirectory(path, options) {
2068
2029
  if (await this.check(path, options)) return;
2069
- if (path === this.#root) throw new BrowserError(`Missing root: ${path}`, "BROWSER_JOURNEY_PATH");
2030
+ if (path === this.#root) throw new BrowserError("STORE_PATH", `Missing root: ${path}`);
2070
2031
  await this.createDirectory(dirname(path), options);
2071
2032
  options?.signal?.throwIfAborted();
2072
2033
  try {
@@ -2116,7 +2077,7 @@ var FileBrowserStore = class {
2116
2077
  await rename(temporary, path);
2117
2078
  } catch (error) {
2118
2079
  const target = await lstat(path).catch(() => void 0);
2119
- if (error instanceof Error && "code" in error && (error.code === "EISDIR" || error.code === "EPERM") && target?.isDirectory()) throw new BrowserError(`Cannot replace directory: ${path}`, "BROWSER_JOURNEY_FILE", { path });
2080
+ if (error instanceof Error && "code" in error && (error.code === "EISDIR" || error.code === "EPERM") && target?.isDirectory()) throw new BrowserError("STORE_FILE", `Cannot replace directory: ${path}`, { path });
2120
2081
  throw error;
2121
2082
  }
2122
2083
  owned = false;
@@ -2147,6 +2108,20 @@ var FileBrowserStore = class {
2147
2108
  * @returns The mutation result
2148
2109
  */
2149
2110
  async lock(path, action, options) {
2111
+ const key = this.resolvePath(path);
2112
+ const previous = FileBrowserStore.#locks.get(key);
2113
+ const released = Promise.withResolvers();
2114
+ FileBrowserStore.#locks.set(key, released.promise);
2115
+ try {
2116
+ await previous;
2117
+ options?.signal?.throwIfAborted();
2118
+ return await this.#lockFile(key, action, options);
2119
+ } finally {
2120
+ released.resolve();
2121
+ if (FileBrowserStore.#locks.get(key) === released.promise) FileBrowserStore.#locks.delete(key);
2122
+ }
2123
+ }
2124
+ async #lockFile(path, action, options) {
2150
2125
  await this.createDirectory(dirname(path), options);
2151
2126
  const name = formatBrowserLockEntry(process.pid, randomUUID());
2152
2127
  const entry = this.resolvePath(path, name);
@@ -2189,7 +2164,7 @@ var FileBrowserStore = class {
2189
2164
  await this.#removeLock(path);
2190
2165
  }
2191
2166
  }
2192
- throw new BrowserError(`Journey is locked: ${path}`, "BROWSER_JOURNEY_LOCKED");
2167
+ throw new BrowserError("STORE_LOCKED", `Journey is locked: ${path}`);
2193
2168
  }
2194
2169
  /**
2195
2170
  * Lists readable entries in sorted order with a bounded page and one extra readable probe.
@@ -2236,7 +2211,7 @@ var FileBrowserStore = class {
2236
2211
  const reason = error instanceof BrowserError ? error.context?.["reason"] : void 0;
2237
2212
  faults.push({
2238
2213
  name,
2239
- reason: typeof reason === "string" ? reason : error instanceof BrowserError && error.code === "BROWSER_JOURNEY_PATH" ? "The entry path is refused" : error instanceof BrowserError && error.code === "BROWSER_JOURNEY_ACCESS" ? "Permission denied" : error instanceof BrowserError && error.code === "BROWSER_JOURNEY_FORMAT" ? "Unknown file format" : "The stored entry is malformed or unreadable"
2214
+ reason: typeof reason === "string" ? reason : error instanceof BrowserError && error.code === "STORE_PATH" ? "The entry path is refused" : error instanceof BrowserError && error.code === "STORE_ACCESS" ? "Permission denied" : error instanceof BrowserError && error.code === "STORE_FORMAT" ? "Unknown file format" : "The stored entry is malformed or unreadable"
2240
2215
  });
2241
2216
  }
2242
2217
  }
@@ -2279,7 +2254,7 @@ var FileBrowserStore = class {
2279
2254
  /** Translates filesystem and validation failures with their path. @param path - Failed path @param error - Cause @returns Coded error */
2280
2255
  translateError(path, error) {
2281
2256
  const code = error instanceof Error && "code" in error ? error.code : void 0;
2282
- return new BrowserError(`${path}: ${error instanceof Error ? error.message : String(error)}`, code === "EACCES" || code === "EPERM" ? "BROWSER_JOURNEY_ACCESS" : error instanceof BrowserError && code !== "BROWSER_JOURNEY_INVALID" ? error.code : "BROWSER_JOURNEY_FILE", { path });
2257
+ return new BrowserError(code === "EACCES" || code === "EPERM" ? "STORE_ACCESS" : error instanceof BrowserError && code !== "JOURNEY_INVALID" ? error.code : "STORE_FILE", `${path}: ${error instanceof Error ? error.message : String(error)}`, { path });
2283
2258
  }
2284
2259
  async #readLock(path, options) {
2285
2260
  await this.check(path, options);
@@ -2287,13 +2262,13 @@ var FileBrowserStore = class {
2287
2262
  const entries = await readdir(path, { withFileTypes: true });
2288
2263
  for (const entry of entries) {
2289
2264
  await this.check(this.resolvePath(path, entry.name), options);
2290
- if (!entry.isFile()) throw new BrowserError(`Journey is locked: ${path}`, "BROWSER_JOURNEY_LOCKED");
2265
+ if (!entry.isFile()) throw new BrowserError("STORE_LOCKED", `Journey is locked: ${path}`);
2291
2266
  }
2292
2267
  return entries.map((entry) => entry.name);
2293
2268
  } catch (error) {
2294
2269
  if (error instanceof Error && "code" in error) {
2295
2270
  if (error.code === "ENOENT") return void 0;
2296
- if (error.code === "ENOTDIR") throw new BrowserError(`Journey is locked: ${path}`, "BROWSER_JOURNEY_LOCKED");
2271
+ if (error.code === "ENOTDIR") throw new BrowserError("STORE_LOCKED", `Journey is locked: ${path}`);
2297
2272
  }
2298
2273
  throw this.translateError(path, error);
2299
2274
  }
@@ -2305,7 +2280,7 @@ var FileBrowserStore = class {
2305
2280
  return true;
2306
2281
  } catch (error) {
2307
2282
  if (error instanceof Error && "code" in error && (error.code === "ENOENT" || error.code === "ENOTEMPTY")) return false;
2308
- throw new BrowserError(`Cannot remove lock entry: ${path}`, "BROWSER_JOURNEY_ACCESS", { path });
2283
+ throw new BrowserError("STORE_ACCESS", `Cannot remove lock entry: ${path}`, { path });
2309
2284
  }
2310
2285
  }
2311
2286
  async #removeLock(path) {
@@ -2321,7 +2296,7 @@ var FileBrowserStore = class {
2321
2296
  if (cause instanceof Error && "code" in cause && cause.code === "ENOENT") return;
2322
2297
  }
2323
2298
  }
2324
- throw new BrowserError(`Cannot remove lock directory: ${path}`, "BROWSER_JOURNEY_ACCESS", { path });
2299
+ throw new BrowserError("STORE_ACCESS", `Cannot remove lock directory: ${path}`, { path });
2325
2300
  }
2326
2301
  }
2327
2302
  };
@@ -2347,24 +2322,29 @@ var FileBrowserJourneyStore = class {
2347
2322
  if (source === void 0) return void 0;
2348
2323
  try {
2349
2324
  const value = parseJSON(source);
2350
- if (!isRecord(value) || typeof value["revision"] !== "number" || !Number.isSafeInteger(value["revision"]) || value["revision"] < 1) throw new BrowserError("Malformed journey revision", "BROWSER_JOURNEY_FILE");
2351
- if (!isRecord(value["journey"]) || !("format" in value["journey"])) throw new BrowserError("Missing journey format", "BROWSER_JOURNEY_FILE");
2325
+ if (!isRecord(value) || typeof value["revision"] !== "number" || !Number.isSafeInteger(value["revision"]) || value["revision"] < 1) throw new BrowserError("STORE_FILE", "Malformed journey revision");
2326
+ if (!isRecord(value["journey"]) || !("format" in value["journey"])) throw new BrowserError("STORE_FILE", "Missing journey format");
2352
2327
  validateBrowserJourney(value["journey"]);
2353
- if (value["journey"].name !== name) throw new BrowserError("Journey name differs from its directory", "BROWSER_JOURNEY_FILE");
2328
+ if (value["journey"].name !== name) throw new BrowserError("STORE_FILE", "Journey name differs from its directory");
2354
2329
  return {
2355
2330
  journey: value["journey"],
2356
2331
  revision: value["revision"]
2357
2332
  };
2358
2333
  } catch (error) {
2359
2334
  const translated = this.#files.translateError(path, error);
2360
- throw new BrowserError(translated.message, translated.code, {
2335
+ throw new BrowserError(translated.code, translated.message, {
2361
2336
  ...translated.context,
2362
2337
  reason: normalizeBrowserJourneyReason(error)
2363
2338
  });
2364
2339
  }
2365
2340
  }
2366
- async set(journey, expected, options) {
2341
+ async set(journey, options) {
2367
2342
  options?.signal?.throwIfAborted();
2343
+ validateBrowserJourneyWriteOptions(options);
2344
+ const condition = {
2345
+ revision: options?.revision,
2346
+ exclusive: options?.exclusive
2347
+ };
2368
2348
  this.#files.validateName(journey.name);
2369
2349
  validateBrowserJourney(journey);
2370
2350
  const owned = structuredClone(journey);
@@ -2372,12 +2352,12 @@ var FileBrowserJourneyStore = class {
2372
2352
  const counter = this.#files.resolvePath(owned.name, BROWSER_JOURNEY_REVISION_FILE);
2373
2353
  return this.#files.lock(this.#files.resolvePath(owned.name, BROWSER_JOURNEY_LOCK_DIRECTORY), async () => {
2374
2354
  const current = await this.get(owned.name, options);
2375
- if (expected !== void 0 && expected !== (current?.revision ?? 0)) throw new BrowserError(`Journey ${owned.name} changed since you read it`, "BROWSER_JOURNEY_STALE");
2355
+ if (condition.exclusive === true && current !== void 0 || condition.revision !== void 0 && condition.revision !== current?.revision) throw new BrowserError("JOURNEY_STALE", `Journey ${owned.name} changed since you read it`);
2376
2356
  const source = await this.#files.read(counter, options);
2377
2357
  const previous = source === void 0 ? 0 : Number(source);
2378
- if (!Number.isSafeInteger(previous) || previous < 0 || source?.trim() === "") throw new BrowserError(`Malformed revision: ${counter}`, "BROWSER_JOURNEY_FILE");
2358
+ if (!Number.isSafeInteger(previous) || previous < 0 || source?.trim() === "") throw new BrowserError("STORE_FILE", `Malformed revision: ${counter}`);
2379
2359
  const revision = Math.max(previous, current?.revision ?? 0) + 1;
2380
- if (!Number.isSafeInteger(revision)) throw new BrowserError(`Exhausted revision: ${counter}`, "BROWSER_JOURNEY_FILE");
2360
+ if (!Number.isSafeInteger(revision)) throw new BrowserError("STORE_FILE", `Exhausted revision: ${counter}`);
2381
2361
  const saved = {
2382
2362
  journey: owned,
2383
2363
  revision
@@ -2402,7 +2382,7 @@ var FileBrowserJourneyStore = class {
2402
2382
  const counter = this.#files.resolvePath(name, BROWSER_JOURNEY_REVISION_FILE);
2403
2383
  const source = await this.#files.read(counter, options);
2404
2384
  const previous = source === void 0 ? 0 : Number(source);
2405
- if (!Number.isSafeInteger(previous) || previous < 0 || source?.trim() === "") throw new BrowserError(`Malformed revision: ${counter}`, "BROWSER_JOURNEY_FILE");
2385
+ if (!Number.isSafeInteger(previous) || previous < 0 || source?.trim() === "") throw new BrowserError("STORE_FILE", `Malformed revision: ${counter}`);
2406
2386
  await this.#files.write(counter, String(Math.max(previous, current.revision ?? 0)), options);
2407
2387
  await this.#files.remove(this.#files.resolvePath(name, BROWSER_JOURNEY_SNAPSHOT_FILE), options);
2408
2388
  }, options);
@@ -2424,7 +2404,7 @@ var FileBrowserJourneyStore = class {
2424
2404
  * still refuses conflicting mutations and allocations from another process.
2425
2405
  * @example
2426
2406
  * const store = new FileBrowserRunStore({ root: directory })
2427
- * const slot = await store.open('check-ready')
2407
+ * const slot = await store.create('check-ready')
2428
2408
  */
2429
2409
  var FileBrowserRunStore = class FileBrowserRunStore {
2430
2410
  static #allocations = /* @__PURE__ */ new Map();
@@ -2434,7 +2414,7 @@ var FileBrowserRunStore = class FileBrowserRunStore {
2434
2414
  constructor(options) {
2435
2415
  this.#files = new FileBrowserStore(options);
2436
2416
  }
2437
- async snapshot(bytes, options) {
2417
+ async write(bytes, options) {
2438
2418
  options?.signal?.throwIfAborted();
2439
2419
  const owned = new Uint8Array(bytes);
2440
2420
  const directory = await this.#files.allocate(this.#files.resolvePath(BROWSER_RUN_DIRECTORY), generateBrowserRunId, options);
@@ -2452,7 +2432,7 @@ var FileBrowserRunStore = class FileBrowserRunStore {
2452
2432
  throw this.#files.translateError(path, error);
2453
2433
  }
2454
2434
  }
2455
- async open(name, options) {
2435
+ async create(name, options) {
2456
2436
  options?.signal?.throwIfAborted();
2457
2437
  this.#files.validateName(name);
2458
2438
  const path = this.#files.resolvePath(name, BROWSER_JOURNEY_LOCK_DIRECTORY);
@@ -2486,10 +2466,10 @@ var FileBrowserRunStore = class FileBrowserRunStore {
2486
2466
  if (source === void 0) return void 0;
2487
2467
  try {
2488
2468
  const value = parseJSON(source);
2489
- if (!isRecord(value) || !("format" in value)) throw new BrowserError("Missing run format", "BROWSER_JOURNEY_FILE");
2490
- if (value["format"] === BROWSER_JOURNEY_FORMAT_VERSION && (!isRecord(value["journey"]) || !("format" in value["journey"]))) throw new BrowserError("Missing run journey format", "BROWSER_JOURNEY_FILE");
2469
+ if (!isRecord(value) || !("format" in value)) throw new BrowserError("STORE_FILE", "Missing run format");
2470
+ if (value["format"] === BROWSER_JOURNEY_FORMAT_VERSION && (!isRecord(value["journey"]) || !("format" in value["journey"]))) throw new BrowserError("STORE_FILE", "Missing run journey format");
2491
2471
  validateBrowserRun(value);
2492
- if (value.id !== id || value.journey.name !== name) throw new BrowserError("Run identity differs from its directory", "BROWSER_JOURNEY_FILE");
2472
+ if (value.id !== id || value.journey.name !== name) throw new BrowserError("STORE_FILE", "Run identity differs from its directory");
2493
2473
  return value;
2494
2474
  } catch (error) {
2495
2475
  throw this.#files.translateError(path, error);
@@ -2508,7 +2488,7 @@ var FileBrowserRunStore = class FileBrowserRunStore {
2508
2488
  async capture(slot, name, bytes, options) {
2509
2489
  options?.signal?.throwIfAborted();
2510
2490
  const directory = this.#slots.get(slot);
2511
- if (directory === void 0 || !/^s[1-9]\d*\.png$/.test(name)) throw new BrowserError("Capture requires an opened slot and an sN.png name", "BROWSER_JOURNEY_PATH");
2491
+ if (directory === void 0 || !/^s[1-9]\d*\.png$/.test(name)) throw new BrowserError("STORE_PATH", "Capture requires an opened slot and an sN.png name");
2512
2492
  const owned = new Uint8Array(bytes);
2513
2493
  await this.#requireDirectory(directory, options);
2514
2494
  await this.#files.write(this.#files.resolvePath(directory, name), owned, options);
@@ -2561,12 +2541,12 @@ var FileBrowserRunStore = class FileBrowserRunStore {
2561
2541
  return this.#files.list(this.#files.resolvePath(name, BROWSER_RUN_DIRECTORY), (id) => this.get(name, id, options), options);
2562
2542
  }
2563
2543
  async #requireDirectory(directory, options) {
2564
- if (!this.#directories.has(directory) || !await this.#files.check(directory, options)) throw new BrowserError(`Run directory was not opened or is missing: ${directory}`, "BROWSER_JOURNEY_PATH");
2544
+ if (!this.#directories.has(directory) || !await this.#files.check(directory, options)) throw new BrowserError("STORE_PATH", `Run directory was not opened or is missing: ${directory}`);
2565
2545
  try {
2566
2546
  options?.signal?.throwIfAborted();
2567
2547
  const status = await lstat(directory);
2568
2548
  options?.signal?.throwIfAborted();
2569
- if (!status.isDirectory()) throw new BrowserError(`Run directory is not a directory: ${directory}`, "BROWSER_JOURNEY_PATH");
2549
+ if (!status.isDirectory()) throw new BrowserError("STORE_PATH", `Run directory is not a directory: ${directory}`);
2570
2550
  } catch (error) {
2571
2551
  options?.signal?.throwIfAborted();
2572
2552
  throw this.#files.translateError(directory, error);
@@ -2603,7 +2583,7 @@ function createBrowser(options) {
2603
2583
  * @param options - The CDP WebSocket debugger URL (and optional timeout)
2604
2584
  * @returns A {@link CDPTransportInterface}
2605
2585
  */
2606
- function createCDPTransport(options) {
2586
+ function createWebSocketCDPTransport(options) {
2607
2587
  return new WebSocketCDPTransport(options);
2608
2588
  }
2609
2589
  /**
@@ -2612,7 +2592,7 @@ function createCDPTransport(options) {
2612
2592
  *
2613
2593
  * @returns A {@link BrowserWriterInterface} that persists bytes through `node:fs/promises`
2614
2594
  */
2615
- function createBrowserWriter() {
2595
+ function createFileBrowserWriter() {
2616
2596
  return new FileBrowserWriter();
2617
2597
  }
2618
2598
  /**
@@ -2646,7 +2626,7 @@ function createFileBrowserRunStore(options) {
2646
2626
  * ```ts
2647
2627
  * import { createBrowserMCPServer } from '@orkestrel/browser/server'
2648
2628
  *
2649
- * const server = createBrowserMCPServer({ root: 'tmp/browsers', headless: true })
2629
+ * const server = createBrowserMCPServer({ root: 'tmp/browsers', browser: { headless: true } })
2650
2630
  * await server.start() // resolves after leasing a connected browser
2651
2631
  * await server.destroy()
2652
2632
  * ```
@@ -2701,7 +2681,7 @@ var Browser = class {
2701
2681
  ...options?.error !== void 0 ? { error: options.error } : {}
2702
2682
  });
2703
2683
  this.#options = options ?? {};
2704
- this.#engine = this.#options.engine ?? (this.#options.executable !== void 0 ? parseBrowserEngine(this.#options.executable) ?? "chromium" : "chromium");
2684
+ this.#engine = this.#options.executable !== void 0 ? parseBrowserEngine(this.#options.executable) ?? "chromium" : this.#options.browsers?.engine ?? "chromium";
2705
2685
  this.#cdpPort = this.#options.cdp?.port ?? 9222;
2706
2686
  this.#cdpHost = this.#options.cdp?.host ?? "127.0.0.1";
2707
2687
  queueMicrotask(() => this.#emitter.emit("idle"));
@@ -2728,9 +2708,9 @@ var Browser = class {
2728
2708
  return this.#endpoint;
2729
2709
  }
2730
2710
  async ping(options) {
2731
- if (this.#destroyed) throw new BrowserDestroyedError();
2711
+ if (this.#destroyed) throw new BrowserError("CLOSED", "Browser has been destroyed", { subject: "browser" });
2732
2712
  const client = this.#client;
2733
- if (this.#status !== "connected" || client === void 0) throw new BrowserNotConnectedError();
2713
+ if (this.#status !== "connected" || client === void 0) throw new BrowserError("DISCONNECTED", "Browser is not connected");
2734
2714
  await client.send("Browser.getVersion", void 0, options);
2735
2715
  }
2736
2716
  async discover() {
@@ -2739,7 +2719,7 @@ var Browser = class {
2739
2719
  return result;
2740
2720
  }
2741
2721
  async connect() {
2742
- if (this.#destroyed) throw new BrowserDestroyedError();
2722
+ if (this.#destroyed) throw new BrowserError("CLOSED", "Browser has been destroyed", { subject: "browser" });
2743
2723
  const active = this.#connecting.pending;
2744
2724
  if (active !== void 0) {
2745
2725
  await active;
@@ -2749,8 +2729,8 @@ var Browser = class {
2749
2729
  await this.#connecting.execute(() => this.#establish());
2750
2730
  }
2751
2731
  adopt() {
2752
- if (this.#destroyed) throw new BrowserDestroyedError();
2753
- if (this.#status !== "connected" || this.#client === void 0 || this.#endpoint === void 0) throw new BrowserNotConnectedError();
2732
+ if (this.#destroyed) throw new BrowserError("CLOSED", "Browser has been destroyed", { subject: "browser" });
2733
+ if (this.#status !== "connected" || this.#client === void 0 || this.#endpoint === void 0) throw new BrowserError("DISCONNECTED", "Browser is not connected");
2754
2734
  this.#owned = true;
2755
2735
  }
2756
2736
  async disconnect() {
@@ -2781,12 +2761,15 @@ var Browser = class {
2781
2761
  }
2782
2762
  }
2783
2763
  async create(options) {
2784
- if (this.#destroyed) throw new BrowserDestroyedError();
2764
+ if (this.#destroyed) throw new BrowserError("CLOSED", "Browser has been destroyed", { subject: "browser" });
2785
2765
  const client = this.#client;
2786
- if (this.#status !== "connected" || client === void 0) throw new BrowserNotConnectedError();
2766
+ if (this.#status !== "connected" || client === void 0) throw new BrowserError("DISCONNECTED", "Browser is not connected");
2787
2767
  let context = this.#contexts[0];
2788
2768
  if (context === void 0) {
2789
- context = new BrowserContext(client, void 0, this.#options.viewport, createBrowserWriter());
2769
+ context = new BrowserContext(client, {
2770
+ ...this.#options.viewport === void 0 ? {} : { viewport: this.#options.viewport },
2771
+ writer: createFileBrowserWriter()
2772
+ });
2790
2773
  this.#registerContext(context);
2791
2774
  }
2792
2775
  const page = await context.create(options);
@@ -2814,10 +2797,11 @@ var Browser = class {
2814
2797
  return shutdown;
2815
2798
  }
2816
2799
  async #isolate(options) {
2817
- if (this.#destroyed) throw new BrowserDestroyedError();
2800
+ if (this.#destroyed) throw new BrowserError("CLOSED", "Browser has been destroyed", { subject: "browser" });
2818
2801
  const client = this.#client;
2819
- if (this.#status !== "connected" || client === void 0) throw new BrowserNotConnectedError();
2802
+ if (this.#status !== "connected" || client === void 0) throw new BrowserError("DISCONNECTED", "Browser is not connected");
2820
2803
  validateBrowserContextOptions(options);
2804
+ if (options !== void 0 && "id" in options && options.id !== void 0) throw new BrowserError("ARGUMENT", "An isolated context receives its id from the browser");
2821
2805
  const params = { disposeOnDetach: false };
2822
2806
  if (options?.proxy !== void 0) {
2823
2807
  params["proxyServer"] = options.proxy.server;
@@ -2825,18 +2809,25 @@ var Browser = class {
2825
2809
  }
2826
2810
  if (options?.origins !== void 0) params["originsWithUniversalNetworkAccess"] = [...options.origins];
2827
2811
  const result = await client.send("Target.createBrowserContext", params);
2828
- if (!isRecord(result) || !isString(result["browserContextId"])) throw new BrowserConnectionError("Failed to create isolated browser context");
2812
+ if (!isRecord(result) || !isString(result["browserContextId"])) throw new BrowserError("CONNECTION", "Failed to create isolated browser context");
2829
2813
  const id = result["browserContextId"];
2830
- const context = new BrowserContext(client, id, options?.emulation?.viewport ?? this.#options.viewport, createBrowserWriter(), options?.emulation, options?.downloads, options);
2814
+ const { proxy: _proxy, origins: _origins, ...defaults } = options ?? {};
2815
+ const viewport = options?.viewport ?? options?.emulation?.viewport ?? this.#options.viewport;
2816
+ const context = new BrowserContext(client, {
2817
+ ...defaults,
2818
+ id,
2819
+ ...viewport === void 0 ? {} : { viewport },
2820
+ writer: options?.writer ?? createFileBrowserWriter()
2821
+ });
2831
2822
  try {
2832
- if (this.#destroyed || this.#client !== client) throw new BrowserDestroyedError();
2823
+ if (this.#destroyed || this.#client !== client) throw new BrowserError("CLOSED", "Browser has been destroyed", { subject: "browser" });
2833
2824
  if (options?.downloads !== void 0) await client.send("Browser.setDownloadBehavior", {
2834
2825
  behavior: options.downloads.named === true ? "allowAndName" : "allow",
2835
2826
  browserContextId: id,
2836
2827
  downloadPath: options.downloads.path,
2837
2828
  eventsEnabled: true
2838
2829
  });
2839
- if (this.#destroyed || this.#client !== client) throw new BrowserDestroyedError();
2830
+ if (this.#destroyed || this.#client !== client) throw new BrowserError("CLOSED", "Browser has been destroyed", { subject: "browser" });
2840
2831
  } catch (error) {
2841
2832
  await context.close().catch(() => void 0);
2842
2833
  throw error;
@@ -2848,7 +2839,7 @@ var Browser = class {
2848
2839
  const disconnecting = this.#disconnecting.pending;
2849
2840
  if (disconnecting !== void 0) await disconnecting;
2850
2841
  await this.#settleExit();
2851
- if (this.#destroyed) throw new BrowserDestroyedError();
2842
+ if (this.#destroyed) throw new BrowserError("CLOSED", "Browser has been destroyed", { subject: "browser" });
2852
2843
  if (this.#status === "connected") return;
2853
2844
  this.#assertNotAborted();
2854
2845
  this.#status = "connecting";
@@ -2873,12 +2864,12 @@ var Browser = class {
2873
2864
  this.#assertNotAborted();
2874
2865
  await this.#launch();
2875
2866
  } catch (error) {
2876
- if (this.#destroyed) throw new BrowserDestroyedError();
2867
+ if (this.#destroyed) throw new BrowserError("CLOSED", "Browser has been destroyed", { subject: "browser" });
2877
2868
  this.#status = "error";
2878
2869
  this.#emitter.emit("error", error);
2879
- if (isBrowserConnectionError(error)) throw error;
2870
+ if (isBrowserError(error) && error.code === "CONNECTION") throw error;
2880
2871
  const message = isError(error) ? error.message : String(error);
2881
- throw new BrowserConnectionError(message, { executable: this.#options.executable });
2872
+ throw new BrowserError("CONNECTION", message, this.#options.executable === void 0 ? void 0 : { executable: this.#options.executable });
2882
2873
  }
2883
2874
  }
2884
2875
  async #detach() {
@@ -2899,7 +2890,7 @@ var Browser = class {
2899
2890
  this.#emitter.emit("idle");
2900
2891
  }
2901
2892
  #assertNotAborted() {
2902
- if (this.#signal().aborted) throw new BrowserConnectionError("Connection aborted");
2893
+ if (this.#signal().aborted) throw new BrowserError("CONNECTION", "Connection aborted");
2903
2894
  }
2904
2895
  #signal() {
2905
2896
  const external = this.#options.signal;
@@ -2907,10 +2898,10 @@ var Browser = class {
2907
2898
  }
2908
2899
  async #raceAbort(promise) {
2909
2900
  const signal = this.#signal();
2910
- if (signal.aborted) throw new BrowserConnectionError("Connection aborted");
2901
+ if (signal.aborted) throw new BrowserError("CONNECTION", "Connection aborted");
2911
2902
  const aborted = Promise.withResolvers();
2912
2903
  const listener = addAbortListener(signal, () => {
2913
- aborted.reject(new BrowserConnectionError("Connection aborted"));
2904
+ aborted.reject(new BrowserError("CONNECTION", "Connection aborted"));
2914
2905
  });
2915
2906
  try {
2916
2907
  return await Promise.race([promise, aborted.promise]);
@@ -2938,7 +2929,7 @@ var Browser = class {
2938
2929
  this.#owned = void 0;
2939
2930
  this.#endpoint = void 0;
2940
2931
  if (connected) this.#status = "disconnected";
2941
- this.#emitter.emit("error", new BrowserConnectionError("The browser process exited unexpectedly", { cause: BROWSER_PROCESS_EXIT_CAUSE }));
2932
+ this.#emitter.emit("error", new BrowserError("CONNECTION", "The browser process exited unexpectedly", { cause: BROWSER_PROCESS_EXIT_CAUSE }));
2942
2933
  if (connected) {
2943
2934
  this.#emitter.emit("disconnect");
2944
2935
  this.#emitter.emit("idle");
@@ -2976,7 +2967,7 @@ var Browser = class {
2976
2967
  }
2977
2968
  this.#destroyContextList(contexts);
2978
2969
  this.#closeClient(client);
2979
- this.#emitter.emit("error", new BrowserConnectionError("The CDP transport connection was lost", { cause: BROWSER_TRANSPORT_LOSS_CAUSE }));
2970
+ this.#emitter.emit("error", new BrowserError("CONNECTION", "The CDP transport connection was lost", { cause: BROWSER_TRANSPORT_LOSS_CAUSE }));
2980
2971
  this.#emitter.emit("disconnect");
2981
2972
  this.#emitter.emit("idle");
2982
2973
  }
@@ -3029,7 +3020,7 @@ var Browser = class {
3029
3020
  };
3030
3021
  }
3031
3022
  async #assertPortFree() {
3032
- if (await this.#probePort()) throw new BrowserConnectionError(`Port ${this.#cdpPort} on ${this.#cdpHost} is already occupied by another CDP endpoint`, {
3023
+ if (await this.#probePort()) throw new BrowserError("CONNECTION", `Port ${this.#cdpPort} on ${this.#cdpHost} is already occupied by another CDP endpoint`, {
3033
3024
  port: this.#cdpPort,
3034
3025
  host: this.#cdpHost
3035
3026
  });
@@ -3043,7 +3034,7 @@ var Browser = class {
3043
3034
  }
3044
3035
  }
3045
3036
  async #connectCDP(endpoint) {
3046
- const transport = createCDPTransport({
3037
+ const transport = createWebSocketCDPTransport({
3047
3038
  url: endpoint,
3048
3039
  timeout: this.#timeout()
3049
3040
  });
@@ -3076,20 +3067,20 @@ var Browser = class {
3076
3067
  }
3077
3068
  }
3078
3069
  async #launch() {
3079
- if (this.#process !== void 0) throw new BrowserConnectionError("A browser process is already active on this instance");
3080
- const requestedEngine = this.#options.engine ?? this.#options.browsers?.engine;
3070
+ if (this.#process !== void 0) throw new BrowserError("CONNECTION", "A browser process is already active on this instance");
3071
+ const requestedEngine = this.#options.browsers?.engine;
3081
3072
  let executable = this.#options.executable;
3082
3073
  let resolvedEngine;
3083
3074
  if (executable !== void 0) resolvedEngine = parseBrowserEngine(executable) ?? "chromium";
3084
3075
  else {
3085
- const found = findSystemBrowser({
3076
+ const found = findSystemBrowsers({
3086
3077
  ...this.#options.browsers,
3087
3078
  ...requestedEngine !== void 0 ? { engine: requestedEngine } : {}
3088
- });
3079
+ })[0];
3089
3080
  executable = found?.executable;
3090
3081
  resolvedEngine = found?.engine;
3091
3082
  }
3092
- if (executable === void 0) throw new BrowserConnectionError("No Chromium browser found. Install Chrome, Edge, or Chromium.", requestedEngine === void 0 ? void 0 : { engine: requestedEngine });
3083
+ if (executable === void 0) throw new BrowserError("CONNECTION", "No Chromium browser found. Install Chrome, Edge, or Chromium.", requestedEngine === void 0 ? void 0 : { engine: requestedEngine });
3093
3084
  this.#engine = resolvedEngine ?? "chromium";
3094
3085
  await this.#releaseProfile();
3095
3086
  const profile = await createBrowserProfile(this.#options.profile);
@@ -3105,7 +3096,7 @@ var Browser = class {
3105
3096
  let client;
3106
3097
  try {
3107
3098
  const endpoint = await this.#waitForLaunch(process, executable, this.#options.args);
3108
- const transport = createCDPTransport({
3099
+ const transport = createWebSocketCDPTransport({
3109
3100
  url: endpoint,
3110
3101
  timeout: this.#timeout()
3111
3102
  });
@@ -3169,9 +3160,9 @@ var Browser = class {
3169
3160
  }
3170
3161
  if (pid === void 0) {
3171
3162
  await this.#closeRemote(client);
3172
- throw new BrowserConnectionError("The browser launcher exited without naming the process serving its CDP endpoint", {
3173
- executable: this.#options.executable,
3174
- pid: process.pid
3163
+ throw new BrowserError("CONNECTION", "The browser launcher exited without naming the process serving its CDP endpoint", {
3164
+ ...this.#options.executable === void 0 ? {} : { executable: this.#options.executable },
3165
+ ...process.pid === void 0 ? {} : { pid: process.pid }
3175
3166
  });
3176
3167
  }
3177
3168
  this.#servingPid = pid;
@@ -3179,7 +3170,7 @@ var Browser = class {
3179
3170
  async #waitForLaunch(process, executable, args) {
3180
3171
  const context = {
3181
3172
  executable,
3182
- args
3173
+ ...args === void 0 ? {} : { args }
3183
3174
  };
3184
3175
  const timeout = this.#timeout();
3185
3176
  const controller = new AbortController();
@@ -3190,32 +3181,32 @@ var Browser = class {
3190
3181
  deadline
3191
3182
  ]);
3192
3183
  const stderr = process.stderr;
3193
- if (stderr === null) throw new BrowserConnectionError("The browser process has no standard error to read", context);
3184
+ if (stderr === null) throw new BrowserError("CONNECTION", "The browser process has no standard error to read", context);
3194
3185
  const exit = once(process, "exit", { signal });
3195
3186
  exit.catch(() => void 0);
3196
3187
  const ready = readBrowserEndpoint(stderr, signal).catch(async (error) => {
3197
3188
  if (signal.aborted) throw error;
3198
3189
  const values = await exit;
3199
3190
  if (values[0] === 0 && values[1] === null) throw error;
3200
- throw new BrowserConnectionError(this.#formatLaunchExit(isInteger(values[0]) ? values[0] : null, isString(values[1]) ? values[1] : null), context);
3191
+ throw new BrowserError("CONNECTION", this.#formatLaunchExit(isInteger(values[0]) ? values[0] : null, isString(values[1]) ? values[1] : null), context);
3201
3192
  });
3202
3193
  const exited = exit.then((values) => {
3203
3194
  const code = isInteger(values[0]) ? values[0] : null;
3204
3195
  const exitSignal = isString(values[1]) ? values[1] : null;
3205
3196
  if (code === 0 && exitSignal === null) return ready;
3206
- throw new BrowserConnectionError(this.#formatLaunchExit(code, exitSignal), context);
3197
+ throw new BrowserError("CONNECTION", this.#formatLaunchExit(code, exitSignal), context);
3207
3198
  });
3208
3199
  try {
3209
3200
  return await Promise.race([ready, exited]);
3210
3201
  } catch (error) {
3211
- if (isBrowserConnectionError(error)) throw error;
3212
- if (this.#signal().aborted) throw new BrowserConnectionError("Connection aborted", context);
3213
- if (deadline.aborted) throw new BrowserConnectionError(`Browser did not report a CDP endpoint within ${timeout}ms`, {
3202
+ if (isBrowserError(error) && error.code === "CONNECTION") throw error;
3203
+ if (this.#signal().aborted) throw new BrowserError("CONNECTION", "Connection aborted", context);
3204
+ if (deadline.aborted) throw new BrowserError("CONNECTION", `Browser did not report a CDP endpoint within ${timeout}ms`, {
3214
3205
  ...context,
3215
3206
  timeout
3216
3207
  });
3217
3208
  const message = isError(error) ? error.message : String(error);
3218
- throw new BrowserConnectionError(message, context);
3209
+ throw new BrowserError("CONNECTION", message, context);
3219
3210
  } finally {
3220
3211
  controller.abort();
3221
3212
  ready.catch(() => void 0);
@@ -3252,7 +3243,10 @@ var Browser = class {
3252
3243
  });
3253
3244
  }
3254
3245
  if (pages.length === 0 || this.#contexts.length > 0) return;
3255
- const context = new BrowserContext(client, void 0, this.#options.viewport, createBrowserWriter());
3246
+ const context = new BrowserContext(client, {
3247
+ ...this.#options.viewport === void 0 ? {} : { viewport: this.#options.viewport },
3248
+ writer: createFileBrowserWriter()
3249
+ });
3256
3250
  await context.sync(pages);
3257
3251
  this.#registerContext(context);
3258
3252
  }
@@ -3332,7 +3326,7 @@ var Browser = class {
3332
3326
  let remote = client;
3333
3327
  let temporary = false;
3334
3328
  if (remote === void 0 && this.#owned === true && this.#endpoint !== void 0) {
3335
- const transport = createCDPTransport({
3329
+ const transport = createWebSocketCDPTransport({
3336
3330
  url: this.#endpoint,
3337
3331
  timeout: this.#timeout()
3338
3332
  });
@@ -3435,10 +3429,11 @@ var Browser = class {
3435
3429
  const code = isError(error) && "code" in error && isString(error.code) ? error.code : void 0;
3436
3430
  if (code === "ESRCH") return false;
3437
3431
  if (signal === 0) return code === "EPERM" ? true : void 0;
3438
- throw new BrowserConnectionError("Failed to signal the browser process", {
3439
- pid: serving ?? pid,
3432
+ const target = serving ?? pid;
3433
+ throw new BrowserError("CONNECTION", "Failed to signal the browser process", {
3434
+ ...target === void 0 ? {} : { pid: target },
3440
3435
  signal,
3441
- cause: error
3436
+ cause: isJSONValue(error) ? error : String(error)
3442
3437
  });
3443
3438
  }
3444
3439
  }
@@ -3477,7 +3472,8 @@ var Browser = class {
3477
3472
  if (await this.#waitForTerminationWithin(process, 3e3)) return;
3478
3473
  if (this.#signalProcess(process, "SIGKILL") === false) return;
3479
3474
  if (await this.#waitForTerminationWithin(process, 3e3, true)) return;
3480
- throw new BrowserConnectionError("Browser process did not exit after SIGKILL", { pid: this.#servingPid ?? process.pid });
3475
+ const target = this.#servingPid ?? process.pid;
3476
+ throw new BrowserError("CONNECTION", "Browser process did not exit after SIGKILL", { ...target === void 0 ? {} : { pid: target } });
3481
3477
  } finally {
3482
3478
  await this.#closeProcessPipe(process);
3483
3479
  }
@@ -3506,6 +3502,6 @@ var Browser = class {
3506
3502
  }
3507
3503
  };
3508
3504
  //#endregion
3509
- export { BROWSER_CDP_LIST_PATH, BROWSER_CDP_PROTOCOL, BROWSER_CDP_VERSION_PATH, BROWSER_DEFAULT_CDP_PORT, BROWSER_DEFAULT_HOST, BROWSER_DEVTOOLS_PATTERN, BROWSER_DRAIN_INTERVAL_MS, BROWSER_ENGINE_HINTS, BROWSER_ENV_PATH_KEYS, BROWSER_EXECUTABLE_NAMES, BROWSER_EXECUTABLE_PATHS, BROWSER_FILE_STORE_LIMIT, BROWSER_HEADLESS_ARG, BROWSER_JOURNEY_LOCK_ATTEMPTS, BROWSER_JOURNEY_LOCK_DIRECTORY, BROWSER_JOURNEY_REVISION_FILE, BROWSER_JOURNEY_SNAPSHOT_FILE, BROWSER_KILL_GRACE_MS, BROWSER_LAUNCH_ARGS, BROWSER_PORT_PROBE_TIMEOUT_MS, BROWSER_PROCESS_EXIT_CAUSE, BROWSER_PROFILE_PREFIX, BROWSER_RUN_DIRECTORY, BROWSER_RUN_FILE, BROWSER_SERVER_BUSY, BROWSER_SERVER_CONTEXTS, BROWSER_SERVER_CONTEXTS_LIMIT, BROWSER_SERVER_COPY, BROWSER_SERVER_CRASH, BROWSER_SERVER_EXHAUSTED, BROWSER_SERVER_HOLDER, BROWSER_SERVER_LAUNCH, BROWSER_SERVER_OPTIONS, BROWSER_SERVER_POOL_LIMIT, BROWSER_SERVER_POOL_SIZE, BROWSER_SERVER_RECORD, BROWSER_SERVER_RESTARTS, BROWSER_SERVER_SWEEP, BROWSER_SERVER_TEARDOWN, BROWSER_SERVER_UNAVAILABLE, BROWSER_SERVER_UNRESOLVED, BROWSER_STORE_CACHE_DIRS, BROWSER_STORE_DEFAULT_DIRS, BROWSER_STORE_ENV_KEY, BROWSER_STORE_GLOBS, BROWSER_STORE_LINK_NAME, BROWSER_TRANSPORT_LOSS_CAUSE, BROWSER_TRANSPORT_LOSS_DEFER_MS, BROWSER_WINDOWS_ROOT_FALLBACKS, BROWSER_WINDOWS_SUFFIXES, Browser, BrowserDestroyedError, BrowserMCPServer, BrowserNotConnectedError, FileBrowserJourneyStore, FileBrowserRunStore, FileBrowserWriter, WebSocketCDPTransport, browserToEngine, buildInstallPaths, buildStoreBases, buildWindowsRoots, createBrowser, createBrowserMCPServer, createBrowserProfile, createBrowserWriter, createCDPTransport, createFileBrowserJourneyStore, createFileBrowserRunStore, describeBrowserServerLoss, fetchCDPTargets, findEnvOverrides, findInstallPaths, findStorePaths, findSystemBrowser, findSystemBrowsers, formatBrowserLockEntry, isBrowserDestroyedError, isBrowserNotConnectedError, launchBrowserProcess, normalizeExecutablePath, parseBrowserEngine, parseBrowserLockEntry, parseBrowserProfileRecord, parseBrowserViewport, probePathNames, probeProcess, readBrowserEndpoint, readFirstLine, removeBrowserProfile };
3505
+ export { BROWSER_CDP_LIST_PATH, BROWSER_CDP_PROTOCOL, BROWSER_CDP_VERSION_PATH, BROWSER_DEFAULT_CDP_PORT, BROWSER_DEFAULT_HOST, BROWSER_DEVTOOLS_PATTERN, BROWSER_DRAIN_INTERVAL_MS, BROWSER_ENGINE_HINTS, BROWSER_ENV_PATH_KEYS, BROWSER_EXECUTABLE_NAMES, BROWSER_EXECUTABLE_PATHS, BROWSER_FILE_STORE_LIMIT, BROWSER_HEADLESS_ARG, BROWSER_JOURNEY_LOCK_ATTEMPTS, BROWSER_JOURNEY_LOCK_DIRECTORY, BROWSER_JOURNEY_REVISION_FILE, BROWSER_JOURNEY_SNAPSHOT_FILE, BROWSER_KILL_GRACE_MS, BROWSER_LAUNCH_ARGS, BROWSER_PORT_PROBE_TIMEOUT_MS, BROWSER_PROCESS_EXIT_CAUSE, BROWSER_PROFILE_PREFIX, BROWSER_RUN_DIRECTORY, BROWSER_RUN_FILE, BROWSER_SERVER_BUSY, BROWSER_SERVER_CONTEXTS, BROWSER_SERVER_CONTEXTS_LIMIT, BROWSER_SERVER_COPY, BROWSER_SERVER_CRASH, BROWSER_SERVER_EXHAUSTED, BROWSER_SERVER_HOLDER, BROWSER_SERVER_LAUNCH, BROWSER_SERVER_OPTIONS, BROWSER_SERVER_POOL_LIMIT, BROWSER_SERVER_POOL_SIZE, BROWSER_SERVER_RECORD, BROWSER_SERVER_RESTARTS, BROWSER_SERVER_SWEEP, BROWSER_SERVER_TEARDOWN, BROWSER_SERVER_UNAVAILABLE, BROWSER_SERVER_UNRESOLVED, BROWSER_STORE_CACHE_DIRS, BROWSER_STORE_DEFAULT_DIRS, BROWSER_STORE_ENV_KEY, BROWSER_STORE_GLOBS, BROWSER_STORE_LINK_NAME, BROWSER_TRANSPORT_LOSS_CAUSE, BROWSER_TRANSPORT_LOSS_DEFER_MS, BROWSER_WINDOWS_ROOT_FALLBACKS, BROWSER_WINDOWS_SUFFIXES, Browser, BrowserMCPServer, FileBrowserJourneyStore, FileBrowserRunStore, FileBrowserWriter, WebSocketCDPTransport, browserToEngine, buildInstallPaths, buildStoreBases, buildWindowsRoots, createBrowser, createBrowserMCPServer, createBrowserProfile, createFileBrowserJourneyStore, createFileBrowserRunStore, createFileBrowserWriter, createWebSocketCDPTransport, describeBrowserServerLoss, fetchCDPTargets, findEnvOverrides, findInstallPaths, findStorePaths, findSystemBrowsers, formatBrowserLockEntry, launchBrowserProcess, normalizeExecutablePath, parseBrowserEngine, parseBrowserLockEntry, parseBrowserProfileRecord, parseBrowserViewport, probePathNames, probeProcess, readBrowserEndpoint, readFirstLine, removeBrowserProfile };
3510
3506
 
3511
3507
  //# sourceMappingURL=index.js.map