gdharness 1.0.2 → 1.0.3
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/build/index.js
CHANGED
|
@@ -10183,7 +10183,7 @@ import {
|
|
|
10183
10183
|
readSync
|
|
10184
10184
|
} from "node:fs";
|
|
10185
10185
|
import { tmpdir as tmpdir4 } from "node:os";
|
|
10186
|
-
import { basename as basename3, dirname as
|
|
10186
|
+
import { basename as basename3, dirname as dirname7, join as join13, normalize as normalize2, relative as relative3 } from "node:path";
|
|
10187
10187
|
import { setTimeout as delay3 } from "node:timers/promises";
|
|
10188
10188
|
import { fileURLToPath as fileURLToPath2 } from "node:url";
|
|
10189
10189
|
import { promisify as promisify5 } from "node:util";
|
|
@@ -27713,20 +27713,61 @@ async function handleLSPTool(client, toolName, args) {
|
|
|
27713
27713
|
}
|
|
27714
27714
|
|
|
27715
27715
|
// src/process-children.ts
|
|
27716
|
-
import { execFile as execFile3 } from "node:child_process";
|
|
27716
|
+
import { execFile as execFile3, execFileSync, spawnSync } from "node:child_process";
|
|
27717
27717
|
import { basename } from "node:path";
|
|
27718
27718
|
import { promisify as promisify3 } from "node:util";
|
|
27719
27719
|
var run3 = promisify3(execFile3);
|
|
27720
|
+
function startTimesOf(pids) {
|
|
27721
|
+
const began = new Map;
|
|
27722
|
+
if (pids.length === 0) {
|
|
27723
|
+
return began;
|
|
27724
|
+
}
|
|
27725
|
+
try {
|
|
27726
|
+
if (process.platform === "win32") {
|
|
27727
|
+
const filter = pids.map((pid) => `ProcessId=${pid}`).join(" OR ");
|
|
27728
|
+
const said = execFileSync("powershell", [
|
|
27729
|
+
"-NoProfile",
|
|
27730
|
+
"-NonInteractive",
|
|
27731
|
+
"-Command",
|
|
27732
|
+
`Get-CimInstance Win32_Process -Filter "${filter}" | ForEach-Object { "$($_.ProcessId) $(([DateTimeOffset]$_.CreationDate).ToUnixTimeMilliseconds())" }`
|
|
27733
|
+
], { encoding: "utf8", timeout: ASK_TIMEOUT_MS, windowsHide: true });
|
|
27734
|
+
for (const line of said.split(`
|
|
27735
|
+
`)) {
|
|
27736
|
+
const found = /^\s*(\d+)\s+(\d+)\s*$/.exec(line);
|
|
27737
|
+
if (found?.[1] !== undefined && found[2] !== undefined) {
|
|
27738
|
+
began.set(Number(found[1]), Number(found[2]));
|
|
27739
|
+
}
|
|
27740
|
+
}
|
|
27741
|
+
return began;
|
|
27742
|
+
}
|
|
27743
|
+
const said = spawnSync("ps", ["-p", pids.join(","), "-o", "pid=,lstart="], {
|
|
27744
|
+
encoding: "utf8",
|
|
27745
|
+
timeout: ASK_TIMEOUT_MS,
|
|
27746
|
+
env: { ...process.env, LC_ALL: "C" }
|
|
27747
|
+
}).stdout;
|
|
27748
|
+
for (const line of said.split(`
|
|
27749
|
+
`)) {
|
|
27750
|
+
const found = /^\s*(\d+)\s+(.+)$/.exec(line);
|
|
27751
|
+
const at = Date.parse(found?.[2]?.trim() ?? "");
|
|
27752
|
+
if (found?.[1] !== undefined && !Number.isNaN(at)) {
|
|
27753
|
+
began.set(Number(found[1]), at);
|
|
27754
|
+
}
|
|
27755
|
+
}
|
|
27756
|
+
} catch {}
|
|
27757
|
+
return began;
|
|
27758
|
+
}
|
|
27720
27759
|
var ASK_TIMEOUT_MS = 15000;
|
|
27721
27760
|
async function processTree() {
|
|
27722
27761
|
try {
|
|
27723
|
-
|
|
27724
|
-
|
|
27762
|
+
if (process.platform === "win32") {
|
|
27763
|
+
return parseProcessTable(await askWindows(), true);
|
|
27764
|
+
}
|
|
27765
|
+
return parseProcessTable(await askPosix());
|
|
27725
27766
|
} catch {
|
|
27726
27767
|
return;
|
|
27727
27768
|
}
|
|
27728
27769
|
}
|
|
27729
|
-
function parseProcessTable(printed) {
|
|
27770
|
+
function parseProcessTable(printed, started = false) {
|
|
27730
27771
|
const tree = new Map;
|
|
27731
27772
|
for (const line of printed.split(/\r?\n/)) {
|
|
27732
27773
|
const fields = /^\s*(\d+)\s+(\d+)(?:\s+(.*))?$/.exec(line);
|
|
@@ -27735,16 +27776,38 @@ function parseProcessTable(printed) {
|
|
|
27735
27776
|
}
|
|
27736
27777
|
const pid = Number(fields[1]);
|
|
27737
27778
|
const parent = Number(fields[2]);
|
|
27738
|
-
|
|
27739
|
-
|
|
27779
|
+
const rest = (fields[3] ?? "").trim();
|
|
27780
|
+
if (pid <= 0) {
|
|
27781
|
+
continue;
|
|
27740
27782
|
}
|
|
27783
|
+
if (!started) {
|
|
27784
|
+
tree.set(pid, { parent, command: rest });
|
|
27785
|
+
continue;
|
|
27786
|
+
}
|
|
27787
|
+
const timed = /^(\d+)(?:\s+(.*))?$/.exec(rest);
|
|
27788
|
+
if (timed === null) {
|
|
27789
|
+
continue;
|
|
27790
|
+
}
|
|
27791
|
+
const began = Number(timed[1]);
|
|
27792
|
+
tree.set(pid, { parent, command: (timed[2] ?? "").trim(), ...began > 0 ? { startedAt: began } : {} });
|
|
27741
27793
|
}
|
|
27742
27794
|
return tree;
|
|
27743
27795
|
}
|
|
27796
|
+
function linked(tree, child, parent) {
|
|
27797
|
+
const from = tree.get(child);
|
|
27798
|
+
const to = tree.get(parent);
|
|
27799
|
+
if (from === undefined || from.parent !== parent) {
|
|
27800
|
+
return false;
|
|
27801
|
+
}
|
|
27802
|
+
if (from.startedAt === undefined || to?.startedAt === undefined) {
|
|
27803
|
+
return true;
|
|
27804
|
+
}
|
|
27805
|
+
return from.startedAt >= to.startedAt;
|
|
27806
|
+
}
|
|
27744
27807
|
function childrenIn(tree, pid) {
|
|
27745
27808
|
const children = [];
|
|
27746
|
-
for (const
|
|
27747
|
-
if (
|
|
27809
|
+
for (const child of tree.keys()) {
|
|
27810
|
+
if (linked(tree, child, pid)) {
|
|
27748
27811
|
children.push(child);
|
|
27749
27812
|
}
|
|
27750
27813
|
}
|
|
@@ -27772,13 +27835,15 @@ function descendantsIn(tree, pid) {
|
|
|
27772
27835
|
function ancestorsIn(tree, pid, root) {
|
|
27773
27836
|
const chain = [];
|
|
27774
27837
|
const seen = new Set([pid]);
|
|
27838
|
+
let child = pid;
|
|
27775
27839
|
let parent = tree.get(pid)?.parent;
|
|
27776
|
-
while (parent !== undefined && !seen.has(parent)) {
|
|
27840
|
+
while (parent !== undefined && !seen.has(parent) && linked(tree, child, parent)) {
|
|
27777
27841
|
chain.push(parent);
|
|
27778
27842
|
if (parent === root) {
|
|
27779
27843
|
break;
|
|
27780
27844
|
}
|
|
27781
27845
|
seen.add(parent);
|
|
27846
|
+
child = parent;
|
|
27782
27847
|
parent = tree.get(parent)?.parent;
|
|
27783
27848
|
}
|
|
27784
27849
|
return chain;
|
|
@@ -27834,7 +27899,7 @@ async function askWindows() {
|
|
|
27834
27899
|
"-NoProfile",
|
|
27835
27900
|
"-NonInteractive",
|
|
27836
27901
|
"-Command",
|
|
27837
|
-
'Get-CimInstance Win32_Process | ForEach-Object { "$($_.ProcessId) $($_.ParentProcessId) $($_.CommandLine)" }'
|
|
27902
|
+
'Get-CimInstance Win32_Process | ForEach-Object { $began = 0; if ($_.CreationDate) { $began = ([DateTimeOffset]$_.CreationDate).ToUnixTimeMilliseconds() }; "$($_.ProcessId) $($_.ParentProcessId) $began $($_.CommandLine)" }'
|
|
27838
27903
|
], { timeout: ASK_TIMEOUT_MS, windowsHide: true, maxBuffer: 16 * 1024 * 1024 });
|
|
27839
27904
|
return stdout;
|
|
27840
27905
|
}
|
|
@@ -28292,7 +28357,7 @@ function restartSettled(projectPath) {
|
|
|
28292
28357
|
}
|
|
28293
28358
|
|
|
28294
28359
|
// src/run-record.ts
|
|
28295
|
-
import { execFileSync } from "node:child_process";
|
|
28360
|
+
import { execFileSync as execFileSync2 } from "node:child_process";
|
|
28296
28361
|
import { mkdirSync as mkdirSync5, openSync, readdirSync as readdirSync5, readFileSync as readFileSync11, rmSync as rmSync5, statSync as statSync4, writeFileSync as writeFileSync6 } from "node:fs";
|
|
28297
28362
|
import { basename as basename2, join as join11 } from "node:path";
|
|
28298
28363
|
|
|
@@ -28300,7 +28365,7 @@ import { basename as basename2, join as join11 } from "node:path";
|
|
|
28300
28365
|
import { existsSync as existsSync7, readdirSync as readdirSync4, readFileSync as readFileSync10, statSync as statSync3, unlinkSync } from "node:fs";
|
|
28301
28366
|
import { createConnection as createConnection3 } from "node:net";
|
|
28302
28367
|
import { tmpdir as tmpdir3 } from "node:os";
|
|
28303
|
-
import { join as join10, resolve as resolve4 } from "node:path";
|
|
28368
|
+
import { dirname as dirname5, join as join10, resolve as resolve4 } from "node:path";
|
|
28304
28369
|
import { setTimeout as delay2 } from "node:timers/promises";
|
|
28305
28370
|
|
|
28306
28371
|
// src/tool-args.ts
|
|
@@ -28474,11 +28539,35 @@ function announcedAndGone(projectPath) {
|
|
|
28474
28539
|
const wanted = resolve4(projectPath);
|
|
28475
28540
|
return gone.filter((one) => one.noticedAt >= since && resolve4(one.project) === wanted);
|
|
28476
28541
|
}
|
|
28542
|
+
var STARTED_AFTER_ANNOUNCING_MS = 2000;
|
|
28543
|
+
var CONFIRMED_FOR_MS = 60000;
|
|
28544
|
+
var JUDGED_AFTER_MS = 60000;
|
|
28545
|
+
var verdicts = new Map;
|
|
28546
|
+
function strangersAmong(candidates, now, startTimes = startTimesOf) {
|
|
28547
|
+
const toJudge = candidates.filter((one) => {
|
|
28548
|
+
if (now - one.writtenAt < JUDGED_AFTER_MS) {
|
|
28549
|
+
return false;
|
|
28550
|
+
}
|
|
28551
|
+
const known = verdicts.get(one.file);
|
|
28552
|
+
return known === undefined || known.writtenAt !== one.writtenAt || !known.stranger && now - known.judgedAt > CONFIRMED_FOR_MS;
|
|
28553
|
+
});
|
|
28554
|
+
if (toJudge.length > 0) {
|
|
28555
|
+
const began = startTimes(toJudge.map((one) => one.pid));
|
|
28556
|
+
for (const one of toJudge) {
|
|
28557
|
+
const at = began.get(one.pid);
|
|
28558
|
+
const stranger = at !== undefined && at - one.writtenAt > STARTED_AFTER_ANNOUNCING_MS;
|
|
28559
|
+
verdicts.set(one.file, { writtenAt: one.writtenAt, judgedAt: now, stranger });
|
|
28560
|
+
}
|
|
28561
|
+
}
|
|
28562
|
+
return new Set(candidates.filter((one) => verdicts.get(one.file)?.stranger === true).map((one) => one.file));
|
|
28563
|
+
}
|
|
28477
28564
|
function announcedIn(directory) {
|
|
28478
28565
|
if (!existsSync7(directory)) {
|
|
28479
28566
|
return [];
|
|
28480
28567
|
}
|
|
28481
28568
|
const found = [];
|
|
28569
|
+
const candidates = [];
|
|
28570
|
+
const entries = [];
|
|
28482
28571
|
for (const entry of readdirSync4(directory)) {
|
|
28483
28572
|
const report = ERROR_REPORT_PATTERN.exec(entry);
|
|
28484
28573
|
if (report) {
|
|
@@ -28492,20 +28581,60 @@ function announcedIn(directory) {
|
|
|
28492
28581
|
const file = join10(directory, entry);
|
|
28493
28582
|
const pid = Number.parseInt(match[1] ?? "", 10);
|
|
28494
28583
|
const alive = processAlive(pid);
|
|
28495
|
-
|
|
28584
|
+
entries.push({ file, pid, alive });
|
|
28585
|
+
if (alive) {
|
|
28586
|
+
try {
|
|
28587
|
+
candidates.push({ file, pid, writtenAt: statSync3(file).mtimeMs });
|
|
28588
|
+
} catch {}
|
|
28589
|
+
}
|
|
28590
|
+
}
|
|
28591
|
+
const strangers = strangersAmong(candidates, Date.now());
|
|
28592
|
+
for (const { file, pid, alive } of entries) {
|
|
28593
|
+
const announced = alive && !strangers.has(file) ? parseAnnouncement(file, pid) : { kind: "rubbish" };
|
|
28496
28594
|
if (announced.kind === "rubbish") {
|
|
28497
|
-
if (!alive) {
|
|
28595
|
+
if (!alive || strangers.has(file)) {
|
|
28498
28596
|
wentAway(file, pid);
|
|
28499
28597
|
}
|
|
28500
28598
|
try {
|
|
28501
28599
|
unlinkSync(file);
|
|
28502
28600
|
} catch {}
|
|
28601
|
+
verdicts.delete(file);
|
|
28503
28602
|
continue;
|
|
28504
28603
|
}
|
|
28505
28604
|
found.push(announced);
|
|
28506
28605
|
}
|
|
28606
|
+
const held = new Set(entries.map((one) => one.file));
|
|
28607
|
+
const here = join10(directory, ".");
|
|
28608
|
+
for (const file of verdicts.keys()) {
|
|
28609
|
+
if (dirname5(file) === here && !held.has(file)) {
|
|
28610
|
+
verdicts.delete(file);
|
|
28611
|
+
}
|
|
28612
|
+
}
|
|
28507
28613
|
return found;
|
|
28508
28614
|
}
|
|
28615
|
+
var ENDED_WITHIN_MS = 3000;
|
|
28616
|
+
async function announcementEnded(pid, directories = runtimeDirectories()) {
|
|
28617
|
+
const files = directories.map((directory) => join10(directory, `runtime-${pid}.json`)).filter((file) => existsSync7(file));
|
|
28618
|
+
const first = files[0];
|
|
28619
|
+
if (first === undefined) {
|
|
28620
|
+
return;
|
|
28621
|
+
}
|
|
28622
|
+
const until = Date.now() + ENDED_WITHIN_MS;
|
|
28623
|
+
while (processAlive(pid)) {
|
|
28624
|
+
const left = until - Date.now();
|
|
28625
|
+
if (left <= 0) {
|
|
28626
|
+
return;
|
|
28627
|
+
}
|
|
28628
|
+
await delay2(Math.min(LOOK_EVERY_MS, left));
|
|
28629
|
+
}
|
|
28630
|
+
wentAway(first, pid);
|
|
28631
|
+
for (const file of files) {
|
|
28632
|
+
try {
|
|
28633
|
+
unlinkSync(file);
|
|
28634
|
+
} catch {}
|
|
28635
|
+
verdicts.delete(file);
|
|
28636
|
+
}
|
|
28637
|
+
}
|
|
28509
28638
|
function sweepErrorReport(path, pid, now = Date.now()) {
|
|
28510
28639
|
if (processAlive(pid)) {
|
|
28511
28640
|
return;
|
|
@@ -28814,7 +28943,7 @@ function runningAs(pid) {
|
|
|
28814
28943
|
const raw = readFileSync11(`/proc/${pid}/cmdline`, "utf8").replaceAll("\x00", " ").trim();
|
|
28815
28944
|
return raw === "" ? null : { kind: "commandLine", text: raw, ...when };
|
|
28816
28945
|
}
|
|
28817
|
-
const args =
|
|
28946
|
+
const args = execFileSync2("ps", ["-p", String(pid), "-o", "args="], {
|
|
28818
28947
|
encoding: "utf8",
|
|
28819
28948
|
timeout: 15000
|
|
28820
28949
|
}).trim();
|
|
@@ -28826,20 +28955,14 @@ function runningAs(pid) {
|
|
|
28826
28955
|
function listeningPid(port) {
|
|
28827
28956
|
try {
|
|
28828
28957
|
if (process.platform === "win32") {
|
|
28829
|
-
|
|
28830
|
-
"-NoProfile",
|
|
28831
|
-
"-NonInteractive",
|
|
28832
|
-
"-Command",
|
|
28833
|
-
`(Get-NetTCPConnection -State Listen -LocalPort ${port} -ErrorAction SilentlyContinue | Select-Object -First 1).OwningProcess`
|
|
28834
|
-
], { encoding: "utf8", timeout: 15000 }).trim();
|
|
28835
|
-
return /^\d+$/.test(said) ? Number(said) : null;
|
|
28958
|
+
return listeningPidInNetstat(execFileSync2("netstat", ["-ano"], { encoding: "utf8", timeout: 15000 }), port);
|
|
28836
28959
|
}
|
|
28837
28960
|
for (const [command, args, pattern] of [
|
|
28838
28961
|
["lsof", ["-nP", `-iTCP:${port}`, "-sTCP:LISTEN", "-t"], /^(\d+)/],
|
|
28839
28962
|
["ss", ["-ltnpH", `sport = :${port}`], /pid=(\d+)/]
|
|
28840
28963
|
]) {
|
|
28841
28964
|
try {
|
|
28842
|
-
const said =
|
|
28965
|
+
const said = execFileSync2(command, [...args], { encoding: "utf8", timeout: 15000 }).trim();
|
|
28843
28966
|
const found = pattern.exec(said);
|
|
28844
28967
|
if (found?.[1] !== undefined) {
|
|
28845
28968
|
return Number(found[1]);
|
|
@@ -28851,9 +28974,22 @@ function listeningPid(port) {
|
|
|
28851
28974
|
return null;
|
|
28852
28975
|
}
|
|
28853
28976
|
}
|
|
28977
|
+
function listeningPidInNetstat(printed, port) {
|
|
28978
|
+
for (const line of printed.split(/\r?\n/)) {
|
|
28979
|
+
const fields = /^\s*TCP\s+(\S+)\s+\S+\s+LISTENING\s+(\d+)\s*$/i.exec(line);
|
|
28980
|
+
if (fields === null) {
|
|
28981
|
+
continue;
|
|
28982
|
+
}
|
|
28983
|
+
const local = fields[1] ?? "";
|
|
28984
|
+
if (Number(local.slice(local.lastIndexOf(":") + 1)) === port) {
|
|
28985
|
+
return Number(fields[2]);
|
|
28986
|
+
}
|
|
28987
|
+
}
|
|
28988
|
+
return null;
|
|
28989
|
+
}
|
|
28854
28990
|
function windowsCommandLine(pid) {
|
|
28855
28991
|
try {
|
|
28856
|
-
const answer =
|
|
28992
|
+
const answer = execFileSync2("powershell", [
|
|
28857
28993
|
"-NoProfile",
|
|
28858
28994
|
"-NonInteractive",
|
|
28859
28995
|
"-Command",
|
|
@@ -28871,7 +29007,7 @@ function startedAt(pid, windowsAnswer) {
|
|
|
28871
29007
|
return Number.isNaN(when) ? null : when;
|
|
28872
29008
|
}
|
|
28873
29009
|
try {
|
|
28874
|
-
const said =
|
|
29010
|
+
const said = execFileSync2("ps", ["-p", String(pid), "-o", "lstart="], {
|
|
28875
29011
|
encoding: "utf8",
|
|
28876
29012
|
timeout: 15000
|
|
28877
29013
|
}).trim();
|
|
@@ -28883,7 +29019,7 @@ function startedAt(pid, windowsAnswer) {
|
|
|
28883
29019
|
}
|
|
28884
29020
|
function windowsImage(pid) {
|
|
28885
29021
|
try {
|
|
28886
|
-
const csv =
|
|
29022
|
+
const csv = execFileSync2("tasklist", ["/FI", `PID eq ${pid}`, "/FO", "CSV", "/NH"], {
|
|
28887
29023
|
encoding: "utf8",
|
|
28888
29024
|
timeout: 15000,
|
|
28889
29025
|
windowsHide: true
|
|
@@ -28969,7 +29105,7 @@ function sweepTranscripts(keepMs = 24 * 60 * 60 * 1000, now = Date.now()) {
|
|
|
28969
29105
|
|
|
28970
29106
|
// src/setup.ts
|
|
28971
29107
|
import { cpSync, existsSync as existsSync8, mkdirSync as mkdirSync6, readFileSync as readFileSync12, rmSync as rmSync6, writeFileSync as writeFileSync7 } from "node:fs";
|
|
28972
|
-
import { dirname as
|
|
29108
|
+
import { dirname as dirname6, join as join12 } from "node:path";
|
|
28973
29109
|
var ADDONS = ["gdharness_editor", "gdharness_runtime", "auto_reload"];
|
|
28974
29110
|
var RUNTIME_AUTOLOAD = {
|
|
28975
29111
|
name: "GdharnessRuntime",
|
|
@@ -29758,7 +29894,7 @@ var TOOL_SPECS = [
|
|
|
29758
29894
|
property: {
|
|
29759
29895
|
type: "string",
|
|
29760
29896
|
ops: ["property", "find"],
|
|
29761
|
-
description: 'property: which one to read. find: read this one off every node matched, so a panel of labels is one call rather than one per label. Colons read through what a node holds, "_game:clock:speed", which is where a game keeps what is worth asking about. A number or a key steps into a list or a map, "_game:run:roster:0:traits", which is how the lists a game keeps its state in are walked: a roster, a board, an in-tray. A negative number counts from the end. A step that is not there is named, and says what was there instead.'
|
|
29897
|
+
description: 'property: which one to read. find: read this one off every node matched, so a panel of labels is one call rather than one per label. Colons read through what a node holds, "_game:clock:speed", which is where a game keeps what is worth asking about. A number or a key steps into a list or a map, "_game:run:roster:0:traits", which is how the lists a game keeps its state in are walked: a roster, a board, an in-tray. A negative number counts from the end. A step written as a call, "get_viewport():gui_get_focus_owner()", calls a method that takes no arguments and walks into what it returned, which is how a question only a method answers is read in one call: which control holds the focus in the viewport this one is in. A step that is not there is named, and says what was there instead.'
|
|
29762
29898
|
},
|
|
29763
29899
|
depth: { type: "number", ops: ["tree"], description: "tree: levels to descend. Default 3." },
|
|
29764
29900
|
includeProperties: {
|
|
@@ -29839,7 +29975,7 @@ var TOOL_SPECS = [
|
|
|
29839
29975
|
property: {
|
|
29840
29976
|
type: "string",
|
|
29841
29977
|
ops: ["set"],
|
|
29842
|
-
description: 'set: which one to write. Colons write through what a node holds, "_game:run:day", and a number or a key steps into a list or a map on the way, "_game:run:roster:0:name", a negative number counting from the end. The answer reads back off the same holder, so a write a typed container refused shows as an unchanged value.'
|
|
29978
|
+
description: 'set: which one to write. Colons write through what a node holds, "_game:run:day", and a number or a key steps into a list or a map on the way, "_game:run:roster:0:name", a negative number counting from the end, and a step written as a call, "get_viewport():gui_embed_subwindows", walks through what a method taking no arguments returned. The answer reads back off the same holder, so a write a typed container refused shows as an unchanged value.'
|
|
29843
29979
|
},
|
|
29844
29980
|
value: {
|
|
29845
29981
|
ops: ["set"],
|
|
@@ -29849,7 +29985,7 @@ var TOOL_SPECS = [
|
|
|
29849
29985
|
method: {
|
|
29850
29986
|
type: "string",
|
|
29851
29987
|
ops: ["call"],
|
|
29852
|
-
description: 'call: which one to call. Colons call through what a node holds, "_game:run:advance", the same way a property is written through them,
|
|
29988
|
+
description: 'call: which one to call. Colons call through what a node holds, "_game:run:advance", the same way a property is written through them, a list or a map is stepped into by index or key, "_game:run:roster:0:retire", and a step written as a call walks through what a method taking no arguments returned, "get_viewport():gui_get_focus_owner".'
|
|
29853
29989
|
},
|
|
29854
29990
|
args: {
|
|
29855
29991
|
type: "array",
|
|
@@ -29909,7 +30045,7 @@ var TOOL_SPECS = [
|
|
|
29909
30045
|
pressed: {
|
|
29910
30046
|
type: "boolean",
|
|
29911
30047
|
ops: ["action", "key", "mouse_click"],
|
|
29912
|
-
description: "action, key: leave it out and the press is a whole one, down and up a frame apart. true holds it down, false lets go of one being held. mouse_click is one raw event, so it is down unless you say false."
|
|
30048
|
+
description: "action, key: leave it out and the press is a whole one, down and up a frame apart. true holds it down, false lets go of one being held. mouse_click is one raw event, so it is down unless you say false; a wheel button down is a whole step, since a wheel is never held."
|
|
29913
30049
|
},
|
|
29914
30050
|
strength: { type: "number", ops: ["action"], description: "action: 0 to 1. Default 1." },
|
|
29915
30051
|
keycode: {
|
|
@@ -29987,7 +30123,7 @@ var TOOL_SPECS = [
|
|
|
29987
30123
|
requires: ["text"]
|
|
29988
30124
|
},
|
|
29989
30125
|
mouse_click: {
|
|
29990
|
-
summary: "one mouse button event at a position, carrying the buttons held once it has happened. A drag is a press held with pressed true, motions, and a release with pressed false",
|
|
30126
|
+
summary: "one mouse button event at a position, carrying the buttons held once it has happened. A drag is a press held with pressed true, motions, and a release with pressed false. A wheel button is a whole step, press and release together as a mouse sends one, and the answer says released: a lone wheel press holds the viewport on the control that took it, so every click after it lands there",
|
|
29991
30127
|
requires: ["x", "y"]
|
|
29992
30128
|
},
|
|
29993
30129
|
mouse_motion: {
|
|
@@ -30009,7 +30145,11 @@ var TOOL_SPECS = [
|
|
|
30009
30145
|
},
|
|
30010
30146
|
nodePath: { type: "string", ops: ["signal", "until"], description: "signal, until: the node." },
|
|
30011
30147
|
signal: { type: "string", ops: ["signal"], description: "signal: the signal name." },
|
|
30012
|
-
property: {
|
|
30148
|
+
property: {
|
|
30149
|
+
type: "string",
|
|
30150
|
+
ops: ["until"],
|
|
30151
|
+
description: 'until: the property name, or a colon path through what the node holds, "_game:run:day", read the way runtime_inspect property reads one, a step written as a call included. Walked again on every frame, so a holder the game replaces while the wait is on is followed.'
|
|
30152
|
+
},
|
|
30013
30153
|
value: { ops: ["until"], description: "until: the value to wait for, fitted to the property's type." },
|
|
30014
30154
|
says: {
|
|
30015
30155
|
type: "string",
|
|
@@ -30134,7 +30274,7 @@ function buildToolDefinitions() {
|
|
|
30134
30274
|
var UPDATE_NOTICE_EVERY = 500;
|
|
30135
30275
|
var FEEDBACK_NOTICE_EVERY = 250;
|
|
30136
30276
|
var run5 = promisify5(execFile5);
|
|
30137
|
-
var __dirname2 =
|
|
30277
|
+
var __dirname2 = dirname7(fileURLToPath2(import.meta.url));
|
|
30138
30278
|
var EDITOR_RESTART_TIMEOUT_MS = 90000;
|
|
30139
30279
|
var HOLD_PING_MS = 2000;
|
|
30140
30280
|
var SLOWEST_FRAME_RATE = 20;
|
|
@@ -31396,11 +31536,11 @@ class GodotServer {
|
|
|
31396
31536
|
if (!engine.ok) {
|
|
31397
31537
|
return engine.response;
|
|
31398
31538
|
}
|
|
31399
|
-
const folder =
|
|
31539
|
+
const folder = dirname7(output.absolutePath);
|
|
31400
31540
|
try {
|
|
31401
31541
|
mkdirSync7(folder, { recursive: true });
|
|
31402
31542
|
} catch (error) {
|
|
31403
|
-
return this.createErrorResponse(`Cannot create ${
|
|
31543
|
+
return this.createErrorResponse(`Cannot create ${dirname7(output.relativePath)} to export into: ${errorMessage(error)}`);
|
|
31404
31544
|
}
|
|
31405
31545
|
const debug = readBoolean(args, "debug") ?? false;
|
|
31406
31546
|
const exportLogs = mkdtempSync2(join13(tmpdir4(), "gdharness-export-"));
|
|
@@ -31854,11 +31994,14 @@ class GodotServer {
|
|
|
31854
31994
|
return null;
|
|
31855
31995
|
}
|
|
31856
31996
|
try {
|
|
31857
|
-
const
|
|
31858
|
-
|
|
31859
|
-
|
|
31860
|
-
]);
|
|
31997
|
+
const askedAt = Date.now();
|
|
31998
|
+
const asked = this.godotBridge.invokeTool("playing_status", {});
|
|
31999
|
+
const answer = await Promise.race([asked, delay3(PLAYING_STATUS_MS).then(() => null)]);
|
|
31861
32000
|
if (answer === null) {
|
|
32001
|
+
this.logDebug(`playing_status did not answer within ${PLAYING_STATUS_MS}ms`);
|
|
32002
|
+
asked.then(() => {
|
|
32003
|
+
this.logDebug(`playing_status answered after ${Date.now() - askedAt}ms`);
|
|
32004
|
+
}).catch(() => {});
|
|
31862
32005
|
return null;
|
|
31863
32006
|
}
|
|
31864
32007
|
return {
|
|
@@ -32178,6 +32321,9 @@ class GodotServer {
|
|
|
32178
32321
|
let asking = null;
|
|
32179
32322
|
let editorAskedAt = 0;
|
|
32180
32323
|
let editorSaysGoing = true;
|
|
32324
|
+
let editorAsks = 0;
|
|
32325
|
+
const waitBegan = Date.now();
|
|
32326
|
+
const lastSaid = () => editorSaysGoing ? "playing" : "not playing";
|
|
32181
32327
|
const endpoint = await announcedSince(projectPath, before, {
|
|
32182
32328
|
budgetMs,
|
|
32183
32329
|
accept: (one) => this.isTheGameOf(this.currentRun(), one, projectPath),
|
|
@@ -32191,6 +32337,7 @@ class GodotServer {
|
|
|
32191
32337
|
}
|
|
32192
32338
|
if (asking === null && Date.now() - editorAskedAt >= RUN_POLL_MS) {
|
|
32193
32339
|
editorAskedAt = Date.now();
|
|
32340
|
+
editorAsks += 1;
|
|
32194
32341
|
asking = this.runStillGoing(run).then((going) => {
|
|
32195
32342
|
editorSaysGoing = going;
|
|
32196
32343
|
}).finally(() => {
|
|
@@ -32200,6 +32347,7 @@ class GodotServer {
|
|
|
32200
32347
|
return !editorSaysGoing;
|
|
32201
32348
|
}
|
|
32202
32349
|
});
|
|
32350
|
+
this.logDebug(`announce wait ended after ${Date.now() - waitBegan}ms: ${endpoint === null ? "nothing announced" : `pid ${endpoint.pid} announced`}, editor asked ${editorAsks} times, last said ${lastSaid()}`);
|
|
32203
32351
|
const going = this.currentRun();
|
|
32204
32352
|
if (endpoint !== null && going !== null && going.announcedPid === undefined) {
|
|
32205
32353
|
going.announcedPid = endpoint.pid;
|
|
@@ -32876,11 +33024,19 @@ class GodotServer {
|
|
|
32876
33024
|
this.drainEditorOutput(stopped);
|
|
32877
33025
|
this.drainTranscript(stopped);
|
|
32878
33026
|
const wasRunning = await this.runStillGoing(stopped);
|
|
32879
|
-
const
|
|
33027
|
+
const announcedPid = this.announcedPidOf(stopped) ?? null;
|
|
33028
|
+
const endedPid = stopped.pid ?? announcedPid;
|
|
32880
33029
|
const children = readBoolean(args, "andChildren") === true && wasRunning ? await this.whatTheRunStarted(stopped) : null;
|
|
32881
33030
|
this.logDebug("Stopping the running game");
|
|
32882
33031
|
await this.endActiveGame("editor_run stop");
|
|
32883
33032
|
const ended = children === null ? null : endChildrenAmong(children);
|
|
33033
|
+
if (wasRunning) {
|
|
33034
|
+
for (const pid of new Set([endedPid, announcedPid])) {
|
|
33035
|
+
if (pid !== null) {
|
|
33036
|
+
await announcementEnded(pid);
|
|
33037
|
+
}
|
|
33038
|
+
}
|
|
33039
|
+
}
|
|
32884
33040
|
return this.jsonTextResponse({
|
|
32885
33041
|
stopped: true,
|
|
32886
33042
|
through: stopped.throughEditor ? "editor" : "gdharness",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "gdharness",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.3",
|
|
4
4
|
"mcpName": "io.github.Aureliolo/gdharness",
|
|
5
5
|
"description": "A harness for driving a Godot 4 project from an agent: editor addons, a runtime bridge into the running game, an MCP server in front of them, and a CLI that installs and diagnoses the Godot side.",
|
|
6
6
|
"type": "module",
|