@csmodding/gameface-devtools-mcp 0.2.0 → 0.3.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 +21 -16
  2. package/dist/server.mjs +923 -340
  3. package/package.json +1 -1
package/dist/server.mjs CHANGED
@@ -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 {}
@@ -30633,7 +30633,7 @@ function describeRemoteObject(obj) {
30633
30633
  }
30634
30634
  function formatException(details) {
30635
30635
  const exception = details?.exception;
30636
- const desc = exception?.description ?? exception?.value ?? details?.text ?? "unknown error";
30636
+ const desc = exception?.description ?? exception?.value ?? details?.text ?? `unknown error`;
30637
30637
  return typeof desc == "string" ? desc : JSON.stringify(desc);
30638
30638
  }
30639
30639
  function valToStr(value) {
@@ -30666,12 +30666,15 @@ class DebuggerSession {
30666
30666
  nextBpId = 1;
30667
30667
  pausedSeq = 0;
30668
30668
  client;
30669
- constructor(client) {
30669
+ constructor(client, reloads) {
30670
30670
  this.client = client;
30671
30671
  client.onConnect((conn) => this.onConnect(conn));
30672
30672
  client.onEvent((method, params) => {
30673
30673
  this.handle(method, params);
30674
30674
  });
30675
+ reloads.onReload(() => {
30676
+ this.scripts.clear();
30677
+ });
30675
30678
  }
30676
30679
  async status(setPause) {
30677
30680
  try {
@@ -30817,7 +30820,7 @@ class DebuggerSession {
30817
30820
  try {
30818
30821
  await this.ensureReady();
30819
30822
  if (!this.paused) {
30820
- return text("Not paused (the UI is running). Set a breakpoint or use game_debug_step pause.");
30823
+ return text(`Not paused (the UI is running). Set a breakpoint or use game_debug_step pause.`);
30821
30824
  }
30822
30825
  const frames = [];
30823
30826
  for (const [index, frame] of this.paused.callFrames.entries()) {
@@ -30886,7 +30889,7 @@ class DebuggerSession {
30886
30889
  return text(paused ? `Paused at ${this.topLocation()}.` : `Pause requested; nothing executed within ${PAUSE_WAIT_MS}ms (UI idle).`);
30887
30890
  }
30888
30891
  if (!this.paused) {
30889
- return errorText("Not paused. Set a breakpoint and trigger it, or use action 'pause' first.");
30892
+ return errorText(`Not paused. Set a breakpoint and trigger it, or use action 'pause' first.`);
30890
30893
  }
30891
30894
  if (action == "resume") {
30892
30895
  await this.client.call("Debugger.resume");
@@ -30894,7 +30897,7 @@ class DebuggerSession {
30894
30897
  while (Date.now() < deadline && this.paused) {
30895
30898
  await sleep(POLL_INTERVAL_MS);
30896
30899
  }
30897
- return text("Resumed (UI unfrozen).");
30900
+ return text(`Resumed (UI unfrozen).`);
30898
30901
  }
30899
30902
  const cdpMethod = { over: "stepOver", into: "stepInto", out: "stepOut" }[action];
30900
30903
  const before = this.pausedSeq;
@@ -31002,204 +31005,15 @@ class DebuggerSession {
31002
31005
  // src/tools.ts
31003
31006
  var import_common_tags3 = __toESM(require_lib(), 1);
31004
31007
  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
31008
  var POLL_INTERVAL_MS2 = 150;
31198
31009
  var MAX_WAIT_MS = 60000;
31199
31010
  var DEFAULT_WAIT_TIMEOUT_MS = 8000;
31011
+ var DEFAULT_RELOAD_WAIT_TIMEOUT_MS = 30000;
31012
+ var DEFAULT_QUIESCENT_MS = 1000;
31200
31013
  var DEFAULT_JPEG_QUALITY = 80;
31201
31014
  var DEFAULT_CONSOLE_LIMIT = 50;
31202
- async function gameStatus(client) {
31015
+ var DEFAULT_FIND_LIMIT = 20;
31016
+ async function gameStatus(client, reloads) {
31203
31017
  const { host, port } = client.config;
31204
31018
  try {
31205
31019
  const target = await client.discover();
@@ -31213,12 +31027,25 @@ async function gameStatus(client) {
31213
31027
  protocol = versionInfo["Protocol-Version"];
31214
31028
  }
31215
31029
  } catch {}
31030
+ let tracking = false;
31031
+ try {
31032
+ await client.connection();
31033
+ tracking = true;
31034
+ } catch {}
31035
+ const { lastReloadAt } = reloads;
31216
31036
  return text(JSON.stringify({
31217
31037
  reachable: true,
31218
31038
  endpoint: `http://${host}:${port}`,
31219
31039
  target: { id: target.id, url: target.url, title: target.title, wsUrl: target.wsUrl },
31220
31040
  browser,
31221
- cdpProtocol: protocol
31041
+ cdpProtocol: protocol,
31042
+ reloads: tracking ? {
31043
+ tracking: true,
31044
+ count: reloads.count,
31045
+ lastReloadAt: lastReloadAt == null ? null : new Date(lastReloadAt).toISOString(),
31046
+ lastReloadAgoMs: lastReloadAt == null ? null : Date.now() - lastReloadAt,
31047
+ contextUniqueId: reloads.contextUniqueId ?? null
31048
+ } : { tracking: false }
31222
31049
  }, null, 2));
31223
31050
  } catch (error51) {
31224
31051
  return text(JSON.stringify({
@@ -31248,36 +31075,45 @@ async function gameEval(client, expression, awaitPromise = false) {
31248
31075
  return toErrorResult(error51);
31249
31076
  }
31250
31077
  }
31251
- async function gameScreenshot(client, format = "png", quality, selector) {
31078
+ async function gameScreenshot(client, options = {}) {
31079
+ const { format = "jpeg", quality, selector, index = 0 } = options;
31252
31080
  try {
31253
31081
  await client.ensureDomain("Page");
31254
31082
  const params = { format };
31255
31083
  if (format == "jpeg") {
31256
31084
  params.quality = quality ?? DEFAULT_JPEG_QUALITY;
31257
31085
  }
31086
+ let caption;
31258
31087
  if (selector) {
31259
31088
  const rectRes = await client.call("Runtime.evaluate", {
31260
- expression: callPageFn(rectFn, selector),
31089
+ expression: callPageFn(rectFn, selector, index),
31261
31090
  returnByValue: true
31262
31091
  });
31263
31092
  const rect = rectRes.result.value;
31264
31093
  if (!rect?.found) {
31265
- return errorText(`No element matched ${JSON.stringify(selector)} for game_screenshot.`);
31094
+ return errorText(import_common_tags3.oneLine`
31095
+ No element matched ${JSON.stringify(selector)} for game_screenshot at index ${index}
31096
+ (matches found: ${rect?.count ?? 0}).
31097
+ `);
31266
31098
  }
31267
31099
  if (!(rect.width > 0 && rect.height > 0)) {
31268
31100
  return errorText(`Element ${JSON.stringify(selector)} has a zero-size box; nothing to capture.`);
31269
31101
  }
31270
31102
  params.clip = { x: rect.x, y: rect.y, width: rect.width, height: rect.height, scale: 1 };
31103
+ caption = import_common_tags3.oneLine`
31104
+ Clipped to ${JSON.stringify(selector)} [index ${index}]. Matches: ${rect.count}.
31105
+ `;
31271
31106
  }
31272
31107
  const res = await client.call("Page.captureScreenshot", params);
31273
31108
  if (!res?.data) {
31274
- return errorText("Page.captureScreenshot returned no image data.");
31109
+ return errorText(`Page.captureScreenshot returned no image data.`);
31275
31110
  }
31276
- return {
31277
- content: [
31278
- { type: "image", data: res.data, mimeType: format == "jpeg" ? "image/jpeg" : "image/png" }
31279
- ]
31111
+ const image = {
31112
+ type: "image",
31113
+ data: res.data,
31114
+ mimeType: format == "jpeg" ? "image/jpeg" : "image/png"
31280
31115
  };
31116
+ return { content: caption ? [{ type: "text", text: caption }, image] : [image] };
31281
31117
  } catch (error51) {
31282
31118
  return toErrorResult(error51);
31283
31119
  }
@@ -31300,6 +31136,44 @@ async function gameDom(client, selector, all = false, maxHtml = 4000) {
31300
31136
  return toErrorResult(error51);
31301
31137
  }
31302
31138
  }
31139
+ async function gameFind(client, options) {
31140
+ const {
31141
+ text: needle,
31142
+ match = "contains",
31143
+ caseSensitive = false,
31144
+ selector = "*",
31145
+ deepest = true,
31146
+ tag = false,
31147
+ limit = DEFAULT_FIND_LIMIT
31148
+ } = options;
31149
+ try {
31150
+ const res = await client.call("Runtime.evaluate", {
31151
+ expression: callPageFn(findFn, {
31152
+ sel: selector,
31153
+ needle,
31154
+ mode: match,
31155
+ caseSensitive,
31156
+ deepest,
31157
+ tag,
31158
+ limit
31159
+ }),
31160
+ returnByValue: true
31161
+ });
31162
+ if (res.exceptionDetails) {
31163
+ return errorText(`Find failed: ${formatException(res.exceptionDetails)}`);
31164
+ }
31165
+ const value = res.result.value;
31166
+ if (!value) {
31167
+ return errorText(`game_find returned no result for selector ${JSON.stringify(selector)}.`);
31168
+ }
31169
+ if ("error" in value) {
31170
+ return errorText(`game_find: ${value.error}`);
31171
+ }
31172
+ return text(JSON.stringify({ selector, match, ...value }, null, 2));
31173
+ } catch (error51) {
31174
+ return toErrorResult(error51);
31175
+ }
31176
+ }
31303
31177
  async function gameClick(client, selector, index = 0) {
31304
31178
  try {
31305
31179
  const res = await client.call("Runtime.evaluate", {
@@ -31325,18 +31199,74 @@ async function gameClick(client, selector, index = 0) {
31325
31199
  return toErrorResult(error51);
31326
31200
  }
31327
31201
  }
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'.");
31202
+ async function gameWait(client, reloads, options) {
31203
+ const {
31204
+ selector,
31205
+ predicate,
31206
+ reload = false,
31207
+ sinceReloads,
31208
+ quiescentMs = DEFAULT_QUIESCENT_MS,
31209
+ visible = false
31210
+ } = options;
31211
+ if (!reload && !selector && !predicate) {
31212
+ return errorText(`game_wait needs at least one of 'reload', 'selector', or 'predicate'.`);
31332
31213
  }
31214
+ if (selector && predicate) {
31215
+ return errorText(`game_wait takes either 'selector' or 'predicate', not both.`);
31216
+ }
31217
+ const timeoutMs = options.timeoutMs ?? (reload ? DEFAULT_RELOAD_WAIT_TIMEOUT_MS : DEFAULT_WAIT_TIMEOUT_MS);
31333
31218
  const budget = Math.min(Math.max(timeoutMs, 0), MAX_WAIT_MS);
31334
- const deadline = Date.now() + budget;
31335
31219
  const start = Date.now();
31336
- const expression = selector ? callPageFn(waitCheckFn, selector, visible) : `Boolean(${predicate ?? ""})`;
31337
- let lastError;
31220
+ const deadline = start + budget;
31338
31221
  try {
31339
- for (;; ) {
31222
+ if (reload) {
31223
+ const failed = await waitForReload();
31224
+ if (failed) {
31225
+ return failed;
31226
+ }
31227
+ if (!selector && !predicate) {
31228
+ return text(`Reload observed after ${Date.now() - start}ms (reload count: ${reloads.count}).`);
31229
+ }
31230
+ }
31231
+ return await pollCondition();
31232
+ } catch (error51) {
31233
+ return toErrorResult(error51);
31234
+ }
31235
+ async function waitForReload() {
31236
+ await client.connection();
31237
+ const baseline = sinceReloads ?? reloads.count;
31238
+ while (reloads.count <= baseline) {
31239
+ if (Date.now() >= deadline) {
31240
+ return errorText(import_common_tags3.oneLine`
31241
+ Timed out after ${budget}ms waiting for a view reload
31242
+ (reload count still ${reloads.count}, baseline ${baseline}).
31243
+ `);
31244
+ }
31245
+ await sleep2(POLL_INTERVAL_MS2);
31246
+ }
31247
+ if (quiescentMs > 0) {
31248
+ let lastCount = reloads.count;
31249
+ let quietSince = Date.now();
31250
+ while (Date.now() - quietSince < quiescentMs) {
31251
+ if (Date.now() >= deadline) {
31252
+ return errorText(import_common_tags3.oneLine`
31253
+ Timed out after ${budget}ms: reload observed, but context swaps kept arriving within the
31254
+ ${quiescentMs}ms quiescence window.
31255
+ `);
31256
+ }
31257
+ await sleep2(POLL_INTERVAL_MS2);
31258
+ if (reloads.count != lastCount) {
31259
+ lastCount = reloads.count;
31260
+ quietSince = Date.now();
31261
+ }
31262
+ }
31263
+ }
31264
+ return;
31265
+ }
31266
+ async function pollCondition() {
31267
+ const expression = selector ? callPageFn(waitCheckFn, selector, visible) : `Boolean(${predicate ?? ""})`;
31268
+ let lastError;
31269
+ while (true) {
31340
31270
  const res = await client.call("Runtime.evaluate", {
31341
31271
  expression,
31342
31272
  returnByValue: true
@@ -31345,7 +31275,8 @@ async function gameWait(client, options) {
31345
31275
  lastError = formatException(res.exceptionDetails);
31346
31276
  } else if (res.result.value) {
31347
31277
  const what = selector ? `selector ${JSON.stringify(selector)}` : "predicate";
31348
- return text(`Condition met (${what}) after ${Date.now() - start}ms.`);
31278
+ const reloadNote = reload ? ` Reload count: ${reloads.count}.` : "";
31279
+ return text(`Condition met (${what}) after ${Date.now() - start}ms.${reloadNote}`);
31349
31280
  } else {
31350
31281
  lastError = undefined;
31351
31282
  }
@@ -31356,14 +31287,12 @@ async function gameWait(client, options) {
31356
31287
  }
31357
31288
  await sleep2(POLL_INTERVAL_MS2);
31358
31289
  }
31359
- } catch (error51) {
31360
- return toErrorResult(error51);
31361
31290
  }
31362
31291
  }
31363
- async function gameFill(client, selector, value) {
31292
+ async function gameFill(client, selector, value, index = 0) {
31364
31293
  try {
31365
31294
  const res = await client.call("Runtime.evaluate", {
31366
- expression: callPageFn(fillFn, selector, value),
31295
+ expression: callPageFn(fillFn, selector, value, index),
31367
31296
  returnByValue: true
31368
31297
  });
31369
31298
  if (res.exceptionDetails) {
@@ -31371,20 +31300,23 @@ async function gameFill(client, selector, value) {
31371
31300
  }
31372
31301
  const info = res.result.value;
31373
31302
  if (!info?.found) {
31374
- return errorText(`No element matched ${JSON.stringify(selector)} for game_fill.`);
31303
+ return errorText(import_common_tags3.oneLine`
31304
+ No element matched ${JSON.stringify(selector)} for game_fill at index ${index}
31305
+ (matches found: ${info?.count ?? 0}).
31306
+ `);
31375
31307
  }
31376
31308
  return text(import_common_tags3.oneLine`
31377
- Filled ${JSON.stringify(selector)} (${info.mode}).
31378
- Value is now ${JSON.stringify(info.value)}.
31309
+ Filled ${JSON.stringify(selector)} [index ${index}] (${info.mode}).
31310
+ Value is now ${JSON.stringify(info.value)}. Matches: ${info.count}.
31379
31311
  `);
31380
31312
  } catch (error51) {
31381
31313
  return toErrorResult(error51);
31382
31314
  }
31383
31315
  }
31384
- async function gameType(client, selector, textToType) {
31316
+ async function gameType(client, selector, textToType, index = 0) {
31385
31317
  try {
31386
31318
  const res = await client.call("Runtime.evaluate", {
31387
- expression: callPageFn(typeFn, selector, textToType),
31319
+ expression: callPageFn(typeFn, selector, textToType, index),
31388
31320
  returnByValue: true
31389
31321
  });
31390
31322
  if (res.exceptionDetails) {
@@ -31392,20 +31324,23 @@ async function gameType(client, selector, textToType) {
31392
31324
  }
31393
31325
  const info = res.result.value;
31394
31326
  if (!info?.found) {
31395
- return errorText(`No element matched ${JSON.stringify(selector)} for game_type.`);
31327
+ return errorText(import_common_tags3.oneLine`
31328
+ No element matched ${JSON.stringify(selector)} for game_type at index ${index}
31329
+ (matches found: ${info?.count ?? 0}).
31330
+ `);
31396
31331
  }
31397
31332
  return text(import_common_tags3.oneLine`
31398
- Typed ${info.typed} char(s) into ${JSON.stringify(selector)}.
31399
- Value is now ${JSON.stringify(info.value)}.
31333
+ Typed ${info.typed} char(s) into ${JSON.stringify(selector)} [index ${index}].
31334
+ Value is now ${JSON.stringify(info.value)}. Matches: ${info.count}.
31400
31335
  `);
31401
31336
  } catch (error51) {
31402
31337
  return toErrorResult(error51);
31403
31338
  }
31404
31339
  }
31405
- async function gameHover(client, selector) {
31340
+ async function gameHover(client, selector, index = 0) {
31406
31341
  try {
31407
31342
  const res = await client.call("Runtime.evaluate", {
31408
- expression: callPageFn(hoverFn, selector),
31343
+ expression: callPageFn(hoverFn, selector, index),
31409
31344
  returnByValue: true
31410
31345
  });
31411
31346
  if (res.exceptionDetails) {
@@ -31413,31 +31348,160 @@ async function gameHover(client, selector) {
31413
31348
  }
31414
31349
  const info = res.result.value;
31415
31350
  if (!info?.found) {
31416
- return errorText(`No element matched ${JSON.stringify(selector)} for game_hover.`);
31351
+ return errorText(import_common_tags3.oneLine`
31352
+ No element matched ${JSON.stringify(selector)} for game_hover at index ${index}
31353
+ (matches found: ${info?.count ?? 0}).
31354
+ `);
31355
+ }
31356
+ return text(import_common_tags3.oneLine`
31357
+ Hovered ${JSON.stringify(selector)} [index ${index}] at
31358
+ (${info.x.toFixed(0)}, ${info.y.toFixed(0)}).
31359
+ Dispatched: ${info.fired.join(", ")}. Matches: ${info.count}.
31360
+ `);
31361
+ } catch (error51) {
31362
+ return toErrorResult(error51);
31363
+ }
31364
+ }
31365
+ async function gameKey(client, options) {
31366
+ const {
31367
+ key,
31368
+ count = 1,
31369
+ ctrl = false,
31370
+ shift = false,
31371
+ alt = false,
31372
+ meta: meta3 = false,
31373
+ selector,
31374
+ index = 0
31375
+ } = options;
31376
+ try {
31377
+ const res = await client.call("Runtime.evaluate", {
31378
+ expression: callPageFn(keyFn, {
31379
+ key,
31380
+ count,
31381
+ ctrl,
31382
+ shift,
31383
+ alt,
31384
+ meta: meta3,
31385
+ sel: selector ?? null,
31386
+ index
31387
+ }),
31388
+ returnByValue: true
31389
+ });
31390
+ if (res.exceptionDetails) {
31391
+ return errorText(`Key press failed: ${formatException(res.exceptionDetails)}`);
31392
+ }
31393
+ const info = res.result.value;
31394
+ if (!info) {
31395
+ return errorText(`game_key returned no result.`);
31396
+ }
31397
+ if (!info.found) {
31398
+ return errorText(import_common_tags3.oneLine`
31399
+ No element matched ${JSON.stringify(selector)} for game_key at index ${index}
31400
+ (matches found: ${info.count}).
31401
+ `);
31417
31402
  }
31403
+ const where = info.via == "selector" ? `${JSON.stringify(selector)} [index ${index}] ${info.target}` : info.via == "activeElement" ? `the focused element ${info.target}` : `document`;
31404
+ const matchNote = info.matches == null ? "" : ` Matches: ${info.matches}.`;
31418
31405
  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(", ")}.
31406
+ Pressed ${keyLabel(key, { ctrl, shift, alt, meta: meta3 })} ${info.presses}x on ${where}.${matchNote}
31407
+ Default prevented: ${info.defaultPrevented ? "yes" : "no"}.
31421
31408
  `);
31422
31409
  } catch (error51) {
31423
31410
  return toErrorResult(error51);
31424
31411
  }
31425
31412
  }
31413
+ function keyLabel(key, mods) {
31414
+ const parts = [];
31415
+ if (mods.ctrl) {
31416
+ parts.push("Ctrl");
31417
+ }
31418
+ if (mods.shift) {
31419
+ parts.push("Shift");
31420
+ }
31421
+ if (mods.alt) {
31422
+ parts.push("Alt");
31423
+ }
31424
+ if (mods.meta) {
31425
+ parts.push("Meta");
31426
+ }
31427
+ parts.push(key == " " ? "Space" : key);
31428
+ return parts.join("+");
31429
+ }
31426
31430
 
31427
- class ConsoleBuffer {
31428
- entries = [];
31429
- max;
31430
- constructor(client, max = 500) {
31431
- this.max = max;
31431
+ class ReloadTracker {
31432
+ reloadCount = 0;
31433
+ lastReloadAtMs;
31434
+ uniqueId;
31435
+ reloadListeners = new Set;
31436
+ constructor(client) {
31432
31437
  client.onConnect(async (conn) => {
31433
31438
  await conn.ensureDomain("Runtime");
31434
- await conn.ensureDomain("Log");
31435
31439
  });
31436
31440
  client.onEvent((method, params) => {
31437
31441
  this.handle(method, params);
31438
31442
  });
31439
31443
  }
31440
- read(limit, level, clear) {
31444
+ get count() {
31445
+ return this.reloadCount;
31446
+ }
31447
+ get lastReloadAt() {
31448
+ return this.lastReloadAtMs;
31449
+ }
31450
+ get contextUniqueId() {
31451
+ return this.uniqueId;
31452
+ }
31453
+ onReload(listener) {
31454
+ this.reloadListeners.add(listener);
31455
+ }
31456
+ handle(method, params) {
31457
+ if (method != "Runtime.executionContextCreated") {
31458
+ return;
31459
+ }
31460
+ const { context } = params;
31461
+ const isDefault = context?.auxData?.isDefault;
31462
+ if (!(isDefault == true || isDefault == "true") || context?.uniqueId == null) {
31463
+ return;
31464
+ }
31465
+ if (this.uniqueId == context.uniqueId) {
31466
+ return;
31467
+ }
31468
+ const isBaseline = this.uniqueId == null;
31469
+ this.uniqueId = context.uniqueId;
31470
+ if (isBaseline) {
31471
+ return;
31472
+ }
31473
+ this.reloadCount++;
31474
+ this.lastReloadAtMs = Date.now();
31475
+ for (const listener of this.reloadListeners) {
31476
+ try {
31477
+ listener(this.reloadCount);
31478
+ } catch {}
31479
+ }
31480
+ }
31481
+ }
31482
+
31483
+ class ConsoleBuffer {
31484
+ entries = [];
31485
+ max;
31486
+ constructor(client, reloads, max = 500) {
31487
+ this.max = max;
31488
+ client.onConnect(async (conn) => {
31489
+ await conn.ensureDomain("Runtime");
31490
+ await conn.ensureDomain("Log");
31491
+ });
31492
+ client.onEvent((method, params) => {
31493
+ this.handle(method, params);
31494
+ });
31495
+ reloads.onReload((count) => {
31496
+ this.push({
31497
+ ts: Date.now(),
31498
+ kind: "reload",
31499
+ level: "info",
31500
+ text: `view reloaded (#${count})`
31501
+ });
31502
+ });
31503
+ }
31504
+ read(limit, level, clear) {
31441
31505
  const filtered = level ? this.entries.filter((entry) => entry.level == level) : this.entries;
31442
31506
  const out = filtered.slice(-limit);
31443
31507
  if (clear) {
@@ -31496,214 +31560,733 @@ async function gameConsole(client, buffer, options) {
31496
31560
  return text(entries.map((entry) => `[${entry.level}] (${entry.kind}) ${entry.text}`).join(`
31497
31561
  `));
31498
31562
  }
31563
+ function callPageFn(fn, ...args) {
31564
+ const serialisedArgs = args.map((arg) => JSON.stringify(arg)).join(", ");
31565
+ return `(${fn.toString()})(${serialisedArgs})`;
31566
+ }
31567
+ function collectDomFn(sel, all, maxHtml) {
31568
+ const matches = document.querySelectorAll(sel);
31569
+ const first = matches.item(0);
31570
+ const chosen = all ? Array.from(matches) : first ? [first] : [];
31571
+ return { count: matches.length, elements: chosen.map((el) => describe3(el)) };
31572
+ function describe3(el) {
31573
+ const rect = el.getBoundingClientRect();
31574
+ const attributes = {};
31575
+ for (const attr of Array.from(el.attributes)) {
31576
+ attributes[attr.name] = attr.value;
31577
+ }
31578
+ const classAttr = el.getAttribute("class");
31579
+ let html = el.outerHTML || "";
31580
+ const truncated = html.length > maxHtml;
31581
+ if (truncated) {
31582
+ html = html.slice(0, maxHtml);
31583
+ }
31584
+ return {
31585
+ tagName: el.tagName ? el.tagName.toLowerCase() : null,
31586
+ id: el.id || null,
31587
+ classes: classAttr != null && classAttr.length > 0 ? classAttr : null,
31588
+ rect: { x: rect.x, y: rect.y, width: rect.width, height: rect.height },
31589
+ attributes,
31590
+ outerHTML: html,
31591
+ truncated
31592
+ };
31593
+ }
31594
+ }
31595
+ function findFn(args) {
31596
+ const { sel, needle, mode, caseSensitive, deepest, tag, limit } = args;
31597
+ const SNIPPET_MAX = 100;
31598
+ const target = caseSensitive ? needle : needle.toLowerCase();
31599
+ let regex;
31600
+ if (mode == "regex") {
31601
+ try {
31602
+ regex = new RegExp(needle, caseSensitive ? "" : "i");
31603
+ } catch (error51) {
31604
+ return { error: `Invalid regex: ${error51 instanceof Error ? error51.message : String(error51)}` };
31605
+ }
31606
+ }
31607
+ const found = [];
31608
+ for (const el of Array.from(document.querySelectorAll(sel))) {
31609
+ if (matches((el.textContent || "").trim())) {
31610
+ found.push(el);
31611
+ }
31612
+ }
31613
+ const kept = deepest ? found.filter((el) => !found.some((other) => other != el && el.contains(other))) : found;
31614
+ const chosen = kept.slice(0, limit);
31615
+ if (tag) {
31616
+ for (const stale of Array.from(document.querySelectorAll("[data-gf-find]"))) {
31617
+ stale.removeAttribute("data-gf-find");
31618
+ }
31619
+ for (const [i, el] of chosen.entries()) {
31620
+ el.setAttribute("data-gf-find", String(i + 1));
31621
+ }
31622
+ }
31623
+ return {
31624
+ unprunedTotal: found.length,
31625
+ total: kept.length,
31626
+ returned: chosen.length,
31627
+ tagged: tag,
31628
+ elements: chosen.map((el, i) => describe3(el, i))
31629
+ };
31630
+ function matches(raw) {
31631
+ if (mode == "regex") {
31632
+ return regex != null && regex.test(raw);
31633
+ }
31634
+ const hay = caseSensitive ? raw : raw.toLowerCase();
31635
+ if (mode == "equals") {
31636
+ return hay == target;
31637
+ }
31638
+ return hay.includes(target);
31639
+ }
31640
+ function describe3(el, i) {
31641
+ const rect = el.getBoundingClientRect();
31642
+ const classAttr = el.getAttribute("class");
31643
+ const raw = (el.textContent || "").trim();
31644
+ const truncated = raw.length > SNIPPET_MAX;
31645
+ const info = {
31646
+ tagName: el.tagName ? el.tagName.toLowerCase() : null,
31647
+ id: el.id || null,
31648
+ classes: classAttr != null && classAttr.length > 0 ? classAttr : null,
31649
+ rect: { x: rect.x, y: rect.y, width: rect.width, height: rect.height },
31650
+ text: truncated ? raw.slice(0, SNIPPET_MAX) : raw,
31651
+ truncated
31652
+ };
31653
+ if (tag) {
31654
+ info.selector = `[data-gf-find="${i + 1}"]`;
31655
+ }
31656
+ return info;
31657
+ }
31658
+ }
31659
+ function clickFn(sel, index) {
31660
+ const nodes = document.querySelectorAll(sel);
31661
+ if (nodes.length == 0) {
31662
+ return { found: false, count: 0, fired: [] };
31663
+ }
31664
+ const node = nodes[index];
31665
+ if (!node) {
31666
+ return { found: false, count: nodes.length, fired: [] };
31667
+ }
31668
+ const el = node;
31669
+ const rect = el.getBoundingClientRect();
31670
+ const cx = rect.x + rect.width / 2;
31671
+ const cy = rect.y + rect.height / 2;
31672
+ const base = {
31673
+ bubbles: true,
31674
+ cancelable: true,
31675
+ view: window,
31676
+ button: 0,
31677
+ clientX: cx,
31678
+ clientY: cy
31679
+ };
31680
+ const Pointer = typeof PointerEvent == "function" ? PointerEvent : MouseEvent;
31681
+ const fired = [];
31682
+ fire(Pointer, "pointerdown", { pointerId: 1, isPrimary: true });
31683
+ fire(MouseEvent, "mousedown");
31684
+ fire(Pointer, "pointerup", { pointerId: 1, isPrimary: true });
31685
+ fire(MouseEvent, "mouseup");
31686
+ fire(MouseEvent, "click");
31687
+ return { found: true, count: nodes.length, x: cx, y: cy, fired };
31688
+ function fire(Ctor, type, extra) {
31689
+ try {
31690
+ el.dispatchEvent(new Ctor(type, { ...base, ...extra }));
31691
+ fired.push(type);
31692
+ } catch {}
31693
+ }
31694
+ }
31695
+ function waitCheckFn(sel, visible) {
31696
+ const el = document.querySelector(sel);
31697
+ if (!el) {
31698
+ return false;
31699
+ }
31700
+ if (!visible) {
31701
+ return true;
31702
+ }
31703
+ const rect = el.getBoundingClientRect();
31704
+ return rect.width > 0 && rect.height > 0;
31705
+ }
31706
+ function fillFn(sel, value, index) {
31707
+ const nodes = document.querySelectorAll(sel);
31708
+ if (nodes.length == 0) {
31709
+ return { found: false, count: 0 };
31710
+ }
31711
+ const node = nodes[index];
31712
+ if (!node) {
31713
+ return { found: false, count: nodes.length };
31714
+ }
31715
+ const el = node;
31716
+ const count = nodes.length;
31717
+ try {
31718
+ el.focus();
31719
+ } catch {}
31720
+ const tag = (el.tagName || "").toLowerCase();
31721
+ if (el.isContentEditable) {
31722
+ el.textContent = value;
31723
+ el.dispatchEvent(new Event("input", { bubbles: true }));
31724
+ el.dispatchEvent(new Event("change", { bubbles: true }));
31725
+ return { found: true, count, mode: "contenteditable", value: el.textContent ?? "" };
31726
+ }
31727
+ const field = el;
31728
+ const proto = tag == "textarea" ? HTMLTextAreaElement.prototype : HTMLInputElement.prototype;
31729
+ const setter = Object.getOwnPropertyDescriptor(proto, "value")?.set;
31730
+ if (setter) {
31731
+ setter.call(el, value);
31732
+ } else {
31733
+ field.value = value;
31734
+ }
31735
+ el.dispatchEvent(new Event("input", { bubbles: true }));
31736
+ el.dispatchEvent(new Event("change", { bubbles: true }));
31737
+ return { found: true, count, mode: tag || "input", value: field.value };
31738
+ }
31739
+ function typeFn(sel, textToType, index) {
31740
+ const nodes = document.querySelectorAll(sel);
31741
+ if (nodes.length == 0) {
31742
+ return { found: false, count: 0, typed: 0 };
31743
+ }
31744
+ const node = nodes[index];
31745
+ if (!node) {
31746
+ return { found: false, count: nodes.length, typed: 0 };
31747
+ }
31748
+ const el = node;
31749
+ const count = nodes.length;
31750
+ try {
31751
+ el.focus();
31752
+ } catch {}
31753
+ const tag = (el.tagName || "").toLowerCase();
31754
+ const editable = el.isContentEditable;
31755
+ const field = el;
31756
+ const proto = tag == "textarea" ? HTMLTextAreaElement.prototype : HTMLInputElement.prototype;
31757
+ const setter = Object.getOwnPropertyDescriptor(proto, "value")?.set;
31758
+ let typed = 0;
31759
+ for (const ch of textToType) {
31760
+ const opts = {
31761
+ bubbles: true,
31762
+ cancelable: true,
31763
+ key: ch,
31764
+ view: window
31765
+ };
31766
+ try {
31767
+ el.dispatchEvent(new KeyboardEvent("keydown", opts));
31768
+ } catch {}
31769
+ setValue(current() + ch);
31770
+ el.dispatchEvent(new Event("input", { bubbles: true }));
31771
+ try {
31772
+ el.dispatchEvent(new KeyboardEvent("keyup", opts));
31773
+ } catch {}
31774
+ typed++;
31775
+ }
31776
+ el.dispatchEvent(new Event("change", { bubbles: true }));
31777
+ return { found: true, count, typed, value: current() };
31778
+ function current() {
31779
+ if (editable) {
31780
+ return el.textContent ?? "";
31781
+ }
31782
+ return field.value || "";
31783
+ }
31784
+ function setValue(next) {
31785
+ if (editable) {
31786
+ el.textContent = next;
31787
+ } else if (setter) {
31788
+ setter.call(el, next);
31789
+ } else {
31790
+ field.value = next;
31791
+ }
31792
+ }
31793
+ }
31794
+ function hoverFn(sel, index) {
31795
+ const nodes = document.querySelectorAll(sel);
31796
+ if (nodes.length == 0) {
31797
+ return { found: false, count: 0 };
31798
+ }
31799
+ const node = nodes[index];
31800
+ if (!node) {
31801
+ return { found: false, count: nodes.length };
31802
+ }
31803
+ const el = node;
31804
+ const count = nodes.length;
31805
+ const rect = el.getBoundingClientRect();
31806
+ const cx = rect.x + rect.width / 2;
31807
+ const cy = rect.y + rect.height / 2;
31808
+ const base = {
31809
+ bubbles: true,
31810
+ cancelable: true,
31811
+ view: window,
31812
+ clientX: cx,
31813
+ clientY: cy
31814
+ };
31815
+ const noBubble = { ...base, bubbles: false };
31816
+ const fired = [];
31817
+ fire("pointerover", base);
31818
+ fire("mouseover", base);
31819
+ fire("pointerenter", noBubble);
31820
+ fire("mouseenter", noBubble);
31821
+ fire("mousemove", base);
31822
+ return { found: true, count, x: cx, y: cy, fired };
31823
+ function fire(type, init) {
31824
+ try {
31825
+ el.dispatchEvent(new MouseEvent(type, init));
31826
+ fired.push(type);
31827
+ } catch {}
31828
+ }
31829
+ }
31830
+ function keyFn(args) {
31831
+ const { key, count, ctrl, shift, alt, meta: meta3, sel, index } = args;
31832
+ let target;
31833
+ let via;
31834
+ let node = null;
31835
+ let matches = null;
31836
+ if (sel == null) {
31837
+ const active = document.activeElement;
31838
+ if (active) {
31839
+ target = active;
31840
+ node = active;
31841
+ via = "activeElement";
31842
+ } else {
31843
+ target = document.body || document;
31844
+ node = document.body;
31845
+ via = "document";
31846
+ }
31847
+ } else {
31848
+ const nodes = document.querySelectorAll(sel);
31849
+ if (nodes.length == 0) {
31850
+ return { found: false, count: 0 };
31851
+ }
31852
+ const chosen = nodes[index];
31853
+ if (!chosen) {
31854
+ return { found: false, count: nodes.length };
31855
+ }
31856
+ try {
31857
+ chosen.focus();
31858
+ } catch {}
31859
+ target = chosen;
31860
+ node = chosen;
31861
+ matches = nodes.length;
31862
+ via = "selector";
31863
+ }
31864
+ const map3 = codeFor(key);
31865
+ const init = {
31866
+ bubbles: true,
31867
+ cancelable: true,
31868
+ view: window,
31869
+ key,
31870
+ ctrlKey: ctrl,
31871
+ shiftKey: shift,
31872
+ altKey: alt,
31873
+ metaKey: meta3
31874
+ };
31875
+ if (map3) {
31876
+ init.code = map3.code;
31877
+ }
31878
+ let defaultPrevented = false;
31879
+ let presses = 0;
31880
+ for (let i = 0;i < count; i++) {
31881
+ if (dispatch("keydown")) {
31882
+ defaultPrevented = true;
31883
+ }
31884
+ dispatch("keyup");
31885
+ presses++;
31886
+ }
31887
+ return {
31888
+ found: true,
31889
+ via,
31890
+ target: node ? describe3(node) : "document",
31891
+ matches,
31892
+ presses,
31893
+ defaultPrevented
31894
+ };
31895
+ function dispatch(type) {
31896
+ try {
31897
+ const ev = new KeyboardEvent(type, init);
31898
+ force(ev, "key", key);
31899
+ if (map3) {
31900
+ force(ev, "code", map3.code);
31901
+ force(ev, "keyCode", map3.keyCode);
31902
+ force(ev, "which", map3.keyCode);
31903
+ }
31904
+ const notPrevented = target.dispatchEvent(ev);
31905
+ return type == "keydown" && !notPrevented;
31906
+ } catch {
31907
+ return false;
31908
+ }
31909
+ }
31910
+ function force(ev, prop, value) {
31911
+ try {
31912
+ Object.defineProperty(ev, prop, { get: () => value, configurable: true });
31913
+ } catch {}
31914
+ }
31915
+ function codeFor(name) {
31916
+ const table = {
31917
+ Escape: { code: "Escape", keyCode: 27 },
31918
+ Enter: { code: "Enter", keyCode: 13 },
31919
+ Tab: { code: "Tab", keyCode: 9 },
31920
+ " ": { code: "Space", keyCode: 32 },
31921
+ Backspace: { code: "Backspace", keyCode: 8 },
31922
+ Delete: { code: "Delete", keyCode: 46 },
31923
+ Home: { code: "Home", keyCode: 36 },
31924
+ End: { code: "End", keyCode: 35 },
31925
+ PageUp: { code: "PageUp", keyCode: 33 },
31926
+ PageDown: { code: "PageDown", keyCode: 34 },
31927
+ ArrowUp: { code: "ArrowUp", keyCode: 38 },
31928
+ ArrowDown: { code: "ArrowDown", keyCode: 40 },
31929
+ ArrowLeft: { code: "ArrowLeft", keyCode: 37 },
31930
+ ArrowRight: { code: "ArrowRight", keyCode: 39 }
31931
+ };
31932
+ const known = table[name];
31933
+ if (known) {
31934
+ return known;
31935
+ }
31936
+ const F1_KEY_CODE = 112;
31937
+ const fn = /^F(?<num>[1-9]|1[0-2])$/u.exec(name);
31938
+ if (fn) {
31939
+ const num2 = Number(fn.groups?.num);
31940
+ return { code: `F${num2}`, keyCode: F1_KEY_CODE + num2 - 1 };
31941
+ }
31942
+ if (/^[a-zA-Z]$/u.test(name)) {
31943
+ const upper = name.toUpperCase();
31944
+ return { code: `Key${upper}`, keyCode: upper.codePointAt(0) ?? 0 };
31945
+ }
31946
+ if (/^[0-9]$/u.test(name)) {
31947
+ return { code: `Digit${name}`, keyCode: name.codePointAt(0) ?? 0 };
31948
+ }
31949
+ return;
31950
+ }
31951
+ function describe3(el) {
31952
+ const tag = el.tagName ? el.tagName.toLowerCase() : "node";
31953
+ const id = el.id ? `#${el.id}` : "";
31954
+ const classAttr = el.getAttribute("class");
31955
+ const cls = classAttr && classAttr.trim() ? `.${classAttr.trim().split(/\s+/u)[0]}` : "";
31956
+ return `<${tag}${id}${cls}>`;
31957
+ }
31958
+ }
31959
+ function rectFn(sel, index) {
31960
+ const nodes = document.querySelectorAll(sel);
31961
+ if (nodes.length == 0) {
31962
+ return { found: false, count: 0 };
31963
+ }
31964
+ const el = nodes[index];
31965
+ if (!el) {
31966
+ return { found: false, count: nodes.length };
31967
+ }
31968
+ const rect = el.getBoundingClientRect();
31969
+ return {
31970
+ found: true,
31971
+ count: nodes.length,
31972
+ x: rect.x,
31973
+ y: rect.y,
31974
+ width: rect.width,
31975
+ height: rect.height
31976
+ };
31977
+ }
31499
31978
 
31500
31979
  // src/server.ts
31501
31980
  var { version: VERSION } = JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf8"));
31502
31981
  async function main() {
31503
31982
  const config2 = loadConfig();
31504
31983
  const client = new CdpClient(config2);
31505
- const consoleBuffer = new ConsoleBuffer(client);
31506
- const debug = new DebuggerSession(client);
31984
+ const reloads = new ReloadTracker(client);
31985
+ const consoleBuffer = new ConsoleBuffer(client, reloads);
31986
+ const debug = new DebuggerSession(client, reloads);
31507
31987
  const server = new McpServer({ name: "gameface-devtools-mcp", version: VERSION });
31508
31988
  server.registerTool("game_status", {
31509
- title: "Gameface UI status",
31989
+ title: `Gameface UI status`,
31510
31990
  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.
31991
+ Check whether the Gameface UI debug endpoint is reachable and report the live page target,
31992
+ engine info, and view-reload tracking (count, last reload time, context id).
31993
+ Calling it arms reload tracking and returns the baseline count for game_wait's sinceReloads.
31994
+ Run this first when other game_* tools fail.
31513
31995
  `
31514
- }, () => gameStatus(client));
31996
+ }, () => gameStatus(client, reloads));
31515
31997
  server.registerTool("game_eval", {
31516
- title: "Evaluate JS in the Gameface UI",
31998
+ title: `Evaluate JS in the Gameface UI`,
31517
31999
  description: import_common_tags4.oneLine`
31518
32000
  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.
32001
+ returnByValue) and return the resulting value as JSON.
32002
+ Use document.querySelector and friends to read the live DOM, inspect state, or call UI APIs.
31521
32003
  `,
31522
32004
  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")
32005
+ expression: exports_external.string().describe(`JavaScript expression to evaluate in the page context`),
32006
+ awaitPromise: exports_external.boolean().optional().describe(`If the expression returns a Promise, await it before returning`)
31525
32007
  }
31526
32008
  }, ({ expression, awaitPromise }) => gameEval(client, expression, awaitPromise));
31527
32009
  server.registerTool("game_screenshot", {
31528
- title: "Screenshot the Gameface UI",
32010
+ title: `Screenshot the Gameface UI`,
31529
32011
  description: import_common_tags4.oneLine`
31530
32012
  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.
32013
+ as an inline image.
32014
+ Pass a selector to clip the capture to one element; use index to pick among matches.
32015
+ Hangs while the JS debugger is paused; resume (game_debug_step) before capturing.
31533
32016
  `,
31534
32017
  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")
32018
+ format: exports_external.enum(["png", "jpeg"]).optional().describe(`Image format (default: jpeg)`),
32019
+ quality: exports_external.number().min(1).max(100).optional().describe(`JPEG quality 1-100 (only used when format is jpeg; default 80)`),
32020
+ selector: exports_external.string().optional().describe(`If set, clip the screenshot to this element's bounding box`),
32021
+ index: exports_external.number().int().min(0).optional().describe(`Which match to clip to when several exist (default: 0)`)
31538
32022
  }
31539
- }, ({ format, quality, selector }) => gameScreenshot(client, format, quality, selector));
32023
+ }, ({ format, quality, selector, index }) => gameScreenshot(client, { format, quality, selector, index }));
31540
32024
  server.registerTool("game_wait", {
31541
- title: "Wait for a condition in the Gameface UI",
32025
+ title: `Wait for a condition in the Gameface UI`,
31542
32026
  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.
32027
+ Wait until a CSS selector matches (optionally visible), a JS predicate becomes truthy,
32028
+ and/or a view reload happens. Provide at least one of reload / selector / predicate
32029
+ (selector and predicate are mutually exclusive).
32030
+ With reload, the phases compose: reload first, then a quiescence window, then the
32031
+ selector/predicate poll in the fresh context.
32032
+ Returns when met or times out.
31546
32033
  `,
31547
32034
  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 }));
32035
+ selector: exports_external.string().optional().describe(`CSS selector to wait for`),
32036
+ predicate: exports_external.string().optional().describe(`JS expression evaluated in the page; waits until it is truthy`),
32037
+ reload: exports_external.boolean().optional().describe(import_common_tags4.oneLine`
32038
+ Wait for a view reload (context reset) before the selector/predicate phase.
32039
+ Without sinceReloads, waits for the next reload after the call starts.
32040
+ `),
32041
+ sinceReloads: exports_external.number().int().min(0).optional().describe(import_common_tags4.oneLine`
32042
+ Baseline reload count (from a prior game_status or game_wait).
32043
+ The reload phase is satisfied as soon as the count exceeds it, even if the reload
32044
+ already happened; use it to avoid racing a reload you triggered yourself.
32045
+ `),
32046
+ quiescentMs: exports_external.number().int().min(0).optional().describe(import_common_tags4.oneLine`
32047
+ After a reload is observed, hold until no further context swap for this long (default
32048
+ 1000, 0 disables); absorbs engines that swap the context several times per reload.
32049
+ `),
32050
+ timeoutMs: exports_external.number().int().min(0).optional().describe(import_common_tags4.oneLine`
32051
+ Max time to wait in ms (default 8000, or 30000 when reload is set; capped at 60000)
32052
+ `),
32053
+ visible: exports_external.boolean().optional().describe(`For selector waits, also require a non-zero bounding box (default false)`)
32054
+ }
32055
+ }, ({ selector, predicate, reload, sinceReloads, quiescentMs, timeoutMs, visible }) => gameWait(client, reloads, {
32056
+ selector,
32057
+ predicate,
32058
+ reload,
32059
+ sinceReloads,
32060
+ quiescentMs,
32061
+ timeoutMs,
32062
+ visible
32063
+ }));
31554
32064
  server.registerTool("game_fill", {
31555
- title: "Set an input value in the Gameface UI",
32065
+ title: `Set an input value in the Gameface UI`,
31556
32066
  description: import_common_tags4.oneLine`
31557
32067
  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.
32068
+ the UI framework reacts as if the user edited it.
32069
+ Best for setting a field in one shot; use game_type for keystrokes.
32070
+ Use index to pick among matches.
31559
32071
  `,
31560
32072
  inputSchema: {
31561
- selector: exports_external.string().describe("CSS selector of the field to fill"),
31562
- value: exports_external.string().describe("Value to set")
32073
+ selector: exports_external.string().describe(`CSS selector of the field to fill`),
32074
+ value: exports_external.string().describe(`Value to set`),
32075
+ index: exports_external.number().int().min(0).optional().describe(`Which match to fill when several exist (default: 0)`)
31563
32076
  }
31564
- }, ({ selector, value }) => gameFill(client, selector, value));
32077
+ }, ({ selector, value, index }) => gameFill(client, selector, value, index));
31565
32078
  server.registerTool("game_type", {
31566
- title: "Type text into the Gameface UI",
32079
+ title: `Type text into the Gameface UI`,
31567
32080
  description: import_common_tags4.oneLine`
31568
32081
  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.
32082
+ the value in sync.
32083
+ Use when handlers react to individual keystrokes; otherwise game_fill.
32084
+ Use index to pick among matches.
31570
32085
  `,
31571
32086
  inputSchema: {
31572
- selector: exports_external.string().describe("CSS selector of the field to type into"),
31573
- text: exports_external.string().describe("Text to type")
32087
+ selector: exports_external.string().describe(`CSS selector of the field to type into`),
32088
+ text: exports_external.string().describe(`Text to type`),
32089
+ index: exports_external.number().int().min(0).optional().describe(`Which match to type into when several exist (default: 0)`)
31574
32090
  }
31575
- }, ({ selector, text: text2 }) => gameType(client, selector, text2));
32091
+ }, ({ selector, text: text2, index }) => gameType(client, selector, text2, index));
31576
32092
  server.registerTool("game_hover", {
31577
- title: "Hover an element in the Gameface UI",
32093
+ title: `Hover an element in the Gameface UI`,
31578
32094
  description: import_common_tags4.oneLine`
31579
32095
  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.
32096
+ the UI's mouseenter / pointerover JS handlers (tooltips) fire.
32097
+ The CSS :hover state is NOT set (only real game-forwarded mouse input sets it); verify a
32098
+ hover by its DOM effect, never by styling.
32099
+ Use index to pick among matches.
31581
32100
  `,
31582
32101
  inputSchema: {
31583
- selector: exports_external.string().describe("CSS selector of the element to hover")
32102
+ selector: exports_external.string().describe(`CSS selector of the element to hover`),
32103
+ index: exports_external.number().int().min(0).optional().describe(`Which match to hover when several exist (default: 0)`)
31584
32104
  }
31585
- }, ({ selector }) => gameHover(client, selector));
32105
+ }, ({ selector, index }) => gameHover(client, selector, index));
32106
+ server.registerTool("game_key", {
32107
+ title: `Press a key in the Gameface UI`,
32108
+ description: import_common_tags4.oneLine`
32109
+ Press a named key by dispatching a real bubbling keydown+keyup in the page
32110
+ (KeyboardEvent.key, e.g. Escape, Enter, ArrowDown, a, F5), optionally with
32111
+ ctrl/shift/alt/meta and a repeat count.
32112
+ With a selector it focuses that element and dispatches on it (index picks among matches);
32113
+ without one it dispatches on the focused element, else document.
32114
+ It fires ONLY keydown and keyup (no keypress) and performs NO default action: no character
32115
+ insertion, no Backspace delete, no Tab focus move, no scrolling; use game_type to enter
32116
+ text.
32117
+ It reaches the UI's JS keydown handlers, but keys the game routes through its own native
32118
+ input layer do NOT respond (e.g. an Escape-to-close handled by the engine rather than the
32119
+ DOM).
32120
+ The result reports whether a handler called preventDefault, the observable signal the key
32121
+ was consumed.
32122
+ `,
32123
+ inputSchema: {
32124
+ key: exports_external.string().describe(`KeyboardEvent.key name to press, e.g. Escape, Enter, ArrowDown, a, F5`),
32125
+ count: exports_external.number().int().min(1).max(100).optional().describe(`How many keydown+keyup presses to fire back-to-back (default 1)`),
32126
+ ctrl: exports_external.boolean().optional().describe(`Hold Ctrl (ctrlKey) during the press (default false)`),
32127
+ shift: exports_external.boolean().optional().describe(`Hold Shift (shiftKey) during the press (default false)`),
32128
+ alt: exports_external.boolean().optional().describe(`Hold Alt (altKey) during the press (default false)`),
32129
+ meta: exports_external.boolean().optional().describe(`Hold Meta / Win / Cmd (metaKey) during the press (default false)`),
32130
+ selector: exports_external.string().optional().describe(`If set, focus this element and dispatch on it; else the focused element`),
32131
+ index: exports_external.number().int().min(0).optional().describe(`Which match to target when the selector has several (default: 0)`)
32132
+ }
32133
+ }, ({ key, count, ctrl, shift, alt, meta: meta3, selector, index }) => gameKey(client, { key, count, ctrl, shift, alt, meta: meta3, selector, index }));
31586
32134
  server.registerTool("game_console", {
31587
- title: "Read the Gameface UI console",
32135
+ title: `Read the Gameface UI console`,
31588
32136
  description: import_common_tags4.oneLine`
31589
32137
  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.
32138
+ Gameface UI.
32139
+ Capture starts when the server first connects to the application.
31591
32140
  `,
31592
32141
  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)")
32142
+ limit: exports_external.number().int().min(1).max(1000).optional().describe(`Max entries to return (default 50)`),
32143
+ level: exports_external.string().optional().describe(`Filter by level, e.g. error / warning / log / info`),
32144
+ clear: exports_external.boolean().optional().describe(`Clear the buffer after reading (default false)`)
31596
32145
  }
31597
32146
  }, ({ limit, level, clear }) => gameConsole(client, consoleBuffer, { limit, level, clear }));
31598
32147
  server.registerTool("game_dom", {
31599
- title: "Inspect Gameface UI DOM",
32148
+ title: `Inspect Gameface UI DOM`,
31600
32149
  description: import_common_tags4.oneLine`
31601
32150
  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.
32151
+ matching a CSS selector in the live Gameface UI.
32152
+ Set all=true to return every match.
31603
32153
  `,
31604
32154
  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)")
32155
+ selector: exports_external.string().describe(`CSS selector to query in the Gameface UI`),
32156
+ all: exports_external.boolean().optional().describe(`Return all matches instead of just the first (default: false)`),
32157
+ maxHtml: exports_external.number().min(0).optional().describe(`Max outerHTML characters per element before truncation (default: 4000)`)
31608
32158
  }
31609
32159
  }, ({ selector, all, maxHtml }) => gameDom(client, selector, all, maxHtml));
32160
+ server.registerTool("game_find", {
32161
+ title: `Find elements by text in the Gameface UI`,
32162
+ description: import_common_tags4.oneLine`
32163
+ Locate elements by their text content in the live Gameface UI: scan a CSS selector's matches
32164
+ (default: every element) and filter on trimmed textContent by equals/contains/regex
32165
+ (case-insensitive by default).
32166
+ Returns tag, id, classes, and bounding rect per match, plus match counts before and after
32167
+ deepest pruning so pruning and limit truncation are both visible.
32168
+ Set tag=true to stamp matches with data-gf-find handles and get back ready-to-use selectors
32169
+ for game_click / game_hover / game_screenshot.
32170
+ The go-to way to find an element when class names are build-hashed and there is no XPath.
32171
+ `,
32172
+ inputSchema: {
32173
+ text: exports_external.string().describe(`Text to match against each element's trimmed textContent`),
32174
+ match: exports_external.enum(["equals", "contains", "regex"]).optional().describe(`How to match the text: equals / contains / regex (default: contains)`),
32175
+ caseSensitive: exports_external.boolean().optional().describe(`Match case-sensitively (default: false)`),
32176
+ selector: exports_external.string().optional().describe(`CSS selector scoping the scan (default: *, every element)`),
32177
+ deepest: exports_external.boolean().optional().describe(`Keep only the innermost match, pruning ancestors that also matched (default: true)`),
32178
+ tag: exports_external.boolean().optional().describe(import_common_tags4.oneLine`
32179
+ Stamp matches with data-gf-find handles and return them as selectors, clearing any
32180
+ prior handles first (default: false).
32181
+ `),
32182
+ limit: exports_external.number().int().min(1).max(100).optional().describe(`Max matches to return (default: 20); the total count is always reported`)
32183
+ }
32184
+ }, ({ text: text2, match, caseSensitive, selector, deepest, tag, limit }) => gameFind(client, { text: text2, match, caseSensitive, selector, deepest, tag, limit }));
31610
32185
  server.registerTool("game_click", {
31611
- title: "Click an element in the Gameface UI",
32186
+ title: `Click an element in the Gameface UI`,
31612
32187
  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.
32188
+ Click the element matching a CSS selector by dispatching a real bubbling pointer/mouse/click
32189
+ sequence in the page (NOT CDP Input, which Gameface ignores for the UI).
32190
+ Use index to pick among matches.
31616
32191
  `,
31617
32192
  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)")
32193
+ selector: exports_external.string().describe(`CSS selector of the element to click`),
32194
+ index: exports_external.number().int().min(0).optional().describe(`Which match to click when several exist (default: 0)`)
31620
32195
  }
31621
32196
  }, ({ selector, index }) => gameClick(client, selector, index));
31622
32197
  server.registerTool("game_debug_status", {
31623
- title: "JS debugger status",
32198
+ title: `JS debugger status`,
31624
32199
  description: import_common_tags4.oneLine`
31625
32200
  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.
32201
+ and parsed script count.
32202
+ Pass setPauseOnExceptions to change exception pausing.
32203
+ Enables the debugger on first use.
32204
+ Hitting a breakpoint FREEZES the UI until resumed.
31628
32205
  `,
31629
32206
  inputSchema: {
31630
- setPauseOnExceptions: exports_external.enum(["none", "uncaught", "all"]).optional().describe("If set, change which exceptions pause execution (default none)")
32207
+ setPauseOnExceptions: exports_external.enum(["none", "uncaught", "all"]).optional().describe(`If set, change which exceptions pause execution (default none)`)
31631
32208
  }
31632
32209
  }, ({ setPauseOnExceptions }) => debug.status(setPauseOnExceptions));
31633
32210
  server.registerTool("game_debug_scripts", {
31634
- title: "List parsed UI scripts",
32211
+ title: `List parsed UI scripts`,
31635
32212
  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.
32213
+ List JavaScript scripts parsed in the Gameface UI (scriptId + url + line count), optionally
32214
+ filtered by a url substring.
32215
+ Only scripts parsed after the debugger attached appear (Gameface does not replay
32216
+ scriptParsed); an empty list means attach first, then trigger a view reload to repopulate.
32217
+ Use the scriptId with game_debug_source.
31638
32218
  `,
31639
32219
  inputSchema: {
31640
- filter: exports_external.string().optional().describe("Only scripts whose url contains this substring")
32220
+ filter: exports_external.string().optional().describe(`Only scripts whose url contains this substring`)
31641
32221
  }
31642
32222
  }, ({ filter }) => debug.listScripts(filter));
31643
32223
  server.registerTool("game_debug_source", {
31644
- title: "Get UI script source",
32224
+ title: `Get UI script source`,
31645
32225
  description: import_common_tags4.oneLine`
31646
32226
  Return the source of a script (by scriptId from game_debug_scripts), with line numbers.
31647
32227
  Pass lineStart/lineEnd to get a range (large scripts are capped at 400 lines).
31648
32228
  `,
31649
32229
  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)")
32230
+ scriptId: exports_external.string().describe(`Script id from game_debug_scripts`),
32231
+ lineStart: exports_external.number().int().min(1).optional().describe(`First line (1-based)`),
32232
+ lineEnd: exports_external.number().int().min(1).optional().describe(`Last line (1-based)`)
31653
32233
  }
31654
32234
  }, ({ scriptId, lineStart, lineEnd }) => debug.getSource(scriptId, lineStart, lineEnd));
31655
32235
  server.registerTool("game_debug_set_breakpoint", {
31656
- title: "Set a breakpoint",
32236
+ title: `Set a breakpoint`,
31657
32237
  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.
32238
+ Set a breakpoint by url substring + line (1-based).
32239
+ Add a condition (JS expression) to only pause when it is truthy, which limits how often the
32240
+ UI freezes.
32241
+ Hitting it FREEZES the UI until you resume with game_debug_step.
31661
32242
  `,
31662
32243
  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")
32244
+ urlContains: exports_external.string().describe(`Substring of the script url to break in`),
32245
+ line: exports_external.number().int().min(1).describe(`Line number (1-based)`),
32246
+ column: exports_external.number().int().min(0).optional().describe(`Column (0-based), optional`),
32247
+ condition: exports_external.string().optional().describe(`Optional JS condition; pause only when it evaluates truthy`)
31667
32248
  }
31668
32249
  }, ({ urlContains, line, column, condition }) => debug.setBreakpoint(urlContains, line, column, condition));
31669
32250
  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'.",
32251
+ title: `Remove a breakpoint`,
32252
+ description: `Remove a breakpoint by its id (from game_debug_status), or pass 'all'.`,
31672
32253
  inputSchema: {
31673
- breakpoint: exports_external.string().describe("Breakpoint id, or 'all'")
32254
+ breakpoint: exports_external.string().describe(`Breakpoint id, or 'all'`)
31674
32255
  }
31675
32256
  }, ({ breakpoint }) => debug.removeBreakpoint(breakpoint));
31676
32257
  server.registerTool("game_debug_pause_state", {
31677
- title: "Inspect the paused stack",
32258
+ title: `Inspect the paused stack`,
31678
32259
  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.
32260
+ When paused, return the call stack (frames with function + location + scope types).
32261
+ Set expandScopes to also list local/closure variables of each frame.
32262
+ Returns 'not paused' otherwise.
31682
32263
  `,
31683
32264
  inputSchema: {
31684
- expandScopes: exports_external.boolean().optional().describe("Also list local/closure variables per frame (default false)")
32265
+ expandScopes: exports_external.boolean().optional().describe(`Also list local/closure variables per frame (default false)`)
31685
32266
  }
31686
32267
  }, ({ expandScopes }) => debug.pauseStateReport(expandScopes ?? false));
31687
32268
  server.registerTool("game_debug_evaluate", {
31688
- title: "Evaluate while debugging",
32269
+ title: `Evaluate while debugging`,
31689
32270
  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.
32271
+ Evaluate a JS expression.
32272
+ When paused, it runs in the selected call frame's scope (Debugger.evaluateOnCallFrame) so
32273
+ you can read locals; otherwise it runs globally.
32274
+ Prefer this over game_eval while paused.
31693
32275
  `,
31694
32276
  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)")
32277
+ expression: exports_external.string().describe(`JS expression to evaluate`),
32278
+ frameIndex: exports_external.number().int().min(0).optional().describe(`Call frame index to evaluate in when paused (default 0 = top)`)
31697
32279
  }
31698
32280
  }, ({ expression, frameIndex }) => debug.evaluate(expression, frameIndex));
31699
32281
  server.registerTool("game_debug_step", {
31700
- title: "Step / resume / pause execution",
32282
+ title: `Step / resume / pause execution`,
31701
32283
  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.
32284
+ Control paused execution: resume (unfreeze the UI), over/into/out (step), or pause (break at
32285
+ the next statement).
32286
+ Stepping reports the new location.
31704
32287
  `,
31705
32288
  inputSchema: {
31706
- action: exports_external.enum(["resume", "over", "into", "out", "pause"]).describe("resume | over | into | out | pause")
32289
+ action: exports_external.enum(["resume", "over", "into", "out", "pause"]).describe(`resume | over | into | out | pause`)
31707
32290
  }
31708
32291
  }, ({ action }) => debug.step(action));
31709
32292
  const transport = new StdioServerTransport;