@orkestrel/browser 0.0.26 → 0.0.28

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,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");
@@ -227,9 +227,9 @@ var BROWSER_RUN_DIRECTORY = "runs";
227
227
  /** Bounds a file-store listing page by default. */
228
228
  var BROWSER_FILE_STORE_LIMIT = 100;
229
229
  /** Names the refusal when no browser can serve a call. */
230
- var BROWSER_SERVER_UNAVAILABLE = "BROWSER_SERVER_UNAVAILABLE";
230
+ var BROWSER_SERVER_UNAVAILABLE = "SERVER_UNAVAILABLE";
231
231
  /** Names a failed attempt to warm a browser. */
232
- var BROWSER_SERVER_LAUNCH = "BROWSER_SERVER_LAUNCH";
232
+ var BROWSER_SERVER_LAUNCH = "SERVER_LAUNCH";
233
233
  /** Sets the default number of warm browsers to 1. */
234
234
  var BROWSER_SERVER_POOL_SIZE = 1;
235
235
  /** Sets the default context capacity per browser, including the shared holder. */
@@ -243,21 +243,21 @@ var BROWSER_SERVER_RESTARTS = 1;
243
243
  /** Names the browser process record in each profile. */
244
244
  var BROWSER_SERVER_RECORD = "browse.json";
245
245
  /** Names the diagnostic when the warm floor spends its restart bound. */
246
- var BROWSER_SERVER_EXHAUSTED = "BROWSER_SERVER_EXHAUSTED";
246
+ var BROWSER_SERVER_EXHAUSTED = "SERVER_EXHAUSTED";
247
247
  /** Names a failed browser server teardown. */
248
- var BROWSER_SERVER_TEARDOWN = "BROWSER_SERVER_TEARDOWN";
248
+ var BROWSER_SERVER_TEARDOWN = "SERVER_TEARDOWN";
249
249
  /** Names a failed profile sweep. */
250
- var BROWSER_SERVER_SWEEP = "BROWSER_SERVER_SWEEP";
250
+ var BROWSER_SERVER_SWEEP = "SERVER_SWEEP";
251
251
  /** Names a refused browser server option. */
252
- var BROWSER_SERVER_OPTIONS = "BROWSER_SERVER_OPTIONS";
252
+ var BROWSER_SERVER_OPTIONS = "ARGUMENT";
253
253
  /** Names the notice that a browser and its session state were lost. */
254
- var BROWSER_SERVER_CRASH = "BROWSER_SERVER_CRASH";
254
+ var BROWSER_SERVER_CRASH = "SERVER_CRASH";
255
255
  /** Names an interrupted call whose outcome is unknown. */
256
- var BROWSER_SERVER_UNRESOLVED = "BROWSER_SERVER_UNRESOLVED";
256
+ var BROWSER_SERVER_UNRESOLVED = "SERVER_UNRESOLVED";
257
257
  /** Names the refusal when every browser is admitted to a holder. */
258
- var BROWSER_SERVER_BUSY = "BROWSER_SERVER_BUSY";
258
+ var BROWSER_SERVER_BUSY = "SERVER_BUSY";
259
259
  /** Names the refusal for an unknown or ended holder. */
260
- var BROWSER_SERVER_HOLDER = "BROWSER_SERVER_HOLDER";
260
+ var BROWSER_SERVER_HOLDER = "SERVER_HOLDER";
261
261
  /** Defines the holder tools independently of the shared browser's vocabulary. */
262
262
  var BROWSER_SERVER_COPY = Object.freeze({
263
263
  acquire: Object.freeze({
@@ -333,46 +333,6 @@ var BROWSER_SERVER_COPY = Object.freeze({
333
333
  })
334
334
  });
335
335
  //#endregion
336
- //#region src/server/errors.ts
337
- /**
338
- * Reports that an operation requiring an active connection was attempted while disconnected,
339
- * under the code `BROWSER_NOT_CONNECTED_ERROR`.
340
- */
341
- var BrowserNotConnectedError = class extends _src_core.BrowserError {
342
- constructor(context) {
343
- super("Browser is not connected", "BROWSER_NOT_CONNECTED_ERROR", context);
344
- this.name = "BrowserNotConnectedError";
345
- }
346
- };
347
- /**
348
- * Reports that an operation was attempted after the browser wrapper was destroyed, under the
349
- * code `BROWSER_DESTROYED_ERROR`.
350
- */
351
- var BrowserDestroyedError = class extends _src_core.BrowserError {
352
- constructor(context) {
353
- super("Browser has been destroyed", "BROWSER_DESTROYED_ERROR", context);
354
- this.name = "BrowserDestroyedError";
355
- }
356
- };
357
- /**
358
- * Narrows an unknown value to a `BrowserNotConnectedError`.
359
- *
360
- * @param value - Value to check
361
- * @returns True if value is a BrowserNotConnectedError instance; false otherwise
362
- */
363
- function isBrowserNotConnectedError(value) {
364
- return (0, _orkestrel_contract.isInstance)(value, BrowserNotConnectedError);
365
- }
366
- /**
367
- * Narrows an unknown value to a `BrowserDestroyedError`.
368
- *
369
- * @param value - Value to check
370
- * @returns True if value is a BrowserDestroyedError instance; false otherwise
371
- */
372
- function isBrowserDestroyedError(value) {
373
- return (0, _orkestrel_contract.isInstance)(value, BrowserDestroyedError);
374
- }
375
- //#endregion
376
336
  //#region src/server/helpers.ts
377
337
  /**
378
338
  * Parses positive integer viewport dimensions in WIDTHxHEIGHT form.
@@ -435,15 +395,15 @@ function parseBrowserProfileRecord(text) {
435
395
  * @param url - The lost page's last URL, when known
436
396
  * @returns The coded diagnostic and any operation-lifecycle guidance
437
397
  * @example
438
- * describeBrowserServerLoss('BROWSER_SERVER_UNAVAILABLE', new Error('Browser exited'))
398
+ * describeBrowserServerLoss('SERVER_UNAVAILABLE', new Error('Browser exited'))
439
399
  */
440
400
  function describeBrowserServerLoss(code, cause, url) {
441
401
  const pid = (0, _src_core.isBrowserError)(cause) ? cause.context?.["pid"] : void 0;
442
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})` : ""}.`;
443
- if (code !== "BROWSER_SERVER_CRASH" && code !== "BROWSER_SERVER_UNRESOLVED") return message;
444
- 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.`;
445
405
  const next = `The next call acquires a browser that starts at about:blank, or answers ${BROWSER_SERVER_UNAVAILABLE} when none can serve.`;
446
- 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}`;
447
407
  }
448
408
  /**
449
409
  * Formats a process identifier and UUID token as a lock entry name.
@@ -518,16 +478,6 @@ function findSystemBrowsers(options) {
518
478
  return browsers;
519
479
  }
520
480
  /**
521
- * Locates a Chrome/Chromium/Edge executable on this machine — the first entry
522
- * of {@link findSystemBrowsers}.
523
- *
524
- * @param options - Overrides for the candidate sources; see {@link SystemBrowserOptions}
525
- * @returns The first discovered browser, or undefined
526
- */
527
- function findSystemBrowser(options) {
528
- return findSystemBrowsers(options)[0];
529
- }
530
- /**
531
481
  * Classifies an executable path/name into a {@link BrowserEngine} by
532
482
  * case-insensitive hint, checked in the order edge → chromium → chrome.
533
483
  *
@@ -583,7 +533,7 @@ async function removeBrowserProfile(profile) {
583
533
  if (!profile.temporary) return;
584
534
  const path = (0, node_path.resolve)(profile.path);
585
535
  const temporary = (0, node_path.resolve)((0, node_os.tmpdir)());
586
- 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 });
587
537
  await (0, node_fs_promises.rm)(path, {
588
538
  recursive: true,
589
539
  force: true,
@@ -773,18 +723,18 @@ function readBrowserEndpoint(stream, signal) {
773
723
  stream.on("close", () => {
774
724
  const endpoint = found ? void 0 : BROWSER_DEVTOOLS_PATTERN.exec(pending)?.[1];
775
725
  if (endpoint !== void 0) read.resolve(endpoint);
776
- 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"));
777
727
  });
778
728
  return read.promise;
779
729
  }
780
730
  /**
781
731
  * Fetches the current CDP target list from a browser's `/json/list` endpoint, as a `Result`
782
- * carrying either the targets or a coded `BrowserConnectionError`.
732
+ * carrying either the targets or a coded `BrowserError`.
783
733
  *
784
734
  * @remarks
785
735
  * The endpoint is a network boundary, so an unreachable host, a non-2xx
786
736
  * response, and a body that is not a JSON array each come back as a failed
787
- * `Result` carrying a coded `BrowserConnectionError`. An entry missing a
737
+ * `Result` carrying a coded `BrowserError`. An entry missing a
788
738
  * required string field is skipped rather than failing the whole list.
789
739
  *
790
740
  * @param port - Port the browser exposes its CDP endpoint on
@@ -809,7 +759,7 @@ async function fetchCDPTargets(port, timeout, host = BROWSER_DEFAULT_HOST) {
809
759
  const response = await fetch(url, { signal: controller.signal });
810
760
  if (!response.ok) return {
811
761
  success: false,
812
- error: new _src_core.BrowserConnectionError("CDP target list request was refused", {
762
+ error: new _src_core.BrowserError("CONNECTION", "CDP target list request was refused", {
813
763
  url,
814
764
  status: response.status
815
765
  })
@@ -817,7 +767,7 @@ async function fetchCDPTargets(port, timeout, host = BROWSER_DEFAULT_HOST) {
817
767
  const list = await response.json();
818
768
  if (!(0, _orkestrel_contract.isArray)(list)) return {
819
769
  success: false,
820
- 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 })
821
771
  };
822
772
  const targets = [];
823
773
  for (const entry of list) {
@@ -837,9 +787,9 @@ async function fetchCDPTargets(port, timeout, host = BROWSER_DEFAULT_HOST) {
837
787
  } catch (error) {
838
788
  return {
839
789
  success: false,
840
- error: new _src_core.BrowserConnectionError("CDP target list is unreachable", {
790
+ error: new _src_core.BrowserError("CONNECTION", "CDP target list is unreachable", {
841
791
  url,
842
- error
792
+ error: (0, _orkestrel_contract.isJSONValue)(error) ? error : String(error)
843
793
  })
844
794
  };
845
795
  } finally {
@@ -905,7 +855,7 @@ var WebSocketCDPTransport = class {
905
855
  }
906
856
  async send(data) {
907
857
  const socket = this.#socket;
908
- 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 });
909
859
  socket.send(data);
910
860
  }
911
861
  async close() {
@@ -913,12 +863,12 @@ var WebSocketCDPTransport = class {
913
863
  }
914
864
  async #start() {
915
865
  const parsed = (0, _orkestrel_contract.attempt)(() => new URL(this.#url));
916
- 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}`, {
917
867
  url: this.#url,
918
- error: parsed.error
868
+ error: (0, _orkestrel_contract.isJSONValue)(parsed.error) ? parsed.error : String(parsed.error)
919
869
  });
920
870
  const url = parsed.value;
921
- 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 });
922
872
  const key = (0, node_crypto.randomBytes)(16).toString("base64");
923
873
  const deferred = Promise.withResolvers();
924
874
  const options = {
@@ -950,7 +900,7 @@ var WebSocketCDPTransport = class {
950
900
  async #close() {
951
901
  const request = this.#request;
952
902
  if (request !== void 0) {
953
- 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 }));
954
904
  request.destroy();
955
905
  }
956
906
  await this.#starting.pending?.catch(() => void 0);
@@ -979,7 +929,7 @@ var WebSocketCDPTransport = class {
979
929
  const accept = response.headers["sec-websocket-accept"];
980
930
  if (!(0, _orkestrel_contract.isString)(accept) || accept !== (0, _orkestrel_websocket.computeWebSocketAccept)(key)) {
981
931
  socket.destroy();
982
- 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 }));
983
933
  return;
984
934
  }
985
935
  try {
@@ -995,13 +945,13 @@ var WebSocketCDPTransport = class {
995
945
  #decline(request, response) {
996
946
  response.resume();
997
947
  const reason = response.statusCode === void 0 ? "upgrade was declined without a status code" : `upgrade declined with status ${response.statusCode}`;
998
- 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 }));
999
949
  }
1000
950
  #fail(request, error) {
1001
- 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 }));
1002
952
  }
1003
953
  #expire(request) {
1004
- 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`, {
1005
955
  url: this.#url,
1006
956
  timeout: this.#timeout
1007
957
  }));
@@ -1073,7 +1023,7 @@ var FileBrowserWriter = class {
1073
1023
  };
1074
1024
  //#endregion
1075
1025
  //#region package.json
1076
- var version = "0.0.26";
1026
+ var version = "0.0.28";
1077
1027
  //#endregion
1078
1028
  //#region src/server/BrowserMCPServer.ts
1079
1029
  /**
@@ -1081,9 +1031,9 @@ var version = "0.0.26";
1081
1031
  * the Model Context Protocol on stdio, and warms Chromium at server start.
1082
1032
  *
1083
1033
  * @remarks
1084
- * The server's own manager holds one dispatcher per name of the vocabulary — `look`, `read`, `plain`,
1085
- * `click`, `type`, `press`, `navigate`, `wait`, `dialog`, `tabs`, `switch`, `record`, `save`,
1086
- * `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
1087
1037
  * `BROWSER_TOOL_COPY` gives, so `tools/list` answers before Chromium starts and with Chromium
1088
1038
  * absent. After the launch, a tool the toolset's manager adds under another name, such as a page
1089
1039
  * tool it adopts, is mirrored as a dispatcher with that tool's definition, and the mirror is
@@ -1120,7 +1070,7 @@ var version = "0.0.26";
1120
1070
  * ```ts
1121
1071
  * import { BrowserMCPServer } from '@orkestrel/browser/server'
1122
1072
  *
1123
- * const server = new BrowserMCPServer({ root: 'tmp/browsers', readonly: true })
1073
+ * const server = new BrowserMCPServer({ root: 'tmp/browsers', journeys: { readonly: true } })
1124
1074
  * await server.start()
1125
1075
  * await server.destroy()
1126
1076
  * ```
@@ -1183,8 +1133,8 @@ var BrowserMCPServer = class {
1183
1133
  constructor(options) {
1184
1134
  const size = options?.pool?.size ?? 1;
1185
1135
  const contexts = options?.pool?.contexts ?? 2;
1186
- 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);
1187
- 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" });
1188
1138
  this.#admission = size * contexts;
1189
1139
  this.#log = options?.log ?? process.stderr;
1190
1140
  this.#reference = this.#issue.bind(this);
@@ -1199,11 +1149,11 @@ var BrowserMCPServer = class {
1199
1149
  restarts: 1
1200
1150
  });
1201
1151
  this.#root = (0, node_path.resolve)(options?.root ?? "tmp/browsers");
1202
- this.#headless = options?.headless ?? true;
1203
- this.#executable = options?.executable;
1204
- this.#viewport = options?.viewport;
1205
- this.#readonly = options?.readonly ?? false;
1206
- 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;
1207
1157
  this.#input = options?.stdio?.input ?? process.stdin;
1208
1158
  this.#tools = (0, _orkestrel_tool.createToolManager)();
1209
1159
  const names = [..._src_core.BROWSER_TOOL_NAMES, ..._src_core.BROWSER_JOURNEY_TOOL_NAMES];
@@ -1267,7 +1217,7 @@ var BrowserMCPServer = class {
1267
1217
  warming.then(this.#exhaust.bind(this));
1268
1218
  } catch (error) {
1269
1219
  if (this.#closing !== void 0) return;
1270
- 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;
1271
1221
  throw this.#unavailable((0, _orkestrel_pool.isPoolError)(error) ? error.cause ?? this.#failure : error);
1272
1222
  }
1273
1223
  }
@@ -1317,12 +1267,16 @@ var BrowserMCPServer = class {
1317
1267
  signal: context.signal,
1318
1268
  ...context.caller === void 0 ? {} : { caller: context.caller }
1319
1269
  });
1320
- 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;
1321
1275
  return {
1322
1276
  resultType: "complete",
1323
1277
  content: [{
1324
1278
  type: "text",
1325
- 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)
1326
1280
  }]
1327
1281
  };
1328
1282
  }
@@ -1338,8 +1292,8 @@ var BrowserMCPServer = class {
1338
1292
  context.signal.throwIfAborted();
1339
1293
  if (this.#closing !== void 0) throw this.#ended();
1340
1294
  const purpose = args["purpose"];
1341
- if (!(0, _orkestrel_contract.isString)(purpose) || purpose.trim().length === 0) throw new _src_core.BrowserError("purpose must describe the work", "BROWSER_TOOLSET_ARGUMENT");
1342
- 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.`);
1343
1297
  const holder = {
1344
1298
  id: (0, node_crypto.randomUUID)(),
1345
1299
  purpose,
@@ -1366,14 +1320,14 @@ var BrowserMCPServer = class {
1366
1320
  }
1367
1321
  #holder(value) {
1368
1322
  const holder = (0, _orkestrel_contract.isString)(value) ? this.#holders.get(value) : void 0;
1369
- 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`);
1370
1324
  return holder;
1371
1325
  }
1372
1326
  async #dispatch(args, context) {
1373
1327
  const holder = this.#holder(args["holder"]);
1374
1328
  const name = args["name"];
1375
1329
  const parameters = args["arguments"];
1376
- 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" });
1377
1331
  return this.#forward(holder, name, parameters, context);
1378
1332
  }
1379
1333
  async #catalog(args, context) {
@@ -1390,8 +1344,8 @@ var BrowserMCPServer = class {
1390
1344
  return this.#annotate(holder, JSON.stringify(catalog));
1391
1345
  } catch (error) {
1392
1346
  context.signal.throwIfAborted();
1393
- if (holder.abort.signal.aborted) throw new _src_core.BrowserError(`${BROWSER_SERVER_UNRESOLVED}: the holder ended during the call`, BROWSER_SERVER_UNRESOLVED);
1394
- 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}`));
1395
1349
  throw error;
1396
1350
  }
1397
1351
  }
@@ -1428,7 +1382,7 @@ var BrowserMCPServer = class {
1428
1382
  context.signal.throwIfAborted();
1429
1383
  const journey = args["journey"];
1430
1384
  if (name !== "replay" && name !== "forget" || !(0, _orkestrel_contract.isString)(journey)) return this.#perform(holder, name, args, context);
1431
- 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 });
1432
1386
  const reader = Symbol(journey);
1433
1387
  if (name === "forget") this.#writers.add(journey);
1434
1388
  else {
@@ -1454,8 +1408,8 @@ var BrowserMCPServer = class {
1454
1408
  lease = await this.#serve(holder, signal);
1455
1409
  } catch (error) {
1456
1410
  context.signal.throwIfAborted();
1457
- 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);
1458
- 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}`));
1459
1413
  throw error;
1460
1414
  }
1461
1415
  let result;
@@ -1480,7 +1434,7 @@ var BrowserMCPServer = class {
1480
1434
  };
1481
1435
  }
1482
1436
  context.signal.throwIfAborted();
1483
- 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");
1484
1438
  if (!result.success) {
1485
1439
  if (this.#leases.get(holder) === lease && !context.signal.aborted) try {
1486
1440
  await this.#race(this.#ping(lease.token.value), context.signal);
@@ -1488,9 +1442,9 @@ var BrowserMCPServer = class {
1488
1442
  context.signal.throwIfAborted();
1489
1443
  if (this.#leases.get(holder) !== lease) {
1490
1444
  const loss = this.#departures.get(lease);
1491
- 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));
1492
1446
  }
1493
- 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));
1494
1448
  }
1495
1449
  return typeof result.value === "string" ? this.#annotate(holder, result.value, lease) : result.value;
1496
1450
  }
@@ -1504,6 +1458,10 @@ var BrowserMCPServer = class {
1504
1458
  }
1505
1459
  return [...notes, value].join("\n");
1506
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
+ }
1507
1465
  async #handshake(options) {
1508
1466
  try {
1509
1467
  await this.#race(this.#starting, options.signal);
@@ -1654,7 +1612,7 @@ var BrowserMCPServer = class {
1654
1612
  if (this.#leases.get(holder) !== lease || this.#holders.get(holder.id) !== holder) return;
1655
1613
  this.#departures.set(lease, {
1656
1614
  cause,
1657
- url: lease.generation.toolset.view.url
1615
+ url: lease.generation.toolset.redact(lease.generation.toolset.view.url)
1658
1616
  });
1659
1617
  if (holder === this.#shared) this.#unmirror();
1660
1618
  this.#leases.delete(holder);
@@ -1796,6 +1754,7 @@ var BrowserMCPServer = class {
1796
1754
  }
1797
1755
  toolset = (0, _src_core.createBrowserToolset)(page, {
1798
1756
  context,
1757
+ notes: this.#notice.bind(this, context),
1799
1758
  journeys: {
1800
1759
  store: createFileBrowserJourneyStore({ root: this.#root }),
1801
1760
  runs: createFileBrowserRunStore({ root: this.#root }),
@@ -1815,7 +1774,7 @@ var BrowserMCPServer = class {
1815
1774
  } catch (error) {
1816
1775
  await toolset?.destroy().catch(this.#fault.bind(this));
1817
1776
  await context?.close().catch((failure) => {
1818
- 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);
1819
1778
  });
1820
1779
  await (0, node_fs_promises.rm)(directory, {
1821
1780
  recursive: true,
@@ -1825,7 +1784,8 @@ var BrowserMCPServer = class {
1825
1784
  }
1826
1785
  }
1827
1786
  async #destroyRecord(slot) {
1828
- 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 {
1829
1789
  process.kill(slot.browser.pid, "SIGKILL");
1830
1790
  } catch (error) {
1831
1791
  if (!(0, _orkestrel_contract.isError)(error) || !("code" in error) || error.code !== "ESRCH") this.#faults.add(error);
@@ -1850,7 +1810,7 @@ var BrowserMCPServer = class {
1850
1810
  }
1851
1811
  async #warm() {
1852
1812
  if (this.#closing !== void 0) throw this.#ended();
1853
- 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);
1854
1814
  const profile = (0, node_path.join)(this.#root, ".profiles", formatBrowserLockEntry(process.pid, (0, node_crypto.randomUUID)()));
1855
1815
  let browser;
1856
1816
  let slot;
@@ -1923,7 +1883,7 @@ var BrowserMCPServer = class {
1923
1883
  const record = parseBrowserProfileRecord(await (0, node_fs_promises.readFile)((0, node_path.join)(folder, BROWSER_SERVER_RECORD), "utf8"));
1924
1884
  if (record === void 0) continue;
1925
1885
  if (probeProcess(record.pid)) {
1926
- const transport = createCDPTransport({ url: record.endpoint });
1886
+ const transport = createWebSocketCDPTransport({ url: record.endpoint });
1927
1887
  const client = (0, _src_core.createCDPClient)({ transport });
1928
1888
  try {
1929
1889
  this.#abort.signal.throwIfAborted();
@@ -1965,7 +1925,7 @@ var BrowserMCPServer = class {
1965
1925
  }
1966
1926
  #unavailable(cause) {
1967
1927
  const message = describeBrowserServerLoss(BROWSER_SERVER_UNAVAILABLE, cause);
1968
- 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));
1969
1929
  }
1970
1930
  #issue() {
1971
1931
  return `e${++this.#references}`;
@@ -1997,7 +1957,7 @@ var BrowserMCPServer = class {
1997
1957
  tools.emitter.off("clear", mirrored.cleared);
1998
1958
  }
1999
1959
  #ended() {
2000
- 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" });
2001
1961
  }
2002
1962
  };
2003
1963
  //#endregion
@@ -2010,31 +1970,32 @@ var BrowserMCPServer = class {
2010
1970
  * const files = new FileBrowserStore({ root: directory })
2011
1971
  * files.validateName('check-ready')
2012
1972
  */
2013
- var FileBrowserStore = class {
1973
+ var FileBrowserStore = class FileBrowserStore {
1974
+ static #locks = /* @__PURE__ */ new Map();
2014
1975
  #root;
2015
1976
  #limit;
2016
1977
  constructor(options) {
2017
1978
  this.#limit = options.limit ?? 100;
2018
- 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" });
2019
1980
  try {
2020
1981
  this.#root = (0, node_fs.realpathSync)(options.root);
2021
1982
  } catch (error) {
2022
- 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 });
2023
1984
  throw this.translateError(options.root, error);
2024
1985
  }
2025
1986
  }
2026
1987
  /** Checks a journey name before filesystem access. @param name - Journey name */
2027
1988
  validateName(name) {
2028
- 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}`);
2029
1990
  }
2030
1991
  /** Checks a run id before filesystem access. @param id - Producer id */
2031
1992
  validateId(id) {
2032
- 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}`);
2033
1994
  }
2034
1995
  /** Resolves a confined target. @param parts - Relative components @returns Absolute target */
2035
1996
  resolvePath(...parts) {
2036
1997
  const path = (0, node_path.resolve)(this.#root, ...parts);
2037
- 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}`);
2038
1999
  return path;
2039
2000
  }
2040
2001
  /**
@@ -2052,8 +2013,8 @@ var FileBrowserStore = class {
2052
2013
  try {
2053
2014
  const status = await (0, node_fs_promises.lstat)(current);
2054
2015
  options?.signal?.throwIfAborted();
2055
- if (status.isSymbolicLink()) throw new _src_core.BrowserError(`Refused symbolic link: ${current}`, "BROWSER_JOURNEY_PATH");
2056
- 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}`);
2057
2018
  } catch (error) {
2058
2019
  options?.signal?.throwIfAborted();
2059
2020
  if (error instanceof Error && "code" in error && error.code === "ENOENT") return false;
@@ -2067,7 +2028,7 @@ var FileBrowserStore = class {
2067
2028
  /** Creates checked directories one component at a time. @param path - Directory @param options - Cancellation options */
2068
2029
  async createDirectory(path, options) {
2069
2030
  if (await this.check(path, options)) return;
2070
- 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}`);
2071
2032
  await this.createDirectory((0, node_path.dirname)(path), options);
2072
2033
  options?.signal?.throwIfAborted();
2073
2034
  try {
@@ -2117,7 +2078,7 @@ var FileBrowserStore = class {
2117
2078
  await (0, node_fs_promises.rename)(temporary, path);
2118
2079
  } catch (error) {
2119
2080
  const target = await (0, node_fs_promises.lstat)(path).catch(() => void 0);
2120
- 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 });
2121
2082
  throw error;
2122
2083
  }
2123
2084
  owned = false;
@@ -2148,6 +2109,20 @@ var FileBrowserStore = class {
2148
2109
  * @returns The mutation result
2149
2110
  */
2150
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) {
2151
2126
  await this.createDirectory((0, node_path.dirname)(path), options);
2152
2127
  const name = formatBrowserLockEntry(process.pid, (0, node_crypto.randomUUID)());
2153
2128
  const entry = this.resolvePath(path, name);
@@ -2190,7 +2165,7 @@ var FileBrowserStore = class {
2190
2165
  await this.#removeLock(path);
2191
2166
  }
2192
2167
  }
2193
- throw new _src_core.BrowserError(`Journey is locked: ${path}`, "BROWSER_JOURNEY_LOCKED");
2168
+ throw new _src_core.BrowserError("STORE_LOCKED", `Journey is locked: ${path}`);
2194
2169
  }
2195
2170
  /**
2196
2171
  * Lists readable entries in sorted order with a bounded page and one extra readable probe.
@@ -2237,7 +2212,7 @@ var FileBrowserStore = class {
2237
2212
  const reason = error instanceof _src_core.BrowserError ? error.context?.["reason"] : void 0;
2238
2213
  faults.push({
2239
2214
  name,
2240
- 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"
2241
2216
  });
2242
2217
  }
2243
2218
  }
@@ -2280,7 +2255,7 @@ var FileBrowserStore = class {
2280
2255
  /** Translates filesystem and validation failures with their path. @param path - Failed path @param error - Cause @returns Coded error */
2281
2256
  translateError(path, error) {
2282
2257
  const code = error instanceof Error && "code" in error ? error.code : void 0;
2283
- 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 });
2284
2259
  }
2285
2260
  async #readLock(path, options) {
2286
2261
  await this.check(path, options);
@@ -2288,13 +2263,13 @@ var FileBrowserStore = class {
2288
2263
  const entries = await (0, node_fs_promises.readdir)(path, { withFileTypes: true });
2289
2264
  for (const entry of entries) {
2290
2265
  await this.check(this.resolvePath(path, entry.name), options);
2291
- 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}`);
2292
2267
  }
2293
2268
  return entries.map((entry) => entry.name);
2294
2269
  } catch (error) {
2295
2270
  if (error instanceof Error && "code" in error) {
2296
2271
  if (error.code === "ENOENT") return void 0;
2297
- 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}`);
2298
2273
  }
2299
2274
  throw this.translateError(path, error);
2300
2275
  }
@@ -2306,7 +2281,7 @@ var FileBrowserStore = class {
2306
2281
  return true;
2307
2282
  } catch (error) {
2308
2283
  if (error instanceof Error && "code" in error && (error.code === "ENOENT" || error.code === "ENOTEMPTY")) return false;
2309
- 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 });
2310
2285
  }
2311
2286
  }
2312
2287
  async #removeLock(path) {
@@ -2322,7 +2297,7 @@ var FileBrowserStore = class {
2322
2297
  if (cause instanceof Error && "code" in cause && cause.code === "ENOENT") return;
2323
2298
  }
2324
2299
  }
2325
- 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 });
2326
2301
  }
2327
2302
  }
2328
2303
  };
@@ -2348,24 +2323,29 @@ var FileBrowserJourneyStore = class {
2348
2323
  if (source === void 0) return void 0;
2349
2324
  try {
2350
2325
  const value = (0, _orkestrel_contract.parseJSON)(source);
2351
- 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");
2352
- 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");
2353
2328
  (0, _src_core.validateBrowserJourney)(value["journey"]);
2354
- 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");
2355
2330
  return {
2356
2331
  journey: value["journey"],
2357
2332
  revision: value["revision"]
2358
2333
  };
2359
2334
  } catch (error) {
2360
2335
  const translated = this.#files.translateError(path, error);
2361
- throw new _src_core.BrowserError(translated.message, translated.code, {
2336
+ throw new _src_core.BrowserError(translated.code, translated.message, {
2362
2337
  ...translated.context,
2363
2338
  reason: (0, _src_core.normalizeBrowserJourneyReason)(error)
2364
2339
  });
2365
2340
  }
2366
2341
  }
2367
- async set(journey, expected, options) {
2342
+ async set(journey, options) {
2368
2343
  options?.signal?.throwIfAborted();
2344
+ (0, _src_core.validateBrowserJourneyWriteOptions)(options);
2345
+ const condition = {
2346
+ revision: options?.revision,
2347
+ exclusive: options?.exclusive
2348
+ };
2369
2349
  this.#files.validateName(journey.name);
2370
2350
  (0, _src_core.validateBrowserJourney)(journey);
2371
2351
  const owned = structuredClone(journey);
@@ -2373,12 +2353,12 @@ var FileBrowserJourneyStore = class {
2373
2353
  const counter = this.#files.resolvePath(owned.name, BROWSER_JOURNEY_REVISION_FILE);
2374
2354
  return this.#files.lock(this.#files.resolvePath(owned.name, BROWSER_JOURNEY_LOCK_DIRECTORY), async () => {
2375
2355
  const current = await this.get(owned.name, options);
2376
- 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`);
2377
2357
  const source = await this.#files.read(counter, options);
2378
2358
  const previous = source === void 0 ? 0 : Number(source);
2379
- 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}`);
2380
2360
  const revision = Math.max(previous, current?.revision ?? 0) + 1;
2381
- 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}`);
2382
2362
  const saved = {
2383
2363
  journey: owned,
2384
2364
  revision
@@ -2403,7 +2383,7 @@ var FileBrowserJourneyStore = class {
2403
2383
  const counter = this.#files.resolvePath(name, BROWSER_JOURNEY_REVISION_FILE);
2404
2384
  const source = await this.#files.read(counter, options);
2405
2385
  const previous = source === void 0 ? 0 : Number(source);
2406
- 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}`);
2407
2387
  await this.#files.write(counter, String(Math.max(previous, current.revision ?? 0)), options);
2408
2388
  await this.#files.remove(this.#files.resolvePath(name, BROWSER_JOURNEY_SNAPSHOT_FILE), options);
2409
2389
  }, options);
@@ -2425,7 +2405,7 @@ var FileBrowserJourneyStore = class {
2425
2405
  * still refuses conflicting mutations and allocations from another process.
2426
2406
  * @example
2427
2407
  * const store = new FileBrowserRunStore({ root: directory })
2428
- * const slot = await store.open('check-ready')
2408
+ * const slot = await store.create('check-ready')
2429
2409
  */
2430
2410
  var FileBrowserRunStore = class FileBrowserRunStore {
2431
2411
  static #allocations = /* @__PURE__ */ new Map();
@@ -2435,7 +2415,7 @@ var FileBrowserRunStore = class FileBrowserRunStore {
2435
2415
  constructor(options) {
2436
2416
  this.#files = new FileBrowserStore(options);
2437
2417
  }
2438
- async snapshot(bytes, options) {
2418
+ async write(bytes, options) {
2439
2419
  options?.signal?.throwIfAborted();
2440
2420
  const owned = new Uint8Array(bytes);
2441
2421
  const directory = await this.#files.allocate(this.#files.resolvePath(BROWSER_RUN_DIRECTORY), _src_core.generateBrowserRunId, options);
@@ -2453,7 +2433,7 @@ var FileBrowserRunStore = class FileBrowserRunStore {
2453
2433
  throw this.#files.translateError(path, error);
2454
2434
  }
2455
2435
  }
2456
- async open(name, options) {
2436
+ async create(name, options) {
2457
2437
  options?.signal?.throwIfAborted();
2458
2438
  this.#files.validateName(name);
2459
2439
  const path = this.#files.resolvePath(name, BROWSER_JOURNEY_LOCK_DIRECTORY);
@@ -2487,10 +2467,10 @@ var FileBrowserRunStore = class FileBrowserRunStore {
2487
2467
  if (source === void 0) return void 0;
2488
2468
  try {
2489
2469
  const value = (0, _orkestrel_contract.parseJSON)(source);
2490
- if (!(0, _orkestrel_contract.isRecord)(value) || !("format" in value)) throw new _src_core.BrowserError("Missing run format", "BROWSER_JOURNEY_FILE");
2491
- 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");
2492
2472
  (0, _src_core.validateBrowserRun)(value);
2493
- 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");
2494
2474
  return value;
2495
2475
  } catch (error) {
2496
2476
  throw this.#files.translateError(path, error);
@@ -2509,7 +2489,7 @@ var FileBrowserRunStore = class FileBrowserRunStore {
2509
2489
  async capture(slot, name, bytes, options) {
2510
2490
  options?.signal?.throwIfAborted();
2511
2491
  const directory = this.#slots.get(slot);
2512
- 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");
2513
2493
  const owned = new Uint8Array(bytes);
2514
2494
  await this.#requireDirectory(directory, options);
2515
2495
  await this.#files.write(this.#files.resolvePath(directory, name), owned, options);
@@ -2562,12 +2542,12 @@ var FileBrowserRunStore = class FileBrowserRunStore {
2562
2542
  return this.#files.list(this.#files.resolvePath(name, BROWSER_RUN_DIRECTORY), (id) => this.get(name, id, options), options);
2563
2543
  }
2564
2544
  async #requireDirectory(directory, options) {
2565
- 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}`);
2566
2546
  try {
2567
2547
  options?.signal?.throwIfAborted();
2568
2548
  const status = await (0, node_fs_promises.lstat)(directory);
2569
2549
  options?.signal?.throwIfAborted();
2570
- 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}`);
2571
2551
  } catch (error) {
2572
2552
  options?.signal?.throwIfAborted();
2573
2553
  throw this.#files.translateError(directory, error);
@@ -2604,7 +2584,7 @@ function createBrowser(options) {
2604
2584
  * @param options - The CDP WebSocket debugger URL (and optional timeout)
2605
2585
  * @returns A {@link CDPTransportInterface}
2606
2586
  */
2607
- function createCDPTransport(options) {
2587
+ function createWebSocketCDPTransport(options) {
2608
2588
  return new WebSocketCDPTransport(options);
2609
2589
  }
2610
2590
  /**
@@ -2613,7 +2593,7 @@ function createCDPTransport(options) {
2613
2593
  *
2614
2594
  * @returns A {@link BrowserWriterInterface} that persists bytes through `node:fs/promises`
2615
2595
  */
2616
- function createBrowserWriter() {
2596
+ function createFileBrowserWriter() {
2617
2597
  return new FileBrowserWriter();
2618
2598
  }
2619
2599
  /**
@@ -2647,7 +2627,7 @@ function createFileBrowserRunStore(options) {
2647
2627
  * ```ts
2648
2628
  * import { createBrowserMCPServer } from '@orkestrel/browser/server'
2649
2629
  *
2650
- * const server = createBrowserMCPServer({ root: 'tmp/browsers', headless: true })
2630
+ * const server = createBrowserMCPServer({ root: 'tmp/browsers', browser: { headless: true } })
2651
2631
  * await server.start() // resolves after leasing a connected browser
2652
2632
  * await server.destroy()
2653
2633
  * ```
@@ -2702,7 +2682,7 @@ var Browser = class {
2702
2682
  ...options?.error !== void 0 ? { error: options.error } : {}
2703
2683
  });
2704
2684
  this.#options = options ?? {};
2705
- 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";
2706
2686
  this.#cdpPort = this.#options.cdp?.port ?? 9222;
2707
2687
  this.#cdpHost = this.#options.cdp?.host ?? "127.0.0.1";
2708
2688
  queueMicrotask(() => this.#emitter.emit("idle"));
@@ -2729,9 +2709,9 @@ var Browser = class {
2729
2709
  return this.#endpoint;
2730
2710
  }
2731
2711
  async ping(options) {
2732
- if (this.#destroyed) throw new BrowserDestroyedError();
2712
+ if (this.#destroyed) throw new _src_core.BrowserError("CLOSED", "Browser has been destroyed", { subject: "browser" });
2733
2713
  const client = this.#client;
2734
- 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");
2735
2715
  await client.send("Browser.getVersion", void 0, options);
2736
2716
  }
2737
2717
  async discover() {
@@ -2740,7 +2720,7 @@ var Browser = class {
2740
2720
  return result;
2741
2721
  }
2742
2722
  async connect() {
2743
- if (this.#destroyed) throw new BrowserDestroyedError();
2723
+ if (this.#destroyed) throw new _src_core.BrowserError("CLOSED", "Browser has been destroyed", { subject: "browser" });
2744
2724
  const active = this.#connecting.pending;
2745
2725
  if (active !== void 0) {
2746
2726
  await active;
@@ -2750,8 +2730,8 @@ var Browser = class {
2750
2730
  await this.#connecting.execute(() => this.#establish());
2751
2731
  }
2752
2732
  adopt() {
2753
- if (this.#destroyed) throw new BrowserDestroyedError();
2754
- 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");
2755
2735
  this.#owned = true;
2756
2736
  }
2757
2737
  async disconnect() {
@@ -2782,12 +2762,15 @@ var Browser = class {
2782
2762
  }
2783
2763
  }
2784
2764
  async create(options) {
2785
- if (this.#destroyed) throw new BrowserDestroyedError();
2765
+ if (this.#destroyed) throw new _src_core.BrowserError("CLOSED", "Browser has been destroyed", { subject: "browser" });
2786
2766
  const client = this.#client;
2787
- 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");
2788
2768
  let context = this.#contexts[0];
2789
2769
  if (context === void 0) {
2790
- 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
+ });
2791
2774
  this.#registerContext(context);
2792
2775
  }
2793
2776
  const page = await context.create(options);
@@ -2815,10 +2798,11 @@ var Browser = class {
2815
2798
  return shutdown;
2816
2799
  }
2817
2800
  async #isolate(options) {
2818
- if (this.#destroyed) throw new BrowserDestroyedError();
2801
+ if (this.#destroyed) throw new _src_core.BrowserError("CLOSED", "Browser has been destroyed", { subject: "browser" });
2819
2802
  const client = this.#client;
2820
- 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");
2821
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");
2822
2806
  const params = { disposeOnDetach: false };
2823
2807
  if (options?.proxy !== void 0) {
2824
2808
  params["proxyServer"] = options.proxy.server;
@@ -2826,18 +2810,25 @@ var Browser = class {
2826
2810
  }
2827
2811
  if (options?.origins !== void 0) params["originsWithUniversalNetworkAccess"] = [...options.origins];
2828
2812
  const result = await client.send("Target.createBrowserContext", params);
2829
- 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");
2830
2814
  const id = result["browserContextId"];
2831
- 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
+ });
2832
2823
  try {
2833
- 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" });
2834
2825
  if (options?.downloads !== void 0) await client.send("Browser.setDownloadBehavior", {
2835
2826
  behavior: options.downloads.named === true ? "allowAndName" : "allow",
2836
2827
  browserContextId: id,
2837
2828
  downloadPath: options.downloads.path,
2838
2829
  eventsEnabled: true
2839
2830
  });
2840
- 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" });
2841
2832
  } catch (error) {
2842
2833
  await context.close().catch(() => void 0);
2843
2834
  throw error;
@@ -2849,7 +2840,7 @@ var Browser = class {
2849
2840
  const disconnecting = this.#disconnecting.pending;
2850
2841
  if (disconnecting !== void 0) await disconnecting;
2851
2842
  await this.#settleExit();
2852
- if (this.#destroyed) throw new BrowserDestroyedError();
2843
+ if (this.#destroyed) throw new _src_core.BrowserError("CLOSED", "Browser has been destroyed", { subject: "browser" });
2853
2844
  if (this.#status === "connected") return;
2854
2845
  this.#assertNotAborted();
2855
2846
  this.#status = "connecting";
@@ -2874,12 +2865,12 @@ var Browser = class {
2874
2865
  this.#assertNotAborted();
2875
2866
  await this.#launch();
2876
2867
  } catch (error) {
2877
- if (this.#destroyed) throw new BrowserDestroyedError();
2868
+ if (this.#destroyed) throw new _src_core.BrowserError("CLOSED", "Browser has been destroyed", { subject: "browser" });
2878
2869
  this.#status = "error";
2879
2870
  this.#emitter.emit("error", error);
2880
- if ((0, _src_core.isBrowserConnectionError)(error)) throw error;
2871
+ if ((0, _src_core.isBrowserError)(error) && error.code === "CONNECTION") throw error;
2881
2872
  const message = (0, _orkestrel_contract.isError)(error) ? error.message : String(error);
2882
- 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 });
2883
2874
  }
2884
2875
  }
2885
2876
  async #detach() {
@@ -2900,7 +2891,7 @@ var Browser = class {
2900
2891
  this.#emitter.emit("idle");
2901
2892
  }
2902
2893
  #assertNotAborted() {
2903
- if (this.#signal().aborted) throw new _src_core.BrowserConnectionError("Connection aborted");
2894
+ if (this.#signal().aborted) throw new _src_core.BrowserError("CONNECTION", "Connection aborted");
2904
2895
  }
2905
2896
  #signal() {
2906
2897
  const external = this.#options.signal;
@@ -2908,10 +2899,10 @@ var Browser = class {
2908
2899
  }
2909
2900
  async #raceAbort(promise) {
2910
2901
  const signal = this.#signal();
2911
- if (signal.aborted) throw new _src_core.BrowserConnectionError("Connection aborted");
2902
+ if (signal.aborted) throw new _src_core.BrowserError("CONNECTION", "Connection aborted");
2912
2903
  const aborted = Promise.withResolvers();
2913
2904
  const listener = (0, node_events.addAbortListener)(signal, () => {
2914
- aborted.reject(new _src_core.BrowserConnectionError("Connection aborted"));
2905
+ aborted.reject(new _src_core.BrowserError("CONNECTION", "Connection aborted"));
2915
2906
  });
2916
2907
  try {
2917
2908
  return await Promise.race([promise, aborted.promise]);
@@ -2939,7 +2930,7 @@ var Browser = class {
2939
2930
  this.#owned = void 0;
2940
2931
  this.#endpoint = void 0;
2941
2932
  if (connected) this.#status = "disconnected";
2942
- 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 }));
2943
2934
  if (connected) {
2944
2935
  this.#emitter.emit("disconnect");
2945
2936
  this.#emitter.emit("idle");
@@ -2977,7 +2968,7 @@ var Browser = class {
2977
2968
  }
2978
2969
  this.#destroyContextList(contexts);
2979
2970
  this.#closeClient(client);
2980
- 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 }));
2981
2972
  this.#emitter.emit("disconnect");
2982
2973
  this.#emitter.emit("idle");
2983
2974
  }
@@ -3030,7 +3021,7 @@ var Browser = class {
3030
3021
  };
3031
3022
  }
3032
3023
  async #assertPortFree() {
3033
- 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`, {
3034
3025
  port: this.#cdpPort,
3035
3026
  host: this.#cdpHost
3036
3027
  });
@@ -3044,7 +3035,7 @@ var Browser = class {
3044
3035
  }
3045
3036
  }
3046
3037
  async #connectCDP(endpoint) {
3047
- const transport = createCDPTransport({
3038
+ const transport = createWebSocketCDPTransport({
3048
3039
  url: endpoint,
3049
3040
  timeout: this.#timeout()
3050
3041
  });
@@ -3077,20 +3068,20 @@ var Browser = class {
3077
3068
  }
3078
3069
  }
3079
3070
  async #launch() {
3080
- if (this.#process !== void 0) throw new _src_core.BrowserConnectionError("A browser process is already active on this instance");
3081
- 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;
3082
3073
  let executable = this.#options.executable;
3083
3074
  let resolvedEngine;
3084
3075
  if (executable !== void 0) resolvedEngine = parseBrowserEngine(executable) ?? "chromium";
3085
3076
  else {
3086
- const found = findSystemBrowser({
3077
+ const found = findSystemBrowsers({
3087
3078
  ...this.#options.browsers,
3088
3079
  ...requestedEngine !== void 0 ? { engine: requestedEngine } : {}
3089
- });
3080
+ })[0];
3090
3081
  executable = found?.executable;
3091
3082
  resolvedEngine = found?.engine;
3092
3083
  }
3093
- 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 });
3094
3085
  this.#engine = resolvedEngine ?? "chromium";
3095
3086
  await this.#releaseProfile();
3096
3087
  const profile = await createBrowserProfile(this.#options.profile);
@@ -3106,7 +3097,7 @@ var Browser = class {
3106
3097
  let client;
3107
3098
  try {
3108
3099
  const endpoint = await this.#waitForLaunch(process, executable, this.#options.args);
3109
- const transport = createCDPTransport({
3100
+ const transport = createWebSocketCDPTransport({
3110
3101
  url: endpoint,
3111
3102
  timeout: this.#timeout()
3112
3103
  });
@@ -3170,9 +3161,9 @@ var Browser = class {
3170
3161
  }
3171
3162
  if (pid === void 0) {
3172
3163
  await this.#closeRemote(client);
3173
- throw new _src_core.BrowserConnectionError("The browser launcher exited without naming the process serving its CDP endpoint", {
3174
- executable: this.#options.executable,
3175
- 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 }
3176
3167
  });
3177
3168
  }
3178
3169
  this.#servingPid = pid;
@@ -3180,7 +3171,7 @@ var Browser = class {
3180
3171
  async #waitForLaunch(process, executable, args) {
3181
3172
  const context = {
3182
3173
  executable,
3183
- args
3174
+ ...args === void 0 ? {} : { args }
3184
3175
  };
3185
3176
  const timeout = this.#timeout();
3186
3177
  const controller = new AbortController();
@@ -3191,32 +3182,32 @@ var Browser = class {
3191
3182
  deadline
3192
3183
  ]);
3193
3184
  const stderr = process.stderr;
3194
- 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);
3195
3186
  const exit = (0, node_events.once)(process, "exit", { signal });
3196
3187
  exit.catch(() => void 0);
3197
3188
  const ready = readBrowserEndpoint(stderr, signal).catch(async (error) => {
3198
3189
  if (signal.aborted) throw error;
3199
3190
  const values = await exit;
3200
3191
  if (values[0] === 0 && values[1] === null) throw error;
3201
- 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);
3202
3193
  });
3203
3194
  const exited = exit.then((values) => {
3204
3195
  const code = (0, _orkestrel_contract.isInteger)(values[0]) ? values[0] : null;
3205
3196
  const exitSignal = (0, _orkestrel_contract.isString)(values[1]) ? values[1] : null;
3206
3197
  if (code === 0 && exitSignal === null) return ready;
3207
- throw new _src_core.BrowserConnectionError(this.#formatLaunchExit(code, exitSignal), context);
3198
+ throw new _src_core.BrowserError("CONNECTION", this.#formatLaunchExit(code, exitSignal), context);
3208
3199
  });
3209
3200
  try {
3210
3201
  return await Promise.race([ready, exited]);
3211
3202
  } catch (error) {
3212
- if ((0, _src_core.isBrowserConnectionError)(error)) throw error;
3213
- if (this.#signal().aborted) throw new _src_core.BrowserConnectionError("Connection aborted", context);
3214
- 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`, {
3215
3206
  ...context,
3216
3207
  timeout
3217
3208
  });
3218
3209
  const message = (0, _orkestrel_contract.isError)(error) ? error.message : String(error);
3219
- throw new _src_core.BrowserConnectionError(message, context);
3210
+ throw new _src_core.BrowserError("CONNECTION", message, context);
3220
3211
  } finally {
3221
3212
  controller.abort();
3222
3213
  ready.catch(() => void 0);
@@ -3253,7 +3244,10 @@ var Browser = class {
3253
3244
  });
3254
3245
  }
3255
3246
  if (pages.length === 0 || this.#contexts.length > 0) return;
3256
- 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
+ });
3257
3251
  await context.sync(pages);
3258
3252
  this.#registerContext(context);
3259
3253
  }
@@ -3333,7 +3327,7 @@ var Browser = class {
3333
3327
  let remote = client;
3334
3328
  let temporary = false;
3335
3329
  if (remote === void 0 && this.#owned === true && this.#endpoint !== void 0) {
3336
- const transport = createCDPTransport({
3330
+ const transport = createWebSocketCDPTransport({
3337
3331
  url: this.#endpoint,
3338
3332
  timeout: this.#timeout()
3339
3333
  });
@@ -3436,10 +3430,11 @@ var Browser = class {
3436
3430
  const code = (0, _orkestrel_contract.isError)(error) && "code" in error && (0, _orkestrel_contract.isString)(error.code) ? error.code : void 0;
3437
3431
  if (code === "ESRCH") return false;
3438
3432
  if (signal === 0) return code === "EPERM" ? true : void 0;
3439
- throw new _src_core.BrowserConnectionError("Failed to signal the browser process", {
3440
- 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 },
3441
3436
  signal,
3442
- cause: error
3437
+ cause: (0, _orkestrel_contract.isJSONValue)(error) ? error : String(error)
3443
3438
  });
3444
3439
  }
3445
3440
  }
@@ -3478,7 +3473,8 @@ var Browser = class {
3478
3473
  if (await this.#waitForTerminationWithin(process, 3e3)) return;
3479
3474
  if (this.#signalProcess(process, "SIGKILL") === false) return;
3480
3475
  if (await this.#waitForTerminationWithin(process, 3e3, true)) return;
3481
- 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 } });
3482
3478
  } finally {
3483
3479
  await this.#closeProcessPipe(process);
3484
3480
  }
@@ -3558,9 +3554,7 @@ exports.BROWSER_TRANSPORT_LOSS_DEFER_MS = BROWSER_TRANSPORT_LOSS_DEFER_MS;
3558
3554
  exports.BROWSER_WINDOWS_ROOT_FALLBACKS = BROWSER_WINDOWS_ROOT_FALLBACKS;
3559
3555
  exports.BROWSER_WINDOWS_SUFFIXES = BROWSER_WINDOWS_SUFFIXES;
3560
3556
  exports.Browser = Browser;
3561
- exports.BrowserDestroyedError = BrowserDestroyedError;
3562
3557
  exports.BrowserMCPServer = BrowserMCPServer;
3563
- exports.BrowserNotConnectedError = BrowserNotConnectedError;
3564
3558
  exports.FileBrowserJourneyStore = FileBrowserJourneyStore;
3565
3559
  exports.FileBrowserRunStore = FileBrowserRunStore;
3566
3560
  exports.FileBrowserWriter = FileBrowserWriter;
@@ -3572,20 +3566,17 @@ exports.buildWindowsRoots = buildWindowsRoots;
3572
3566
  exports.createBrowser = createBrowser;
3573
3567
  exports.createBrowserMCPServer = createBrowserMCPServer;
3574
3568
  exports.createBrowserProfile = createBrowserProfile;
3575
- exports.createBrowserWriter = createBrowserWriter;
3576
- exports.createCDPTransport = createCDPTransport;
3577
3569
  exports.createFileBrowserJourneyStore = createFileBrowserJourneyStore;
3578
3570
  exports.createFileBrowserRunStore = createFileBrowserRunStore;
3571
+ exports.createFileBrowserWriter = createFileBrowserWriter;
3572
+ exports.createWebSocketCDPTransport = createWebSocketCDPTransport;
3579
3573
  exports.describeBrowserServerLoss = describeBrowserServerLoss;
3580
3574
  exports.fetchCDPTargets = fetchCDPTargets;
3581
3575
  exports.findEnvOverrides = findEnvOverrides;
3582
3576
  exports.findInstallPaths = findInstallPaths;
3583
3577
  exports.findStorePaths = findStorePaths;
3584
- exports.findSystemBrowser = findSystemBrowser;
3585
3578
  exports.findSystemBrowsers = findSystemBrowsers;
3586
3579
  exports.formatBrowserLockEntry = formatBrowserLockEntry;
3587
- exports.isBrowserDestroyedError = isBrowserDestroyedError;
3588
- exports.isBrowserNotConnectedError = isBrowserNotConnectedError;
3589
3580
  exports.launchBrowserProcess = launchBrowserProcess;
3590
3581
  exports.normalizeExecutablePath = normalizeExecutablePath;
3591
3582
  exports.parseBrowserEngine = parseBrowserEngine;