@csmodding/gameface-devtools-mcp 0.2.0 → 1.0.0

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