@csmodding/gameface-devtools-mcp 0.3.0 → 1.0.1

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.
Files changed (3) hide show
  1. package/README.md +36 -30
  2. package/dist/server.mjs +972 -268
  3. 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 import_common_tags4 = __toESM(require_lib(), 1);
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/debugger.ts
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 SCRIPT_REPLAY_WAIT_MS = 350;
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
- resolved: bp.locations.length
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 needle = filter?.toLowerCase();
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 || undefined
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 getSource(scriptId, lineStart, lineEnd) {
31241
+ async searchSource(query, urlContains) {
30735
31242
  try {
30736
31243
  await this.ensureReady();
30737
- const res = await this.client.call("Debugger.getScriptSource", {
30738
- scriptId
30739
- });
30740
- const source = res?.scriptSource;
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 from = lineStart && lineStart > 0 ? lineStart : 1;
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 to = lineEnd && lineEnd >= from ? Math.min(lineEnd, from + cap - 1) : Math.min(lines.length, from + cap - 1);
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 slice = lines.slice(from - 1, to).map((line, index) => `${String(from + index).padStart(padWidth)} ${line}`).join(`
31283
+ const rendered = lines.slice(from - first, to - offset).map((line, index) => `${String(from + index).padStart(padWidth)} ${line}`).join(`
30751
31284
  `);
30752
- const note = to < lines.length || from > 1 ? `
30753
- ... showing lines ${from}-${to} of ${lines.length}.` : "";
30754
- return text(slice + note);
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: column, condition });
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: column,
31322
+ columnNumber,
30773
31323
  condition,
30774
31324
  cdpId: res.breakpointId,
30775
- locations
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
- condition: condition ?? null,
30782
- resolvedLocations: locations.map((loc) => this.locStr(loc)),
31335
+ column,
31336
+ condition,
31337
+ resolvedLocations: resolved,
30783
31338
  pending: locations.length == 0,
30784
- note: locations.length == 0 ? import_common_tags2.oneLine`
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
- ` : import_common_tags2.oneLine`
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(import_common_tags2.oneLine`
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
- const deadline = Date.now() + RESUME_WAIT_MS;
30897
- while (Date.now() < deadline && this.paused) {
30898
- await sleep(POLL_INTERVAL_MS);
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.locations = res.locations ?? [];
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
- await sleep(SCRIPT_REPLAY_WAIT_MS);
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
- async waitForNewPause(beforeSeq, timeoutMs) {
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 (this.pausedSeq > beforeSeq) {
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 import_common_tags3 = __toESM(require_lib(), 1);
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 DEFAULT_CONSOLE_LIMIT = 50;
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: import_common_tags3.oneLine`
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 = "jpeg", quality, selector, index = 0 } = options;
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(import_common_tags3.oneLine`
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 = import_common_tags3.oneLine`
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 gameFind(client, options) {
31140
- const {
31141
- text: needle,
31142
- match = "contains",
31143
- caseSensitive = false,
31144
- selector = "*",
31145
- deepest = true,
31146
- tag = false,
31147
- limit = DEFAULT_FIND_LIMIT
31148
- } = options;
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(findFn, {
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(`Find failed: ${formatException(res.exceptionDetails)}`);
31837
+ return errorText(`Query failed: ${formatException(res.exceptionDetails)}`);
31164
31838
  }
31165
31839
  const value = res.result.value;
31166
31840
  if (!value) {
31167
- return errorText(`game_find returned no result for selector ${JSON.stringify(selector)}.`);
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(`game_find: ${value.error}`);
31844
+ return errorText(`game_query: ${value.error}`);
31171
31845
  }
31172
- return text(JSON.stringify({ selector, match, ...value }, null, 2));
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(import_common_tags3.oneLine`
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(import_common_tags3.oneLine`
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 errorText(import_common_tags3.oneLine`
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 errorText(import_common_tags3.oneLine`
31253
- Timed out after ${budget}ms: reload observed, but context swaps kept arriving within the
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 errorText(`Timed out after ${budget}ms waiting for ${what}.${errorNote}`);
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(import_common_tags3.oneLine`
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(import_common_tags3.oneLine`
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(import_common_tags3.oneLine`
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(import_common_tags3.oneLine`
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(import_common_tags3.oneLine`
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(import_common_tags3.oneLine`
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(import_common_tags3.oneLine`
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(import_common_tags3.oneLine`
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 findFn(args) {
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 ? found.filter((el) => !found.some((other) => other != el && el.contains(other))) : found;
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-find]"))) {
31617
- stale.removeAttribute("data-gf-find");
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-find", String(i + 1));
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-find="${i + 1}"]`;
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: import_common_tags4.oneLine`
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 (count, last reload time, context id).
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: import_common_tags4.oneLine`
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: import_common_tags4.oneLine`
32012
- Capture a screenshot of the Gameface viewport (CDP Page.captureScreenshot) and return it
32013
- as an inline image.
32014
- Pass a selector to clip the capture to one element; use index to pick among matches.
32015
- Hangs while the JS debugger is paused; resume (game_debug_step) before capturing.
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: jpeg)`),
32019
- quality: exports_external.number().min(1).max(100).optional().describe(`JPEG quality 1-100 (only used when format is jpeg; default 80)`),
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: import_common_tags4.oneLine`
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
- Returns when met or times out.
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(import_common_tags4.oneLine`
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(import_common_tags4.oneLine`
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(import_common_tags4.oneLine`
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(import_common_tags4.oneLine`
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: import_common_tags4.oneLine`
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: import_common_tags4.oneLine`
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: import_common_tags4.oneLine`
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: import_common_tags4.oneLine`
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
- With a selector it focuses that element and dispatches on it (index picks among matches);
32113
- without one it dispatches on the focused element, else document.
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: import_common_tags4.oneLine`
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
- clear: exports_external.boolean().optional().describe(`Clear the buffer after reading (default false)`)
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: import_common_tags4.oneLine`
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
- Set all=true to return every match.
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("game_find", {
32161
- title: `Find elements by text in the Gameface UI`,
32162
- description: import_common_tags4.oneLine`
32163
- Locate elements by their text content in the live Gameface UI: scan a CSS selector's matches
32164
- (default: every element) and filter on trimmed textContent by equals/contains/regex
32165
- (case-insensitive by default).
32166
- Returns tag, id, classes, and bounding rect per match, plus match counts before and after
32167
- deepest pruning so pruning and limit truncation are both visible.
32168
- Set tag=true to stamp matches with data-gf-find handles and get back ready-to-use selectors
32169
- for game_click / game_hover / game_screenshot.
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(`Text to match against each element's trimmed textContent`),
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 (default: *, every element)`),
32177
- deepest: exports_external.boolean().optional().describe(`Keep only the innermost match, pruning ancestors that also matched (default: true)`),
32178
- tag: exports_external.boolean().optional().describe(import_common_tags4.oneLine`
32179
- Stamp matches with data-gf-find handles and return them as selectors, clearing any
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 }) => gameFind(client, { 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: import_common_tags4.oneLine`
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: import_common_tags4.oneLine`
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: import_common_tags4.oneLine`
32213
- List JavaScript scripts parsed in the Gameface UI (scriptId + url + line count), optionally
32214
- filtered by a url substring.
32215
- Only scripts parsed after the debugger attached appear (Gameface does not replay
32216
- scriptParsed); an empty list means attach first, then trigger a view reload to repopulate.
32217
- Use the scriptId with game_debug_source.
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: import_common_tags4.oneLine`
32900
+ description: import_common_tags5.oneLine`
32226
32901
  Return the source of a script (by scriptId from game_debug_scripts), with line numbers.
32227
- Pass lineStart/lineEnd to get a range (large scripts are capped at 400 lines).
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(`First line (1-based)`),
32232
- lineEnd: exports_external.number().int().min(1).optional().describe(`Last line (1-based)`)
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
- }, ({ scriptId, lineStart, lineEnd }) => debug.getSource(scriptId, lineStart, lineEnd));
32935
+ }, ({ query, urlContains }) => debug.searchSource(query, urlContains));
32235
32936
  server.registerTool("game_debug_set_breakpoint", {
32236
32937
  title: `Set a breakpoint`,
32237
- description: import_common_tags4.oneLine`
32238
- Set a breakpoint by url substring + line (1-based).
32239
- Add a condition (JS expression) to only pause when it is truthy, which limits how often the
32240
- UI freezes.
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(`Substring of the script url to break in`),
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(0).optional().describe(`Column (0-based), optional`),
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: import_common_tags4.oneLine`
32260
- When paused, return the call stack (frames with function + location + scope types).
32261
- Set expandScopes to also list local/closure variables of each frame.
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: import_common_tags4.oneLine`
32973
+ description: import_common_tags5.oneLine`
32271
32974
  Evaluate a JS expression.
32272
- When paused, it runs in the selected call frame's scope (Debugger.evaluateOnCallFrame) so
32273
- you can read locals; otherwise it runs globally.
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: import_common_tags4.oneLine`
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`)