@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.
- package/dist/bin/main.js +9 -3
- package/dist/bin/main.js.map +1 -1
- package/dist/src/core/index.cjs +58 -13
- package/dist/src/core/index.cjs.map +1 -1
- package/dist/src/core/index.d.cts +18 -10
- package/dist/src/core/index.d.ts +18 -10
- package/dist/src/core/index.js +58 -13
- package/dist/src/core/index.js.map +1 -1
- package/dist/src/server/index.cjs +392 -84
- package/dist/src/server/index.cjs.map +1 -1
- package/dist/src/server/index.d.cts +58 -4
- package/dist/src/server/index.d.ts +58 -4
- package/dist/src/server/index.js +390 -86
- package/dist/src/server/index.js.map +1 -1
- package/package.json +4 -4
package/dist/bin/main.js
CHANGED
|
@@ -1,16 +1,17 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
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
6
|
process.stderr.on("error", () => {
|
|
7
7
|
process.exitCode = 1;
|
|
8
8
|
});
|
|
9
9
|
try {
|
|
10
|
-
const { BROWSE_ROOT, BROWSE_HEADLESS, BROWSE_EXECUTABLE, BROWSE_READONLY, BROWSE_POOL } = process.env;
|
|
10
|
+
const { BROWSE_ROOT, BROWSE_HEADLESS, BROWSE_EXECUTABLE, BROWSE_READONLY, BROWSE_POOL, BROWSE_VIEWPORT } = process.env;
|
|
11
11
|
const headless = parseBoolean(BROWSE_HEADLESS);
|
|
12
12
|
const readonly = parseBoolean(BROWSE_READONLY);
|
|
13
13
|
const size = parseInteger(BROWSE_POOL);
|
|
14
|
+
const viewport = parseBrowserViewport(BROWSE_VIEWPORT);
|
|
14
15
|
for (const [name, value, parsed] of [[
|
|
15
16
|
"BROWSE_HEADLESS",
|
|
16
17
|
BROWSE_HEADLESS,
|
|
@@ -27,12 +28,17 @@ try {
|
|
|
27
28
|
name: "BROWSE_POOL",
|
|
28
29
|
value: BROWSE_POOL
|
|
29
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
|
+
});
|
|
30
35
|
await createBrowserMCPServer({
|
|
31
36
|
...BROWSE_ROOT === void 0 || BROWSE_ROOT === "" ? {} : { root: BROWSE_ROOT },
|
|
32
37
|
...headless === void 0 ? {} : { headless },
|
|
33
38
|
...BROWSE_EXECUTABLE === void 0 || BROWSE_EXECUTABLE === "" ? {} : { executable: BROWSE_EXECUTABLE },
|
|
34
39
|
...readonly === void 0 ? {} : { readonly },
|
|
35
|
-
...size === void 0 ? {} : { pool: { size } }
|
|
40
|
+
...size === void 0 ? {} : { pool: { size } },
|
|
41
|
+
...viewport === void 0 ? {} : { viewport }
|
|
36
42
|
}).start();
|
|
37
43
|
} catch (error) {
|
|
38
44
|
if (!isBrowserError(error)) throw error;
|
package/dist/bin/main.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
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 } 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 {
|
|
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"}
|
package/dist/src/core/index.cjs
CHANGED
|
@@ -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.
|
|
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
|
|
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 `
|
|
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
|
-
|
|
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(
|
|
6532
|
+
await this.#call(`async function() {
|
|
6533
|
+
await (${compileActionabilityFunction({
|
|
6518
6534
|
visible: true,
|
|
6519
6535
|
enabled: true,
|
|
6520
6536
|
stable: true
|
|
6521
|
-
})
|
|
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) {
|
|
@@ -12278,13 +12306,13 @@ function createMemoryBrowserRunStore() {
|
|
|
12278
12306
|
//#endregion
|
|
12279
12307
|
//#region src/core/BrowserJourneyToolset.ts
|
|
12280
12308
|
/**
|
|
12281
|
-
* Registers the journey tools `record`, `save`, `journeys`, `edit`, `replay`, and `
|
|
12309
|
+
* Registers the journey tools `record`, `save`, `journeys`, `edit`, `replay`, `forget`, and `capture` over a toolset
|
|
12282
12310
|
* and owns the recording and the active replay.
|
|
12283
12311
|
*
|
|
12284
12312
|
* @remarks
|
|
12285
12313
|
* The journey toolset composes the toolset's public members: `perform` reads the view a receipt
|
|
12286
12314
|
* carries through `look`, `hold` and `emitter` drive the recorder and the replay, `view` converts
|
|
12287
|
-
* an edit's `ref` to a target, and `tools` receives the
|
|
12315
|
+
* an edit's `ref` to a target, and `tools` receives the journey tools at construction, which refuses
|
|
12288
12316
|
* with `BROWSER_TOOLSET_RESERVED` when the manager already holds one of the names. Every refusal
|
|
12289
12317
|
* is a sentence that names the next call, and a reason inside it is a clause without a directive
|
|
12290
12318
|
* or a final period.
|
|
@@ -12300,7 +12328,7 @@ function createMemoryBrowserRunStore() {
|
|
|
12300
12328
|
* by the view after the run.
|
|
12301
12329
|
*
|
|
12302
12330
|
* `destroy()` aborts the active replay and waits for it to finish, stops a recording without
|
|
12303
|
-
* saving it, and removes the
|
|
12331
|
+
* saving it, and removes the journey tools the manager still holds under the instances it added.
|
|
12304
12332
|
*
|
|
12305
12333
|
* @example
|
|
12306
12334
|
* ```ts
|
|
@@ -12341,7 +12369,8 @@ var BrowserJourneyToolset = class {
|
|
|
12341
12369
|
this.#create("journeys", this.#journeys.bind(this)),
|
|
12342
12370
|
this.#create("edit", this.#edit.bind(this)),
|
|
12343
12371
|
this.#create("replay", this.#replayJourney.bind(this)),
|
|
12344
|
-
this.#create("forget", this.#forget.bind(this))
|
|
12372
|
+
this.#create("forget", this.#forget.bind(this)),
|
|
12373
|
+
this.#create("capture", this.#capture.bind(this))
|
|
12345
12374
|
]);
|
|
12346
12375
|
toolset.tools.add(this.#tools);
|
|
12347
12376
|
}
|
|
@@ -12368,6 +12397,22 @@ var BrowserJourneyToolset = class {
|
|
|
12368
12397
|
signal.throwIfAborted();
|
|
12369
12398
|
return handler(args, signal);
|
|
12370
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
|
+
}
|
|
12371
12416
|
async #record(args, signal) {
|
|
12372
12417
|
if (this.#readonly) throw new BrowserError(BROWSER_JOURNEY_READONLY_REFUSAL, "BROWSER_JOURNEY_READONLY");
|
|
12373
12418
|
this.#idleReplay();
|