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