@orkestrel/browser 0.0.18 → 0.0.19

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.
@@ -6,13 +6,15 @@ let node_fs_promises = require("node:fs/promises");
6
6
  let node_os = require("node:os");
7
7
  let node_path = require("node:path");
8
8
  let node_child_process = require("node:child_process");
9
- let node_timers_promises = require("node:timers/promises");
10
9
  let node_crypto = require("node:crypto");
11
10
  let node_http = require("node:http");
12
11
  let node_https = require("node:https");
13
12
  let _orkestrel_emitter = require("@orkestrel/emitter");
14
13
  let _orkestrel_websocket = require("@orkestrel/websocket");
15
14
  let node_events = require("node:events");
15
+ let _orkestrel_mcp = require("@orkestrel/mcp");
16
+ let _orkestrel_mcp_server = require("@orkestrel/mcp/server");
17
+ let _orkestrel_tool = require("@orkestrel/tool");
16
18
  //#region src/server/constants.ts
17
19
  /**
18
20
  * Sets the default CDP port probed for an existing browser and used for launches, `9222`.
@@ -58,9 +60,28 @@ var BROWSER_KILL_GRACE_MS = 3e3;
58
60
  */
59
61
  var BROWSER_PORT_PROBE_TIMEOUT_MS = 200;
60
62
  /**
63
+ * Matches the stderr line Chromium prints when its CDP endpoint accepts connections, and
64
+ * captures the `ws://` endpoint the line names.
65
+ *
66
+ * @remarks
67
+ * Chromium prints the line only when launched with `--remote-debugging-port`, which is why an
68
+ * owned launch always passes that flag.
69
+ */
70
+ var BROWSER_DEVTOOLS_PATTERN = /DevTools listening on (ws:\/\/\S+)/;
71
+ /**
72
+ * Sets the interval in milliseconds between liveness probes of a terminated browser process
73
+ * group.
74
+ *
75
+ * @remarks
76
+ * Node raises an exit event for the direct child only. Neither the rest of a POSIX process
77
+ * group nor a process a launcher handed the endpoint to raises one, so the bounded drain in
78
+ * `Browser` probes on this interval until the group is gone or the grace period ends.
79
+ */
80
+ var BROWSER_DRAIN_INTERVAL_MS = 100;
81
+ /**
61
82
  * Defers once for `50` milliseconds when a transport loss is observed on an owned process,
62
- * giving a near-simultaneous process-exit event, which libuv may reap slightly later than the
63
- * socket close, first say over the diagnosis.
83
+ * so a near-simultaneous process-exit event, which libuv might reap slightly later than the
84
+ * socket close, decides the diagnosis first.
64
85
  */
65
86
  var BROWSER_TRANSPORT_LOSS_DEFER_MS = 50;
66
87
  /**
@@ -177,19 +198,23 @@ var BROWSER_STORE_GLOBS = Object.freeze({
177
198
  darwin: "chromium-*/chrome-mac*/Chromium.app/Contents/MacOS/Chromium",
178
199
  win32: "chromium-*/chrome-win*/chrome.exe"
179
200
  });
201
+ /** Names the persisted journey snapshot. */
202
+ var BROWSER_JOURNEY_SNAPSHOT_FILE = "journey.json";
203
+ /** Names the retained journey revision counter. */
204
+ var BROWSER_JOURNEY_REVISION_FILE = "revision";
205
+ /** Names the exclusive journey write lock directory. */
206
+ var BROWSER_JOURNEY_LOCK_DIRECTORY = "journey.lock";
207
+ /** Bounds attempts to acquire a journey lock after concurrent recovery. */
208
+ var BROWSER_JOURNEY_LOCK_ATTEMPTS = 8;
209
+ /** Names the persisted run snapshot. */
210
+ var BROWSER_RUN_FILE = "run.json";
211
+ /** Names the journey directory holding its runs. */
212
+ var BROWSER_RUN_DIRECTORY = "runs";
213
+ /** Bounds a file-store listing page by default. */
214
+ var BROWSER_FILE_STORE_LIMIT = 100;
180
215
  //#endregion
181
216
  //#region src/server/errors.ts
182
217
  /**
183
- * Reports that a CDP connection, discovery, or launch attempt failed, under the code
184
- * `BROWSER_CONNECTION_ERROR`.
185
- */
186
- var BrowserConnectionError = class extends _src_core.BrowserError {
187
- constructor(message, context) {
188
- super(message, "BROWSER_CONNECTION_ERROR", context);
189
- this.name = "BrowserConnectionError";
190
- }
191
- };
192
- /**
193
218
  * Reports that an operation requiring an active connection was attempted while disconnected,
194
219
  * under the code `BROWSER_NOT_CONNECTED_ERROR`.
195
220
  */
@@ -210,15 +235,6 @@ var BrowserDestroyedError = class extends _src_core.BrowserError {
210
235
  }
211
236
  };
212
237
  /**
213
- * Narrows an unknown value to a `BrowserConnectionError`.
214
- *
215
- * @param value - Value to check
216
- * @returns True if value is a BrowserConnectionError instance; false otherwise
217
- */
218
- function isBrowserConnectionError(value) {
219
- return (0, _orkestrel_contract.isInstance)(value, BrowserConnectionError);
220
- }
221
- /**
222
238
  * Narrows an unknown value to a `BrowserNotConnectedError`.
223
239
  *
224
240
  * @param value - Value to check
@@ -239,6 +255,30 @@ function isBrowserDestroyedError(value) {
239
255
  //#endregion
240
256
  //#region src/server/helpers.ts
241
257
  /**
258
+ * Formats a process identifier and UUID token as a lock entry name.
259
+ * @param pid - Positive safe integer identifying the holder process
260
+ * @param token - Lowercase UUID token identifying this acquisition
261
+ * @returns The entry name
262
+ * @example
263
+ * formatBrowserLockEntry(123, '11111111-1111-4111-8111-111111111111')
264
+ */
265
+ function formatBrowserLockEntry(pid, token) {
266
+ return `${pid}-${token}`;
267
+ }
268
+ /**
269
+ * Parses the holder process identifier from a lock entry name.
270
+ * @remarks Requires a positive safe integer followed by a lowercase UUID token.
271
+ * @param name - Directory entry name
272
+ * @returns The process identifier, or undefined when the entry is malformed
273
+ * @example
274
+ * parseBrowserLockEntry('123-11111111-1111-4111-8111-111111111111') // 123
275
+ */
276
+ function parseBrowserLockEntry(name) {
277
+ const match = /^([1-9]\d*)-[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}$/.exec(name);
278
+ const pid = Number(match?.[1]);
279
+ return Number.isSafeInteger(pid) && pid > 0 ? pid : void 0;
280
+ }
281
+ /**
242
282
  * Enumerates every Chrome/Chromium/Edge executable discoverable on this
243
283
  * machine, deduplicated by normalized absolute path.
244
284
  *
@@ -468,57 +508,69 @@ function findStorePaths(base, platform) {
468
508
  * subprocesses with it, so that single signal drains the tree.
469
509
  *
470
510
  * @param executable - Absolute path to the browser executable
471
- * @param port - Port the browser exposes its CDP endpoint on
511
+ * @param port - Port the browser exposes its CDP endpoint on, or undefined for a port the operating system picks
472
512
  * @param headless - Whether to launch in headless mode
473
513
  * @param profile - Optional user-data-dir for a persistent profile
474
514
  * @param extra - Additional command-line flags
475
- * @returns The spawned ChildProcess
515
+ * @returns The spawned ChildProcess, whose standard error carries the endpoint line `readBrowserEndpoint` reads
476
516
  */
477
517
  function launchBrowserProcess(executable, port, headless, profile, extra) {
478
518
  const args = [];
479
519
  if (extra !== void 0) args.push(...extra);
480
- args.push(`--remote-debugging-port=${port}`, ...BROWSER_LAUNCH_ARGS);
520
+ args.push(`--remote-debugging-port=${port ?? 0}`, ...BROWSER_LAUNCH_ARGS);
481
521
  if (headless) args.push(BROWSER_HEADLESS_ARG);
482
522
  if (profile !== void 0) args.push(`--user-data-dir=${profile}`);
483
523
  return (0, node_child_process.spawn)(executable, args, {
484
- stdio: "ignore",
524
+ stdio: [
525
+ "ignore",
526
+ "ignore",
527
+ "pipe"
528
+ ],
485
529
  detached: process.platform !== "win32"
486
530
  });
487
531
  }
488
532
  /**
489
- * Polls a browser's CDP version endpoint until it responds or the timeout elapses.
533
+ * Reads the CDP endpoint a launched browser announces on its standard error.
490
534
  *
491
- * @param port - Port the browser exposes its CDP endpoint on
492
- * @param timeout - Maximum time to wait in milliseconds
493
- * @param host - Host the browser exposes its CDP endpoint on (default `127.0.0.1`)
494
- * @param signal - Optional external abort; an abort while waiting rethrows rather than resolving
495
- * @returns The browser's WebSocket debugger URL
535
+ * @remarks
536
+ * Chromium prints `DevTools listening on ws://HOST:PORT/devtools/browser/ID` after it binds the
537
+ * debugging port. The read splits arriving text on line breaks across chunks, so a line split
538
+ * between two chunks and a line ending `\r\n` both resolve. After resolving, the stream keeps
539
+ * draining until it closes so the browser's logging never fills the pipe.
540
+ *
541
+ * @param stream - The browser's standard error
542
+ * @param signal - Abort that rejects the read with its reason
543
+ * @returns The `ws://` endpoint the browser announced
496
544
  *
497
- * @throws When the endpoint does not become ready before the timeout
545
+ * @throws When the stream closes without the line, or the signal aborts first
498
546
  */
499
- async function waitForCDPReady(port, timeout, host = BROWSER_DEFAULT_HOST, signal) {
500
- const url = `${BROWSER_CDP_PROTOCOL}://${host}:${port}${BROWSER_CDP_VERSION_PATH}`;
501
- const deadline = Date.now() + timeout;
502
- while (Date.now() < deadline) {
503
- signal?.throwIfAborted();
504
- const remaining = Math.max(0, deadline - Date.now());
505
- const requestSignal = signal === void 0 ? AbortSignal.timeout(remaining) : AbortSignal.any([signal, AbortSignal.timeout(remaining)]);
506
- try {
507
- const response = await fetch(url, { signal: requestSignal });
508
- if (response.ok) {
509
- const info = await response.json();
510
- if ((0, _orkestrel_contract.isRecord)(info) && (0, _orkestrel_contract.isString)(info["webSocketDebuggerUrl"])) return info["webSocketDebuggerUrl"];
511
- }
512
- } catch (error) {
513
- if (signal?.aborted === true) throw error;
547
+ function readBrowserEndpoint(stream, signal) {
548
+ const read = Promise.withResolvers();
549
+ let pending = "";
550
+ let found = false;
551
+ if (signal.aborted) read.reject(signal.reason);
552
+ signal.addEventListener("abort", () => read.reject(signal.reason), { once: true });
553
+ stream.on("error", read.reject);
554
+ stream.setEncoding("utf8");
555
+ stream.on("data", (chunk) => {
556
+ if (found || !(0, _orkestrel_contract.isString)(chunk)) return;
557
+ const lines = (pending + chunk).split(/\r\n|\n/);
558
+ pending = lines.pop() ?? "";
559
+ for (const line of lines) {
560
+ const endpoint = BROWSER_DEVTOOLS_PATTERN.exec(line)?.[1];
561
+ if (endpoint === void 0) continue;
562
+ found = true;
563
+ pending = "";
564
+ read.resolve(endpoint);
565
+ return;
514
566
  }
515
- const delay = Math.min(_src_core.BROWSER_WAIT_POLL_INTERVAL_MS, Math.max(0, deadline - Date.now()));
516
- if (delay > 0) await (0, node_timers_promises.setTimeout)(delay, void 0, { signal });
517
- }
518
- throw new BrowserConnectionError(`CDP endpoint on port ${port} did not become ready within ${timeout}ms`, {
519
- port,
520
- timeout
521
567
  });
568
+ stream.on("close", () => {
569
+ const endpoint = found ? void 0 : BROWSER_DEVTOOLS_PATTERN.exec(pending)?.[1];
570
+ if (endpoint !== void 0) read.resolve(endpoint);
571
+ else read.reject(new _src_core.BrowserConnectionError("Browser closed its standard error before reporting a CDP endpoint"));
572
+ });
573
+ return read.promise;
522
574
  }
523
575
  /**
524
576
  * Fetches the current CDP target list from a browser's `/json/list` endpoint, as a `Result`
@@ -552,7 +604,7 @@ async function fetchCDPTargets(port, timeout, host = BROWSER_DEFAULT_HOST) {
552
604
  const response = await fetch(url, { signal: controller.signal });
553
605
  if (!response.ok) return {
554
606
  success: false,
555
- error: new BrowserConnectionError("CDP target list request was refused", {
607
+ error: new _src_core.BrowserConnectionError("CDP target list request was refused", {
556
608
  url,
557
609
  status: response.status
558
610
  })
@@ -560,7 +612,7 @@ async function fetchCDPTargets(port, timeout, host = BROWSER_DEFAULT_HOST) {
560
612
  const list = await response.json();
561
613
  if (!(0, _orkestrel_contract.isArray)(list)) return {
562
614
  success: false,
563
- error: new BrowserConnectionError("CDP target list is not a JSON array", { url })
615
+ error: new _src_core.BrowserConnectionError("CDP target list is not a JSON array", { url })
564
616
  };
565
617
  const targets = [];
566
618
  for (const entry of list) {
@@ -580,7 +632,7 @@ async function fetchCDPTargets(port, timeout, host = BROWSER_DEFAULT_HOST) {
580
632
  } catch (error) {
581
633
  return {
582
634
  success: false,
583
- error: new BrowserConnectionError("CDP target list is unreachable", {
635
+ error: new _src_core.BrowserConnectionError("CDP target list is unreachable", {
584
636
  url,
585
637
  error
586
638
  })
@@ -648,7 +700,7 @@ var WebSocketCDPTransport = class {
648
700
  }
649
701
  async send(data) {
650
702
  const socket = this.#socket;
651
- if (socket === void 0 || socket.readyState !== _orkestrel_websocket.WEBSOCKET_READY_OPEN) throw new BrowserConnectionError("WebSocket CDP transport is not open", { url: this.#url });
703
+ 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 });
652
704
  socket.send(data);
653
705
  }
654
706
  async close() {
@@ -656,12 +708,12 @@ var WebSocketCDPTransport = class {
656
708
  }
657
709
  async #start() {
658
710
  const parsed = (0, _orkestrel_contract.attempt)(() => new URL(this.#url));
659
- if (!parsed.success) throw new BrowserConnectionError(`WebSocket CDP URL is invalid: ${this.#url}`, {
711
+ if (!parsed.success) throw new _src_core.BrowserConnectionError(`WebSocket CDP URL is invalid: ${this.#url}`, {
660
712
  url: this.#url,
661
713
  error: parsed.error
662
714
  });
663
715
  const url = parsed.value;
664
- if (url.protocol !== "ws:" && url.protocol !== "wss:") throw new BrowserConnectionError(`WebSocket CDP connection requires a ws: or wss: URL: ${this.#url}`, { url: this.#url });
716
+ 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 });
665
717
  const key = (0, node_crypto.randomBytes)(16).toString("base64");
666
718
  const deferred = Promise.withResolvers();
667
719
  const options = {
@@ -693,7 +745,7 @@ var WebSocketCDPTransport = class {
693
745
  async #close() {
694
746
  const request = this.#request;
695
747
  if (request !== void 0) {
696
- this.#rejectRequest(request, new BrowserConnectionError(`WebSocket CDP connection to ${this.#url} was closed before it finished connecting`, { url: this.#url }));
748
+ this.#rejectRequest(request, new _src_core.BrowserConnectionError(`WebSocket CDP connection to ${this.#url} was closed before it finished connecting`, { url: this.#url }));
697
749
  request.destroy();
698
750
  }
699
751
  await this.#starting.pending?.catch(() => void 0);
@@ -722,7 +774,7 @@ var WebSocketCDPTransport = class {
722
774
  const accept = response.headers["sec-websocket-accept"];
723
775
  if (!(0, _orkestrel_contract.isString)(accept) || accept !== (0, _orkestrel_websocket.computeWebSocketAccept)(key)) {
724
776
  socket.destroy();
725
- this.#rejectRequest(request, new BrowserConnectionError(`WebSocket CDP connection to ${this.#url} failed: Sec-WebSocket-Accept mismatch`, { url: this.#url }));
777
+ this.#rejectRequest(request, new _src_core.BrowserConnectionError(`WebSocket CDP connection to ${this.#url} failed: Sec-WebSocket-Accept mismatch`, { url: this.#url }));
726
778
  return;
727
779
  }
728
780
  try {
@@ -738,13 +790,13 @@ var WebSocketCDPTransport = class {
738
790
  #decline(request, response) {
739
791
  response.resume();
740
792
  const reason = response.statusCode === void 0 ? "upgrade was declined without a status code" : `upgrade declined with status ${response.statusCode}`;
741
- this.#rejectRequest(request, new BrowserConnectionError(`WebSocket CDP connection to ${this.#url} failed: ${reason}`, { url: this.#url }));
793
+ this.#rejectRequest(request, new _src_core.BrowserConnectionError(`WebSocket CDP connection to ${this.#url} failed: ${reason}`, { url: this.#url }));
742
794
  }
743
795
  #fail(request, error) {
744
- this.#rejectRequest(request, new BrowserConnectionError(`WebSocket CDP connection to ${this.#url} failed: ${error.message}`, { url: this.#url }));
796
+ this.#rejectRequest(request, new _src_core.BrowserConnectionError(`WebSocket CDP connection to ${this.#url} failed: ${error.message}`, { url: this.#url }));
745
797
  }
746
798
  #expire(request) {
747
- this.#rejectRequest(request, new BrowserConnectionError(`WebSocket CDP connection to ${this.#url} timed out after ${this.#timeout}ms`, {
799
+ this.#rejectRequest(request, new _src_core.BrowserConnectionError(`WebSocket CDP connection to ${this.#url} timed out after ${this.#timeout}ms`, {
748
800
  url: this.#url,
749
801
  timeout: this.#timeout
750
802
  }));
@@ -815,6 +867,790 @@ var FileBrowserWriter = class {
815
867
  }
816
868
  };
817
869
  //#endregion
870
+ //#region package.json
871
+ var version = "0.0.19";
872
+ //#endregion
873
+ //#region src/server/BrowserMCPServer.ts
874
+ /**
875
+ * Implements `BrowserMCPServerInterface`: serves the browser vocabulary and the journey tools over
876
+ * the Model Context Protocol on stdio, and launches Chromium on the first tool call.
877
+ *
878
+ * @remarks
879
+ * The server's own manager holds one dispatcher per name of the vocabulary — `look`, `read`,
880
+ * `click`, `type`, `press`, `navigate`, `wait`, `dialog`, `tabs`, `switch`, `record`, `save`,
881
+ * `journeys`, `edit`, and `replay` — each carrying the description, parameters, and annotations
882
+ * `BROWSER_TOOL_COPY` gives, so `tools/list` answers before Chromium starts and with Chromium
883
+ * absent. After the launch, a tool the toolset's manager adds under another name, such as a page
884
+ * tool it adopts, is mirrored as a dispatcher with that tool's definition, and the mirror is
885
+ * removed when the toolset withdraws the tool, so the server's tool list changes and a subscribed
886
+ * client is notified. The first call of any dispatcher launches Chromium one time: concurrent callers await
887
+ * the same launch, a failed launch rejects every one of them, and the next call launches again.
888
+ * A launch creates `ROOT/.profiles/ID/` exclusively, connects a browser that never attaches to an
889
+ * existing endpoint, opens one page in an isolated context, and constructs the page toolset with
890
+ * that context and the file journey and run stores under the root, on a manager the toolset owns.
891
+ * A dispatcher forwards its arguments and the call's context, signal included, to the toolset's
892
+ * manager and returns that manager's value, or rejects with its failure message; a text value
893
+ * reaches the client as one text block rather than as its JSON text.
894
+ *
895
+ * `start()` serves stdio and destroys the server at the end of its input, on `SIGINT`, and on
896
+ * `SIGTERM`. `destroy()` stops reading requests and removes every dispatcher, then destroys the
897
+ * toolset, which aborts the active replay, then the browser it launched, then removes the profile.
898
+ *
899
+ * Chromium on Linux refuses to start as the root user unless its sandbox is disabled, so a server
900
+ * running as root on Linux launches Chromium with `--no-sandbox`; a launch that could run
901
+ * sandboxed is never changed.
902
+ *
903
+ * @example
904
+ * ```ts
905
+ * import { BrowserMCPServer } from '@orkestrel/browser/server'
906
+ *
907
+ * const server = new BrowserMCPServer({ root: 'tmp/browsers', readonly: true })
908
+ * await server.start()
909
+ * await server.destroy()
910
+ * ```
911
+ */
912
+ var BrowserMCPServer = class {
913
+ #root;
914
+ #headless;
915
+ #executable;
916
+ #readonly;
917
+ #launcher;
918
+ #input;
919
+ #tools;
920
+ #transport;
921
+ #abort = new AbortController();
922
+ #signal;
923
+ #vocabulary;
924
+ #added;
925
+ #removed;
926
+ #cleared;
927
+ #mirrored;
928
+ #session;
929
+ #browser;
930
+ #profile;
931
+ #started = false;
932
+ #closing;
933
+ /**
934
+ * Registers the dispatchers and binds the protocol to the stdio streams without reading them.
935
+ *
936
+ * @param options - The root, the launch, the journeys' read-only switch, and the streams
937
+ */
938
+ constructor(options) {
939
+ this.#root = (0, node_path.resolve)(options?.root ?? "tmp/browsers");
940
+ this.#headless = options?.headless ?? true;
941
+ this.#executable = options?.executable;
942
+ this.#readonly = options?.readonly ?? false;
943
+ this.#launcher = options?.launch ?? createBrowser;
944
+ this.#input = options?.stdio?.input ?? process.stdin;
945
+ this.#tools = (0, _orkestrel_tool.createToolManager)();
946
+ const names = [..._src_core.BROWSER_TOOL_NAMES, ..._src_core.BROWSER_JOURNEY_TOOL_NAMES];
947
+ this.#vocabulary = new Set(names);
948
+ for (const name of names) this.#tools.add((0, _orkestrel_tool.createTool)({
949
+ ..._src_core.BROWSER_TOOL_COPY[name],
950
+ execute: this.#forward.bind(this, name)
951
+ }));
952
+ const server = (0, _orkestrel_mcp.createMCPServer)({
953
+ identity: {
954
+ name: "browse",
955
+ version
956
+ },
957
+ tools: this.#tools,
958
+ execution: this.#execute.bind(this)
959
+ });
960
+ this.#transport = (0, _orkestrel_mcp_server.createStdioServer)((0, _orkestrel_mcp.createMCPLegacy)(server), {
961
+ input: this.#input,
962
+ ...options?.stdio?.output === void 0 ? {} : { output: options.stdio.output }
963
+ });
964
+ this.#signal = this.#end.bind(this);
965
+ this.#added = this.#mirror.bind(this);
966
+ this.#removed = this.#withdraw.bind(this);
967
+ this.#cleared = this.#clear.bind(this);
968
+ }
969
+ async start() {
970
+ if (this.#closing !== void 0) throw this.#ended();
971
+ if (this.#started) return;
972
+ this.#started = true;
973
+ this.#transport.start();
974
+ this.#input.on("end", this.#signal);
975
+ process.on("SIGINT", this.#signal);
976
+ process.on("SIGTERM", this.#signal);
977
+ }
978
+ destroy() {
979
+ this.#closing ??= this.#destroy();
980
+ return this.#closing;
981
+ }
982
+ async #destroy() {
983
+ process.removeListener("SIGINT", this.#signal);
984
+ process.removeListener("SIGTERM", this.#signal);
985
+ this.#input.removeListener("end", this.#signal);
986
+ this.#transport.stop();
987
+ for (const tool of this.#tools.tools()) this.#tools.remove(tool.name);
988
+ this.#unmirror();
989
+ this.#abort.abort();
990
+ const failures = [];
991
+ await (await this.#session?.catch(() => void 0))?.destroy().catch((error) => failures.push(error));
992
+ await this.#browser?.destroy().catch((error) => failures.push(error));
993
+ this.#browser = void 0;
994
+ const profile = this.#profile;
995
+ this.#profile = void 0;
996
+ if (profile !== void 0) await (0, node_fs_promises.rm)(profile, {
997
+ recursive: true,
998
+ force: true,
999
+ maxRetries: 5
1000
+ });
1001
+ if (failures.length > 0) throw new AggregateError(failures, "The browse server teardown failed");
1002
+ }
1003
+ async #execute(context) {
1004
+ const result = await context.tools.execute(context.call, {
1005
+ signal: context.signal,
1006
+ ...context.caller === void 0 ? {} : { caller: context.caller }
1007
+ });
1008
+ if (!result.success || typeof result.value !== "string") return result;
1009
+ return {
1010
+ resultType: "complete",
1011
+ content: [{
1012
+ type: "text",
1013
+ text: result.value
1014
+ }]
1015
+ };
1016
+ }
1017
+ #end() {
1018
+ this.destroy();
1019
+ }
1020
+ async #forward(name, args, context) {
1021
+ const result = await (await this.#open()).tools.execute({
1022
+ id: name,
1023
+ name,
1024
+ arguments: args
1025
+ }, context);
1026
+ if (!result.success) throw new _src_core.BrowserError(result.error);
1027
+ return result.value;
1028
+ }
1029
+ #open() {
1030
+ if (this.#closing !== void 0) return Promise.reject(this.#ended());
1031
+ const current = this.#session;
1032
+ if (current !== void 0) return current;
1033
+ const session = this.#launch();
1034
+ this.#session = session;
1035
+ session.catch(() => {
1036
+ if (this.#session === session) this.#session = void 0;
1037
+ });
1038
+ return session;
1039
+ }
1040
+ async #launch() {
1041
+ await (0, node_fs_promises.mkdir)(this.#root, { recursive: true });
1042
+ const profiles = (0, node_path.join)(this.#root, ".profiles");
1043
+ await (0, node_fs_promises.mkdir)(profiles, { recursive: true });
1044
+ const profile = (0, node_path.join)(profiles, (0, node_crypto.randomUUID)());
1045
+ await (0, node_fs_promises.mkdir)(profile);
1046
+ this.#profile = profile;
1047
+ let browser;
1048
+ let toolset;
1049
+ try {
1050
+ browser = this.#launcher({
1051
+ headless: this.#headless,
1052
+ profile,
1053
+ cdp: { discover: false },
1054
+ signal: this.#abort.signal,
1055
+ ...this.#executable === void 0 ? {} : { executable: this.#executable },
1056
+ ...process.platform === "linux" && process.getuid?.() === 0 ? { args: ["--no-sandbox"] } : {}
1057
+ });
1058
+ this.#browser = browser;
1059
+ await browser.connect();
1060
+ const context = await browser.isolate();
1061
+ const page = await context.create();
1062
+ toolset = (0, _src_core.createBrowserToolset)(page, {
1063
+ context,
1064
+ journeys: {
1065
+ store: createFileBrowserJourneyStore({ root: this.#root }),
1066
+ runs: createFileBrowserRunStore({ root: this.#root }),
1067
+ readonly: this.#readonly
1068
+ }
1069
+ });
1070
+ this.#mirrored = toolset.tools;
1071
+ toolset.tools.emitter.on("add", this.#added);
1072
+ toolset.tools.emitter.on("remove", this.#removed);
1073
+ toolset.tools.emitter.on("clear", this.#cleared);
1074
+ await toolset.start();
1075
+ return toolset;
1076
+ } catch (error) {
1077
+ this.#unmirror();
1078
+ if (this.#browser === browser) this.#browser = void 0;
1079
+ if (this.#profile === profile) this.#profile = void 0;
1080
+ await toolset?.destroy().catch(() => void 0);
1081
+ await browser?.destroy().catch(() => void 0);
1082
+ await (0, node_fs_promises.rm)(profile, {
1083
+ recursive: true,
1084
+ force: true,
1085
+ maxRetries: 5
1086
+ });
1087
+ throw error;
1088
+ }
1089
+ }
1090
+ #mirror(tool) {
1091
+ if (this.#vocabulary.has(tool.name)) return;
1092
+ this.#tools.add((0, _orkestrel_tool.createTool)({
1093
+ ...(0, _orkestrel_tool.toolToDefinition)(tool),
1094
+ execute: this.#forward.bind(this, tool.name)
1095
+ }));
1096
+ }
1097
+ #withdraw(tool) {
1098
+ if (this.#vocabulary.has(tool.name)) return;
1099
+ const current = this.#mirrored?.tool(tool.name);
1100
+ if (current === void 0) this.#tools.remove(tool.name);
1101
+ else this.#mirror(current);
1102
+ }
1103
+ #clear(tools) {
1104
+ for (const tool of tools) this.#withdraw(tool);
1105
+ }
1106
+ #unmirror() {
1107
+ const mirrored = this.#mirrored;
1108
+ if (mirrored === void 0) return;
1109
+ this.#mirrored = void 0;
1110
+ mirrored.emitter.off("add", this.#added);
1111
+ mirrored.emitter.off("remove", this.#removed);
1112
+ mirrored.emitter.off("clear", this.#cleared);
1113
+ }
1114
+ #ended() {
1115
+ return new _src_core.BrowserError("the browser session ended", "BROWSER_TOOLSET_ENDED");
1116
+ }
1117
+ };
1118
+ //#endregion
1119
+ //#region src/server/stores/FileBrowserStore.ts
1120
+ /**
1121
+ * Shares confined filesystem operations between the journey and run stores.
1122
+ * @remarks Requires an existing root directory. Links present during a component check are refused;
1123
+ * replacement between a check and its use by another filesystem writer is outside this boundary.
1124
+ * @example
1125
+ * const files = new FileBrowserStore({ root: directory })
1126
+ * files.validateName('check-ready')
1127
+ */
1128
+ var FileBrowserStore = class {
1129
+ #root;
1130
+ #limit;
1131
+ constructor(options) {
1132
+ this.#limit = options.limit ?? 100;
1133
+ if (!Number.isSafeInteger(this.#limit) || this.#limit < 1) throw new _src_core.BrowserError("The listing cap must be a positive integer", "BROWSER_JOURNEY_ARGUMENT");
1134
+ try {
1135
+ this.#root = (0, node_fs.realpathSync)(options.root);
1136
+ } catch (error) {
1137
+ 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 });
1138
+ throw this.translateError(options.root, error);
1139
+ }
1140
+ }
1141
+ /** Checks a journey name before filesystem access. @param name - Journey name */
1142
+ validateName(name) {
1143
+ if (!_src_core.BROWSER_JOURNEY_NAME_PATTERN.test(name)) throw new _src_core.BrowserError(`Refused journey name: ${name}`, "BROWSER_JOURNEY_PATH");
1144
+ }
1145
+ /** Checks a run id before filesystem access. @param id - Producer id */
1146
+ validateId(id) {
1147
+ if (!_src_core.BROWSER_RUN_ID_PATTERN.test(id)) throw new _src_core.BrowserError(`Refused run id: ${id}`, "BROWSER_JOURNEY_PATH");
1148
+ }
1149
+ /** Resolves a confined target. @param parts - Relative components @returns Absolute target */
1150
+ resolvePath(...parts) {
1151
+ const path = (0, node_path.resolve)(this.#root, ...parts);
1152
+ 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");
1153
+ return path;
1154
+ }
1155
+ /**
1156
+ * Checks every existing component including the root and target.
1157
+ * @param path - Absolute target
1158
+ * @param options - Cancellation options
1159
+ * @returns True if the target exists; false if a component is missing
1160
+ */
1161
+ async check(path, options) {
1162
+ this.resolvePath(path);
1163
+ let current = this.#root;
1164
+ const parts = (0, node_path.relative)(this.#root, path).split(node_path.sep).filter(Boolean);
1165
+ for (let index = 0; index <= parts.length; index += 1) {
1166
+ options?.signal?.throwIfAborted();
1167
+ try {
1168
+ const status = await (0, node_fs_promises.lstat)(current);
1169
+ options?.signal?.throwIfAborted();
1170
+ if (status.isSymbolicLink()) throw new _src_core.BrowserError(`Refused symbolic link: ${current}`, "BROWSER_JOURNEY_PATH");
1171
+ if (index < parts.length && !status.isDirectory()) throw new _src_core.BrowserError(`Not a directory: ${current}`, "BROWSER_JOURNEY_PATH");
1172
+ } catch (error) {
1173
+ options?.signal?.throwIfAborted();
1174
+ if (error instanceof Error && "code" in error && error.code === "ENOENT") return false;
1175
+ throw this.translateError(current, error);
1176
+ }
1177
+ const part = parts[index];
1178
+ if (part !== void 0) current = this.resolvePath(current, part);
1179
+ }
1180
+ return true;
1181
+ }
1182
+ /** Creates checked directories one component at a time. @param path - Directory @param options - Cancellation options */
1183
+ async createDirectory(path, options) {
1184
+ if (await this.check(path, options)) return;
1185
+ if (path === this.#root) throw new _src_core.BrowserError(`Missing root: ${path}`, "BROWSER_JOURNEY_PATH");
1186
+ await this.createDirectory((0, node_path.dirname)(path), options);
1187
+ options?.signal?.throwIfAborted();
1188
+ try {
1189
+ await (0, node_fs_promises.mkdir)(path);
1190
+ } catch (error) {
1191
+ if (!(error instanceof Error && "code" in error && error.code === "EEXIST")) throw this.translateError(path, error);
1192
+ }
1193
+ await this.check(path, options);
1194
+ }
1195
+ /** Reads an existing file without collapsing a fault to absence. @param path - File @param options - Cancellation options @returns Bytes as UTF-8, or absence */
1196
+ async read(path, options) {
1197
+ if (!await this.check(path, options)) return void 0;
1198
+ try {
1199
+ options?.signal?.throwIfAborted();
1200
+ const value = await (0, node_fs_promises.readFile)(path, {
1201
+ encoding: "utf8",
1202
+ signal: options?.signal
1203
+ });
1204
+ options?.signal?.throwIfAborted();
1205
+ return value;
1206
+ } catch (error) {
1207
+ options?.signal?.throwIfAborted();
1208
+ if (error instanceof Error && "code" in error && error.code === "ENOENT") return void 0;
1209
+ throw this.translateError(path, error);
1210
+ }
1211
+ }
1212
+ /** Atomically replaces a file and cleans its owned temporary file on failure. @param path - Destination @param value - Bytes or UTF-8 @param options - Cancellation options */
1213
+ async write(path, value, options) {
1214
+ const temporary = this.resolvePath((0, node_path.dirname)(path), `.${(0, node_crypto.randomUUID)()}.tmp`);
1215
+ let owned = false;
1216
+ try {
1217
+ await this.check(path, options);
1218
+ await this.check(temporary, options);
1219
+ options?.signal?.throwIfAborted();
1220
+ const file = await (0, node_fs_promises.open)(temporary, "wx");
1221
+ owned = true;
1222
+ try {
1223
+ options?.signal?.throwIfAborted();
1224
+ await file.writeFile(value, { signal: options?.signal });
1225
+ } finally {
1226
+ await file.close();
1227
+ }
1228
+ await this.check(path, options);
1229
+ await this.check(temporary, options);
1230
+ options?.signal?.throwIfAborted();
1231
+ await (0, node_fs_promises.rename)(temporary, path);
1232
+ owned = false;
1233
+ } catch (error) {
1234
+ options?.signal?.throwIfAborted();
1235
+ throw this.translateError(path, error);
1236
+ } finally {
1237
+ if (owned) await this.remove(temporary);
1238
+ }
1239
+ }
1240
+ /** Removes a checked file, accepting absence. @param path - File @param options - Cancellation options */
1241
+ async remove(path, options) {
1242
+ if (!await this.check(path, options)) return;
1243
+ try {
1244
+ options?.signal?.throwIfAborted();
1245
+ await (0, node_fs_promises.unlink)(path);
1246
+ } catch (error) {
1247
+ options?.signal?.throwIfAborted();
1248
+ if (!(error instanceof Error && "code" in error && error.code === "ENOENT")) throw this.translateError(path, error);
1249
+ }
1250
+ }
1251
+ /**
1252
+ * Holds a directory lock through a mutation and releases only its named entry.
1253
+ * @remarks Reclaims dead or empty locks within a bounded attempt count; a live or unknown holder refuses.
1254
+ * @param path - Lock directory
1255
+ * @param action - Mutation inside the lock
1256
+ * @param options - Cancellation options
1257
+ * @returns The mutation result
1258
+ */
1259
+ async lock(path, action, options) {
1260
+ await this.createDirectory((0, node_path.dirname)(path), options);
1261
+ const name = formatBrowserLockEntry(process.pid, (0, node_crypto.randomUUID)());
1262
+ const entry = this.resolvePath(path, name);
1263
+ for (let attempt = 0; attempt < 8; attempt += 1) {
1264
+ await this.check(path, options);
1265
+ options?.signal?.throwIfAborted();
1266
+ try {
1267
+ await (0, node_fs_promises.mkdir)(path);
1268
+ } catch (error) {
1269
+ if (!(error instanceof Error && "code" in error && error.code === "EEXIST")) throw this.translateError(path, error);
1270
+ const entries = await this.#readLock(path, options);
1271
+ if (entries === void 0) continue;
1272
+ if (entries.length > 1) break;
1273
+ const holder = entries[0];
1274
+ if (holder !== void 0) {
1275
+ const pid = parseBrowserLockEntry(holder);
1276
+ if (pid === void 0) break;
1277
+ try {
1278
+ process.kill(pid, 0);
1279
+ break;
1280
+ } catch (cause) {
1281
+ if (!(cause instanceof Error && "code" in cause && cause.code === "ESRCH")) break;
1282
+ }
1283
+ if (!await this.#unlinkLock(this.resolvePath(path, holder))) continue;
1284
+ }
1285
+ await this.#removeLock(path);
1286
+ continue;
1287
+ }
1288
+ let owned = false;
1289
+ try {
1290
+ await this.check(entry, options);
1291
+ try {
1292
+ const file = await (0, node_fs_promises.open)(entry, "wx");
1293
+ owned = true;
1294
+ await file.close();
1295
+ } catch (error) {
1296
+ if (error instanceof Error && "code" in error && error.code === "ENOENT") continue;
1297
+ throw this.translateError(entry, error);
1298
+ }
1299
+ const entries = await this.#readLock(path, options);
1300
+ if (entries?.length !== 1 || entries[0] !== name) break;
1301
+ options?.signal?.throwIfAborted();
1302
+ return await action();
1303
+ } finally {
1304
+ if (owned) await this.#unlinkLock(entry);
1305
+ await this.#removeLock(path);
1306
+ }
1307
+ }
1308
+ throw new _src_core.BrowserError(`Journey is locked: ${path}`, "BROWSER_JOURNEY_LOCKED");
1309
+ }
1310
+ /**
1311
+ * Lists readable entries in sorted order with a bounded page and one extra readable probe.
1312
+ * @param path - Parent directory
1313
+ * @param reader - Entry reader
1314
+ * @param options - Paging and cancellation
1315
+ * @returns Page and observed entry faults
1316
+ */
1317
+ async list(path, reader, options) {
1318
+ const offset = options?.offset ?? 0;
1319
+ const requested = options?.limit ?? this.#limit;
1320
+ (0, _src_core.validateBrowserStorePage)(offset, requested);
1321
+ const limit = Math.min(requested, this.#limit);
1322
+ const entries = [];
1323
+ const faults = [];
1324
+ if (!await this.check(path, options)) return {
1325
+ entries,
1326
+ faults,
1327
+ truncated: false
1328
+ };
1329
+ let names;
1330
+ try {
1331
+ options?.signal?.throwIfAborted();
1332
+ names = (await (0, node_fs_promises.readdir)(path)).sort();
1333
+ } catch (error) {
1334
+ options?.signal?.throwIfAborted();
1335
+ throw this.translateError(path, error);
1336
+ }
1337
+ let count = 0;
1338
+ for (const name of names) {
1339
+ options?.signal?.throwIfAborted();
1340
+ try {
1341
+ const entry = await reader(name);
1342
+ if (entry === void 0) continue;
1343
+ if (count++ < offset) continue;
1344
+ if (entries.length === limit) return {
1345
+ entries,
1346
+ faults,
1347
+ truncated: true
1348
+ };
1349
+ entries.push(entry);
1350
+ } catch (error) {
1351
+ options?.signal?.throwIfAborted();
1352
+ const reason = error instanceof _src_core.BrowserError ? error.context?.["reason"] : void 0;
1353
+ faults.push({
1354
+ name,
1355
+ 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"
1356
+ });
1357
+ }
1358
+ }
1359
+ return {
1360
+ entries,
1361
+ faults,
1362
+ truncated: false
1363
+ };
1364
+ }
1365
+ /**
1366
+ * Allocates a run directory exclusively, retrying colliding producer candidates.
1367
+ * @param parent - Checked parent directory
1368
+ * @param candidate - Producer id source
1369
+ * @param options - Cancellation options
1370
+ * @returns Allocated id and absolute directory
1371
+ */
1372
+ async allocate(parent, candidate, options) {
1373
+ await this.createDirectory(parent, options);
1374
+ for (;;) {
1375
+ options?.signal?.throwIfAborted();
1376
+ const id = candidate();
1377
+ this.validateId(id);
1378
+ const path = this.resolvePath(parent, id);
1379
+ await this.check(path, options);
1380
+ try {
1381
+ options?.signal?.throwIfAborted();
1382
+ await (0, node_fs_promises.mkdir)(path);
1383
+ options?.signal?.throwIfAborted();
1384
+ return {
1385
+ id,
1386
+ path
1387
+ };
1388
+ } catch (error) {
1389
+ options?.signal?.throwIfAborted();
1390
+ if (error instanceof Error && "code" in error && error.code === "EEXIST") continue;
1391
+ throw this.translateError(path, error);
1392
+ }
1393
+ }
1394
+ }
1395
+ /** Translates filesystem and validation failures with their path. @param path - Failed path @param error - Cause @returns Coded error */
1396
+ translateError(path, error) {
1397
+ const code = error instanceof Error && "code" in error ? error.code : void 0;
1398
+ 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 });
1399
+ }
1400
+ async #readLock(path, options) {
1401
+ await this.check(path, options);
1402
+ try {
1403
+ const entries = await (0, node_fs_promises.readdir)(path, { withFileTypes: true });
1404
+ for (const entry of entries) {
1405
+ await this.check(this.resolvePath(path, entry.name), options);
1406
+ if (!entry.isFile()) throw new _src_core.BrowserError(`Journey is locked: ${path}`, "BROWSER_JOURNEY_LOCKED");
1407
+ }
1408
+ return entries.map((entry) => entry.name);
1409
+ } catch (error) {
1410
+ if (error instanceof Error && "code" in error) {
1411
+ if (error.code === "ENOENT") return void 0;
1412
+ if (error.code === "ENOTDIR") throw new _src_core.BrowserError(`Journey is locked: ${path}`, "BROWSER_JOURNEY_LOCKED");
1413
+ }
1414
+ throw this.translateError(path, error);
1415
+ }
1416
+ }
1417
+ async #unlinkLock(path) {
1418
+ await this.check(path);
1419
+ try {
1420
+ await (0, node_fs_promises.unlink)(path);
1421
+ return true;
1422
+ } catch (error) {
1423
+ if (error instanceof Error && "code" in error && (error.code === "ENOENT" || error.code === "ENOTEMPTY")) return false;
1424
+ throw new _src_core.BrowserError(`Cannot remove lock entry: ${path}`, "BROWSER_JOURNEY_ACCESS", { path });
1425
+ }
1426
+ }
1427
+ async #removeLock(path) {
1428
+ await this.check(path);
1429
+ try {
1430
+ await (0, node_fs_promises.rmdir)(path);
1431
+ } catch (error) {
1432
+ if (error instanceof Error && "code" in error && (error.code === "ENOENT" || error.code === "ENOTEMPTY")) return;
1433
+ throw new _src_core.BrowserError(`Cannot remove lock directory: ${path}`, "BROWSER_JOURNEY_ACCESS", { path });
1434
+ }
1435
+ }
1436
+ };
1437
+ //#endregion
1438
+ //#region src/server/stores/FileBrowserJourneyStore.ts
1439
+ /**
1440
+ * Persists journeys with exclusive writes and revision counters retained across deletion.
1441
+ * @remarks Requires an existing root. A live holder refuses immediately; dead or empty locks are reclaimed within a bounded attempt count.
1442
+ * @example
1443
+ * const store = new FileBrowserJourneyStore({ root: directory })
1444
+ * await store.set(journey)
1445
+ */
1446
+ var FileBrowserJourneyStore = class {
1447
+ #files;
1448
+ constructor(options) {
1449
+ this.#files = new FileBrowserStore(options);
1450
+ }
1451
+ async get(name, options) {
1452
+ options?.signal?.throwIfAborted();
1453
+ this.#files.validateName(name);
1454
+ const path = this.#files.resolvePath(name, BROWSER_JOURNEY_SNAPSHOT_FILE);
1455
+ const source = await this.#files.read(path, options);
1456
+ if (source === void 0) return void 0;
1457
+ try {
1458
+ const value = (0, _orkestrel_contract.parseJSON)(source);
1459
+ 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");
1460
+ if (!(0, _orkestrel_contract.isRecord)(value["journey"]) || !("format" in value["journey"])) throw new _src_core.BrowserError("Missing journey format", "BROWSER_JOURNEY_FILE");
1461
+ (0, _src_core.validateBrowserJourney)(value["journey"]);
1462
+ if (value["journey"].name !== name) throw new _src_core.BrowserError("Journey name differs from its directory", "BROWSER_JOURNEY_FILE");
1463
+ return {
1464
+ journey: value["journey"],
1465
+ revision: value["revision"]
1466
+ };
1467
+ } catch (error) {
1468
+ const translated = this.#files.translateError(path, error);
1469
+ throw new _src_core.BrowserError(translated.message, translated.code, {
1470
+ ...translated.context,
1471
+ reason: (0, _src_core.normalizeBrowserJourneyReason)(error)
1472
+ });
1473
+ }
1474
+ }
1475
+ async set(journey, expected, options) {
1476
+ options?.signal?.throwIfAborted();
1477
+ this.#files.validateName(journey.name);
1478
+ (0, _src_core.validateBrowserJourney)(journey);
1479
+ const owned = structuredClone(journey);
1480
+ const path = this.#files.resolvePath(owned.name, BROWSER_JOURNEY_SNAPSHOT_FILE);
1481
+ const counter = this.#files.resolvePath(owned.name, BROWSER_JOURNEY_REVISION_FILE);
1482
+ return this.#files.lock(this.#files.resolvePath(owned.name, BROWSER_JOURNEY_LOCK_DIRECTORY), async () => {
1483
+ const current = await this.get(owned.name, options);
1484
+ if (expected !== void 0 && expected !== (current?.revision ?? 0)) throw new _src_core.BrowserError(`Journey ${owned.name} changed since you read it`, "BROWSER_JOURNEY_STALE");
1485
+ const source = await this.#files.read(counter, options);
1486
+ const previous = source === void 0 ? 0 : Number(source);
1487
+ if (!Number.isSafeInteger(previous) || previous < 0 || source?.trim() === "") throw new _src_core.BrowserError(`Malformed revision: ${counter}`, "BROWSER_JOURNEY_FILE");
1488
+ const revision = Math.max(previous, current?.revision ?? 0) + 1;
1489
+ if (!Number.isSafeInteger(revision)) throw new _src_core.BrowserError(`Exhausted revision: ${counter}`, "BROWSER_JOURNEY_FILE");
1490
+ const saved = {
1491
+ journey: owned,
1492
+ revision
1493
+ };
1494
+ await this.#files.write(path, JSON.stringify(saved), options);
1495
+ try {
1496
+ await this.#files.write(counter, String(revision), options);
1497
+ } catch (error) {
1498
+ if (current === void 0) await this.#files.remove(path);
1499
+ else await this.#files.write(path, JSON.stringify(current));
1500
+ throw error;
1501
+ }
1502
+ return saved;
1503
+ }, options);
1504
+ }
1505
+ async delete(name, options) {
1506
+ options?.signal?.throwIfAborted();
1507
+ this.#files.validateName(name);
1508
+ await this.#files.lock(this.#files.resolvePath(name, BROWSER_JOURNEY_LOCK_DIRECTORY), async () => {
1509
+ const current = await this.get(name, options);
1510
+ if (current === void 0) return;
1511
+ const counter = this.#files.resolvePath(name, BROWSER_JOURNEY_REVISION_FILE);
1512
+ const source = await this.#files.read(counter, options);
1513
+ const previous = source === void 0 ? 0 : Number(source);
1514
+ if (!Number.isSafeInteger(previous) || previous < 0 || source?.trim() === "") throw new _src_core.BrowserError(`Malformed revision: ${counter}`, "BROWSER_JOURNEY_FILE");
1515
+ await this.#files.write(counter, String(Math.max(previous, current.revision ?? 0)), options);
1516
+ await this.#files.remove(this.#files.resolvePath(name, BROWSER_JOURNEY_SNAPSHOT_FILE), options);
1517
+ }, options);
1518
+ }
1519
+ async list(options) {
1520
+ options?.signal?.throwIfAborted();
1521
+ return this.#files.list(this.#files.resolvePath(), async (name) => {
1522
+ if (!_src_core.BROWSER_JOURNEY_NAME_PATTERN.test(name)) return void 0;
1523
+ return this.get(name, options);
1524
+ }, options);
1525
+ }
1526
+ };
1527
+ //#endregion
1528
+ //#region src/server/stores/FileBrowserRunStore.ts
1529
+ /**
1530
+ * Persists runs and captures only in directories allocated by this instance.
1531
+ * @remarks Requires an existing root. Reopened stores can read and delete saved runs.
1532
+ * @example
1533
+ * const store = new FileBrowserRunStore({ root: directory })
1534
+ * const slot = await store.open('check-ready')
1535
+ */
1536
+ var FileBrowserRunStore = class {
1537
+ #files;
1538
+ #slots = /* @__PURE__ */ new WeakMap();
1539
+ #directories = /* @__PURE__ */ new Set();
1540
+ constructor(options) {
1541
+ this.#files = new FileBrowserStore(options);
1542
+ }
1543
+ async open(name, options) {
1544
+ options?.signal?.throwIfAborted();
1545
+ this.#files.validateName(name);
1546
+ return this.#files.lock(this.#files.resolvePath(name, BROWSER_JOURNEY_LOCK_DIRECTORY), async () => {
1547
+ const directory = await this.#files.allocate(this.#files.resolvePath(name, BROWSER_RUN_DIRECTORY), _src_core.generateBrowserRunId, options);
1548
+ const slot = Object.freeze({
1549
+ id: directory.id,
1550
+ directory: directory.path
1551
+ });
1552
+ this.#directories.add(directory.path);
1553
+ this.#slots.set(slot, directory.path);
1554
+ return slot;
1555
+ }, options);
1556
+ }
1557
+ async get(name, id, options) {
1558
+ options?.signal?.throwIfAborted();
1559
+ this.#files.validateName(name);
1560
+ this.#files.validateId(id);
1561
+ const path = this.#files.resolvePath(name, BROWSER_RUN_DIRECTORY, id, BROWSER_RUN_FILE);
1562
+ const source = await this.#files.read(path, options);
1563
+ if (source === void 0) return void 0;
1564
+ try {
1565
+ const value = (0, _orkestrel_contract.parseJSON)(source);
1566
+ if (!(0, _orkestrel_contract.isRecord)(value) || !("format" in value)) throw new _src_core.BrowserError("Missing run format", "BROWSER_JOURNEY_FILE");
1567
+ 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");
1568
+ (0, _src_core.validateBrowserRun)(value);
1569
+ if (value.id !== id || value.journey.name !== name) throw new _src_core.BrowserError("Run identity differs from its directory", "BROWSER_JOURNEY_FILE");
1570
+ return value;
1571
+ } catch (error) {
1572
+ throw this.#files.translateError(path, error);
1573
+ }
1574
+ }
1575
+ async set(run, options) {
1576
+ options?.signal?.throwIfAborted();
1577
+ this.#files.validateName(run.journey.name);
1578
+ this.#files.validateId(run.id);
1579
+ (0, _src_core.validateBrowserRun)(run);
1580
+ const source = JSON.stringify(run);
1581
+ const directory = this.#files.resolvePath(run.journey.name, BROWSER_RUN_DIRECTORY, run.id);
1582
+ await this.#requireDirectory(directory, options);
1583
+ await this.#files.write(this.#files.resolvePath(directory, BROWSER_RUN_FILE), source, options);
1584
+ }
1585
+ async capture(slot, name, bytes, options) {
1586
+ options?.signal?.throwIfAborted();
1587
+ const directory = this.#slots.get(slot);
1588
+ 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");
1589
+ const owned = new Uint8Array(bytes);
1590
+ await this.#requireDirectory(directory, options);
1591
+ await this.#files.write(this.#files.resolvePath(directory, name), owned, options);
1592
+ return name;
1593
+ }
1594
+ async delete(name, id, options) {
1595
+ options?.signal?.throwIfAborted();
1596
+ this.#files.validateName(name);
1597
+ this.#files.validateId(id);
1598
+ const directory = this.#files.resolvePath(name, BROWSER_RUN_DIRECTORY, id);
1599
+ await this.#files.check(this.#files.resolvePath(directory, BROWSER_RUN_FILE), options);
1600
+ try {
1601
+ options?.signal?.throwIfAborted();
1602
+ await (0, node_fs_promises.rm)(directory, {
1603
+ recursive: true,
1604
+ force: true,
1605
+ maxRetries: 3
1606
+ });
1607
+ this.#directories.delete(directory);
1608
+ options?.signal?.throwIfAborted();
1609
+ } catch (error) {
1610
+ options?.signal?.throwIfAborted();
1611
+ throw this.#files.translateError(directory, error);
1612
+ }
1613
+ }
1614
+ async clear(name, options) {
1615
+ options?.signal?.throwIfAborted();
1616
+ this.#files.validateName(name);
1617
+ const parent = this.#files.resolvePath(name, BROWSER_RUN_DIRECTORY);
1618
+ if (!await this.#files.check(parent, options)) return 0;
1619
+ return this.#files.lock(this.#files.resolvePath(name, BROWSER_JOURNEY_LOCK_DIRECTORY), async () => {
1620
+ try {
1621
+ options?.signal?.throwIfAborted();
1622
+ const ids = await (0, node_fs_promises.readdir)(parent);
1623
+ for (const id of ids) {
1624
+ this.#files.validateId(id);
1625
+ await this.#files.check(this.#files.resolvePath(parent, id, BROWSER_RUN_FILE), options);
1626
+ }
1627
+ for (const id of ids) await this.delete(name, id, options);
1628
+ return ids.length;
1629
+ } catch (error) {
1630
+ options?.signal?.throwIfAborted();
1631
+ throw this.#files.translateError(parent, error);
1632
+ }
1633
+ }, options);
1634
+ }
1635
+ async list(name, options) {
1636
+ options?.signal?.throwIfAborted();
1637
+ this.#files.validateName(name);
1638
+ return this.#files.list(this.#files.resolvePath(name, BROWSER_RUN_DIRECTORY), (id) => this.get(name, id, options), options);
1639
+ }
1640
+ async #requireDirectory(directory, options) {
1641
+ 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");
1642
+ try {
1643
+ options?.signal?.throwIfAborted();
1644
+ const status = await (0, node_fs_promises.lstat)(directory);
1645
+ options?.signal?.throwIfAborted();
1646
+ if (!status.isDirectory()) throw new _src_core.BrowserError(`Run directory is not a directory: ${directory}`, "BROWSER_JOURNEY_PATH");
1647
+ } catch (error) {
1648
+ options?.signal?.throwIfAborted();
1649
+ throw this.#files.translateError(directory, error);
1650
+ }
1651
+ }
1652
+ };
1653
+ //#endregion
818
1654
  //#region src/server/factories.ts
819
1655
  /**
820
1656
  * Creates a raw-CDP `BrowserInterface` façade with discovery, connection, and lifecycle
@@ -830,7 +1666,7 @@ var FileBrowserWriter = class {
830
1666
  * const browser = createBrowser({ headless: true })
831
1667
  * await browser.connect() // CDP endpoint discovery → connect, else launch
832
1668
  * const page = await browser.create({ url: 'https://example.com' })
833
- * await page.click('#accept')
1669
+ * await (await page.elements.find({ css: '#accept' }))[0]?.click()
834
1670
  * const shot = await page.screenshot({ path: './out.png' })
835
1671
  * await browser.destroy()
836
1672
  * ```
@@ -856,6 +1692,45 @@ function createCDPTransport(options) {
856
1692
  function createBrowserWriter() {
857
1693
  return new FileBrowserWriter();
858
1694
  }
1695
+ /**
1696
+ * Creates a journey store over an existing filesystem root.
1697
+ * @param options - Root and listing cap
1698
+ * @returns A durable journey store
1699
+ * @example
1700
+ * const store = createFileBrowserJourneyStore({ root: directory })
1701
+ */
1702
+ function createFileBrowserJourneyStore(options) {
1703
+ return new FileBrowserJourneyStore(options);
1704
+ }
1705
+ /**
1706
+ * Creates a run store over an existing filesystem root.
1707
+ * @param options - Root and listing cap
1708
+ * @returns A durable run and capture store
1709
+ * @example
1710
+ * const store = createFileBrowserRunStore({ root: directory })
1711
+ */
1712
+ function createFileBrowserRunStore(options) {
1713
+ return new FileBrowserRunStore(options);
1714
+ }
1715
+ /**
1716
+ * Creates the browse server, which serves the browser vocabulary and the journey tools over the
1717
+ * Model Context Protocol on stdio and launches Chromium on the first tool call.
1718
+ *
1719
+ * @param options - The root, the launch, the journeys' read-only switch, and the streams
1720
+ * @returns A {@link BrowserMCPServerInterface} that reads nothing until `start()`
1721
+ *
1722
+ * @example
1723
+ * ```ts
1724
+ * import { createBrowserMCPServer } from '@orkestrel/browser/server'
1725
+ *
1726
+ * const server = createBrowserMCPServer({ root: 'tmp/browsers', headless: true })
1727
+ * await server.start() // answers tools/list before Chromium starts
1728
+ * await server.destroy()
1729
+ * ```
1730
+ */
1731
+ function createBrowserMCPServer(options) {
1732
+ return new BrowserMCPServer(options);
1733
+ }
859
1734
  //#endregion
860
1735
  //#region src/server/Browser.ts
861
1736
  /**
@@ -975,7 +1850,7 @@ var Browser = class {
975
1850
  }
976
1851
  if (options?.origins !== void 0) params["originsWithUniversalNetworkAccess"] = [...options.origins];
977
1852
  const result = await client.send("Target.createBrowserContext", params);
978
- if (!(0, _orkestrel_contract.isRecord)(result) || !(0, _orkestrel_contract.isString)(result["browserContextId"])) throw new BrowserConnectionError("Failed to create isolated browser context");
1853
+ if (!(0, _orkestrel_contract.isRecord)(result) || !(0, _orkestrel_contract.isString)(result["browserContextId"])) throw new _src_core.BrowserConnectionError("Failed to create isolated browser context");
979
1854
  const id = result["browserContextId"];
980
1855
  const context = new _src_core.BrowserContext(client, id, options?.emulation?.viewport ?? this.#options.viewport, createBrowserWriter(), options?.emulation, options?.downloads, options);
981
1856
  try {
@@ -1057,8 +1932,9 @@ var Browser = class {
1057
1932
  if (this.#destroyed) throw new BrowserDestroyedError();
1058
1933
  this.#status = "error";
1059
1934
  this.#emitter.emit("error", error);
1060
- if (isBrowserConnectionError(error)) throw error;
1061
- throw new BrowserConnectionError((0, _orkestrel_contract.isError)(error) ? error.message : String(error), { executable: this.#options.executable });
1935
+ if ((0, _src_core.isBrowserConnectionError)(error)) throw error;
1936
+ const message = (0, _orkestrel_contract.isError)(error) ? error.message : String(error);
1937
+ throw new _src_core.BrowserConnectionError(message, { executable: this.#options.executable });
1062
1938
  }
1063
1939
  }
1064
1940
  async #detach() {
@@ -1079,7 +1955,7 @@ var Browser = class {
1079
1955
  this.#emitter.emit("idle");
1080
1956
  }
1081
1957
  #assertNotAborted() {
1082
- if (this.#signal().aborted) throw new BrowserConnectionError("Connection aborted");
1958
+ if (this.#signal().aborted) throw new _src_core.BrowserConnectionError("Connection aborted");
1083
1959
  }
1084
1960
  #signal() {
1085
1961
  const external = this.#options.signal;
@@ -1087,10 +1963,10 @@ var Browser = class {
1087
1963
  }
1088
1964
  async #raceAbort(promise) {
1089
1965
  const signal = this.#signal();
1090
- if (signal.aborted) throw new BrowserConnectionError("Connection aborted");
1966
+ if (signal.aborted) throw new _src_core.BrowserConnectionError("Connection aborted");
1091
1967
  const aborted = Promise.withResolvers();
1092
1968
  const listener = (0, node_events.addAbortListener)(signal, () => {
1093
- aborted.reject(new BrowserConnectionError("Connection aborted"));
1969
+ aborted.reject(new _src_core.BrowserConnectionError("Connection aborted"));
1094
1970
  });
1095
1971
  try {
1096
1972
  return await Promise.race([promise, aborted.promise]);
@@ -1118,7 +1994,7 @@ var Browser = class {
1118
1994
  this.#owned = void 0;
1119
1995
  this.#endpoint = void 0;
1120
1996
  if (connected) this.#status = "disconnected";
1121
- this.#emitter.emit("error", new BrowserConnectionError("The browser process exited unexpectedly", { cause: BROWSER_PROCESS_EXIT_CAUSE }));
1997
+ this.#emitter.emit("error", new _src_core.BrowserConnectionError("The browser process exited unexpectedly", { cause: BROWSER_PROCESS_EXIT_CAUSE }));
1122
1998
  if (connected) {
1123
1999
  this.#emitter.emit("disconnect");
1124
2000
  this.#emitter.emit("idle");
@@ -1156,7 +2032,7 @@ var Browser = class {
1156
2032
  }
1157
2033
  this.#destroyContextList(contexts);
1158
2034
  this.#closeClient(client);
1159
- this.#emitter.emit("error", new BrowserConnectionError("The CDP transport connection was lost", { cause: BROWSER_TRANSPORT_LOSS_CAUSE }));
2035
+ this.#emitter.emit("error", new _src_core.BrowserConnectionError("The CDP transport connection was lost", { cause: BROWSER_TRANSPORT_LOSS_CAUSE }));
1160
2036
  this.#emitter.emit("disconnect");
1161
2037
  this.#emitter.emit("idle");
1162
2038
  }
@@ -1209,7 +2085,7 @@ var Browser = class {
1209
2085
  };
1210
2086
  }
1211
2087
  async #assertPortFree() {
1212
- if (await this.#probePort()) throw new BrowserConnectionError(`Port ${this.#cdpPort} on ${this.#cdpHost} is already occupied by another CDP endpoint`, {
2088
+ if (await this.#probePort()) throw new _src_core.BrowserConnectionError(`Port ${this.#cdpPort} on ${this.#cdpHost} is already occupied by another CDP endpoint`, {
1213
2089
  port: this.#cdpPort,
1214
2090
  host: this.#cdpHost
1215
2091
  });
@@ -1256,7 +2132,7 @@ var Browser = class {
1256
2132
  }
1257
2133
  }
1258
2134
  async #launch() {
1259
- if (this.#process !== void 0) throw new BrowserConnectionError("A browser process is already active on this instance");
2135
+ if (this.#process !== void 0) throw new _src_core.BrowserConnectionError("A browser process is already active on this instance");
1260
2136
  const requestedEngine = this.#options.engine ?? this.#options.browsers?.engine;
1261
2137
  let executable = this.#options.executable;
1262
2138
  let resolvedEngine;
@@ -1269,14 +2145,14 @@ var Browser = class {
1269
2145
  executable = found?.executable;
1270
2146
  resolvedEngine = found?.engine;
1271
2147
  }
1272
- if (executable === void 0) throw new BrowserConnectionError("No Chromium browser found. Install Chrome, Edge, or Chromium.", requestedEngine === void 0 ? void 0 : { engine: requestedEngine });
2148
+ 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 });
1273
2149
  this.#engine = resolvedEngine ?? "chromium";
1274
2150
  await this.#releaseProfile();
1275
2151
  const profile = await createBrowserProfile(this.#options.profile);
1276
2152
  this.#profile = profile;
1277
2153
  let process;
1278
2154
  try {
1279
- process = launchBrowserProcess(executable, this.#cdpPort, this.#options.headless ?? true, profile.path, this.#options.args);
2155
+ process = launchBrowserProcess(executable, this.#options.cdp?.port, this.#options.headless ?? true, profile.path, this.#options.args);
1280
2156
  } catch (error) {
1281
2157
  await this.#releaseProfile();
1282
2158
  throw error;
@@ -1349,7 +2225,7 @@ var Browser = class {
1349
2225
  }
1350
2226
  if (pid === void 0) {
1351
2227
  await this.#closeRemote(client);
1352
- throw new BrowserConnectionError("The browser launcher exited without naming the process serving its CDP endpoint", {
2228
+ throw new _src_core.BrowserConnectionError("The browser launcher exited without naming the process serving its CDP endpoint", {
1353
2229
  executable: this.#options.executable,
1354
2230
  pid: process.pid
1355
2231
  });
@@ -1361,21 +2237,41 @@ var Browser = class {
1361
2237
  executable,
1362
2238
  args
1363
2239
  };
2240
+ const timeout = this.#timeout();
1364
2241
  const controller = new AbortController();
1365
- const signal = AbortSignal.any([controller.signal, this.#signal()]);
1366
- const ready = waitForCDPReady(this.#cdpPort, this.#timeout(), this.#cdpHost, signal);
1367
- const exited = (0, node_events.once)(process, "exit", { signal }).then((values) => {
2242
+ const deadline = AbortSignal.timeout(timeout);
2243
+ const signal = AbortSignal.any([
2244
+ controller.signal,
2245
+ this.#signal(),
2246
+ deadline
2247
+ ]);
2248
+ const stderr = process.stderr;
2249
+ if (stderr === null) throw new _src_core.BrowserConnectionError("The browser process has no standard error to read", context);
2250
+ const exit = (0, node_events.once)(process, "exit", { signal });
2251
+ exit.catch(() => void 0);
2252
+ const ready = readBrowserEndpoint(stderr, signal).catch(async (error) => {
2253
+ if (signal.aborted) throw error;
2254
+ const values = await exit;
2255
+ if (values[0] === 0 && values[1] === null) throw error;
2256
+ 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);
2257
+ });
2258
+ const exited = exit.then((values) => {
1368
2259
  const code = (0, _orkestrel_contract.isInteger)(values[0]) ? values[0] : null;
1369
2260
  const exitSignal = (0, _orkestrel_contract.isString)(values[1]) ? values[1] : null;
1370
2261
  if (code === 0 && exitSignal === null) return ready;
1371
- throw new BrowserConnectionError(this.#formatLaunchExit(code, exitSignal), context);
2262
+ throw new _src_core.BrowserConnectionError(this.#formatLaunchExit(code, exitSignal), context);
1372
2263
  });
1373
2264
  try {
1374
2265
  return await Promise.race([ready, exited]);
1375
2266
  } catch (error) {
1376
- if (isBrowserConnectionError(error)) throw error;
1377
- if (this.#signal().aborted) throw new BrowserConnectionError("Connection aborted", context);
1378
- throw new BrowserConnectionError((0, _orkestrel_contract.isError)(error) ? error.message : String(error), context);
2267
+ if ((0, _src_core.isBrowserConnectionError)(error)) throw error;
2268
+ if (this.#signal().aborted) throw new _src_core.BrowserConnectionError("Connection aborted", context);
2269
+ if (deadline.aborted) throw new _src_core.BrowserConnectionError(`Browser did not report a CDP endpoint within ${timeout}ms`, {
2270
+ ...context,
2271
+ timeout
2272
+ });
2273
+ const message = (0, _orkestrel_contract.isError)(error) ? error.message : String(error);
2274
+ throw new _src_core.BrowserConnectionError(message, context);
1379
2275
  } finally {
1380
2276
  controller.abort();
1381
2277
  ready.catch(() => void 0);
@@ -1593,13 +2489,23 @@ var Browser = class {
1593
2489
  const code = (0, _orkestrel_contract.isError)(error) && "code" in error && (0, _orkestrel_contract.isString)(error.code) ? error.code : void 0;
1594
2490
  if (code === "ESRCH") return false;
1595
2491
  if (signal === 0) return code === "EPERM" ? true : void 0;
1596
- throw new BrowserConnectionError("Failed to signal the browser process", {
2492
+ throw new _src_core.BrowserConnectionError("Failed to signal the browser process", {
1597
2493
  pid: serving ?? pid,
1598
2494
  signal,
1599
2495
  cause: error
1600
2496
  });
1601
2497
  }
1602
2498
  }
2499
+ /**
2500
+ * Waits within a bound for the rest of a terminated process group, or the process a
2501
+ * launcher handed the endpoint to, to disappear.
2502
+ *
2503
+ * @remarks
2504
+ * This is the one interval loop in the server. Node raises an exit event for the direct
2505
+ * child only, so no event source reports the remainder of a POSIX process group or a
2506
+ * handed-off process, and the drain probes on `BROWSER_DRAIN_INTERVAL_MS` until the
2507
+ * remainder is gone or `timeout` elapses.
2508
+ */
1603
2509
  async #waitForRemainderWithin(process, timeout) {
1604
2510
  const deadline = performance.now() + timeout;
1605
2511
  let confirmed = false;
@@ -1609,7 +2515,7 @@ var Browser = class {
1609
2515
  if (alive === true) confirmed = true;
1610
2516
  const remaining = deadline - performance.now();
1611
2517
  if (remaining <= 0) return confirmed ? false : void 0;
1612
- await new Promise((resolve) => setTimeout(resolve, Math.min(_src_core.BROWSER_WAIT_POLL_INTERVAL_MS, remaining)));
2518
+ await new Promise((resolve) => setTimeout(resolve, Math.min(100, remaining)));
1613
2519
  }
1614
2520
  }
1615
2521
  async #waitForTerminationWithin(process, timeout, final = false) {
@@ -1624,7 +2530,7 @@ var Browser = class {
1624
2530
  if (await this.#waitForTerminationWithin(process, 3e3)) return;
1625
2531
  if (this.#signalProcess(process, "SIGKILL") === false) return;
1626
2532
  if (await this.#waitForTerminationWithin(process, 3e3, true)) return;
1627
- throw new BrowserConnectionError("Browser process did not exit after SIGKILL", { pid: this.#servingPid ?? process.pid });
2533
+ throw new _src_core.BrowserConnectionError("Browser process did not exit after SIGKILL", { pid: this.#servingPid ?? process.pid });
1628
2534
  }
1629
2535
  #finish() {
1630
2536
  this.#unbindTransport();
@@ -1648,16 +2554,25 @@ exports.BROWSER_CDP_PROTOCOL = BROWSER_CDP_PROTOCOL;
1648
2554
  exports.BROWSER_CDP_VERSION_PATH = BROWSER_CDP_VERSION_PATH;
1649
2555
  exports.BROWSER_DEFAULT_CDP_PORT = BROWSER_DEFAULT_CDP_PORT;
1650
2556
  exports.BROWSER_DEFAULT_HOST = BROWSER_DEFAULT_HOST;
2557
+ exports.BROWSER_DEVTOOLS_PATTERN = BROWSER_DEVTOOLS_PATTERN;
2558
+ exports.BROWSER_DRAIN_INTERVAL_MS = BROWSER_DRAIN_INTERVAL_MS;
1651
2559
  exports.BROWSER_ENGINE_HINTS = BROWSER_ENGINE_HINTS;
1652
2560
  exports.BROWSER_ENV_PATH_KEYS = BROWSER_ENV_PATH_KEYS;
1653
2561
  exports.BROWSER_EXECUTABLE_NAMES = BROWSER_EXECUTABLE_NAMES;
1654
2562
  exports.BROWSER_EXECUTABLE_PATHS = BROWSER_EXECUTABLE_PATHS;
2563
+ exports.BROWSER_FILE_STORE_LIMIT = BROWSER_FILE_STORE_LIMIT;
1655
2564
  exports.BROWSER_HEADLESS_ARG = BROWSER_HEADLESS_ARG;
2565
+ exports.BROWSER_JOURNEY_LOCK_ATTEMPTS = BROWSER_JOURNEY_LOCK_ATTEMPTS;
2566
+ exports.BROWSER_JOURNEY_LOCK_DIRECTORY = BROWSER_JOURNEY_LOCK_DIRECTORY;
2567
+ exports.BROWSER_JOURNEY_REVISION_FILE = BROWSER_JOURNEY_REVISION_FILE;
2568
+ exports.BROWSER_JOURNEY_SNAPSHOT_FILE = BROWSER_JOURNEY_SNAPSHOT_FILE;
1656
2569
  exports.BROWSER_KILL_GRACE_MS = BROWSER_KILL_GRACE_MS;
1657
2570
  exports.BROWSER_LAUNCH_ARGS = BROWSER_LAUNCH_ARGS;
1658
2571
  exports.BROWSER_PORT_PROBE_TIMEOUT_MS = BROWSER_PORT_PROBE_TIMEOUT_MS;
1659
2572
  exports.BROWSER_PROCESS_EXIT_CAUSE = BROWSER_PROCESS_EXIT_CAUSE;
1660
2573
  exports.BROWSER_PROFILE_PREFIX = BROWSER_PROFILE_PREFIX;
2574
+ exports.BROWSER_RUN_DIRECTORY = BROWSER_RUN_DIRECTORY;
2575
+ exports.BROWSER_RUN_FILE = BROWSER_RUN_FILE;
1661
2576
  exports.BROWSER_STORE_CACHE_DIRS = BROWSER_STORE_CACHE_DIRS;
1662
2577
  exports.BROWSER_STORE_DEFAULT_DIRS = BROWSER_STORE_DEFAULT_DIRS;
1663
2578
  exports.BROWSER_STORE_ENV_KEY = BROWSER_STORE_ENV_KEY;
@@ -1668,9 +2583,11 @@ exports.BROWSER_TRANSPORT_LOSS_DEFER_MS = BROWSER_TRANSPORT_LOSS_DEFER_MS;
1668
2583
  exports.BROWSER_WINDOWS_ROOT_FALLBACKS = BROWSER_WINDOWS_ROOT_FALLBACKS;
1669
2584
  exports.BROWSER_WINDOWS_SUFFIXES = BROWSER_WINDOWS_SUFFIXES;
1670
2585
  exports.Browser = Browser;
1671
- exports.BrowserConnectionError = BrowserConnectionError;
1672
2586
  exports.BrowserDestroyedError = BrowserDestroyedError;
2587
+ exports.BrowserMCPServer = BrowserMCPServer;
1673
2588
  exports.BrowserNotConnectedError = BrowserNotConnectedError;
2589
+ exports.FileBrowserJourneyStore = FileBrowserJourneyStore;
2590
+ exports.FileBrowserRunStore = FileBrowserRunStore;
1674
2591
  exports.FileBrowserWriter = FileBrowserWriter;
1675
2592
  exports.WebSocketCDPTransport = WebSocketCDPTransport;
1676
2593
  exports.browserToEngine = browserToEngine;
@@ -1678,24 +2595,28 @@ exports.buildInstallPaths = buildInstallPaths;
1678
2595
  exports.buildStoreBases = buildStoreBases;
1679
2596
  exports.buildWindowsRoots = buildWindowsRoots;
1680
2597
  exports.createBrowser = createBrowser;
2598
+ exports.createBrowserMCPServer = createBrowserMCPServer;
1681
2599
  exports.createBrowserProfile = createBrowserProfile;
1682
2600
  exports.createBrowserWriter = createBrowserWriter;
1683
2601
  exports.createCDPTransport = createCDPTransport;
2602
+ exports.createFileBrowserJourneyStore = createFileBrowserJourneyStore;
2603
+ exports.createFileBrowserRunStore = createFileBrowserRunStore;
1684
2604
  exports.fetchCDPTargets = fetchCDPTargets;
1685
2605
  exports.findEnvOverrides = findEnvOverrides;
1686
2606
  exports.findInstallPaths = findInstallPaths;
1687
2607
  exports.findStorePaths = findStorePaths;
1688
2608
  exports.findSystemBrowser = findSystemBrowser;
1689
2609
  exports.findSystemBrowsers = findSystemBrowsers;
1690
- exports.isBrowserConnectionError = isBrowserConnectionError;
2610
+ exports.formatBrowserLockEntry = formatBrowserLockEntry;
1691
2611
  exports.isBrowserDestroyedError = isBrowserDestroyedError;
1692
2612
  exports.isBrowserNotConnectedError = isBrowserNotConnectedError;
1693
2613
  exports.launchBrowserProcess = launchBrowserProcess;
1694
2614
  exports.normalizeExecutablePath = normalizeExecutablePath;
1695
2615
  exports.parseBrowserEngine = parseBrowserEngine;
2616
+ exports.parseBrowserLockEntry = parseBrowserLockEntry;
1696
2617
  exports.probePathNames = probePathNames;
2618
+ exports.readBrowserEndpoint = readBrowserEndpoint;
1697
2619
  exports.readFirstLine = readFirstLine;
1698
2620
  exports.removeBrowserProfile = removeBrowserProfile;
1699
- exports.waitForCDPReady = waitForCDPReady;
1700
2621
 
1701
2622
  //# sourceMappingURL=index.cjs.map