@orkestrel/browser 0.0.25 → 0.0.27

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,11 +1,11 @@
1
1
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
- let _src_core = require("../core/index.cjs");
3
- let _orkestrel_contract = require("@orkestrel/contract");
4
2
  let node_fs = require("node:fs");
5
3
  let node_fs_promises = require("node:fs/promises");
6
4
  let node_os = require("node:os");
7
5
  let node_path = require("node:path");
8
6
  let node_child_process = require("node:child_process");
7
+ let _orkestrel_contract = require("@orkestrel/contract");
8
+ let _src_core = require("../core/index.cjs");
9
9
  let node_crypto = require("node:crypto");
10
10
  let node_http = require("node:http");
11
11
  let node_https = require("node:https");
@@ -43,12 +43,15 @@ var BROWSER_CDP_LIST_PATH = "/json/list";
43
43
  *
44
44
  * @remarks
45
45
  * Disables account sync so extension installs cannot reset URL loader factories during page imports.
46
- * Edge can still sign a fresh Windows profile in to the operating-system account.
46
+ * Disables Edge's implicit sign-in so fresh automation profiles carry no Microsoft account.
47
+ * Chromium ignores the unknown Edge feature name on Chrome and Chromium.
48
+ * The launcher merges disabled features with the caller's into one switch.
47
49
  */
48
50
  var BROWSER_LAUNCH_ARGS = Object.freeze([
49
51
  "--no-first-run",
50
52
  "--no-default-browser-check",
51
- "--disable-sync"
53
+ "--disable-sync",
54
+ "--disable-features=msImplicitSignin"
52
55
  ]);
53
56
  /**
54
57
  * Names the flag that enables headless mode on a launched browser process, `'--headless=new'`.
@@ -224,9 +227,9 @@ var BROWSER_RUN_DIRECTORY = "runs";
224
227
  /** Bounds a file-store listing page by default. */
225
228
  var BROWSER_FILE_STORE_LIMIT = 100;
226
229
  /** Names the refusal when no browser can serve a call. */
227
- var BROWSER_SERVER_UNAVAILABLE = "BROWSER_SERVER_UNAVAILABLE";
230
+ var BROWSER_SERVER_UNAVAILABLE = "SERVER_UNAVAILABLE";
228
231
  /** Names a failed attempt to warm a browser. */
229
- var BROWSER_SERVER_LAUNCH = "BROWSER_SERVER_LAUNCH";
232
+ var BROWSER_SERVER_LAUNCH = "SERVER_LAUNCH";
230
233
  /** Sets the default number of warm browsers to 1. */
231
234
  var BROWSER_SERVER_POOL_SIZE = 1;
232
235
  /** Sets the default context capacity per browser, including the shared holder. */
@@ -240,21 +243,21 @@ var BROWSER_SERVER_RESTARTS = 1;
240
243
  /** Names the browser process record in each profile. */
241
244
  var BROWSER_SERVER_RECORD = "browse.json";
242
245
  /** Names the diagnostic when the warm floor spends its restart bound. */
243
- var BROWSER_SERVER_EXHAUSTED = "BROWSER_SERVER_EXHAUSTED";
246
+ var BROWSER_SERVER_EXHAUSTED = "SERVER_EXHAUSTED";
244
247
  /** Names a failed browser server teardown. */
245
- var BROWSER_SERVER_TEARDOWN = "BROWSER_SERVER_TEARDOWN";
248
+ var BROWSER_SERVER_TEARDOWN = "SERVER_TEARDOWN";
246
249
  /** Names a failed profile sweep. */
247
- var BROWSER_SERVER_SWEEP = "BROWSER_SERVER_SWEEP";
250
+ var BROWSER_SERVER_SWEEP = "SERVER_SWEEP";
248
251
  /** Names a refused browser server option. */
249
- var BROWSER_SERVER_OPTIONS = "BROWSER_SERVER_OPTIONS";
252
+ var BROWSER_SERVER_OPTIONS = "ARGUMENT";
250
253
  /** Names the notice that a browser and its session state were lost. */
251
- var BROWSER_SERVER_CRASH = "BROWSER_SERVER_CRASH";
254
+ var BROWSER_SERVER_CRASH = "SERVER_CRASH";
252
255
  /** Names an interrupted call whose outcome is unknown. */
253
- var BROWSER_SERVER_UNRESOLVED = "BROWSER_SERVER_UNRESOLVED";
256
+ var BROWSER_SERVER_UNRESOLVED = "SERVER_UNRESOLVED";
254
257
  /** Names the refusal when every browser is admitted to a holder. */
255
- var BROWSER_SERVER_BUSY = "BROWSER_SERVER_BUSY";
258
+ var BROWSER_SERVER_BUSY = "SERVER_BUSY";
256
259
  /** Names the refusal for an unknown or ended holder. */
257
- var BROWSER_SERVER_HOLDER = "BROWSER_SERVER_HOLDER";
260
+ var BROWSER_SERVER_HOLDER = "SERVER_HOLDER";
258
261
  /** Defines the holder tools independently of the shared browser's vocabulary. */
259
262
  var BROWSER_SERVER_COPY = Object.freeze({
260
263
  acquire: Object.freeze({
@@ -330,46 +333,6 @@ var BROWSER_SERVER_COPY = Object.freeze({
330
333
  })
331
334
  });
332
335
  //#endregion
333
- //#region src/server/errors.ts
334
- /**
335
- * Reports that an operation requiring an active connection was attempted while disconnected,
336
- * under the code `BROWSER_NOT_CONNECTED_ERROR`.
337
- */
338
- var BrowserNotConnectedError = class extends _src_core.BrowserError {
339
- constructor(context) {
340
- super("Browser is not connected", "BROWSER_NOT_CONNECTED_ERROR", context);
341
- this.name = "BrowserNotConnectedError";
342
- }
343
- };
344
- /**
345
- * Reports that an operation was attempted after the browser wrapper was destroyed, under the
346
- * code `BROWSER_DESTROYED_ERROR`.
347
- */
348
- var BrowserDestroyedError = class extends _src_core.BrowserError {
349
- constructor(context) {
350
- super("Browser has been destroyed", "BROWSER_DESTROYED_ERROR", context);
351
- this.name = "BrowserDestroyedError";
352
- }
353
- };
354
- /**
355
- * Narrows an unknown value to a `BrowserNotConnectedError`.
356
- *
357
- * @param value - Value to check
358
- * @returns True if value is a BrowserNotConnectedError instance; false otherwise
359
- */
360
- function isBrowserNotConnectedError(value) {
361
- return (0, _orkestrel_contract.isInstance)(value, BrowserNotConnectedError);
362
- }
363
- /**
364
- * Narrows an unknown value to a `BrowserDestroyedError`.
365
- *
366
- * @param value - Value to check
367
- * @returns True if value is a BrowserDestroyedError instance; false otherwise
368
- */
369
- function isBrowserDestroyedError(value) {
370
- return (0, _orkestrel_contract.isInstance)(value, BrowserDestroyedError);
371
- }
372
- //#endregion
373
336
  //#region src/server/helpers.ts
374
337
  /**
375
338
  * Parses positive integer viewport dimensions in WIDTHxHEIGHT form.
@@ -432,15 +395,15 @@ function parseBrowserProfileRecord(text) {
432
395
  * @param url - The lost page's last URL, when known
433
396
  * @returns The coded diagnostic and any operation-lifecycle guidance
434
397
  * @example
435
- * describeBrowserServerLoss('BROWSER_SERVER_UNAVAILABLE', new Error('Browser exited'))
398
+ * describeBrowserServerLoss('SERVER_UNAVAILABLE', new Error('Browser exited'))
436
399
  */
437
400
  function describeBrowserServerLoss(code, cause, url) {
438
401
  const pid = (0, _src_core.isBrowserError)(cause) ? cause.context?.["pid"] : void 0;
439
402
  const message = `${code}: ${(cause === void 0 ? "Browser session lost" : (0, _orkestrel_contract.isError)(cause) ? cause.message : String(cause)).replace(/\.+$/u, "")}${(0, _orkestrel_contract.isNumber)(pid) ? ` (pid ${pid})` : ""}.`;
440
- if (code !== "BROWSER_SERVER_CRASH" && code !== "BROWSER_SERVER_UNRESOLVED") return message;
441
- 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.`;
403
+ if (code !== "SERVER_CRASH" && code !== "SERVER_UNRESOLVED") return message;
404
+ 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.`;
442
405
  const next = `The next call acquires a browser that starts at about:blank, or answers ${BROWSER_SERVER_UNAVAILABLE} when none can serve.`;
443
- return code === "BROWSER_SERVER_UNRESOLVED" ? `${message} The outcome is unknown. ${lost} Browse did not repeat the call. ${next}` : `${message} ${lost}`;
406
+ return code === "SERVER_UNRESOLVED" ? `${message} The outcome is unknown. ${lost} Browse did not repeat the call. ${next}` : `${message} ${lost}`;
444
407
  }
445
408
  /**
446
409
  * Formats a process identifier and UUID token as a lock entry name.
@@ -515,16 +478,6 @@ function findSystemBrowsers(options) {
515
478
  return browsers;
516
479
  }
517
480
  /**
518
- * Locates a Chrome/Chromium/Edge executable on this machine — the first entry
519
- * of {@link findSystemBrowsers}.
520
- *
521
- * @param options - Overrides for the candidate sources; see {@link SystemBrowserOptions}
522
- * @returns The first discovered browser, or undefined
523
- */
524
- function findSystemBrowser(options) {
525
- return findSystemBrowsers(options)[0];
526
- }
527
- /**
528
481
  * Classifies an executable path/name into a {@link BrowserEngine} by
529
482
  * case-insensitive hint, checked in the order edge → chromium → chrome.
530
483
  *
@@ -580,7 +533,7 @@ async function removeBrowserProfile(profile) {
580
533
  if (!profile.temporary) return;
581
534
  const path = (0, node_path.resolve)(profile.path);
582
535
  const temporary = (0, node_path.resolve)((0, node_os.tmpdir)());
583
- if ((0, node_path.dirname)(path) !== temporary || !(0, node_path.basename)(path).startsWith("orkestrel-browser-")) throw new _src_core.BrowserError("Refusing to remove an unsafe browser profile path", void 0, { path });
536
+ if ((0, node_path.dirname)(path) !== temporary || !(0, node_path.basename)(path).startsWith("orkestrel-browser-")) throw new _src_core.BrowserError("ARGUMENT", "Refusing to remove an unsafe browser profile path", { path });
584
537
  await (0, node_fs_promises.rm)(path, {
585
538
  recursive: true,
586
539
  force: true,
@@ -687,6 +640,10 @@ function findStorePaths(base, platform) {
687
640
  * Launches a browser process with raw-CDP debugging flags.
688
641
  *
689
642
  * @remarks
643
+ * Merges every caller and library `--disable-features` value into one switch because Chromium
644
+ * reads only the last occurrence. Keeps `msImplicitSignin` disabled alongside caller features;
645
+ * Chrome and Chromium ignore this Edge feature name. Preserves the caller's other arguments.
646
+ *
690
647
  * POSIX launches own an isolated process group so lifecycle teardown can
691
648
  * signal and await every Chromium subprocess without affecting the caller.
692
649
  * Windows launches are not detached and own no group, so teardown there
@@ -704,8 +661,18 @@ function findStorePaths(base, platform) {
704
661
  */
705
662
  function launchBrowserProcess(executable, port, headless, profile, extra) {
706
663
  const args = [];
707
- if (extra !== void 0) args.push(...extra);
708
- args.push(`--remote-debugging-port=${port ?? 0}`, ...BROWSER_LAUNCH_ARGS);
664
+ const features = /* @__PURE__ */ new Set();
665
+ for (const arg of [
666
+ ...extra ?? [],
667
+ `--remote-debugging-port=${port ?? 0}`,
668
+ ...BROWSER_LAUNCH_ARGS
669
+ ]) {
670
+ if (arg === "--disable-features") continue;
671
+ if (arg.startsWith("--disable-features=")) {
672
+ for (const feature of arg.slice(19).split(",")) if (feature.trim() !== "") features.add(feature.trim());
673
+ } else args.push(arg);
674
+ }
675
+ args.push(`--disable-features=${[...features].join(",")}`);
709
676
  if (headless) args.push(BROWSER_HEADLESS_ARG);
710
677
  if (profile !== void 0) args.push(`--user-data-dir=${profile}`);
711
678
  return (0, node_child_process.spawn)(executable, args, {
@@ -756,18 +723,18 @@ function readBrowserEndpoint(stream, signal) {
756
723
  stream.on("close", () => {
757
724
  const endpoint = found ? void 0 : BROWSER_DEVTOOLS_PATTERN.exec(pending)?.[1];
758
725
  if (endpoint !== void 0) read.resolve(endpoint);
759
- else read.reject(new _src_core.BrowserConnectionError("Browser closed its standard error before reporting a CDP endpoint"));
726
+ else read.reject(new _src_core.BrowserError("CONNECTION", "Browser closed its standard error before reporting a CDP endpoint"));
760
727
  });
761
728
  return read.promise;
762
729
  }
763
730
  /**
764
731
  * Fetches the current CDP target list from a browser's `/json/list` endpoint, as a `Result`
765
- * carrying either the targets or a coded `BrowserConnectionError`.
732
+ * carrying either the targets or a coded `BrowserError`.
766
733
  *
767
734
  * @remarks
768
735
  * The endpoint is a network boundary, so an unreachable host, a non-2xx
769
736
  * response, and a body that is not a JSON array each come back as a failed
770
- * `Result` carrying a coded `BrowserConnectionError`. An entry missing a
737
+ * `Result` carrying a coded `BrowserError`. An entry missing a
771
738
  * required string field is skipped rather than failing the whole list.
772
739
  *
773
740
  * @param port - Port the browser exposes its CDP endpoint on
@@ -792,7 +759,7 @@ async function fetchCDPTargets(port, timeout, host = BROWSER_DEFAULT_HOST) {
792
759
  const response = await fetch(url, { signal: controller.signal });
793
760
  if (!response.ok) return {
794
761
  success: false,
795
- error: new _src_core.BrowserConnectionError("CDP target list request was refused", {
762
+ error: new _src_core.BrowserError("CONNECTION", "CDP target list request was refused", {
796
763
  url,
797
764
  status: response.status
798
765
  })
@@ -800,7 +767,7 @@ async function fetchCDPTargets(port, timeout, host = BROWSER_DEFAULT_HOST) {
800
767
  const list = await response.json();
801
768
  if (!(0, _orkestrel_contract.isArray)(list)) return {
802
769
  success: false,
803
- error: new _src_core.BrowserConnectionError("CDP target list is not a JSON array", { url })
770
+ error: new _src_core.BrowserError("CONNECTION", "CDP target list is not a JSON array", { url })
804
771
  };
805
772
  const targets = [];
806
773
  for (const entry of list) {
@@ -820,9 +787,9 @@ async function fetchCDPTargets(port, timeout, host = BROWSER_DEFAULT_HOST) {
820
787
  } catch (error) {
821
788
  return {
822
789
  success: false,
823
- error: new _src_core.BrowserConnectionError("CDP target list is unreachable", {
790
+ error: new _src_core.BrowserError("CONNECTION", "CDP target list is unreachable", {
824
791
  url,
825
- error
792
+ error: (0, _orkestrel_contract.isJSONValue)(error) ? error : String(error)
826
793
  })
827
794
  };
828
795
  } finally {
@@ -888,7 +855,7 @@ var WebSocketCDPTransport = class {
888
855
  }
889
856
  async send(data) {
890
857
  const socket = this.#socket;
891
- if (socket === void 0 || socket.readyState !== _orkestrel_websocket.WEBSOCKET_READY_OPEN) throw new _src_core.BrowserConnectionError("WebSocket CDP transport is not open", { url: this.#url });
858
+ if (socket === void 0 || socket.readyState !== _orkestrel_websocket.WEBSOCKET_READY_OPEN) throw new _src_core.BrowserError("CONNECTION", "WebSocket CDP transport is not open", { url: this.#url });
892
859
  socket.send(data);
893
860
  }
894
861
  async close() {
@@ -896,12 +863,12 @@ var WebSocketCDPTransport = class {
896
863
  }
897
864
  async #start() {
898
865
  const parsed = (0, _orkestrel_contract.attempt)(() => new URL(this.#url));
899
- if (!parsed.success) throw new _src_core.BrowserConnectionError(`WebSocket CDP URL is invalid: ${this.#url}`, {
866
+ if (!parsed.success) throw new _src_core.BrowserError("CONNECTION", `WebSocket CDP URL is invalid: ${this.#url}`, {
900
867
  url: this.#url,
901
- error: parsed.error
868
+ error: (0, _orkestrel_contract.isJSONValue)(parsed.error) ? parsed.error : String(parsed.error)
902
869
  });
903
870
  const url = parsed.value;
904
- if (url.protocol !== "ws:" && url.protocol !== "wss:") throw new _src_core.BrowserConnectionError(`WebSocket CDP connection requires a ws: or wss: URL: ${this.#url}`, { url: this.#url });
871
+ if (url.protocol !== "ws:" && url.protocol !== "wss:") throw new _src_core.BrowserError("CONNECTION", `WebSocket CDP connection requires a ws: or wss: URL: ${this.#url}`, { url: this.#url });
905
872
  const key = (0, node_crypto.randomBytes)(16).toString("base64");
906
873
  const deferred = Promise.withResolvers();
907
874
  const options = {
@@ -933,7 +900,7 @@ var WebSocketCDPTransport = class {
933
900
  async #close() {
934
901
  const request = this.#request;
935
902
  if (request !== void 0) {
936
- this.#rejectRequest(request, new _src_core.BrowserConnectionError(`WebSocket CDP connection to ${this.#url} was closed before it finished connecting`, { url: this.#url }));
903
+ this.#rejectRequest(request, new _src_core.BrowserError("CONNECTION", `WebSocket CDP connection to ${this.#url} was closed before it finished connecting`, { url: this.#url }));
937
904
  request.destroy();
938
905
  }
939
906
  await this.#starting.pending?.catch(() => void 0);
@@ -962,7 +929,7 @@ var WebSocketCDPTransport = class {
962
929
  const accept = response.headers["sec-websocket-accept"];
963
930
  if (!(0, _orkestrel_contract.isString)(accept) || accept !== (0, _orkestrel_websocket.computeWebSocketAccept)(key)) {
964
931
  socket.destroy();
965
- this.#rejectRequest(request, new _src_core.BrowserConnectionError(`WebSocket CDP connection to ${this.#url} failed: Sec-WebSocket-Accept mismatch`, { url: this.#url }));
932
+ this.#rejectRequest(request, new _src_core.BrowserError("CONNECTION", `WebSocket CDP connection to ${this.#url} failed: Sec-WebSocket-Accept mismatch`, { url: this.#url }));
966
933
  return;
967
934
  }
968
935
  try {
@@ -978,13 +945,13 @@ var WebSocketCDPTransport = class {
978
945
  #decline(request, response) {
979
946
  response.resume();
980
947
  const reason = response.statusCode === void 0 ? "upgrade was declined without a status code" : `upgrade declined with status ${response.statusCode}`;
981
- this.#rejectRequest(request, new _src_core.BrowserConnectionError(`WebSocket CDP connection to ${this.#url} failed: ${reason}`, { url: this.#url }));
948
+ this.#rejectRequest(request, new _src_core.BrowserError("CONNECTION", `WebSocket CDP connection to ${this.#url} failed: ${reason}`, { url: this.#url }));
982
949
  }
983
950
  #fail(request, error) {
984
- this.#rejectRequest(request, new _src_core.BrowserConnectionError(`WebSocket CDP connection to ${this.#url} failed: ${error.message}`, { url: this.#url }));
951
+ this.#rejectRequest(request, new _src_core.BrowserError("CONNECTION", `WebSocket CDP connection to ${this.#url} failed: ${error.message}`, { url: this.#url }));
985
952
  }
986
953
  #expire(request) {
987
- this.#rejectRequest(request, new _src_core.BrowserConnectionError(`WebSocket CDP connection to ${this.#url} timed out after ${this.#timeout}ms`, {
954
+ this.#rejectRequest(request, new _src_core.BrowserError("CONNECTION", `WebSocket CDP connection to ${this.#url} timed out after ${this.#timeout}ms`, {
988
955
  url: this.#url,
989
956
  timeout: this.#timeout
990
957
  }));
@@ -1056,7 +1023,7 @@ var FileBrowserWriter = class {
1056
1023
  };
1057
1024
  //#endregion
1058
1025
  //#region package.json
1059
- var version = "0.0.25";
1026
+ var version = "0.0.27";
1060
1027
  //#endregion
1061
1028
  //#region src/server/BrowserMCPServer.ts
1062
1029
  /**
@@ -1064,9 +1031,9 @@ var version = "0.0.25";
1064
1031
  * the Model Context Protocol on stdio, and warms Chromium at server start.
1065
1032
  *
1066
1033
  * @remarks
1067
- * The server's own manager holds one dispatcher per name of the vocabulary — `look`, `read`, `plain`,
1068
- * `click`, `type`, `press`, `navigate`, `wait`, `dialog`, `tabs`, `switch`, `record`, `save`,
1069
- * `journeys`, `edit`, `replay`, and `forget` — each carrying the description, parameters, and annotations
1034
+ * The server's own manager holds one dispatcher per name of the vocabulary — `read`, `click`,
1035
+ * `type`, `press`, `navigate`, `wait`, `dialog`, `switch`, `record`, `save`, `journeys`, `edit`,
1036
+ * `replay`, `forget`, and `capture` — each carrying the description, parameters, and annotations
1070
1037
  * `BROWSER_TOOL_COPY` gives, so `tools/list` answers before Chromium starts and with Chromium
1071
1038
  * absent. After the launch, a tool the toolset's manager adds under another name, such as a page
1072
1039
  * tool it adopts, is mirrored as a dispatcher with that tool's definition, and the mirror is
@@ -1103,7 +1070,7 @@ var version = "0.0.25";
1103
1070
  * ```ts
1104
1071
  * import { BrowserMCPServer } from '@orkestrel/browser/server'
1105
1072
  *
1106
- * const server = new BrowserMCPServer({ root: 'tmp/browsers', readonly: true })
1073
+ * const server = new BrowserMCPServer({ root: 'tmp/browsers', journeys: { readonly: true } })
1107
1074
  * await server.start()
1108
1075
  * await server.destroy()
1109
1076
  * ```
@@ -1166,8 +1133,8 @@ var BrowserMCPServer = class {
1166
1133
  constructor(options) {
1167
1134
  const size = options?.pool?.size ?? 1;
1168
1135
  const contexts = options?.pool?.contexts ?? 2;
1169
- if (!(0, _orkestrel_contract.isInteger)(size) || size < 1 || size > 3) throw new _src_core.BrowserError(`pool.size must be an integer from 1 through 3`, BROWSER_SERVER_OPTIONS);
1170
- if (!(0, _orkestrel_contract.isInteger)(contexts) || contexts < 1 || contexts > 4) throw new _src_core.BrowserError(`pool.contexts must be an integer from 1 through 4`, BROWSER_SERVER_OPTIONS);
1136
+ if (!(0, _orkestrel_contract.isInteger)(size) || size < 1 || size > 3) throw new _src_core.BrowserError(BROWSER_SERVER_OPTIONS, `pool.size must be an integer from 1 through 3`, { subject: "server" });
1137
+ if (!(0, _orkestrel_contract.isInteger)(contexts) || contexts < 1 || contexts > 4) throw new _src_core.BrowserError(BROWSER_SERVER_OPTIONS, `pool.contexts must be an integer from 1 through 4`, { subject: "server" });
1171
1138
  this.#admission = size * contexts;
1172
1139
  this.#log = options?.log ?? process.stderr;
1173
1140
  this.#reference = this.#issue.bind(this);
@@ -1182,11 +1149,11 @@ var BrowserMCPServer = class {
1182
1149
  restarts: 1
1183
1150
  });
1184
1151
  this.#root = (0, node_path.resolve)(options?.root ?? "tmp/browsers");
1185
- this.#headless = options?.headless ?? true;
1186
- this.#executable = options?.executable;
1187
- this.#viewport = options?.viewport;
1188
- this.#readonly = options?.readonly ?? false;
1189
- this.#launcher = options?.launch ?? createBrowser;
1152
+ this.#headless = options?.browser?.headless ?? true;
1153
+ this.#executable = options?.browser?.executable;
1154
+ this.#viewport = options?.browser?.viewport;
1155
+ this.#readonly = options?.journeys?.readonly ?? false;
1156
+ this.#launcher = options?.pool?.launch ?? createBrowser;
1190
1157
  this.#input = options?.stdio?.input ?? process.stdin;
1191
1158
  this.#tools = (0, _orkestrel_tool.createToolManager)();
1192
1159
  const names = [..._src_core.BROWSER_TOOL_NAMES, ..._src_core.BROWSER_JOURNEY_TOOL_NAMES];
@@ -1250,7 +1217,7 @@ var BrowserMCPServer = class {
1250
1217
  warming.then(this.#exhaust.bind(this));
1251
1218
  } catch (error) {
1252
1219
  if (this.#closing !== void 0) return;
1253
- if ((0, _src_core.isBrowserError)(error) && error.code === "BROWSER_SERVER_UNAVAILABLE") throw error;
1220
+ if ((0, _src_core.isBrowserError)(error) && error.code === "SERVER_UNAVAILABLE") throw error;
1254
1221
  throw this.#unavailable((0, _orkestrel_pool.isPoolError)(error) ? error.cause ?? this.#failure : error);
1255
1222
  }
1256
1223
  }
@@ -1300,12 +1267,16 @@ var BrowserMCPServer = class {
1300
1267
  signal: context.signal,
1301
1268
  ...context.caller === void 0 ? {} : { caller: context.caller }
1302
1269
  });
1303
- if (!result.success || typeof result.value !== "string") return result;
1270
+ if (!result.success) return {
1271
+ ...result,
1272
+ error: (0, _src_core.boundBrowserText)(result.error, _src_core.BROWSER_TOOL_LIMIT, _src_core.BROWSER_TOOL_CUT_FOOTER)
1273
+ };
1274
+ if (typeof result.value !== "string") return result;
1304
1275
  return {
1305
1276
  resultType: "complete",
1306
1277
  content: [{
1307
1278
  type: "text",
1308
- text: result.value
1279
+ text: context.call.name === "acquire" || context.call.name === "tools" ? result.value : (0, _src_core.boundBrowserText)(result.value, _src_core.BROWSER_TOOL_LIMIT, _src_core.BROWSER_TOOL_CUT_FOOTER)
1309
1280
  }]
1310
1281
  };
1311
1282
  }
@@ -1321,8 +1292,8 @@ var BrowserMCPServer = class {
1321
1292
  context.signal.throwIfAborted();
1322
1293
  if (this.#closing !== void 0) throw this.#ended();
1323
1294
  const purpose = args["purpose"];
1324
- if (!(0, _orkestrel_contract.isString)(purpose) || purpose.trim().length === 0) throw new _src_core.BrowserError("purpose must describe the work", "BROWSER_TOOLSET_ARGUMENT");
1325
- if (this.#holders.size >= this.#admission) throw new _src_core.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);
1295
+ if (!(0, _orkestrel_contract.isString)(purpose) || purpose.trim().length === 0) throw new _src_core.BrowserError("ARGUMENT", "purpose must describe the work", { subject: "toolset" });
1296
+ if (this.#holders.size >= this.#admission) throw new _src_core.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.`);
1326
1297
  const holder = {
1327
1298
  id: (0, node_crypto.randomUUID)(),
1328
1299
  purpose,
@@ -1349,14 +1320,14 @@ var BrowserMCPServer = class {
1349
1320
  }
1350
1321
  #holder(value) {
1351
1322
  const holder = (0, _orkestrel_contract.isString)(value) ? this.#holders.get(value) : void 0;
1352
- if (holder === void 0 || holder === this.#shared || holder.abort.signal.aborted) throw new _src_core.BrowserError(`${BROWSER_SERVER_HOLDER}: unknown or ended holder`, BROWSER_SERVER_HOLDER);
1323
+ if (holder === void 0 || holder === this.#shared || holder.abort.signal.aborted) throw new _src_core.BrowserError(BROWSER_SERVER_HOLDER, `${BROWSER_SERVER_HOLDER}: unknown or ended holder`);
1353
1324
  return holder;
1354
1325
  }
1355
1326
  async #dispatch(args, context) {
1356
1327
  const holder = this.#holder(args["holder"]);
1357
1328
  const name = args["name"];
1358
1329
  const parameters = args["arguments"];
1359
- if (!(0, _orkestrel_contract.isString)(name) || !(0, _orkestrel_contract.isRecord)(parameters)) throw new _src_core.BrowserError("execute requires name and arguments", "BROWSER_TOOLSET_ARGUMENT");
1330
+ if (!(0, _orkestrel_contract.isString)(name) || !(0, _orkestrel_contract.isRecord)(parameters)) throw new _src_core.BrowserError("ARGUMENT", "execute requires name and arguments", { subject: "toolset" });
1360
1331
  return this.#forward(holder, name, parameters, context);
1361
1332
  }
1362
1333
  async #catalog(args, context) {
@@ -1373,8 +1344,8 @@ var BrowserMCPServer = class {
1373
1344
  return this.#annotate(holder, JSON.stringify(catalog));
1374
1345
  } catch (error) {
1375
1346
  context.signal.throwIfAborted();
1376
- if (holder.abort.signal.aborted) throw new _src_core.BrowserError(`${BROWSER_SERVER_UNRESOLVED}: the holder ended during the call`, BROWSER_SERVER_UNRESOLVED);
1377
- if ((0, _src_core.isBrowserError)(error)) throw new _src_core.BrowserError(this.#annotate(holder, `${error.code}: ${error.message}`), error.code);
1347
+ if (holder.abort.signal.aborted) throw new _src_core.BrowserError(BROWSER_SERVER_UNRESOLVED, `${BROWSER_SERVER_UNRESOLVED}: the holder ended during the call`);
1348
+ if ((0, _src_core.isBrowserError)(error)) throw new _src_core.BrowserError(error.code, this.#annotate(holder, `${error.code}: ${error.message}`));
1378
1349
  throw error;
1379
1350
  }
1380
1351
  }
@@ -1411,7 +1382,7 @@ var BrowserMCPServer = class {
1411
1382
  context.signal.throwIfAborted();
1412
1383
  const journey = args["journey"];
1413
1384
  if (name !== "replay" && name !== "forget" || !(0, _orkestrel_contract.isString)(journey)) return this.#perform(holder, name, args, context);
1414
- if (this.#writers.has(journey) || name === "forget" && this.#readers.has(journey)) throw new _src_core.BrowserError(`BROWSER_JOURNEY_LOCKED: Journey ${journey} is locked; call ${name} again.`, "BROWSER_JOURNEY_LOCKED", { name: journey });
1385
+ if (this.#writers.has(journey) || name === "forget" && this.#readers.has(journey)) throw new _src_core.BrowserError("STORE_LOCKED", `STORE_LOCKED: Journey ${journey} is locked; call ${name} again.`, { name: journey });
1415
1386
  const reader = Symbol(journey);
1416
1387
  if (name === "forget") this.#writers.add(journey);
1417
1388
  else {
@@ -1437,8 +1408,8 @@ var BrowserMCPServer = class {
1437
1408
  lease = await this.#serve(holder, signal);
1438
1409
  } catch (error) {
1439
1410
  context.signal.throwIfAborted();
1440
- if (holder !== this.#shared && holder.abort.signal.aborted) throw new _src_core.BrowserError(`${BROWSER_SERVER_UNRESOLVED}: the holder ended during the call`, BROWSER_SERVER_UNRESOLVED);
1441
- if ((0, _src_core.isBrowserError)(error)) throw new _src_core.BrowserError(this.#annotate(holder, `${error.code}: ${error.message}`), error.code);
1411
+ if (holder !== this.#shared && holder.abort.signal.aborted) throw new _src_core.BrowserError(BROWSER_SERVER_UNRESOLVED, `${BROWSER_SERVER_UNRESOLVED}: the holder ended during the call`);
1412
+ if ((0, _src_core.isBrowserError)(error)) throw new _src_core.BrowserError(error.code, this.#annotate(holder, `${error.code}: ${error.message}`));
1442
1413
  throw error;
1443
1414
  }
1444
1415
  let result;
@@ -1463,7 +1434,7 @@ var BrowserMCPServer = class {
1463
1434
  };
1464
1435
  }
1465
1436
  context.signal.throwIfAborted();
1466
- if (holder !== this.#shared && holder.abort.signal.aborted) throw new _src_core.BrowserError(BROWSER_SERVER_UNRESOLVED + ": the holder ended during the call", BROWSER_SERVER_UNRESOLVED);
1437
+ if (holder !== this.#shared && holder.abort.signal.aborted) throw new _src_core.BrowserError(BROWSER_SERVER_UNRESOLVED, BROWSER_SERVER_UNRESOLVED + ": the holder ended during the call");
1467
1438
  if (!result.success) {
1468
1439
  if (this.#leases.get(holder) === lease && !context.signal.aborted) try {
1469
1440
  await this.#race(this.#ping(lease.token.value), context.signal);
@@ -1471,9 +1442,9 @@ var BrowserMCPServer = class {
1471
1442
  context.signal.throwIfAborted();
1472
1443
  if (this.#leases.get(holder) !== lease) {
1473
1444
  const loss = this.#departures.get(lease);
1474
- throw new _src_core.BrowserError(this.#annotate(holder, describeBrowserServerLoss(BROWSER_SERVER_UNRESOLVED, loss?.cause, loss?.url), lease), BROWSER_SERVER_UNRESOLVED);
1445
+ throw new _src_core.BrowserError(BROWSER_SERVER_UNRESOLVED, this.#annotate(holder, describeBrowserServerLoss(BROWSER_SERVER_UNRESOLVED, loss?.cause, loss?.url), lease));
1475
1446
  }
1476
- throw new _src_core.BrowserError(this.#annotate(holder, result.error, lease));
1447
+ throw new _src_core.BrowserError("PROTOCOL", (0, _src_core.boundBrowserText)(this.#annotate(holder, result.error, lease), _src_core.BROWSER_TOOL_LIMIT, _src_core.BROWSER_TOOL_CUT_FOOTER));
1477
1448
  }
1478
1449
  return typeof result.value === "string" ? this.#annotate(holder, result.value, lease) : result.value;
1479
1450
  }
@@ -1487,6 +1458,10 @@ var BrowserMCPServer = class {
1487
1458
  }
1488
1459
  return [...notes, value].join("\n");
1489
1460
  }
1461
+ #notice(context) {
1462
+ const entry = [...this.#leases].find(([, lease]) => lease.generation.context === context);
1463
+ return entry === void 0 ? "" : this.#annotate(entry[0], "");
1464
+ }
1490
1465
  async #handshake(options) {
1491
1466
  try {
1492
1467
  await this.#race(this.#starting, options.signal);
@@ -1637,7 +1612,7 @@ var BrowserMCPServer = class {
1637
1612
  if (this.#leases.get(holder) !== lease || this.#holders.get(holder.id) !== holder) return;
1638
1613
  this.#departures.set(lease, {
1639
1614
  cause,
1640
- url: lease.generation.toolset.view.url
1615
+ url: lease.generation.toolset.redact(lease.generation.toolset.view.url)
1641
1616
  });
1642
1617
  if (holder === this.#shared) this.#unmirror();
1643
1618
  this.#leases.delete(holder);
@@ -1779,6 +1754,7 @@ var BrowserMCPServer = class {
1779
1754
  }
1780
1755
  toolset = (0, _src_core.createBrowserToolset)(page, {
1781
1756
  context,
1757
+ notes: this.#notice.bind(this, context),
1782
1758
  journeys: {
1783
1759
  store: createFileBrowserJourneyStore({ root: this.#root }),
1784
1760
  runs: createFileBrowserRunStore({ root: this.#root }),
@@ -1798,7 +1774,7 @@ var BrowserMCPServer = class {
1798
1774
  } catch (error) {
1799
1775
  await toolset?.destroy().catch(this.#fault.bind(this));
1800
1776
  await context?.close().catch((failure) => {
1801
- if (!this.#losses.has(slot) && slot.browser.status === "connected" && this.#closing === void 0 && !(0, _src_core.isCDPConnectionError)(failure)) this.#fault(failure);
1777
+ if (!this.#losses.has(slot) && slot.browser.status === "connected" && this.#closing === void 0 && !((0, _src_core.isBrowserError)(failure) && failure.code === "DISCONNECTED")) this.#fault(failure);
1802
1778
  });
1803
1779
  await (0, node_fs_promises.rm)(directory, {
1804
1780
  recursive: true,
@@ -1808,7 +1784,8 @@ var BrowserMCPServer = class {
1808
1784
  }
1809
1785
  }
1810
1786
  async #destroyRecord(slot) {
1811
- if ((0, _src_core.isCDPTimeoutError)(this.#losses.get(slot)?.cause) && slot.browser.pid !== void 0) try {
1787
+ const cause = this.#losses.get(slot)?.cause;
1788
+ if ((0, _src_core.isBrowserError)(cause) && cause.code === "TIMEOUT" && slot.browser.pid !== void 0) try {
1812
1789
  process.kill(slot.browser.pid, "SIGKILL");
1813
1790
  } catch (error) {
1814
1791
  if (!(0, _orkestrel_contract.isError)(error) || !("code" in error) || error.code !== "ESRCH") this.#faults.add(error);
@@ -1833,7 +1810,7 @@ var BrowserMCPServer = class {
1833
1810
  }
1834
1811
  async #warm() {
1835
1812
  if (this.#closing !== void 0) throw this.#ended();
1836
- if (this.#stranded !== void 0) throw new _src_core.BrowserError((0, _orkestrel_contract.isError)(this.#stranded) ? this.#stranded.message : String(this.#stranded), BROWSER_SERVER_TEARDOWN, (0, _src_core.isBrowserError)(this.#stranded) ? this.#stranded.context : void 0);
1813
+ if (this.#stranded !== void 0) throw new _src_core.BrowserError(BROWSER_SERVER_TEARDOWN, (0, _orkestrel_contract.isError)(this.#stranded) ? this.#stranded.message : String(this.#stranded), (0, _src_core.isBrowserError)(this.#stranded) ? this.#stranded.context : void 0);
1837
1814
  const profile = (0, node_path.join)(this.#root, ".profiles", formatBrowserLockEntry(process.pid, (0, node_crypto.randomUUID)()));
1838
1815
  let browser;
1839
1816
  let slot;
@@ -1906,7 +1883,7 @@ var BrowserMCPServer = class {
1906
1883
  const record = parseBrowserProfileRecord(await (0, node_fs_promises.readFile)((0, node_path.join)(folder, BROWSER_SERVER_RECORD), "utf8"));
1907
1884
  if (record === void 0) continue;
1908
1885
  if (probeProcess(record.pid)) {
1909
- const transport = createCDPTransport({ url: record.endpoint });
1886
+ const transport = createWebSocketCDPTransport({ url: record.endpoint });
1910
1887
  const client = (0, _src_core.createCDPClient)({ transport });
1911
1888
  try {
1912
1889
  this.#abort.signal.throwIfAborted();
@@ -1948,7 +1925,7 @@ var BrowserMCPServer = class {
1948
1925
  }
1949
1926
  #unavailable(cause) {
1950
1927
  const message = describeBrowserServerLoss(BROWSER_SERVER_UNAVAILABLE, cause);
1951
- return new _src_core.BrowserError(message.slice(BROWSER_SERVER_UNAVAILABLE.length + 2), BROWSER_SERVER_UNAVAILABLE);
1928
+ return new _src_core.BrowserError(BROWSER_SERVER_UNAVAILABLE, message.slice(BROWSER_SERVER_UNAVAILABLE.length + 2));
1952
1929
  }
1953
1930
  #issue() {
1954
1931
  return `e${++this.#references}`;
@@ -1980,7 +1957,7 @@ var BrowserMCPServer = class {
1980
1957
  tools.emitter.off("clear", mirrored.cleared);
1981
1958
  }
1982
1959
  #ended() {
1983
- return new _src_core.BrowserError("the browser session ended", "BROWSER_TOOLSET_ENDED");
1960
+ return new _src_core.BrowserError("CLOSED", "the browser session ended", { subject: "toolset" });
1984
1961
  }
1985
1962
  };
1986
1963
  //#endregion
@@ -1993,31 +1970,32 @@ var BrowserMCPServer = class {
1993
1970
  * const files = new FileBrowserStore({ root: directory })
1994
1971
  * files.validateName('check-ready')
1995
1972
  */
1996
- var FileBrowserStore = class {
1973
+ var FileBrowserStore = class FileBrowserStore {
1974
+ static #locks = /* @__PURE__ */ new Map();
1997
1975
  #root;
1998
1976
  #limit;
1999
1977
  constructor(options) {
2000
1978
  this.#limit = options.limit ?? 100;
2001
- if (!Number.isSafeInteger(this.#limit) || this.#limit < 1) throw new _src_core.BrowserError("The listing cap must be a positive integer", "BROWSER_JOURNEY_ARGUMENT");
1979
+ if (!Number.isSafeInteger(this.#limit) || this.#limit < 1) throw new _src_core.BrowserError("ARGUMENT", "The listing cap must be a positive integer", { subject: "store" });
2002
1980
  try {
2003
1981
  this.#root = (0, node_fs.realpathSync)(options.root);
2004
1982
  } catch (error) {
2005
- if (error instanceof Error && "code" in error && error.code === "ENOENT") throw new _src_core.BrowserError(`Missing root: ${options.root}`, "BROWSER_JOURNEY_PATH", { path: options.root });
1983
+ if (error instanceof Error && "code" in error && error.code === "ENOENT") throw new _src_core.BrowserError("STORE_PATH", `Missing root: ${options.root}`, { path: options.root });
2006
1984
  throw this.translateError(options.root, error);
2007
1985
  }
2008
1986
  }
2009
1987
  /** Checks a journey name before filesystem access. @param name - Journey name */
2010
1988
  validateName(name) {
2011
- if (!_src_core.BROWSER_JOURNEY_NAME_PATTERN.test(name)) throw new _src_core.BrowserError(`Refused journey name: ${name}`, "BROWSER_JOURNEY_PATH");
1989
+ if (!_src_core.BROWSER_JOURNEY_NAME_PATTERN.test(name)) throw new _src_core.BrowserError("STORE_PATH", `Refused journey name: ${name}`);
2012
1990
  }
2013
1991
  /** Checks a run id before filesystem access. @param id - Producer id */
2014
1992
  validateId(id) {
2015
- if (!_src_core.BROWSER_RUN_ID_PATTERN.test(id)) throw new _src_core.BrowserError(`Refused run id: ${id}`, "BROWSER_JOURNEY_PATH");
1993
+ if (!_src_core.BROWSER_RUN_ID_PATTERN.test(id)) throw new _src_core.BrowserError("STORE_PATH", `Refused run id: ${id}`);
2016
1994
  }
2017
1995
  /** Resolves a confined target. @param parts - Relative components @returns Absolute target */
2018
1996
  resolvePath(...parts) {
2019
1997
  const path = (0, node_path.resolve)(this.#root, ...parts);
2020
- if (path !== this.#root && !path.startsWith(this.#root.endsWith(node_path.sep) ? this.#root : this.#root + node_path.sep)) throw new _src_core.BrowserError(`Refused path: ${path}`, "BROWSER_JOURNEY_PATH");
1998
+ if (path !== this.#root && !path.startsWith(this.#root.endsWith(node_path.sep) ? this.#root : this.#root + node_path.sep)) throw new _src_core.BrowserError("STORE_PATH", `Refused path: ${path}`);
2021
1999
  return path;
2022
2000
  }
2023
2001
  /**
@@ -2035,8 +2013,8 @@ var FileBrowserStore = class {
2035
2013
  try {
2036
2014
  const status = await (0, node_fs_promises.lstat)(current);
2037
2015
  options?.signal?.throwIfAborted();
2038
- if (status.isSymbolicLink()) throw new _src_core.BrowserError(`Refused symbolic link: ${current}`, "BROWSER_JOURNEY_PATH");
2039
- if (index < parts.length && !status.isDirectory()) throw new _src_core.BrowserError(`Not a directory: ${current}`, "BROWSER_JOURNEY_PATH");
2016
+ if (status.isSymbolicLink()) throw new _src_core.BrowserError("STORE_PATH", `Refused symbolic link: ${current}`);
2017
+ if (index < parts.length && !status.isDirectory()) throw new _src_core.BrowserError("STORE_PATH", `Not a directory: ${current}`);
2040
2018
  } catch (error) {
2041
2019
  options?.signal?.throwIfAborted();
2042
2020
  if (error instanceof Error && "code" in error && error.code === "ENOENT") return false;
@@ -2050,7 +2028,7 @@ var FileBrowserStore = class {
2050
2028
  /** Creates checked directories one component at a time. @param path - Directory @param options - Cancellation options */
2051
2029
  async createDirectory(path, options) {
2052
2030
  if (await this.check(path, options)) return;
2053
- if (path === this.#root) throw new _src_core.BrowserError(`Missing root: ${path}`, "BROWSER_JOURNEY_PATH");
2031
+ if (path === this.#root) throw new _src_core.BrowserError("STORE_PATH", `Missing root: ${path}`);
2054
2032
  await this.createDirectory((0, node_path.dirname)(path), options);
2055
2033
  options?.signal?.throwIfAborted();
2056
2034
  try {
@@ -2100,7 +2078,7 @@ var FileBrowserStore = class {
2100
2078
  await (0, node_fs_promises.rename)(temporary, path);
2101
2079
  } catch (error) {
2102
2080
  const target = await (0, node_fs_promises.lstat)(path).catch(() => void 0);
2103
- if (error instanceof Error && "code" in error && (error.code === "EISDIR" || error.code === "EPERM") && target?.isDirectory()) throw new _src_core.BrowserError(`Cannot replace directory: ${path}`, "BROWSER_JOURNEY_FILE", { path });
2081
+ if (error instanceof Error && "code" in error && (error.code === "EISDIR" || error.code === "EPERM") && target?.isDirectory()) throw new _src_core.BrowserError("STORE_FILE", `Cannot replace directory: ${path}`, { path });
2104
2082
  throw error;
2105
2083
  }
2106
2084
  owned = false;
@@ -2131,6 +2109,20 @@ var FileBrowserStore = class {
2131
2109
  * @returns The mutation result
2132
2110
  */
2133
2111
  async lock(path, action, options) {
2112
+ const key = this.resolvePath(path);
2113
+ const previous = FileBrowserStore.#locks.get(key);
2114
+ const released = Promise.withResolvers();
2115
+ FileBrowserStore.#locks.set(key, released.promise);
2116
+ try {
2117
+ await previous;
2118
+ options?.signal?.throwIfAborted();
2119
+ return await this.#lockFile(key, action, options);
2120
+ } finally {
2121
+ released.resolve();
2122
+ if (FileBrowserStore.#locks.get(key) === released.promise) FileBrowserStore.#locks.delete(key);
2123
+ }
2124
+ }
2125
+ async #lockFile(path, action, options) {
2134
2126
  await this.createDirectory((0, node_path.dirname)(path), options);
2135
2127
  const name = formatBrowserLockEntry(process.pid, (0, node_crypto.randomUUID)());
2136
2128
  const entry = this.resolvePath(path, name);
@@ -2173,7 +2165,7 @@ var FileBrowserStore = class {
2173
2165
  await this.#removeLock(path);
2174
2166
  }
2175
2167
  }
2176
- throw new _src_core.BrowserError(`Journey is locked: ${path}`, "BROWSER_JOURNEY_LOCKED");
2168
+ throw new _src_core.BrowserError("STORE_LOCKED", `Journey is locked: ${path}`);
2177
2169
  }
2178
2170
  /**
2179
2171
  * Lists readable entries in sorted order with a bounded page and one extra readable probe.
@@ -2220,7 +2212,7 @@ var FileBrowserStore = class {
2220
2212
  const reason = error instanceof _src_core.BrowserError ? error.context?.["reason"] : void 0;
2221
2213
  faults.push({
2222
2214
  name,
2223
- reason: typeof reason === "string" ? reason : error instanceof _src_core.BrowserError && error.code === "BROWSER_JOURNEY_PATH" ? "The entry path is refused" : error instanceof _src_core.BrowserError && error.code === "BROWSER_JOURNEY_ACCESS" ? "Permission denied" : error instanceof _src_core.BrowserError && error.code === "BROWSER_JOURNEY_FORMAT" ? "Unknown file format" : "The stored entry is malformed or unreadable"
2215
+ reason: typeof reason === "string" ? reason : error instanceof _src_core.BrowserError && error.code === "STORE_PATH" ? "The entry path is refused" : error instanceof _src_core.BrowserError && error.code === "STORE_ACCESS" ? "Permission denied" : error instanceof _src_core.BrowserError && error.code === "STORE_FORMAT" ? "Unknown file format" : "The stored entry is malformed or unreadable"
2224
2216
  });
2225
2217
  }
2226
2218
  }
@@ -2263,7 +2255,7 @@ var FileBrowserStore = class {
2263
2255
  /** Translates filesystem and validation failures with their path. @param path - Failed path @param error - Cause @returns Coded error */
2264
2256
  translateError(path, error) {
2265
2257
  const code = error instanceof Error && "code" in error ? error.code : void 0;
2266
- return new _src_core.BrowserError(`${path}: ${error instanceof Error ? error.message : String(error)}`, code === "EACCES" || code === "EPERM" ? "BROWSER_JOURNEY_ACCESS" : error instanceof _src_core.BrowserError && code !== "BROWSER_JOURNEY_INVALID" ? error.code : "BROWSER_JOURNEY_FILE", { path });
2258
+ return new _src_core.BrowserError(code === "EACCES" || code === "EPERM" ? "STORE_ACCESS" : error instanceof _src_core.BrowserError && code !== "JOURNEY_INVALID" ? error.code : "STORE_FILE", `${path}: ${error instanceof Error ? error.message : String(error)}`, { path });
2267
2259
  }
2268
2260
  async #readLock(path, options) {
2269
2261
  await this.check(path, options);
@@ -2271,13 +2263,13 @@ var FileBrowserStore = class {
2271
2263
  const entries = await (0, node_fs_promises.readdir)(path, { withFileTypes: true });
2272
2264
  for (const entry of entries) {
2273
2265
  await this.check(this.resolvePath(path, entry.name), options);
2274
- if (!entry.isFile()) throw new _src_core.BrowserError(`Journey is locked: ${path}`, "BROWSER_JOURNEY_LOCKED");
2266
+ if (!entry.isFile()) throw new _src_core.BrowserError("STORE_LOCKED", `Journey is locked: ${path}`);
2275
2267
  }
2276
2268
  return entries.map((entry) => entry.name);
2277
2269
  } catch (error) {
2278
2270
  if (error instanceof Error && "code" in error) {
2279
2271
  if (error.code === "ENOENT") return void 0;
2280
- if (error.code === "ENOTDIR") throw new _src_core.BrowserError(`Journey is locked: ${path}`, "BROWSER_JOURNEY_LOCKED");
2272
+ if (error.code === "ENOTDIR") throw new _src_core.BrowserError("STORE_LOCKED", `Journey is locked: ${path}`);
2281
2273
  }
2282
2274
  throw this.translateError(path, error);
2283
2275
  }
@@ -2289,7 +2281,7 @@ var FileBrowserStore = class {
2289
2281
  return true;
2290
2282
  } catch (error) {
2291
2283
  if (error instanceof Error && "code" in error && (error.code === "ENOENT" || error.code === "ENOTEMPTY")) return false;
2292
- throw new _src_core.BrowserError(`Cannot remove lock entry: ${path}`, "BROWSER_JOURNEY_ACCESS", { path });
2284
+ throw new _src_core.BrowserError("STORE_ACCESS", `Cannot remove lock entry: ${path}`, { path });
2293
2285
  }
2294
2286
  }
2295
2287
  async #removeLock(path) {
@@ -2305,7 +2297,7 @@ var FileBrowserStore = class {
2305
2297
  if (cause instanceof Error && "code" in cause && cause.code === "ENOENT") return;
2306
2298
  }
2307
2299
  }
2308
- throw new _src_core.BrowserError(`Cannot remove lock directory: ${path}`, "BROWSER_JOURNEY_ACCESS", { path });
2300
+ throw new _src_core.BrowserError("STORE_ACCESS", `Cannot remove lock directory: ${path}`, { path });
2309
2301
  }
2310
2302
  }
2311
2303
  };
@@ -2331,24 +2323,29 @@ var FileBrowserJourneyStore = class {
2331
2323
  if (source === void 0) return void 0;
2332
2324
  try {
2333
2325
  const value = (0, _orkestrel_contract.parseJSON)(source);
2334
- if (!(0, _orkestrel_contract.isRecord)(value) || typeof value["revision"] !== "number" || !Number.isSafeInteger(value["revision"]) || value["revision"] < 1) throw new _src_core.BrowserError("Malformed journey revision", "BROWSER_JOURNEY_FILE");
2335
- if (!(0, _orkestrel_contract.isRecord)(value["journey"]) || !("format" in value["journey"])) throw new _src_core.BrowserError("Missing journey format", "BROWSER_JOURNEY_FILE");
2326
+ if (!(0, _orkestrel_contract.isRecord)(value) || typeof value["revision"] !== "number" || !Number.isSafeInteger(value["revision"]) || value["revision"] < 1) throw new _src_core.BrowserError("STORE_FILE", "Malformed journey revision");
2327
+ if (!(0, _orkestrel_contract.isRecord)(value["journey"]) || !("format" in value["journey"])) throw new _src_core.BrowserError("STORE_FILE", "Missing journey format");
2336
2328
  (0, _src_core.validateBrowserJourney)(value["journey"]);
2337
- if (value["journey"].name !== name) throw new _src_core.BrowserError("Journey name differs from its directory", "BROWSER_JOURNEY_FILE");
2329
+ if (value["journey"].name !== name) throw new _src_core.BrowserError("STORE_FILE", "Journey name differs from its directory");
2338
2330
  return {
2339
2331
  journey: value["journey"],
2340
2332
  revision: value["revision"]
2341
2333
  };
2342
2334
  } catch (error) {
2343
2335
  const translated = this.#files.translateError(path, error);
2344
- throw new _src_core.BrowserError(translated.message, translated.code, {
2336
+ throw new _src_core.BrowserError(translated.code, translated.message, {
2345
2337
  ...translated.context,
2346
2338
  reason: (0, _src_core.normalizeBrowserJourneyReason)(error)
2347
2339
  });
2348
2340
  }
2349
2341
  }
2350
- async set(journey, expected, options) {
2342
+ async set(journey, options) {
2351
2343
  options?.signal?.throwIfAborted();
2344
+ (0, _src_core.validateBrowserJourneyWriteOptions)(options);
2345
+ const condition = {
2346
+ revision: options?.revision,
2347
+ exclusive: options?.exclusive
2348
+ };
2352
2349
  this.#files.validateName(journey.name);
2353
2350
  (0, _src_core.validateBrowserJourney)(journey);
2354
2351
  const owned = structuredClone(journey);
@@ -2356,12 +2353,12 @@ var FileBrowserJourneyStore = class {
2356
2353
  const counter = this.#files.resolvePath(owned.name, BROWSER_JOURNEY_REVISION_FILE);
2357
2354
  return this.#files.lock(this.#files.resolvePath(owned.name, BROWSER_JOURNEY_LOCK_DIRECTORY), async () => {
2358
2355
  const current = await this.get(owned.name, options);
2359
- if (expected !== void 0 && expected !== (current?.revision ?? 0)) throw new _src_core.BrowserError(`Journey ${owned.name} changed since you read it`, "BROWSER_JOURNEY_STALE");
2356
+ if (condition.exclusive === true && current !== void 0 || condition.revision !== void 0 && condition.revision !== current?.revision) throw new _src_core.BrowserError("JOURNEY_STALE", `Journey ${owned.name} changed since you read it`);
2360
2357
  const source = await this.#files.read(counter, options);
2361
2358
  const previous = source === void 0 ? 0 : Number(source);
2362
- if (!Number.isSafeInteger(previous) || previous < 0 || source?.trim() === "") throw new _src_core.BrowserError(`Malformed revision: ${counter}`, "BROWSER_JOURNEY_FILE");
2359
+ if (!Number.isSafeInteger(previous) || previous < 0 || source?.trim() === "") throw new _src_core.BrowserError("STORE_FILE", `Malformed revision: ${counter}`);
2363
2360
  const revision = Math.max(previous, current?.revision ?? 0) + 1;
2364
- if (!Number.isSafeInteger(revision)) throw new _src_core.BrowserError(`Exhausted revision: ${counter}`, "BROWSER_JOURNEY_FILE");
2361
+ if (!Number.isSafeInteger(revision)) throw new _src_core.BrowserError("STORE_FILE", `Exhausted revision: ${counter}`);
2365
2362
  const saved = {
2366
2363
  journey: owned,
2367
2364
  revision
@@ -2386,7 +2383,7 @@ var FileBrowserJourneyStore = class {
2386
2383
  const counter = this.#files.resolvePath(name, BROWSER_JOURNEY_REVISION_FILE);
2387
2384
  const source = await this.#files.read(counter, options);
2388
2385
  const previous = source === void 0 ? 0 : Number(source);
2389
- if (!Number.isSafeInteger(previous) || previous < 0 || source?.trim() === "") throw new _src_core.BrowserError(`Malformed revision: ${counter}`, "BROWSER_JOURNEY_FILE");
2386
+ if (!Number.isSafeInteger(previous) || previous < 0 || source?.trim() === "") throw new _src_core.BrowserError("STORE_FILE", `Malformed revision: ${counter}`);
2390
2387
  await this.#files.write(counter, String(Math.max(previous, current.revision ?? 0)), options);
2391
2388
  await this.#files.remove(this.#files.resolvePath(name, BROWSER_JOURNEY_SNAPSHOT_FILE), options);
2392
2389
  }, options);
@@ -2408,7 +2405,7 @@ var FileBrowserJourneyStore = class {
2408
2405
  * still refuses conflicting mutations and allocations from another process.
2409
2406
  * @example
2410
2407
  * const store = new FileBrowserRunStore({ root: directory })
2411
- * const slot = await store.open('check-ready')
2408
+ * const slot = await store.create('check-ready')
2412
2409
  */
2413
2410
  var FileBrowserRunStore = class FileBrowserRunStore {
2414
2411
  static #allocations = /* @__PURE__ */ new Map();
@@ -2418,7 +2415,7 @@ var FileBrowserRunStore = class FileBrowserRunStore {
2418
2415
  constructor(options) {
2419
2416
  this.#files = new FileBrowserStore(options);
2420
2417
  }
2421
- async snapshot(bytes, options) {
2418
+ async write(bytes, options) {
2422
2419
  options?.signal?.throwIfAborted();
2423
2420
  const owned = new Uint8Array(bytes);
2424
2421
  const directory = await this.#files.allocate(this.#files.resolvePath(BROWSER_RUN_DIRECTORY), _src_core.generateBrowserRunId, options);
@@ -2436,7 +2433,7 @@ var FileBrowserRunStore = class FileBrowserRunStore {
2436
2433
  throw this.#files.translateError(path, error);
2437
2434
  }
2438
2435
  }
2439
- async open(name, options) {
2436
+ async create(name, options) {
2440
2437
  options?.signal?.throwIfAborted();
2441
2438
  this.#files.validateName(name);
2442
2439
  const path = this.#files.resolvePath(name, BROWSER_JOURNEY_LOCK_DIRECTORY);
@@ -2470,10 +2467,10 @@ var FileBrowserRunStore = class FileBrowserRunStore {
2470
2467
  if (source === void 0) return void 0;
2471
2468
  try {
2472
2469
  const value = (0, _orkestrel_contract.parseJSON)(source);
2473
- if (!(0, _orkestrel_contract.isRecord)(value) || !("format" in value)) throw new _src_core.BrowserError("Missing run format", "BROWSER_JOURNEY_FILE");
2474
- if (value["format"] === _src_core.BROWSER_JOURNEY_FORMAT_VERSION && (!(0, _orkestrel_contract.isRecord)(value["journey"]) || !("format" in value["journey"]))) throw new _src_core.BrowserError("Missing run journey format", "BROWSER_JOURNEY_FILE");
2470
+ if (!(0, _orkestrel_contract.isRecord)(value) || !("format" in value)) throw new _src_core.BrowserError("STORE_FILE", "Missing run format");
2471
+ if (value["format"] === _src_core.BROWSER_JOURNEY_FORMAT_VERSION && (!(0, _orkestrel_contract.isRecord)(value["journey"]) || !("format" in value["journey"]))) throw new _src_core.BrowserError("STORE_FILE", "Missing run journey format");
2475
2472
  (0, _src_core.validateBrowserRun)(value);
2476
- if (value.id !== id || value.journey.name !== name) throw new _src_core.BrowserError("Run identity differs from its directory", "BROWSER_JOURNEY_FILE");
2473
+ if (value.id !== id || value.journey.name !== name) throw new _src_core.BrowserError("STORE_FILE", "Run identity differs from its directory");
2477
2474
  return value;
2478
2475
  } catch (error) {
2479
2476
  throw this.#files.translateError(path, error);
@@ -2492,7 +2489,7 @@ var FileBrowserRunStore = class FileBrowserRunStore {
2492
2489
  async capture(slot, name, bytes, options) {
2493
2490
  options?.signal?.throwIfAborted();
2494
2491
  const directory = this.#slots.get(slot);
2495
- if (directory === void 0 || !/^s[1-9]\d*\.png$/.test(name)) throw new _src_core.BrowserError("Capture requires an opened slot and an sN.png name", "BROWSER_JOURNEY_PATH");
2492
+ if (directory === void 0 || !/^s[1-9]\d*\.png$/.test(name)) throw new _src_core.BrowserError("STORE_PATH", "Capture requires an opened slot and an sN.png name");
2496
2493
  const owned = new Uint8Array(bytes);
2497
2494
  await this.#requireDirectory(directory, options);
2498
2495
  await this.#files.write(this.#files.resolvePath(directory, name), owned, options);
@@ -2545,12 +2542,12 @@ var FileBrowserRunStore = class FileBrowserRunStore {
2545
2542
  return this.#files.list(this.#files.resolvePath(name, BROWSER_RUN_DIRECTORY), (id) => this.get(name, id, options), options);
2546
2543
  }
2547
2544
  async #requireDirectory(directory, options) {
2548
- if (!this.#directories.has(directory) || !await this.#files.check(directory, options)) throw new _src_core.BrowserError(`Run directory was not opened or is missing: ${directory}`, "BROWSER_JOURNEY_PATH");
2545
+ if (!this.#directories.has(directory) || !await this.#files.check(directory, options)) throw new _src_core.BrowserError("STORE_PATH", `Run directory was not opened or is missing: ${directory}`);
2549
2546
  try {
2550
2547
  options?.signal?.throwIfAborted();
2551
2548
  const status = await (0, node_fs_promises.lstat)(directory);
2552
2549
  options?.signal?.throwIfAborted();
2553
- if (!status.isDirectory()) throw new _src_core.BrowserError(`Run directory is not a directory: ${directory}`, "BROWSER_JOURNEY_PATH");
2550
+ if (!status.isDirectory()) throw new _src_core.BrowserError("STORE_PATH", `Run directory is not a directory: ${directory}`);
2554
2551
  } catch (error) {
2555
2552
  options?.signal?.throwIfAborted();
2556
2553
  throw this.#files.translateError(directory, error);
@@ -2587,7 +2584,7 @@ function createBrowser(options) {
2587
2584
  * @param options - The CDP WebSocket debugger URL (and optional timeout)
2588
2585
  * @returns A {@link CDPTransportInterface}
2589
2586
  */
2590
- function createCDPTransport(options) {
2587
+ function createWebSocketCDPTransport(options) {
2591
2588
  return new WebSocketCDPTransport(options);
2592
2589
  }
2593
2590
  /**
@@ -2596,7 +2593,7 @@ function createCDPTransport(options) {
2596
2593
  *
2597
2594
  * @returns A {@link BrowserWriterInterface} that persists bytes through `node:fs/promises`
2598
2595
  */
2599
- function createBrowserWriter() {
2596
+ function createFileBrowserWriter() {
2600
2597
  return new FileBrowserWriter();
2601
2598
  }
2602
2599
  /**
@@ -2630,7 +2627,7 @@ function createFileBrowserRunStore(options) {
2630
2627
  * ```ts
2631
2628
  * import { createBrowserMCPServer } from '@orkestrel/browser/server'
2632
2629
  *
2633
- * const server = createBrowserMCPServer({ root: 'tmp/browsers', headless: true })
2630
+ * const server = createBrowserMCPServer({ root: 'tmp/browsers', browser: { headless: true } })
2634
2631
  * await server.start() // resolves after leasing a connected browser
2635
2632
  * await server.destroy()
2636
2633
  * ```
@@ -2685,7 +2682,7 @@ var Browser = class {
2685
2682
  ...options?.error !== void 0 ? { error: options.error } : {}
2686
2683
  });
2687
2684
  this.#options = options ?? {};
2688
- this.#engine = this.#options.engine ?? (this.#options.executable !== void 0 ? parseBrowserEngine(this.#options.executable) ?? "chromium" : "chromium");
2685
+ this.#engine = this.#options.executable !== void 0 ? parseBrowserEngine(this.#options.executable) ?? "chromium" : this.#options.browsers?.engine ?? "chromium";
2689
2686
  this.#cdpPort = this.#options.cdp?.port ?? 9222;
2690
2687
  this.#cdpHost = this.#options.cdp?.host ?? "127.0.0.1";
2691
2688
  queueMicrotask(() => this.#emitter.emit("idle"));
@@ -2712,9 +2709,9 @@ var Browser = class {
2712
2709
  return this.#endpoint;
2713
2710
  }
2714
2711
  async ping(options) {
2715
- if (this.#destroyed) throw new BrowserDestroyedError();
2712
+ if (this.#destroyed) throw new _src_core.BrowserError("CLOSED", "Browser has been destroyed", { subject: "browser" });
2716
2713
  const client = this.#client;
2717
- if (this.#status !== "connected" || client === void 0) throw new BrowserNotConnectedError();
2714
+ if (this.#status !== "connected" || client === void 0) throw new _src_core.BrowserError("DISCONNECTED", "Browser is not connected");
2718
2715
  await client.send("Browser.getVersion", void 0, options);
2719
2716
  }
2720
2717
  async discover() {
@@ -2723,7 +2720,7 @@ var Browser = class {
2723
2720
  return result;
2724
2721
  }
2725
2722
  async connect() {
2726
- if (this.#destroyed) throw new BrowserDestroyedError();
2723
+ if (this.#destroyed) throw new _src_core.BrowserError("CLOSED", "Browser has been destroyed", { subject: "browser" });
2727
2724
  const active = this.#connecting.pending;
2728
2725
  if (active !== void 0) {
2729
2726
  await active;
@@ -2733,8 +2730,8 @@ var Browser = class {
2733
2730
  await this.#connecting.execute(() => this.#establish());
2734
2731
  }
2735
2732
  adopt() {
2736
- if (this.#destroyed) throw new BrowserDestroyedError();
2737
- if (this.#status !== "connected" || this.#client === void 0 || this.#endpoint === void 0) throw new BrowserNotConnectedError();
2733
+ if (this.#destroyed) throw new _src_core.BrowserError("CLOSED", "Browser has been destroyed", { subject: "browser" });
2734
+ if (this.#status !== "connected" || this.#client === void 0 || this.#endpoint === void 0) throw new _src_core.BrowserError("DISCONNECTED", "Browser is not connected");
2738
2735
  this.#owned = true;
2739
2736
  }
2740
2737
  async disconnect() {
@@ -2765,12 +2762,15 @@ var Browser = class {
2765
2762
  }
2766
2763
  }
2767
2764
  async create(options) {
2768
- if (this.#destroyed) throw new BrowserDestroyedError();
2765
+ if (this.#destroyed) throw new _src_core.BrowserError("CLOSED", "Browser has been destroyed", { subject: "browser" });
2769
2766
  const client = this.#client;
2770
- if (this.#status !== "connected" || client === void 0) throw new BrowserNotConnectedError();
2767
+ if (this.#status !== "connected" || client === void 0) throw new _src_core.BrowserError("DISCONNECTED", "Browser is not connected");
2771
2768
  let context = this.#contexts[0];
2772
2769
  if (context === void 0) {
2773
- context = new _src_core.BrowserContext(client, void 0, this.#options.viewport, createBrowserWriter());
2770
+ context = new _src_core.BrowserContext(client, {
2771
+ ...this.#options.viewport === void 0 ? {} : { viewport: this.#options.viewport },
2772
+ writer: createFileBrowserWriter()
2773
+ });
2774
2774
  this.#registerContext(context);
2775
2775
  }
2776
2776
  const page = await context.create(options);
@@ -2798,10 +2798,11 @@ var Browser = class {
2798
2798
  return shutdown;
2799
2799
  }
2800
2800
  async #isolate(options) {
2801
- if (this.#destroyed) throw new BrowserDestroyedError();
2801
+ if (this.#destroyed) throw new _src_core.BrowserError("CLOSED", "Browser has been destroyed", { subject: "browser" });
2802
2802
  const client = this.#client;
2803
- if (this.#status !== "connected" || client === void 0) throw new BrowserNotConnectedError();
2803
+ if (this.#status !== "connected" || client === void 0) throw new _src_core.BrowserError("DISCONNECTED", "Browser is not connected");
2804
2804
  (0, _src_core.validateBrowserContextOptions)(options);
2805
+ if (options !== void 0 && "id" in options && options.id !== void 0) throw new _src_core.BrowserError("ARGUMENT", "An isolated context receives its id from the browser");
2805
2806
  const params = { disposeOnDetach: false };
2806
2807
  if (options?.proxy !== void 0) {
2807
2808
  params["proxyServer"] = options.proxy.server;
@@ -2809,18 +2810,25 @@ var Browser = class {
2809
2810
  }
2810
2811
  if (options?.origins !== void 0) params["originsWithUniversalNetworkAccess"] = [...options.origins];
2811
2812
  const result = await client.send("Target.createBrowserContext", params);
2812
- if (!(0, _orkestrel_contract.isRecord)(result) || !(0, _orkestrel_contract.isString)(result["browserContextId"])) throw new _src_core.BrowserConnectionError("Failed to create isolated browser context");
2813
+ if (!(0, _orkestrel_contract.isRecord)(result) || !(0, _orkestrel_contract.isString)(result["browserContextId"])) throw new _src_core.BrowserError("CONNECTION", "Failed to create isolated browser context");
2813
2814
  const id = result["browserContextId"];
2814
- const context = new _src_core.BrowserContext(client, id, options?.emulation?.viewport ?? this.#options.viewport, createBrowserWriter(), options?.emulation, options?.downloads, options);
2815
+ const { proxy: _proxy, origins: _origins, ...defaults } = options ?? {};
2816
+ const viewport = options?.viewport ?? options?.emulation?.viewport ?? this.#options.viewport;
2817
+ const context = new _src_core.BrowserContext(client, {
2818
+ ...defaults,
2819
+ id,
2820
+ ...viewport === void 0 ? {} : { viewport },
2821
+ writer: options?.writer ?? createFileBrowserWriter()
2822
+ });
2815
2823
  try {
2816
- if (this.#destroyed || this.#client !== client) throw new BrowserDestroyedError();
2824
+ if (this.#destroyed || this.#client !== client) throw new _src_core.BrowserError("CLOSED", "Browser has been destroyed", { subject: "browser" });
2817
2825
  if (options?.downloads !== void 0) await client.send("Browser.setDownloadBehavior", {
2818
2826
  behavior: options.downloads.named === true ? "allowAndName" : "allow",
2819
2827
  browserContextId: id,
2820
2828
  downloadPath: options.downloads.path,
2821
2829
  eventsEnabled: true
2822
2830
  });
2823
- if (this.#destroyed || this.#client !== client) throw new BrowserDestroyedError();
2831
+ if (this.#destroyed || this.#client !== client) throw new _src_core.BrowserError("CLOSED", "Browser has been destroyed", { subject: "browser" });
2824
2832
  } catch (error) {
2825
2833
  await context.close().catch(() => void 0);
2826
2834
  throw error;
@@ -2832,7 +2840,7 @@ var Browser = class {
2832
2840
  const disconnecting = this.#disconnecting.pending;
2833
2841
  if (disconnecting !== void 0) await disconnecting;
2834
2842
  await this.#settleExit();
2835
- if (this.#destroyed) throw new BrowserDestroyedError();
2843
+ if (this.#destroyed) throw new _src_core.BrowserError("CLOSED", "Browser has been destroyed", { subject: "browser" });
2836
2844
  if (this.#status === "connected") return;
2837
2845
  this.#assertNotAborted();
2838
2846
  this.#status = "connecting";
@@ -2857,12 +2865,12 @@ var Browser = class {
2857
2865
  this.#assertNotAborted();
2858
2866
  await this.#launch();
2859
2867
  } catch (error) {
2860
- if (this.#destroyed) throw new BrowserDestroyedError();
2868
+ if (this.#destroyed) throw new _src_core.BrowserError("CLOSED", "Browser has been destroyed", { subject: "browser" });
2861
2869
  this.#status = "error";
2862
2870
  this.#emitter.emit("error", error);
2863
- if ((0, _src_core.isBrowserConnectionError)(error)) throw error;
2871
+ if ((0, _src_core.isBrowserError)(error) && error.code === "CONNECTION") throw error;
2864
2872
  const message = (0, _orkestrel_contract.isError)(error) ? error.message : String(error);
2865
- throw new _src_core.BrowserConnectionError(message, { executable: this.#options.executable });
2873
+ throw new _src_core.BrowserError("CONNECTION", message, this.#options.executable === void 0 ? void 0 : { executable: this.#options.executable });
2866
2874
  }
2867
2875
  }
2868
2876
  async #detach() {
@@ -2883,7 +2891,7 @@ var Browser = class {
2883
2891
  this.#emitter.emit("idle");
2884
2892
  }
2885
2893
  #assertNotAborted() {
2886
- if (this.#signal().aborted) throw new _src_core.BrowserConnectionError("Connection aborted");
2894
+ if (this.#signal().aborted) throw new _src_core.BrowserError("CONNECTION", "Connection aborted");
2887
2895
  }
2888
2896
  #signal() {
2889
2897
  const external = this.#options.signal;
@@ -2891,10 +2899,10 @@ var Browser = class {
2891
2899
  }
2892
2900
  async #raceAbort(promise) {
2893
2901
  const signal = this.#signal();
2894
- if (signal.aborted) throw new _src_core.BrowserConnectionError("Connection aborted");
2902
+ if (signal.aborted) throw new _src_core.BrowserError("CONNECTION", "Connection aborted");
2895
2903
  const aborted = Promise.withResolvers();
2896
2904
  const listener = (0, node_events.addAbortListener)(signal, () => {
2897
- aborted.reject(new _src_core.BrowserConnectionError("Connection aborted"));
2905
+ aborted.reject(new _src_core.BrowserError("CONNECTION", "Connection aborted"));
2898
2906
  });
2899
2907
  try {
2900
2908
  return await Promise.race([promise, aborted.promise]);
@@ -2922,7 +2930,7 @@ var Browser = class {
2922
2930
  this.#owned = void 0;
2923
2931
  this.#endpoint = void 0;
2924
2932
  if (connected) this.#status = "disconnected";
2925
- this.#emitter.emit("error", new _src_core.BrowserConnectionError("The browser process exited unexpectedly", { cause: BROWSER_PROCESS_EXIT_CAUSE }));
2933
+ this.#emitter.emit("error", new _src_core.BrowserError("CONNECTION", "The browser process exited unexpectedly", { cause: BROWSER_PROCESS_EXIT_CAUSE }));
2926
2934
  if (connected) {
2927
2935
  this.#emitter.emit("disconnect");
2928
2936
  this.#emitter.emit("idle");
@@ -2960,7 +2968,7 @@ var Browser = class {
2960
2968
  }
2961
2969
  this.#destroyContextList(contexts);
2962
2970
  this.#closeClient(client);
2963
- this.#emitter.emit("error", new _src_core.BrowserConnectionError("The CDP transport connection was lost", { cause: BROWSER_TRANSPORT_LOSS_CAUSE }));
2971
+ this.#emitter.emit("error", new _src_core.BrowserError("CONNECTION", "The CDP transport connection was lost", { cause: BROWSER_TRANSPORT_LOSS_CAUSE }));
2964
2972
  this.#emitter.emit("disconnect");
2965
2973
  this.#emitter.emit("idle");
2966
2974
  }
@@ -3013,7 +3021,7 @@ var Browser = class {
3013
3021
  };
3014
3022
  }
3015
3023
  async #assertPortFree() {
3016
- if (await this.#probePort()) throw new _src_core.BrowserConnectionError(`Port ${this.#cdpPort} on ${this.#cdpHost} is already occupied by another CDP endpoint`, {
3024
+ if (await this.#probePort()) throw new _src_core.BrowserError("CONNECTION", `Port ${this.#cdpPort} on ${this.#cdpHost} is already occupied by another CDP endpoint`, {
3017
3025
  port: this.#cdpPort,
3018
3026
  host: this.#cdpHost
3019
3027
  });
@@ -3027,7 +3035,7 @@ var Browser = class {
3027
3035
  }
3028
3036
  }
3029
3037
  async #connectCDP(endpoint) {
3030
- const transport = createCDPTransport({
3038
+ const transport = createWebSocketCDPTransport({
3031
3039
  url: endpoint,
3032
3040
  timeout: this.#timeout()
3033
3041
  });
@@ -3060,20 +3068,20 @@ var Browser = class {
3060
3068
  }
3061
3069
  }
3062
3070
  async #launch() {
3063
- if (this.#process !== void 0) throw new _src_core.BrowserConnectionError("A browser process is already active on this instance");
3064
- const requestedEngine = this.#options.engine ?? this.#options.browsers?.engine;
3071
+ if (this.#process !== void 0) throw new _src_core.BrowserError("CONNECTION", "A browser process is already active on this instance");
3072
+ const requestedEngine = this.#options.browsers?.engine;
3065
3073
  let executable = this.#options.executable;
3066
3074
  let resolvedEngine;
3067
3075
  if (executable !== void 0) resolvedEngine = parseBrowserEngine(executable) ?? "chromium";
3068
3076
  else {
3069
- const found = findSystemBrowser({
3077
+ const found = findSystemBrowsers({
3070
3078
  ...this.#options.browsers,
3071
3079
  ...requestedEngine !== void 0 ? { engine: requestedEngine } : {}
3072
- });
3080
+ })[0];
3073
3081
  executable = found?.executable;
3074
3082
  resolvedEngine = found?.engine;
3075
3083
  }
3076
- if (executable === void 0) throw new _src_core.BrowserConnectionError("No Chromium browser found. Install Chrome, Edge, or Chromium.", requestedEngine === void 0 ? void 0 : { engine: requestedEngine });
3084
+ if (executable === void 0) throw new _src_core.BrowserError("CONNECTION", "No Chromium browser found. Install Chrome, Edge, or Chromium.", requestedEngine === void 0 ? void 0 : { engine: requestedEngine });
3077
3085
  this.#engine = resolvedEngine ?? "chromium";
3078
3086
  await this.#releaseProfile();
3079
3087
  const profile = await createBrowserProfile(this.#options.profile);
@@ -3089,7 +3097,7 @@ var Browser = class {
3089
3097
  let client;
3090
3098
  try {
3091
3099
  const endpoint = await this.#waitForLaunch(process, executable, this.#options.args);
3092
- const transport = createCDPTransport({
3100
+ const transport = createWebSocketCDPTransport({
3093
3101
  url: endpoint,
3094
3102
  timeout: this.#timeout()
3095
3103
  });
@@ -3153,9 +3161,9 @@ var Browser = class {
3153
3161
  }
3154
3162
  if (pid === void 0) {
3155
3163
  await this.#closeRemote(client);
3156
- throw new _src_core.BrowserConnectionError("The browser launcher exited without naming the process serving its CDP endpoint", {
3157
- executable: this.#options.executable,
3158
- pid: process.pid
3164
+ throw new _src_core.BrowserError("CONNECTION", "The browser launcher exited without naming the process serving its CDP endpoint", {
3165
+ ...this.#options.executable === void 0 ? {} : { executable: this.#options.executable },
3166
+ ...process.pid === void 0 ? {} : { pid: process.pid }
3159
3167
  });
3160
3168
  }
3161
3169
  this.#servingPid = pid;
@@ -3163,7 +3171,7 @@ var Browser = class {
3163
3171
  async #waitForLaunch(process, executable, args) {
3164
3172
  const context = {
3165
3173
  executable,
3166
- args
3174
+ ...args === void 0 ? {} : { args }
3167
3175
  };
3168
3176
  const timeout = this.#timeout();
3169
3177
  const controller = new AbortController();
@@ -3174,32 +3182,32 @@ var Browser = class {
3174
3182
  deadline
3175
3183
  ]);
3176
3184
  const stderr = process.stderr;
3177
- if (stderr === null) throw new _src_core.BrowserConnectionError("The browser process has no standard error to read", context);
3185
+ if (stderr === null) throw new _src_core.BrowserError("CONNECTION", "The browser process has no standard error to read", context);
3178
3186
  const exit = (0, node_events.once)(process, "exit", { signal });
3179
3187
  exit.catch(() => void 0);
3180
3188
  const ready = readBrowserEndpoint(stderr, signal).catch(async (error) => {
3181
3189
  if (signal.aborted) throw error;
3182
3190
  const values = await exit;
3183
3191
  if (values[0] === 0 && values[1] === null) throw error;
3184
- throw new _src_core.BrowserConnectionError(this.#formatLaunchExit((0, _orkestrel_contract.isInteger)(values[0]) ? values[0] : null, (0, _orkestrel_contract.isString)(values[1]) ? values[1] : null), context);
3192
+ throw new _src_core.BrowserError("CONNECTION", this.#formatLaunchExit((0, _orkestrel_contract.isInteger)(values[0]) ? values[0] : null, (0, _orkestrel_contract.isString)(values[1]) ? values[1] : null), context);
3185
3193
  });
3186
3194
  const exited = exit.then((values) => {
3187
3195
  const code = (0, _orkestrel_contract.isInteger)(values[0]) ? values[0] : null;
3188
3196
  const exitSignal = (0, _orkestrel_contract.isString)(values[1]) ? values[1] : null;
3189
3197
  if (code === 0 && exitSignal === null) return ready;
3190
- throw new _src_core.BrowserConnectionError(this.#formatLaunchExit(code, exitSignal), context);
3198
+ throw new _src_core.BrowserError("CONNECTION", this.#formatLaunchExit(code, exitSignal), context);
3191
3199
  });
3192
3200
  try {
3193
3201
  return await Promise.race([ready, exited]);
3194
3202
  } catch (error) {
3195
- if ((0, _src_core.isBrowserConnectionError)(error)) throw error;
3196
- if (this.#signal().aborted) throw new _src_core.BrowserConnectionError("Connection aborted", context);
3197
- if (deadline.aborted) throw new _src_core.BrowserConnectionError(`Browser did not report a CDP endpoint within ${timeout}ms`, {
3203
+ if ((0, _src_core.isBrowserError)(error) && error.code === "CONNECTION") throw error;
3204
+ if (this.#signal().aborted) throw new _src_core.BrowserError("CONNECTION", "Connection aborted", context);
3205
+ if (deadline.aborted) throw new _src_core.BrowserError("CONNECTION", `Browser did not report a CDP endpoint within ${timeout}ms`, {
3198
3206
  ...context,
3199
3207
  timeout
3200
3208
  });
3201
3209
  const message = (0, _orkestrel_contract.isError)(error) ? error.message : String(error);
3202
- throw new _src_core.BrowserConnectionError(message, context);
3210
+ throw new _src_core.BrowserError("CONNECTION", message, context);
3203
3211
  } finally {
3204
3212
  controller.abort();
3205
3213
  ready.catch(() => void 0);
@@ -3236,7 +3244,10 @@ var Browser = class {
3236
3244
  });
3237
3245
  }
3238
3246
  if (pages.length === 0 || this.#contexts.length > 0) return;
3239
- const context = new _src_core.BrowserContext(client, void 0, this.#options.viewport, createBrowserWriter());
3247
+ const context = new _src_core.BrowserContext(client, {
3248
+ ...this.#options.viewport === void 0 ? {} : { viewport: this.#options.viewport },
3249
+ writer: createFileBrowserWriter()
3250
+ });
3240
3251
  await context.sync(pages);
3241
3252
  this.#registerContext(context);
3242
3253
  }
@@ -3316,7 +3327,7 @@ var Browser = class {
3316
3327
  let remote = client;
3317
3328
  let temporary = false;
3318
3329
  if (remote === void 0 && this.#owned === true && this.#endpoint !== void 0) {
3319
- const transport = createCDPTransport({
3330
+ const transport = createWebSocketCDPTransport({
3320
3331
  url: this.#endpoint,
3321
3332
  timeout: this.#timeout()
3322
3333
  });
@@ -3419,10 +3430,11 @@ var Browser = class {
3419
3430
  const code = (0, _orkestrel_contract.isError)(error) && "code" in error && (0, _orkestrel_contract.isString)(error.code) ? error.code : void 0;
3420
3431
  if (code === "ESRCH") return false;
3421
3432
  if (signal === 0) return code === "EPERM" ? true : void 0;
3422
- throw new _src_core.BrowserConnectionError("Failed to signal the browser process", {
3423
- pid: serving ?? pid,
3433
+ const target = serving ?? pid;
3434
+ throw new _src_core.BrowserError("CONNECTION", "Failed to signal the browser process", {
3435
+ ...target === void 0 ? {} : { pid: target },
3424
3436
  signal,
3425
- cause: error
3437
+ cause: (0, _orkestrel_contract.isJSONValue)(error) ? error : String(error)
3426
3438
  });
3427
3439
  }
3428
3440
  }
@@ -3461,7 +3473,8 @@ var Browser = class {
3461
3473
  if (await this.#waitForTerminationWithin(process, 3e3)) return;
3462
3474
  if (this.#signalProcess(process, "SIGKILL") === false) return;
3463
3475
  if (await this.#waitForTerminationWithin(process, 3e3, true)) return;
3464
- throw new _src_core.BrowserConnectionError("Browser process did not exit after SIGKILL", { pid: this.#servingPid ?? process.pid });
3476
+ const target = this.#servingPid ?? process.pid;
3477
+ throw new _src_core.BrowserError("CONNECTION", "Browser process did not exit after SIGKILL", { ...target === void 0 ? {} : { pid: target } });
3465
3478
  } finally {
3466
3479
  await this.#closeProcessPipe(process);
3467
3480
  }
@@ -3541,9 +3554,7 @@ exports.BROWSER_TRANSPORT_LOSS_DEFER_MS = BROWSER_TRANSPORT_LOSS_DEFER_MS;
3541
3554
  exports.BROWSER_WINDOWS_ROOT_FALLBACKS = BROWSER_WINDOWS_ROOT_FALLBACKS;
3542
3555
  exports.BROWSER_WINDOWS_SUFFIXES = BROWSER_WINDOWS_SUFFIXES;
3543
3556
  exports.Browser = Browser;
3544
- exports.BrowserDestroyedError = BrowserDestroyedError;
3545
3557
  exports.BrowserMCPServer = BrowserMCPServer;
3546
- exports.BrowserNotConnectedError = BrowserNotConnectedError;
3547
3558
  exports.FileBrowserJourneyStore = FileBrowserJourneyStore;
3548
3559
  exports.FileBrowserRunStore = FileBrowserRunStore;
3549
3560
  exports.FileBrowserWriter = FileBrowserWriter;
@@ -3555,20 +3566,17 @@ exports.buildWindowsRoots = buildWindowsRoots;
3555
3566
  exports.createBrowser = createBrowser;
3556
3567
  exports.createBrowserMCPServer = createBrowserMCPServer;
3557
3568
  exports.createBrowserProfile = createBrowserProfile;
3558
- exports.createBrowserWriter = createBrowserWriter;
3559
- exports.createCDPTransport = createCDPTransport;
3560
3569
  exports.createFileBrowserJourneyStore = createFileBrowserJourneyStore;
3561
3570
  exports.createFileBrowserRunStore = createFileBrowserRunStore;
3571
+ exports.createFileBrowserWriter = createFileBrowserWriter;
3572
+ exports.createWebSocketCDPTransport = createWebSocketCDPTransport;
3562
3573
  exports.describeBrowserServerLoss = describeBrowserServerLoss;
3563
3574
  exports.fetchCDPTargets = fetchCDPTargets;
3564
3575
  exports.findEnvOverrides = findEnvOverrides;
3565
3576
  exports.findInstallPaths = findInstallPaths;
3566
3577
  exports.findStorePaths = findStorePaths;
3567
- exports.findSystemBrowser = findSystemBrowser;
3568
3578
  exports.findSystemBrowsers = findSystemBrowsers;
3569
3579
  exports.formatBrowserLockEntry = formatBrowserLockEntry;
3570
- exports.isBrowserDestroyedError = isBrowserDestroyedError;
3571
- exports.isBrowserNotConnectedError = isBrowserNotConnectedError;
3572
3580
  exports.launchBrowserProcess = launchBrowserProcess;
3573
3581
  exports.normalizeExecutablePath = normalizeExecutablePath;
3574
3582
  exports.parseBrowserEngine = parseBrowserEngine;