@csmodding/gameface-devtools-mcp 0.3.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 +36 -30
- package/dist/server.mjs +972 -268
- 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);
|
|
@@ -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) {
|
|
@@ -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
|
}
|
|
@@ -30676,6 +31179,12 @@ class DebuggerSession {
|
|
|
30676
31179
|
this.scripts.clear();
|
|
30677
31180
|
});
|
|
30678
31181
|
}
|
|
31182
|
+
get pause() {
|
|
31183
|
+
if (!this.paused) {
|
|
31184
|
+
return;
|
|
31185
|
+
}
|
|
31186
|
+
return { reason: this.paused.reason, location: this.topLocation() };
|
|
31187
|
+
}
|
|
30679
31188
|
async status(setPause) {
|
|
30680
31189
|
try {
|
|
30681
31190
|
await this.ensureReady();
|
|
@@ -30687,8 +31196,9 @@ class DebuggerSession {
|
|
|
30687
31196
|
id: bp.id,
|
|
30688
31197
|
urlContains: bp.urlContains,
|
|
30689
31198
|
line: bp.lineNumber + 1,
|
|
31199
|
+
column: bp.columnNumber == null ? undefined : bp.columnNumber + 1,
|
|
30690
31200
|
condition: bp.condition,
|
|
30691
|
-
|
|
31201
|
+
resolvedLocations: bp.resolved
|
|
30692
31202
|
}));
|
|
30693
31203
|
return text(JSON.stringify({
|
|
30694
31204
|
enabled: true,
|
|
@@ -30709,49 +31219,87 @@ class DebuggerSession {
|
|
|
30709
31219
|
async listScripts(filter) {
|
|
30710
31220
|
try {
|
|
30711
31221
|
await this.ensureReady();
|
|
30712
|
-
const
|
|
30713
|
-
let scripts = [...this.scripts.values()];
|
|
30714
|
-
if (needle) {
|
|
30715
|
-
scripts = scripts.filter((script) => script.url.toLowerCase().includes(needle));
|
|
30716
|
-
}
|
|
30717
|
-
scripts.sort((a, b) => a.url.localeCompare(b.url));
|
|
31222
|
+
const scripts = this.filteredScripts(filter);
|
|
30718
31223
|
const cap = 120;
|
|
30719
31224
|
const shown = scripts.slice(0, cap).map((script) => ({
|
|
30720
31225
|
scriptId: script.scriptId,
|
|
30721
31226
|
url: script.url || "(anonymous)",
|
|
30722
|
-
lines: script.endLine
|
|
31227
|
+
lines: script.endLine - script.startLine + 1,
|
|
31228
|
+
firstLine: script.startLine == 0 ? undefined : script.startLine + 1
|
|
30723
31229
|
}));
|
|
30724
31230
|
return text(JSON.stringify({
|
|
30725
31231
|
total: scripts.length,
|
|
30726
31232
|
shown: shown.length,
|
|
30727
31233
|
truncated: scripts.length > cap,
|
|
30728
|
-
scripts: shown
|
|
31234
|
+
scripts: shown,
|
|
31235
|
+
note: this.listNote(scripts.length)
|
|
30729
31236
|
}, null, 2));
|
|
30730
31237
|
} catch (error51) {
|
|
30731
31238
|
return toErrorResult(error51);
|
|
30732
31239
|
}
|
|
30733
31240
|
}
|
|
30734
|
-
async
|
|
31241
|
+
async searchSource(query, urlContains) {
|
|
30735
31242
|
try {
|
|
30736
31243
|
await this.ensureReady();
|
|
30737
|
-
const
|
|
30738
|
-
|
|
30739
|
-
|
|
30740
|
-
|
|
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);
|
|
30741
31266
|
if (source == null) {
|
|
30742
31267
|
return errorText(`No source for scriptId ${scriptId}.`);
|
|
30743
31268
|
}
|
|
30744
31269
|
const lines = source.split(`
|
|
30745
31270
|
`);
|
|
30746
|
-
const
|
|
31271
|
+
const offset = this.scripts.get(scriptId)?.startLine ?? 0;
|
|
31272
|
+
const first = offset + 1;
|
|
31273
|
+
const last = offset + lines.length;
|
|
30747
31274
|
const cap = 400;
|
|
30748
|
-
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);
|
|
30749
31282
|
const padWidth = 5;
|
|
30750
|
-
const
|
|
31283
|
+
const rendered = lines.slice(from - first, to - offset).map((line, index) => `${String(from + index).padStart(padWidth)} ${line}`).join(`
|
|
30751
31284
|
`);
|
|
30752
|
-
const
|
|
30753
|
-
|
|
30754
|
-
|
|
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);
|
|
30755
31303
|
} catch (error51) {
|
|
30756
31304
|
return toErrorResult(error51);
|
|
30757
31305
|
}
|
|
@@ -30760,34 +31308,42 @@ class DebuggerSession {
|
|
|
30760
31308
|
try {
|
|
30761
31309
|
await this.ensureReady();
|
|
30762
31310
|
const lineNumber = Math.max(0, line - 1);
|
|
31311
|
+
const columnNumber = column == null ? undefined : Math.max(0, column - 1);
|
|
30763
31312
|
const urlRegex = escapeRegex2(urlContains);
|
|
30764
|
-
const res = await this.client.call("Debugger.setBreakpointByUrl", { urlRegex, lineNumber, columnNumber
|
|
31313
|
+
const res = await this.client.call("Debugger.setBreakpointByUrl", { urlRegex, lineNumber, columnNumber, condition });
|
|
30765
31314
|
const id = this.nextBpId++;
|
|
30766
31315
|
const locations = res.locations ?? [];
|
|
31316
|
+
const resolved = locations.map((loc) => this.locStr(loc));
|
|
30767
31317
|
this.breakpoints.set(id, {
|
|
30768
31318
|
id,
|
|
30769
31319
|
urlContains,
|
|
30770
31320
|
urlRegex,
|
|
30771
31321
|
lineNumber,
|
|
30772
|
-
columnNumber
|
|
31322
|
+
columnNumber,
|
|
30773
31323
|
condition,
|
|
30774
31324
|
cdpId: res.breakpointId,
|
|
30775
|
-
|
|
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;
|
|
30776
31330
|
});
|
|
30777
31331
|
return text(JSON.stringify({
|
|
30778
31332
|
id,
|
|
30779
31333
|
urlContains,
|
|
30780
31334
|
line,
|
|
30781
|
-
|
|
30782
|
-
|
|
31335
|
+
column,
|
|
31336
|
+
condition,
|
|
31337
|
+
resolvedLocations: resolved,
|
|
30783
31338
|
pending: locations.length == 0,
|
|
30784
|
-
note: locations.length == 0 ?
|
|
31339
|
+
note: locations.length == 0 ? import_common_tags3.oneLine`
|
|
30785
31340
|
Pending: no matching script/line loaded yet, or the line has no code.
|
|
30786
31341
|
It will bind when the script loads.
|
|
30787
|
-
` :
|
|
31342
|
+
` : import_common_tags3.oneLine`
|
|
30788
31343
|
Hitting this breakpoint FREEZES the UI until you resume
|
|
30789
31344
|
(game_debug_step resume).
|
|
30790
|
-
|
|
31345
|
+
`,
|
|
31346
|
+
hint: singleLine ? SINGLE_LINE_HINT : undefined
|
|
30791
31347
|
}, null, 2));
|
|
30792
31348
|
} catch (error51) {
|
|
30793
31349
|
return toErrorResult(error51);
|
|
@@ -30846,7 +31402,7 @@ class DebuggerSession {
|
|
|
30846
31402
|
if (this.paused) {
|
|
30847
31403
|
const frame = this.paused.callFrames[frameIndex];
|
|
30848
31404
|
if (!frame) {
|
|
30849
|
-
return errorText(
|
|
31405
|
+
return errorText(import_common_tags3.oneLine`
|
|
30850
31406
|
No call frame at index ${frameIndex}
|
|
30851
31407
|
(paused stack has ${this.paused.callFrames.length}).
|
|
30852
31408
|
`);
|
|
@@ -30893,9 +31449,15 @@ class DebuggerSession {
|
|
|
30893
31449
|
}
|
|
30894
31450
|
if (action == "resume") {
|
|
30895
31451
|
await this.client.call("Debugger.resume");
|
|
30896
|
-
|
|
30897
|
-
|
|
30898
|
-
|
|
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
|
+
`);
|
|
30899
31461
|
}
|
|
30900
31462
|
return text(`Resumed (UI unfrozen).`);
|
|
30901
31463
|
}
|
|
@@ -30922,7 +31484,7 @@ class DebuggerSession {
|
|
|
30922
31484
|
condition: bp.condition
|
|
30923
31485
|
});
|
|
30924
31486
|
bp.cdpId = res.breakpointId;
|
|
30925
|
-
bp.
|
|
31487
|
+
bp.resolved = (res.locations ?? []).map((loc) => this.locStr(loc));
|
|
30926
31488
|
} catch {}
|
|
30927
31489
|
}
|
|
30928
31490
|
}
|
|
@@ -30932,6 +31494,7 @@ class DebuggerSession {
|
|
|
30932
31494
|
scriptId: params.scriptId,
|
|
30933
31495
|
url: params.url || "",
|
|
30934
31496
|
startLine: params.startLine ?? 0,
|
|
31497
|
+
startColumn: params.startColumn ?? 0,
|
|
30935
31498
|
endLine: params.endLine ?? 0,
|
|
30936
31499
|
length: params.length
|
|
30937
31500
|
});
|
|
@@ -30944,13 +31507,79 @@ class DebuggerSession {
|
|
|
30944
31507
|
this.pausedSeq++;
|
|
30945
31508
|
} else if (method == "Debugger.resumed") {
|
|
30946
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);
|
|
30947
31523
|
}
|
|
30948
31524
|
}
|
|
30949
31525
|
async ensureReady() {
|
|
30950
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) {
|
|
30951
31532
|
if (this.scripts.size == 0) {
|
|
30952
|
-
|
|
31533
|
+
return EMPTY_SCRIPT_MAP_NOTE;
|
|
31534
|
+
}
|
|
31535
|
+
return selected == 0 ? FILTER_MISS_LIST_NOTE : undefined;
|
|
31536
|
+
}
|
|
31537
|
+
missNote(scriptsSearched, unreadable) {
|
|
31538
|
+
if (this.scripts.size == 0) {
|
|
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
|
+
}
|
|
30953
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;
|
|
30954
31583
|
}
|
|
30955
31584
|
async removeCdpBreakpoint(bp) {
|
|
30956
31585
|
if (bp.cdpId == null) {
|
|
@@ -30978,10 +31607,13 @@ class DebuggerSession {
|
|
|
30978
31607
|
}
|
|
30979
31608
|
return variables;
|
|
30980
31609
|
}
|
|
30981
|
-
|
|
31610
|
+
waitForNewPause(beforeSeq, timeoutMs) {
|
|
31611
|
+
return this.waitUntil(() => this.pausedSeq > beforeSeq, timeoutMs);
|
|
31612
|
+
}
|
|
31613
|
+
async waitUntil(done, timeoutMs) {
|
|
30982
31614
|
const deadline = Date.now() + timeoutMs;
|
|
30983
31615
|
while (Date.now() < deadline) {
|
|
30984
|
-
if (
|
|
31616
|
+
if (done()) {
|
|
30985
31617
|
return true;
|
|
30986
31618
|
}
|
|
30987
31619
|
await sleep(POLL_INTERVAL_MS);
|
|
@@ -30990,8 +31622,8 @@ class DebuggerSession {
|
|
|
30990
31622
|
}
|
|
30991
31623
|
locStr(loc) {
|
|
30992
31624
|
const url2 = this.scripts.get(loc.scriptId)?.url ?? "";
|
|
30993
|
-
const label = url2.length > 0 ? url2 : loc.scriptId
|
|
30994
|
-
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}`;
|
|
30995
31627
|
}
|
|
30996
31628
|
topLocation() {
|
|
30997
31629
|
const frame = this.paused?.callFrames[0];
|
|
@@ -31001,9 +31633,39 @@ class DebuggerSession {
|
|
|
31001
31633
|
return `${frame.functionName || "(anonymous)"} at ${this.locStr(frame.location)}`;
|
|
31002
31634
|
}
|
|
31003
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
|
+
}
|
|
31004
31666
|
|
|
31005
31667
|
// src/tools.ts
|
|
31006
|
-
var
|
|
31668
|
+
var import_common_tags4 = __toESM(require_lib(), 1);
|
|
31007
31669
|
import { setTimeout as sleep2 } from "node:timers/promises";
|
|
31008
31670
|
var POLL_INTERVAL_MS2 = 150;
|
|
31009
31671
|
var MAX_WAIT_MS = 60000;
|
|
@@ -31011,8 +31673,7 @@ var DEFAULT_WAIT_TIMEOUT_MS = 8000;
|
|
|
31011
31673
|
var DEFAULT_RELOAD_WAIT_TIMEOUT_MS = 30000;
|
|
31012
31674
|
var DEFAULT_QUIESCENT_MS = 1000;
|
|
31013
31675
|
var DEFAULT_JPEG_QUALITY = 80;
|
|
31014
|
-
var
|
|
31015
|
-
var DEFAULT_FIND_LIMIT = 20;
|
|
31676
|
+
var DEFAULT_QUERY_LIMIT = 20;
|
|
31016
31677
|
async function gameStatus(client, reloads) {
|
|
31017
31678
|
const { host, port } = client.config;
|
|
31018
31679
|
try {
|
|
@@ -31052,7 +31713,7 @@ async function gameStatus(client, reloads) {
|
|
|
31052
31713
|
reachable: false,
|
|
31053
31714
|
endpoint: `http://${host}:${port}`,
|
|
31054
31715
|
error: error51 instanceof Error ? error51.message : String(error51),
|
|
31055
|
-
hint:
|
|
31716
|
+
hint: import_common_tags4.oneLine`
|
|
31056
31717
|
Launch the Gameface application with its CDP debug port open, then retry.
|
|
31057
31718
|
Override host/port via GAMEFACE_HOST / GAMEFACE_PORT.
|
|
31058
31719
|
`
|
|
@@ -31075,9 +31736,22 @@ async function gameEval(client, expression, awaitPromise = false) {
|
|
|
31075
31736
|
return toErrorResult(error51);
|
|
31076
31737
|
}
|
|
31077
31738
|
}
|
|
31078
|
-
async function gameScreenshot(client, options = {}) {
|
|
31079
|
-
const { format = "
|
|
31739
|
+
async function gameScreenshot(client, debug, options = {}) {
|
|
31740
|
+
const { format = "png", quality, selector, index = 0 } = options;
|
|
31080
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
|
+
}
|
|
31081
31755
|
await client.ensureDomain("Page");
|
|
31082
31756
|
const params = { format };
|
|
31083
31757
|
if (format == "jpeg") {
|
|
@@ -31091,7 +31765,7 @@ async function gameScreenshot(client, options = {}) {
|
|
|
31091
31765
|
});
|
|
31092
31766
|
const rect = rectRes.result.value;
|
|
31093
31767
|
if (!rect?.found) {
|
|
31094
|
-
return errorText(
|
|
31768
|
+
return errorText(import_common_tags4.oneLine`
|
|
31095
31769
|
No element matched ${JSON.stringify(selector)} for game_screenshot at index ${index}
|
|
31096
31770
|
(matches found: ${rect?.count ?? 0}).
|
|
31097
31771
|
`);
|
|
@@ -31100,7 +31774,7 @@ async function gameScreenshot(client, options = {}) {
|
|
|
31100
31774
|
return errorText(`Element ${JSON.stringify(selector)} has a zero-size box; nothing to capture.`);
|
|
31101
31775
|
}
|
|
31102
31776
|
params.clip = { x: rect.x, y: rect.y, width: rect.width, height: rect.height, scale: 1 };
|
|
31103
|
-
caption =
|
|
31777
|
+
caption = import_common_tags4.oneLine`
|
|
31104
31778
|
Clipped to ${JSON.stringify(selector)} [index ${index}]. Matches: ${rect.count}.
|
|
31105
31779
|
`;
|
|
31106
31780
|
}
|
|
@@ -31136,44 +31810,86 @@ async function gameDom(client, selector, all = false, maxHtml = 4000) {
|
|
|
31136
31810
|
return toErrorResult(error51);
|
|
31137
31811
|
}
|
|
31138
31812
|
}
|
|
31139
|
-
async function
|
|
31140
|
-
const
|
|
31141
|
-
|
|
31142
|
-
|
|
31143
|
-
|
|
31144
|
-
|
|
31145
|
-
|
|
31146
|
-
|
|
31147
|
-
|
|
31148
|
-
|
|
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
|
+
};
|
|
31149
31831
|
try {
|
|
31150
31832
|
const res = await client.call("Runtime.evaluate", {
|
|
31151
|
-
expression: callPageFn(
|
|
31152
|
-
sel: selector,
|
|
31153
|
-
needle,
|
|
31154
|
-
mode: match,
|
|
31155
|
-
caseSensitive,
|
|
31156
|
-
deepest,
|
|
31157
|
-
tag,
|
|
31158
|
-
limit
|
|
31159
|
-
}),
|
|
31833
|
+
expression: callPageFn(queryFn, args),
|
|
31160
31834
|
returnByValue: true
|
|
31161
31835
|
});
|
|
31162
31836
|
if (res.exceptionDetails) {
|
|
31163
|
-
return errorText(`
|
|
31837
|
+
return errorText(`Query failed: ${formatException(res.exceptionDetails)}`);
|
|
31164
31838
|
}
|
|
31165
31839
|
const value = res.result.value;
|
|
31166
31840
|
if (!value) {
|
|
31167
|
-
return errorText(`
|
|
31841
|
+
return errorText(`game_query returned no result for selector ${JSON.stringify(args.sel)}.`);
|
|
31168
31842
|
}
|
|
31169
31843
|
if ("error" in value) {
|
|
31170
|
-
return errorText(`
|
|
31844
|
+
return errorText(`game_query: ${value.error}`);
|
|
31171
31845
|
}
|
|
31172
|
-
|
|
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));
|
|
31173
31853
|
} catch (error51) {
|
|
31174
31854
|
return toErrorResult(error51);
|
|
31175
31855
|
}
|
|
31176
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
|
+
}
|
|
31177
31893
|
async function gameClick(client, selector, index = 0) {
|
|
31178
31894
|
try {
|
|
31179
31895
|
const res = await client.call("Runtime.evaluate", {
|
|
@@ -31185,12 +31901,12 @@ async function gameClick(client, selector, index = 0) {
|
|
|
31185
31901
|
}
|
|
31186
31902
|
const info = res.result.value;
|
|
31187
31903
|
if (!info?.found) {
|
|
31188
|
-
return errorText(
|
|
31904
|
+
return errorText(import_common_tags4.oneLine`
|
|
31189
31905
|
No element to click for selector ${JSON.stringify(selector)} at index ${index}
|
|
31190
31906
|
(matches found: ${info?.count ?? 0}).
|
|
31191
31907
|
`);
|
|
31192
31908
|
}
|
|
31193
|
-
return text(
|
|
31909
|
+
return text(import_common_tags4.oneLine`
|
|
31194
31910
|
Clicked ${JSON.stringify(selector)} [index ${index}] at
|
|
31195
31911
|
(${info.x.toFixed(0)}, ${info.y.toFixed(0)}).
|
|
31196
31912
|
Dispatched: ${info.fired.join(", ")}. Matches: ${info.count}.
|
|
@@ -31199,7 +31915,7 @@ async function gameClick(client, selector, index = 0) {
|
|
|
31199
31915
|
return toErrorResult(error51);
|
|
31200
31916
|
}
|
|
31201
31917
|
}
|
|
31202
|
-
async function gameWait(client, reloads, options) {
|
|
31918
|
+
async function gameWait(client, reloads, debug, options) {
|
|
31203
31919
|
const {
|
|
31204
31920
|
selector,
|
|
31205
31921
|
predicate,
|
|
@@ -31232,12 +31948,16 @@ async function gameWait(client, reloads, options) {
|
|
|
31232
31948
|
} catch (error51) {
|
|
31233
31949
|
return toErrorResult(error51);
|
|
31234
31950
|
}
|
|
31951
|
+
function timedOut(reason) {
|
|
31952
|
+
const paused = pausedNote(debug);
|
|
31953
|
+
return errorText(paused.length > 0 ? `${reason} ${paused}` : reason);
|
|
31954
|
+
}
|
|
31235
31955
|
async function waitForReload() {
|
|
31236
31956
|
await client.connection();
|
|
31237
31957
|
const baseline = sinceReloads ?? reloads.count;
|
|
31238
31958
|
while (reloads.count <= baseline) {
|
|
31239
31959
|
if (Date.now() >= deadline) {
|
|
31240
|
-
return
|
|
31960
|
+
return timedOut(import_common_tags4.oneLine`
|
|
31241
31961
|
Timed out after ${budget}ms waiting for a view reload
|
|
31242
31962
|
(reload count still ${reloads.count}, baseline ${baseline}).
|
|
31243
31963
|
`);
|
|
@@ -31249,9 +31969,9 @@ async function gameWait(client, reloads, options) {
|
|
|
31249
31969
|
let quietSince = Date.now();
|
|
31250
31970
|
while (Date.now() - quietSince < quiescentMs) {
|
|
31251
31971
|
if (Date.now() >= deadline) {
|
|
31252
|
-
return
|
|
31253
|
-
Timed out after ${budget}ms: reload observed, but context swaps kept arriving within
|
|
31254
|
-
${quiescentMs}ms quiescence window.
|
|
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.
|
|
31255
31975
|
`);
|
|
31256
31976
|
}
|
|
31257
31977
|
await sleep2(POLL_INTERVAL_MS2);
|
|
@@ -31283,7 +32003,7 @@ async function gameWait(client, reloads, options) {
|
|
|
31283
32003
|
if (Date.now() >= deadline) {
|
|
31284
32004
|
const what = selector ? `selector ${JSON.stringify(selector)}` : "predicate";
|
|
31285
32005
|
const errorNote = lastError ? ` Last predicate error: ${lastError}` : "";
|
|
31286
|
-
return
|
|
32006
|
+
return timedOut(`Timed out after ${budget}ms waiting for ${what}.${errorNote}`);
|
|
31287
32007
|
}
|
|
31288
32008
|
await sleep2(POLL_INTERVAL_MS2);
|
|
31289
32009
|
}
|
|
@@ -31300,12 +32020,12 @@ async function gameFill(client, selector, value, index = 0) {
|
|
|
31300
32020
|
}
|
|
31301
32021
|
const info = res.result.value;
|
|
31302
32022
|
if (!info?.found) {
|
|
31303
|
-
return errorText(
|
|
32023
|
+
return errorText(import_common_tags4.oneLine`
|
|
31304
32024
|
No element matched ${JSON.stringify(selector)} for game_fill at index ${index}
|
|
31305
32025
|
(matches found: ${info?.count ?? 0}).
|
|
31306
32026
|
`);
|
|
31307
32027
|
}
|
|
31308
|
-
return text(
|
|
32028
|
+
return text(import_common_tags4.oneLine`
|
|
31309
32029
|
Filled ${JSON.stringify(selector)} [index ${index}] (${info.mode}).
|
|
31310
32030
|
Value is now ${JSON.stringify(info.value)}. Matches: ${info.count}.
|
|
31311
32031
|
`);
|
|
@@ -31324,12 +32044,12 @@ async function gameType(client, selector, textToType, index = 0) {
|
|
|
31324
32044
|
}
|
|
31325
32045
|
const info = res.result.value;
|
|
31326
32046
|
if (!info?.found) {
|
|
31327
|
-
return errorText(
|
|
32047
|
+
return errorText(import_common_tags4.oneLine`
|
|
31328
32048
|
No element matched ${JSON.stringify(selector)} for game_type at index ${index}
|
|
31329
32049
|
(matches found: ${info?.count ?? 0}).
|
|
31330
32050
|
`);
|
|
31331
32051
|
}
|
|
31332
|
-
return text(
|
|
32052
|
+
return text(import_common_tags4.oneLine`
|
|
31333
32053
|
Typed ${info.typed} char(s) into ${JSON.stringify(selector)} [index ${index}].
|
|
31334
32054
|
Value is now ${JSON.stringify(info.value)}. Matches: ${info.count}.
|
|
31335
32055
|
`);
|
|
@@ -31348,12 +32068,12 @@ async function gameHover(client, selector, index = 0) {
|
|
|
31348
32068
|
}
|
|
31349
32069
|
const info = res.result.value;
|
|
31350
32070
|
if (!info?.found) {
|
|
31351
|
-
return errorText(
|
|
32071
|
+
return errorText(import_common_tags4.oneLine`
|
|
31352
32072
|
No element matched ${JSON.stringify(selector)} for game_hover at index ${index}
|
|
31353
32073
|
(matches found: ${info?.count ?? 0}).
|
|
31354
32074
|
`);
|
|
31355
32075
|
}
|
|
31356
|
-
return text(
|
|
32076
|
+
return text(import_common_tags4.oneLine`
|
|
31357
32077
|
Hovered ${JSON.stringify(selector)} [index ${index}] at
|
|
31358
32078
|
(${info.x.toFixed(0)}, ${info.y.toFixed(0)}).
|
|
31359
32079
|
Dispatched: ${info.fired.join(", ")}. Matches: ${info.count}.
|
|
@@ -31395,14 +32115,14 @@ async function gameKey(client, options) {
|
|
|
31395
32115
|
return errorText(`game_key returned no result.`);
|
|
31396
32116
|
}
|
|
31397
32117
|
if (!info.found) {
|
|
31398
|
-
return errorText(
|
|
32118
|
+
return errorText(import_common_tags4.oneLine`
|
|
31399
32119
|
No element matched ${JSON.stringify(selector)} for game_key at index ${index}
|
|
31400
32120
|
(matches found: ${info.count}).
|
|
31401
32121
|
`);
|
|
31402
32122
|
}
|
|
31403
32123
|
const where = info.via == "selector" ? `${JSON.stringify(selector)} [index ${index}] ${info.target}` : info.via == "activeElement" ? `the focused element ${info.target}` : `document`;
|
|
31404
32124
|
const matchNote = info.matches == null ? "" : ` Matches: ${info.matches}.`;
|
|
31405
|
-
return text(
|
|
32125
|
+
return text(import_common_tags4.oneLine`
|
|
31406
32126
|
Pressed ${keyLabel(key, { ctrl, shift, alt, meta: meta3 })} ${info.presses}x on ${where}.${matchNote}
|
|
31407
32127
|
Default prevented: ${info.defaultPrevented ? "yes" : "no"}.
|
|
31408
32128
|
`);
|
|
@@ -31479,87 +32199,6 @@ class ReloadTracker {
|
|
|
31479
32199
|
}
|
|
31480
32200
|
}
|
|
31481
32201
|
}
|
|
31482
|
-
|
|
31483
|
-
class ConsoleBuffer {
|
|
31484
|
-
entries = [];
|
|
31485
|
-
max;
|
|
31486
|
-
constructor(client, reloads, max = 500) {
|
|
31487
|
-
this.max = max;
|
|
31488
|
-
client.onConnect(async (conn) => {
|
|
31489
|
-
await conn.ensureDomain("Runtime");
|
|
31490
|
-
await conn.ensureDomain("Log");
|
|
31491
|
-
});
|
|
31492
|
-
client.onEvent((method, params) => {
|
|
31493
|
-
this.handle(method, params);
|
|
31494
|
-
});
|
|
31495
|
-
reloads.onReload((count) => {
|
|
31496
|
-
this.push({
|
|
31497
|
-
ts: Date.now(),
|
|
31498
|
-
kind: "reload",
|
|
31499
|
-
level: "info",
|
|
31500
|
-
text: `view reloaded (#${count})`
|
|
31501
|
-
});
|
|
31502
|
-
});
|
|
31503
|
-
}
|
|
31504
|
-
read(limit, level, clear) {
|
|
31505
|
-
const filtered = level ? this.entries.filter((entry) => entry.level == level) : this.entries;
|
|
31506
|
-
const out = filtered.slice(-limit);
|
|
31507
|
-
if (clear) {
|
|
31508
|
-
this.entries.length = 0;
|
|
31509
|
-
}
|
|
31510
|
-
return out;
|
|
31511
|
-
}
|
|
31512
|
-
push(entry) {
|
|
31513
|
-
this.entries.push(entry);
|
|
31514
|
-
if (this.entries.length > this.max) {
|
|
31515
|
-
this.entries.splice(0, this.entries.length - this.max);
|
|
31516
|
-
}
|
|
31517
|
-
}
|
|
31518
|
-
handle(method, params) {
|
|
31519
|
-
if (method == "Runtime.consoleAPICalled") {
|
|
31520
|
-
const args = (params.args ?? []).map((arg) => valToStr(describeRemoteObject(arg)));
|
|
31521
|
-
this.push({
|
|
31522
|
-
ts: params.timestamp ?? 0,
|
|
31523
|
-
kind: "console",
|
|
31524
|
-
level: params.type ?? "log",
|
|
31525
|
-
text: args.join(" ")
|
|
31526
|
-
});
|
|
31527
|
-
} else if (method == "Log.entryAdded") {
|
|
31528
|
-
const entry = params.entry ?? {};
|
|
31529
|
-
this.push({
|
|
31530
|
-
ts: entry.timestamp ?? 0,
|
|
31531
|
-
kind: entry.source ?? "log",
|
|
31532
|
-
level: entry.level ?? "info",
|
|
31533
|
-
text: entry.text ?? ""
|
|
31534
|
-
});
|
|
31535
|
-
} else if (method == "Runtime.exceptionThrown") {
|
|
31536
|
-
this.push({
|
|
31537
|
-
ts: params.timestamp ?? 0,
|
|
31538
|
-
kind: "exception",
|
|
31539
|
-
level: "error",
|
|
31540
|
-
text: formatException(params.exceptionDetails)
|
|
31541
|
-
});
|
|
31542
|
-
}
|
|
31543
|
-
}
|
|
31544
|
-
}
|
|
31545
|
-
async function gameConsole(client, buffer, options) {
|
|
31546
|
-
const { limit = DEFAULT_CONSOLE_LIMIT, level, clear = false } = options;
|
|
31547
|
-
try {
|
|
31548
|
-
await client.connection();
|
|
31549
|
-
} catch (error51) {
|
|
31550
|
-
return toErrorResult(error51);
|
|
31551
|
-
}
|
|
31552
|
-
const entries = buffer.read(limit, level, clear);
|
|
31553
|
-
if (entries.length == 0) {
|
|
31554
|
-
return text(import_common_tags3.oneLine`
|
|
31555
|
-
No console entries captured yet.
|
|
31556
|
-
Capture begins once the server connects to the application;
|
|
31557
|
-
trigger some UI activity (or a game_eval console.log) and retry.
|
|
31558
|
-
`);
|
|
31559
|
-
}
|
|
31560
|
-
return text(entries.map((entry) => `[${entry.level}] (${entry.kind}) ${entry.text}`).join(`
|
|
31561
|
-
`));
|
|
31562
|
-
}
|
|
31563
32202
|
function callPageFn(fn, ...args) {
|
|
31564
32203
|
const serialisedArgs = args.map((arg) => JSON.stringify(arg)).join(", ");
|
|
31565
32204
|
return `(${fn.toString()})(${serialisedArgs})`;
|
|
@@ -31592,12 +32231,12 @@ function collectDomFn(sel, all, maxHtml) {
|
|
|
31592
32231
|
};
|
|
31593
32232
|
}
|
|
31594
32233
|
}
|
|
31595
|
-
function
|
|
31596
|
-
const { sel, needle, mode, caseSensitive, deepest, tag, limit } = args;
|
|
32234
|
+
function queryFn(args) {
|
|
32235
|
+
const { sel, needle, mode, caseSensitive, deepest, tag, attributes, limit } = args;
|
|
31597
32236
|
const SNIPPET_MAX = 100;
|
|
31598
|
-
const target = caseSensitive ? needle : needle.toLowerCase();
|
|
32237
|
+
const target = needle == null ? "" : caseSensitive ? needle : needle.toLowerCase();
|
|
31599
32238
|
let regex;
|
|
31600
|
-
if (mode == "regex") {
|
|
32239
|
+
if (needle != null && mode == "regex") {
|
|
31601
32240
|
try {
|
|
31602
32241
|
regex = new RegExp(needle, caseSensitive ? "" : "i");
|
|
31603
32242
|
} catch (error51) {
|
|
@@ -31606,18 +32245,18 @@ function findFn(args) {
|
|
|
31606
32245
|
}
|
|
31607
32246
|
const found = [];
|
|
31608
32247
|
for (const el of Array.from(document.querySelectorAll(sel))) {
|
|
31609
|
-
if (matches((el.textContent || "").trim())) {
|
|
32248
|
+
if (needle == null || matches((el.textContent || "").trim())) {
|
|
31610
32249
|
found.push(el);
|
|
31611
32250
|
}
|
|
31612
32251
|
}
|
|
31613
|
-
const kept = deepest ?
|
|
32252
|
+
const kept = deepest ? innermost(found) : found;
|
|
31614
32253
|
const chosen = kept.slice(0, limit);
|
|
31615
32254
|
if (tag) {
|
|
31616
|
-
for (const stale of Array.from(document.querySelectorAll("[data-gf-
|
|
31617
|
-
stale.removeAttribute("data-gf-
|
|
32255
|
+
for (const stale of Array.from(document.querySelectorAll("[data-gf-tag]"))) {
|
|
32256
|
+
stale.removeAttribute("data-gf-tag");
|
|
31618
32257
|
}
|
|
31619
32258
|
for (const [i, el] of chosen.entries()) {
|
|
31620
|
-
el.setAttribute("data-gf-
|
|
32259
|
+
el.setAttribute("data-gf-tag", String(i + 1));
|
|
31621
32260
|
}
|
|
31622
32261
|
}
|
|
31623
32262
|
return {
|
|
@@ -31627,6 +32266,15 @@ function findFn(args) {
|
|
|
31627
32266
|
tagged: tag,
|
|
31628
32267
|
elements: chosen.map((el, i) => describe3(el, i))
|
|
31629
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
|
+
}
|
|
31630
32278
|
function matches(raw) {
|
|
31631
32279
|
if (mode == "regex") {
|
|
31632
32280
|
return regex != null && regex.test(raw);
|
|
@@ -31650,8 +32298,17 @@ function findFn(args) {
|
|
|
31650
32298
|
text: truncated ? raw.slice(0, SNIPPET_MAX) : raw,
|
|
31651
32299
|
truncated
|
|
31652
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
|
+
}
|
|
31653
32310
|
if (tag) {
|
|
31654
|
-
info.selector = `[data-gf-
|
|
32311
|
+
info.selector = `[data-gf-tag="${i + 1}"]`;
|
|
31655
32312
|
}
|
|
31656
32313
|
return info;
|
|
31657
32314
|
}
|
|
@@ -31987,19 +32644,18 @@ async function main() {
|
|
|
31987
32644
|
const server = new McpServer({ name: "gameface-devtools-mcp", version: VERSION });
|
|
31988
32645
|
server.registerTool("game_status", {
|
|
31989
32646
|
title: `Gameface UI status`,
|
|
31990
|
-
description:
|
|
32647
|
+
description: import_common_tags5.oneLine`
|
|
31991
32648
|
Check whether the Gameface UI debug endpoint is reachable and report the live page target,
|
|
31992
|
-
engine info, and view-reload tracking
|
|
31993
|
-
Calling it arms reload tracking and returns the baseline count for game_wait's sinceReloads.
|
|
32649
|
+
engine info, and view-reload tracking.
|
|
31994
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.
|
|
31995
32652
|
`
|
|
31996
32653
|
}, () => gameStatus(client, reloads));
|
|
31997
32654
|
server.registerTool("game_eval", {
|
|
31998
32655
|
title: `Evaluate JS in the Gameface UI`,
|
|
31999
|
-
description:
|
|
32656
|
+
description: import_common_tags5.oneLine`
|
|
32000
32657
|
Evaluate a JavaScript expression in the running Gameface UI (CDP Runtime.evaluate,
|
|
32001
32658
|
returnByValue) and return the resulting value as JSON.
|
|
32002
|
-
Use document.querySelector and friends to read the live DOM, inspect state, or call UI APIs.
|
|
32003
32659
|
`,
|
|
32004
32660
|
inputSchema: {
|
|
32005
32661
|
expression: exports_external.string().describe(`JavaScript expression to evaluate in the page context`),
|
|
@@ -32008,51 +32664,56 @@ async function main() {
|
|
|
32008
32664
|
}, ({ expression, awaitPromise }) => gameEval(client, expression, awaitPromise));
|
|
32009
32665
|
server.registerTool("game_screenshot", {
|
|
32010
32666
|
title: `Screenshot the Gameface UI`,
|
|
32011
|
-
description:
|
|
32012
|
-
Capture a screenshot of the Gameface viewport
|
|
32013
|
-
|
|
32014
|
-
|
|
32015
|
-
|
|
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.
|
|
32016
32674
|
`,
|
|
32017
32675
|
inputSchema: {
|
|
32018
|
-
format: exports_external.enum(["png", "jpeg"]).optional().describe(`Image format (default:
|
|
32019
|
-
quality: exports_external.number().min(1).max(100).optional().describe(`
|
|
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
|
+
`),
|
|
32020
32681
|
selector: exports_external.string().optional().describe(`If set, clip the screenshot to this element's bounding box`),
|
|
32021
32682
|
index: exports_external.number().int().min(0).optional().describe(`Which match to clip to when several exist (default: 0)`)
|
|
32022
32683
|
}
|
|
32023
|
-
}, ({ format, quality, selector, index }) => gameScreenshot(client, { format, quality, selector, index }));
|
|
32684
|
+
}, ({ format, quality, selector, index }) => gameScreenshot(client, debug, { format, quality, selector, index }));
|
|
32024
32685
|
server.registerTool("game_wait", {
|
|
32025
32686
|
title: `Wait for a condition in the Gameface UI`,
|
|
32026
|
-
description:
|
|
32687
|
+
description: import_common_tags5.oneLine`
|
|
32027
32688
|
Wait until a CSS selector matches (optionally visible), a JS predicate becomes truthy,
|
|
32028
32689
|
and/or a view reload happens. Provide at least one of reload / selector / predicate
|
|
32029
32690
|
(selector and predicate are mutually exclusive).
|
|
32030
32691
|
With reload, the phases compose: reload first, then a quiescence window, then the
|
|
32031
32692
|
selector/predicate poll in the fresh context.
|
|
32032
|
-
|
|
32693
|
+
It stays usable while the JS debugger is paused, and a timeout says so when it was.
|
|
32033
32694
|
`,
|
|
32034
32695
|
inputSchema: {
|
|
32035
32696
|
selector: exports_external.string().optional().describe(`CSS selector to wait for`),
|
|
32036
32697
|
predicate: exports_external.string().optional().describe(`JS expression evaluated in the page; waits until it is truthy`),
|
|
32037
|
-
reload: exports_external.boolean().optional().describe(
|
|
32698
|
+
reload: exports_external.boolean().optional().describe(import_common_tags5.oneLine`
|
|
32038
32699
|
Wait for a view reload (context reset) before the selector/predicate phase.
|
|
32039
32700
|
Without sinceReloads, waits for the next reload after the call starts.
|
|
32040
32701
|
`),
|
|
32041
|
-
sinceReloads: exports_external.number().int().min(0).optional().describe(
|
|
32702
|
+
sinceReloads: exports_external.number().int().min(0).optional().describe(import_common_tags5.oneLine`
|
|
32042
32703
|
Baseline reload count (from a prior game_status or game_wait).
|
|
32043
32704
|
The reload phase is satisfied as soon as the count exceeds it, even if the reload
|
|
32044
32705
|
already happened; use it to avoid racing a reload you triggered yourself.
|
|
32045
32706
|
`),
|
|
32046
|
-
quiescentMs: exports_external.number().int().min(0).optional().describe(
|
|
32707
|
+
quiescentMs: exports_external.number().int().min(0).optional().describe(import_common_tags5.oneLine`
|
|
32047
32708
|
After a reload is observed, hold until no further context swap for this long (default
|
|
32048
32709
|
1000, 0 disables); absorbs engines that swap the context several times per reload.
|
|
32049
32710
|
`),
|
|
32050
|
-
timeoutMs: exports_external.number().int().min(0).optional().describe(
|
|
32711
|
+
timeoutMs: exports_external.number().int().min(0).optional().describe(import_common_tags5.oneLine`
|
|
32051
32712
|
Max time to wait in ms (default 8000, or 30000 when reload is set; capped at 60000)
|
|
32052
32713
|
`),
|
|
32053
32714
|
visible: exports_external.boolean().optional().describe(`For selector waits, also require a non-zero bounding box (default false)`)
|
|
32054
32715
|
}
|
|
32055
|
-
}, ({ selector, predicate, reload, sinceReloads, quiescentMs, timeoutMs, visible }) => gameWait(client, reloads, {
|
|
32716
|
+
}, ({ selector, predicate, reload, sinceReloads, quiescentMs, timeoutMs, visible }) => gameWait(client, reloads, debug, {
|
|
32056
32717
|
selector,
|
|
32057
32718
|
predicate,
|
|
32058
32719
|
reload,
|
|
@@ -32063,11 +32724,10 @@ async function main() {
|
|
|
32063
32724
|
}));
|
|
32064
32725
|
server.registerTool("game_fill", {
|
|
32065
32726
|
title: `Set an input value in the Gameface UI`,
|
|
32066
|
-
description:
|
|
32727
|
+
description: import_common_tags5.oneLine`
|
|
32067
32728
|
Set the value of an input, textarea, or contenteditable element and fire input/change so
|
|
32068
32729
|
the UI framework reacts as if the user edited it.
|
|
32069
32730
|
Best for setting a field in one shot; use game_type for keystrokes.
|
|
32070
|
-
Use index to pick among matches.
|
|
32071
32731
|
`,
|
|
32072
32732
|
inputSchema: {
|
|
32073
32733
|
selector: exports_external.string().describe(`CSS selector of the field to fill`),
|
|
@@ -32077,11 +32737,10 @@ async function main() {
|
|
|
32077
32737
|
}, ({ selector, value, index }) => gameFill(client, selector, value, index));
|
|
32078
32738
|
server.registerTool("game_type", {
|
|
32079
32739
|
title: `Type text into the Gameface UI`,
|
|
32080
|
-
description:
|
|
32740
|
+
description: import_common_tags5.oneLine`
|
|
32081
32741
|
Type text into an element character by character, firing real KeyboardEvents plus keeping
|
|
32082
32742
|
the value in sync.
|
|
32083
32743
|
Use when handlers react to individual keystrokes; otherwise game_fill.
|
|
32084
|
-
Use index to pick among matches.
|
|
32085
32744
|
`,
|
|
32086
32745
|
inputSchema: {
|
|
32087
32746
|
selector: exports_external.string().describe(`CSS selector of the field to type into`),
|
|
@@ -32091,12 +32750,11 @@ async function main() {
|
|
|
32091
32750
|
}, ({ selector, text: text2, index }) => gameType(client, selector, text2, index));
|
|
32092
32751
|
server.registerTool("game_hover", {
|
|
32093
32752
|
title: `Hover an element in the Gameface UI`,
|
|
32094
|
-
description:
|
|
32753
|
+
description: import_common_tags5.oneLine`
|
|
32095
32754
|
Hover an element by dispatching the pointer/mouse over/enter/move sequence in the page, so
|
|
32096
32755
|
the UI's mouseenter / pointerover JS handlers (tooltips) fire.
|
|
32097
32756
|
The CSS :hover state is NOT set (only real game-forwarded mouse input sets it); verify a
|
|
32098
32757
|
hover by its DOM effect, never by styling.
|
|
32099
|
-
Use index to pick among matches.
|
|
32100
32758
|
`,
|
|
32101
32759
|
inputSchema: {
|
|
32102
32760
|
selector: exports_external.string().describe(`CSS selector of the element to hover`),
|
|
@@ -32105,15 +32763,12 @@ async function main() {
|
|
|
32105
32763
|
}, ({ selector, index }) => gameHover(client, selector, index));
|
|
32106
32764
|
server.registerTool("game_key", {
|
|
32107
32765
|
title: `Press a key in the Gameface UI`,
|
|
32108
|
-
description:
|
|
32109
|
-
Press a named key by dispatching a real bubbling keydown+keyup in the page
|
|
32766
|
+
description: import_common_tags5.oneLine`
|
|
32767
|
+
Press a named key by dispatching a real bubbling keydown+keyup (no keypress) in the page
|
|
32110
32768
|
(KeyboardEvent.key, e.g. Escape, Enter, ArrowDown, a, F5), optionally with
|
|
32111
32769
|
ctrl/shift/alt/meta and a repeat count.
|
|
32112
|
-
|
|
32113
|
-
|
|
32114
|
-
It fires ONLY keydown and keyup (no keypress) and performs NO default action: no character
|
|
32115
|
-
insertion, no Backspace delete, no Tab focus move, no scrolling; use game_type to enter
|
|
32116
|
-
text.
|
|
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.
|
|
32117
32772
|
It reaches the UI's JS keydown handlers, but keys the game routes through its own native
|
|
32118
32773
|
input layer do NOT respond (e.g. an Escape-to-close handled by the engine rather than the
|
|
32119
32774
|
DOM).
|
|
@@ -32127,29 +32782,44 @@ async function main() {
|
|
|
32127
32782
|
shift: exports_external.boolean().optional().describe(`Hold Shift (shiftKey) during the press (default false)`),
|
|
32128
32783
|
alt: exports_external.boolean().optional().describe(`Hold Alt (altKey) during the press (default false)`),
|
|
32129
32784
|
meta: exports_external.boolean().optional().describe(`Hold Meta / Win / Cmd (metaKey) during the press (default false)`),
|
|
32130
|
-
selector: exports_external.string().optional().describe(`If set, focus this element and dispatch on it; else the focused element`),
|
|
32785
|
+
selector: exports_external.string().optional().describe(`If set, focus this element and dispatch on it; else the focused element, else document`),
|
|
32131
32786
|
index: exports_external.number().int().min(0).optional().describe(`Which match to target when the selector has several (default: 0)`)
|
|
32132
32787
|
}
|
|
32133
32788
|
}, ({ key, count, ctrl, shift, alt, meta: meta3, selector, index }) => gameKey(client, { key, count, ctrl, shift, alt, meta: meta3, selector, index }));
|
|
32134
32789
|
server.registerTool("game_console", {
|
|
32135
32790
|
title: `Read the Gameface UI console`,
|
|
32136
|
-
description:
|
|
32791
|
+
description: import_common_tags5.oneLine`
|
|
32137
32792
|
Return recent console.* calls, log entries, and uncaught exceptions captured from the
|
|
32138
|
-
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]}).
|
|
32139
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.
|
|
32140
32799
|
`,
|
|
32141
32800
|
inputSchema: {
|
|
32142
32801
|
limit: exports_external.number().int().min(1).max(1000).optional().describe(`Max entries to return (default 50)`),
|
|
32143
32802
|
level: exports_external.string().optional().describe(`Filter by level, e.g. error / warning / log / info`),
|
|
32144
|
-
|
|
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
|
+
`)
|
|
32145
32814
|
}
|
|
32146
|
-
}, ({ limit, level, clear }) => gameConsole(client, consoleBuffer, { limit, level, clear }));
|
|
32815
|
+
}, ({ limit, level, depth, clear }) => gameConsole(client, consoleBuffer, { limit, level, depth, clear }));
|
|
32147
32816
|
server.registerTool("game_dom", {
|
|
32148
32817
|
title: `Inspect Gameface UI DOM`,
|
|
32149
|
-
description:
|
|
32818
|
+
description: import_common_tags5.oneLine`
|
|
32150
32819
|
Return DOM details (tag, id, classes, attributes, bounding rect, outerHTML) for elements
|
|
32151
32820
|
matching a CSS selector in the live Gameface UI.
|
|
32152
|
-
|
|
32821
|
+
outerHTML dominates the response: to locate elements, get a targetable handle, or read
|
|
32822
|
+
attributes across many matches, use game_query.
|
|
32153
32823
|
`,
|
|
32154
32824
|
inputSchema: {
|
|
32155
32825
|
selector: exports_external.string().describe(`CSS selector to query in the Gameface UI`),
|
|
@@ -32157,37 +32827,43 @@ async function main() {
|
|
|
32157
32827
|
maxHtml: exports_external.number().min(0).optional().describe(`Max outerHTML characters per element before truncation (default: 4000)`)
|
|
32158
32828
|
}
|
|
32159
32829
|
}, ({ selector, all, maxHtml }) => gameDom(client, selector, all, maxHtml));
|
|
32160
|
-
server.registerTool("
|
|
32161
|
-
title: `Find elements
|
|
32162
|
-
description:
|
|
32163
|
-
Locate elements
|
|
32164
|
-
(
|
|
32165
|
-
|
|
32166
|
-
Returns tag, id, classes
|
|
32167
|
-
|
|
32168
|
-
Set tag=true
|
|
32169
|
-
|
|
32170
|
-
The go-to way to find an element when class names are build-hashed and there is no XPath.
|
|
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.
|
|
32171
32840
|
`,
|
|
32172
32841
|
inputSchema: {
|
|
32173
|
-
text: exports_external.string().describe(`
|
|
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
|
+
`),
|
|
32174
32847
|
match: exports_external.enum(["equals", "contains", "regex"]).optional().describe(`How to match the text: equals / contains / regex (default: contains)`),
|
|
32175
32848
|
caseSensitive: exports_external.boolean().optional().describe(`Match case-sensitively (default: false)`),
|
|
32176
|
-
selector: exports_external.string().optional().describe(`CSS selector scoping the scan (
|
|
32177
|
-
deepest: exports_external.boolean().optional().describe(`
|
|
32178
|
-
|
|
32179
|
-
|
|
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
|
|
32180
32856
|
prior handles first (default: false).
|
|
32181
32857
|
`),
|
|
32858
|
+
attributes: exports_external.boolean().optional().describe(`Return each match's attributes, this tool's own data-gf-tag aside (default: false)`),
|
|
32182
32859
|
limit: exports_external.number().int().min(1).max(100).optional().describe(`Max matches to return (default: 20); the total count is always reported`)
|
|
32183
32860
|
}
|
|
32184
|
-
}, ({ text: text2, match, caseSensitive, selector, deepest, tag, limit }) =>
|
|
32861
|
+
}, ({ text: text2, match, caseSensitive, selector, deepest, tag, attributes, limit }) => gameQuery(client, { text: text2, match, caseSensitive, selector, deepest, tag, attributes, limit }));
|
|
32185
32862
|
server.registerTool("game_click", {
|
|
32186
32863
|
title: `Click an element in the Gameface UI`,
|
|
32187
|
-
description:
|
|
32864
|
+
description: import_common_tags5.oneLine`
|
|
32188
32865
|
Click the element matching a CSS selector by dispatching a real bubbling pointer/mouse/click
|
|
32189
32866
|
sequence in the page (NOT CDP Input, which Gameface ignores for the UI).
|
|
32190
|
-
Use index to pick among matches.
|
|
32191
32867
|
`,
|
|
32192
32868
|
inputSchema: {
|
|
32193
32869
|
selector: exports_external.string().describe(`CSS selector of the element to click`),
|
|
@@ -32196,12 +32872,10 @@ async function main() {
|
|
|
32196
32872
|
}, ({ selector, index }) => gameClick(client, selector, index));
|
|
32197
32873
|
server.registerTool("game_debug_status", {
|
|
32198
32874
|
title: `JS debugger status`,
|
|
32199
|
-
description:
|
|
32875
|
+
description: import_common_tags5.oneLine`
|
|
32200
32876
|
Report debugger state: whether paused (and where), pause-on-exceptions mode, breakpoints,
|
|
32201
32877
|
and parsed script count.
|
|
32202
|
-
Pass setPauseOnExceptions to change exception pausing.
|
|
32203
32878
|
Enables the debugger on first use.
|
|
32204
|
-
Hitting a breakpoint FREEZES the UI until resumed.
|
|
32205
32879
|
`,
|
|
32206
32880
|
inputSchema: {
|
|
32207
32881
|
setPauseOnExceptions: exports_external.enum(["none", "uncaught", "all"]).optional().describe(`If set, change which exceptions pause execution (default none)`)
|
|
@@ -32209,12 +32883,13 @@ async function main() {
|
|
|
32209
32883
|
}, ({ setPauseOnExceptions }) => debug.status(setPauseOnExceptions));
|
|
32210
32884
|
server.registerTool("game_debug_scripts", {
|
|
32211
32885
|
title: `List parsed UI scripts`,
|
|
32212
|
-
description:
|
|
32213
|
-
List JavaScript scripts parsed in the Gameface UI (scriptId + url + line count)
|
|
32214
|
-
|
|
32215
|
-
|
|
32216
|
-
|
|
32217
|
-
|
|
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.
|
|
32218
32893
|
`,
|
|
32219
32894
|
inputSchema: {
|
|
32220
32895
|
filter: exports_external.string().optional().describe(`Only scripts whose url contains this substring`)
|
|
@@ -32222,28 +32897,57 @@ async function main() {
|
|
|
32222
32897
|
}, ({ filter }) => debug.listScripts(filter));
|
|
32223
32898
|
server.registerTool("game_debug_source", {
|
|
32224
32899
|
title: `Get UI script source`,
|
|
32225
|
-
description:
|
|
32900
|
+
description: import_common_tags5.oneLine`
|
|
32226
32901
|
Return the source of a script (by scriptId from game_debug_scripts), with line numbers.
|
|
32227
|
-
|
|
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.
|
|
32228
32906
|
`,
|
|
32229
32907
|
inputSchema: {
|
|
32230
32908
|
scriptId: exports_external.string().describe(`Script id from game_debug_scripts`),
|
|
32231
|
-
lineStart: exports_external.number().int().min(1).optional().describe(`
|
|
32232
|
-
|
|
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.
|
|
32930
|
+
`,
|
|
32931
|
+
inputSchema: {
|
|
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)`)
|
|
32233
32934
|
}
|
|
32234
|
-
}, ({
|
|
32935
|
+
}, ({ query, urlContains }) => debug.searchSource(query, urlContains));
|
|
32235
32936
|
server.registerTool("game_debug_set_breakpoint", {
|
|
32236
32937
|
title: `Set a breakpoint`,
|
|
32237
|
-
description:
|
|
32238
|
-
Set a breakpoint by url substring + line
|
|
32239
|
-
|
|
32240
|
-
|
|
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.
|
|
32241
32942
|
Hitting it FREEZES the UI until you resume with game_debug_step.
|
|
32242
32943
|
`,
|
|
32243
32944
|
inputSchema: {
|
|
32244
|
-
urlContains: exports_external.string().describe(`
|
|
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
|
+
`),
|
|
32245
32949
|
line: exports_external.number().int().min(1).describe(`Line number (1-based)`),
|
|
32246
|
-
column: exports_external.number().int().min(
|
|
32950
|
+
column: exports_external.number().int().min(1).optional().describe(`Column (1-based), optional; take one from game_debug_search_source`),
|
|
32247
32951
|
condition: exports_external.string().optional().describe(`Optional JS condition; pause only when it evaluates truthy`)
|
|
32248
32952
|
}
|
|
32249
32953
|
}, ({ urlContains, line, column, condition }) => debug.setBreakpoint(urlContains, line, column, condition));
|
|
@@ -32256,10 +32960,9 @@ async function main() {
|
|
|
32256
32960
|
}, ({ breakpoint }) => debug.removeBreakpoint(breakpoint));
|
|
32257
32961
|
server.registerTool("game_debug_pause_state", {
|
|
32258
32962
|
title: `Inspect the paused stack`,
|
|
32259
|
-
description:
|
|
32260
|
-
When paused, return the call stack (frames with function + location + scope types)
|
|
32261
|
-
|
|
32262
|
-
Returns 'not paused' otherwise.
|
|
32963
|
+
description: import_common_tags5.oneLine`
|
|
32964
|
+
When paused, return the call stack (frames with function + location + scope types);
|
|
32965
|
+
'not paused' otherwise.
|
|
32263
32966
|
`,
|
|
32264
32967
|
inputSchema: {
|
|
32265
32968
|
expandScopes: exports_external.boolean().optional().describe(`Also list local/closure variables per frame (default false)`)
|
|
@@ -32267,10 +32970,10 @@ async function main() {
|
|
|
32267
32970
|
}, ({ expandScopes }) => debug.pauseStateReport(expandScopes ?? false));
|
|
32268
32971
|
server.registerTool("game_debug_evaluate", {
|
|
32269
32972
|
title: `Evaluate while debugging`,
|
|
32270
|
-
description:
|
|
32973
|
+
description: import_common_tags5.oneLine`
|
|
32271
32974
|
Evaluate a JS expression.
|
|
32272
|
-
When paused, it runs in the selected call frame's scope
|
|
32273
|
-
|
|
32975
|
+
When paused, it runs in the selected call frame's scope so you can read locals; otherwise
|
|
32976
|
+
it runs globally.
|
|
32274
32977
|
Prefer this over game_eval while paused.
|
|
32275
32978
|
`,
|
|
32276
32979
|
inputSchema: {
|
|
@@ -32280,10 +32983,11 @@ async function main() {
|
|
|
32280
32983
|
}, ({ expression, frameIndex }) => debug.evaluate(expression, frameIndex));
|
|
32281
32984
|
server.registerTool("game_debug_step", {
|
|
32282
32985
|
title: `Step / resume / pause execution`,
|
|
32283
|
-
description:
|
|
32986
|
+
description: import_common_tags5.oneLine`
|
|
32284
32987
|
Control paused execution: resume (unfreeze the UI), over/into/out (step), or pause (break at
|
|
32285
32988
|
the next statement).
|
|
32286
|
-
Stepping reports the new location
|
|
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).
|
|
32287
32991
|
`,
|
|
32288
32992
|
inputSchema: {
|
|
32289
32993
|
action: exports_external.enum(["resume", "over", "into", "out", "pause"]).describe(`resume | over | into | out | pause`)
|