@csmodding/gameface-devtools-mcp 0.2.0 → 1.0.0
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 +43 -32
- package/dist/server.mjs +1750 -463
- package/package.json +1 -1
package/dist/server.mjs
CHANGED
|
@@ -30319,7 +30319,7 @@ class StdioServerTransport {
|
|
|
30319
30319
|
}
|
|
30320
30320
|
|
|
30321
30321
|
// src/server.ts
|
|
30322
|
-
var
|
|
30322
|
+
var import_common_tags5 = __toESM(require_lib(), 1);
|
|
30323
30323
|
|
|
30324
30324
|
// src/cdp.ts
|
|
30325
30325
|
var import_common_tags = __toESM(require_lib(), 1);
|
|
@@ -30399,8 +30399,8 @@ class CdpConnection {
|
|
|
30399
30399
|
this.target = target;
|
|
30400
30400
|
this.onEvent = onEvent;
|
|
30401
30401
|
ws.addEventListener("message", (event) => this.handleMessage(event));
|
|
30402
|
-
ws.addEventListener("close", () => this.handleClose(
|
|
30403
|
-
ws.addEventListener("error", () => this.handleClose(
|
|
30402
|
+
ws.addEventListener("close", () => this.handleClose(`connection closed`));
|
|
30403
|
+
ws.addEventListener("error", () => this.handleClose(`connection error`));
|
|
30404
30404
|
}
|
|
30405
30405
|
static async open(cfg, target, onEvent) {
|
|
30406
30406
|
const ws = new WebSocket(target.wsUrl);
|
|
@@ -30427,7 +30427,7 @@ class CdpConnection {
|
|
|
30427
30427
|
}
|
|
30428
30428
|
call(method, params) {
|
|
30429
30429
|
if (!this.isOpen) {
|
|
30430
|
-
return Promise.reject(new CdpError(
|
|
30430
|
+
return Promise.reject(new CdpError(`CDP connection is not open`));
|
|
30431
30431
|
}
|
|
30432
30432
|
const id = this.nextId++;
|
|
30433
30433
|
return new Promise((resolve, reject) => {
|
|
@@ -30453,7 +30453,7 @@ class CdpConnection {
|
|
|
30453
30453
|
this.enabledDomains.add(domain2);
|
|
30454
30454
|
}
|
|
30455
30455
|
close() {
|
|
30456
|
-
this.handleClose(
|
|
30456
|
+
this.handleClose(`closed by client`);
|
|
30457
30457
|
try {
|
|
30458
30458
|
this.ws.close();
|
|
30459
30459
|
} catch {}
|
|
@@ -30602,9 +30602,8 @@ function num(value, fallback) {
|
|
|
30602
30602
|
return Number.isFinite(parsed) && parsed > 0 ? parsed : fallback;
|
|
30603
30603
|
}
|
|
30604
30604
|
|
|
30605
|
-
// src/
|
|
30605
|
+
// src/console.ts
|
|
30606
30606
|
var import_common_tags2 = __toESM(require_lib(), 1);
|
|
30607
|
-
import { setTimeout as sleep } from "node:timers/promises";
|
|
30608
30607
|
|
|
30609
30608
|
// src/shared.ts
|
|
30610
30609
|
function text(value) {
|
|
@@ -30633,7 +30632,7 @@ function describeRemoteObject(obj) {
|
|
|
30633
30632
|
}
|
|
30634
30633
|
function formatException(details) {
|
|
30635
30634
|
const exception = details?.exception;
|
|
30636
|
-
const desc = exception?.description ?? exception?.value ?? details?.text ??
|
|
30635
|
+
const desc = exception?.description ?? exception?.value ?? details?.text ?? `unknown error`;
|
|
30637
30636
|
return typeof desc == "string" ? desc : JSON.stringify(desc);
|
|
30638
30637
|
}
|
|
30639
30638
|
function valToStr(value) {
|
|
@@ -30647,13 +30646,517 @@ function valToStr(value) {
|
|
|
30647
30646
|
}
|
|
30648
30647
|
}
|
|
30649
30648
|
|
|
30649
|
+
// src/console.ts
|
|
30650
|
+
var DEFAULT_CONSOLE_LIMIT = 50;
|
|
30651
|
+
var DEFAULT_MAX_ENTRIES = 500;
|
|
30652
|
+
var MILLIS_DIGITS = 3;
|
|
30653
|
+
var EPOCH_MS_FLOOR = 1000000000000;
|
|
30654
|
+
var CAPTURE_DEPTH_CAP = 4;
|
|
30655
|
+
var DEFAULT_RENDER_DEPTH = 2;
|
|
30656
|
+
var LEVEL_WIDTH = 20;
|
|
30657
|
+
var STRING_CLIP = 200;
|
|
30658
|
+
var DEFAULT_EXPAND_TIMEOUT_MS = 2000;
|
|
30659
|
+
var DEFAULT_MAX_PENDING = 32;
|
|
30660
|
+
|
|
30661
|
+
class ConsoleBuffer {
|
|
30662
|
+
entries = [];
|
|
30663
|
+
queue = [];
|
|
30664
|
+
isDraining = false;
|
|
30665
|
+
generation = 0;
|
|
30666
|
+
cdp;
|
|
30667
|
+
max;
|
|
30668
|
+
maxPending;
|
|
30669
|
+
expandTimeoutMs;
|
|
30670
|
+
constructor(cdp, reloads, options = {}) {
|
|
30671
|
+
this.cdp = cdp;
|
|
30672
|
+
this.max = options.max ?? DEFAULT_MAX_ENTRIES;
|
|
30673
|
+
this.maxPending = options.maxPending ?? DEFAULT_MAX_PENDING;
|
|
30674
|
+
this.expandTimeoutMs = options.expandTimeoutMs ?? DEFAULT_EXPAND_TIMEOUT_MS;
|
|
30675
|
+
cdp.onConnect(async (conn) => {
|
|
30676
|
+
await conn.ensureDomain("Runtime");
|
|
30677
|
+
await conn.ensureDomain("Log");
|
|
30678
|
+
});
|
|
30679
|
+
cdp.onEvent((method, params) => {
|
|
30680
|
+
this.handle(method, params ?? {});
|
|
30681
|
+
});
|
|
30682
|
+
reloads?.onReload((count) => {
|
|
30683
|
+
this.enqueueText("reload", "info", `view reloaded (#${count})`);
|
|
30684
|
+
});
|
|
30685
|
+
}
|
|
30686
|
+
read(options) {
|
|
30687
|
+
const { limit = DEFAULT_CONSOLE_LIMIT, level, clear = false } = options;
|
|
30688
|
+
const depth = Math.min(Math.max(options.depth ?? DEFAULT_RENDER_DEPTH, 1), CAPTURE_DEPTH_CAP);
|
|
30689
|
+
const filtered = level ? this.entries.filter((entry) => entry.level == level) : this.entries;
|
|
30690
|
+
const taken = filtered.slice(-limit);
|
|
30691
|
+
if (clear) {
|
|
30692
|
+
this.clear();
|
|
30693
|
+
}
|
|
30694
|
+
return taken.map((entry) => renderLine(entry, depth));
|
|
30695
|
+
}
|
|
30696
|
+
clear() {
|
|
30697
|
+
this.entries.length = 0;
|
|
30698
|
+
this.queue.length = 0;
|
|
30699
|
+
this.generation++;
|
|
30700
|
+
}
|
|
30701
|
+
handle(method, params) {
|
|
30702
|
+
if (method == "Runtime.consoleAPICalled") {
|
|
30703
|
+
this.enqueue({
|
|
30704
|
+
at: entryTime(params.timestamp),
|
|
30705
|
+
kind: "console",
|
|
30706
|
+
level: params.type ?? "log",
|
|
30707
|
+
args: params.args ?? [],
|
|
30708
|
+
expand: this.expanding() < this.maxPending
|
|
30709
|
+
});
|
|
30710
|
+
} else if (method == "Log.entryAdded") {
|
|
30711
|
+
const entry = params.entry ?? {};
|
|
30712
|
+
this.enqueueText(entry.source ?? "log", entry.level ?? "info", entry.text ?? "", entry.timestamp);
|
|
30713
|
+
} else if (method == "Runtime.exceptionThrown") {
|
|
30714
|
+
this.enqueueText("exception", "error", formatException(params.exceptionDetails), params.timestamp);
|
|
30715
|
+
}
|
|
30716
|
+
}
|
|
30717
|
+
enqueueText(kind, level, line, timestamp) {
|
|
30718
|
+
this.enqueue({
|
|
30719
|
+
at: entryTime(timestamp),
|
|
30720
|
+
kind,
|
|
30721
|
+
level,
|
|
30722
|
+
args: [{ type: "string", value: line }],
|
|
30723
|
+
expand: false
|
|
30724
|
+
});
|
|
30725
|
+
}
|
|
30726
|
+
enqueue(capture) {
|
|
30727
|
+
this.queue.push(capture);
|
|
30728
|
+
this.drain();
|
|
30729
|
+
}
|
|
30730
|
+
expanding() {
|
|
30731
|
+
return this.queue.filter((capture) => capture.expand).length;
|
|
30732
|
+
}
|
|
30733
|
+
async drain() {
|
|
30734
|
+
if (this.isDraining) {
|
|
30735
|
+
return;
|
|
30736
|
+
}
|
|
30737
|
+
this.isDraining = true;
|
|
30738
|
+
try {
|
|
30739
|
+
for (let next = this.queue.shift();next; next = this.queue.shift()) {
|
|
30740
|
+
const { generation } = this;
|
|
30741
|
+
const args = await Promise.all(next.args.map((arg) => this.resolveArg(arg, next.expand)));
|
|
30742
|
+
if (generation == this.generation) {
|
|
30743
|
+
this.push({ at: next.at, kind: next.kind, level: next.level, args });
|
|
30744
|
+
}
|
|
30745
|
+
}
|
|
30746
|
+
} finally {
|
|
30747
|
+
this.isDraining = false;
|
|
30748
|
+
}
|
|
30749
|
+
}
|
|
30750
|
+
async resolveArg(arg, expand) {
|
|
30751
|
+
if (arg.objectId == null) {
|
|
30752
|
+
return describedValue(arg);
|
|
30753
|
+
}
|
|
30754
|
+
if (expand) {
|
|
30755
|
+
const tree = await this.expand(arg.objectId);
|
|
30756
|
+
if (tree) {
|
|
30757
|
+
return tree;
|
|
30758
|
+
}
|
|
30759
|
+
}
|
|
30760
|
+
return previewValue(arg.preview) ?? describedValue(arg);
|
|
30761
|
+
}
|
|
30762
|
+
async expand(objectId) {
|
|
30763
|
+
try {
|
|
30764
|
+
const response = await withTimeout(this.cdp.call("Runtime.callFunctionOn", {
|
|
30765
|
+
objectId,
|
|
30766
|
+
functionDeclaration: SERIALIZE_SOURCE,
|
|
30767
|
+
arguments: [{ value: CAPTURE_DEPTH_CAP }, { value: LEVEL_WIDTH }, { value: STRING_CLIP }],
|
|
30768
|
+
returnByValue: true
|
|
30769
|
+
}), this.expandTimeoutMs);
|
|
30770
|
+
const value = response.result?.value;
|
|
30771
|
+
if (response.exceptionDetails || value == null || typeof value != "object") {
|
|
30772
|
+
return;
|
|
30773
|
+
}
|
|
30774
|
+
return value;
|
|
30775
|
+
} catch {
|
|
30776
|
+
return;
|
|
30777
|
+
}
|
|
30778
|
+
}
|
|
30779
|
+
push(entry) {
|
|
30780
|
+
this.entries.push(entry);
|
|
30781
|
+
if (this.entries.length > this.max) {
|
|
30782
|
+
this.entries.splice(0, this.entries.length - this.max);
|
|
30783
|
+
}
|
|
30784
|
+
}
|
|
30785
|
+
}
|
|
30786
|
+
async function gameConsole(client, buffer, options) {
|
|
30787
|
+
try {
|
|
30788
|
+
await client.connection();
|
|
30789
|
+
} catch (error51) {
|
|
30790
|
+
return toErrorResult(error51);
|
|
30791
|
+
}
|
|
30792
|
+
const lines = buffer.read(options);
|
|
30793
|
+
if (lines.length == 0) {
|
|
30794
|
+
return text(import_common_tags2.oneLine`
|
|
30795
|
+
No console entries captured yet.
|
|
30796
|
+
Capture begins once the server connects to the application;
|
|
30797
|
+
trigger some UI activity (or a game_eval console.log) and retry.
|
|
30798
|
+
`);
|
|
30799
|
+
}
|
|
30800
|
+
return text(lines.join(`
|
|
30801
|
+
`));
|
|
30802
|
+
}
|
|
30803
|
+
function entryTime(timestamp) {
|
|
30804
|
+
const isEpoch = typeof timestamp == "number" && timestamp > EPOCH_MS_FLOOR;
|
|
30805
|
+
return isEpoch ? timestamp : Date.now();
|
|
30806
|
+
}
|
|
30807
|
+
function withTimeout(promise3, timeoutMs) {
|
|
30808
|
+
let timer;
|
|
30809
|
+
const deadline = new Promise((_resolve, reject) => {
|
|
30810
|
+
timer = setTimeout(() => reject(new Error(`console expansion timed out`)), timeoutMs);
|
|
30811
|
+
});
|
|
30812
|
+
return Promise.race([promise3, deadline]).finally(() => clearTimeout(timer));
|
|
30813
|
+
}
|
|
30814
|
+
function serializeConsoleArgFn(cap, width, clip) {
|
|
30815
|
+
const seen = new Set;
|
|
30816
|
+
return walk(this, 0);
|
|
30817
|
+
function walk(value, depth) {
|
|
30818
|
+
if (typeof value == "string") {
|
|
30819
|
+
return value.length > clip ? { kind: "string", text: value.slice(0, clip), clipped: true } : { kind: "string", text: value };
|
|
30820
|
+
}
|
|
30821
|
+
if (value === null || typeof value != "object") {
|
|
30822
|
+
return { kind: "raw", text: scalarText(value) };
|
|
30823
|
+
}
|
|
30824
|
+
const node = value;
|
|
30825
|
+
if (typeof node.nodeType == "number") {
|
|
30826
|
+
return { kind: "node", text: nodeText(node) };
|
|
30827
|
+
}
|
|
30828
|
+
if (value instanceof Date) {
|
|
30829
|
+
return { kind: "raw", text: dateText(value) };
|
|
30830
|
+
}
|
|
30831
|
+
if (value instanceof RegExp || value instanceof Error) {
|
|
30832
|
+
return { kind: "raw", text: String(value) };
|
|
30833
|
+
}
|
|
30834
|
+
if (seen.has(value)) {
|
|
30835
|
+
return { kind: "circular" };
|
|
30836
|
+
}
|
|
30837
|
+
const shape = Array.isArray(value) ? "array" : value instanceof Map ? "map" : value instanceof Set ? "set" : "object";
|
|
30838
|
+
if (depth >= cap) {
|
|
30839
|
+
return {
|
|
30840
|
+
kind: "capped",
|
|
30841
|
+
of: shape,
|
|
30842
|
+
ctor: shape == "object" ? ctorName(value) : undefined,
|
|
30843
|
+
size: sizeOf(value, shape)
|
|
30844
|
+
};
|
|
30845
|
+
}
|
|
30846
|
+
seen.add(value);
|
|
30847
|
+
try {
|
|
30848
|
+
return expand(value, shape, depth);
|
|
30849
|
+
} finally {
|
|
30850
|
+
seen.delete(value);
|
|
30851
|
+
}
|
|
30852
|
+
}
|
|
30853
|
+
function expand(value, shape, depth) {
|
|
30854
|
+
if (shape == "array") {
|
|
30855
|
+
const all = value;
|
|
30856
|
+
const items = all.slice(0, width).map((item) => walk(item, depth + 1));
|
|
30857
|
+
return { kind: "array", items, more: dropped(all.length, items.length) };
|
|
30858
|
+
}
|
|
30859
|
+
if (shape == "map") {
|
|
30860
|
+
const all = Array.from(value.entries());
|
|
30861
|
+
const pairs = all.slice(0, width).map((pair) => [walk(pair[0], depth + 1), walk(pair[1], depth + 1)]);
|
|
30862
|
+
return { kind: "map", size: all.length, pairs, more: dropped(all.length, pairs.length) };
|
|
30863
|
+
}
|
|
30864
|
+
if (shape == "set") {
|
|
30865
|
+
const all = Array.from(value.values());
|
|
30866
|
+
const items = all.slice(0, width).map((item) => walk(item, depth + 1));
|
|
30867
|
+
return { kind: "set", size: all.length, items, more: dropped(all.length, items.length) };
|
|
30868
|
+
}
|
|
30869
|
+
const bag = value;
|
|
30870
|
+
const keys = Object.keys(bag);
|
|
30871
|
+
const entries = keys.slice(0, width).map((key) => [key, readProperty(bag, key, depth)]);
|
|
30872
|
+
return {
|
|
30873
|
+
kind: "object",
|
|
30874
|
+
ctor: ctorName(value),
|
|
30875
|
+
entries,
|
|
30876
|
+
more: dropped(keys.length, entries.length)
|
|
30877
|
+
};
|
|
30878
|
+
}
|
|
30879
|
+
function dropped(total, kept) {
|
|
30880
|
+
return total > kept ? total - kept : undefined;
|
|
30881
|
+
}
|
|
30882
|
+
function readProperty(bag, key, depth) {
|
|
30883
|
+
try {
|
|
30884
|
+
return walk(bag[key], depth + 1);
|
|
30885
|
+
} catch {
|
|
30886
|
+
return { kind: "raw", text: "[unreadable]" };
|
|
30887
|
+
}
|
|
30888
|
+
}
|
|
30889
|
+
function scalarText(value) {
|
|
30890
|
+
if (typeof value == "function") {
|
|
30891
|
+
const named = value;
|
|
30892
|
+
return named.name ? `[Function: ${named.name}]` : `[Function (anonymous)]`;
|
|
30893
|
+
}
|
|
30894
|
+
if (typeof value == "bigint") {
|
|
30895
|
+
return `${value}n`;
|
|
30896
|
+
}
|
|
30897
|
+
if (typeof value == "symbol") {
|
|
30898
|
+
return value.toString();
|
|
30899
|
+
}
|
|
30900
|
+
return String(value);
|
|
30901
|
+
}
|
|
30902
|
+
function nodeText(node) {
|
|
30903
|
+
const name = typeof node.tagName == "string" ? node.tagName : String(node.nodeName);
|
|
30904
|
+
const id = typeof node.id == "string" && node.id ? `#${node.id}` : "";
|
|
30905
|
+
const classes = typeof node.className == "string" ? node.className.trim() : "";
|
|
30906
|
+
const suffix = classes ? `.${classes.split(/\s+/u).join(".")}` : "";
|
|
30907
|
+
return `<${name.toLowerCase()}${id}${suffix}>`;
|
|
30908
|
+
}
|
|
30909
|
+
function dateText(value) {
|
|
30910
|
+
try {
|
|
30911
|
+
return value.toISOString();
|
|
30912
|
+
} catch {
|
|
30913
|
+
return String(value);
|
|
30914
|
+
}
|
|
30915
|
+
}
|
|
30916
|
+
function ctorName(value) {
|
|
30917
|
+
const name = value.constructor?.name;
|
|
30918
|
+
return name && name != "Object" ? name : undefined;
|
|
30919
|
+
}
|
|
30920
|
+
function sizeOf(value, shape) {
|
|
30921
|
+
return shape == "map" || shape == "set" ? value.size : undefined;
|
|
30922
|
+
}
|
|
30923
|
+
}
|
|
30924
|
+
var SERIALIZE_SOURCE = serializeConsoleArgFn.toString();
|
|
30925
|
+
function renderLine(entry, depth) {
|
|
30926
|
+
const args = entry.args.map((arg) => renderArgument(arg, depth));
|
|
30927
|
+
return `${formatClock(entry.at)} [${entry.level}] (${entry.kind}) ${args.join(" ")}`;
|
|
30928
|
+
}
|
|
30929
|
+
function renderArgument(value, depth) {
|
|
30930
|
+
try {
|
|
30931
|
+
return renderValue(value, depth);
|
|
30932
|
+
} catch {
|
|
30933
|
+
return `[unrenderable]`;
|
|
30934
|
+
}
|
|
30935
|
+
}
|
|
30936
|
+
function formatClock(at) {
|
|
30937
|
+
const date6 = new Date(at);
|
|
30938
|
+
const hours = String(date6.getHours()).padStart(2, "0");
|
|
30939
|
+
const minutes = String(date6.getMinutes()).padStart(2, "0");
|
|
30940
|
+
const seconds = String(date6.getSeconds()).padStart(2, "0");
|
|
30941
|
+
const millis = String(date6.getMilliseconds()).padStart(MILLIS_DIGITS, "0");
|
|
30942
|
+
return `${hours}:${minutes}:${seconds}.${millis}`;
|
|
30943
|
+
}
|
|
30944
|
+
function renderValue(value, depth) {
|
|
30945
|
+
switch (value.kind) {
|
|
30946
|
+
case "raw":
|
|
30947
|
+
case "node": {
|
|
30948
|
+
return value.text;
|
|
30949
|
+
}
|
|
30950
|
+
case "string": {
|
|
30951
|
+
const held = value.text.replaceAll("\r", String.raw`\r`).replaceAll(`
|
|
30952
|
+
`, String.raw`\n`);
|
|
30953
|
+
return `'${held}${value.clipped ? "…" : ""}'`;
|
|
30954
|
+
}
|
|
30955
|
+
case "circular": {
|
|
30956
|
+
return `[Circular]`;
|
|
30957
|
+
}
|
|
30958
|
+
case "capped": {
|
|
30959
|
+
return collapsed(value);
|
|
30960
|
+
}
|
|
30961
|
+
case "object": {
|
|
30962
|
+
if (depth <= 0) {
|
|
30963
|
+
return collapsed({ of: "object", ctor: value.ctor });
|
|
30964
|
+
}
|
|
30965
|
+
const parts = value.entries.map(([key, held]) => `${renderKey(key)}: ${renderValue(held, depth - 1)}`);
|
|
30966
|
+
return `${value.ctor ? `${value.ctor} ` : ""}{${joinParts(parts, value.more)}}`;
|
|
30967
|
+
}
|
|
30968
|
+
case "array": {
|
|
30969
|
+
if (depth <= 0) {
|
|
30970
|
+
return collapsed({ of: "array" });
|
|
30971
|
+
}
|
|
30972
|
+
return `[${joinParts(value.items.map((item) => renderValue(item, depth - 1)), value.more)}]`;
|
|
30973
|
+
}
|
|
30974
|
+
case "map": {
|
|
30975
|
+
if (depth <= 0) {
|
|
30976
|
+
return collapsed({ of: "map", size: value.size });
|
|
30977
|
+
}
|
|
30978
|
+
const parts = value.pairs.map(([key, held]) => `${renderValue(key, depth - 1)} => ${renderValue(held, depth - 1)}`);
|
|
30979
|
+
return `Map(${value.size}) {${joinParts(parts, value.more)}}`;
|
|
30980
|
+
}
|
|
30981
|
+
case "set": {
|
|
30982
|
+
if (depth <= 0) {
|
|
30983
|
+
return collapsed({ of: "set", size: value.size });
|
|
30984
|
+
}
|
|
30985
|
+
return `Set(${value.size}) {${joinParts(value.items.map((item) => renderValue(item, depth - 1)), value.more)}}`;
|
|
30986
|
+
}
|
|
30987
|
+
default: {
|
|
30988
|
+
const unreachable = value;
|
|
30989
|
+
throw new Error(`Unhandled console value ${JSON.stringify(unreachable)}`);
|
|
30990
|
+
}
|
|
30991
|
+
}
|
|
30992
|
+
}
|
|
30993
|
+
function collapsed(value) {
|
|
30994
|
+
if (value.of == "array") {
|
|
30995
|
+
return `[…]`;
|
|
30996
|
+
}
|
|
30997
|
+
if (value.of == "map" || value.of == "set") {
|
|
30998
|
+
const label = value.of == "map" ? "Map" : "Set";
|
|
30999
|
+
return `${label}(${value.size ?? "?"}) {…}`;
|
|
31000
|
+
}
|
|
31001
|
+
return `${value.ctor ? `${value.ctor} ` : ""}{…}`;
|
|
31002
|
+
}
|
|
31003
|
+
function joinParts(parts, more) {
|
|
31004
|
+
const marked = more == null ? parts : [...parts, more === true ? `…more` : `…${more} more`];
|
|
31005
|
+
return marked.join(", ");
|
|
31006
|
+
}
|
|
31007
|
+
function renderKey(key) {
|
|
31008
|
+
return /^[A-Za-z_$][\w$]*$/u.test(key) ? key : `'${key}'`;
|
|
31009
|
+
}
|
|
31010
|
+
function previewValue(preview) {
|
|
31011
|
+
if (!preview) {
|
|
31012
|
+
return;
|
|
31013
|
+
}
|
|
31014
|
+
const scalar = previewScalar(preview);
|
|
31015
|
+
if (scalar) {
|
|
31016
|
+
return scalar;
|
|
31017
|
+
}
|
|
31018
|
+
if (preview.subtype == "array") {
|
|
31019
|
+
const items = (preview.properties ?? []).map((property) => previewProperty(property));
|
|
31020
|
+
const size2 = previewSize(preview.description) ?? items.length;
|
|
31021
|
+
return { kind: "array", items, more: previewOverflow(preview, items.length, size2) };
|
|
31022
|
+
}
|
|
31023
|
+
if (preview.subtype == "set") {
|
|
31024
|
+
const items = (preview.entries ?? []).map((entry) => previewValue(entry.value) ?? unknownValue());
|
|
31025
|
+
const size2 = previewSize(preview.description) ?? items.length;
|
|
31026
|
+
return { kind: "set", size: size2, items, more: previewOverflow(preview, items.length, size2) };
|
|
31027
|
+
}
|
|
31028
|
+
if (preview.subtype == "map") {
|
|
31029
|
+
const pairs = (preview.entries ?? []).map((entry) => {
|
|
31030
|
+
const key = previewValue(entry.key) ?? unknownValue();
|
|
31031
|
+
return [key, previewValue(entry.value) ?? unknownValue()];
|
|
31032
|
+
});
|
|
31033
|
+
const size2 = previewSize(preview.description) ?? pairs.length;
|
|
31034
|
+
return { kind: "map", size: size2, pairs, more: previewOverflow(preview, pairs.length, size2) };
|
|
31035
|
+
}
|
|
31036
|
+
if (isStringified(preview)) {
|
|
31037
|
+
return { kind: "raw", text: oneLineText(preview.description ?? preview.subtype ?? "object") };
|
|
31038
|
+
}
|
|
31039
|
+
const label = preview.description;
|
|
31040
|
+
const entries = (preview.properties ?? []).map((property) => [property.name, previewProperty(property)]);
|
|
31041
|
+
const size = preview.subtype ? previewSize(preview.description) : undefined;
|
|
31042
|
+
return {
|
|
31043
|
+
kind: "object",
|
|
31044
|
+
ctor: label && label != "Object" ? label : undefined,
|
|
31045
|
+
entries,
|
|
31046
|
+
more: previewOverflow(preview, entries.length, size)
|
|
31047
|
+
};
|
|
31048
|
+
}
|
|
31049
|
+
function previewScalar(preview) {
|
|
31050
|
+
if (preview.type && preview.type != "object") {
|
|
31051
|
+
return preview.type == "string" ? clippedString(preview.description ?? "") : { kind: "raw", text: oneLineText(preview.description ?? preview.type) };
|
|
31052
|
+
}
|
|
31053
|
+
if (preview.subtype == "node") {
|
|
31054
|
+
return { kind: "node", text: preview.description ?? "node" };
|
|
31055
|
+
}
|
|
31056
|
+
return;
|
|
31057
|
+
}
|
|
31058
|
+
function isStringified(preview) {
|
|
31059
|
+
return Boolean(preview.subtype) && !preview.properties?.length;
|
|
31060
|
+
}
|
|
31061
|
+
function previewProperty(property) {
|
|
31062
|
+
if (property.valuePreview) {
|
|
31063
|
+
return previewValue(property.valuePreview) ?? unknownValue();
|
|
31064
|
+
}
|
|
31065
|
+
if (property.type == "string") {
|
|
31066
|
+
return clippedString(property.value ?? "");
|
|
31067
|
+
}
|
|
31068
|
+
if (property.type == "object") {
|
|
31069
|
+
return previewObjectProperty(property);
|
|
31070
|
+
}
|
|
31071
|
+
const described = property.value?.length ? property.value : property.type;
|
|
31072
|
+
return { kind: "raw", text: oneLineText(described) };
|
|
31073
|
+
}
|
|
31074
|
+
function previewObjectProperty(property) {
|
|
31075
|
+
const { subtype, value } = property;
|
|
31076
|
+
if (subtype == "array" || subtype == "map" || subtype == "set") {
|
|
31077
|
+
return { kind: "capped", of: subtype, size: previewSize(value) };
|
|
31078
|
+
}
|
|
31079
|
+
if (subtype == "node") {
|
|
31080
|
+
return { kind: "node", text: value ?? "node" };
|
|
31081
|
+
}
|
|
31082
|
+
if (subtype) {
|
|
31083
|
+
return { kind: "raw", text: oneLineText(value ?? subtype) };
|
|
31084
|
+
}
|
|
31085
|
+
const named = value && value != "Object";
|
|
31086
|
+
return { kind: "capped", of: "object", ctor: named ? value : undefined };
|
|
31087
|
+
}
|
|
31088
|
+
function previewOverflow(preview, shown, size = shown) {
|
|
31089
|
+
if (size > shown) {
|
|
31090
|
+
return size - shown;
|
|
31091
|
+
}
|
|
31092
|
+
return preview.overflow ? true : undefined;
|
|
31093
|
+
}
|
|
31094
|
+
function previewSize(description) {
|
|
31095
|
+
const size = /\((?<size>\d+)\)$/u.exec(description ?? "")?.groups?.size;
|
|
31096
|
+
return size == null ? undefined : Number(size);
|
|
31097
|
+
}
|
|
31098
|
+
function clippedString(held) {
|
|
31099
|
+
return held.length > STRING_CLIP ? { kind: "string", text: held.slice(0, STRING_CLIP), clipped: true } : { kind: "string", text: held };
|
|
31100
|
+
}
|
|
31101
|
+
function oneLineText(held) {
|
|
31102
|
+
const flat = held.split(/\r\n|[\r\n]/u).map((line) => line.trim()).find((line) => line.length);
|
|
31103
|
+
return clipText(flat ?? "");
|
|
31104
|
+
}
|
|
31105
|
+
function clipText(held) {
|
|
31106
|
+
return held.length > STRING_CLIP ? `${held.slice(0, STRING_CLIP)}…` : held;
|
|
31107
|
+
}
|
|
31108
|
+
function describedValue(arg) {
|
|
31109
|
+
return { kind: "raw", text: valToStr(describeRemoteObject(arg)) };
|
|
31110
|
+
}
|
|
31111
|
+
function unknownValue() {
|
|
31112
|
+
return { kind: "capped", of: "object" };
|
|
31113
|
+
}
|
|
31114
|
+
|
|
30650
31115
|
// src/debugger.ts
|
|
30651
|
-
var
|
|
31116
|
+
var import_common_tags3 = __toESM(require_lib(), 1);
|
|
31117
|
+
import { setTimeout as sleep } from "node:timers/promises";
|
|
30652
31118
|
var POLL_INTERVAL_MS = 50;
|
|
30653
31119
|
var PAUSE_WAIT_MS = 3000;
|
|
30654
31120
|
var RESUME_WAIT_MS = 2000;
|
|
30655
31121
|
var STEP_WAIT_MS = 2000;
|
|
30656
31122
|
var MAX_SCOPE_VARIABLES = 50;
|
|
31123
|
+
var MAX_SEARCH_MATCHES = 10;
|
|
31124
|
+
var SEARCH_SNIPPET_MARGIN = 40;
|
|
31125
|
+
var DEFAULT_SOURCE_MAX_CHARS = 4000;
|
|
31126
|
+
var EMPTY_SCRIPT_MAP_NOTE = import_common_tags3.oneLine`
|
|
31127
|
+
No scripts are parsed on this connection: a view reload re-parses every one of them.
|
|
31128
|
+
Take the reload count from game_status, game_eval of location.reload(), then game_wait with
|
|
31129
|
+
reload true and that count as sinceReloads, then call this again.
|
|
31130
|
+
`;
|
|
31131
|
+
var NO_MATCH_NOTE = import_common_tags3.oneLine`
|
|
31132
|
+
Nothing matched. The search is case-sensitive and literal (no regex), and it only reaches scripts
|
|
31133
|
+
parsed on this connection: game_debug_scripts lists them.
|
|
31134
|
+
`;
|
|
31135
|
+
var FILTER_MISS_NOTE = import_common_tags3.oneLine`
|
|
31136
|
+
No parsed script's url contains that urlContains, so the query never ran against anything.
|
|
31137
|
+
game_debug_scripts lists the urls; the url filter is case-insensitive, so case is not the cause.
|
|
31138
|
+
`;
|
|
31139
|
+
var FILTER_MISS_LIST_NOTE = import_common_tags3.oneLine`
|
|
31140
|
+
No parsed script's url contains that filter, though others are parsed: call this without one to
|
|
31141
|
+
see every url. Matching is case-insensitive, so case is not the cause.
|
|
31142
|
+
`;
|
|
31143
|
+
var UNREADABLE_NOTE = import_common_tags3.oneLine`
|
|
31144
|
+
The engine has no source for any of the scripts matched, so the query never ran: their ids are
|
|
31145
|
+
stale, which a view reload clears by re-parsing everything under fresh ones.
|
|
31146
|
+
`;
|
|
31147
|
+
var SEARCH_HINT = import_common_tags3.oneLine`
|
|
31148
|
+
Pass a match's line and column to game_debug_set_breakpoint to break there; both are 1-based, as
|
|
31149
|
+
everywhere in these tools, and its urlContains takes the match's url as printed.
|
|
31150
|
+
`;
|
|
31151
|
+
var SINGLE_LINE_HINT = import_common_tags3.oneLine`
|
|
31152
|
+
That script is one line, as a minified bundle is, so the whole module sits on the line the
|
|
31153
|
+
resolved location names and the breakpoint bound to the first breakable position at or after the
|
|
31154
|
+
one you asked for: read the column above to see where it landed.
|
|
31155
|
+
At the module's own first column that is evaluation code, which runs on load and never again
|
|
31156
|
+
during interaction.
|
|
31157
|
+
To break inside a function, find its column with game_debug_search_source and pass that column
|
|
31158
|
+
here.
|
|
31159
|
+
`;
|
|
30657
31160
|
function escapeRegex2(value) {
|
|
30658
31161
|
return value.replaceAll(/[.*+?^${}()|[\]\\]/gu, String.raw`\$&`);
|
|
30659
31162
|
}
|
|
@@ -30666,12 +31169,21 @@ class DebuggerSession {
|
|
|
30666
31169
|
nextBpId = 1;
|
|
30667
31170
|
pausedSeq = 0;
|
|
30668
31171
|
client;
|
|
30669
|
-
constructor(client) {
|
|
31172
|
+
constructor(client, reloads) {
|
|
30670
31173
|
this.client = client;
|
|
30671
31174
|
client.onConnect((conn) => this.onConnect(conn));
|
|
30672
31175
|
client.onEvent((method, params) => {
|
|
30673
31176
|
this.handle(method, params);
|
|
30674
31177
|
});
|
|
31178
|
+
reloads.onReload(() => {
|
|
31179
|
+
this.scripts.clear();
|
|
31180
|
+
});
|
|
31181
|
+
}
|
|
31182
|
+
get pause() {
|
|
31183
|
+
if (!this.paused) {
|
|
31184
|
+
return;
|
|
31185
|
+
}
|
|
31186
|
+
return { reason: this.paused.reason, location: this.topLocation() };
|
|
30675
31187
|
}
|
|
30676
31188
|
async status(setPause) {
|
|
30677
31189
|
try {
|
|
@@ -30684,8 +31196,9 @@ class DebuggerSession {
|
|
|
30684
31196
|
id: bp.id,
|
|
30685
31197
|
urlContains: bp.urlContains,
|
|
30686
31198
|
line: bp.lineNumber + 1,
|
|
31199
|
+
column: bp.columnNumber == null ? undefined : bp.columnNumber + 1,
|
|
30687
31200
|
condition: bp.condition,
|
|
30688
|
-
|
|
31201
|
+
resolvedLocations: bp.resolved
|
|
30689
31202
|
}));
|
|
30690
31203
|
return text(JSON.stringify({
|
|
30691
31204
|
enabled: true,
|
|
@@ -30706,49 +31219,87 @@ class DebuggerSession {
|
|
|
30706
31219
|
async listScripts(filter) {
|
|
30707
31220
|
try {
|
|
30708
31221
|
await this.ensureReady();
|
|
30709
|
-
const
|
|
30710
|
-
let scripts = [...this.scripts.values()];
|
|
30711
|
-
if (needle) {
|
|
30712
|
-
scripts = scripts.filter((script) => script.url.toLowerCase().includes(needle));
|
|
30713
|
-
}
|
|
30714
|
-
scripts.sort((a, b) => a.url.localeCompare(b.url));
|
|
31222
|
+
const scripts = this.filteredScripts(filter);
|
|
30715
31223
|
const cap = 120;
|
|
30716
31224
|
const shown = scripts.slice(0, cap).map((script) => ({
|
|
30717
31225
|
scriptId: script.scriptId,
|
|
30718
31226
|
url: script.url || "(anonymous)",
|
|
30719
|
-
lines: script.endLine
|
|
31227
|
+
lines: script.endLine - script.startLine + 1,
|
|
31228
|
+
firstLine: script.startLine == 0 ? undefined : script.startLine + 1
|
|
30720
31229
|
}));
|
|
30721
31230
|
return text(JSON.stringify({
|
|
30722
31231
|
total: scripts.length,
|
|
30723
31232
|
shown: shown.length,
|
|
30724
31233
|
truncated: scripts.length > cap,
|
|
30725
|
-
scripts: shown
|
|
31234
|
+
scripts: shown,
|
|
31235
|
+
note: this.listNote(scripts.length)
|
|
30726
31236
|
}, null, 2));
|
|
30727
31237
|
} catch (error51) {
|
|
30728
31238
|
return toErrorResult(error51);
|
|
30729
31239
|
}
|
|
30730
31240
|
}
|
|
30731
|
-
async
|
|
31241
|
+
async searchSource(query, urlContains) {
|
|
30732
31242
|
try {
|
|
30733
31243
|
await this.ensureReady();
|
|
30734
|
-
const
|
|
30735
|
-
|
|
30736
|
-
|
|
30737
|
-
|
|
31244
|
+
const scripts = this.filteredScripts(urlContains);
|
|
31245
|
+
const { matches, total, unreadable } = await this.collectMatches(scripts, query);
|
|
31246
|
+
const missNote = this.missNote(scripts.length, unreadable);
|
|
31247
|
+
return text(JSON.stringify({
|
|
31248
|
+
query,
|
|
31249
|
+
urlContains,
|
|
31250
|
+
scriptsSearched: urlContains == null ? undefined : scripts.length,
|
|
31251
|
+
total,
|
|
31252
|
+
returned: matches.length,
|
|
31253
|
+
truncated: total > matches.length,
|
|
31254
|
+
matches,
|
|
31255
|
+
note: total == 0 ? missNote : undefined,
|
|
31256
|
+
hint: total == 0 ? undefined : SEARCH_HINT
|
|
31257
|
+
}, null, 2));
|
|
31258
|
+
} catch (error51) {
|
|
31259
|
+
return toErrorResult(error51);
|
|
31260
|
+
}
|
|
31261
|
+
}
|
|
31262
|
+
async getSource(scriptId, lineStart, lineEnd, maxChars) {
|
|
31263
|
+
try {
|
|
31264
|
+
await this.ensureReady();
|
|
31265
|
+
const source = await this.scriptSource(scriptId);
|
|
30738
31266
|
if (source == null) {
|
|
30739
31267
|
return errorText(`No source for scriptId ${scriptId}.`);
|
|
30740
31268
|
}
|
|
30741
31269
|
const lines = source.split(`
|
|
30742
31270
|
`);
|
|
30743
|
-
const
|
|
31271
|
+
const offset = this.scripts.get(scriptId)?.startLine ?? 0;
|
|
31272
|
+
const first = offset + 1;
|
|
31273
|
+
const last = offset + lines.length;
|
|
30744
31274
|
const cap = 400;
|
|
30745
|
-
const
|
|
31275
|
+
const from = lineStart && lineStart > first ? lineStart : first;
|
|
31276
|
+
if (from > last) {
|
|
31277
|
+
return errorText(import_common_tags3.oneLine`
|
|
31278
|
+
Script ${scriptId} spans lines ${first}-${last}, so there is nothing at line ${from}.
|
|
31279
|
+
`);
|
|
31280
|
+
}
|
|
31281
|
+
const to = Math.min(lineEnd && lineEnd >= from ? lineEnd : last, last, from + cap - 1);
|
|
30746
31282
|
const padWidth = 5;
|
|
30747
|
-
const
|
|
31283
|
+
const rendered = lines.slice(from - first, to - offset).map((line, index) => `${String(from + index).padStart(padWidth)} ${line}`).join(`
|
|
30748
31284
|
`);
|
|
30749
|
-
const
|
|
30750
|
-
|
|
30751
|
-
|
|
31285
|
+
const budget = maxChars ?? DEFAULT_SOURCE_MAX_CHARS;
|
|
31286
|
+
const clipped = rendered.length > budget;
|
|
31287
|
+
const body = clipped ? rendered.slice(0, budget) : rendered;
|
|
31288
|
+
const shownTo = from + body.replace(/\n$/u, "").split(`
|
|
31289
|
+
`).length - 1;
|
|
31290
|
+
const notes = [];
|
|
31291
|
+
if (shownTo < last || from > first) {
|
|
31292
|
+
notes.push(`showing lines ${from}-${shownTo} of ${last}`);
|
|
31293
|
+
}
|
|
31294
|
+
if (clipped) {
|
|
31295
|
+
notes.push(import_common_tags3.oneLine`
|
|
31296
|
+
clipped to ${budget} of ${rendered.length} characters: raise maxChars for more, or reach
|
|
31297
|
+
a position inside a minified line with game_debug_search_source
|
|
31298
|
+
`);
|
|
31299
|
+
}
|
|
31300
|
+
const note = notes.length > 0 ? `
|
|
31301
|
+
... ${notes.join(". ")}.` : "";
|
|
31302
|
+
return text(body + note);
|
|
30752
31303
|
} catch (error51) {
|
|
30753
31304
|
return toErrorResult(error51);
|
|
30754
31305
|
}
|
|
@@ -30757,34 +31308,42 @@ class DebuggerSession {
|
|
|
30757
31308
|
try {
|
|
30758
31309
|
await this.ensureReady();
|
|
30759
31310
|
const lineNumber = Math.max(0, line - 1);
|
|
31311
|
+
const columnNumber = column == null ? undefined : Math.max(0, column - 1);
|
|
30760
31312
|
const urlRegex = escapeRegex2(urlContains);
|
|
30761
|
-
const res = await this.client.call("Debugger.setBreakpointByUrl", { urlRegex, lineNumber, columnNumber
|
|
31313
|
+
const res = await this.client.call("Debugger.setBreakpointByUrl", { urlRegex, lineNumber, columnNumber, condition });
|
|
30762
31314
|
const id = this.nextBpId++;
|
|
30763
31315
|
const locations = res.locations ?? [];
|
|
31316
|
+
const resolved = locations.map((loc) => this.locStr(loc));
|
|
30764
31317
|
this.breakpoints.set(id, {
|
|
30765
31318
|
id,
|
|
30766
31319
|
urlContains,
|
|
30767
31320
|
urlRegex,
|
|
30768
31321
|
lineNumber,
|
|
30769
|
-
columnNumber
|
|
31322
|
+
columnNumber,
|
|
30770
31323
|
condition,
|
|
30771
31324
|
cdpId: res.breakpointId,
|
|
30772
|
-
|
|
31325
|
+
resolved
|
|
31326
|
+
});
|
|
31327
|
+
const singleLine = locations.some((loc) => {
|
|
31328
|
+
const script = this.scripts.get(loc.scriptId);
|
|
31329
|
+
return script != null && script.endLine == script.startLine;
|
|
30773
31330
|
});
|
|
30774
31331
|
return text(JSON.stringify({
|
|
30775
31332
|
id,
|
|
30776
31333
|
urlContains,
|
|
30777
31334
|
line,
|
|
30778
|
-
|
|
30779
|
-
|
|
31335
|
+
column,
|
|
31336
|
+
condition,
|
|
31337
|
+
resolvedLocations: resolved,
|
|
30780
31338
|
pending: locations.length == 0,
|
|
30781
|
-
note: locations.length == 0 ?
|
|
31339
|
+
note: locations.length == 0 ? import_common_tags3.oneLine`
|
|
30782
31340
|
Pending: no matching script/line loaded yet, or the line has no code.
|
|
30783
31341
|
It will bind when the script loads.
|
|
30784
|
-
` :
|
|
31342
|
+
` : import_common_tags3.oneLine`
|
|
30785
31343
|
Hitting this breakpoint FREEZES the UI until you resume
|
|
30786
31344
|
(game_debug_step resume).
|
|
30787
|
-
|
|
31345
|
+
`,
|
|
31346
|
+
hint: singleLine ? SINGLE_LINE_HINT : undefined
|
|
30788
31347
|
}, null, 2));
|
|
30789
31348
|
} catch (error51) {
|
|
30790
31349
|
return toErrorResult(error51);
|
|
@@ -30817,7 +31376,7 @@ class DebuggerSession {
|
|
|
30817
31376
|
try {
|
|
30818
31377
|
await this.ensureReady();
|
|
30819
31378
|
if (!this.paused) {
|
|
30820
|
-
return text(
|
|
31379
|
+
return text(`Not paused (the UI is running). Set a breakpoint or use game_debug_step pause.`);
|
|
30821
31380
|
}
|
|
30822
31381
|
const frames = [];
|
|
30823
31382
|
for (const [index, frame] of this.paused.callFrames.entries()) {
|
|
@@ -30843,7 +31402,7 @@ class DebuggerSession {
|
|
|
30843
31402
|
if (this.paused) {
|
|
30844
31403
|
const frame = this.paused.callFrames[frameIndex];
|
|
30845
31404
|
if (!frame) {
|
|
30846
|
-
return errorText(
|
|
31405
|
+
return errorText(import_common_tags3.oneLine`
|
|
30847
31406
|
No call frame at index ${frameIndex}
|
|
30848
31407
|
(paused stack has ${this.paused.callFrames.length}).
|
|
30849
31408
|
`);
|
|
@@ -30886,15 +31445,21 @@ class DebuggerSession {
|
|
|
30886
31445
|
return text(paused ? `Paused at ${this.topLocation()}.` : `Pause requested; nothing executed within ${PAUSE_WAIT_MS}ms (UI idle).`);
|
|
30887
31446
|
}
|
|
30888
31447
|
if (!this.paused) {
|
|
30889
|
-
return errorText(
|
|
31448
|
+
return errorText(`Not paused. Set a breakpoint and trigger it, or use action 'pause' first.`);
|
|
30890
31449
|
}
|
|
30891
31450
|
if (action == "resume") {
|
|
30892
31451
|
await this.client.call("Debugger.resume");
|
|
30893
|
-
|
|
30894
|
-
|
|
30895
|
-
|
|
31452
|
+
await this.waitUntil(() => !this.paused, RESUME_WAIT_MS);
|
|
31453
|
+
if (this.paused) {
|
|
31454
|
+
return errorText(import_common_tags3.oneLine`
|
|
31455
|
+
Resume sent, but the UI is paused again at ${this.topLocation()}
|
|
31456
|
+
(reason: ${this.paused.reason}), so it is still frozen.
|
|
31457
|
+
Whatever armed that pause sits on the resumed path, so clear it before resuming again:
|
|
31458
|
+
game_debug_remove_breakpoint for a breakpoint, or game_debug_status
|
|
31459
|
+
setPauseOnExceptions none when the reason above is an exception.
|
|
31460
|
+
`);
|
|
30896
31461
|
}
|
|
30897
|
-
return text(
|
|
31462
|
+
return text(`Resumed (UI unfrozen).`);
|
|
30898
31463
|
}
|
|
30899
31464
|
const cdpMethod = { over: "stepOver", into: "stepInto", out: "stepOut" }[action];
|
|
30900
31465
|
const before = this.pausedSeq;
|
|
@@ -30919,7 +31484,7 @@ class DebuggerSession {
|
|
|
30919
31484
|
condition: bp.condition
|
|
30920
31485
|
});
|
|
30921
31486
|
bp.cdpId = res.breakpointId;
|
|
30922
|
-
bp.
|
|
31487
|
+
bp.resolved = (res.locations ?? []).map((loc) => this.locStr(loc));
|
|
30923
31488
|
} catch {}
|
|
30924
31489
|
}
|
|
30925
31490
|
}
|
|
@@ -30929,6 +31494,7 @@ class DebuggerSession {
|
|
|
30929
31494
|
scriptId: params.scriptId,
|
|
30930
31495
|
url: params.url || "",
|
|
30931
31496
|
startLine: params.startLine ?? 0,
|
|
31497
|
+
startColumn: params.startColumn ?? 0,
|
|
30932
31498
|
endLine: params.endLine ?? 0,
|
|
30933
31499
|
length: params.length
|
|
30934
31500
|
});
|
|
@@ -30941,13 +31507,79 @@ class DebuggerSession {
|
|
|
30941
31507
|
this.pausedSeq++;
|
|
30942
31508
|
} else if (method == "Debugger.resumed") {
|
|
30943
31509
|
this.paused = undefined;
|
|
31510
|
+
} else if (method == "Debugger.breakpointResolved") {
|
|
31511
|
+
this.recordResolved(params);
|
|
31512
|
+
}
|
|
31513
|
+
}
|
|
31514
|
+
recordResolved(params) {
|
|
31515
|
+
const location = params.location;
|
|
31516
|
+
const bp = [...this.breakpoints.values()].find((entry) => entry.cdpId == params.breakpointId);
|
|
31517
|
+
if (location == null || bp == null) {
|
|
31518
|
+
return;
|
|
31519
|
+
}
|
|
31520
|
+
const at = this.locStr(location);
|
|
31521
|
+
if (!bp.resolved.includes(at)) {
|
|
31522
|
+
bp.resolved.push(at);
|
|
30944
31523
|
}
|
|
30945
31524
|
}
|
|
30946
31525
|
async ensureReady() {
|
|
30947
31526
|
await this.client.connection();
|
|
31527
|
+
}
|
|
31528
|
+
filteredScripts(contains) {
|
|
31529
|
+
return [...this.scripts.values()].filter((script) => matchesUrl(script.url, contains)).toSorted((a, b) => a.url.localeCompare(b.url));
|
|
31530
|
+
}
|
|
31531
|
+
listNote(selected) {
|
|
31532
|
+
if (this.scripts.size == 0) {
|
|
31533
|
+
return EMPTY_SCRIPT_MAP_NOTE;
|
|
31534
|
+
}
|
|
31535
|
+
return selected == 0 ? FILTER_MISS_LIST_NOTE : undefined;
|
|
31536
|
+
}
|
|
31537
|
+
missNote(scriptsSearched, unreadable) {
|
|
30948
31538
|
if (this.scripts.size == 0) {
|
|
30949
|
-
|
|
31539
|
+
return EMPTY_SCRIPT_MAP_NOTE;
|
|
31540
|
+
}
|
|
31541
|
+
if (scriptsSearched == 0) {
|
|
31542
|
+
return FILTER_MISS_NOTE;
|
|
31543
|
+
}
|
|
31544
|
+
return unreadable == scriptsSearched ? UNREADABLE_NOTE : NO_MATCH_NOTE;
|
|
31545
|
+
}
|
|
31546
|
+
async collectMatches(scripts, query) {
|
|
31547
|
+
const matches = [];
|
|
31548
|
+
let total = 0;
|
|
31549
|
+
let unreadable = 0;
|
|
31550
|
+
for (const script of scripts) {
|
|
31551
|
+
let source;
|
|
31552
|
+
try {
|
|
31553
|
+
source = await this.scriptSource(script.scriptId);
|
|
31554
|
+
} catch {
|
|
31555
|
+
unreadable++;
|
|
31556
|
+
continue;
|
|
31557
|
+
}
|
|
31558
|
+
if (source == null) {
|
|
31559
|
+
unreadable++;
|
|
31560
|
+
continue;
|
|
31561
|
+
}
|
|
31562
|
+
let at = source.indexOf(query);
|
|
31563
|
+
while (at >= 0) {
|
|
31564
|
+
total++;
|
|
31565
|
+
if (matches.length < MAX_SEARCH_MATCHES) {
|
|
31566
|
+
matches.push({
|
|
31567
|
+
url: script.url || "(anonymous)",
|
|
31568
|
+
scriptId: script.scriptId,
|
|
31569
|
+
...resourcePosition(script, positionAt(source, at)),
|
|
31570
|
+
snippet: snippetAt(source, at, query.length)
|
|
31571
|
+
});
|
|
31572
|
+
}
|
|
31573
|
+
at = source.indexOf(query, at + query.length);
|
|
31574
|
+
}
|
|
30950
31575
|
}
|
|
31576
|
+
return { matches, total, unreadable };
|
|
31577
|
+
}
|
|
31578
|
+
async scriptSource(scriptId) {
|
|
31579
|
+
const res = await this.client.call("Debugger.getScriptSource", {
|
|
31580
|
+
scriptId
|
|
31581
|
+
});
|
|
31582
|
+
return res?.scriptSource;
|
|
30951
31583
|
}
|
|
30952
31584
|
async removeCdpBreakpoint(bp) {
|
|
30953
31585
|
if (bp.cdpId == null) {
|
|
@@ -30975,10 +31607,13 @@ class DebuggerSession {
|
|
|
30975
31607
|
}
|
|
30976
31608
|
return variables;
|
|
30977
31609
|
}
|
|
30978
|
-
|
|
31610
|
+
waitForNewPause(beforeSeq, timeoutMs) {
|
|
31611
|
+
return this.waitUntil(() => this.pausedSeq > beforeSeq, timeoutMs);
|
|
31612
|
+
}
|
|
31613
|
+
async waitUntil(done, timeoutMs) {
|
|
30979
31614
|
const deadline = Date.now() + timeoutMs;
|
|
30980
31615
|
while (Date.now() < deadline) {
|
|
30981
|
-
if (
|
|
31616
|
+
if (done()) {
|
|
30982
31617
|
return true;
|
|
30983
31618
|
}
|
|
30984
31619
|
await sleep(POLL_INTERVAL_MS);
|
|
@@ -30987,8 +31622,8 @@ class DebuggerSession {
|
|
|
30987
31622
|
}
|
|
30988
31623
|
locStr(loc) {
|
|
30989
31624
|
const url2 = this.scripts.get(loc.scriptId)?.url ?? "";
|
|
30990
|
-
const label = url2.length > 0 ? url2 : loc.scriptId
|
|
30991
|
-
return `${label}:${loc.lineNumber + 1}`;
|
|
31625
|
+
const label = url2.length > 0 ? url2 : `(unknown script ${loc.scriptId})`;
|
|
31626
|
+
return `${label}:${loc.lineNumber + 1}:${(loc.columnNumber ?? 0) + 1}`;
|
|
30992
31627
|
}
|
|
30993
31628
|
topLocation() {
|
|
30994
31629
|
const frame = this.paused?.callFrames[0];
|
|
@@ -30998,208 +31633,48 @@ class DebuggerSession {
|
|
|
30998
31633
|
return `${frame.functionName || "(anonymous)"} at ${this.locStr(frame.location)}`;
|
|
30999
31634
|
}
|
|
31000
31635
|
}
|
|
31636
|
+
function matchesUrl(url2, contains) {
|
|
31637
|
+
if (contains == null) {
|
|
31638
|
+
return true;
|
|
31639
|
+
}
|
|
31640
|
+
return url2.toLowerCase().includes(contains.toLowerCase());
|
|
31641
|
+
}
|
|
31642
|
+
function resourcePosition(script, position) {
|
|
31643
|
+
return {
|
|
31644
|
+
line: position.line + script.startLine,
|
|
31645
|
+
column: position.line == 1 ? position.column + script.startColumn : position.column
|
|
31646
|
+
};
|
|
31647
|
+
}
|
|
31648
|
+
function positionAt(source, index) {
|
|
31649
|
+
let line = 1;
|
|
31650
|
+
let lineStart = 0;
|
|
31651
|
+
for (let at = source.indexOf(`
|
|
31652
|
+
`);at >= 0 && at < index; at = source.indexOf(`
|
|
31653
|
+
`, at + 1)) {
|
|
31654
|
+
line++;
|
|
31655
|
+
lineStart = at + 1;
|
|
31656
|
+
}
|
|
31657
|
+
return { line, column: index - lineStart + 1 };
|
|
31658
|
+
}
|
|
31659
|
+
function snippetAt(source, index, length) {
|
|
31660
|
+
const from = Math.max(0, index - SEARCH_SNIPPET_MARGIN);
|
|
31661
|
+
const to = Math.min(source.length, index + length + SEARCH_SNIPPET_MARGIN);
|
|
31662
|
+
const head = from > 0 ? "…" : "";
|
|
31663
|
+
const tail = to < source.length ? "…" : "";
|
|
31664
|
+
return `${head}${source.slice(from, to)}${tail}`;
|
|
31665
|
+
}
|
|
31001
31666
|
|
|
31002
31667
|
// src/tools.ts
|
|
31003
|
-
var
|
|
31668
|
+
var import_common_tags4 = __toESM(require_lib(), 1);
|
|
31004
31669
|
import { setTimeout as sleep2 } from "node:timers/promises";
|
|
31005
|
-
var collectDomFn = (sel, all, maxHtml) => {
|
|
31006
|
-
const matches = document.querySelectorAll(sel);
|
|
31007
|
-
const first = matches.item(0);
|
|
31008
|
-
const chosen = all ? Array.from(matches) : first ? [first] : [];
|
|
31009
|
-
const describe3 = (el) => {
|
|
31010
|
-
const rect = el.getBoundingClientRect();
|
|
31011
|
-
const attributes = {};
|
|
31012
|
-
for (const attr of Array.from(el.attributes)) {
|
|
31013
|
-
attributes[attr.name] = attr.value;
|
|
31014
|
-
}
|
|
31015
|
-
const classAttr = el.getAttribute("class");
|
|
31016
|
-
let html = el.outerHTML || "";
|
|
31017
|
-
const truncated = html.length > maxHtml;
|
|
31018
|
-
if (truncated) {
|
|
31019
|
-
html = html.slice(0, maxHtml);
|
|
31020
|
-
}
|
|
31021
|
-
return {
|
|
31022
|
-
tagName: el.tagName ? el.tagName.toLowerCase() : null,
|
|
31023
|
-
id: el.id || null,
|
|
31024
|
-
classes: classAttr != null && classAttr.length > 0 ? classAttr : null,
|
|
31025
|
-
rect: { x: rect.x, y: rect.y, width: rect.width, height: rect.height },
|
|
31026
|
-
attributes,
|
|
31027
|
-
outerHTML: html,
|
|
31028
|
-
truncated
|
|
31029
|
-
};
|
|
31030
|
-
};
|
|
31031
|
-
return { count: matches.length, elements: chosen.map((el) => describe3(el)) };
|
|
31032
|
-
};
|
|
31033
|
-
var clickFn = (sel, index) => {
|
|
31034
|
-
const nodes = document.querySelectorAll(sel);
|
|
31035
|
-
if (nodes.length == 0) {
|
|
31036
|
-
return { found: false, count: 0, fired: [] };
|
|
31037
|
-
}
|
|
31038
|
-
const el = nodes[index];
|
|
31039
|
-
if (!el) {
|
|
31040
|
-
return { found: false, count: nodes.length, fired: [] };
|
|
31041
|
-
}
|
|
31042
|
-
const rect = el.getBoundingClientRect();
|
|
31043
|
-
const cx = rect.x + rect.width / 2;
|
|
31044
|
-
const cy = rect.y + rect.height / 2;
|
|
31045
|
-
const base = {
|
|
31046
|
-
bubbles: true,
|
|
31047
|
-
cancelable: true,
|
|
31048
|
-
view: window,
|
|
31049
|
-
button: 0,
|
|
31050
|
-
clientX: cx,
|
|
31051
|
-
clientY: cy
|
|
31052
|
-
};
|
|
31053
|
-
const Pointer = typeof PointerEvent == "function" ? PointerEvent : MouseEvent;
|
|
31054
|
-
const fired = [];
|
|
31055
|
-
const fire = (Ctor, type, extra) => {
|
|
31056
|
-
try {
|
|
31057
|
-
el.dispatchEvent(new Ctor(type, { ...base, ...extra }));
|
|
31058
|
-
fired.push(type);
|
|
31059
|
-
} catch {}
|
|
31060
|
-
};
|
|
31061
|
-
fire(Pointer, "pointerdown", { pointerId: 1, isPrimary: true });
|
|
31062
|
-
fire(MouseEvent, "mousedown");
|
|
31063
|
-
fire(Pointer, "pointerup", { pointerId: 1, isPrimary: true });
|
|
31064
|
-
fire(MouseEvent, "mouseup");
|
|
31065
|
-
fire(MouseEvent, "click");
|
|
31066
|
-
return { found: true, count: nodes.length, x: cx, y: cy, fired };
|
|
31067
|
-
};
|
|
31068
|
-
var waitCheckFn = (sel, visible) => {
|
|
31069
|
-
const el = document.querySelector(sel);
|
|
31070
|
-
if (!el) {
|
|
31071
|
-
return false;
|
|
31072
|
-
}
|
|
31073
|
-
if (!visible) {
|
|
31074
|
-
return true;
|
|
31075
|
-
}
|
|
31076
|
-
const rect = el.getBoundingClientRect();
|
|
31077
|
-
return rect.width > 0 && rect.height > 0;
|
|
31078
|
-
};
|
|
31079
|
-
var fillFn = (sel, value) => {
|
|
31080
|
-
const el = document.querySelector(sel);
|
|
31081
|
-
if (!el) {
|
|
31082
|
-
return { found: false };
|
|
31083
|
-
}
|
|
31084
|
-
try {
|
|
31085
|
-
el.focus();
|
|
31086
|
-
} catch {}
|
|
31087
|
-
const tag = (el.tagName || "").toLowerCase();
|
|
31088
|
-
if (el.isContentEditable) {
|
|
31089
|
-
el.textContent = value;
|
|
31090
|
-
el.dispatchEvent(new Event("input", { bubbles: true }));
|
|
31091
|
-
el.dispatchEvent(new Event("change", { bubbles: true }));
|
|
31092
|
-
return { found: true, mode: "contenteditable", value: el.textContent ?? "" };
|
|
31093
|
-
}
|
|
31094
|
-
const field = el;
|
|
31095
|
-
const proto = tag == "textarea" ? HTMLTextAreaElement.prototype : HTMLInputElement.prototype;
|
|
31096
|
-
const setter = Object.getOwnPropertyDescriptor(proto, "value")?.set;
|
|
31097
|
-
if (setter) {
|
|
31098
|
-
setter.call(el, value);
|
|
31099
|
-
} else {
|
|
31100
|
-
field.value = value;
|
|
31101
|
-
}
|
|
31102
|
-
el.dispatchEvent(new Event("input", { bubbles: true }));
|
|
31103
|
-
el.dispatchEvent(new Event("change", { bubbles: true }));
|
|
31104
|
-
return { found: true, mode: tag || "input", value: field.value };
|
|
31105
|
-
};
|
|
31106
|
-
var typeFn = (sel, textToType) => {
|
|
31107
|
-
const el = document.querySelector(sel);
|
|
31108
|
-
if (!el) {
|
|
31109
|
-
return { found: false, typed: 0 };
|
|
31110
|
-
}
|
|
31111
|
-
try {
|
|
31112
|
-
el.focus();
|
|
31113
|
-
} catch {}
|
|
31114
|
-
const tag = (el.tagName || "").toLowerCase();
|
|
31115
|
-
const editable = el.isContentEditable;
|
|
31116
|
-
const field = el;
|
|
31117
|
-
const proto = tag == "textarea" ? HTMLTextAreaElement.prototype : HTMLInputElement.prototype;
|
|
31118
|
-
const setter = Object.getOwnPropertyDescriptor(proto, "value")?.set;
|
|
31119
|
-
const current = () => {
|
|
31120
|
-
if (editable) {
|
|
31121
|
-
return el.textContent ?? "";
|
|
31122
|
-
}
|
|
31123
|
-
return field.value || "";
|
|
31124
|
-
};
|
|
31125
|
-
const setValue = (next) => {
|
|
31126
|
-
if (editable) {
|
|
31127
|
-
el.textContent = next;
|
|
31128
|
-
} else if (setter) {
|
|
31129
|
-
setter.call(el, next);
|
|
31130
|
-
} else {
|
|
31131
|
-
field.value = next;
|
|
31132
|
-
}
|
|
31133
|
-
};
|
|
31134
|
-
let typed = 0;
|
|
31135
|
-
for (const ch of textToType) {
|
|
31136
|
-
const opts = {
|
|
31137
|
-
bubbles: true,
|
|
31138
|
-
cancelable: true,
|
|
31139
|
-
key: ch,
|
|
31140
|
-
view: window
|
|
31141
|
-
};
|
|
31142
|
-
try {
|
|
31143
|
-
el.dispatchEvent(new KeyboardEvent("keydown", opts));
|
|
31144
|
-
} catch {}
|
|
31145
|
-
setValue(current() + ch);
|
|
31146
|
-
el.dispatchEvent(new Event("input", { bubbles: true }));
|
|
31147
|
-
try {
|
|
31148
|
-
el.dispatchEvent(new KeyboardEvent("keyup", opts));
|
|
31149
|
-
} catch {}
|
|
31150
|
-
typed++;
|
|
31151
|
-
}
|
|
31152
|
-
el.dispatchEvent(new Event("change", { bubbles: true }));
|
|
31153
|
-
return { found: true, typed, value: current() };
|
|
31154
|
-
};
|
|
31155
|
-
var hoverFn = (sel) => {
|
|
31156
|
-
const el = document.querySelector(sel);
|
|
31157
|
-
if (!el) {
|
|
31158
|
-
return { found: false };
|
|
31159
|
-
}
|
|
31160
|
-
const rect = el.getBoundingClientRect();
|
|
31161
|
-
const cx = rect.x + rect.width / 2;
|
|
31162
|
-
const cy = rect.y + rect.height / 2;
|
|
31163
|
-
const base = {
|
|
31164
|
-
bubbles: true,
|
|
31165
|
-
cancelable: true,
|
|
31166
|
-
view: window,
|
|
31167
|
-
clientX: cx,
|
|
31168
|
-
clientY: cy
|
|
31169
|
-
};
|
|
31170
|
-
const noBubble = { ...base, bubbles: false };
|
|
31171
|
-
const fired = [];
|
|
31172
|
-
const fire = (type, init) => {
|
|
31173
|
-
try {
|
|
31174
|
-
el.dispatchEvent(new MouseEvent(type, init));
|
|
31175
|
-
fired.push(type);
|
|
31176
|
-
} catch {}
|
|
31177
|
-
};
|
|
31178
|
-
fire("pointerover", base);
|
|
31179
|
-
fire("mouseover", base);
|
|
31180
|
-
fire("pointerenter", noBubble);
|
|
31181
|
-
fire("mouseenter", noBubble);
|
|
31182
|
-
fire("mousemove", base);
|
|
31183
|
-
return { found: true, x: cx, y: cy, fired };
|
|
31184
|
-
};
|
|
31185
|
-
var rectFn = (sel) => {
|
|
31186
|
-
const el = document.querySelector(sel);
|
|
31187
|
-
if (!el) {
|
|
31188
|
-
return { found: false };
|
|
31189
|
-
}
|
|
31190
|
-
const rect = el.getBoundingClientRect();
|
|
31191
|
-
return { found: true, x: rect.x, y: rect.y, width: rect.width, height: rect.height };
|
|
31192
|
-
};
|
|
31193
|
-
function callPageFn(fn, ...args) {
|
|
31194
|
-
const serialisedArgs = args.map((arg) => JSON.stringify(arg)).join(", ");
|
|
31195
|
-
return `(${fn.toString()})(${serialisedArgs})`;
|
|
31196
|
-
}
|
|
31197
31670
|
var POLL_INTERVAL_MS2 = 150;
|
|
31198
31671
|
var MAX_WAIT_MS = 60000;
|
|
31199
31672
|
var DEFAULT_WAIT_TIMEOUT_MS = 8000;
|
|
31673
|
+
var DEFAULT_RELOAD_WAIT_TIMEOUT_MS = 30000;
|
|
31674
|
+
var DEFAULT_QUIESCENT_MS = 1000;
|
|
31200
31675
|
var DEFAULT_JPEG_QUALITY = 80;
|
|
31201
|
-
var
|
|
31202
|
-
async function gameStatus(client) {
|
|
31676
|
+
var DEFAULT_QUERY_LIMIT = 20;
|
|
31677
|
+
async function gameStatus(client, reloads) {
|
|
31203
31678
|
const { host, port } = client.config;
|
|
31204
31679
|
try {
|
|
31205
31680
|
const target = await client.discover();
|
|
@@ -31213,19 +31688,32 @@ async function gameStatus(client) {
|
|
|
31213
31688
|
protocol = versionInfo["Protocol-Version"];
|
|
31214
31689
|
}
|
|
31215
31690
|
} catch {}
|
|
31691
|
+
let tracking = false;
|
|
31692
|
+
try {
|
|
31693
|
+
await client.connection();
|
|
31694
|
+
tracking = true;
|
|
31695
|
+
} catch {}
|
|
31696
|
+
const { lastReloadAt } = reloads;
|
|
31216
31697
|
return text(JSON.stringify({
|
|
31217
31698
|
reachable: true,
|
|
31218
31699
|
endpoint: `http://${host}:${port}`,
|
|
31219
31700
|
target: { id: target.id, url: target.url, title: target.title, wsUrl: target.wsUrl },
|
|
31220
31701
|
browser,
|
|
31221
|
-
cdpProtocol: protocol
|
|
31702
|
+
cdpProtocol: protocol,
|
|
31703
|
+
reloads: tracking ? {
|
|
31704
|
+
tracking: true,
|
|
31705
|
+
count: reloads.count,
|
|
31706
|
+
lastReloadAt: lastReloadAt == null ? null : new Date(lastReloadAt).toISOString(),
|
|
31707
|
+
lastReloadAgoMs: lastReloadAt == null ? null : Date.now() - lastReloadAt,
|
|
31708
|
+
contextUniqueId: reloads.contextUniqueId ?? null
|
|
31709
|
+
} : { tracking: false }
|
|
31222
31710
|
}, null, 2));
|
|
31223
31711
|
} catch (error51) {
|
|
31224
31712
|
return text(JSON.stringify({
|
|
31225
31713
|
reachable: false,
|
|
31226
31714
|
endpoint: `http://${host}:${port}`,
|
|
31227
31715
|
error: error51 instanceof Error ? error51.message : String(error51),
|
|
31228
|
-
hint:
|
|
31716
|
+
hint: import_common_tags4.oneLine`
|
|
31229
31717
|
Launch the Gameface application with its CDP debug port open, then retry.
|
|
31230
31718
|
Override host/port via GAMEFACE_HOST / GAMEFACE_PORT.
|
|
31231
31719
|
`
|
|
@@ -31248,36 +31736,58 @@ async function gameEval(client, expression, awaitPromise = false) {
|
|
|
31248
31736
|
return toErrorResult(error51);
|
|
31249
31737
|
}
|
|
31250
31738
|
}
|
|
31251
|
-
async function gameScreenshot(client,
|
|
31739
|
+
async function gameScreenshot(client, debug, options = {}) {
|
|
31740
|
+
const { format = "png", quality, selector, index = 0 } = options;
|
|
31252
31741
|
try {
|
|
31742
|
+
await client.connection();
|
|
31743
|
+
const { pause } = debug;
|
|
31744
|
+
if (pause) {
|
|
31745
|
+
return errorText(import_common_tags4.oneLine`
|
|
31746
|
+
game_screenshot cannot capture while the JS debugger holds the UI paused
|
|
31747
|
+
(${pause.reason}, at ${pause.location}): the capture needs the frame loop the pause has
|
|
31748
|
+
frozen, so it would hang until the call times out.
|
|
31749
|
+
Resume with game_debug_step action=resume, then capture.
|
|
31750
|
+
Resuming abandons this stopped frame, so read what you still need from it first, with
|
|
31751
|
+
game_debug_pause_state and game_debug_evaluate; both work while paused, as do game_eval,
|
|
31752
|
+
game_dom, game_query and the input tools.
|
|
31753
|
+
`);
|
|
31754
|
+
}
|
|
31253
31755
|
await client.ensureDomain("Page");
|
|
31254
31756
|
const params = { format };
|
|
31255
31757
|
if (format == "jpeg") {
|
|
31256
31758
|
params.quality = quality ?? DEFAULT_JPEG_QUALITY;
|
|
31257
31759
|
}
|
|
31760
|
+
let caption;
|
|
31258
31761
|
if (selector) {
|
|
31259
31762
|
const rectRes = await client.call("Runtime.evaluate", {
|
|
31260
|
-
expression: callPageFn(rectFn, selector),
|
|
31763
|
+
expression: callPageFn(rectFn, selector, index),
|
|
31261
31764
|
returnByValue: true
|
|
31262
31765
|
});
|
|
31263
31766
|
const rect = rectRes.result.value;
|
|
31264
31767
|
if (!rect?.found) {
|
|
31265
|
-
return errorText(`
|
|
31768
|
+
return errorText(import_common_tags4.oneLine`
|
|
31769
|
+
No element matched ${JSON.stringify(selector)} for game_screenshot at index ${index}
|
|
31770
|
+
(matches found: ${rect?.count ?? 0}).
|
|
31771
|
+
`);
|
|
31266
31772
|
}
|
|
31267
31773
|
if (!(rect.width > 0 && rect.height > 0)) {
|
|
31268
31774
|
return errorText(`Element ${JSON.stringify(selector)} has a zero-size box; nothing to capture.`);
|
|
31269
31775
|
}
|
|
31270
31776
|
params.clip = { x: rect.x, y: rect.y, width: rect.width, height: rect.height, scale: 1 };
|
|
31777
|
+
caption = import_common_tags4.oneLine`
|
|
31778
|
+
Clipped to ${JSON.stringify(selector)} [index ${index}]. Matches: ${rect.count}.
|
|
31779
|
+
`;
|
|
31271
31780
|
}
|
|
31272
31781
|
const res = await client.call("Page.captureScreenshot", params);
|
|
31273
31782
|
if (!res?.data) {
|
|
31274
|
-
return errorText(
|
|
31783
|
+
return errorText(`Page.captureScreenshot returned no image data.`);
|
|
31275
31784
|
}
|
|
31276
|
-
|
|
31277
|
-
|
|
31278
|
-
|
|
31279
|
-
|
|
31785
|
+
const image = {
|
|
31786
|
+
type: "image",
|
|
31787
|
+
data: res.data,
|
|
31788
|
+
mimeType: format == "jpeg" ? "image/jpeg" : "image/png"
|
|
31280
31789
|
};
|
|
31790
|
+
return { content: caption ? [{ type: "text", text: caption }, image] : [image] };
|
|
31281
31791
|
} catch (error51) {
|
|
31282
31792
|
return toErrorResult(error51);
|
|
31283
31793
|
}
|
|
@@ -31300,6 +31810,86 @@ async function gameDom(client, selector, all = false, maxHtml = 4000) {
|
|
|
31300
31810
|
return toErrorResult(error51);
|
|
31301
31811
|
}
|
|
31302
31812
|
}
|
|
31813
|
+
async function gameQuery(client, options) {
|
|
31814
|
+
const mode = options.match ?? "contains";
|
|
31815
|
+
const needle = options.text ?? null;
|
|
31816
|
+
const { selector } = options;
|
|
31817
|
+
const rejection = queryRejection(options, mode);
|
|
31818
|
+
if (rejection != null) {
|
|
31819
|
+
return errorText(rejection);
|
|
31820
|
+
}
|
|
31821
|
+
const args = {
|
|
31822
|
+
sel: selector ?? "*",
|
|
31823
|
+
needle,
|
|
31824
|
+
mode,
|
|
31825
|
+
caseSensitive: options.caseSensitive ?? false,
|
|
31826
|
+
deepest: options.deepest ?? (needle != null && needle.length > 0),
|
|
31827
|
+
tag: options.tag ?? false,
|
|
31828
|
+
attributes: options.attributes ?? false,
|
|
31829
|
+
limit: options.limit ?? DEFAULT_QUERY_LIMIT
|
|
31830
|
+
};
|
|
31831
|
+
try {
|
|
31832
|
+
const res = await client.call("Runtime.evaluate", {
|
|
31833
|
+
expression: callPageFn(queryFn, args),
|
|
31834
|
+
returnByValue: true
|
|
31835
|
+
});
|
|
31836
|
+
if (res.exceptionDetails) {
|
|
31837
|
+
return errorText(`Query failed: ${formatException(res.exceptionDetails)}`);
|
|
31838
|
+
}
|
|
31839
|
+
const value = res.result.value;
|
|
31840
|
+
if (!value) {
|
|
31841
|
+
return errorText(`game_query returned no result for selector ${JSON.stringify(args.sel)}.`);
|
|
31842
|
+
}
|
|
31843
|
+
if ("error" in value) {
|
|
31844
|
+
return errorText(`game_query: ${value.error}`);
|
|
31845
|
+
}
|
|
31846
|
+
const envelope = {
|
|
31847
|
+
selector: args.sel,
|
|
31848
|
+
match: args.needle == null ? undefined : args.mode,
|
|
31849
|
+
deepest: args.deepest,
|
|
31850
|
+
...value
|
|
31851
|
+
};
|
|
31852
|
+
return text(JSON.stringify(envelope, null, 2));
|
|
31853
|
+
} catch (error51) {
|
|
31854
|
+
return toErrorResult(error51);
|
|
31855
|
+
}
|
|
31856
|
+
}
|
|
31857
|
+
function queryRejection(options, mode) {
|
|
31858
|
+
const { text: needle, selector } = options;
|
|
31859
|
+
if (needle == null && selector == null) {
|
|
31860
|
+
return `game_query needs text, selector, or both; neither was given.`;
|
|
31861
|
+
}
|
|
31862
|
+
if (selector != null && selector.trim().length == 0) {
|
|
31863
|
+
return `game_query: selector is blank; pass a real one.`;
|
|
31864
|
+
}
|
|
31865
|
+
if (mode == "equals" && needle != null && needle.trim() != needle) {
|
|
31866
|
+
return import_common_tags4.oneLine`
|
|
31867
|
+
game_query: equals compares against trimmed text, so trim yours; where that empties it, pass
|
|
31868
|
+
'' with a selector to find elements carrying no text.
|
|
31869
|
+
`;
|
|
31870
|
+
}
|
|
31871
|
+
if (needle == "" && mode != "equals") {
|
|
31872
|
+
return import_common_tags4.oneLine`
|
|
31873
|
+
game_query: empty text matches every element under ${mode}; pass match: 'equals' with a
|
|
31874
|
+
selector to find elements carrying no text.
|
|
31875
|
+
`;
|
|
31876
|
+
}
|
|
31877
|
+
if (needle == "" && selector == null) {
|
|
31878
|
+
return `game_query: empty text needs a selector to scope it.`;
|
|
31879
|
+
}
|
|
31880
|
+
return;
|
|
31881
|
+
}
|
|
31882
|
+
function pausedNote(debug) {
|
|
31883
|
+
const { pause } = debug;
|
|
31884
|
+
if (!pause) {
|
|
31885
|
+
return "";
|
|
31886
|
+
}
|
|
31887
|
+
return import_common_tags4.oneLine`
|
|
31888
|
+
The UI is paused in the JS debugger (${pause.reason}, at ${pause.location}): nothing in it can
|
|
31889
|
+
change until you resume with game_debug_step action=resume, so resume and retry rather than
|
|
31890
|
+
reading this as an unreachable condition.
|
|
31891
|
+
`;
|
|
31892
|
+
}
|
|
31303
31893
|
async function gameClick(client, selector, index = 0) {
|
|
31304
31894
|
try {
|
|
31305
31895
|
const res = await client.call("Runtime.evaluate", {
|
|
@@ -31311,12 +31901,12 @@ async function gameClick(client, selector, index = 0) {
|
|
|
31311
31901
|
}
|
|
31312
31902
|
const info = res.result.value;
|
|
31313
31903
|
if (!info?.found) {
|
|
31314
|
-
return errorText(
|
|
31904
|
+
return errorText(import_common_tags4.oneLine`
|
|
31315
31905
|
No element to click for selector ${JSON.stringify(selector)} at index ${index}
|
|
31316
31906
|
(matches found: ${info?.count ?? 0}).
|
|
31317
31907
|
`);
|
|
31318
31908
|
}
|
|
31319
|
-
return text(
|
|
31909
|
+
return text(import_common_tags4.oneLine`
|
|
31320
31910
|
Clicked ${JSON.stringify(selector)} [index ${index}] at
|
|
31321
31911
|
(${info.x.toFixed(0)}, ${info.y.toFixed(0)}).
|
|
31322
31912
|
Dispatched: ${info.fired.join(", ")}. Matches: ${info.count}.
|
|
@@ -31325,18 +31915,78 @@ async function gameClick(client, selector, index = 0) {
|
|
|
31325
31915
|
return toErrorResult(error51);
|
|
31326
31916
|
}
|
|
31327
31917
|
}
|
|
31328
|
-
async function gameWait(client, options) {
|
|
31329
|
-
const {
|
|
31330
|
-
|
|
31331
|
-
|
|
31918
|
+
async function gameWait(client, reloads, debug, options) {
|
|
31919
|
+
const {
|
|
31920
|
+
selector,
|
|
31921
|
+
predicate,
|
|
31922
|
+
reload = false,
|
|
31923
|
+
sinceReloads,
|
|
31924
|
+
quiescentMs = DEFAULT_QUIESCENT_MS,
|
|
31925
|
+
visible = false
|
|
31926
|
+
} = options;
|
|
31927
|
+
if (!reload && !selector && !predicate) {
|
|
31928
|
+
return errorText(`game_wait needs at least one of 'reload', 'selector', or 'predicate'.`);
|
|
31332
31929
|
}
|
|
31930
|
+
if (selector && predicate) {
|
|
31931
|
+
return errorText(`game_wait takes either 'selector' or 'predicate', not both.`);
|
|
31932
|
+
}
|
|
31933
|
+
const timeoutMs = options.timeoutMs ?? (reload ? DEFAULT_RELOAD_WAIT_TIMEOUT_MS : DEFAULT_WAIT_TIMEOUT_MS);
|
|
31333
31934
|
const budget = Math.min(Math.max(timeoutMs, 0), MAX_WAIT_MS);
|
|
31334
|
-
const deadline = Date.now() + budget;
|
|
31335
31935
|
const start = Date.now();
|
|
31336
|
-
const
|
|
31337
|
-
let lastError;
|
|
31936
|
+
const deadline = start + budget;
|
|
31338
31937
|
try {
|
|
31339
|
-
|
|
31938
|
+
if (reload) {
|
|
31939
|
+
const failed = await waitForReload();
|
|
31940
|
+
if (failed) {
|
|
31941
|
+
return failed;
|
|
31942
|
+
}
|
|
31943
|
+
if (!selector && !predicate) {
|
|
31944
|
+
return text(`Reload observed after ${Date.now() - start}ms (reload count: ${reloads.count}).`);
|
|
31945
|
+
}
|
|
31946
|
+
}
|
|
31947
|
+
return await pollCondition();
|
|
31948
|
+
} catch (error51) {
|
|
31949
|
+
return toErrorResult(error51);
|
|
31950
|
+
}
|
|
31951
|
+
function timedOut(reason) {
|
|
31952
|
+
const paused = pausedNote(debug);
|
|
31953
|
+
return errorText(paused.length > 0 ? `${reason} ${paused}` : reason);
|
|
31954
|
+
}
|
|
31955
|
+
async function waitForReload() {
|
|
31956
|
+
await client.connection();
|
|
31957
|
+
const baseline = sinceReloads ?? reloads.count;
|
|
31958
|
+
while (reloads.count <= baseline) {
|
|
31959
|
+
if (Date.now() >= deadline) {
|
|
31960
|
+
return timedOut(import_common_tags4.oneLine`
|
|
31961
|
+
Timed out after ${budget}ms waiting for a view reload
|
|
31962
|
+
(reload count still ${reloads.count}, baseline ${baseline}).
|
|
31963
|
+
`);
|
|
31964
|
+
}
|
|
31965
|
+
await sleep2(POLL_INTERVAL_MS2);
|
|
31966
|
+
}
|
|
31967
|
+
if (quiescentMs > 0) {
|
|
31968
|
+
let lastCount = reloads.count;
|
|
31969
|
+
let quietSince = Date.now();
|
|
31970
|
+
while (Date.now() - quietSince < quiescentMs) {
|
|
31971
|
+
if (Date.now() >= deadline) {
|
|
31972
|
+
return timedOut(import_common_tags4.oneLine`
|
|
31973
|
+
Timed out after ${budget}ms: reload observed, but context swaps kept arriving within
|
|
31974
|
+
the ${quiescentMs}ms quiescence window.
|
|
31975
|
+
`);
|
|
31976
|
+
}
|
|
31977
|
+
await sleep2(POLL_INTERVAL_MS2);
|
|
31978
|
+
if (reloads.count != lastCount) {
|
|
31979
|
+
lastCount = reloads.count;
|
|
31980
|
+
quietSince = Date.now();
|
|
31981
|
+
}
|
|
31982
|
+
}
|
|
31983
|
+
}
|
|
31984
|
+
return;
|
|
31985
|
+
}
|
|
31986
|
+
async function pollCondition() {
|
|
31987
|
+
const expression = selector ? callPageFn(waitCheckFn, selector, visible) : `Boolean(${predicate ?? ""})`;
|
|
31988
|
+
let lastError;
|
|
31989
|
+
while (true) {
|
|
31340
31990
|
const res = await client.call("Runtime.evaluate", {
|
|
31341
31991
|
expression,
|
|
31342
31992
|
returnByValue: true
|
|
@@ -31345,25 +31995,24 @@ async function gameWait(client, options) {
|
|
|
31345
31995
|
lastError = formatException(res.exceptionDetails);
|
|
31346
31996
|
} else if (res.result.value) {
|
|
31347
31997
|
const what = selector ? `selector ${JSON.stringify(selector)}` : "predicate";
|
|
31348
|
-
|
|
31998
|
+
const reloadNote = reload ? ` Reload count: ${reloads.count}.` : "";
|
|
31999
|
+
return text(`Condition met (${what}) after ${Date.now() - start}ms.${reloadNote}`);
|
|
31349
32000
|
} else {
|
|
31350
32001
|
lastError = undefined;
|
|
31351
32002
|
}
|
|
31352
32003
|
if (Date.now() >= deadline) {
|
|
31353
32004
|
const what = selector ? `selector ${JSON.stringify(selector)}` : "predicate";
|
|
31354
32005
|
const errorNote = lastError ? ` Last predicate error: ${lastError}` : "";
|
|
31355
|
-
return
|
|
32006
|
+
return timedOut(`Timed out after ${budget}ms waiting for ${what}.${errorNote}`);
|
|
31356
32007
|
}
|
|
31357
32008
|
await sleep2(POLL_INTERVAL_MS2);
|
|
31358
32009
|
}
|
|
31359
|
-
} catch (error51) {
|
|
31360
|
-
return toErrorResult(error51);
|
|
31361
32010
|
}
|
|
31362
32011
|
}
|
|
31363
|
-
async function gameFill(client, selector, value) {
|
|
32012
|
+
async function gameFill(client, selector, value, index = 0) {
|
|
31364
32013
|
try {
|
|
31365
32014
|
const res = await client.call("Runtime.evaluate", {
|
|
31366
|
-
expression: callPageFn(fillFn, selector, value),
|
|
32015
|
+
expression: callPageFn(fillFn, selector, value, index),
|
|
31367
32016
|
returnByValue: true
|
|
31368
32017
|
});
|
|
31369
32018
|
if (res.exceptionDetails) {
|
|
@@ -31371,20 +32020,23 @@ async function gameFill(client, selector, value) {
|
|
|
31371
32020
|
}
|
|
31372
32021
|
const info = res.result.value;
|
|
31373
32022
|
if (!info?.found) {
|
|
31374
|
-
return errorText(`
|
|
32023
|
+
return errorText(import_common_tags4.oneLine`
|
|
32024
|
+
No element matched ${JSON.stringify(selector)} for game_fill at index ${index}
|
|
32025
|
+
(matches found: ${info?.count ?? 0}).
|
|
32026
|
+
`);
|
|
31375
32027
|
}
|
|
31376
|
-
return text(
|
|
31377
|
-
Filled ${JSON.stringify(selector)} (${info.mode}).
|
|
31378
|
-
Value is now ${JSON.stringify(info.value)}.
|
|
32028
|
+
return text(import_common_tags4.oneLine`
|
|
32029
|
+
Filled ${JSON.stringify(selector)} [index ${index}] (${info.mode}).
|
|
32030
|
+
Value is now ${JSON.stringify(info.value)}. Matches: ${info.count}.
|
|
31379
32031
|
`);
|
|
31380
32032
|
} catch (error51) {
|
|
31381
32033
|
return toErrorResult(error51);
|
|
31382
32034
|
}
|
|
31383
32035
|
}
|
|
31384
|
-
async function gameType(client, selector, textToType) {
|
|
32036
|
+
async function gameType(client, selector, textToType, index = 0) {
|
|
31385
32037
|
try {
|
|
31386
32038
|
const res = await client.call("Runtime.evaluate", {
|
|
31387
|
-
expression: callPageFn(typeFn, selector, textToType),
|
|
32039
|
+
expression: callPageFn(typeFn, selector, textToType, index),
|
|
31388
32040
|
returnByValue: true
|
|
31389
32041
|
});
|
|
31390
32042
|
if (res.exceptionDetails) {
|
|
@@ -31392,20 +32044,23 @@ async function gameType(client, selector, textToType) {
|
|
|
31392
32044
|
}
|
|
31393
32045
|
const info = res.result.value;
|
|
31394
32046
|
if (!info?.found) {
|
|
31395
|
-
return errorText(`
|
|
32047
|
+
return errorText(import_common_tags4.oneLine`
|
|
32048
|
+
No element matched ${JSON.stringify(selector)} for game_type at index ${index}
|
|
32049
|
+
(matches found: ${info?.count ?? 0}).
|
|
32050
|
+
`);
|
|
31396
32051
|
}
|
|
31397
|
-
return text(
|
|
31398
|
-
Typed ${info.typed} char(s) into ${JSON.stringify(selector)}.
|
|
31399
|
-
Value is now ${JSON.stringify(info.value)}.
|
|
32052
|
+
return text(import_common_tags4.oneLine`
|
|
32053
|
+
Typed ${info.typed} char(s) into ${JSON.stringify(selector)} [index ${index}].
|
|
32054
|
+
Value is now ${JSON.stringify(info.value)}. Matches: ${info.count}.
|
|
31400
32055
|
`);
|
|
31401
32056
|
} catch (error51) {
|
|
31402
32057
|
return toErrorResult(error51);
|
|
31403
32058
|
}
|
|
31404
32059
|
}
|
|
31405
|
-
async function gameHover(client, selector) {
|
|
32060
|
+
async function gameHover(client, selector, index = 0) {
|
|
31406
32061
|
try {
|
|
31407
32062
|
const res = await client.call("Runtime.evaluate", {
|
|
31408
|
-
expression: callPageFn(hoverFn, selector),
|
|
32063
|
+
expression: callPageFn(hoverFn, selector, index),
|
|
31409
32064
|
returnByValue: true
|
|
31410
32065
|
});
|
|
31411
32066
|
if (res.exceptionDetails) {
|
|
@@ -31413,88 +32068,569 @@ async function gameHover(client, selector) {
|
|
|
31413
32068
|
}
|
|
31414
32069
|
const info = res.result.value;
|
|
31415
32070
|
if (!info?.found) {
|
|
31416
|
-
return errorText(`
|
|
32071
|
+
return errorText(import_common_tags4.oneLine`
|
|
32072
|
+
No element matched ${JSON.stringify(selector)} for game_hover at index ${index}
|
|
32073
|
+
(matches found: ${info?.count ?? 0}).
|
|
32074
|
+
`);
|
|
32075
|
+
}
|
|
32076
|
+
return text(import_common_tags4.oneLine`
|
|
32077
|
+
Hovered ${JSON.stringify(selector)} [index ${index}] at
|
|
32078
|
+
(${info.x.toFixed(0)}, ${info.y.toFixed(0)}).
|
|
32079
|
+
Dispatched: ${info.fired.join(", ")}. Matches: ${info.count}.
|
|
32080
|
+
`);
|
|
32081
|
+
} catch (error51) {
|
|
32082
|
+
return toErrorResult(error51);
|
|
32083
|
+
}
|
|
32084
|
+
}
|
|
32085
|
+
async function gameKey(client, options) {
|
|
32086
|
+
const {
|
|
32087
|
+
key,
|
|
32088
|
+
count = 1,
|
|
32089
|
+
ctrl = false,
|
|
32090
|
+
shift = false,
|
|
32091
|
+
alt = false,
|
|
32092
|
+
meta: meta3 = false,
|
|
32093
|
+
selector,
|
|
32094
|
+
index = 0
|
|
32095
|
+
} = options;
|
|
32096
|
+
try {
|
|
32097
|
+
const res = await client.call("Runtime.evaluate", {
|
|
32098
|
+
expression: callPageFn(keyFn, {
|
|
32099
|
+
key,
|
|
32100
|
+
count,
|
|
32101
|
+
ctrl,
|
|
32102
|
+
shift,
|
|
32103
|
+
alt,
|
|
32104
|
+
meta: meta3,
|
|
32105
|
+
sel: selector ?? null,
|
|
32106
|
+
index
|
|
32107
|
+
}),
|
|
32108
|
+
returnByValue: true
|
|
32109
|
+
});
|
|
32110
|
+
if (res.exceptionDetails) {
|
|
32111
|
+
return errorText(`Key press failed: ${formatException(res.exceptionDetails)}`);
|
|
32112
|
+
}
|
|
32113
|
+
const info = res.result.value;
|
|
32114
|
+
if (!info) {
|
|
32115
|
+
return errorText(`game_key returned no result.`);
|
|
31417
32116
|
}
|
|
31418
|
-
|
|
31419
|
-
|
|
31420
|
-
|
|
32117
|
+
if (!info.found) {
|
|
32118
|
+
return errorText(import_common_tags4.oneLine`
|
|
32119
|
+
No element matched ${JSON.stringify(selector)} for game_key at index ${index}
|
|
32120
|
+
(matches found: ${info.count}).
|
|
32121
|
+
`);
|
|
32122
|
+
}
|
|
32123
|
+
const where = info.via == "selector" ? `${JSON.stringify(selector)} [index ${index}] ${info.target}` : info.via == "activeElement" ? `the focused element ${info.target}` : `document`;
|
|
32124
|
+
const matchNote = info.matches == null ? "" : ` Matches: ${info.matches}.`;
|
|
32125
|
+
return text(import_common_tags4.oneLine`
|
|
32126
|
+
Pressed ${keyLabel(key, { ctrl, shift, alt, meta: meta3 })} ${info.presses}x on ${where}.${matchNote}
|
|
32127
|
+
Default prevented: ${info.defaultPrevented ? "yes" : "no"}.
|
|
31421
32128
|
`);
|
|
31422
32129
|
} catch (error51) {
|
|
31423
32130
|
return toErrorResult(error51);
|
|
31424
32131
|
}
|
|
31425
32132
|
}
|
|
32133
|
+
function keyLabel(key, mods) {
|
|
32134
|
+
const parts = [];
|
|
32135
|
+
if (mods.ctrl) {
|
|
32136
|
+
parts.push("Ctrl");
|
|
32137
|
+
}
|
|
32138
|
+
if (mods.shift) {
|
|
32139
|
+
parts.push("Shift");
|
|
32140
|
+
}
|
|
32141
|
+
if (mods.alt) {
|
|
32142
|
+
parts.push("Alt");
|
|
32143
|
+
}
|
|
32144
|
+
if (mods.meta) {
|
|
32145
|
+
parts.push("Meta");
|
|
32146
|
+
}
|
|
32147
|
+
parts.push(key == " " ? "Space" : key);
|
|
32148
|
+
return parts.join("+");
|
|
32149
|
+
}
|
|
31426
32150
|
|
|
31427
|
-
class
|
|
31428
|
-
|
|
31429
|
-
|
|
31430
|
-
|
|
31431
|
-
|
|
32151
|
+
class ReloadTracker {
|
|
32152
|
+
reloadCount = 0;
|
|
32153
|
+
lastReloadAtMs;
|
|
32154
|
+
uniqueId;
|
|
32155
|
+
reloadListeners = new Set;
|
|
32156
|
+
constructor(client) {
|
|
31432
32157
|
client.onConnect(async (conn) => {
|
|
31433
32158
|
await conn.ensureDomain("Runtime");
|
|
31434
|
-
await conn.ensureDomain("Log");
|
|
31435
32159
|
});
|
|
31436
32160
|
client.onEvent((method, params) => {
|
|
31437
32161
|
this.handle(method, params);
|
|
31438
32162
|
});
|
|
31439
32163
|
}
|
|
31440
|
-
|
|
31441
|
-
|
|
31442
|
-
const out = filtered.slice(-limit);
|
|
31443
|
-
if (clear) {
|
|
31444
|
-
this.entries.length = 0;
|
|
31445
|
-
}
|
|
31446
|
-
return out;
|
|
32164
|
+
get count() {
|
|
32165
|
+
return this.reloadCount;
|
|
31447
32166
|
}
|
|
31448
|
-
|
|
31449
|
-
this.
|
|
31450
|
-
|
|
31451
|
-
|
|
31452
|
-
|
|
32167
|
+
get lastReloadAt() {
|
|
32168
|
+
return this.lastReloadAtMs;
|
|
32169
|
+
}
|
|
32170
|
+
get contextUniqueId() {
|
|
32171
|
+
return this.uniqueId;
|
|
32172
|
+
}
|
|
32173
|
+
onReload(listener) {
|
|
32174
|
+
this.reloadListeners.add(listener);
|
|
31453
32175
|
}
|
|
31454
32176
|
handle(method, params) {
|
|
31455
|
-
if (method
|
|
31456
|
-
|
|
31457
|
-
|
|
31458
|
-
|
|
31459
|
-
|
|
31460
|
-
|
|
31461
|
-
|
|
31462
|
-
|
|
31463
|
-
|
|
31464
|
-
|
|
31465
|
-
|
|
31466
|
-
|
|
31467
|
-
|
|
31468
|
-
|
|
31469
|
-
|
|
31470
|
-
|
|
31471
|
-
|
|
31472
|
-
|
|
31473
|
-
|
|
31474
|
-
|
|
31475
|
-
|
|
31476
|
-
|
|
31477
|
-
});
|
|
32177
|
+
if (method != "Runtime.executionContextCreated") {
|
|
32178
|
+
return;
|
|
32179
|
+
}
|
|
32180
|
+
const { context } = params;
|
|
32181
|
+
const isDefault = context?.auxData?.isDefault;
|
|
32182
|
+
if (!(isDefault == true || isDefault == "true") || context?.uniqueId == null) {
|
|
32183
|
+
return;
|
|
32184
|
+
}
|
|
32185
|
+
if (this.uniqueId == context.uniqueId) {
|
|
32186
|
+
return;
|
|
32187
|
+
}
|
|
32188
|
+
const isBaseline = this.uniqueId == null;
|
|
32189
|
+
this.uniqueId = context.uniqueId;
|
|
32190
|
+
if (isBaseline) {
|
|
32191
|
+
return;
|
|
32192
|
+
}
|
|
32193
|
+
this.reloadCount++;
|
|
32194
|
+
this.lastReloadAtMs = Date.now();
|
|
32195
|
+
for (const listener of this.reloadListeners) {
|
|
32196
|
+
try {
|
|
32197
|
+
listener(this.reloadCount);
|
|
32198
|
+
} catch {}
|
|
31478
32199
|
}
|
|
31479
32200
|
}
|
|
31480
32201
|
}
|
|
31481
|
-
|
|
31482
|
-
const
|
|
32202
|
+
function callPageFn(fn, ...args) {
|
|
32203
|
+
const serialisedArgs = args.map((arg) => JSON.stringify(arg)).join(", ");
|
|
32204
|
+
return `(${fn.toString()})(${serialisedArgs})`;
|
|
32205
|
+
}
|
|
32206
|
+
function collectDomFn(sel, all, maxHtml) {
|
|
32207
|
+
const matches = document.querySelectorAll(sel);
|
|
32208
|
+
const first = matches.item(0);
|
|
32209
|
+
const chosen = all ? Array.from(matches) : first ? [first] : [];
|
|
32210
|
+
return { count: matches.length, elements: chosen.map((el) => describe3(el)) };
|
|
32211
|
+
function describe3(el) {
|
|
32212
|
+
const rect = el.getBoundingClientRect();
|
|
32213
|
+
const attributes = {};
|
|
32214
|
+
for (const attr of Array.from(el.attributes)) {
|
|
32215
|
+
attributes[attr.name] = attr.value;
|
|
32216
|
+
}
|
|
32217
|
+
const classAttr = el.getAttribute("class");
|
|
32218
|
+
let html = el.outerHTML || "";
|
|
32219
|
+
const truncated = html.length > maxHtml;
|
|
32220
|
+
if (truncated) {
|
|
32221
|
+
html = html.slice(0, maxHtml);
|
|
32222
|
+
}
|
|
32223
|
+
return {
|
|
32224
|
+
tagName: el.tagName ? el.tagName.toLowerCase() : null,
|
|
32225
|
+
id: el.id || null,
|
|
32226
|
+
classes: classAttr != null && classAttr.length > 0 ? classAttr : null,
|
|
32227
|
+
rect: { x: rect.x, y: rect.y, width: rect.width, height: rect.height },
|
|
32228
|
+
attributes,
|
|
32229
|
+
outerHTML: html,
|
|
32230
|
+
truncated
|
|
32231
|
+
};
|
|
32232
|
+
}
|
|
32233
|
+
}
|
|
32234
|
+
function queryFn(args) {
|
|
32235
|
+
const { sel, needle, mode, caseSensitive, deepest, tag, attributes, limit } = args;
|
|
32236
|
+
const SNIPPET_MAX = 100;
|
|
32237
|
+
const target = needle == null ? "" : caseSensitive ? needle : needle.toLowerCase();
|
|
32238
|
+
let regex;
|
|
32239
|
+
if (needle != null && mode == "regex") {
|
|
32240
|
+
try {
|
|
32241
|
+
regex = new RegExp(needle, caseSensitive ? "" : "i");
|
|
32242
|
+
} catch (error51) {
|
|
32243
|
+
return { error: `Invalid regex: ${error51 instanceof Error ? error51.message : String(error51)}` };
|
|
32244
|
+
}
|
|
32245
|
+
}
|
|
32246
|
+
const found = [];
|
|
32247
|
+
for (const el of Array.from(document.querySelectorAll(sel))) {
|
|
32248
|
+
if (needle == null || matches((el.textContent || "").trim())) {
|
|
32249
|
+
found.push(el);
|
|
32250
|
+
}
|
|
32251
|
+
}
|
|
32252
|
+
const kept = deepest ? innermost(found) : found;
|
|
32253
|
+
const chosen = kept.slice(0, limit);
|
|
32254
|
+
if (tag) {
|
|
32255
|
+
for (const stale of Array.from(document.querySelectorAll("[data-gf-tag]"))) {
|
|
32256
|
+
stale.removeAttribute("data-gf-tag");
|
|
32257
|
+
}
|
|
32258
|
+
for (const [i, el] of chosen.entries()) {
|
|
32259
|
+
el.setAttribute("data-gf-tag", String(i + 1));
|
|
32260
|
+
}
|
|
32261
|
+
}
|
|
32262
|
+
return {
|
|
32263
|
+
unprunedTotal: found.length,
|
|
32264
|
+
total: kept.length,
|
|
32265
|
+
returned: chosen.length,
|
|
32266
|
+
tagged: tag,
|
|
32267
|
+
elements: chosen.map((el, i) => describe3(el, i))
|
|
32268
|
+
};
|
|
32269
|
+
function innermost(all) {
|
|
32270
|
+
const remaining = new Set(all);
|
|
32271
|
+
for (const el of all) {
|
|
32272
|
+
for (let parent = el.parentElement;parent != null; parent = parent.parentElement) {
|
|
32273
|
+
remaining.delete(parent);
|
|
32274
|
+
}
|
|
32275
|
+
}
|
|
32276
|
+
return Array.from(remaining);
|
|
32277
|
+
}
|
|
32278
|
+
function matches(raw) {
|
|
32279
|
+
if (mode == "regex") {
|
|
32280
|
+
return regex != null && regex.test(raw);
|
|
32281
|
+
}
|
|
32282
|
+
const hay = caseSensitive ? raw : raw.toLowerCase();
|
|
32283
|
+
if (mode == "equals") {
|
|
32284
|
+
return hay == target;
|
|
32285
|
+
}
|
|
32286
|
+
return hay.includes(target);
|
|
32287
|
+
}
|
|
32288
|
+
function describe3(el, i) {
|
|
32289
|
+
const rect = el.getBoundingClientRect();
|
|
32290
|
+
const classAttr = el.getAttribute("class");
|
|
32291
|
+
const raw = (el.textContent || "").trim();
|
|
32292
|
+
const truncated = raw.length > SNIPPET_MAX;
|
|
32293
|
+
const info = {
|
|
32294
|
+
tagName: el.tagName ? el.tagName.toLowerCase() : null,
|
|
32295
|
+
id: el.id || null,
|
|
32296
|
+
classes: classAttr != null && classAttr.length > 0 ? classAttr : null,
|
|
32297
|
+
rect: { x: rect.x, y: rect.y, width: rect.width, height: rect.height },
|
|
32298
|
+
text: truncated ? raw.slice(0, SNIPPET_MAX) : raw,
|
|
32299
|
+
truncated
|
|
32300
|
+
};
|
|
32301
|
+
if (attributes) {
|
|
32302
|
+
const map3 = {};
|
|
32303
|
+
for (const attr of Array.from(el.attributes)) {
|
|
32304
|
+
if (attr.name != "data-gf-tag") {
|
|
32305
|
+
map3[attr.name] = attr.value;
|
|
32306
|
+
}
|
|
32307
|
+
}
|
|
32308
|
+
info.attributes = map3;
|
|
32309
|
+
}
|
|
32310
|
+
if (tag) {
|
|
32311
|
+
info.selector = `[data-gf-tag="${i + 1}"]`;
|
|
32312
|
+
}
|
|
32313
|
+
return info;
|
|
32314
|
+
}
|
|
32315
|
+
}
|
|
32316
|
+
function clickFn(sel, index) {
|
|
32317
|
+
const nodes = document.querySelectorAll(sel);
|
|
32318
|
+
if (nodes.length == 0) {
|
|
32319
|
+
return { found: false, count: 0, fired: [] };
|
|
32320
|
+
}
|
|
32321
|
+
const node = nodes[index];
|
|
32322
|
+
if (!node) {
|
|
32323
|
+
return { found: false, count: nodes.length, fired: [] };
|
|
32324
|
+
}
|
|
32325
|
+
const el = node;
|
|
32326
|
+
const rect = el.getBoundingClientRect();
|
|
32327
|
+
const cx = rect.x + rect.width / 2;
|
|
32328
|
+
const cy = rect.y + rect.height / 2;
|
|
32329
|
+
const base = {
|
|
32330
|
+
bubbles: true,
|
|
32331
|
+
cancelable: true,
|
|
32332
|
+
view: window,
|
|
32333
|
+
button: 0,
|
|
32334
|
+
clientX: cx,
|
|
32335
|
+
clientY: cy
|
|
32336
|
+
};
|
|
32337
|
+
const Pointer = typeof PointerEvent == "function" ? PointerEvent : MouseEvent;
|
|
32338
|
+
const fired = [];
|
|
32339
|
+
fire(Pointer, "pointerdown", { pointerId: 1, isPrimary: true });
|
|
32340
|
+
fire(MouseEvent, "mousedown");
|
|
32341
|
+
fire(Pointer, "pointerup", { pointerId: 1, isPrimary: true });
|
|
32342
|
+
fire(MouseEvent, "mouseup");
|
|
32343
|
+
fire(MouseEvent, "click");
|
|
32344
|
+
return { found: true, count: nodes.length, x: cx, y: cy, fired };
|
|
32345
|
+
function fire(Ctor, type, extra) {
|
|
32346
|
+
try {
|
|
32347
|
+
el.dispatchEvent(new Ctor(type, { ...base, ...extra }));
|
|
32348
|
+
fired.push(type);
|
|
32349
|
+
} catch {}
|
|
32350
|
+
}
|
|
32351
|
+
}
|
|
32352
|
+
function waitCheckFn(sel, visible) {
|
|
32353
|
+
const el = document.querySelector(sel);
|
|
32354
|
+
if (!el) {
|
|
32355
|
+
return false;
|
|
32356
|
+
}
|
|
32357
|
+
if (!visible) {
|
|
32358
|
+
return true;
|
|
32359
|
+
}
|
|
32360
|
+
const rect = el.getBoundingClientRect();
|
|
32361
|
+
return rect.width > 0 && rect.height > 0;
|
|
32362
|
+
}
|
|
32363
|
+
function fillFn(sel, value, index) {
|
|
32364
|
+
const nodes = document.querySelectorAll(sel);
|
|
32365
|
+
if (nodes.length == 0) {
|
|
32366
|
+
return { found: false, count: 0 };
|
|
32367
|
+
}
|
|
32368
|
+
const node = nodes[index];
|
|
32369
|
+
if (!node) {
|
|
32370
|
+
return { found: false, count: nodes.length };
|
|
32371
|
+
}
|
|
32372
|
+
const el = node;
|
|
32373
|
+
const count = nodes.length;
|
|
31483
32374
|
try {
|
|
31484
|
-
|
|
31485
|
-
} catch
|
|
31486
|
-
|
|
32375
|
+
el.focus();
|
|
32376
|
+
} catch {}
|
|
32377
|
+
const tag = (el.tagName || "").toLowerCase();
|
|
32378
|
+
if (el.isContentEditable) {
|
|
32379
|
+
el.textContent = value;
|
|
32380
|
+
el.dispatchEvent(new Event("input", { bubbles: true }));
|
|
32381
|
+
el.dispatchEvent(new Event("change", { bubbles: true }));
|
|
32382
|
+
return { found: true, count, mode: "contenteditable", value: el.textContent ?? "" };
|
|
31487
32383
|
}
|
|
31488
|
-
const
|
|
31489
|
-
|
|
31490
|
-
|
|
31491
|
-
|
|
31492
|
-
|
|
31493
|
-
|
|
31494
|
-
|
|
32384
|
+
const field = el;
|
|
32385
|
+
const proto = tag == "textarea" ? HTMLTextAreaElement.prototype : HTMLInputElement.prototype;
|
|
32386
|
+
const setter = Object.getOwnPropertyDescriptor(proto, "value")?.set;
|
|
32387
|
+
if (setter) {
|
|
32388
|
+
setter.call(el, value);
|
|
32389
|
+
} else {
|
|
32390
|
+
field.value = value;
|
|
32391
|
+
}
|
|
32392
|
+
el.dispatchEvent(new Event("input", { bubbles: true }));
|
|
32393
|
+
el.dispatchEvent(new Event("change", { bubbles: true }));
|
|
32394
|
+
return { found: true, count, mode: tag || "input", value: field.value };
|
|
32395
|
+
}
|
|
32396
|
+
function typeFn(sel, textToType, index) {
|
|
32397
|
+
const nodes = document.querySelectorAll(sel);
|
|
32398
|
+
if (nodes.length == 0) {
|
|
32399
|
+
return { found: false, count: 0, typed: 0 };
|
|
32400
|
+
}
|
|
32401
|
+
const node = nodes[index];
|
|
32402
|
+
if (!node) {
|
|
32403
|
+
return { found: false, count: nodes.length, typed: 0 };
|
|
32404
|
+
}
|
|
32405
|
+
const el = node;
|
|
32406
|
+
const count = nodes.length;
|
|
32407
|
+
try {
|
|
32408
|
+
el.focus();
|
|
32409
|
+
} catch {}
|
|
32410
|
+
const tag = (el.tagName || "").toLowerCase();
|
|
32411
|
+
const editable = el.isContentEditable;
|
|
32412
|
+
const field = el;
|
|
32413
|
+
const proto = tag == "textarea" ? HTMLTextAreaElement.prototype : HTMLInputElement.prototype;
|
|
32414
|
+
const setter = Object.getOwnPropertyDescriptor(proto, "value")?.set;
|
|
32415
|
+
let typed = 0;
|
|
32416
|
+
for (const ch of textToType) {
|
|
32417
|
+
const opts = {
|
|
32418
|
+
bubbles: true,
|
|
32419
|
+
cancelable: true,
|
|
32420
|
+
key: ch,
|
|
32421
|
+
view: window
|
|
32422
|
+
};
|
|
32423
|
+
try {
|
|
32424
|
+
el.dispatchEvent(new KeyboardEvent("keydown", opts));
|
|
32425
|
+
} catch {}
|
|
32426
|
+
setValue(current() + ch);
|
|
32427
|
+
el.dispatchEvent(new Event("input", { bubbles: true }));
|
|
32428
|
+
try {
|
|
32429
|
+
el.dispatchEvent(new KeyboardEvent("keyup", opts));
|
|
32430
|
+
} catch {}
|
|
32431
|
+
typed++;
|
|
32432
|
+
}
|
|
32433
|
+
el.dispatchEvent(new Event("change", { bubbles: true }));
|
|
32434
|
+
return { found: true, count, typed, value: current() };
|
|
32435
|
+
function current() {
|
|
32436
|
+
if (editable) {
|
|
32437
|
+
return el.textContent ?? "";
|
|
32438
|
+
}
|
|
32439
|
+
return field.value || "";
|
|
32440
|
+
}
|
|
32441
|
+
function setValue(next) {
|
|
32442
|
+
if (editable) {
|
|
32443
|
+
el.textContent = next;
|
|
32444
|
+
} else if (setter) {
|
|
32445
|
+
setter.call(el, next);
|
|
32446
|
+
} else {
|
|
32447
|
+
field.value = next;
|
|
32448
|
+
}
|
|
32449
|
+
}
|
|
32450
|
+
}
|
|
32451
|
+
function hoverFn(sel, index) {
|
|
32452
|
+
const nodes = document.querySelectorAll(sel);
|
|
32453
|
+
if (nodes.length == 0) {
|
|
32454
|
+
return { found: false, count: 0 };
|
|
32455
|
+
}
|
|
32456
|
+
const node = nodes[index];
|
|
32457
|
+
if (!node) {
|
|
32458
|
+
return { found: false, count: nodes.length };
|
|
32459
|
+
}
|
|
32460
|
+
const el = node;
|
|
32461
|
+
const count = nodes.length;
|
|
32462
|
+
const rect = el.getBoundingClientRect();
|
|
32463
|
+
const cx = rect.x + rect.width / 2;
|
|
32464
|
+
const cy = rect.y + rect.height / 2;
|
|
32465
|
+
const base = {
|
|
32466
|
+
bubbles: true,
|
|
32467
|
+
cancelable: true,
|
|
32468
|
+
view: window,
|
|
32469
|
+
clientX: cx,
|
|
32470
|
+
clientY: cy
|
|
32471
|
+
};
|
|
32472
|
+
const noBubble = { ...base, bubbles: false };
|
|
32473
|
+
const fired = [];
|
|
32474
|
+
fire("pointerover", base);
|
|
32475
|
+
fire("mouseover", base);
|
|
32476
|
+
fire("pointerenter", noBubble);
|
|
32477
|
+
fire("mouseenter", noBubble);
|
|
32478
|
+
fire("mousemove", base);
|
|
32479
|
+
return { found: true, count, x: cx, y: cy, fired };
|
|
32480
|
+
function fire(type, init) {
|
|
32481
|
+
try {
|
|
32482
|
+
el.dispatchEvent(new MouseEvent(type, init));
|
|
32483
|
+
fired.push(type);
|
|
32484
|
+
} catch {}
|
|
31495
32485
|
}
|
|
31496
|
-
|
|
31497
|
-
|
|
32486
|
+
}
|
|
32487
|
+
function keyFn(args) {
|
|
32488
|
+
const { key, count, ctrl, shift, alt, meta: meta3, sel, index } = args;
|
|
32489
|
+
let target;
|
|
32490
|
+
let via;
|
|
32491
|
+
let node = null;
|
|
32492
|
+
let matches = null;
|
|
32493
|
+
if (sel == null) {
|
|
32494
|
+
const active = document.activeElement;
|
|
32495
|
+
if (active) {
|
|
32496
|
+
target = active;
|
|
32497
|
+
node = active;
|
|
32498
|
+
via = "activeElement";
|
|
32499
|
+
} else {
|
|
32500
|
+
target = document.body || document;
|
|
32501
|
+
node = document.body;
|
|
32502
|
+
via = "document";
|
|
32503
|
+
}
|
|
32504
|
+
} else {
|
|
32505
|
+
const nodes = document.querySelectorAll(sel);
|
|
32506
|
+
if (nodes.length == 0) {
|
|
32507
|
+
return { found: false, count: 0 };
|
|
32508
|
+
}
|
|
32509
|
+
const chosen = nodes[index];
|
|
32510
|
+
if (!chosen) {
|
|
32511
|
+
return { found: false, count: nodes.length };
|
|
32512
|
+
}
|
|
32513
|
+
try {
|
|
32514
|
+
chosen.focus();
|
|
32515
|
+
} catch {}
|
|
32516
|
+
target = chosen;
|
|
32517
|
+
node = chosen;
|
|
32518
|
+
matches = nodes.length;
|
|
32519
|
+
via = "selector";
|
|
32520
|
+
}
|
|
32521
|
+
const map3 = codeFor(key);
|
|
32522
|
+
const init = {
|
|
32523
|
+
bubbles: true,
|
|
32524
|
+
cancelable: true,
|
|
32525
|
+
view: window,
|
|
32526
|
+
key,
|
|
32527
|
+
ctrlKey: ctrl,
|
|
32528
|
+
shiftKey: shift,
|
|
32529
|
+
altKey: alt,
|
|
32530
|
+
metaKey: meta3
|
|
32531
|
+
};
|
|
32532
|
+
if (map3) {
|
|
32533
|
+
init.code = map3.code;
|
|
32534
|
+
}
|
|
32535
|
+
let defaultPrevented = false;
|
|
32536
|
+
let presses = 0;
|
|
32537
|
+
for (let i = 0;i < count; i++) {
|
|
32538
|
+
if (dispatch("keydown")) {
|
|
32539
|
+
defaultPrevented = true;
|
|
32540
|
+
}
|
|
32541
|
+
dispatch("keyup");
|
|
32542
|
+
presses++;
|
|
32543
|
+
}
|
|
32544
|
+
return {
|
|
32545
|
+
found: true,
|
|
32546
|
+
via,
|
|
32547
|
+
target: node ? describe3(node) : "document",
|
|
32548
|
+
matches,
|
|
32549
|
+
presses,
|
|
32550
|
+
defaultPrevented
|
|
32551
|
+
};
|
|
32552
|
+
function dispatch(type) {
|
|
32553
|
+
try {
|
|
32554
|
+
const ev = new KeyboardEvent(type, init);
|
|
32555
|
+
force(ev, "key", key);
|
|
32556
|
+
if (map3) {
|
|
32557
|
+
force(ev, "code", map3.code);
|
|
32558
|
+
force(ev, "keyCode", map3.keyCode);
|
|
32559
|
+
force(ev, "which", map3.keyCode);
|
|
32560
|
+
}
|
|
32561
|
+
const notPrevented = target.dispatchEvent(ev);
|
|
32562
|
+
return type == "keydown" && !notPrevented;
|
|
32563
|
+
} catch {
|
|
32564
|
+
return false;
|
|
32565
|
+
}
|
|
32566
|
+
}
|
|
32567
|
+
function force(ev, prop, value) {
|
|
32568
|
+
try {
|
|
32569
|
+
Object.defineProperty(ev, prop, { get: () => value, configurable: true });
|
|
32570
|
+
} catch {}
|
|
32571
|
+
}
|
|
32572
|
+
function codeFor(name) {
|
|
32573
|
+
const table = {
|
|
32574
|
+
Escape: { code: "Escape", keyCode: 27 },
|
|
32575
|
+
Enter: { code: "Enter", keyCode: 13 },
|
|
32576
|
+
Tab: { code: "Tab", keyCode: 9 },
|
|
32577
|
+
" ": { code: "Space", keyCode: 32 },
|
|
32578
|
+
Backspace: { code: "Backspace", keyCode: 8 },
|
|
32579
|
+
Delete: { code: "Delete", keyCode: 46 },
|
|
32580
|
+
Home: { code: "Home", keyCode: 36 },
|
|
32581
|
+
End: { code: "End", keyCode: 35 },
|
|
32582
|
+
PageUp: { code: "PageUp", keyCode: 33 },
|
|
32583
|
+
PageDown: { code: "PageDown", keyCode: 34 },
|
|
32584
|
+
ArrowUp: { code: "ArrowUp", keyCode: 38 },
|
|
32585
|
+
ArrowDown: { code: "ArrowDown", keyCode: 40 },
|
|
32586
|
+
ArrowLeft: { code: "ArrowLeft", keyCode: 37 },
|
|
32587
|
+
ArrowRight: { code: "ArrowRight", keyCode: 39 }
|
|
32588
|
+
};
|
|
32589
|
+
const known = table[name];
|
|
32590
|
+
if (known) {
|
|
32591
|
+
return known;
|
|
32592
|
+
}
|
|
32593
|
+
const F1_KEY_CODE = 112;
|
|
32594
|
+
const fn = /^F(?<num>[1-9]|1[0-2])$/u.exec(name);
|
|
32595
|
+
if (fn) {
|
|
32596
|
+
const num2 = Number(fn.groups?.num);
|
|
32597
|
+
return { code: `F${num2}`, keyCode: F1_KEY_CODE + num2 - 1 };
|
|
32598
|
+
}
|
|
32599
|
+
if (/^[a-zA-Z]$/u.test(name)) {
|
|
32600
|
+
const upper = name.toUpperCase();
|
|
32601
|
+
return { code: `Key${upper}`, keyCode: upper.codePointAt(0) ?? 0 };
|
|
32602
|
+
}
|
|
32603
|
+
if (/^[0-9]$/u.test(name)) {
|
|
32604
|
+
return { code: `Digit${name}`, keyCode: name.codePointAt(0) ?? 0 };
|
|
32605
|
+
}
|
|
32606
|
+
return;
|
|
32607
|
+
}
|
|
32608
|
+
function describe3(el) {
|
|
32609
|
+
const tag = el.tagName ? el.tagName.toLowerCase() : "node";
|
|
32610
|
+
const id = el.id ? `#${el.id}` : "";
|
|
32611
|
+
const classAttr = el.getAttribute("class");
|
|
32612
|
+
const cls = classAttr && classAttr.trim() ? `.${classAttr.trim().split(/\s+/u)[0]}` : "";
|
|
32613
|
+
return `<${tag}${id}${cls}>`;
|
|
32614
|
+
}
|
|
32615
|
+
}
|
|
32616
|
+
function rectFn(sel, index) {
|
|
32617
|
+
const nodes = document.querySelectorAll(sel);
|
|
32618
|
+
if (nodes.length == 0) {
|
|
32619
|
+
return { found: false, count: 0 };
|
|
32620
|
+
}
|
|
32621
|
+
const el = nodes[index];
|
|
32622
|
+
if (!el) {
|
|
32623
|
+
return { found: false, count: nodes.length };
|
|
32624
|
+
}
|
|
32625
|
+
const rect = el.getBoundingClientRect();
|
|
32626
|
+
return {
|
|
32627
|
+
found: true,
|
|
32628
|
+
count: nodes.length,
|
|
32629
|
+
x: rect.x,
|
|
32630
|
+
y: rect.y,
|
|
32631
|
+
width: rect.width,
|
|
32632
|
+
height: rect.height
|
|
32633
|
+
};
|
|
31498
32634
|
}
|
|
31499
32635
|
|
|
31500
32636
|
// src/server.ts
|
|
@@ -31502,208 +32638,359 @@ var { version: VERSION } = JSON.parse(readFileSync(new URL("../package.json", im
|
|
|
31502
32638
|
async function main() {
|
|
31503
32639
|
const config2 = loadConfig();
|
|
31504
32640
|
const client = new CdpClient(config2);
|
|
31505
|
-
const
|
|
31506
|
-
const
|
|
32641
|
+
const reloads = new ReloadTracker(client);
|
|
32642
|
+
const consoleBuffer = new ConsoleBuffer(client, reloads);
|
|
32643
|
+
const debug = new DebuggerSession(client, reloads);
|
|
31507
32644
|
const server = new McpServer({ name: "gameface-devtools-mcp", version: VERSION });
|
|
31508
32645
|
server.registerTool("game_status", {
|
|
31509
|
-
title:
|
|
31510
|
-
description:
|
|
31511
|
-
Check whether the Gameface UI debug endpoint is reachable and report the live page
|
|
31512
|
-
|
|
32646
|
+
title: `Gameface UI status`,
|
|
32647
|
+
description: import_common_tags5.oneLine`
|
|
32648
|
+
Check whether the Gameface UI debug endpoint is reachable and report the live page target,
|
|
32649
|
+
engine info, and view-reload tracking.
|
|
32650
|
+
Run this first when other game_* tools fail.
|
|
32651
|
+
Calling it arms reload tracking and returns the baseline count for game_wait's sinceReloads.
|
|
31513
32652
|
`
|
|
31514
|
-
}, () => gameStatus(client));
|
|
32653
|
+
}, () => gameStatus(client, reloads));
|
|
31515
32654
|
server.registerTool("game_eval", {
|
|
31516
|
-
title:
|
|
31517
|
-
description:
|
|
32655
|
+
title: `Evaluate JS in the Gameface UI`,
|
|
32656
|
+
description: import_common_tags5.oneLine`
|
|
31518
32657
|
Evaluate a JavaScript expression in the running Gameface UI (CDP Runtime.evaluate,
|
|
31519
|
-
returnByValue) and return the resulting value as JSON.
|
|
31520
|
-
friends to read the live DOM, inspect React state, or call UI APIs.
|
|
32658
|
+
returnByValue) and return the resulting value as JSON.
|
|
31521
32659
|
`,
|
|
31522
32660
|
inputSchema: {
|
|
31523
|
-
expression: exports_external.string().describe(
|
|
31524
|
-
awaitPromise: exports_external.boolean().optional().describe(
|
|
32661
|
+
expression: exports_external.string().describe(`JavaScript expression to evaluate in the page context`),
|
|
32662
|
+
awaitPromise: exports_external.boolean().optional().describe(`If the expression returns a Promise, await it before returning`)
|
|
31525
32663
|
}
|
|
31526
32664
|
}, ({ expression, awaitPromise }) => gameEval(client, expression, awaitPromise));
|
|
31527
32665
|
server.registerTool("game_screenshot", {
|
|
31528
|
-
title:
|
|
31529
|
-
description:
|
|
31530
|
-
Capture a screenshot of the Gameface viewport
|
|
31531
|
-
|
|
31532
|
-
|
|
32666
|
+
title: `Screenshot the Gameface UI`,
|
|
32667
|
+
description: import_common_tags5.oneLine`
|
|
32668
|
+
Capture a screenshot of the Gameface viewport and return it as an inline image; a selector
|
|
32669
|
+
clips the capture to one element.
|
|
32670
|
+
Clipping is the only lever on what the image costs you in context: that cost tracks the
|
|
32671
|
+
pixel area captured, never the encoded file size.
|
|
32672
|
+
Refuses while the JS debugger holds the UI paused: the capture needs the frame loop a
|
|
32673
|
+
pause freezes.
|
|
31533
32674
|
`,
|
|
31534
32675
|
inputSchema: {
|
|
31535
|
-
format: exports_external.enum(["png", "jpeg"]).optional().describe(
|
|
31536
|
-
quality: exports_external.number().min(1).max(100).optional().describe(
|
|
31537
|
-
|
|
31538
|
-
|
|
31539
|
-
|
|
32676
|
+
format: exports_external.enum(["png", "jpeg"]).optional().describe(`Image format (default: png)`),
|
|
32677
|
+
quality: exports_external.number().min(1).max(100).optional().describe(import_common_tags5.oneLine`
|
|
32678
|
+
JPEG quality 1-100 (only used when format is jpeg; default 80).
|
|
32679
|
+
Trades fidelity for transfer bytes, leaving the image's context cost unchanged.
|
|
32680
|
+
`),
|
|
32681
|
+
selector: exports_external.string().optional().describe(`If set, clip the screenshot to this element's bounding box`),
|
|
32682
|
+
index: exports_external.number().int().min(0).optional().describe(`Which match to clip to when several exist (default: 0)`)
|
|
32683
|
+
}
|
|
32684
|
+
}, ({ format, quality, selector, index }) => gameScreenshot(client, debug, { format, quality, selector, index }));
|
|
31540
32685
|
server.registerTool("game_wait", {
|
|
31541
|
-
title:
|
|
31542
|
-
description:
|
|
31543
|
-
Wait until a CSS selector matches (optionally visible)
|
|
31544
|
-
|
|
31545
|
-
|
|
32686
|
+
title: `Wait for a condition in the Gameface UI`,
|
|
32687
|
+
description: import_common_tags5.oneLine`
|
|
32688
|
+
Wait until a CSS selector matches (optionally visible), a JS predicate becomes truthy,
|
|
32689
|
+
and/or a view reload happens. Provide at least one of reload / selector / predicate
|
|
32690
|
+
(selector and predicate are mutually exclusive).
|
|
32691
|
+
With reload, the phases compose: reload first, then a quiescence window, then the
|
|
32692
|
+
selector/predicate poll in the fresh context.
|
|
32693
|
+
It stays usable while the JS debugger is paused, and a timeout says so when it was.
|
|
31546
32694
|
`,
|
|
31547
32695
|
inputSchema: {
|
|
31548
|
-
selector: exports_external.string().optional().describe(
|
|
31549
|
-
predicate: exports_external.string().optional().describe(
|
|
31550
|
-
|
|
31551
|
-
|
|
31552
|
-
|
|
31553
|
-
|
|
32696
|
+
selector: exports_external.string().optional().describe(`CSS selector to wait for`),
|
|
32697
|
+
predicate: exports_external.string().optional().describe(`JS expression evaluated in the page; waits until it is truthy`),
|
|
32698
|
+
reload: exports_external.boolean().optional().describe(import_common_tags5.oneLine`
|
|
32699
|
+
Wait for a view reload (context reset) before the selector/predicate phase.
|
|
32700
|
+
Without sinceReloads, waits for the next reload after the call starts.
|
|
32701
|
+
`),
|
|
32702
|
+
sinceReloads: exports_external.number().int().min(0).optional().describe(import_common_tags5.oneLine`
|
|
32703
|
+
Baseline reload count (from a prior game_status or game_wait).
|
|
32704
|
+
The reload phase is satisfied as soon as the count exceeds it, even if the reload
|
|
32705
|
+
already happened; use it to avoid racing a reload you triggered yourself.
|
|
32706
|
+
`),
|
|
32707
|
+
quiescentMs: exports_external.number().int().min(0).optional().describe(import_common_tags5.oneLine`
|
|
32708
|
+
After a reload is observed, hold until no further context swap for this long (default
|
|
32709
|
+
1000, 0 disables); absorbs engines that swap the context several times per reload.
|
|
32710
|
+
`),
|
|
32711
|
+
timeoutMs: exports_external.number().int().min(0).optional().describe(import_common_tags5.oneLine`
|
|
32712
|
+
Max time to wait in ms (default 8000, or 30000 when reload is set; capped at 60000)
|
|
32713
|
+
`),
|
|
32714
|
+
visible: exports_external.boolean().optional().describe(`For selector waits, also require a non-zero bounding box (default false)`)
|
|
32715
|
+
}
|
|
32716
|
+
}, ({ selector, predicate, reload, sinceReloads, quiescentMs, timeoutMs, visible }) => gameWait(client, reloads, debug, {
|
|
32717
|
+
selector,
|
|
32718
|
+
predicate,
|
|
32719
|
+
reload,
|
|
32720
|
+
sinceReloads,
|
|
32721
|
+
quiescentMs,
|
|
32722
|
+
timeoutMs,
|
|
32723
|
+
visible
|
|
32724
|
+
}));
|
|
31554
32725
|
server.registerTool("game_fill", {
|
|
31555
|
-
title:
|
|
31556
|
-
description:
|
|
32726
|
+
title: `Set an input value in the Gameface UI`,
|
|
32727
|
+
description: import_common_tags5.oneLine`
|
|
31557
32728
|
Set the value of an input, textarea, or contenteditable element and fire input/change so
|
|
31558
|
-
|
|
32729
|
+
the UI framework reacts as if the user edited it.
|
|
32730
|
+
Best for setting a field in one shot; use game_type for keystrokes.
|
|
31559
32731
|
`,
|
|
31560
32732
|
inputSchema: {
|
|
31561
|
-
selector: exports_external.string().describe(
|
|
31562
|
-
value: exports_external.string().describe(
|
|
32733
|
+
selector: exports_external.string().describe(`CSS selector of the field to fill`),
|
|
32734
|
+
value: exports_external.string().describe(`Value to set`),
|
|
32735
|
+
index: exports_external.number().int().min(0).optional().describe(`Which match to fill when several exist (default: 0)`)
|
|
31563
32736
|
}
|
|
31564
|
-
}, ({ selector, value }) => gameFill(client, selector, value));
|
|
32737
|
+
}, ({ selector, value, index }) => gameFill(client, selector, value, index));
|
|
31565
32738
|
server.registerTool("game_type", {
|
|
31566
|
-
title:
|
|
31567
|
-
description:
|
|
32739
|
+
title: `Type text into the Gameface UI`,
|
|
32740
|
+
description: import_common_tags5.oneLine`
|
|
31568
32741
|
Type text into an element character by character, firing real KeyboardEvents plus keeping
|
|
31569
|
-
the value in sync.
|
|
32742
|
+
the value in sync.
|
|
32743
|
+
Use when handlers react to individual keystrokes; otherwise game_fill.
|
|
31570
32744
|
`,
|
|
31571
32745
|
inputSchema: {
|
|
31572
|
-
selector: exports_external.string().describe(
|
|
31573
|
-
text: exports_external.string().describe(
|
|
32746
|
+
selector: exports_external.string().describe(`CSS selector of the field to type into`),
|
|
32747
|
+
text: exports_external.string().describe(`Text to type`),
|
|
32748
|
+
index: exports_external.number().int().min(0).optional().describe(`Which match to type into when several exist (default: 0)`)
|
|
31574
32749
|
}
|
|
31575
|
-
}, ({ selector, text: text2 }) => gameType(client, selector, text2));
|
|
32750
|
+
}, ({ selector, text: text2, index }) => gameType(client, selector, text2, index));
|
|
31576
32751
|
server.registerTool("game_hover", {
|
|
31577
|
-
title:
|
|
31578
|
-
description:
|
|
32752
|
+
title: `Hover an element in the Gameface UI`,
|
|
32753
|
+
description: import_common_tags5.oneLine`
|
|
31579
32754
|
Hover an element by dispatching the pointer/mouse over/enter/move sequence in the page, so
|
|
31580
|
-
|
|
32755
|
+
the UI's mouseenter / pointerover JS handlers (tooltips) fire.
|
|
32756
|
+
The CSS :hover state is NOT set (only real game-forwarded mouse input sets it); verify a
|
|
32757
|
+
hover by its DOM effect, never by styling.
|
|
31581
32758
|
`,
|
|
31582
32759
|
inputSchema: {
|
|
31583
|
-
selector: exports_external.string().describe(
|
|
31584
|
-
|
|
31585
|
-
|
|
32760
|
+
selector: exports_external.string().describe(`CSS selector of the element to hover`),
|
|
32761
|
+
index: exports_external.number().int().min(0).optional().describe(`Which match to hover when several exist (default: 0)`)
|
|
32762
|
+
}
|
|
32763
|
+
}, ({ selector, index }) => gameHover(client, selector, index));
|
|
32764
|
+
server.registerTool("game_key", {
|
|
32765
|
+
title: `Press a key in the Gameface UI`,
|
|
32766
|
+
description: import_common_tags5.oneLine`
|
|
32767
|
+
Press a named key by dispatching a real bubbling keydown+keyup (no keypress) in the page
|
|
32768
|
+
(KeyboardEvent.key, e.g. Escape, Enter, ArrowDown, a, F5), optionally with
|
|
32769
|
+
ctrl/shift/alt/meta and a repeat count.
|
|
32770
|
+
It performs NO default action: no character insertion, no Backspace delete, no Tab focus
|
|
32771
|
+
move, no scrolling; use game_type to enter text.
|
|
32772
|
+
It reaches the UI's JS keydown handlers, but keys the game routes through its own native
|
|
32773
|
+
input layer do NOT respond (e.g. an Escape-to-close handled by the engine rather than the
|
|
32774
|
+
DOM).
|
|
32775
|
+
The result reports whether a handler called preventDefault, the observable signal the key
|
|
32776
|
+
was consumed.
|
|
32777
|
+
`,
|
|
32778
|
+
inputSchema: {
|
|
32779
|
+
key: exports_external.string().describe(`KeyboardEvent.key name to press, e.g. Escape, Enter, ArrowDown, a, F5`),
|
|
32780
|
+
count: exports_external.number().int().min(1).max(100).optional().describe(`How many keydown+keyup presses to fire back-to-back (default 1)`),
|
|
32781
|
+
ctrl: exports_external.boolean().optional().describe(`Hold Ctrl (ctrlKey) during the press (default false)`),
|
|
32782
|
+
shift: exports_external.boolean().optional().describe(`Hold Shift (shiftKey) during the press (default false)`),
|
|
32783
|
+
alt: exports_external.boolean().optional().describe(`Hold Alt (altKey) during the press (default false)`),
|
|
32784
|
+
meta: exports_external.boolean().optional().describe(`Hold Meta / Win / Cmd (metaKey) during the press (default false)`),
|
|
32785
|
+
selector: exports_external.string().optional().describe(`If set, focus this element and dispatch on it; else the focused element, else document`),
|
|
32786
|
+
index: exports_external.number().int().min(0).optional().describe(`Which match to target when the selector has several (default: 0)`)
|
|
32787
|
+
}
|
|
32788
|
+
}, ({ key, count, ctrl, shift, alt, meta: meta3, selector, index }) => gameKey(client, { key, count, ctrl, shift, alt, meta: meta3, selector, index }));
|
|
31586
32789
|
server.registerTool("game_console", {
|
|
31587
|
-
title:
|
|
31588
|
-
description:
|
|
32790
|
+
title: `Read the Gameface UI console`,
|
|
32791
|
+
description: import_common_tags5.oneLine`
|
|
31589
32792
|
Return recent console.* calls, log entries, and uncaught exceptions captured from the
|
|
31590
|
-
Gameface UI
|
|
32793
|
+
Gameface UI, each prefixed with local wall-clock time and its object arguments expanded to
|
|
32794
|
+
their real values (state {a: 1, b: {c: {…}}, arr: [1, 2, 3]}).
|
|
32795
|
+
Capture starts when the server first connects to the application.
|
|
32796
|
+
Truncation is always marked: {…} / […] at the rendered depth, …N more past the per-level
|
|
32797
|
+
cap, a trailing ellipsis inside a clipped string.
|
|
32798
|
+
For unbounded depth or parseable output, read the value with game_eval + JSON.stringify.
|
|
31591
32799
|
`,
|
|
31592
32800
|
inputSchema: {
|
|
31593
|
-
limit: exports_external.number().int().min(1).max(1000).optional().describe(
|
|
31594
|
-
level: exports_external.string().optional().describe(
|
|
31595
|
-
|
|
31596
|
-
|
|
31597
|
-
|
|
32801
|
+
limit: exports_external.number().int().min(1).max(1000).optional().describe(`Max entries to return (default 50)`),
|
|
32802
|
+
level: exports_external.string().optional().describe(`Filter by level, e.g. error / warning / log / info`),
|
|
32803
|
+
depth: exports_external.number().int().min(1).max(CAPTURE_DEPTH_CAP).optional().describe(import_common_tags5.oneLine`
|
|
32804
|
+
How many levels of an expanded object to render (default
|
|
32805
|
+
${String(DEFAULT_RENDER_DEPTH)}).
|
|
32806
|
+
Entries are captured ${String(CAPTURE_DEPTH_CAP)} levels deep, so re-reading the same
|
|
32807
|
+
entries deeper works up to that cap.
|
|
32808
|
+
`),
|
|
32809
|
+
clear: exports_external.boolean().optional().describe(import_common_tags5.oneLine`
|
|
32810
|
+
Empty the buffer once this read has taken its entries (default false).
|
|
32811
|
+
It also drops captures still being expanded, so a line logged moments before the call
|
|
32812
|
+
is discarded rather than surfacing on the next read.
|
|
32813
|
+
`)
|
|
32814
|
+
}
|
|
32815
|
+
}, ({ limit, level, depth, clear }) => gameConsole(client, consoleBuffer, { limit, level, depth, clear }));
|
|
31598
32816
|
server.registerTool("game_dom", {
|
|
31599
|
-
title:
|
|
31600
|
-
description:
|
|
32817
|
+
title: `Inspect Gameface UI DOM`,
|
|
32818
|
+
description: import_common_tags5.oneLine`
|
|
31601
32819
|
Return DOM details (tag, id, classes, attributes, bounding rect, outerHTML) for elements
|
|
31602
|
-
matching a CSS selector in the live Gameface UI.
|
|
32820
|
+
matching a CSS selector in the live Gameface UI.
|
|
32821
|
+
outerHTML dominates the response: to locate elements, get a targetable handle, or read
|
|
32822
|
+
attributes across many matches, use game_query.
|
|
31603
32823
|
`,
|
|
31604
32824
|
inputSchema: {
|
|
31605
|
-
selector: exports_external.string().describe(
|
|
31606
|
-
all: exports_external.boolean().optional().describe(
|
|
31607
|
-
maxHtml: exports_external.number().min(0).optional().describe(
|
|
32825
|
+
selector: exports_external.string().describe(`CSS selector to query in the Gameface UI`),
|
|
32826
|
+
all: exports_external.boolean().optional().describe(`Return all matches instead of just the first (default: false)`),
|
|
32827
|
+
maxHtml: exports_external.number().min(0).optional().describe(`Max outerHTML characters per element before truncation (default: 4000)`)
|
|
31608
32828
|
}
|
|
31609
32829
|
}, ({ selector, all, maxHtml }) => gameDom(client, selector, all, maxHtml));
|
|
32830
|
+
server.registerTool("game_query", {
|
|
32831
|
+
title: `Find elements in the Gameface UI`,
|
|
32832
|
+
description: import_common_tags5.oneLine`
|
|
32833
|
+
Locate elements in the live Gameface UI by CSS selector, by trimmed textContent
|
|
32834
|
+
(equals/contains/regex, case-insensitive by default), or by both; one of the two is
|
|
32835
|
+
required, and match/caseSensitive are ignored without text.
|
|
32836
|
+
Returns tag, id, classes and bounding rect per match, attributes on request, with the
|
|
32837
|
+
counts before and after deepest pruning and the limit, so both truncations are visible.
|
|
32838
|
+
Set tag=true for data-gf-tag handles feeding game_click / game_hover / game_screenshot.
|
|
32839
|
+
To read an element's markup, use game_dom.
|
|
32840
|
+
`,
|
|
32841
|
+
inputSchema: {
|
|
32842
|
+
text: exports_external.string().optional().describe(import_common_tags5.oneLine`
|
|
32843
|
+
Text to match against each element's trimmed textContent, so pass it trimmed under
|
|
32844
|
+
match=equals; omit it to select on the selector alone, or pass '' with match=equals
|
|
32845
|
+
and a selector for elements carrying no text.
|
|
32846
|
+
`),
|
|
32847
|
+
match: exports_external.enum(["equals", "contains", "regex"]).optional().describe(`How to match the text: equals / contains / regex (default: contains)`),
|
|
32848
|
+
caseSensitive: exports_external.boolean().optional().describe(`Match case-sensitively (default: false)`),
|
|
32849
|
+
selector: exports_external.string().optional().describe(`CSS selector to match, alone or scoping the text scan (with text, defaults to *)`),
|
|
32850
|
+
deepest: exports_external.boolean().optional().describe(import_common_tags5.oneLine`
|
|
32851
|
+
Keep only the innermost match, pruning ancestors that also matched (default: true
|
|
32852
|
+
when non-empty text drives the match, false otherwise).
|
|
32853
|
+
`),
|
|
32854
|
+
tag: exports_external.boolean().optional().describe(import_common_tags5.oneLine`
|
|
32855
|
+
Stamp matches with data-gf-tag handles and return them as selectors, clearing any
|
|
32856
|
+
prior handles first (default: false).
|
|
32857
|
+
`),
|
|
32858
|
+
attributes: exports_external.boolean().optional().describe(`Return each match's attributes, this tool's own data-gf-tag aside (default: false)`),
|
|
32859
|
+
limit: exports_external.number().int().min(1).max(100).optional().describe(`Max matches to return (default: 20); the total count is always reported`)
|
|
32860
|
+
}
|
|
32861
|
+
}, ({ text: text2, match, caseSensitive, selector, deepest, tag, attributes, limit }) => gameQuery(client, { text: text2, match, caseSensitive, selector, deepest, tag, attributes, limit }));
|
|
31610
32862
|
server.registerTool("game_click", {
|
|
31611
|
-
title:
|
|
31612
|
-
description:
|
|
31613
|
-
Click the element matching a CSS selector by dispatching a real bubbling
|
|
31614
|
-
|
|
31615
|
-
UI). React onClick handlers fire via event delegation. Use index to pick among matches.
|
|
32863
|
+
title: `Click an element in the Gameface UI`,
|
|
32864
|
+
description: import_common_tags5.oneLine`
|
|
32865
|
+
Click the element matching a CSS selector by dispatching a real bubbling pointer/mouse/click
|
|
32866
|
+
sequence in the page (NOT CDP Input, which Gameface ignores for the UI).
|
|
31616
32867
|
`,
|
|
31617
32868
|
inputSchema: {
|
|
31618
|
-
selector: exports_external.string().describe(
|
|
31619
|
-
index: exports_external.number().int().min(0).optional().describe(
|
|
32869
|
+
selector: exports_external.string().describe(`CSS selector of the element to click`),
|
|
32870
|
+
index: exports_external.number().int().min(0).optional().describe(`Which match to click when several exist (default: 0)`)
|
|
31620
32871
|
}
|
|
31621
32872
|
}, ({ selector, index }) => gameClick(client, selector, index));
|
|
31622
32873
|
server.registerTool("game_debug_status", {
|
|
31623
|
-
title:
|
|
31624
|
-
description:
|
|
32874
|
+
title: `JS debugger status`,
|
|
32875
|
+
description: import_common_tags5.oneLine`
|
|
31625
32876
|
Report debugger state: whether paused (and where), pause-on-exceptions mode, breakpoints,
|
|
31626
|
-
and parsed script count.
|
|
31627
|
-
the debugger on first use.
|
|
32877
|
+
and parsed script count.
|
|
32878
|
+
Enables the debugger on first use.
|
|
31628
32879
|
`,
|
|
31629
32880
|
inputSchema: {
|
|
31630
|
-
setPauseOnExceptions: exports_external.enum(["none", "uncaught", "all"]).optional().describe(
|
|
32881
|
+
setPauseOnExceptions: exports_external.enum(["none", "uncaught", "all"]).optional().describe(`If set, change which exceptions pause execution (default none)`)
|
|
31631
32882
|
}
|
|
31632
32883
|
}, ({ setPauseOnExceptions }) => debug.status(setPauseOnExceptions));
|
|
31633
32884
|
server.registerTool("game_debug_scripts", {
|
|
31634
|
-
title:
|
|
31635
|
-
description:
|
|
31636
|
-
List JavaScript scripts parsed in the Gameface UI (scriptId + url + line count)
|
|
31637
|
-
|
|
32885
|
+
title: `List parsed UI scripts`,
|
|
32886
|
+
description: import_common_tags5.oneLine`
|
|
32887
|
+
List JavaScript scripts parsed in the Gameface UI (scriptId + url + line count).
|
|
32888
|
+
Only scripts parsed since the debugger attached appear, since Gameface does not replay
|
|
32889
|
+
scriptParsed; an empty list is what a late attach looks like, and the result says how to
|
|
32890
|
+
fill it.
|
|
32891
|
+
Feed the scriptId to game_debug_source; game_debug_search_source finds a position inside
|
|
32892
|
+
one.
|
|
31638
32893
|
`,
|
|
31639
32894
|
inputSchema: {
|
|
31640
|
-
filter: exports_external.string().optional().describe(
|
|
32895
|
+
filter: exports_external.string().optional().describe(`Only scripts whose url contains this substring`)
|
|
31641
32896
|
}
|
|
31642
32897
|
}, ({ filter }) => debug.listScripts(filter));
|
|
31643
32898
|
server.registerTool("game_debug_source", {
|
|
31644
|
-
title:
|
|
31645
|
-
description:
|
|
32899
|
+
title: `Get UI script source`,
|
|
32900
|
+
description: import_common_tags5.oneLine`
|
|
31646
32901
|
Return the source of a script (by scriptId from game_debug_scripts), with line numbers.
|
|
31647
|
-
|
|
32902
|
+
The window is capped at 400 lines, range or not; the result says which lines it showed.
|
|
32903
|
+
It renders whole lines, so a low line count is no promise of a small answer: one minified
|
|
32904
|
+
line can be the whole module, and reaching a position inside such a line is
|
|
32905
|
+
game_debug_search_source's job.
|
|
32906
|
+
`,
|
|
32907
|
+
inputSchema: {
|
|
32908
|
+
scriptId: exports_external.string().describe(`Script id from game_debug_scripts`),
|
|
32909
|
+
lineStart: exports_external.number().int().min(1).optional().describe(import_common_tags5.oneLine`
|
|
32910
|
+
First line, 1-based and numbered as the whole file or document is, so a script embedded
|
|
32911
|
+
in a page starts at its firstLine from game_debug_scripts rather than at 1.
|
|
32912
|
+
`),
|
|
32913
|
+
lineEnd: exports_external.number().int().min(1).optional().describe(`Last line, numbered as lineStart is`),
|
|
32914
|
+
maxChars: exports_external.number().int().min(1).optional().describe(import_common_tags5.oneLine`
|
|
32915
|
+
Characters of source to return before clipping (default
|
|
32916
|
+
${String(DEFAULT_SOURCE_MAX_CHARS)}); the result says when it clipped and at what.
|
|
32917
|
+
`)
|
|
32918
|
+
}
|
|
32919
|
+
}, ({ scriptId, lineStart, lineEnd, maxChars }) => debug.getSource(scriptId, lineStart, lineEnd, maxChars));
|
|
32920
|
+
server.registerTool("game_debug_search_source", {
|
|
32921
|
+
title: `Search UI script sources`,
|
|
32922
|
+
description: import_common_tags5.oneLine`
|
|
32923
|
+
Find a literal string across the parsed script sources and return each hit as
|
|
32924
|
+
url + line + column (1-based) with a snippet of surrounding source.
|
|
32925
|
+
The query is case-sensitive and literal, no regex; read the total/truncated fields for what
|
|
32926
|
+
the ${MAX_SEARCH_MATCHES}-match cap hid.
|
|
32927
|
+
This is how you target a column in a minified one-line bundle, where a line breakpoint binds
|
|
32928
|
+
to module-evaluation code that never runs during interaction: search for the code you mean,
|
|
32929
|
+
then pass its line and column to game_debug_set_breakpoint.
|
|
31648
32930
|
`,
|
|
31649
32931
|
inputSchema: {
|
|
31650
|
-
|
|
31651
|
-
|
|
31652
|
-
lineEnd: exports_external.number().int().min(1).optional().describe("Last line (1-based)")
|
|
32932
|
+
query: exports_external.string().min(1).describe(`Literal string to find (case-sensitive, not a regex)`),
|
|
32933
|
+
urlContains: exports_external.string().optional().describe(`Only search scripts whose url contains this substring (case-insensitive)`)
|
|
31653
32934
|
}
|
|
31654
|
-
}, ({
|
|
32935
|
+
}, ({ query, urlContains }) => debug.searchSource(query, urlContains));
|
|
31655
32936
|
server.registerTool("game_debug_set_breakpoint", {
|
|
31656
|
-
title:
|
|
31657
|
-
description:
|
|
31658
|
-
Set a breakpoint by url substring + line
|
|
31659
|
-
|
|
31660
|
-
the
|
|
32937
|
+
title: `Set a breakpoint`,
|
|
32938
|
+
description: import_common_tags5.oneLine`
|
|
32939
|
+
Set a breakpoint by url substring + line, and optionally column; both are 1-based.
|
|
32940
|
+
A condition limits how often the UI freezes; a line alone can bind somewhere that never
|
|
32941
|
+
runs again, so check the resolved location the result reports back.
|
|
32942
|
+
Hitting it FREEZES the UI until you resume with game_debug_step.
|
|
31661
32943
|
`,
|
|
31662
32944
|
inputSchema: {
|
|
31663
|
-
urlContains: exports_external.string().describe(
|
|
31664
|
-
|
|
31665
|
-
|
|
31666
|
-
|
|
32945
|
+
urlContains: exports_external.string().describe(import_common_tags5.oneLine`
|
|
32946
|
+
Substring of the script url to break in, matched case-sensitively; copy it from the url
|
|
32947
|
+
game_debug_scripts or game_debug_search_source printed, whose own filters are not.
|
|
32948
|
+
`),
|
|
32949
|
+
line: exports_external.number().int().min(1).describe(`Line number (1-based)`),
|
|
32950
|
+
column: exports_external.number().int().min(1).optional().describe(`Column (1-based), optional; take one from game_debug_search_source`),
|
|
32951
|
+
condition: exports_external.string().optional().describe(`Optional JS condition; pause only when it evaluates truthy`)
|
|
31667
32952
|
}
|
|
31668
32953
|
}, ({ urlContains, line, column, condition }) => debug.setBreakpoint(urlContains, line, column, condition));
|
|
31669
32954
|
server.registerTool("game_debug_remove_breakpoint", {
|
|
31670
|
-
title:
|
|
31671
|
-
description:
|
|
32955
|
+
title: `Remove a breakpoint`,
|
|
32956
|
+
description: `Remove a breakpoint by its id (from game_debug_status), or pass 'all'.`,
|
|
31672
32957
|
inputSchema: {
|
|
31673
|
-
breakpoint: exports_external.string().describe(
|
|
32958
|
+
breakpoint: exports_external.string().describe(`Breakpoint id, or 'all'`)
|
|
31674
32959
|
}
|
|
31675
32960
|
}, ({ breakpoint }) => debug.removeBreakpoint(breakpoint));
|
|
31676
32961
|
server.registerTool("game_debug_pause_state", {
|
|
31677
|
-
title:
|
|
31678
|
-
description:
|
|
31679
|
-
When paused, return the call stack (frames with function + location + scope types)
|
|
31680
|
-
|
|
31681
|
-
otherwise.
|
|
32962
|
+
title: `Inspect the paused stack`,
|
|
32963
|
+
description: import_common_tags5.oneLine`
|
|
32964
|
+
When paused, return the call stack (frames with function + location + scope types);
|
|
32965
|
+
'not paused' otherwise.
|
|
31682
32966
|
`,
|
|
31683
32967
|
inputSchema: {
|
|
31684
|
-
expandScopes: exports_external.boolean().optional().describe(
|
|
32968
|
+
expandScopes: exports_external.boolean().optional().describe(`Also list local/closure variables per frame (default false)`)
|
|
31685
32969
|
}
|
|
31686
32970
|
}, ({ expandScopes }) => debug.pauseStateReport(expandScopes ?? false));
|
|
31687
32971
|
server.registerTool("game_debug_evaluate", {
|
|
31688
|
-
title:
|
|
31689
|
-
description:
|
|
31690
|
-
Evaluate a JS expression.
|
|
31691
|
-
|
|
31692
|
-
|
|
32972
|
+
title: `Evaluate while debugging`,
|
|
32973
|
+
description: import_common_tags5.oneLine`
|
|
32974
|
+
Evaluate a JS expression.
|
|
32975
|
+
When paused, it runs in the selected call frame's scope so you can read locals; otherwise
|
|
32976
|
+
it runs globally.
|
|
32977
|
+
Prefer this over game_eval while paused.
|
|
31693
32978
|
`,
|
|
31694
32979
|
inputSchema: {
|
|
31695
|
-
expression: exports_external.string().describe(
|
|
31696
|
-
frameIndex: exports_external.number().int().min(0).optional().describe(
|
|
32980
|
+
expression: exports_external.string().describe(`JS expression to evaluate`),
|
|
32981
|
+
frameIndex: exports_external.number().int().min(0).optional().describe(`Call frame index to evaluate in when paused (default 0 = top)`)
|
|
31697
32982
|
}
|
|
31698
32983
|
}, ({ expression, frameIndex }) => debug.evaluate(expression, frameIndex));
|
|
31699
32984
|
server.registerTool("game_debug_step", {
|
|
31700
|
-
title:
|
|
31701
|
-
description:
|
|
31702
|
-
Control paused execution: resume (unfreeze the UI), over
|
|
31703
|
-
|
|
32985
|
+
title: `Step / resume / pause execution`,
|
|
32986
|
+
description: import_common_tags5.oneLine`
|
|
32987
|
+
Control paused execution: resume (unfreeze the UI), over/into/out (step), or pause (break at
|
|
32988
|
+
the next statement).
|
|
32989
|
+
Stepping reports the new location, and resume fails rather than claiming success when the
|
|
32990
|
+
UI is paused again the moment it continues (a breakpoint on the resumed path).
|
|
31704
32991
|
`,
|
|
31705
32992
|
inputSchema: {
|
|
31706
|
-
action: exports_external.enum(["resume", "over", "into", "out", "pause"]).describe(
|
|
32993
|
+
action: exports_external.enum(["resume", "over", "into", "out", "pause"]).describe(`resume | over | into | out | pause`)
|
|
31707
32994
|
}
|
|
31708
32995
|
}, ({ action }) => debug.step(action));
|
|
31709
32996
|
const transport = new StdioServerTransport;
|