@orkestrel/browser 0.0.23 → 0.0.24

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.
@@ -237,6 +237,84 @@ var BROWSER_SERVER_OPTIONS = "BROWSER_SERVER_OPTIONS";
237
237
  var BROWSER_SERVER_CRASH = "BROWSER_SERVER_CRASH";
238
238
  /** Names an interrupted call whose outcome is unknown. */
239
239
  var BROWSER_SERVER_UNRESOLVED = "BROWSER_SERVER_UNRESOLVED";
240
+ /** Names the refusal when every browser is admitted to a holder. */
241
+ var BROWSER_SERVER_BUSY = "BROWSER_SERVER_BUSY";
242
+ /** Names the refusal for an unknown or ended holder. */
243
+ var BROWSER_SERVER_HOLDER = "BROWSER_SERVER_HOLDER";
244
+ /** Defines the holder tools independently of the shared browser's vocabulary. */
245
+ var BROWSER_SERVER_COPY = Object.freeze({
246
+ acquire: Object.freeze({
247
+ name: "acquire",
248
+ description: "Acquires an independent browser and returns its holder id and tool catalog. Destroy the holder when its work ends.",
249
+ parameters: Object.freeze({
250
+ type: "object",
251
+ properties: Object.freeze({ purpose: Object.freeze({
252
+ type: "string",
253
+ description: "The work this browser will serve."
254
+ }) }),
255
+ required: Object.freeze(["purpose"])
256
+ })
257
+ }),
258
+ execute: Object.freeze({
259
+ name: "execute",
260
+ description: "Runs a tool on the selected holder, with that tool’s arguments inside arguments. Get its available tools with tools.",
261
+ parameters: Object.freeze({
262
+ type: "object",
263
+ properties: Object.freeze({
264
+ holder: Object.freeze({
265
+ type: "string",
266
+ description: "The holder id returned by acquire."
267
+ }),
268
+ name: Object.freeze({
269
+ type: "string",
270
+ description: "The tool name from this holder’s catalog."
271
+ }),
272
+ arguments: Object.freeze({
273
+ type: "object",
274
+ description: "The arguments for the selected tool."
275
+ })
276
+ }),
277
+ required: Object.freeze([
278
+ "holder",
279
+ "name",
280
+ "arguments"
281
+ ])
282
+ }),
283
+ annotations: Object.freeze({
284
+ pure: false,
285
+ untrusted: true
286
+ })
287
+ }),
288
+ tools: Object.freeze({
289
+ name: "tools",
290
+ description: "Lists the selected holder’s tools, including tools supplied by its page.",
291
+ parameters: Object.freeze({
292
+ type: "object",
293
+ properties: Object.freeze({ holder: Object.freeze({
294
+ type: "string",
295
+ description: "The holder id returned by acquire."
296
+ }) }),
297
+ required: Object.freeze(["holder"])
298
+ }),
299
+ annotations: Object.freeze({
300
+ pure: true,
301
+ untrusted: true
302
+ })
303
+ }),
304
+ destroy: Object.freeze({
305
+ name: "destroy",
306
+ description: "Ends the selected holder and destroys its browser. Calls still running on that holder report an unresolved outcome.",
307
+ parameters: Object.freeze({
308
+ type: "object",
309
+ properties: Object.freeze({ holder: Object.freeze({
310
+ type: "string",
311
+ description: "The holder id returned by acquire."
312
+ }) }),
313
+ required: Object.freeze(["holder"])
314
+ }),
315
+ annotations: Object.freeze({ pure: false })
316
+ })
317
+ });
240
318
  //#endregion
241
319
  //#region src/server/errors.ts
242
320
  /**
@@ -280,6 +358,25 @@ function isBrowserDestroyedError(value) {
280
358
  //#endregion
281
359
  //#region src/server/helpers.ts
282
360
  /**
361
+ * Parses positive integer viewport dimensions in WIDTHxHEIGHT form.
362
+ * @param value - The viewport environment variable, or undefined when unset
363
+ * @returns The viewport, or undefined when absent or malformed
364
+ * @example
365
+ * parseBrowserViewport('1280x720') // { width: 1280, height: 720 }
366
+ */
367
+ function parseBrowserViewport(value) {
368
+ if (value === void 0) return void 0;
369
+ const match = /^([0-9]+)x([0-9]+)$/u.exec(value);
370
+ if (match === null || match[0] !== value) return void 0;
371
+ const width = (0, _orkestrel_contract.parseInteger)(match[1]);
372
+ const height = (0, _orkestrel_contract.parseInteger)(match[2]);
373
+ if (width === void 0 || height === void 0 || !Number.isSafeInteger(width) || !Number.isSafeInteger(height) || width <= 0 || height <= 0) return void 0;
374
+ return {
375
+ width,
376
+ height
377
+ };
378
+ }
379
+ /**
283
380
  * Probes whether a process has not been confirmed absent.
284
381
  * @param pid - Process identifier to probe with signal zero
285
382
  * @returns True if the host has not confirmed absence with ESRCH; false otherwise
@@ -945,7 +1042,7 @@ var FileBrowserWriter = class {
945
1042
  };
946
1043
  //#endregion
947
1044
  //#region package.json
948
- var version = "0.0.23";
1045
+ var version = "0.0.24";
949
1046
  //#endregion
950
1047
  //#region src/server/BrowserMCPServer.ts
951
1048
  /**
@@ -960,8 +1057,11 @@ var version = "0.0.23";
960
1057
  * absent. After the launch, a tool the toolset's manager adds under another name, such as a page
961
1058
  * tool it adopts, is mirrored as a dispatcher with that tool's definition, and the mirror is
962
1059
  * removed when the toolset withdraws the tool, so the server's tool list changes and a subscribed
963
- * client is notified. Startup warms the pool and leases one validated browser. The legacy
964
- * handshake and every tool call await that setup; concurrent callers share a replacement lease.
1060
+ * client is notified. The server also advertises `acquire`, `execute`, `tools`, and `destroy` with
1061
+ * `BROWSER_SERVER_COPY`. Startup warms the pool and leases the shared browser. The legacy
1062
+ * handshake and browser calls await that setup. Named holders each own a lease, a pending grant,
1063
+ * and loss notices; concurrent calls on a holder share its replacement grant. Ending a holder
1064
+ * destroys its token, and admission counts that holder until disposal settles.
965
1065
  * A launch creates `ROOT/.profiles/PID-UUID/` exclusively, connects a browser that never attaches to an
966
1066
  * existing endpoint, opens one page in an isolated context, and constructs the page toolset with
967
1067
  * that context and the file journey and run stores under the root, on a manager the toolset owns.
@@ -971,6 +1071,8 @@ var version = "0.0.23";
971
1071
  * A disconnect or current-page crash retires its slot. A failed call receives a liveness ping;
972
1072
  * loss makes its outcome unresolved, while a known success remains successful. Pending losses
973
1073
  * prefix the next outcome on a successor, or a refusal, and calls are never repeated.
1074
+ * Concurrent checks share one ping per slot. Cancelling a call ends only its wait; the ping keeps
1075
+ * its deadline and records loss for the next call.
974
1076
  *
975
1077
  * `start()` serves stdio and destroys the server at the end of its input, on `SIGINT`, and on
976
1078
  * `SIGTERM`. `destroy()` stops reading requests and removes every dispatcher, then destroys every
@@ -994,6 +1096,7 @@ var BrowserMCPServer = class {
994
1096
  #root;
995
1097
  #headless;
996
1098
  #executable;
1099
+ #viewport;
997
1100
  #readonly;
998
1101
  #launcher;
999
1102
  #input;
@@ -1002,22 +1105,32 @@ var BrowserMCPServer = class {
1002
1105
  #abort = new AbortController();
1003
1106
  #signal;
1004
1107
  #vocabulary;
1005
- #added;
1006
- #removed;
1007
- #cleared;
1108
+ #mirrored;
1008
1109
  #pool;
1009
1110
  #log;
1010
1111
  #folders = /* @__PURE__ */ new Set();
1011
1112
  #faults = /* @__PURE__ */ new Set();
1012
1113
  #losses = /* @__PURE__ */ new WeakMap();
1114
+ #pings = /* @__PURE__ */ new WeakMap();
1013
1115
  #watches = /* @__PURE__ */ new Map();
1014
- #notice = /* @__PURE__ */ new Set();
1116
+ #shared = {
1117
+ id: "shared",
1118
+ purpose: "Shared browser",
1119
+ abort: this.#abort
1120
+ };
1121
+ #holders = /* @__PURE__ */ new Map([[this.#shared.id, this.#shared]]);
1122
+ #leases = /* @__PURE__ */ new Map();
1123
+ #grants = /* @__PURE__ */ new Map();
1124
+ #notices = /* @__PURE__ */ new Map();
1125
+ #disposals = /* @__PURE__ */ new Map();
1126
+ #drains = /* @__PURE__ */ new Map();
1127
+ #readers = /* @__PURE__ */ new Map();
1128
+ #writers = /* @__PURE__ */ new Set();
1129
+ #size;
1015
1130
  #reference;
1016
1131
  #references = 0;
1017
1132
  #failure;
1018
1133
  #stranded;
1019
- #lease;
1020
- #granting;
1021
1134
  #starting;
1022
1135
  #sweeping;
1023
1136
  #closing;
@@ -1029,6 +1142,7 @@ var BrowserMCPServer = class {
1029
1142
  constructor(options) {
1030
1143
  const size = options?.pool?.size ?? 1;
1031
1144
  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);
1145
+ this.#size = size;
1032
1146
  this.#log = options?.log ?? process.stderr;
1033
1147
  this.#reference = this.#issue.bind(this);
1034
1148
  this.#pool = (0, _orkestrel_pool.createPool)({
@@ -1043,15 +1157,32 @@ var BrowserMCPServer = class {
1043
1157
  this.#root = (0, node_path.resolve)(options?.root ?? "tmp/browsers");
1044
1158
  this.#headless = options?.headless ?? true;
1045
1159
  this.#executable = options?.executable;
1160
+ this.#viewport = options?.viewport;
1046
1161
  this.#readonly = options?.readonly ?? false;
1047
1162
  this.#launcher = options?.launch ?? createBrowser;
1048
1163
  this.#input = options?.stdio?.input ?? process.stdin;
1049
1164
  this.#tools = (0, _orkestrel_tool.createToolManager)();
1050
1165
  const names = [..._src_core.BROWSER_TOOL_NAMES, ..._src_core.BROWSER_JOURNEY_TOOL_NAMES];
1051
- this.#vocabulary = new Set(names);
1166
+ this.#vocabulary = /* @__PURE__ */ new Set([...names, ...Object.keys(BROWSER_SERVER_COPY)]);
1052
1167
  for (const name of names) this.#tools.add((0, _orkestrel_tool.createTool)({
1053
1168
  ..._src_core.BROWSER_TOOL_COPY[name],
1054
- execute: this.#forward.bind(this, name)
1169
+ execute: this.#forward.bind(this, this.#shared, name)
1170
+ }));
1171
+ this.#tools.add((0, _orkestrel_tool.createTool)({
1172
+ ...BROWSER_SERVER_COPY.acquire,
1173
+ execute: this.#acquire.bind(this)
1174
+ }));
1175
+ this.#tools.add((0, _orkestrel_tool.createTool)({
1176
+ ...BROWSER_SERVER_COPY.execute,
1177
+ execute: this.#dispatch.bind(this)
1178
+ }));
1179
+ this.#tools.add((0, _orkestrel_tool.createTool)({
1180
+ ...BROWSER_SERVER_COPY.tools,
1181
+ execute: this.#catalog.bind(this)
1182
+ }));
1183
+ this.#tools.add((0, _orkestrel_tool.createTool)({
1184
+ ...BROWSER_SERVER_COPY.destroy,
1185
+ execute: this.#dispose.bind(this)
1055
1186
  }));
1056
1187
  const server = (0, _orkestrel_mcp.createMCPServer)({
1057
1188
  identity: {
@@ -1067,9 +1198,6 @@ var BrowserMCPServer = class {
1067
1198
  ...options?.stdio?.output === void 0 ? {} : { output: options.stdio.output }
1068
1199
  });
1069
1200
  this.#signal = this.#end.bind(this);
1070
- this.#added = this.#mirror.bind(this);
1071
- this.#removed = this.#withdraw.bind(this);
1072
- this.#cleared = this.#clear.bind(this);
1073
1201
  }
1074
1202
  async start() {
1075
1203
  if (this.#closing !== void 0) throw this.#ended();
@@ -1090,7 +1218,7 @@ var BrowserMCPServer = class {
1090
1218
  if (this.#closing !== void 0) return;
1091
1219
  this.#sweeping = this.#sweep();
1092
1220
  const warming = this.#pool.start().then(void 0, (error) => error);
1093
- await this.#grant(this.#abort.signal);
1221
+ await this.#grant(this.#shared, this.#abort.signal);
1094
1222
  if (this.#closing !== void 0) return;
1095
1223
  warming.then(this.#exhaust.bind(this));
1096
1224
  } catch (error) {
@@ -1106,13 +1234,16 @@ var BrowserMCPServer = class {
1106
1234
  this.#transport.stop();
1107
1235
  for (const tool of this.#tools.tools()) this.#tools.remove(tool.name);
1108
1236
  this.#unmirror();
1109
- this.#lease = void 0;
1237
+ this.#leases.clear();
1238
+ for (const holder of this.#holders.values()) holder.abort.abort();
1110
1239
  this.#abort.abort();
1111
1240
  const barrier = this.#pool.destroy().catch((error) => {
1112
1241
  for (const failure of (0, _orkestrel_pool.isPoolError)(error) ? error.context?.failures ?? [error] : [error]) this.#faults.add(failure);
1113
1242
  });
1114
1243
  await this.#starting?.catch(() => void 0);
1115
1244
  await barrier;
1245
+ await Promise.allSettled(this.#disposals.values());
1246
+ this.#holders.clear();
1116
1247
  await this.#sweeping;
1117
1248
  if (this.#stranded !== void 0) this.#faults.add(this.#stranded);
1118
1249
  for (const folder of this.#folders) {
@@ -1157,22 +1288,141 @@ var BrowserMCPServer = class {
1157
1288
  process.exitCode = 1;
1158
1289
  this.#write(`browse: ${describeBrowserServerLoss(BROWSER_SERVER_TEARDOWN, error)}`);
1159
1290
  }
1160
- async #forward(name, args, context) {
1291
+ async #acquire(args, context) {
1292
+ context.signal.throwIfAborted();
1293
+ if (this.#closing !== void 0) throw this.#ended();
1294
+ const purpose = args["purpose"];
1295
+ if (!(0, _orkestrel_contract.isString)(purpose) || purpose.trim().length === 0) throw new _src_core.BrowserError("purpose must describe the work", "BROWSER_TOOLSET_ARGUMENT");
1296
+ if (this.#holders.size >= this.#size) 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);
1297
+ const holder = {
1298
+ id: (0, node_crypto.randomUUID)(),
1299
+ purpose,
1300
+ abort: new AbortController()
1301
+ };
1302
+ this.#holders.set(holder.id, holder);
1303
+ const listener = (0, node_events.addAbortListener)(context.signal, () => holder.abort.abort(context.signal.reason));
1304
+ try {
1305
+ const slot = await this.#serve(holder, context.signal);
1306
+ context.signal.throwIfAborted();
1307
+ if (holder.abort.signal.aborted || this.#holders.get(holder.id) !== holder) throw this.#ended();
1308
+ if (this.#leases.get(holder)?.value !== slot) throw this.#unavailable(this.#losses.get(slot)?.cause);
1309
+ const catalog = {
1310
+ holder: holder.id,
1311
+ tools: slot.toolset.tools.definitions()
1312
+ };
1313
+ return JSON.stringify(catalog);
1314
+ } catch (error) {
1315
+ await this.#retire(holder).catch(this.#fault.bind(this));
1316
+ throw error;
1317
+ } finally {
1318
+ listener[Symbol.dispose]();
1319
+ }
1320
+ }
1321
+ #holder(value) {
1322
+ const holder = (0, _orkestrel_contract.isString)(value) ? this.#holders.get(value) : void 0;
1323
+ 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);
1324
+ return holder;
1325
+ }
1326
+ async #dispatch(args, context) {
1327
+ const holder = this.#holder(args["holder"]);
1328
+ const name = args["name"];
1329
+ const parameters = args["arguments"];
1330
+ if (!(0, _orkestrel_contract.isString)(name) || !(0, _orkestrel_contract.isRecord)(parameters)) throw new _src_core.BrowserError("execute requires name and arguments", "BROWSER_TOOLSET_ARGUMENT");
1331
+ return this.#forward(holder, name, parameters, context);
1332
+ }
1333
+ async #catalog(args, context) {
1334
+ const holder = this.#holder(args["holder"]);
1335
+ const signal = AbortSignal.any([context.signal, holder.abort.signal]);
1336
+ try {
1337
+ const slot = await this.#serve(holder, signal);
1338
+ signal.throwIfAborted();
1339
+ if (this.#leases.get(holder)?.value !== slot) throw this.#unavailable(this.#losses.get(slot)?.cause);
1340
+ const catalog = {
1341
+ holder: holder.id,
1342
+ tools: slot.toolset.tools.definitions()
1343
+ };
1344
+ return this.#annotate(holder, JSON.stringify(catalog));
1345
+ } catch (error) {
1346
+ context.signal.throwIfAborted();
1347
+ if (holder.abort.signal.aborted) throw new _src_core.BrowserError(`${BROWSER_SERVER_UNRESOLVED}: the holder ended during the call`, BROWSER_SERVER_UNRESOLVED);
1348
+ if ((0, _src_core.isBrowserError)(error)) throw new _src_core.BrowserError(this.#annotate(holder, `${error.code}: ${error.message}`), error.code);
1349
+ throw error;
1350
+ }
1351
+ }
1352
+ async #dispose(args) {
1353
+ const id = args["holder"];
1354
+ await (((0, _orkestrel_contract.isString)(id) ? this.#disposals.get(id) : void 0) ?? this.#retire(this.#holder(id)));
1355
+ return "Holder destroyed.";
1356
+ }
1357
+ #retire(holder) {
1358
+ const pending = this.#disposals.get(holder.id);
1359
+ if (pending !== void 0) return pending;
1360
+ holder.abort.abort();
1361
+ const token = this.#leases.get(holder);
1362
+ this.#leases.delete(holder);
1363
+ const disposal = Promise.resolve().then(async () => {
1364
+ try {
1365
+ await this.#grants.get(holder)?.catch(() => void 0);
1366
+ await token?.destroy();
1367
+ await Promise.all(this.#drains.get(holder) ?? []);
1368
+ } catch (error) {
1369
+ this.#faults.add(error);
1370
+ } finally {
1371
+ if (this.#holders.get(holder.id) === holder) this.#holders.delete(holder.id);
1372
+ this.#notices.delete(holder);
1373
+ this.#drains.delete(holder);
1374
+ }
1375
+ });
1376
+ this.#disposals.set(holder.id, disposal);
1377
+ return disposal;
1378
+ }
1379
+ async #forward(holder, name, args, context) {
1380
+ context.signal.throwIfAborted();
1381
+ const journey = args["journey"];
1382
+ if (name !== "replay" && name !== "forget" || !(0, _orkestrel_contract.isString)(journey)) return this.#perform(holder, name, args, context);
1383
+ 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 });
1384
+ const reader = Symbol(journey);
1385
+ if (name === "forget") this.#writers.add(journey);
1386
+ else {
1387
+ const readers = this.#readers.get(journey) ?? /* @__PURE__ */ new Set();
1388
+ readers.add(reader);
1389
+ this.#readers.set(journey, readers);
1390
+ }
1391
+ try {
1392
+ return await this.#perform(holder, name, args, context);
1393
+ } finally {
1394
+ if (name === "forget") this.#writers.delete(journey);
1395
+ else {
1396
+ const readers = this.#readers.get(journey);
1397
+ readers?.delete(reader);
1398
+ if (readers?.size === 0) this.#readers.delete(journey);
1399
+ }
1400
+ }
1401
+ }
1402
+ async #perform(holder, name, args, context) {
1403
+ const signal = holder === this.#shared ? context.signal : AbortSignal.any([context.signal, holder.abort.signal]);
1161
1404
  let slot;
1162
1405
  try {
1163
- slot = await this.#serve(context.signal);
1406
+ slot = await this.#serve(holder, signal);
1164
1407
  } catch (error) {
1165
1408
  context.signal.throwIfAborted();
1166
- if ((0, _src_core.isBrowserError)(error)) throw new _src_core.BrowserError(this.#annotate(`${error.code}: ${error.message}`), error.code);
1409
+ 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);
1410
+ if ((0, _src_core.isBrowserError)(error)) throw new _src_core.BrowserError(this.#annotate(holder, `${error.code}: ${error.message}`), error.code);
1167
1411
  throw error;
1168
1412
  }
1169
1413
  let result;
1170
1414
  try {
1171
- result = await slot.toolset.tools.execute({
1415
+ const execution = slot.toolset.tools.execute({
1172
1416
  id: name,
1173
1417
  name,
1174
1418
  arguments: args
1175
- }, context);
1419
+ }, {
1420
+ ...context,
1421
+ signal
1422
+ });
1423
+ const outcome = await (name === "replay" || name === "forget" ? execution : this.#race(execution, signal));
1424
+ if (outcome === void 0) throw this.#ended();
1425
+ result = outcome;
1176
1426
  } catch (error) {
1177
1427
  result = {
1178
1428
  id: name,
@@ -1182,28 +1432,27 @@ var BrowserMCPServer = class {
1182
1432
  };
1183
1433
  }
1184
1434
  context.signal.throwIfAborted();
1435
+ 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);
1185
1436
  if (!result.success) {
1186
- if (this.#lease?.value === slot && !context.signal.aborted) try {
1187
- await slot.browser.ping({ signal: AbortSignal.any([context.signal, this.#abort.signal]) });
1188
- } catch (error) {
1189
- if (!context.signal.aborted && !this.#abort.signal.aborted) this.#lose(slot, error);
1190
- }
1437
+ if (this.#leases.get(holder)?.value === slot && !context.signal.aborted) try {
1438
+ await this.#race(this.#ping(slot), context.signal);
1439
+ } catch {}
1191
1440
  context.signal.throwIfAborted();
1192
- if (this.#lease?.value !== slot) {
1441
+ if (this.#leases.get(holder)?.value !== slot) {
1193
1442
  const loss = this.#losses.get(slot);
1194
- throw new _src_core.BrowserError(this.#annotate(describeBrowserServerLoss(BROWSER_SERVER_UNRESOLVED, loss?.cause, loss?.url), slot), BROWSER_SERVER_UNRESOLVED);
1443
+ throw new _src_core.BrowserError(this.#annotate(holder, describeBrowserServerLoss(BROWSER_SERVER_UNRESOLVED, loss?.cause, loss?.url), slot), BROWSER_SERVER_UNRESOLVED);
1195
1444
  }
1196
- throw new _src_core.BrowserError(this.#annotate(result.error, slot));
1445
+ throw new _src_core.BrowserError(this.#annotate(holder, result.error, slot));
1197
1446
  }
1198
- return typeof result.value === "string" ? this.#annotate(result.value, slot) : result.value;
1447
+ return typeof result.value === "string" ? this.#annotate(holder, result.value, slot) : result.value;
1199
1448
  }
1200
- #annotate(value, slot) {
1449
+ #annotate(holder, value, slot) {
1201
1450
  const notes = [];
1202
- for (const lost of this.#notice) {
1451
+ for (const lost of this.#notices.get(holder) ?? []) {
1203
1452
  if (lost === slot) continue;
1204
1453
  const loss = this.#losses.get(lost);
1205
1454
  notes.push(describeBrowserServerLoss(BROWSER_SERVER_CRASH, loss?.cause, loss?.url));
1206
- this.#notice.delete(lost);
1455
+ this.#notices.get(holder)?.delete(lost);
1207
1456
  }
1208
1457
  return [...notes, value].join("\n");
1209
1458
  }
@@ -1227,52 +1476,66 @@ var BrowserMCPServer = class {
1227
1476
  listener[Symbol.dispose]();
1228
1477
  }
1229
1478
  }
1230
- async #grant(signal) {
1231
- if (this.#lease !== void 0) return this.#lease;
1232
- this.#granting ??= this.#pool.acquire(this.#abort.signal).then(this.#hold.bind(this), this.#refuse.bind(this));
1233
- const token = await this.#race(this.#granting, signal);
1479
+ async #grant(holder, signal) {
1480
+ const held = this.#leases.get(holder);
1481
+ if (held !== void 0) return held;
1482
+ let granting = this.#grants.get(holder);
1483
+ if (granting === void 0) {
1484
+ granting = this.#pool.acquire(holder.abort.signal).then(this.#hold.bind(this, holder));
1485
+ this.#grants.set(holder, granting);
1486
+ granting.finally(() => {
1487
+ if (this.#grants.get(holder) === granting) this.#grants.delete(holder);
1488
+ }).catch(() => void 0);
1489
+ }
1490
+ const token = await this.#race(granting, signal);
1234
1491
  if (token === void 0) throw this.#ended();
1235
1492
  return token;
1236
1493
  }
1237
- #hold(token) {
1238
- this.#granting = void 0;
1239
- if (this.#closing !== void 0) throw this.#ended();
1494
+ async #hold(holder, token) {
1495
+ if (this.#closing !== void 0 || this.#holders.get(holder.id) !== holder || holder.abort.signal.aborted) {
1496
+ await token.destroy();
1497
+ throw this.#ended();
1498
+ }
1240
1499
  if (this.#losses.has(token.value)) {
1241
- token.destroy().catch(() => void 0);
1500
+ await token.destroy().catch(() => void 0);
1242
1501
  throw this.#unavailable(this.#losses.get(token.value)?.cause);
1243
1502
  }
1244
- this.#lease = token;
1245
- const tools = token.value.toolset.tools;
1246
- for (const tool of tools.tools()) this.#mirror(tool);
1247
- tools.emitter.on("add", this.#added);
1248
- tools.emitter.on("remove", this.#removed);
1249
- tools.emitter.on("clear", this.#cleared);
1503
+ this.#leases.set(holder, token);
1504
+ if (holder === this.#shared) {
1505
+ const tools = token.value.toolset.tools;
1506
+ const mirrored = {
1507
+ token,
1508
+ added: this.#mirror.bind(this, token),
1509
+ removed: this.#withdraw.bind(this, token),
1510
+ cleared: this.#clear.bind(this, token)
1511
+ };
1512
+ this.#mirrored = mirrored;
1513
+ for (const tool of tools.tools()) this.#mirror(token, tool);
1514
+ tools.emitter.on("add", mirrored.added);
1515
+ tools.emitter.on("remove", mirrored.removed);
1516
+ tools.emitter.on("clear", mirrored.cleared);
1517
+ }
1250
1518
  return token;
1251
1519
  }
1252
- #refuse(error) {
1253
- this.#granting = void 0;
1254
- throw error;
1255
- }
1256
- async #serve(signal) {
1257
- if (this.#closing !== void 0) throw this.#ended();
1520
+ async #serve(holder, signal) {
1521
+ if (this.#closing !== void 0 || holder.abort.signal.aborted) throw this.#ended();
1258
1522
  await this.#race(this.#starting, signal);
1259
- if (this.#closing !== void 0) throw this.#ended();
1260
- const held = this.#lease;
1523
+ if (this.#closing !== void 0 || holder.abort.signal.aborted) throw this.#ended();
1524
+ const held = this.#leases.get(holder);
1261
1525
  if (held !== void 0) try {
1262
- await held.value.browser.ping({ signal: AbortSignal.any([signal, this.#abort.signal]) });
1263
- if (this.#lease === held) return held.value;
1264
- } catch (error) {
1526
+ await this.#race(this.#ping(held.value), signal);
1527
+ if (this.#leases.get(holder) === held) return held.value;
1528
+ } catch {
1265
1529
  signal.throwIfAborted();
1266
- if (this.#abort.signal.aborted) throw this.#ended();
1267
- this.#lose(held.value, error);
1530
+ if (holder.abort.signal.aborted) throw this.#ended();
1268
1531
  }
1269
1532
  try {
1270
- const token = await this.#grant(signal);
1271
- if (this.#lease !== token) throw this.#unavailable(this.#losses.get(token.value)?.cause);
1533
+ const token = await this.#grant(holder, signal);
1534
+ if (this.#leases.get(holder) !== token) throw this.#unavailable(this.#losses.get(token.value)?.cause);
1272
1535
  return token.value;
1273
1536
  } catch (error) {
1274
1537
  signal.throwIfAborted();
1275
- if (this.#abort.signal.aborted || (0, _orkestrel_pool.isPoolError)(error) && error.code === "destroyed") throw this.#ended();
1538
+ if (holder.abort.signal.aborted || (0, _orkestrel_pool.isPoolError)(error) && error.code === "destroyed") throw this.#ended();
1276
1539
  if ((0, _orkestrel_pool.isPoolError)(error) && (error.code === "create" || error.code === "cleanup")) {
1277
1540
  this.#exhaust(error);
1278
1541
  throw this.#unavailable(error.cause ?? this.#failure);
@@ -1280,6 +1543,15 @@ var BrowserMCPServer = class {
1280
1543
  throw error;
1281
1544
  }
1282
1545
  }
1546
+ #ping(slot) {
1547
+ const pending = this.#pings.get(slot);
1548
+ if (pending !== void 0) return pending;
1549
+ const ping = slot.browser.ping({ signal: this.#abort.signal }).catch((error) => {
1550
+ if (!this.#abort.signal.aborted) this.#lose(slot, error);
1551
+ }).finally(() => this.#pings.delete(slot));
1552
+ this.#pings.set(slot, ping);
1553
+ return ping;
1554
+ }
1283
1555
  async #validate(slot) {
1284
1556
  try {
1285
1557
  await slot.browser.ping({ signal: this.#abort.signal });
@@ -1296,12 +1568,19 @@ var BrowserMCPServer = class {
1296
1568
  url: slot.toolset.view.url
1297
1569
  });
1298
1570
  this.#failure = cause;
1299
- if (this.#lease?.value !== slot) return;
1300
- const token = this.#lease;
1301
- this.#unmirror();
1302
- this.#lease = void 0;
1303
- this.#notice.add(slot);
1304
- token.destroy().catch(() => void 0);
1571
+ for (const [holder, token] of this.#leases) {
1572
+ if (token.value !== slot || this.#holders.get(holder.id) !== holder) continue;
1573
+ if (holder === this.#shared) this.#unmirror();
1574
+ this.#leases.delete(holder);
1575
+ const notices = this.#notices.get(holder) ?? /* @__PURE__ */ new Set();
1576
+ notices.add(slot);
1577
+ this.#notices.set(holder, notices);
1578
+ const drain = token.destroy();
1579
+ const drains = this.#drains.get(holder) ?? /* @__PURE__ */ new Set();
1580
+ drains.add(drain);
1581
+ this.#drains.set(holder, drains);
1582
+ drain.finally(() => drains.delete(drain)).catch(() => void 0);
1583
+ }
1305
1584
  }
1306
1585
  #watch(slot, signal) {
1307
1586
  const loss = Promise.withResolvers();
@@ -1374,6 +1653,7 @@ var BrowserMCPServer = class {
1374
1653
  cdp: { discover: false },
1375
1654
  signal: this.#abort.signal,
1376
1655
  ...this.#executable === void 0 ? {} : { executable: this.#executable },
1656
+ ...this.#viewport === void 0 ? {} : { viewport: this.#viewport },
1377
1657
  ...process.platform === "linux" && process.getuid?.() === 0 ? { args: ["--no-sandbox"] } : {}
1378
1658
  });
1379
1659
  await browser.connect();
@@ -1385,7 +1665,11 @@ var BrowserMCPServer = class {
1385
1665
  }));
1386
1666
  await (0, node_fs_promises.rename)(`${record}.tmp`, record);
1387
1667
  }
1388
- const context = await browser.isolate({ reference: this.#reference });
1668
+ const context = await browser.isolate({
1669
+ reference: this.#reference,
1670
+ downloads: { path: (0, node_path.join)(profile, "downloads") },
1671
+ ...this.#viewport === void 0 ? {} : { emulation: { viewport: this.#viewport } }
1672
+ });
1389
1673
  const page = await context.create();
1390
1674
  toolset = (0, _src_core.createBrowserToolset)(page, {
1391
1675
  context,
@@ -1492,29 +1776,31 @@ var BrowserMCPServer = class {
1492
1776
  #issue() {
1493
1777
  return `e${++this.#references}`;
1494
1778
  }
1495
- #mirror(tool) {
1496
- if (this.#vocabulary.has(tool.name)) return;
1779
+ #mirror(token, tool) {
1780
+ if (this.#leases.get(this.#shared) !== token || this.#vocabulary.has(tool.name)) return;
1497
1781
  this.#tools.add((0, _orkestrel_tool.createTool)({
1498
1782
  ...(0, _orkestrel_tool.toolToDefinition)(tool),
1499
- execute: this.#forward.bind(this, tool.name)
1783
+ execute: this.#forward.bind(this, this.#shared, tool.name)
1500
1784
  }));
1501
1785
  }
1502
- #withdraw(tool) {
1503
- if (this.#vocabulary.has(tool.name)) return;
1504
- const current = this.#lease?.value.toolset.tools.tool(tool.name);
1786
+ #withdraw(token, tool) {
1787
+ if (this.#leases.get(this.#shared) !== token || this.#vocabulary.has(tool.name)) return;
1788
+ const current = token.value.toolset.tools.tool(tool.name);
1505
1789
  if (current === void 0) this.#tools.remove(tool.name);
1506
- else this.#mirror(current);
1790
+ else this.#mirror(token, current);
1507
1791
  }
1508
- #clear(tools) {
1509
- for (const tool of tools) this.#withdraw(tool);
1792
+ #clear(token, tools) {
1793
+ for (const tool of tools) this.#withdraw(token, tool);
1510
1794
  }
1511
1795
  #unmirror() {
1512
- const mirrored = this.#lease?.value.toolset.tools;
1796
+ const mirrored = this.#mirrored;
1513
1797
  if (mirrored === void 0) return;
1514
- for (const tool of mirrored.tools()) if (!this.#vocabulary.has(tool.name)) this.#tools.remove(tool.name);
1515
- mirrored.emitter.off("add", this.#added);
1516
- mirrored.emitter.off("remove", this.#removed);
1517
- mirrored.emitter.off("clear", this.#cleared);
1798
+ this.#mirrored = void 0;
1799
+ const tools = mirrored.token.value.toolset.tools;
1800
+ for (const tool of tools.tools()) if (!this.#vocabulary.has(tool.name)) this.#tools.remove(tool.name);
1801
+ tools.emitter.off("add", mirrored.added);
1802
+ tools.emitter.off("remove", mirrored.removed);
1803
+ tools.emitter.off("clear", mirrored.cleared);
1518
1804
  }
1519
1805
  #ended() {
1520
1806
  return new _src_core.BrowserError("the browser session ended", "BROWSER_TOOLSET_ENDED");
@@ -1952,6 +2238,24 @@ var FileBrowserRunStore = class {
1952
2238
  constructor(options) {
1953
2239
  this.#files = new FileBrowserStore(options);
1954
2240
  }
2241
+ async snapshot(bytes, options) {
2242
+ options?.signal?.throwIfAborted();
2243
+ const owned = new Uint8Array(bytes);
2244
+ const directory = await this.#files.allocate(this.#files.resolvePath(BROWSER_RUN_DIRECTORY), _src_core.generateBrowserRunId, options);
2245
+ const path = this.#files.resolvePath(directory.path, "view.png");
2246
+ try {
2247
+ await this.#files.check(path, options);
2248
+ options?.signal?.throwIfAborted();
2249
+ await (0, node_fs_promises.writeFile)(path, owned, {
2250
+ flag: "wx",
2251
+ signal: options?.signal
2252
+ });
2253
+ return path;
2254
+ } catch (error) {
2255
+ options?.signal?.throwIfAborted();
2256
+ throw this.#files.translateError(path, error);
2257
+ }
2258
+ }
1955
2259
  async open(name, options) {
1956
2260
  options?.signal?.throwIfAborted();
1957
2261
  this.#files.validateName(name);
@@ -3006,8 +3310,11 @@ exports.BROWSER_PROCESS_EXIT_CAUSE = BROWSER_PROCESS_EXIT_CAUSE;
3006
3310
  exports.BROWSER_PROFILE_PREFIX = BROWSER_PROFILE_PREFIX;
3007
3311
  exports.BROWSER_RUN_DIRECTORY = BROWSER_RUN_DIRECTORY;
3008
3312
  exports.BROWSER_RUN_FILE = BROWSER_RUN_FILE;
3313
+ exports.BROWSER_SERVER_BUSY = BROWSER_SERVER_BUSY;
3314
+ exports.BROWSER_SERVER_COPY = BROWSER_SERVER_COPY;
3009
3315
  exports.BROWSER_SERVER_CRASH = BROWSER_SERVER_CRASH;
3010
3316
  exports.BROWSER_SERVER_EXHAUSTED = BROWSER_SERVER_EXHAUSTED;
3317
+ exports.BROWSER_SERVER_HOLDER = BROWSER_SERVER_HOLDER;
3011
3318
  exports.BROWSER_SERVER_LAUNCH = BROWSER_SERVER_LAUNCH;
3012
3319
  exports.BROWSER_SERVER_OPTIONS = BROWSER_SERVER_OPTIONS;
3013
3320
  exports.BROWSER_SERVER_POOL_LIMIT = BROWSER_SERVER_POOL_LIMIT;
@@ -3061,6 +3368,7 @@ exports.normalizeExecutablePath = normalizeExecutablePath;
3061
3368
  exports.parseBrowserEngine = parseBrowserEngine;
3062
3369
  exports.parseBrowserLockEntry = parseBrowserLockEntry;
3063
3370
  exports.parseBrowserProfileRecord = parseBrowserProfileRecord;
3371
+ exports.parseBrowserViewport = parseBrowserViewport;
3064
3372
  exports.probePathNames = probePathNames;
3065
3373
  exports.probeProcess = probeProcess;
3066
3374
  exports.readBrowserEndpoint = readBrowserEndpoint;