@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.
- package/README.md +21 -16
- package/dist/server.mjs +923 -340
- 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(
|
|
30403
|
-
ws.addEventListener("error", () => this.handleClose(
|
|
30402
|
+
ws.addEventListener("close", () => this.handleClose(`connection closed`));
|
|
30403
|
+
ws.addEventListener("error", () => this.handleClose(`connection error`));
|
|
30404
30404
|
}
|
|
30405
30405
|
static async open(cfg, target, onEvent) {
|
|
30406
30406
|
const ws = new WebSocket(target.wsUrl);
|
|
@@ -30427,7 +30427,7 @@ class CdpConnection {
|
|
|
30427
30427
|
}
|
|
30428
30428
|
call(method, params) {
|
|
30429
30429
|
if (!this.isOpen) {
|
|
30430
|
-
return Promise.reject(new CdpError(
|
|
30430
|
+
return Promise.reject(new CdpError(`CDP connection is not open`));
|
|
30431
30431
|
}
|
|
30432
30432
|
const id = this.nextId++;
|
|
30433
30433
|
return new Promise((resolve, reject) => {
|
|
@@ -30453,7 +30453,7 @@ class CdpConnection {
|
|
|
30453
30453
|
this.enabledDomains.add(domain2);
|
|
30454
30454
|
}
|
|
30455
30455
|
close() {
|
|
30456
|
-
this.handleClose(
|
|
30456
|
+
this.handleClose(`closed by client`);
|
|
30457
30457
|
try {
|
|
30458
30458
|
this.ws.close();
|
|
30459
30459
|
} catch {}
|
|
@@ -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 ??
|
|
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(
|
|
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(
|
|
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(
|
|
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
|
-
|
|
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,
|
|
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(`
|
|
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(
|
|
31109
|
+
return errorText(`Page.captureScreenshot returned no image data.`);
|
|
31275
31110
|
}
|
|
31276
|
-
|
|
31277
|
-
|
|
31278
|
-
|
|
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 {
|
|
31330
|
-
|
|
31331
|
-
|
|
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
|
|
31337
|
-
let lastError;
|
|
31220
|
+
const deadline = start + budget;
|
|
31338
31221
|
try {
|
|
31339
|
-
|
|
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
|
-
|
|
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(`
|
|
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(`
|
|
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(`
|
|
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
|
-
|
|
31420
|
-
|
|
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
|
|
31428
|
-
|
|
31429
|
-
|
|
31430
|
-
|
|
31431
|
-
|
|
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
|
-
|
|
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
|
|
31506
|
-
const
|
|
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:
|
|
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
|
-
|
|
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:
|
|
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.
|
|
31520
|
-
friends to read the live DOM, inspect
|
|
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(
|
|
31524
|
-
awaitPromise: exports_external.boolean().optional().describe(
|
|
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:
|
|
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.
|
|
31532
|
-
|
|
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(
|
|
31536
|
-
quality: exports_external.number().min(1).max(100).optional().describe(
|
|
31537
|
-
selector: exports_external.string().optional().describe(
|
|
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:
|
|
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)
|
|
31544
|
-
|
|
31545
|
-
|
|
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(
|
|
31549
|
-
predicate: exports_external.string().optional().describe(
|
|
31550
|
-
|
|
31551
|
-
|
|
31552
|
-
|
|
31553
|
-
|
|
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:
|
|
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
|
-
|
|
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(
|
|
31562
|
-
value: exports_external.string().describe(
|
|
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:
|
|
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.
|
|
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(
|
|
31573
|
-
text: exports_external.string().describe(
|
|
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:
|
|
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
|
-
|
|
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(
|
|
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:
|
|
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.
|
|
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(
|
|
31594
|
-
level: exports_external.string().optional().describe(
|
|
31595
|
-
clear: exports_external.boolean().optional().describe(
|
|
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:
|
|
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.
|
|
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(
|
|
31606
|
-
all: exports_external.boolean().optional().describe(
|
|
31607
|
-
maxHtml: exports_external.number().min(0).optional().describe(
|
|
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:
|
|
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
|
-
|
|
31615
|
-
|
|
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(
|
|
31619
|
-
index: exports_external.number().int().min(0).optional().describe(
|
|
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:
|
|
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.
|
|
31627
|
-
|
|
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(
|
|
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:
|
|
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
|
-
|
|
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(
|
|
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:
|
|
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(
|
|
31651
|
-
lineStart: exports_external.number().int().min(1).optional().describe(
|
|
31652
|
-
lineEnd: exports_external.number().int().min(1).optional().describe(
|
|
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:
|
|
32236
|
+
title: `Set a breakpoint`,
|
|
31657
32237
|
description: import_common_tags4.oneLine`
|
|
31658
|
-
Set a breakpoint by url substring + line (1-based).
|
|
31659
|
-
only pause when it is truthy, which limits how often the
|
|
31660
|
-
|
|
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(
|
|
31664
|
-
line: exports_external.number().int().min(1).describe(
|
|
31665
|
-
column: exports_external.number().int().min(0).optional().describe(
|
|
31666
|
-
condition: exports_external.string().optional().describe(
|
|
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:
|
|
31671
|
-
description:
|
|
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(
|
|
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:
|
|
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).
|
|
31680
|
-
expandScopes to also list local/closure variables of each frame.
|
|
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(
|
|
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:
|
|
32269
|
+
title: `Evaluate while debugging`,
|
|
31689
32270
|
description: import_common_tags4.oneLine`
|
|
31690
|
-
Evaluate a JS expression.
|
|
31691
|
-
|
|
31692
|
-
|
|
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(
|
|
31696
|
-
frameIndex: exports_external.number().int().min(0).optional().describe(
|
|
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:
|
|
32282
|
+
title: `Step / resume / pause execution`,
|
|
31701
32283
|
description: import_common_tags4.oneLine`
|
|
31702
|
-
Control paused execution: resume (unfreeze the UI), over
|
|
31703
|
-
|
|
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(
|
|
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;
|