gdharness 1.0.11 → 1.0.13

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
@@ -10047,11 +10047,20 @@ var SERVER_VERSION = (() => {
10047
10047
  }
10048
10048
  })();
10049
10049
  var UNVERSIONED = "an addon from before versions were reported";
10050
- function addonMismatch(addonVersion, serverVersion) {
10051
- if (addonVersion === serverVersion) {
10050
+ function editorIsStale(addonVersion, serverVersion, addonDigest, shippedDigest) {
10051
+ if (addonDigest !== undefined && addonDigest !== "" && shippedDigest !== undefined) {
10052
+ return addonDigest !== shippedDigest;
10053
+ }
10054
+ return addonVersion !== serverVersion;
10055
+ }
10056
+ function addonMismatch(addonVersion, serverVersion, addonDigest, shippedDigest) {
10057
+ if (!editorIsStale(addonVersion, serverVersion, addonDigest, shippedDigest)) {
10052
10058
  return;
10053
10059
  }
10054
10060
  const reported = addonVersion ?? "";
10061
+ if (reported === serverVersion) {
10062
+ return `The editor is running a different build of the ${reported} addon from the one this server ships. Restart it with editor_launch restart to pick this one up.`;
10063
+ }
10055
10064
  const editor = reported === "" ? UNVERSIONED : `the ${reported} addon`;
10056
10065
  const both = `The editor is running ${editor} while this server ships ${serverVersion}.`;
10057
10066
  if (reported !== "" && isNewer(reported, serverVersion)) {
@@ -10059,8 +10068,8 @@ function addonMismatch(addonVersion, serverVersion) {
10059
10068
  }
10060
10069
  return `${both} Restart it with editor_launch restart to pick the new one up.`;
10061
10070
  }
10062
- function markIfStale(answer, addonVersion, serverVersion) {
10063
- const staleNote = addonMismatch(addonVersion, serverVersion);
10071
+ function markIfStale(answer, addonVersion, serverVersion, addonDigest, shippedDigest) {
10072
+ const staleNote = addonMismatch(addonVersion, serverVersion, addonDigest, shippedDigest);
10064
10073
  if (staleNote === undefined || typeof answer !== "object" || answer === null || Array.isArray(answer)) {
10065
10074
  return answer;
10066
10075
  }
@@ -10172,22 +10181,22 @@ import { execFile as execFile5, spawn } from "node:child_process";
10172
10181
  import { randomUUID as randomUUID2 } from "node:crypto";
10173
10182
  import {
10174
10183
  appendFileSync,
10175
- closeSync,
10176
- existsSync as existsSync9,
10184
+ closeSync as closeSync2,
10185
+ existsSync as existsSync10,
10177
10186
  fstatSync,
10178
- mkdirSync as mkdirSync9,
10187
+ mkdirSync as mkdirSync10,
10179
10188
  mkdtempSync as mkdtempSync2,
10180
- openSync as openSync2,
10181
- readdirSync as readdirSync6,
10182
- readFileSync as readFileSync15,
10183
- readSync,
10189
+ openSync as openSync3,
10190
+ readdirSync as readdirSync7,
10191
+ readFileSync as readFileSync16,
10192
+ readSync as readSync2,
10184
10193
  rmdirSync,
10185
- statSync as statSync5
10194
+ statSync as statSync7
10186
10195
  } from "node:fs";
10187
10196
  import { tmpdir as tmpdir4 } from "node:os";
10188
- import { basename as basename3, dirname as dirname9, isAbsolute as isAbsolute3, join as join15, normalize as normalize2, relative as relative3 } from "node:path";
10197
+ import { basename as basename3, dirname as dirname10, isAbsolute as isAbsolute3, join as join16, normalize as normalize2, relative as relative3 } from "node:path";
10189
10198
  import { setTimeout as delay3 } from "node:timers/promises";
10190
- import { fileURLToPath as fileURLToPath2 } from "node:url";
10199
+ import { fileURLToPath as fileURLToPath3 } from "node:url";
10191
10200
  import { promisify as promisify5 } from "node:util";
10192
10201
 
10193
10202
  // node_modules/zod/v3/helpers/util.js
@@ -24791,8 +24800,18 @@ function runArguments(options) {
24791
24800
  }
24792
24801
  return args;
24793
24802
  }
24794
- function editorArguments(projectPath, ports) {
24795
- return ["-e", "--path", projectPath, "--lsp-port", String(ports.lsp), "--dap-port", String(ports.dap)];
24803
+ function editorArguments(projectPath, ports, logFile) {
24804
+ return [
24805
+ "-e",
24806
+ "--path",
24807
+ projectPath,
24808
+ "--lsp-port",
24809
+ String(ports.lsp),
24810
+ "--dap-port",
24811
+ String(ports.dap),
24812
+ "--log-file",
24813
+ logFile
24814
+ ];
24796
24815
  }
24797
24816
  var OPENED_BY_A_SERVER = "GDHARNESS_OPENED_BY_A_SERVER";
24798
24817
  function userDataIn(home, variables = process.env) {
@@ -25318,10 +25337,37 @@ async function announcedSince(projectPath, before, waiting = {}) {
25318
25337
  function describe2(endpoint) {
25319
25338
  return `pid ${endpoint.pid} on ${endpoint.address}:${endpoint.port} (${endpoint.project.name || "unnamed"} at ${endpoint.project.path})`;
25320
25339
  }
25340
+ function describeWithin(endpoint) {
25341
+ return `pid ${endpoint.pid} on ${endpoint.address}:${endpoint.port}`;
25342
+ }
25343
+ function theOneProject(endpoints) {
25344
+ if (endpoints.some((endpoint) => endpoint.project.path === "")) {
25345
+ return;
25346
+ }
25347
+ const paths = new Set(endpoints.map((endpoint) => resolve(endpoint.project.path)));
25348
+ const only = [...paths][0];
25349
+ return paths.size === 1 ? only : undefined;
25350
+ }
25351
+ function severalFrom(project, games) {
25352
+ return `${games.length} games are running from ${project}: ${games.map(describeWithin).join("; ")}. ` + "Pass pid to choose one; projectPath cannot, because every one of them has this one. " + "editor_status lists them under runtimes.";
25353
+ }
25354
+ function whichHalfIsBehind(unspoken) {
25355
+ const pids = (games) => games.map((game) => `pid ${game.pid}`).join(", ");
25356
+ const ahead = unspoken.filter((game) => game.protocol > RUNTIME_PROTOCOL);
25357
+ const behind = unspoken.filter((game) => game.protocol < RUNTIME_PROTOCOL);
25358
+ if (ahead.length > 0 && behind.length > 0) {
25359
+ return `they are on both sides of it: reconnect this server for ${pids(ahead)}, and reinstall the ` + `addon and restart ${pids(behind)}`;
25360
+ }
25361
+ if (ahead.length > 0) {
25362
+ return "this server is the older half: reconnect it so it spawns the installed version";
25363
+ }
25364
+ return `the addon is the older half: reinstall it and restart ${behind.length === 1 ? "the game" : "those games"}`;
25365
+ }
25321
25366
  function tooNew(unspoken) {
25322
25367
  const named = unspoken.map((game) => `pid ${game.pid} speaks ${game.protocol} (${game.project.name || "unnamed"} at ${game.project.path})`).join("; ");
25323
- const half = unspoken.some((game) => game.protocol > RUNTIME_PROTOCOL) ? "this server is the older half: reconnect it so it spawns the installed version" : "the addon is the older half: reinstall it and restart the game";
25324
- return `A game is running, in a protocol this server does not speak ${named}. This server speaks ${RUNTIME_PROTOCOL}, so ${half}.`;
25368
+ const count = unspoken.length === 1 ? "A game is running" : `${unspoken.length} games are running`;
25369
+ const spoken = new Set(unspoken.map((game) => game.protocol)).size === 1 ? "a protocol" : "protocols";
25370
+ return `${count}, in ${spoken} this server does not speak: ${named}. This server speaks ${RUNTIME_PROTOCOL}, so ${whichHalfIsBehind(unspoken)}.`;
25325
25371
  }
25326
25372
  function chooseRuntime(endpoints, projectPath, unspoken = [], pid) {
25327
25373
  if (pid !== undefined) {
@@ -25338,8 +25384,16 @@ function chooseRuntime(endpoints, projectPath, unspoken = [], pid) {
25338
25384
  if (tooNewToo !== undefined) {
25339
25385
  return { problem: tooNew([tooNewToo]) };
25340
25386
  }
25387
+ if (endpoints.length > 0) {
25388
+ return {
25389
+ problem: `No running game has pid ${pid}. Running: ${endpoints.map(describe2).join("; ")}. editor_status lists them under runtimes as they come and go.`
25390
+ };
25391
+ }
25392
+ if (unspoken.length > 0) {
25393
+ return { problem: `No game reachable from here has pid ${pid}. ${tooNew(unspoken)}` };
25394
+ }
25341
25395
  return {
25342
- problem: endpoints.length === 0 ? `No game with the runtime addon is running, so there is none with pid ${pid}. Start one with editor_run, or play the project from the editor with the addon enabled.` : `No running game has pid ${pid}. Running: ${endpoints.map(describe2).join("; ")}. editor_status lists them under runtimes as they come and go.`
25396
+ problem: `No game with the runtime addon is running, so there is none with pid ${pid}. Start one with editor_run, or play the project from the editor with the addon enabled.`
25343
25397
  };
25344
25398
  }
25345
25399
  if (endpoints.length === 0) {
@@ -25371,15 +25425,17 @@ function chooseRuntime(endpoints, projectPath, unspoken = [], pid) {
25371
25425
  if (matching.length === 1 && matching[0]) {
25372
25426
  return { endpoint: matching[0] };
25373
25427
  }
25374
- return {
25375
- problem: `Several games are running from ${wanted}: ${matching.map(describe2).join("; ")}. Pass pid to choose one.`
25376
- };
25428
+ return { problem: severalFrom(wanted, matching) };
25377
25429
  }
25378
25430
  if (endpoints.length === 1 && endpoints[0]) {
25379
25431
  return { endpoint: endpoints[0] };
25380
25432
  }
25433
+ const shared = theOneProject(endpoints);
25434
+ if (shared !== undefined) {
25435
+ return { problem: severalFrom(shared, endpoints) };
25436
+ }
25381
25437
  return {
25382
- problem: `Several games are running: ${endpoints.map(describe2).join("; ")}. Pass projectPath to choose one, or pid when they are from one project.`
25438
+ problem: `Several games are running: ${endpoints.map(describe2).join("; ")}. Pass pid to choose one, or projectPath when the project you mean is running only one.`
25383
25439
  };
25384
25440
  }
25385
25441
  var nextRequestId = 1;
@@ -25802,6 +25858,19 @@ function cacheWrittenAt(projectPath) {
25802
25858
  return null;
25803
25859
  }
25804
25860
  }
25861
+ function declaredSince(projectPath, paths, names, since) {
25862
+ return names.filter((name) => {
25863
+ const path = paths.get(name);
25864
+ if (path === undefined || !path.startsWith("res://")) {
25865
+ return false;
25866
+ }
25867
+ try {
25868
+ return statSync2(join6(projectPath, path.slice("res://".length))).mtimeMs > since;
25869
+ } catch {
25870
+ return false;
25871
+ }
25872
+ });
25873
+ }
25805
25874
  function cachedAtMissingPaths(cached, projectPath) {
25806
25875
  return [...cached].filter(([, path]) => path.startsWith("res://") && !existsSync3(join6(projectPath, path.slice("res://".length)))).map(([name]) => name).sort();
25807
25876
  }
@@ -26613,6 +26682,20 @@ function dictionary(entries) {
26613
26682
  return Object.assign(emptyRecord(), entries);
26614
26683
  }
26615
26684
 
26685
+ // src/editor-log.ts
26686
+ import {
26687
+ closeSync,
26688
+ existsSync as existsSync5,
26689
+ mkdirSync as mkdirSync6,
26690
+ openSync,
26691
+ readFileSync as readFileSync10,
26692
+ readSync,
26693
+ rmSync as rmSync3,
26694
+ statSync as statSync3,
26695
+ writeFileSync as writeFileSync6
26696
+ } from "node:fs";
26697
+ import { dirname as dirname6, join as join9 } from "node:path";
26698
+
26616
26699
  // src/game-log.ts
26617
26700
  import { StringDecoder } from "node:string_decoder";
26618
26701
  var HEADLINE = /^(USER )?(SCRIPT ERROR|SHADER ERROR|ERROR|WARNING):\s?(.*)$/;
@@ -26699,6 +26782,9 @@ class GameLog {
26699
26782
  count(severity) {
26700
26783
  return this.entries.filter((entry) => entry.severity === severity).length;
26701
26784
  }
26785
+ everything() {
26786
+ return this.entries;
26787
+ }
26702
26788
  select(options) {
26703
26789
  const floor = options.sinceLastCall ? this.seen[options.severity] : 0;
26704
26790
  const wanted = ADMITTED[options.severity];
@@ -26722,6 +26808,221 @@ class GameLog {
26722
26808
  }
26723
26809
  }
26724
26810
 
26811
+ // src/editor-log.ts
26812
+ function editorLogPath(projectPath) {
26813
+ return join9(projectPath, ".godot", "gdharness-editor.log");
26814
+ }
26815
+ function editorLogNotePath(projectPath) {
26816
+ return join9(projectPath, ".godot", "gdharness-editor.json");
26817
+ }
26818
+ function readEditorLogNote(projectPath) {
26819
+ try {
26820
+ const fields = asParams(JSON.parse(readFileSync10(editorLogNotePath(projectPath), "utf8")));
26821
+ const pid = readNumber(fields, "pid");
26822
+ const startedAt = readString(fields, "startedAt");
26823
+ return pid !== undefined && Number.isInteger(pid) && pid > 0 && startedAt !== undefined ? { pid, startedAt } : null;
26824
+ } catch {
26825
+ return null;
26826
+ }
26827
+ }
26828
+ function clearEditorLog(projectPath) {
26829
+ try {
26830
+ const path = editorLogPath(projectPath);
26831
+ mkdirSync6(dirname6(path), { recursive: true });
26832
+ rmSync3(path, { force: true });
26833
+ rmSync3(editorLogNotePath(projectPath), { force: true });
26834
+ } catch {}
26835
+ }
26836
+ function writeEditorLogNote(projectPath, pid) {
26837
+ try {
26838
+ const path = editorLogNotePath(projectPath);
26839
+ mkdirSync6(dirname6(path), { recursive: true });
26840
+ writeFileSync6(path, JSON.stringify({ pid, startedAt: new Date().toISOString() }, null, 2), "utf8");
26841
+ } catch {}
26842
+ }
26843
+ function theConsoleWasNotCaptured(why, projectPath) {
26844
+ if (why.kind === "not ours") {
26845
+ return "This editor was not opened by gdharness, so its console was never captured and nothing " + "here has it: an editor prints to a console no plugin can read, and the addon cannot get " + "at it from inside. This is not an editor that printed nothing.";
26846
+ }
26847
+ if (why.kind === "older server") {
26848
+ return `This editor was opened by a gdharness server, but the console beside ${projectPath} belongs ` + `to editor ${why.notedPid ?? "none recorded"} rather than to this one, so it holds an ` + "earlier session and is not offered as this one. A server from before consoles were " + "captured leaves exactly this.";
26849
+ }
26850
+ return "This editor was opened by gdharness and has written nothing to its console file yet, not " + "even the engine version it prints first, so the file is missing rather than empty. An " + "editor that could not open the file for writing leaves this.";
26851
+ }
26852
+ function editorConsole(projectPath, editorPid, openedByAServer) {
26853
+ if (!openedByAServer) {
26854
+ return { kind: "not ours" };
26855
+ }
26856
+ const noted = readEditorLogNote(projectPath);
26857
+ if (noted === null || editorPid === null || noted.pid !== editorPid) {
26858
+ return { kind: "older server", notedPid: noted?.pid ?? null };
26859
+ }
26860
+ const path = editorLogPath(projectPath);
26861
+ if (!existsSync5(path)) {
26862
+ return { kind: "not written" };
26863
+ }
26864
+ const log = new GameLog;
26865
+ const { text, of } = theFirstOf(path);
26866
+ log.append("transcript", text);
26867
+ log.finish();
26868
+ return { log, path, ...of === undefined ? {} : { readBytes: text.length, of } };
26869
+ }
26870
+ var MOST_CONSOLE_BYTES = 8 * 1024 * 1024;
26871
+ function theFirstOf(path) {
26872
+ const whole = statSync3(path).size;
26873
+ if (whole <= MOST_CONSOLE_BYTES) {
26874
+ return { text: readFileSync10(path, "utf8") };
26875
+ }
26876
+ const handle = openSync(path, "r");
26877
+ try {
26878
+ const buffer = Buffer.alloc(MOST_CONSOLE_BYTES);
26879
+ const read = readSync(handle, buffer, 0, MOST_CONSOLE_BYTES, 0);
26880
+ const decoded = buffer.subarray(0, read).toString("utf8");
26881
+ const lastLine = decoded.lastIndexOf(`
26882
+ `);
26883
+ return { text: lastLine === -1 ? decoded : decoded.slice(0, lastLine + 1), of: whole };
26884
+ } finally {
26885
+ closeSync(handle);
26886
+ }
26887
+ }
26888
+ var NAMED_VALUES = 6;
26889
+ var WALK_BACK_LIMIT = 500;
26890
+ var SLOTS = [/"[^"]*"/g, /'[^']*'/g, /\b(?:res|user):\/\/\S+/g, /(?<=\.gd|\.tscn|\.cs):\d+(?::\d+)?/g];
26891
+ function shapeOf(text) {
26892
+ const values = [];
26893
+ let shape = "";
26894
+ let at = 0;
26895
+ while (at < text.length) {
26896
+ let earliest = null;
26897
+ for (const pattern of SLOTS) {
26898
+ pattern.lastIndex = at;
26899
+ const found = pattern.exec(text);
26900
+ if (found !== null && (earliest === null || found.index < earliest.index || found.index === earliest.index && found[0].length > earliest.value.length)) {
26901
+ earliest = { index: found.index, value: found[0] };
26902
+ }
26903
+ }
26904
+ if (earliest === null) {
26905
+ shape += text.slice(at);
26906
+ break;
26907
+ }
26908
+ if (earliest.value.length === 0) {
26909
+ shape += text.slice(at, at + 1);
26910
+ at += 1;
26911
+ continue;
26912
+ }
26913
+ shape += `${text.slice(at, earliest.index)}…`;
26914
+ values.push(earliest.value);
26915
+ at = earliest.index + earliest.value.length;
26916
+ }
26917
+ return { shape, values };
26918
+ }
26919
+ function bursts(entries, before) {
26920
+ const shapes = new Map;
26921
+ const shaped = entries.map((entry) => shapeOf(entry.text));
26922
+ for (const [at, entry] of entries.entries()) {
26923
+ const key = `${entry.severity}\x00${shaped[at]?.shape ?? entry.text}`;
26924
+ const group = shapes.get(key) ?? { indices: [], values: [] };
26925
+ group.indices.push(at);
26926
+ group.values.push(shaped[at]?.values ?? []);
26927
+ shapes.set(key, group);
26928
+ }
26929
+ const collapsed = new Set;
26930
+ for (const group of shapes.values()) {
26931
+ if (group.indices.length > 1) {
26932
+ for (const at of group.indices) {
26933
+ collapsed.add(at);
26934
+ }
26935
+ }
26936
+ }
26937
+ const repeated = [];
26938
+ for (const [key, group] of shapes) {
26939
+ const first = group.indices[0];
26940
+ if (group.indices.length < 2 || first === undefined) {
26941
+ continue;
26942
+ }
26943
+ const entry = entries[first];
26944
+ if (entry === undefined) {
26945
+ continue;
26946
+ }
26947
+ repeated.push({
26948
+ shape: key.slice(key.indexOf("\x00") + 1),
26949
+ count: group.indices.length,
26950
+ severity: entry.severity,
26951
+ first: { index: entry.index, text: entry.text },
26952
+ slots: slotsOf(group.values, group.indices.length),
26953
+ before: linesBefore(entries, first, collapsed, before)
26954
+ });
26955
+ }
26956
+ return repeated.sort((one, other) => other.count - one.count);
26957
+ }
26958
+ function slotsOf(values, count) {
26959
+ const width = Math.max(0, ...values.map((row) => row.length));
26960
+ const distinct = [];
26961
+ for (let at = 0;at < width; at += 1) {
26962
+ distinct.push([...new Set(values.map((row) => row[at]).filter((one) => one !== undefined))]);
26963
+ }
26964
+ const slots = [];
26965
+ for (let at = 0;at < width; at += 1) {
26966
+ const seen = distinct[at] ?? [];
26967
+ const partner = theSlotItMovesWith(values, distinct, at);
26968
+ const paired = partner === undefined ? {} : { movesWith: partner };
26969
+ if (seen.length === count && count > 1) {
26970
+ slots.push({ distinct: seen.length, allDifferent: true, ...paired });
26971
+ continue;
26972
+ }
26973
+ const some = seen.slice(0, NAMED_VALUES);
26974
+ slots.push({
26975
+ distinct: seen.length,
26976
+ some,
26977
+ ...seen.length > some.length ? { more: seen.length - some.length } : {},
26978
+ ...paired
26979
+ });
26980
+ }
26981
+ return slots;
26982
+ }
26983
+ function theSlotItMovesWith(values, distinct, at) {
26984
+ const mine = distinct[at] ?? [];
26985
+ if (mine.length < 2) {
26986
+ return;
26987
+ }
26988
+ for (let other = 0;other < at; other += 1) {
26989
+ if ((distinct[other] ?? []).length !== mine.length) {
26990
+ continue;
26991
+ }
26992
+ const goesWith = new Map;
26993
+ let oneForOne = true;
26994
+ for (const row of values) {
26995
+ const theirs = row[other];
26996
+ const ours = row[at];
26997
+ if (theirs === undefined || ours === undefined) {
26998
+ oneForOne = false;
26999
+ break;
27000
+ }
27001
+ const already = goesWith.get(theirs);
27002
+ if (already === undefined) {
27003
+ goesWith.set(theirs, ours);
27004
+ } else if (already !== ours) {
27005
+ oneForOne = false;
27006
+ break;
27007
+ }
27008
+ }
27009
+ if (oneForOne) {
27010
+ return other;
27011
+ }
27012
+ }
27013
+ return;
27014
+ }
27015
+ function linesBefore(entries, at, collapsed, wanted) {
27016
+ const found = [];
27017
+ for (let back = at - 1;back >= 0 && at - back <= WALK_BACK_LIMIT && found.length < wanted; back -= 1) {
27018
+ const entry = entries[back];
27019
+ if (entry !== undefined && !collapsed.has(back)) {
27020
+ found.push(entry.text);
27021
+ }
27022
+ }
27023
+ return found.reverse();
27024
+ }
27025
+
26725
27026
  // src/godot-bridge.ts
26726
27027
  import { randomUUID } from "node:crypto";
26727
27028
  import { EventEmitter } from "node:events";
@@ -26900,6 +27201,7 @@ class GodotBridge extends EventEmitter {
26900
27201
  connectedAt: this.connectionInfo?.connectedAt,
26901
27202
  lastPongAt: this.connectionInfo?.lastPongAt,
26902
27203
  addonVersion: this.connectionInfo?.addonVersion,
27204
+ addonDigest: this.connectionInfo?.addonDigest,
26903
27205
  editorPid: this.connectionInfo?.editorPid,
26904
27206
  lspPort: this.connectionInfo?.lspPort,
26905
27207
  dapPort: this.connectionInfo?.dapPort,
@@ -27043,6 +27345,7 @@ class GodotBridge extends EventEmitter {
27043
27345
  }
27044
27346
  this.connectionInfo.projectPath = message.project_path;
27045
27347
  this.connectionInfo.addonVersion = message.addon_version ?? "";
27348
+ this.connectionInfo.addonDigest = message.addon_digest;
27046
27349
  this.connectionInfo.editorPid = message.editor_pid;
27047
27350
  this.connectionInfo.lspPort = servedPort(message.lsp_port);
27048
27351
  this.connectionInfo.dapPort = servedPort(message.dap_port);
@@ -27223,14 +27526,14 @@ function getDefaultBridge(ownProject = null) {
27223
27526
 
27224
27527
  // src/godot-path.ts
27225
27528
  import { execFile as execFile2 } from "node:child_process";
27226
- import { existsSync as existsSync6 } from "node:fs";
27529
+ import { existsSync as existsSync7 } from "node:fs";
27227
27530
  import { normalize } from "node:path";
27228
27531
  import { promisify as promisify2 } from "node:util";
27229
27532
 
27230
27533
  // src/detection.ts
27231
- import { existsSync as existsSync5, readdirSync as readdirSync3, statSync as statSync3 } from "node:fs";
27534
+ import { existsSync as existsSync6, readdirSync as readdirSync3, statSync as statSync4 } from "node:fs";
27232
27535
  import { homedir as homedir2 } from "node:os";
27233
- import { join as join9 } from "node:path";
27536
+ import { join as join10 } from "node:path";
27234
27537
  function resolveHomeDirectory() {
27235
27538
  try {
27236
27539
  return homedir2();
@@ -27239,7 +27542,7 @@ function resolveHomeDirectory() {
27239
27542
  }
27240
27543
  }
27241
27544
  function scanDirectoryForGodotBinaries(directory, platform) {
27242
- if (!directory || !existsSync5(directory)) {
27545
+ if (!directory || !existsSync6(directory)) {
27243
27546
  return [];
27244
27547
  }
27245
27548
  let entries;
@@ -27254,16 +27557,16 @@ function scanDirectoryForGodotBinaries(directory, platform) {
27254
27557
  if (!pattern.test(name)) {
27255
27558
  continue;
27256
27559
  }
27257
- const fullPath = join9(directory, name);
27560
+ const fullPath = join10(directory, name);
27258
27561
  try {
27259
- const stat = statSync3(fullPath);
27562
+ const stat = statSync4(fullPath);
27260
27563
  if (stat.isFile()) {
27261
27564
  matches.push({ name, mtime: stat.mtimeMs });
27262
27565
  }
27263
27566
  } catch {}
27264
27567
  }
27265
27568
  matches.sort((a, b) => b.mtime - a.mtime);
27266
- return matches.map((m) => join9(directory, m.name));
27569
+ return matches.map((m) => join10(directory, m.name));
27267
27570
  }
27268
27571
  function conventionalPaths(platform, home) {
27269
27572
  const paths = ["godot"];
@@ -27366,7 +27669,7 @@ class GodotLocator {
27366
27669
  return known;
27367
27670
  }
27368
27671
  let reason;
27369
- if (path !== "godot" && !existsSync6(path)) {
27672
+ if (path !== "godot" && !existsSync7(path)) {
27370
27673
  reason = "does not exist";
27371
27674
  } else {
27372
27675
  try {
@@ -27383,13 +27686,13 @@ class GodotLocator {
27383
27686
 
27384
27687
  // src/headless.ts
27385
27688
  import { execFile as execFile3 } from "node:child_process";
27386
- import { mkdtempSync, writeFileSync as writeFileSync6 } from "node:fs";
27689
+ import { mkdtempSync, writeFileSync as writeFileSync7 } from "node:fs";
27387
27690
  import { tmpdir as tmpdir3 } from "node:os";
27388
- import { join as join10 } from "node:path";
27691
+ import { join as join11 } from "node:path";
27389
27692
  import { promisify as promisify3 } from "node:util";
27390
27693
 
27391
27694
  // src/scratch.ts
27392
- import { rmSync as rmSync3 } from "node:fs";
27695
+ import { rmSync as rmSync4 } from "node:fs";
27393
27696
  var SWEPT = { recursive: true, force: true, maxRetries: 20, retryDelay: 250 };
27394
27697
  function discardWith(remove, paths) {
27395
27698
  const left = [];
@@ -27407,7 +27710,7 @@ function discardWith(remove, paths) {
27407
27710
  }
27408
27711
  function discard(...paths) {
27409
27712
  return discardWith((path) => {
27410
- rmSync3(path, SWEPT);
27713
+ rmSync4(path, SWEPT);
27411
27714
  }, paths);
27412
27715
  }
27413
27716
 
@@ -27451,12 +27754,12 @@ function reason(stdout, stderr) {
27451
27754
  return stdout.trim().split(/\r?\n/).at(-1) ?? "no output at all";
27452
27755
  }
27453
27756
  async function runImport(godotPath, projectPath) {
27454
- const logDir = mkdtempSync(join10(tmpdir3(), "gdharness-import-"));
27757
+ const logDir = mkdtempSync(join11(tmpdir3(), "gdharness-import-"));
27455
27758
  try {
27456
27759
  const { stderr } = await run3(godotPath, [
27457
27760
  "--headless",
27458
27761
  "--log-file",
27459
- join10(logDir, "engine.log"),
27762
+ join11(logDir, "engine.log"),
27460
27763
  "--path",
27461
27764
  projectPath,
27462
27765
  "--import"
@@ -27481,13 +27784,13 @@ async function runImport(godotPath, projectPath) {
27481
27784
  }
27482
27785
  }
27483
27786
  async function runOperation(engine, operation, params, projectPath) {
27484
- const paramsDir = mkdtempSync(join10(tmpdir3(), "gdharness-params-"));
27485
- const paramsFile = join10(paramsDir, `${operation}.json`);
27486
- writeFileSync6(paramsFile, JSON.stringify(snakeCased(params)), "utf8");
27787
+ const paramsDir = mkdtempSync(join11(tmpdir3(), "gdharness-params-"));
27788
+ const paramsFile = join11(paramsDir, `${operation}.json`);
27789
+ writeFileSync7(paramsFile, JSON.stringify(snakeCased(params)), "utf8");
27487
27790
  const args = [
27488
27791
  "--headless",
27489
27792
  "--log-file",
27490
- join10(paramsDir, "engine.log"),
27793
+ join11(paramsDir, "engine.log"),
27491
27794
  "--path",
27492
27795
  projectPath,
27493
27796
  "--script",
@@ -27916,7 +28219,7 @@ function orphansPrinted(printed) {
27916
28219
  import { realpathSync as realpathSync2 } from "node:fs";
27917
28220
  import { readFile, realpath } from "node:fs/promises";
27918
28221
  import { createConnection as createConnection3 } from "node:net";
27919
- import { dirname as dirname6, resolve as resolve3 } from "node:path";
28222
+ import { dirname as dirname7, resolve as resolve3 } from "node:path";
27920
28223
  import { fileURLToPath, pathToFileURL } from "node:url";
27921
28224
  var DEFAULT_LSP_PORT = 6005;
27922
28225
  var DIAGNOSTICS_TIMEOUT_MS = 5000;
@@ -28046,7 +28349,7 @@ class GodotLSPClient {
28046
28349
  if (this.initialized) {
28047
28350
  return;
28048
28351
  }
28049
- const rootPath = this.rootPath ?? dirname6(resolve3(filePath));
28352
+ const rootPath = this.rootPath ?? dirname7(resolve3(filePath));
28050
28353
  await this.initialize(rootPath);
28051
28354
  }
28052
28355
  async sendRequest(method, params) {
@@ -28482,14 +28785,14 @@ function secondsFromClock(printed) {
28482
28785
  }
28483
28786
 
28484
28787
  // src/project-scan.ts
28485
- import { existsSync as existsSync7, readdirSync as readdirSync4, readFileSync as readFileSync10 } from "node:fs";
28486
- import { join as join11 } from "node:path";
28788
+ import { existsSync as existsSync8, readdirSync as readdirSync4, readFileSync as readFileSync11 } from "node:fs";
28789
+ import { join as join12 } from "node:path";
28487
28790
  var SKIPPED = new Set(["node_modules"]);
28488
28791
  function skipped(name) {
28489
28792
  return name.startsWith(".") || SKIPPED.has(name);
28490
28793
  }
28491
28794
  function steppedOver(directory) {
28492
- return existsSync7(join11(directory, ".gdignore"));
28795
+ return existsSync8(join12(directory, ".gdignore"));
28493
28796
  }
28494
28797
  var ASSET_EXTENSIONS = new Set(["png", "jpg", "jpeg", "webp", "svg", "ttf", "otf", "wav", "mp3", "ogg"]);
28495
28798
  function projectStructure(projectPath) {
@@ -28503,7 +28806,7 @@ function projectStructure(projectPath) {
28503
28806
  continue;
28504
28807
  }
28505
28808
  if (entry.isDirectory()) {
28506
- visit(join11(directory, entry.name));
28809
+ visit(join12(directory, entry.name));
28507
28810
  } else if (entry.isFile()) {
28508
28811
  const extension = entry.name.split(".").pop()?.toLowerCase() ?? "";
28509
28812
  if (extension === "tscn") {
@@ -28534,14 +28837,14 @@ function scriptsWithoutUid(projectPath) {
28534
28837
  }
28535
28838
  const spelled = prefix === "" ? entry.name : `${prefix}/${entry.name}`;
28536
28839
  if (entry.isDirectory()) {
28537
- visit(join11(directory, entry.name), spelled);
28840
+ visit(join12(directory, entry.name), spelled);
28538
28841
  continue;
28539
28842
  }
28540
28843
  if (!entry.isFile()) {
28541
28844
  continue;
28542
28845
  }
28543
28846
  const extension = entry.name.split(".").pop()?.toLowerCase() ?? "";
28544
- if (CARRIES_A_UID.has(extension) && !existsSync7(join11(directory, `${entry.name}.uid`))) {
28847
+ if (CARRIES_A_UID.has(extension) && !existsSync8(join12(directory, `${entry.name}.uid`))) {
28545
28848
  found.push(spelled);
28546
28849
  }
28547
28850
  }
@@ -28576,7 +28879,7 @@ function searchProject(projectPath, options) {
28576
28879
  if (skipped(entry.name)) {
28577
28880
  continue;
28578
28881
  }
28579
- const entryPath = join11(directory, entry.name);
28882
+ const entryPath = join12(directory, entry.name);
28580
28883
  if (entry.isDirectory()) {
28581
28884
  visit(entryPath);
28582
28885
  continue;
@@ -28587,7 +28890,7 @@ function searchProject(projectPath, options) {
28587
28890
  }
28588
28891
  result.summary.files_searched += 1;
28589
28892
  const matches = [];
28590
- for (const [index, line] of readFileSync10(entryPath, "utf8").split(`
28893
+ for (const [index, line] of readFileSync11(entryPath, "utf8").split(`
28591
28894
  `).entries()) {
28592
28895
  if (full()) {
28593
28896
  break;
@@ -28610,7 +28913,7 @@ function searchProject(projectPath, options) {
28610
28913
  }
28611
28914
 
28612
28915
  // src/resources.ts
28613
- import { readFileSync as readFileSync11 } from "node:fs";
28916
+ import { readFileSync as readFileSync12 } from "node:fs";
28614
28917
  import { extname, resolve as resolve4 } from "node:path";
28615
28918
  var STATIC_RESOURCES = [
28616
28919
  {
@@ -28805,7 +29108,7 @@ function readResourceText(uri, getProjectPath) {
28805
29108
  const parsedUri = parseGodotUri(uri);
28806
29109
  if (parsedUri.kind === "project-info") {
28807
29110
  const projectFilePath = resolveProjectFile(projectPath, "project.godot");
28808
- const rawProject = readFileSync11(projectFilePath, "utf-8");
29111
+ const rawProject = readFileSync12(projectFilePath, "utf-8");
28809
29112
  const parsedProject = parseProjectGodot(rawProject);
28810
29113
  return {
28811
29114
  mimeType: "application/json",
@@ -28814,7 +29117,7 @@ function readResourceText(uri, getProjectPath) {
28814
29117
  }
28815
29118
  const filePath = resolveProjectFile(projectPath, parsedUri.resourcePath);
28816
29119
  ensureAllowedExtension(parsedUri.kind, filePath);
28817
- const text = readFileSync11(filePath, "utf-8");
29120
+ const text = readFileSync12(filePath, "utf-8");
28818
29121
  return {
28819
29122
  mimeType: parsedUri.kind === "script" ? "text/x-gdscript" : "text/plain",
28820
29123
  text
@@ -28851,16 +29154,16 @@ function setupResourceHandlers(mcp, getProjectPath) {
28851
29154
  }
28852
29155
 
28853
29156
  // src/restart-note.ts
28854
- import { mkdirSync as mkdirSync6, readFileSync as readFileSync12, rmSync as rmSync4, writeFileSync as writeFileSync7 } from "node:fs";
28855
- import { dirname as dirname7, join as join12 } from "node:path";
29157
+ import { mkdirSync as mkdirSync7, readFileSync as readFileSync13, rmSync as rmSync5, writeFileSync as writeFileSync8 } from "node:fs";
29158
+ import { dirname as dirname8, join as join13 } from "node:path";
28856
29159
  function restartNotePath(projectPath) {
28857
- return join12(projectPath, ".godot", "gdharness-restart.json");
29160
+ return join13(projectPath, ".godot", "gdharness-restart.json");
28858
29161
  }
28859
29162
  function noteRestartBegun(note) {
28860
29163
  const path = restartNotePath(note.projectPath);
28861
29164
  try {
28862
- mkdirSync6(dirname7(path), { recursive: true });
28863
- writeFileSync7(path, `${JSON.stringify(note, null, 2)}
29165
+ mkdirSync7(dirname8(path), { recursive: true });
29166
+ writeFileSync8(path, `${JSON.stringify(note, null, 2)}
28864
29167
  `, "utf8");
28865
29168
  } catch (error) {
28866
29169
  console.error(`[SERVER] Could not record the restart at ${path}: ${errorMessage(error)}`);
@@ -28868,7 +29171,7 @@ function noteRestartBegun(note) {
28868
29171
  }
28869
29172
  function restartOwed(projectPath) {
28870
29173
  try {
28871
- const parsed = JSON.parse(readFileSync12(restartNotePath(projectPath), "utf8"));
29174
+ const parsed = JSON.parse(readFileSync13(restartNotePath(projectPath), "utf8"));
28872
29175
  if (typeof parsed !== "object" || parsed === null) {
28873
29176
  return null;
28874
29177
  }
@@ -28883,7 +29186,7 @@ function restartOwed(projectPath) {
28883
29186
  }
28884
29187
  function restartSettled(projectPath) {
28885
29188
  try {
28886
- rmSync4(restartNotePath(projectPath), { force: true });
29189
+ rmSync5(restartNotePath(projectPath), { force: true });
28887
29190
  } catch (error) {
28888
29191
  console.error(`[SERVER] Could not clear the restart note: ${errorMessage(error)}`);
28889
29192
  }
@@ -28891,36 +29194,36 @@ function restartSettled(projectPath) {
28891
29194
 
28892
29195
  // src/run-record.ts
28893
29196
  import { execFileSync as execFileSync2 } from "node:child_process";
28894
- import { mkdirSync as mkdirSync7, openSync, readdirSync as readdirSync5, readFileSync as readFileSync13, rmSync as rmSync5, statSync as statSync4, writeFileSync as writeFileSync8 } from "node:fs";
28895
- import { basename as basename2, join as join13 } from "node:path";
29197
+ import { mkdirSync as mkdirSync8, openSync as openSync2, readdirSync as readdirSync5, readFileSync as readFileSync14, rmSync as rmSync6, statSync as statSync5, writeFileSync as writeFileSync9 } from "node:fs";
29198
+ import { basename as basename2, join as join14 } from "node:path";
28896
29199
  function runsDirectory() {
28897
- return join13(runtimeDirectory(), "runs");
29200
+ return join14(runtimeDirectory(), "runs");
28898
29201
  }
28899
29202
  function recordPath() {
28900
- return join13(runsDirectory(), "run.json");
29203
+ return join14(runsDirectory(), "run.json");
28901
29204
  }
28902
29205
  function recordPaths() {
28903
- return runtimeDirectories().map((directory) => join13(directory, "runs", "run.json"));
29206
+ return runtimeDirectories().map((directory) => join14(directory, "runs", "run.json"));
28904
29207
  }
28905
29208
  function openTranscript(startedAt) {
28906
- mkdirSync7(runsDirectory(), { recursive: true });
28907
- const path = join13(runsDirectory(), `run-${startedAt}.log`);
28908
- return { path, fd: openSync(path, "a") };
29209
+ mkdirSync8(runsDirectory(), { recursive: true });
29210
+ const path = join14(runsDirectory(), `run-${startedAt}.log`);
29211
+ return { path, fd: openSync2(path, "a") };
28909
29212
  }
28910
29213
  function writeRunRecord(record) {
28911
- mkdirSync7(runsDirectory(), { recursive: true });
28912
- writeFileSync8(recordPath(), JSON.stringify(record, null, 2), "utf8");
29214
+ mkdirSync8(runsDirectory(), { recursive: true });
29215
+ writeFileSync9(recordPath(), JSON.stringify(record, null, 2), "utf8");
28913
29216
  }
28914
29217
  function editorNotePath() {
28915
- return join13(runsDirectory(), "editor-run.json");
29218
+ return join14(runsDirectory(), "editor-run.json");
28916
29219
  }
28917
29220
  function writeEditorRunNote(note) {
28918
- mkdirSync7(runsDirectory(), { recursive: true });
28919
- writeFileSync8(editorNotePath(), JSON.stringify(note, null, 2), "utf8");
29221
+ mkdirSync8(runsDirectory(), { recursive: true });
29222
+ writeFileSync9(editorNotePath(), JSON.stringify(note, null, 2), "utf8");
28920
29223
  }
28921
29224
  function readEditorRunNote() {
28922
29225
  for (const directory of runtimeDirectories()) {
28923
- const note = editorNoteAt(join13(directory, "runs", "editor-run.json"));
29226
+ const note = editorNoteAt(join14(directory, "runs", "editor-run.json"));
28924
29227
  if (note !== null) {
28925
29228
  return note;
28926
29229
  }
@@ -28930,7 +29233,7 @@ function readEditorRunNote() {
28930
29233
  function editorNoteAt(path) {
28931
29234
  let parsed;
28932
29235
  try {
28933
- parsed = JSON.parse(readFileSync13(path, "utf8"));
29236
+ parsed = JSON.parse(readFileSync14(path, "utf8"));
28934
29237
  } catch {
28935
29238
  return null;
28936
29239
  }
@@ -28953,7 +29256,7 @@ function recordRunEnded(pid, exitCode) {
28953
29256
  return;
28954
29257
  }
28955
29258
  try {
28956
- writeFileSync8(path, JSON.stringify({ ...record, exitCode }, null, 2), "utf8");
29259
+ writeFileSync9(path, JSON.stringify({ ...record, exitCode }, null, 2), "utf8");
28957
29260
  } catch {}
28958
29261
  }
28959
29262
  function readRunRecord() {
@@ -28968,7 +29271,7 @@ function readRunRecord() {
28968
29271
  function recordAt(path) {
28969
29272
  let parsed;
28970
29273
  try {
28971
- parsed = JSON.parse(readFileSync13(path, "utf8"));
29274
+ parsed = JSON.parse(readFileSync14(path, "utf8"));
28972
29275
  } catch {
28973
29276
  return null;
28974
29277
  }
@@ -29013,7 +29316,7 @@ function runningAs(pid) {
29013
29316
  const began = startedAt(pid, null);
29014
29317
  const when = began === null ? {} : { startedAt: began };
29015
29318
  if (process.platform === "linux") {
29016
- const raw = readFileSync13(`/proc/${pid}/cmdline`, "utf8").replaceAll("\x00", " ").trim();
29319
+ const raw = readFileSync14(`/proc/${pid}/cmdline`, "utf8").replaceAll("\x00", " ").trim();
29017
29320
  return raw === "" ? null : { kind: "commandLine", text: raw, ...when };
29018
29321
  }
29019
29322
  const args = execFileSync2("ps", ["-p", String(pid), "-o", "args="], {
@@ -29142,13 +29445,13 @@ function clearRunRecord(ours = () => true) {
29142
29445
  continue;
29143
29446
  }
29144
29447
  try {
29145
- rmSync5(path);
29448
+ rmSync6(path);
29146
29449
  } catch {}
29147
29450
  }
29148
29451
  }
29149
29452
  function ageMs(path, now) {
29150
29453
  try {
29151
- return now - statSync4(path).mtimeMs;
29454
+ return now - statSync5(path).mtimeMs;
29152
29455
  } catch {
29153
29456
  return 0;
29154
29457
  }
@@ -29166,31 +29469,83 @@ function sweepTranscripts(keepMs = 24 * 60 * 60 * 1000, now = Date.now()) {
29166
29469
  if (!entry.startsWith("run-") || !entry.endsWith(".log")) {
29167
29470
  continue;
29168
29471
  }
29169
- const path = join13(directory, entry);
29472
+ const path = join14(directory, entry);
29170
29473
  if (path === keep || ageMs(path, now) < keepMs) {
29171
29474
  continue;
29172
29475
  }
29173
29476
  try {
29174
- rmSync5(path);
29477
+ rmSync6(path);
29175
29478
  } catch {}
29176
29479
  }
29177
29480
  }
29178
29481
 
29179
29482
  // src/setup.ts
29180
- import { cpSync, existsSync as existsSync8, mkdirSync as mkdirSync8, readFileSync as readFileSync14, rmSync as rmSync6, writeFileSync as writeFileSync9 } from "node:fs";
29181
- import { dirname as dirname8, join as join14 } from "node:path";
29483
+ import { createHash as createHash3 } from "node:crypto";
29484
+ import {
29485
+ cpSync,
29486
+ existsSync as existsSync9,
29487
+ mkdirSync as mkdirSync9,
29488
+ readdirSync as readdirSync6,
29489
+ readFileSync as readFileSync15,
29490
+ rmSync as rmSync7,
29491
+ statSync as statSync6,
29492
+ writeFileSync as writeFileSync10
29493
+ } from "node:fs";
29494
+ import { dirname as dirname9, join as join15 } from "node:path";
29495
+ import { fileURLToPath as fileURLToPath2 } from "node:url";
29182
29496
  var ADDONS = ["gdharness_editor", "gdharness_runtime", "auto_reload"];
29497
+ var EDITOR_PLUGINS = ["gdharness_editor", "auto_reload"];
29183
29498
  var RUNTIME_AUTOLOAD = {
29184
29499
  name: "GdharnessRuntime",
29185
29500
  path: "addons/gdharness_runtime/runtime_autoload.gd"
29186
29501
  };
29187
29502
  var VERSION_MARKER = ".gdharness-version";
29503
+ var DIGEST_MARKER = ".gdharness-digest";
29504
+ function editorAddonDigest(from = shippedAddonsDirectory()) {
29505
+ const hash = createHash3("sha256");
29506
+ const walk = (directory, relative) => {
29507
+ for (const name of readdirSync6(directory).sort()) {
29508
+ if (name === VERSION_MARKER || name === DIGEST_MARKER) {
29509
+ continue;
29510
+ }
29511
+ const path = join15(directory, name);
29512
+ const inside = `${relative}/${name}`;
29513
+ if (statSync6(path).isDirectory()) {
29514
+ walk(path, inside);
29515
+ } else {
29516
+ hash.update(`${inside}
29517
+ `);
29518
+ hash.update(readFileSync15(path));
29519
+ hash.update(`
29520
+ `);
29521
+ }
29522
+ }
29523
+ };
29524
+ for (const name of EDITOR_PLUGINS) {
29525
+ walk(join15(from, name), name);
29526
+ }
29527
+ return hash.digest("hex");
29528
+ }
29529
+ var shippedDigest;
29530
+ function shippedEditorDigest() {
29531
+ if (shippedDigest === undefined) {
29532
+ try {
29533
+ shippedDigest = editorAddonDigest();
29534
+ } catch {
29535
+ shippedDigest = null;
29536
+ }
29537
+ }
29538
+ return shippedDigest ?? undefined;
29539
+ }
29540
+ function shippedAddonsDirectory() {
29541
+ return join15(dirname9(fileURLToPath2(import.meta.url)), "godot", "addons");
29542
+ }
29188
29543
  function installedAddonVersion(projectPath) {
29189
- const marker = join14(projectPath, "addons", ADDONS[0], VERSION_MARKER);
29190
- if (!existsSync8(marker)) {
29544
+ const marker = join15(projectPath, "addons", ADDONS[0], VERSION_MARKER);
29545
+ if (!existsSync9(marker)) {
29191
29546
  return null;
29192
29547
  }
29193
- const said = readFileSync14(marker, "utf8").trim();
29548
+ const said = readFileSync15(marker, "utf8").trim();
29194
29549
  return said === "" ? null : said;
29195
29550
  }
29196
29551
 
@@ -29211,7 +29566,7 @@ function argumentsOf(spec, op) {
29211
29566
  return Object.keys(spec.parameters).filter((name) => opTakes(spec, op, name));
29212
29567
  }
29213
29568
  function theGamePicked() {
29214
- return "the game this server started or is playing when it is among those running, named under " + "`answeredBy` where there was a choice; else the only game there is; else a refusal naming " + "every game, which is what a machine running two projects at once gets";
29569
+ return "the game this server started or is playing when it is among those running, named under " + "`answeredBy` where there was a choice; else the only game there is; else a refusal naming " + "every game and which argument tells them apart, `pid` when they are all one project and " + "`projectPath` when they are not";
29215
29570
  }
29216
29571
  var PROJECT_PATH = {
29217
29572
  type: "string",
@@ -29878,7 +30233,7 @@ var TOOL_SPECS = [
29878
30233
  },
29879
30234
  {
29880
30235
  name: "editor_output",
29881
- description: "What the project started by editor_run has printed, as entries with a severity: the errors and warnings the engine reported, each with where it happened, and everything else as info. For a run the editor plays the reports come from the game itself, through the runtime addon, since the editor's debugger relays what a game prints and not what it reports; a played game without the runtime addon answers here with its prints alone. Answers with the counts and the verdict as well as the entries. heldAt is three answers: null for a run this server knows is not held, running or over, an object for one it knows is held at a breakpoint, whether the adapter said so or the runtime did by accepting a connection and never answering, and absent with heldUnknown beside it for a server that connected to the debugger after the stop and has no runtime to ask, since the adapter reports a stop only to the sessions connected when it happened. A run that has quit still answers here, with running false and its exit code, until the next one starts. A run this server started also answers with transcript, the file both its streams are written to: uncapped, written while the run is going, and the thing to read or tail for a long run rather than the engine's own log, which every engine start rotates away. Read the file for output and this answer for state: endedBy says whether gdharness ended the run, and a transcript that has stopped growing is a run between prints rather than a run that is over. running is asked of the operating system whenever there is a process to ask about, which is every run this server started and every editor-played run whose game announced its runtime. For an editor-played run that never announced, there is no process id here and running is the editor's answer, which can lag: one measured elsewhere reported a game as playing for fifteen seconds after its process had been ended from outside. editor_status names that game under runtimes, which does not depend on the editor answering.",
30236
+ description: "What the project started by editor_run has printed, as entries with a severity: the errors and warnings the engine reported, each with where it happened, and everything else as info. For a run the editor plays the reports come from the game itself, through the runtime addon, since the editor's debugger relays what a game prints and not what it reports; a played game without the runtime addon answers here with its prints alone. Answers with the counts and the verdict as well as the entries. heldAt is three answers: null for a run this server knows is not held, running or over, an object for one it knows is held at a breakpoint, whether the adapter said so or the runtime did by accepting a connection and never answering, and absent with heldUnknown beside it for a server that connected to the debugger after the stop and has no runtime to ask, since the adapter reports a stop only to the sessions connected when it happened. A run that has quit still answers here, with running false and its exit code, until the next one starts. A run this server started also answers with transcript, the file both its streams are written to: uncapped, written while the run is going, and the thing to read or tail for a long run rather than the engine's own log, which every engine start rotates away. Read the file for output and this answer for state: endedBy says whether gdharness ended the run, and a transcript that has stopped growing is a run between prints rather than a run that is over. running is asked of the operating system whenever there is a process to ask about, which is every run this server started and every editor-played run whose game announced its runtime. For an editor-played run that never announced, there is no process id here and running is the editor's answer, which can lag: one measured elsewhere reported a game as playing for fifteen seconds after its process had been ended from outside. editor_status names that game under runtimes, which does not depend on the editor answering. op: \"editor\" answers about the editor itself instead, which is a different log and the only way anything here can read what an editor printed: its console reaches no plugin, so the addon cannot scrape it, and an editor gdharness opened is asked for a log file as it starts. That covers the whole session from the editor's first line, which is where the output worth reading usually is, since the class cache, the plugins and the language server all say what they have to say before any harness has connected. The answer carries the entries in the order they were printed and, beside them under repeated, each message shape several lines share: how many, what varied in each slot of it and how many different values stood there, the first occurrence, and the ordinary lines above the group, found by walking back past the group rather than by counting lines. An editor somebody opened by hand has no console here and is refused saying so rather than answered empty, because an editor that printed nothing and an editor nobody captured are not the same thing.",
29882
30237
  parameters: {
29883
30238
  severity: {
29884
30239
  type: "string",
@@ -29886,10 +30241,12 @@ var TOOL_SPECS = [
29886
30241
  description: "The least severe entry to include. Default info, which is everything."
29887
30242
  },
29888
30243
  sinceLastCall: {
30244
+ ops: ["run"],
29889
30245
  type: "boolean",
29890
30246
  description: "Only entries this has not already answered with at this severity. Default false. Measured per severity floor and not across them, so polling for errors leaves the rest of the output for a later read rather than consuming it, and a read with contains is a search over the whole run and consumes nothing."
29891
30247
  },
29892
30248
  cpu: {
30249
+ ops: ["run"],
29893
30250
  type: "boolean",
29894
30251
  description: "Also answer with cpuSeconds, the processor time the run has used. Default false, because asking costs a subprocess. Worth it when elapsedMs is climbing and nothing is being printed: processor time standing still is a run that has stopped doing anything, rather than one that is slow."
29895
30252
  },
@@ -29900,8 +30257,24 @@ var TOOL_SPECS = [
29900
30257
  limit: {
29901
30258
  type: "number",
29902
30259
  description: "The most entries to answer with, newest kept. Default 200, and omitted says how many matching entries that left out. A run long enough to pass it needs this raised, not just filtered: filtering narrows what counts as matching, and the newest of those is still all one answer carries."
30260
+ },
30261
+ before: {
30262
+ ops: ["editor"],
30263
+ type: "number",
30264
+ description: "How many lines above a repeated group to carry with it, counting only lines that are not themselves part of a group. Default 3. The line that explains a wall of identical errors is the last ordinary line above it, so this walks past the wall rather than a fixed distance back."
29903
30265
  }
29904
30266
  },
30267
+ operations: {
30268
+ run: {
30269
+ summary: "What the game a run is playing has printed, which is everything described above",
30270
+ requires: []
30271
+ },
30272
+ editor: {
30273
+ summary: "What the connected editor itself has printed, from its first line, with repeated messages grouped and the ordinary lines above each group carried with it",
30274
+ requires: []
30275
+ }
30276
+ },
30277
+ defaultOperation: "run",
29905
30278
  requires: []
29906
30279
  },
29907
30280
  {
@@ -30360,7 +30733,7 @@ function buildToolDefinitions() {
30360
30733
  var UPDATE_NOTICE_EVERY = 500;
30361
30734
  var FEEDBACK_NOTICE_EVERY = 250;
30362
30735
  var run5 = promisify5(execFile5);
30363
- var __dirname2 = dirname9(fileURLToPath2(import.meta.url));
30736
+ var __dirname2 = dirname10(fileURLToPath3(import.meta.url));
30364
30737
  var EDITOR_RESTART_TIMEOUT_MS = 90000;
30365
30738
  var HOLD_PING_MS = 2000;
30366
30739
  var SLOWEST_FRAME_RATE = 20;
@@ -30473,6 +30846,7 @@ function endedToStartThis(ended) {
30473
30846
  return ` ${which} was ended to start this one; its output is no longer what editor_output answers about.`;
30474
30847
  }
30475
30848
  var LONGEST_BOOT_NOTED_MS = 5 * 60000;
30849
+ var MOST_GROUPS = 20;
30476
30850
  function runtimeVerdict(endpoint, after) {
30477
30851
  if (endpoint !== null) {
30478
30852
  return { listening: true, pid: endpoint.pid, port: endpoint.port };
@@ -30647,13 +31021,13 @@ function aboveTheRunner(entry) {
30647
31021
  }
30648
31022
  function elsewhereIn(projectPath, asked) {
30649
31023
  const wanted = asked.replace(/^res:\/\//, "").replace(/\/+$/, "");
30650
- if (wanted === "" || existsSync9(join15(projectPath, wanted))) {
31024
+ if (wanted === "" || existsSync10(join16(projectPath, wanted))) {
30651
31025
  return "";
30652
31026
  }
30653
31027
  const root = (wanted.split("/")[0] ?? "").toLowerCase();
30654
31028
  let near = [];
30655
31029
  try {
30656
- near = readdirSync6(projectPath, { withFileTypes: true }).filter((entry) => entry.isDirectory() && entry.name.toLowerCase() !== root).map((entry) => entry.name).filter((name) => name.toLowerCase().startsWith("test") || root.startsWith(name.toLowerCase()));
31030
+ near = readdirSync7(projectPath, { withFileTypes: true }).filter((entry) => entry.isDirectory() && entry.name.toLowerCase() !== root).map((entry) => entry.name).filter((name) => name.toLowerCase().startsWith("test") || root.startsWith(name.toLowerCase()));
30657
31031
  } catch {
30658
31032
  return "";
30659
31033
  }
@@ -30661,7 +31035,7 @@ function elsewhereIn(projectPath, asked) {
30661
31035
  return near.length === 0 ? "" : ` The project does have ${there}.`;
30662
31036
  }
30663
31037
  function hasMainScene(projectFile) {
30664
- const scene = parseProjectGodot(readFileSync15(projectFile, "utf8"))["application"]?.["run/main_scene"];
31038
+ const scene = parseProjectGodot(readFileSync16(projectFile, "utf8"))["application"]?.["run/main_scene"];
30665
31039
  return typeof scene === "string" && scene !== "";
30666
31040
  }
30667
31041
  function runtimeAddonAnnouncesEditors(projectPath) {
@@ -30669,13 +31043,13 @@ function runtimeAddonAnnouncesEditors(projectPath) {
30669
31043
  return false;
30670
31044
  }
30671
31045
  try {
30672
- return readFileSync15(join15(projectPath, RUNTIME_AUTOLOAD.path), "utf8").includes('"editor_pid"');
31046
+ return readFileSync16(join16(projectPath, RUNTIME_AUTOLOAD.path), "utf8").includes('"editor_pid"');
30673
31047
  } catch {
30674
31048
  return false;
30675
31049
  }
30676
31050
  }
30677
31051
  function editorPlaysHeadless(projectFile) {
30678
- const runArgs = parseProjectGodot(readFileSync15(projectFile, "utf8"))["editor"]?.["run/main_run_args"];
31052
+ const runArgs = parseProjectGodot(readFileSync16(projectFile, "utf8"))["editor"]?.["run/main_run_args"];
30679
31053
  return typeof runArgs === "string" && /(?:^|\s)--headless(?:\s|$)/.test(runArgs);
30680
31054
  }
30681
31055
  function camelCased(params) {
@@ -30694,7 +31068,7 @@ function needsItsOwnProcess(asked) {
30694
31068
  class GodotServer {
30695
31069
  mcp;
30696
31070
  locator = new GodotLocator;
30697
- operationsScript = join15(__dirname2, "godot", "operations", "godot_operations.gd");
31071
+ operationsScript = join16(__dirname2, "godot", "operations", "godot_operations.gd");
30698
31072
  godotBridge;
30699
31073
  tools = buildToolDefinitions();
30700
31074
  activeProcess = null;
@@ -31359,8 +31733,8 @@ class GodotServer {
31359
31733
  if (path === undefined) {
31360
31734
  return { ok: false, response: this.createErrorResponse("projectPath is required.") };
31361
31735
  }
31362
- const file = join15(path, "project.godot");
31363
- if (!existsSync9(file)) {
31736
+ const file = join16(path, "project.godot");
31737
+ if (!existsSync10(file)) {
31364
31738
  return {
31365
31739
  ok: false,
31366
31740
  response: this.createErrorResponse(`Not a Godot project: ${path}`, [
@@ -31628,7 +32002,7 @@ class GodotServer {
31628
32002
  if (!engine.ok) {
31629
32003
  return engine.response;
31630
32004
  }
31631
- const application = parseProjectGodot(readFileSync15(project.value.file, "utf8"))["application"] ?? {};
32005
+ const application = parseProjectGodot(readFileSync16(project.value.file, "utf8"))["application"] ?? {};
31632
32006
  const name = application["config/name"];
31633
32007
  const mainScene = application["run/main_scene"];
31634
32008
  const info = {
@@ -31689,18 +32063,18 @@ class GodotServer {
31689
32063
  if (!engine.ok) {
31690
32064
  return engine.response;
31691
32065
  }
31692
- const folder = dirname9(output.absolutePath);
32066
+ const folder = dirname10(output.absolutePath);
31693
32067
  try {
31694
- mkdirSync9(folder, { recursive: true });
32068
+ mkdirSync10(folder, { recursive: true });
31695
32069
  } catch (error) {
31696
- return this.createErrorResponse(`Cannot create ${dirname9(output.relativePath)} to export into: ${errorMessage(error)}`);
32070
+ return this.createErrorResponse(`Cannot create ${dirname10(output.relativePath)} to export into: ${errorMessage(error)}`);
31697
32071
  }
31698
32072
  const debug = readBoolean(args, "debug") ?? false;
31699
- const exportLogs = mkdtempSync2(join15(tmpdir4(), "gdharness-export-"));
32073
+ const exportLogs = mkdtempSync2(join16(tmpdir4(), "gdharness-export-"));
31700
32074
  const exportArgs = [
31701
32075
  "--headless",
31702
32076
  "--log-file",
31703
- join15(exportLogs, "engine.log"),
32077
+ join16(exportLogs, "engine.log"),
31704
32078
  "--path",
31705
32079
  project.value.path,
31706
32080
  debug ? "--export-debug" : "--export-release",
@@ -31728,7 +32102,7 @@ class GodotServer {
31728
32102
  log.finish();
31729
32103
  const problems = log.select({ severity: "warning", sinceLastCall: false, limit: 200 });
31730
32104
  const verdict = {
31731
- exported: exitCode === 0 && log.count("error") === 0 && existsSync9(output.absolutePath),
32105
+ exported: exitCode === 0 && log.count("error") === 0 && existsSync10(output.absolutePath),
31732
32106
  preset,
31733
32107
  outputPath: output.relativePath,
31734
32108
  debug,
@@ -31758,7 +32132,7 @@ class GodotServer {
31758
32132
  return contained.response;
31759
32133
  }
31760
32134
  const runner = "addons/gdUnit4/bin/GdUnitCmdTool.gd";
31761
- if (!existsSync9(join15(project.value.path, runner))) {
32135
+ if (!existsSync10(join16(project.value.path, runner))) {
31762
32136
  return this.createErrorResponse(`gdUnit4 is not installed in this project: no ${runner}.`, [
31763
32137
  "Install gdUnit4 under addons/gdUnit4, from https://github.com/godot-gdunit-labs/gdUnit4"
31764
32138
  ]);
@@ -31793,7 +32167,7 @@ class GodotServer {
31793
32167
  ];
31794
32168
  const timeoutMs = readPositiveNumber(args, "timeoutMs") ?? 600000;
31795
32169
  this.logDebug(`Running tests: ${engine.value} ${cmdArgs.join(" ")}`);
31796
- const userData = mkdtempSync2(join15(tmpdir4(), "gdharness-tests-"));
32170
+ const userData = mkdtempSync2(join16(tmpdir4(), "gdharness-tests-"));
31797
32171
  const run = this.spawnGame(engine.value, cmdArgs, userDataIn(userData));
31798
32172
  const hung = await new Promise((resolve) => {
31799
32173
  const timer = setTimeout(() => {
@@ -31809,21 +32183,21 @@ class GodotServer {
31809
32183
  resolve(false);
31810
32184
  });
31811
32185
  });
31812
- const reportsDir = join15(project.value.path, ".godot", ...ours.split("/"));
32186
+ const reportsDir = join16(project.value.path, ".godot", ...ours.split("/"));
31813
32187
  let report = null;
31814
32188
  let reportProblem = null;
31815
32189
  try {
31816
- const written = existsSync9(reportsDir) ? readdirSync6(reportsDir).filter((name) => name.startsWith("report_")).sort((a, b) => Number(a.slice("report_".length)) - Number(b.slice("report_".length))) : [];
32190
+ const written = existsSync10(reportsDir) ? readdirSync7(reportsDir).filter((name) => name.startsWith("report_")).sort((a, b) => Number(a.slice("report_".length)) - Number(b.slice("report_".length))) : [];
31817
32191
  const newest = written.at(-1);
31818
32192
  if (newest !== undefined) {
31819
- report = parseJUnit(readFileSync15(join15(reportsDir, newest, "results.xml"), "utf8"));
32193
+ report = parseJUnit(readFileSync16(join16(reportsDir, newest, "results.xml"), "utf8"));
31820
32194
  }
31821
32195
  } catch (error) {
31822
32196
  reportProblem = errorMessage(error);
31823
32197
  } finally {
31824
32198
  discard(reportsDir, userData);
31825
32199
  try {
31826
- rmdirSync(dirname9(reportsDir));
32200
+ rmdirSync(dirname10(reportsDir));
31827
32201
  } catch {}
31828
32202
  }
31829
32203
  const engineEntries = run.log.select({ severity: "warning", sinceLastCall: false, limit: 200 }).entries.map(aboveTheRunner);
@@ -31942,7 +32316,7 @@ class GodotServer {
31942
32316
  warnings: diagnostics.length - errors,
31943
32317
  diagnostics,
31944
32318
  ...disagrees
31945
- }, this.godotBridge.getStatus().addonVersion, SERVER_VERSION));
32319
+ }, this.godotBridge.getStatus().addonVersion, SERVER_VERSION, this.godotBridge.getStatus().addonDigest, shippedEditorDigest()));
31946
32320
  }
31947
32321
  whereTheFileDisagrees(diagnostics, args) {
31948
32322
  const messages = diagnostics.map((entry) => asParams(entry)["message"]).filter((message) => typeof message === "string");
@@ -31955,11 +32329,11 @@ class GodotServer {
31955
32329
  const cached = cachedClasses(projectPath);
31956
32330
  const contradicted = cached === null ? [] : contradictedDiagnostics(messages, cached, (resourcePath) => {
31957
32331
  const contained = resolveWithinProject(projectPath, resourcePath);
31958
- if (!contained.ok || !existsSync9(contained.absolutePath)) {
32332
+ if (!contained.ok || !existsSync10(contained.absolutePath)) {
31959
32333
  return null;
31960
32334
  }
31961
32335
  try {
31962
- return readFileSync15(contained.absolutePath, "utf8");
32336
+ return readFileSync16(contained.absolutePath, "utf8");
31963
32337
  } catch {
31964
32338
  return null;
31965
32339
  }
@@ -32009,11 +32383,11 @@ class GodotServer {
32009
32383
  }
32010
32384
  sourceOfClass(projectPath, resourcePath) {
32011
32385
  const contained = resolveWithinProject(projectPath, resourcePath);
32012
- if (!contained.ok || !existsSync9(contained.absolutePath)) {
32386
+ if (!contained.ok || !existsSync10(contained.absolutePath)) {
32013
32387
  return null;
32014
32388
  }
32015
32389
  try {
32016
- return readFileSync15(contained.absolutePath, "utf8");
32390
+ return readFileSync16(contained.absolutePath, "utf8");
32017
32391
  } catch {
32018
32392
  return null;
32019
32393
  }
@@ -32129,7 +32503,7 @@ class GodotServer {
32129
32503
  getEditorStatusPayload() {
32130
32504
  const status = this.godotBridge.getStatus();
32131
32505
  const isPortConflict = this.bridgeStartupError?.includes("EADDRINUSE") ?? false;
32132
- const stale = status.connected && status.addonVersion !== SERVER_VERSION;
32506
+ const stale = status.connected && editorIsStale(status.addonVersion, SERVER_VERSION, status.addonDigest, shippedEditorDigest());
32133
32507
  const unfinished = this.restartLeftUnfinished(status.connected);
32134
32508
  return {
32135
32509
  ...status,
@@ -32146,7 +32520,7 @@ class GodotServer {
32146
32520
  awaitingLaunchedEditor: status.connected || !this.launchedEditorIsUp() ? undefined : this.launchedEditor?.pid ?? undefined,
32147
32521
  ...status.connected ? this.whatTheLaunchedEditorDropped() : {},
32148
32522
  startupError: this.bridgeStartupError,
32149
- staleNote: stale ? addonMismatch(status.addonVersion, SERVER_VERSION) : undefined,
32523
+ staleNote: stale ? addonMismatch(status.addonVersion, SERVER_VERSION, status.addonDigest, shippedEditorDigest()) : undefined,
32150
32524
  ...this.breakpointsAtRisk(),
32151
32525
  retryingBridge: this.bridgeRetry === null ? undefined : true,
32152
32526
  announcedAt: this.announcedAt ?? undefined,
@@ -32242,7 +32616,7 @@ class GodotServer {
32242
32616
  return null;
32243
32617
  }
32244
32618
  try {
32245
- return settingKeys(readFileSync15(join15(projectPath, "project.godot"), "utf8"));
32619
+ return settingKeys(readFileSync16(join16(projectPath, "project.godot"), "utf8"));
32246
32620
  } catch {
32247
32621
  return null;
32248
32622
  }
@@ -32278,8 +32652,8 @@ class GodotServer {
32278
32652
  editorPid: now.editorPid,
32279
32653
  addonVersion: now.addonVersion,
32280
32654
  serverVersion: SERVER_VERSION,
32281
- addonIsStale: now.addonVersion !== SERVER_VERSION,
32282
- staleNote: addonMismatch(now.addonVersion, SERVER_VERSION),
32655
+ addonIsStale: editorIsStale(now.addonVersion, SERVER_VERSION, now.addonDigest, shippedEditorDigest()),
32656
+ staleNote: addonMismatch(now.addonVersion, SERVER_VERSION, now.addonDigest, shippedEditorDigest()),
32283
32657
  ...this.whatTheEditorDropped(settingsBefore, after),
32284
32658
  tookMs: Date.now() - began
32285
32659
  });
@@ -32372,7 +32746,8 @@ class GodotServer {
32372
32746
  });
32373
32747
  }
32374
32748
  async openAnEditor(engine, projectPath, ports) {
32375
- const editor = spawn(engine, editorArguments(projectPath, ports), {
32749
+ clearEditorLog(projectPath);
32750
+ const editor = spawn(engine, editorArguments(projectPath, ports, editorLogPath(projectPath)), {
32376
32751
  stdio: "ignore",
32377
32752
  detached: true,
32378
32753
  env: {
@@ -32383,6 +32758,9 @@ class GodotServer {
32383
32758
  [OPENED_BY_A_SERVER]: "1"
32384
32759
  }
32385
32760
  });
32761
+ if (editor.pid !== undefined) {
32762
+ writeEditorLogNote(projectPath, editor.pid);
32763
+ }
32386
32764
  const started = await new Promise((resolve) => {
32387
32765
  editor.once("spawn", () => {
32388
32766
  resolve(null);
@@ -32489,7 +32867,7 @@ class GodotServer {
32489
32867
  }
32490
32868
  async runtimeUp(projectPath, before, budget, withArgs) {
32491
32869
  const budgetMs = budget.ms;
32492
- if (!existsSync9(join15(projectPath, RUNTIME_AUTOLOAD.path))) {
32870
+ if (!existsSync10(join16(projectPath, RUNTIME_AUTOLOAD.path))) {
32493
32871
  return runtimeVerdict(null, { addon: false, budgetMs, heldAt: null, running: false, withArgs });
32494
32872
  }
32495
32873
  let asking = null;
@@ -32552,7 +32930,7 @@ class GodotServer {
32552
32930
  }
32553
32931
  let announcedAt;
32554
32932
  try {
32555
- announcedAt = statSync5(endpoint.file).mtimeMs;
32933
+ announcedAt = statSync7(endpoint.file).mtimeMs;
32556
32934
  } catch {
32557
32935
  return;
32558
32936
  }
@@ -32588,7 +32966,7 @@ class GodotServer {
32588
32966
  }
32589
32967
  const startedAt = Date.now();
32590
32968
  const transcript = openTranscript(startedAt);
32591
- closeSync(transcript.fd);
32969
+ closeSync2(transcript.fd);
32592
32970
  const playAnswer = asParams(JSON.parse(answer.content[0]?.text ?? "{}"));
32593
32971
  const played = {
32594
32972
  process: null,
@@ -32655,7 +33033,7 @@ class GodotServer {
32655
33033
  }
32656
33034
  let handle;
32657
33035
  try {
32658
- handle = openSync2(game.errorReport, "r");
33036
+ handle = openSync3(game.errorReport, "r");
32659
33037
  } catch {
32660
33038
  return;
32661
33039
  }
@@ -32666,7 +33044,7 @@ class GodotServer {
32666
33044
  return;
32667
33045
  }
32668
33046
  const buffer = Buffer.alloc(size - from);
32669
- const read = readSync(handle, buffer, 0, buffer.length, from);
33047
+ const read = readSync2(handle, buffer, 0, buffer.length, from);
32670
33048
  game.errorReportOffset = from + read;
32671
33049
  const reported = buffer.subarray(0, read);
32672
33050
  if (game.transcript !== null) {
@@ -32675,7 +33053,7 @@ class GodotServer {
32675
33053
  game.log.append("stderr", reported);
32676
33054
  }
32677
33055
  } finally {
32678
- closeSync(handle);
33056
+ closeSync2(handle);
32679
33057
  }
32680
33058
  }
32681
33059
  recordWhatItBrokeOn(game) {
@@ -32769,7 +33147,7 @@ class GodotServer {
32769
33147
  ...env ? { env } : {}
32770
33148
  });
32771
33149
  } finally {
32772
- closeSync(transcript.fd);
33150
+ closeSync2(transcript.fd);
32773
33151
  }
32774
33152
  child.unref();
32775
33153
  const log = new GameLog;
@@ -32816,7 +33194,7 @@ class GodotServer {
32816
33194
  }
32817
33195
  let handle;
32818
33196
  try {
32819
- handle = openSync2(game.transcript, "r");
33197
+ handle = openSync3(game.transcript, "r");
32820
33198
  } catch {
32821
33199
  return;
32822
33200
  }
@@ -32826,11 +33204,11 @@ class GodotServer {
32826
33204
  return;
32827
33205
  }
32828
33206
  const buffer = Buffer.alloc(size - game.readOffset);
32829
- const read = readSync(handle, buffer, 0, buffer.length, game.readOffset);
33207
+ const read = readSync2(handle, buffer, 0, buffer.length, game.readOffset);
32830
33208
  game.readOffset += read;
32831
33209
  game.log.append("transcript", buffer.subarray(0, read));
32832
33210
  } finally {
32833
- closeSync(handle);
33211
+ closeSync2(handle);
32834
33212
  }
32835
33213
  }
32836
33214
  async pickUpWhatTheEditorIsPlaying() {
@@ -33028,7 +33406,7 @@ class GodotServer {
33028
33406
  }
33029
33407
  const unreadable = this.aRuntimeOfOursIsTooFarOff();
33030
33408
  if (unreadable !== null) {
33031
- const behind = unreadable.protocol > RUNTIME_PROTOCOL ? "this server is the older half: reconnect it so it spawns the installed version" : "the addon is the older half: reinstall it and restart the game";
33409
+ const behind = whichHalfIsBehind([unreadable]);
33032
33410
  return `A game is running for ${unreadable.project.path}, pid ${unreadable.pid}, announced in protocol` + ` ${unreadable.protocol}, which this server does not speak: it speaks ${RUNTIME_PROTOCOL}, so ${behind}.` + " Until then neither editor_run nor the runtime_* tools can reach it." + " A run started here is a separate process rather than a replacement, so starting one leaves" + " this game running as well.";
33033
33411
  }
33034
33412
  const record = readRunRecord();
@@ -33153,7 +33531,66 @@ class GodotServer {
33153
33531
  }
33154
33532
  return runIsUp(run, said);
33155
33533
  }
33534
+ handleEditorConsole(args) {
33535
+ const status = this.godotBridge.getStatus();
33536
+ const projectPath = status.projectPath ?? this.ownProject ?? undefined;
33537
+ if (projectPath === undefined) {
33538
+ return this.createErrorResponse("No editor is connected, so there is no console to read.", [
33539
+ "editor_status says whether one is connected and whether it may yet be",
33540
+ "editor_launch opens one, and an editor opened here has its console captured"
33541
+ ]);
33542
+ }
33543
+ const launched = status.connected ? null : this.launchedEditor?.pid ?? null;
33544
+ if (!status.connected && launched === null) {
33545
+ return this.createErrorResponse("No editor is connected, so there is no console to read.", [
33546
+ "editor_status says whether one is connected and whether it may yet be",
33547
+ "editor_launch opens one, and an editor opened here has its console captured"
33548
+ ]);
33549
+ }
33550
+ const editorPid = status.connected ? status.editorPid ?? null : launched;
33551
+ const console2 = editorConsole(projectPath, editorPid, status.connected ? status.openedByAServer === true : true);
33552
+ if ("kind" in console2) {
33553
+ return this.createErrorResponse(theConsoleWasNotCaptured(console2, projectPath), [
33554
+ "editor_launch restart replaces this editor with one whose console is captured",
33555
+ "editor_output with no op answers about the game a run is playing, which is a different log"
33556
+ ]);
33557
+ }
33558
+ const severity = readString(args, "severity");
33559
+ const selected = console2.log.select({
33560
+ severity: severity === "error" || severity === "warning" ? severity : "info",
33561
+ sinceLastCall: false,
33562
+ contains: readNonEmptyString(args, "contains"),
33563
+ limit: readPositiveNumber(args, "limit") ?? 200
33564
+ });
33565
+ const everything = console2.log.everything();
33566
+ const grouped = bursts(everything, readPositiveNumber(args, "before") ?? 3);
33567
+ const shown = grouped.slice(0, MOST_GROUPS);
33568
+ return this.jsonTextResponse({
33569
+ editorPid: editorPid ?? undefined,
33570
+ projectPath,
33571
+ ...status.connected ? {} : this.launchedEditorIsUp() ? { stillConnecting: true } : { exitedBeforeConnecting: true },
33572
+ capturedIn: console2.path,
33573
+ ...console2.of === undefined ? {} : {
33574
+ readFromTheStart: console2.readBytes,
33575
+ consoleBytes: console2.of,
33576
+ truncatedNote: `This editor's console is ${console2.of} bytes and the first ${console2.readBytes} of them were read, which is the start of the session rather than the end of it. Restart the editor to begin a fresh console.`
33577
+ },
33578
+ lines: everything.length,
33579
+ counts: {
33580
+ error: console2.log.count("error"),
33581
+ warning: console2.log.count("warning"),
33582
+ info: console2.log.count("info")
33583
+ },
33584
+ repeated: shown,
33585
+ moreShapes: grouped.length > shown.length ? grouped.length - shown.length : undefined,
33586
+ entries: forAnswer(selected.entries),
33587
+ omitted: selected.omitted === 0 ? undefined : selected.omitted
33588
+ });
33589
+ }
33156
33590
  async handleGetDebugOutput(args, waitedMs) {
33591
+ if (readString(args, "op") === "editor") {
33592
+ return this.handleEditorConsole(args);
33593
+ }
33157
33594
  await this.pickUpWhatTheEditorIsPlaying();
33158
33595
  const run = this.currentRun();
33159
33596
  if (!run) {
@@ -33430,6 +33867,7 @@ class GodotServer {
33430
33867
  }
33431
33868
  async rebuildClassCache(projectPath) {
33432
33869
  const before = cachedClasses(projectPath);
33870
+ const writtenBefore = cacheWrittenAt(projectPath);
33433
33871
  const noted = readClassNote(projectPath);
33434
33872
  const rebuilt = await this.operation("refresh_class_cache", {}, projectPath);
33435
33873
  if (!rebuilt.ok) {
@@ -33440,21 +33878,30 @@ class GodotServer {
33440
33878
  return rebuilt;
33441
33879
  }
33442
33880
  const lost = noted === null || before === null ? [] : noted.rebuiltWith.filter((name) => !before.has(name) && after.has(name)).sort();
33443
- const seenLosing = lost.length > 0 && this.godotBridge.isConnected();
33881
+ const cameBack = writtenBefore === null ? [] : declaredSince(projectPath, after, lost, writtenBefore);
33882
+ const droppedByTheEditor = lost.filter((name) => !cameBack.includes(name));
33883
+ const seenLosing = droppedByTheEditor.length > 0 && this.godotBridge.isConnected();
33444
33884
  writeClassNote(projectPath, {
33445
33885
  rebuiltWith: [...after.keys()].sort(),
33446
33886
  ...seenLosing ? { shortListEditorPid: this.godotBridge.getStatus().editorPid ?? null } : noted?.shortListEditorPid === undefined ? {} : { shortListEditorPid: noted.shortListEditorPid }
33447
33887
  });
33448
- if (!seenLosing) {
33888
+ const notes = [];
33889
+ if (seenLosing) {
33890
+ const them = droppedByTheEditor.length === 1 ? "it" : "them";
33891
+ notes.push(`The cache held ${droppedByTheEditor.join(", ")} after the last rebuild and not when this one began, so the editor holding this project wrote it without ${them} in between: it holds a list shorter than the files and writes that list on every save and scan, and each rebuild puts ${them} back. editor_launch restart is what ends it.`);
33892
+ }
33893
+ if (cameBack.length > 0) {
33894
+ notes.push(`${cameBack.join(", ")} left the cache while ${cameBack.length === 1 ? "its script was" : "their scripts were"} gone: the script${cameBack.length === 1 ? " is" : "s are"} newer than the cache the editor last wrote, so it listed what was on disk then. editor_rescan picks ${cameBack.length === 1 ? "it" : "them"} up.`);
33895
+ }
33896
+ if (notes.length === 0) {
33449
33897
  return rebuilt;
33450
33898
  }
33451
- const them = lost.length === 1 ? "it" : "them";
33452
33899
  return {
33453
33900
  ...rebuilt,
33454
33901
  payload: {
33455
33902
  ...rebuilt.payload,
33456
33903
  lostSinceLastRebuild: lost,
33457
- note: `The cache held ${lost.join(", ")} after the last rebuild and not when this one began, so the editor holding this project wrote it without ${them} in between: it holds a list shorter than the files and writes that list on every save and scan, and each rebuild puts ${them} back. editor_launch restart is what ends it.`
33904
+ note: notes.join(" ")
33458
33905
  }
33459
33906
  };
33460
33907
  }
@@ -33530,7 +33977,7 @@ class GodotServer {
33530
33977
  ]);
33531
33978
  }
33532
33979
  try {
33533
- return this.jsonTextResponse(markIfStale(await this.godotBridge.invokeTool(toolName, args), this.godotBridge.getStatus().addonVersion, SERVER_VERSION));
33980
+ return this.jsonTextResponse(markIfStale(await this.godotBridge.invokeTool(toolName, args), this.godotBridge.getStatus().addonVersion, SERVER_VERSION, this.godotBridge.getStatus().addonDigest, shippedEditorDigest()));
33534
33981
  } catch (error) {
33535
33982
  return this.createErrorResponse(`The editor answered ${toolName} with an error: ${errorMessage(error)}`);
33536
33983
  }
@@ -33604,8 +34051,8 @@ class GodotServer {
33604
34051
  ]);
33605
34052
  }
33606
34053
  const expectsScreenshot = command === "capture_screenshot" || command === "capture_viewport";
33607
- const screenshotDir = expectsScreenshot ? mkdtempSync2(join15(tmpdir4(), "gdharness-runtime-screenshot-")) : null;
33608
- const screenshotPath = screenshotDir ? join15(screenshotDir, "capture.png") : null;
34054
+ const screenshotDir = expectsScreenshot ? mkdtempSync2(join16(tmpdir4(), "gdharness-runtime-screenshot-")) : null;
34055
+ const screenshotPath = screenshotDir ? join16(screenshotDir, "capture.png") : null;
33609
34056
  try {
33610
34057
  const reply = await runtimeRequest(choice.endpoint, command, screenshotPath ? { ...params, output_path: screenshotPath } : params, timeoutMs);
33611
34058
  if (!reply.ok) {
@@ -33629,7 +34076,7 @@ class GodotServer {
33629
34076
  type: "text",
33630
34077
  text: `Screenshot captured: ${dimensions}${chosenAmongSeveral ? ` from pid ${own}, the game this server holds` : ""}`
33631
34078
  },
33632
- { type: "image", data: readFileSync15(screenshotPath).toString("base64"), mimeType: "image/png" }
34079
+ { type: "image", data: readFileSync16(screenshotPath).toString("base64"), mimeType: "image/png" }
33633
34080
  ]
33634
34081
  };
33635
34082
  } finally {