@orkestrel/browser 0.0.22 → 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.
package/README.md CHANGED
@@ -73,7 +73,7 @@ await client.close()
73
73
 
74
74
  ## Browse binary
75
75
 
76
- The package ships the `browse` binary, which serves the browser vocabulary and the journey tools over MCP on stdio and launches Chromium on the first tool call, keeping journeys and runs under `tmp/browsers`. Register it for a checkout with `claude mcp add --scope project browse -- node node_modules/@orkestrel/browser/dist/bin/main.js`, which writes the `browse` entry of the project's `.mcp.json`, then start `claude` in the checkout and approve the server; `BROWSE_ROOT`, `BROWSE_HEADLESS`, `BROWSE_EXECUTABLE`, and `BROWSE_READONLY` configure it. See [Register the browse binary with Claude Code](guides/browser.md#register-the-browse-binary-with-claude-code) for the entry, the approval, and a recorded exchange.
76
+ The package ships the `browse` binary, which serves the browser vocabulary and the journey tools over MCP on stdio and starts Chromium when the server starts, before the client's first request, keeping journeys and runs under `tmp/browsers`. Register it for a checkout with `claude mcp add --scope project browse -- node node_modules/@orkestrel/browser/dist/bin/main.js`, which writes the `browse` entry of the project's `.mcp.json`, then start `claude` in the checkout and approve the server; `BROWSE_ROOT`, `BROWSE_HEADLESS`, `BROWSE_EXECUTABLE`, `BROWSE_READONLY`, and `BROWSE_POOL` configure it. See [Register the browse binary with Claude Code](guides/browser.md#register-the-browse-binary-with-claude-code) for the entry, the approval, the startup and recovery behavior, and a recorded exchange.
77
77
 
78
78
  ## Guide
79
79
 
package/dist/bin/main.js CHANGED
@@ -1,12 +1,17 @@
1
1
  #!/usr/bin/env node
2
- import { parseBoolean } from "@orkestrel/contract";
2
+ import { parseBoolean, parseInteger } from "@orkestrel/contract";
3
3
  import { BrowserError, isBrowserError } from "../src/core/index.js";
4
- import { createBrowserMCPServer } from "../src/server/index.js";
4
+ import { createBrowserMCPServer, parseBrowserViewport } from "../src/server/index.js";
5
5
  //#region src/bin/main.ts
6
+ process.stderr.on("error", () => {
7
+ process.exitCode = 1;
8
+ });
6
9
  try {
7
- const { BROWSE_ROOT, BROWSE_HEADLESS, BROWSE_EXECUTABLE, BROWSE_READONLY } = process.env;
10
+ const { BROWSE_ROOT, BROWSE_HEADLESS, BROWSE_EXECUTABLE, BROWSE_READONLY, BROWSE_POOL, BROWSE_VIEWPORT } = process.env;
8
11
  const headless = parseBoolean(BROWSE_HEADLESS);
9
12
  const readonly = parseBoolean(BROWSE_READONLY);
13
+ const size = parseInteger(BROWSE_POOL);
14
+ const viewport = parseBrowserViewport(BROWSE_VIEWPORT);
10
15
  for (const [name, value, parsed] of [[
11
16
  "BROWSE_HEADLESS",
12
17
  BROWSE_HEADLESS,
@@ -19,11 +24,21 @@ try {
19
24
  name,
20
25
  value
21
26
  });
27
+ if (BROWSE_POOL !== void 0 && BROWSE_POOL !== "" && size === void 0) throw new BrowserError(`BROWSE_POOL must be an integer, not ${JSON.stringify(BROWSE_POOL)}`, "BROWSER_SERVER_ENVIRONMENT", {
28
+ name: "BROWSE_POOL",
29
+ value: BROWSE_POOL
30
+ });
31
+ if (BROWSE_VIEWPORT !== void 0 && BROWSE_VIEWPORT !== "" && viewport === void 0) throw new BrowserError(`BROWSE_VIEWPORT must be positive integers in WIDTHxHEIGHT form, not ${JSON.stringify(BROWSE_VIEWPORT)}`, "BROWSER_SERVER_ENVIRONMENT", {
32
+ name: "BROWSE_VIEWPORT",
33
+ value: BROWSE_VIEWPORT
34
+ });
22
35
  await createBrowserMCPServer({
23
36
  ...BROWSE_ROOT === void 0 || BROWSE_ROOT === "" ? {} : { root: BROWSE_ROOT },
24
37
  ...headless === void 0 ? {} : { headless },
25
38
  ...BROWSE_EXECUTABLE === void 0 || BROWSE_EXECUTABLE === "" ? {} : { executable: BROWSE_EXECUTABLE },
26
- ...readonly === void 0 ? {} : { readonly }
39
+ ...readonly === void 0 ? {} : { readonly },
40
+ ...size === void 0 ? {} : { pool: { size } },
41
+ ...viewport === void 0 ? {} : { viewport }
27
42
  }).start();
28
43
  } catch (error) {
29
44
  if (!isBrowserError(error)) throw error;
@@ -1 +1 @@
1
- {"version":3,"file":"main.js","names":[],"sources":["../../src/bin/main.ts"],"sourcesContent":["import { parseBoolean } from '@orkestrel/contract'\nimport { BrowserError, isBrowserError } from '@src/core'\nimport { createBrowserMCPServer } from '@src/server'\n\ntry {\n\tconst { BROWSE_ROOT, BROWSE_HEADLESS, BROWSE_EXECUTABLE, BROWSE_READONLY } = process.env\n\tconst headless = parseBoolean(BROWSE_HEADLESS)\n\tconst readonly = parseBoolean(BROWSE_READONLY)\n\t// An empty variable counts as unset, as a shell's `NAME=` leaves it.\n\tfor (const [name, value, parsed] of [\n\t\t['BROWSE_HEADLESS', BROWSE_HEADLESS, headless],\n\t\t['BROWSE_READONLY', BROWSE_READONLY, readonly],\n\t] as const) {\n\t\tif (value !== undefined && value !== '' && parsed === undefined) {\n\t\t\tthrow new BrowserError(\n\t\t\t\t`${name} must be true, false, 1, or 0, not ${JSON.stringify(value)}`,\n\t\t\t\t'BROWSER_SERVER_ENVIRONMENT',\n\t\t\t\t{ name, value },\n\t\t\t)\n\t\t}\n\t}\n\tawait createBrowserMCPServer({\n\t\t...(BROWSE_ROOT === undefined || BROWSE_ROOT === '' ? {} : { root: BROWSE_ROOT }),\n\t\t...(headless === undefined ? {} : { headless }),\n\t\t...(BROWSE_EXECUTABLE === undefined || BROWSE_EXECUTABLE === ''\n\t\t\t? {}\n\t\t\t: { executable: BROWSE_EXECUTABLE }),\n\t\t...(readonly === undefined ? {} : { readonly }),\n\t}).start()\n} catch (error) {\n\tif (!isBrowserError(error)) throw error\n\tconsole.error(`browse: ${error.code}: ${error.message.split(/\\r\\n|\\n/u).join(' ')}`)\n\tprocess.exitCode = 1\n}\n"],"mappings":";;;;;AAIA,IAAI;CACH,MAAM,EAAE,aAAa,iBAAiB,mBAAmB,oBAAoB,QAAQ;CACrF,MAAM,WAAW,aAAa,eAAe;CAC7C,MAAM,WAAW,aAAa,eAAe;CAE7C,KAAK,MAAM,CAAC,MAAM,OAAO,WAAW,CACnC;EAAC;EAAmB;EAAiB;CAAQ,GAC7C;EAAC;EAAmB;EAAiB;CAAQ,CAC9C,GACC,IAAI,UAAU,KAAA,KAAa,UAAU,MAAM,WAAW,KAAA,GACrD,MAAM,IAAI,aACT,GAAG,KAAK,qCAAqC,KAAK,UAAU,KAAK,KACjE,8BACA;EAAE;EAAM;CAAM,CACf;CAGF,MAAM,uBAAuB;EAC5B,GAAI,gBAAgB,KAAA,KAAa,gBAAgB,KAAK,CAAC,IAAI,EAAE,MAAM,YAAY;EAC/E,GAAI,aAAa,KAAA,IAAY,CAAC,IAAI,EAAE,SAAS;EAC7C,GAAI,sBAAsB,KAAA,KAAa,sBAAsB,KAC1D,CAAC,IACD,EAAE,YAAY,kBAAkB;EACnC,GAAI,aAAa,KAAA,IAAY,CAAC,IAAI,EAAE,SAAS;CAC9C,CAAC,CAAC,CAAC,MAAM;AACV,SAAS,OAAO;CACf,IAAI,CAAC,eAAe,KAAK,GAAG,MAAM;CAClC,QAAQ,MAAM,WAAW,MAAM,KAAK,IAAI,MAAM,QAAQ,MAAM,UAAU,CAAC,CAAC,KAAK,GAAG,GAAG;CACnF,QAAQ,WAAW;AACpB"}
1
+ {"version":3,"file":"main.js","names":[],"sources":["../../src/bin/main.ts"],"sourcesContent":["import { parseBoolean, parseInteger } from '@orkestrel/contract'\nimport { BrowserError, isBrowserError } from '@src/core'\nimport { createBrowserMCPServer, parseBrowserViewport } from '@src/server'\n\n// The entry owns stderr; a closed reader must not interrupt the server's teardown barrier.\nprocess.stderr.on('error', () => {\n\tprocess.exitCode = 1\n})\n\ntry {\n\tconst {\n\t\tBROWSE_ROOT,\n\t\tBROWSE_HEADLESS,\n\t\tBROWSE_EXECUTABLE,\n\t\tBROWSE_READONLY,\n\t\tBROWSE_POOL,\n\t\tBROWSE_VIEWPORT,\n\t} = process.env\n\tconst headless = parseBoolean(BROWSE_HEADLESS)\n\tconst readonly = parseBoolean(BROWSE_READONLY)\n\tconst size = parseInteger(BROWSE_POOL)\n\tconst viewport = parseBrowserViewport(BROWSE_VIEWPORT)\n\t// An empty variable counts as unset, as a shell's `NAME=` leaves it.\n\tfor (const [name, value, parsed] of [\n\t\t['BROWSE_HEADLESS', BROWSE_HEADLESS, headless],\n\t\t['BROWSE_READONLY', BROWSE_READONLY, readonly],\n\t] as const) {\n\t\tif (value !== undefined && value !== '' && parsed === undefined) {\n\t\t\tthrow new BrowserError(\n\t\t\t\t`${name} must be true, false, 1, or 0, not ${JSON.stringify(value)}`,\n\t\t\t\t'BROWSER_SERVER_ENVIRONMENT',\n\t\t\t\t{ name, value },\n\t\t\t)\n\t\t}\n\t}\n\tif (BROWSE_POOL !== undefined && BROWSE_POOL !== '' && size === undefined)\n\t\tthrow new BrowserError(\n\t\t\t`BROWSE_POOL must be an integer, not ${JSON.stringify(BROWSE_POOL)}`,\n\t\t\t'BROWSER_SERVER_ENVIRONMENT',\n\t\t\t{ name: 'BROWSE_POOL', value: BROWSE_POOL },\n\t\t)\n\tif (BROWSE_VIEWPORT !== undefined && BROWSE_VIEWPORT !== '' && viewport === undefined)\n\t\tthrow new BrowserError(\n\t\t\t`BROWSE_VIEWPORT must be positive integers in WIDTHxHEIGHT form, not ${JSON.stringify(BROWSE_VIEWPORT)}`,\n\t\t\t'BROWSER_SERVER_ENVIRONMENT',\n\t\t\t{ name: 'BROWSE_VIEWPORT', value: BROWSE_VIEWPORT },\n\t\t)\n\tawait createBrowserMCPServer({\n\t\t...(BROWSE_ROOT === undefined || BROWSE_ROOT === '' ? {} : { root: BROWSE_ROOT }),\n\t\t...(headless === undefined ? {} : { headless }),\n\t\t...(BROWSE_EXECUTABLE === undefined || BROWSE_EXECUTABLE === ''\n\t\t\t? {}\n\t\t\t: { executable: BROWSE_EXECUTABLE }),\n\t\t...(readonly === undefined ? {} : { readonly }),\n\t\t...(size === undefined ? {} : { pool: { size } }),\n\t\t...(viewport === undefined ? {} : { viewport }),\n\t}).start()\n} catch (error) {\n\tif (!isBrowserError(error)) throw error\n\tconsole.error(`browse: ${error.code}: ${error.message.split(/\\r\\n|\\n/u).join(' ')}`)\n\tprocess.exitCode = 1\n}\n"],"mappings":";;;;;AAKA,QAAQ,OAAO,GAAG,eAAe;CAChC,QAAQ,WAAW;AACpB,CAAC;AAED,IAAI;CACH,MAAM,EACL,aACA,iBACA,mBACA,iBACA,aACA,oBACG,QAAQ;CACZ,MAAM,WAAW,aAAa,eAAe;CAC7C,MAAM,WAAW,aAAa,eAAe;CAC7C,MAAM,OAAO,aAAa,WAAW;CACrC,MAAM,WAAW,qBAAqB,eAAe;CAErD,KAAK,MAAM,CAAC,MAAM,OAAO,WAAW,CACnC;EAAC;EAAmB;EAAiB;CAAQ,GAC7C;EAAC;EAAmB;EAAiB;CAAQ,CAC9C,GACC,IAAI,UAAU,KAAA,KAAa,UAAU,MAAM,WAAW,KAAA,GACrD,MAAM,IAAI,aACT,GAAG,KAAK,qCAAqC,KAAK,UAAU,KAAK,KACjE,8BACA;EAAE;EAAM;CAAM,CACf;CAGF,IAAI,gBAAgB,KAAA,KAAa,gBAAgB,MAAM,SAAS,KAAA,GAC/D,MAAM,IAAI,aACT,uCAAuC,KAAK,UAAU,WAAW,KACjE,8BACA;EAAE,MAAM;EAAe,OAAO;CAAY,CAC3C;CACD,IAAI,oBAAoB,KAAA,KAAa,oBAAoB,MAAM,aAAa,KAAA,GAC3E,MAAM,IAAI,aACT,uEAAuE,KAAK,UAAU,eAAe,KACrG,8BACA;EAAE,MAAM;EAAmB,OAAO;CAAgB,CACnD;CACD,MAAM,uBAAuB;EAC5B,GAAI,gBAAgB,KAAA,KAAa,gBAAgB,KAAK,CAAC,IAAI,EAAE,MAAM,YAAY;EAC/E,GAAI,aAAa,KAAA,IAAY,CAAC,IAAI,EAAE,SAAS;EAC7C,GAAI,sBAAsB,KAAA,KAAa,sBAAsB,KAC1D,CAAC,IACD,EAAE,YAAY,kBAAkB;EACnC,GAAI,aAAa,KAAA,IAAY,CAAC,IAAI,EAAE,SAAS;EAC7C,GAAI,SAAS,KAAA,IAAY,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE;EAC/C,GAAI,aAAa,KAAA,IAAY,CAAC,IAAI,EAAE,SAAS;CAC9C,CAAC,CAAC,CAAC,MAAM;AACV,SAAS,OAAO;CACf,IAAI,CAAC,eAAe,KAAK,GAAG,MAAM;CAClC,QAAQ,MAAM,WAAW,MAAM,KAAK,IAAI,MAAM,QAAQ,MAAM,UAAU,CAAC,CAAC,KAAK,GAAG,GAAG;CACnF,QAAQ,WAAW;AACpB"}
@@ -163,7 +163,7 @@ var BROWSER_MOUSE_BUTTON_MASKS = Object.freeze({
163
163
  */
164
164
  var BROWSER_HAR_CREATOR = Object.freeze({
165
165
  name: "@orkestrel/browser",
166
- version: "0.0.22"
166
+ version: "0.0.24"
167
167
  });
168
168
  /** Names the attribute that tags temporary screenshot styles and masks. */
169
169
  var BROWSER_SCREENSHOT_ATTRIBUTE = "data-orkestrel-screenshot";
@@ -742,7 +742,7 @@ var BROWSER_TOOL_COPY = Object.freeze({
742
742
  description: "The journey name, such as add-kettle."
743
743
  }),
744
744
  edits: Object.freeze({
745
- description: "The changes, as an array or a JSON string of the array, applied in order; one invalid change refuses them all.",
745
+ description: "The changes, as an array or its JSON string, applied in order; one invalid change refuses them all.",
746
746
  anyOf: Object.freeze([Object.freeze({
747
747
  type: "array",
748
748
  items: Object.freeze({
@@ -818,6 +818,18 @@ var BROWSER_TOOL_COPY = Object.freeze({
818
818
  required: Object.freeze(["journey"])
819
819
  })
820
820
  }),
821
+ capture: Object.freeze({
822
+ name: "capture",
823
+ description: "Saves the current view as a PNG in the runs directory and returns its path.",
824
+ parameters: Object.freeze({
825
+ type: "object",
826
+ properties: Object.freeze({ full: Object.freeze({
827
+ type: "boolean",
828
+ description: "True captures the full page; false captures the viewport."
829
+ }) }),
830
+ required: Object.freeze(["full"])
831
+ })
832
+ }),
821
833
  forget: Object.freeze({
822
834
  name: "forget",
823
835
  description: "Removes a saved journey and all its runs; the name is free to record again.",
@@ -886,7 +898,7 @@ var BROWSER_ACTION_STAGES = Object.freeze([
886
898
  var BROWSER_JOURNEY_EMPTY_LISTING = "No journeys are saved; call record to start one.";
887
899
  /**
888
900
  * Names the journey tools a toolset constructed with `journeys` registers and reserves: `record`,
889
- * `save`, `journeys`, `edit`, `replay`, and `forget`.
901
+ * `save`, `journeys`, `edit`, `replay`, `forget`, and `capture`.
890
902
  */
891
903
  var BROWSER_JOURNEY_TOOL_NAMES = Object.freeze([
892
904
  "record",
@@ -894,7 +906,8 @@ var BROWSER_JOURNEY_TOOL_NAMES = Object.freeze([
894
906
  "journeys",
895
907
  "edit",
896
908
  "replay",
897
- "forget"
909
+ "forget",
910
+ "capture"
898
911
  ]);
899
912
  /** Names observation and journey tools that cannot become journey steps. */
900
913
  var BROWSER_JOURNEY_NON_STEP_TOOLS = Object.freeze([...BROWSER_OBSERVATION_TOOL_NAMES, ...BROWSER_JOURNEY_TOOL_NAMES]);
@@ -6451,9 +6464,11 @@ var BrowserPageElement = class {
6451
6464
  object: result["object"]["objectId"],
6452
6465
  context
6453
6466
  };
6454
- } catch {
6467
+ } catch (error) {
6455
6468
  options?.signal?.throwIfAborted();
6456
- throw new BrowserElementError(this.reference, "GONE");
6469
+ if (isCDPTimeoutError(error) || isCDPConnectionError(error)) throw error;
6470
+ if (isCDPError(error) && /Could not find node|No node with given id|No node found for given backend id/i.test(error.message)) throw new BrowserElementError(this.reference, "GONE");
6471
+ throw error;
6457
6472
  }
6458
6473
  }
6459
6474
  async #call(declaration, options, target) {
@@ -6482,7 +6497,7 @@ var BrowserPageElement = class {
6482
6497
  }
6483
6498
  #failure(options, error) {
6484
6499
  options?.signal?.throwIfAborted();
6485
- if (isBrowserElementError(error)) throw error;
6500
+ if (isBrowserElementError(error) || isCDPTimeoutError(error) || isCDPConnectionError(error)) throw error;
6486
6501
  const message = ((0, _orkestrel_contract.isError)(error) ? error.message : String(error)).split("\n", 1)[0] ?? "";
6487
6502
  const known = BROWSER_ELEMENT_REFUSALS.get(message.replace(/^Error: /, ""));
6488
6503
  if (known !== void 0) throw new BrowserElementError(this.reference, known.reason, known.detail);
@@ -6514,11 +6529,20 @@ var BrowserPageElement = class {
6514
6529
  session: this.#input.node.session,
6515
6530
  ...options
6516
6531
  }).catch(this.#failure.bind(this, options));
6517
- await this.#call(compileActionabilityFunction({
6532
+ await this.#call(`async function() {
6533
+ await (${compileActionabilityFunction({
6518
6534
  visible: true,
6519
6535
  enabled: true,
6520
6536
  stable: true
6521
- }), options);
6537
+ })}).call(this)
6538
+ let previous
6539
+ for (;;) {
6540
+ await new Promise((resolve) => requestAnimationFrame(resolve))
6541
+ const current = [window.scrollX, window.scrollY]
6542
+ if (previous && current.every((value, index) => value === previous[index])) return true
6543
+ previous = current
6544
+ }
6545
+ }`, options);
6522
6546
  const geometry = await this.#geometry(options);
6523
6547
  const point = geometry.page.center;
6524
6548
  const local = geometry.local.center;
@@ -6529,6 +6553,10 @@ var BrowserPageElement = class {
6529
6553
  }, {
6530
6554
  session: this.#input.node.session,
6531
6555
  ...options
6556
+ }).catch((error) => {
6557
+ options?.signal?.throwIfAborted();
6558
+ if (isCDPError(error) && error.context?.["message"] === "No node found at given location") throw new BrowserElementError(this.reference, "OCCLUDED", "hit-test location held no node");
6559
+ throw error;
6532
6560
  });
6533
6561
  if (!(0, _orkestrel_contract.isRecord)(hit) || !(0, _orkestrel_contract.isInteger)(hit["backendNodeId"]) || hit["frameId"] !== void 0 && hit["frameId"] !== this.#input.frame) throw new BrowserElementError(this.reference, "OCCLUDED");
6534
6562
  if (hit["backendNodeId"] !== this.#input.backend && await this.#call(compileHitFunction(), options, hit["backendNodeId"]) !== true) {
@@ -11184,6 +11212,7 @@ var BrowserContext = class {
11184
11212
  #viewport;
11185
11213
  #writer;
11186
11214
  #downloads;
11215
+ #allocator;
11187
11216
  #emitter;
11188
11217
  #cookies;
11189
11218
  #permissions;
@@ -11203,6 +11232,7 @@ var BrowserContext = class {
11203
11232
  this.#viewport = viewport;
11204
11233
  this.#writer = writer;
11205
11234
  this.#downloads = downloads;
11235
+ this.#allocator = options?.reference;
11206
11236
  this.#emitter = new _orkestrel_emitter.Emitter({
11207
11237
  ...options?.on !== void 0 ? { on: options.on } : {},
11208
11238
  ...options?.error !== void 0 ? { error: options.error } : {}
@@ -11339,7 +11369,7 @@ var BrowserContext = class {
11339
11369
  sessionId = await this.#openSession(targetId);
11340
11370
  await this.#enableSession(sessionId);
11341
11371
  const frameId = await this.#mainFrame(sessionId);
11342
- page = new BrowserPage(this.#client, targetId, sessionId, this.#writer, url, frameId, this.#id, void 0, options, this.#nextReference.bind(this), ready);
11372
+ page = new BrowserPage(this.#client, targetId, sessionId, this.#writer, url, frameId, this.#id, void 0, options, this.#allocator ?? this.#nextReference.bind(this), ready);
11343
11373
  this.#observe(page);
11344
11374
  await this.#configurePage(page);
11345
11375
  await this.#emulation.attach(page);
@@ -11358,7 +11388,7 @@ var BrowserContext = class {
11358
11388
  sessionId = await this.#openSession(targetId);
11359
11389
  await this.#enableSession(sessionId);
11360
11390
  const frameId = await this.#mainFrame(sessionId);
11361
- page = new BrowserPage(this.#client, targetId, sessionId, this.#writer, url, frameId, this.#id, void 0, void 0, this.#nextReference.bind(this), ready);
11391
+ page = new BrowserPage(this.#client, targetId, sessionId, this.#writer, url, frameId, this.#id, void 0, void 0, this.#allocator ?? this.#nextReference.bind(this), ready);
11362
11392
  this.#observe(page);
11363
11393
  await this.#configurePage(page);
11364
11394
  await this.#emulation.attach(page);
@@ -12276,13 +12306,13 @@ function createMemoryBrowserRunStore() {
12276
12306
  //#endregion
12277
12307
  //#region src/core/BrowserJourneyToolset.ts
12278
12308
  /**
12279
- * Registers the journey tools `record`, `save`, `journeys`, `edit`, `replay`, and `forget` over a toolset
12309
+ * Registers the journey tools `record`, `save`, `journeys`, `edit`, `replay`, `forget`, and `capture` over a toolset
12280
12310
  * and owns the recording and the active replay.
12281
12311
  *
12282
12312
  * @remarks
12283
12313
  * The journey toolset composes the toolset's public members: `perform` reads the view a receipt
12284
12314
  * carries through `look`, `hold` and `emitter` drive the recorder and the replay, `view` converts
12285
- * an edit's `ref` to a target, and `tools` receives the six tools at construction, which refuses
12315
+ * an edit's `ref` to a target, and `tools` receives the journey tools at construction, which refuses
12286
12316
  * with `BROWSER_TOOLSET_RESERVED` when the manager already holds one of the names. Every refusal
12287
12317
  * is a sentence that names the next call, and a reason inside it is a clause without a directive
12288
12318
  * or a final period.
@@ -12298,7 +12328,7 @@ function createMemoryBrowserRunStore() {
12298
12328
  * by the view after the run.
12299
12329
  *
12300
12330
  * `destroy()` aborts the active replay and waits for it to finish, stops a recording without
12301
- * saving it, and removes the six tools the manager still holds under the instances it added.
12331
+ * saving it, and removes the journey tools the manager still holds under the instances it added.
12302
12332
  *
12303
12333
  * @example
12304
12334
  * ```ts
@@ -12339,7 +12369,8 @@ var BrowserJourneyToolset = class {
12339
12369
  this.#create("journeys", this.#journeys.bind(this)),
12340
12370
  this.#create("edit", this.#edit.bind(this)),
12341
12371
  this.#create("replay", this.#replayJourney.bind(this)),
12342
- this.#create("forget", this.#forget.bind(this))
12372
+ this.#create("forget", this.#forget.bind(this)),
12373
+ this.#create("capture", this.#capture.bind(this))
12343
12374
  ]);
12344
12375
  toolset.tools.add(this.#tools);
12345
12376
  }
@@ -12366,6 +12397,22 @@ var BrowserJourneyToolset = class {
12366
12397
  signal.throwIfAborted();
12367
12398
  return handler(args, signal);
12368
12399
  }
12400
+ async #capture(args, signal) {
12401
+ const full = args["full"];
12402
+ if (!(0, _orkestrel_contract.isBoolean)(full)) throw new BrowserError("The full parameter must be true or false.", "BROWSER_TOOLSET_ARGUMENT");
12403
+ this.#idleReplay();
12404
+ const view = this.#toolset.view;
12405
+ if (!view.trusted) throw new BrowserError("This view is untrusted; capture requires a trusted browser view.", "BROWSER_CAPTURE_UNTRUSTED");
12406
+ if (view.screenshot === void 0) throw new BrowserError("This view cannot capture an image.", "BROWSER_CAPTURE_UNAVAILABLE");
12407
+ const runs = this.#runs;
12408
+ if (runs?.snapshot === void 0) throw new BrowserError("Capture requires a runs store with standalone file storage.", "BROWSER_CAPTURE_UNAVAILABLE");
12409
+ const screenshot = await view.screenshot({
12410
+ format: "png",
12411
+ full
12412
+ });
12413
+ signal.throwIfAborted();
12414
+ return runs.snapshot(screenshot.bytes, { signal });
12415
+ }
12369
12416
  async #record(args, signal) {
12370
12417
  if (this.#readonly) throw new BrowserError(BROWSER_JOURNEY_READONLY_REFUSAL, "BROWSER_JOURNEY_READONLY");
12371
12418
  this.#idleReplay();