gdharness 0.5.8 → 0.5.10

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
@@ -10126,9 +10126,9 @@ function defectReport(where, error, godotVersion) {
10126
10126
 
10127
10127
  // src/server.ts
10128
10128
  import { execFile as execFile3, spawn } from "node:child_process";
10129
- import { existsSync as existsSync6, mkdtempSync as mkdtempSync2, readdirSync as readdirSync5, readFileSync as readFileSync7, realpathSync as realpathSync2, rmSync as rmSync2 } from "node:fs";
10129
+ import { existsSync as existsSync7, mkdtempSync as mkdtempSync2, readdirSync as readdirSync5, readFileSync as readFileSync8, realpathSync as realpathSync2, rmSync as rmSync4 } from "node:fs";
10130
10130
  import { tmpdir as tmpdir4 } from "node:os";
10131
- import { basename, dirname as dirname2, join as join7, normalize as normalize2 } from "node:path";
10131
+ import { basename, dirname as dirname4, join as join9, normalize as normalize2 } from "node:path";
10132
10132
  import { setTimeout as delay2 } from "node:timers/promises";
10133
10133
  import { fileURLToPath as fileURLToPath2 } from "node:url";
10134
10134
  import { promisify as promisify3 } from "node:util";
@@ -24696,9 +24696,47 @@ class StdioServerTransport {
24696
24696
  }
24697
24697
  }
24698
24698
 
24699
+ // src/bridge-announce.ts
24700
+ import { mkdirSync as mkdirSync2, rmSync, writeFileSync as writeFileSync2 } from "node:fs";
24701
+ import { dirname, join as join2 } from "node:path";
24702
+ function announcementPath(projectPath) {
24703
+ return join2(projectPath, ".godot", "gdharness-bridge.json");
24704
+ }
24705
+ var BRIDGE_ANNOUNCE_PROTOCOL = 1;
24706
+ function announceBridge(projectPath, bridge) {
24707
+ const path = announcementPath(projectPath);
24708
+ try {
24709
+ mkdirSync2(dirname(path), { recursive: true });
24710
+ const announcement = {
24711
+ protocol: BRIDGE_ANNOUNCE_PROTOCOL,
24712
+ host: bridge.host,
24713
+ port: bridge.port,
24714
+ pid: process.pid,
24715
+ version: bridge.version,
24716
+ startedAt: new Date().toISOString()
24717
+ };
24718
+ writeFileSync2(path, `${JSON.stringify(announcement, null, 2)}
24719
+ `, "utf8");
24720
+ return path;
24721
+ } catch (error) {
24722
+ console.error(`[SERVER] Could not announce the editor bridge at ${path}: ${errorMessage(error)}`);
24723
+ return null;
24724
+ }
24725
+ }
24726
+ function withdrawBridge(path) {
24727
+ if (path === null) {
24728
+ return;
24729
+ }
24730
+ try {
24731
+ rmSync(path, { force: true });
24732
+ } catch (error) {
24733
+ console.error(`[SERVER] Could not withdraw the editor bridge announcement: ${errorMessage(error)}`);
24734
+ }
24735
+ }
24736
+
24699
24737
  // src/class-cache.ts
24700
24738
  import { existsSync as existsSync2, readdirSync, readFileSync as readFileSync3 } from "node:fs";
24701
- import { join as join2 } from "node:path";
24739
+ import { join as join3 } from "node:path";
24702
24740
  function declaredClasses(projectPath) {
24703
24741
  const declared = new Map;
24704
24742
  const visit = (directory, prefix) => {
@@ -24706,7 +24744,7 @@ function declaredClasses(projectPath) {
24706
24744
  if (entry.name.startsWith(".")) {
24707
24745
  continue;
24708
24746
  }
24709
- const path = join2(directory, entry.name);
24747
+ const path = join3(directory, entry.name);
24710
24748
  if (entry.isDirectory()) {
24711
24749
  visit(path, `${prefix}${entry.name}/`);
24712
24750
  } else if (entry.isFile() && entry.name.endsWith(".gd")) {
@@ -24721,7 +24759,7 @@ function declaredClasses(projectPath) {
24721
24759
  return declared;
24722
24760
  }
24723
24761
  function cachedClasses(projectPath) {
24724
- const cache = join2(projectPath, ".godot", "global_script_class_cache.cfg");
24762
+ const cache = join3(projectPath, ".godot", "global_script_class_cache.cfg");
24725
24763
  if (!existsSync2(cache)) {
24726
24764
  return null;
24727
24765
  }
@@ -24840,6 +24878,10 @@ var DEFAULT_DAP_PORT = 6006;
24840
24878
  var DAP_REQUEST_TIMEOUT_MS = 1e4;
24841
24879
  var SCOPES_TIMEOUT_MS = 20000;
24842
24880
  var VARIABLES_POLL_TIMEOUT_MS = 2000;
24881
+ function said(body, field) {
24882
+ const value = body?.[field];
24883
+ return typeof value === "string" ? value : "";
24884
+ }
24843
24885
 
24844
24886
  class GodotDAPClient {
24845
24887
  socket = null;
@@ -24854,7 +24896,7 @@ class GodotDAPClient {
24854
24896
  initialized = false;
24855
24897
  attached = false;
24856
24898
  lastThreadId = 1;
24857
- stopped = false;
24899
+ halt = null;
24858
24900
  breakpoints = new Map;
24859
24901
  constructor(port = portFromEnv("GDHARNESS_DAP_PORT", DEFAULT_DAP_PORT), host = "127.0.0.1") {
24860
24902
  this.port = port;
@@ -24903,7 +24945,7 @@ class GodotDAPClient {
24903
24945
  this.connected = false;
24904
24946
  this.initialized = false;
24905
24947
  this.attached = false;
24906
- this.stopped = false;
24948
+ this.halt = null;
24907
24949
  this.socket = null;
24908
24950
  this.failPendingRequests(new Error("DAP connection closed"));
24909
24951
  });
@@ -24923,7 +24965,7 @@ class GodotDAPClient {
24923
24965
  this.connected = false;
24924
24966
  this.initialized = false;
24925
24967
  this.attached = false;
24926
- this.stopped = false;
24968
+ this.halt = null;
24927
24969
  return;
24928
24970
  }
24929
24971
  if (this.connected) {
@@ -24952,7 +24994,7 @@ class GodotDAPClient {
24952
24994
  this.connected = false;
24953
24995
  this.initialized = false;
24954
24996
  this.attached = false;
24955
- this.stopped = false;
24997
+ this.halt = null;
24956
24998
  }
24957
24999
  async ensureConnected() {
24958
25000
  if (!this.connected) {
@@ -25029,12 +25071,16 @@ class GodotDAPClient {
25029
25071
  if (typeof threadId === "number") {
25030
25072
  this.lastThreadId = threadId;
25031
25073
  }
25032
- this.stopped = true;
25074
+ this.halt = {
25075
+ reason: said(body, "reason"),
25076
+ description: said(body, "description"),
25077
+ text: said(body, "text")
25078
+ };
25033
25079
  return;
25034
25080
  }
25035
25081
  if (eventName === "terminated" || eventName === "exited") {
25036
25082
  this.attached = false;
25037
- this.stopped = false;
25083
+ this.halt = null;
25038
25084
  }
25039
25085
  }
25040
25086
  async initialize() {
@@ -25108,7 +25154,7 @@ class GodotDAPClient {
25108
25154
  await this.attach();
25109
25155
  const resolvedThreadId = await this.resolveThreadId(threadId);
25110
25156
  await this.sendRequest("continue", { threadId: resolvedThreadId });
25111
- this.stopped = false;
25157
+ this.halt = null;
25112
25158
  }
25113
25159
  async stepOver(threadId) {
25114
25160
  await this.attach();
@@ -25179,7 +25225,10 @@ class GodotDAPClient {
25179
25225
  return this.connected;
25180
25226
  }
25181
25227
  isStopped() {
25182
- return this.stopped;
25228
+ return this.halt !== null;
25229
+ }
25230
+ whereItStopped() {
25231
+ return this.halt;
25183
25232
  }
25184
25233
  async resolveThreadId(threadId) {
25185
25234
  if (typeof threadId === "number" && threadId > 0) {
@@ -25208,7 +25257,7 @@ class GodotDAPClient {
25208
25257
  this.connected = false;
25209
25258
  this.initialized = false;
25210
25259
  this.attached = false;
25211
- this.stopped = false;
25260
+ this.halt = null;
25212
25261
  this.reader = new FrameReader;
25213
25262
  socket?.destroy();
25214
25263
  this.failPendingRequests(new Error(`Godot DAP ${detail}. The connection was dropped.`));
@@ -25364,6 +25413,16 @@ class GameLog {
25364
25413
  this.entries.push(entry);
25365
25414
  this.lastHeadline = headline ? { index: entry.index, detail } : null;
25366
25415
  }
25416
+ record(severity, text) {
25417
+ this.entries.push({
25418
+ index: this.entries.length,
25419
+ severity,
25420
+ source: "debugger",
25421
+ text,
25422
+ detail: []
25423
+ });
25424
+ this.lastHeadline = null;
25425
+ }
25367
25426
  get all() {
25368
25427
  return this.entries;
25369
25428
  }
@@ -25426,19 +25485,37 @@ class GodotBridge extends EventEmitter {
25426
25485
  connectionInfo = null;
25427
25486
  pendingRequests = new Map;
25428
25487
  resourceQueues = new Map;
25429
- port;
25488
+ wantedPort;
25489
+ boundPort = null;
25430
25490
  host;
25431
25491
  timeoutMs;
25432
- constructor(port = DEFAULT_PORT, host = DEFAULT_HOST, timeoutMs = DEFAULT_TIMEOUT_MS) {
25492
+ mayMove;
25493
+ constructor(port = DEFAULT_PORT, host = DEFAULT_HOST, timeoutMs = DEFAULT_TIMEOUT_MS, mayMove = false) {
25433
25494
  super();
25434
- this.port = port;
25495
+ this.wantedPort = port;
25435
25496
  this.host = host;
25436
25497
  this.timeoutMs = timeoutMs;
25498
+ this.mayMove = mayMove;
25499
+ }
25500
+ get port() {
25501
+ return this.boundPort ?? this.wantedPort;
25437
25502
  }
25438
- start() {
25503
+ async start() {
25439
25504
  if (this.httpServer) {
25440
- return Promise.resolve();
25505
+ return;
25506
+ }
25507
+ try {
25508
+ await this.listenOn(this.wantedPort);
25509
+ } catch (error) {
25510
+ const held = error instanceof Error && "code" in error && error.code === "EADDRINUSE";
25511
+ if (!held || !this.mayMove) {
25512
+ throw error;
25513
+ }
25514
+ this.log("warn", `Editor bridge port ${this.wantedPort} is held; taking another one.`);
25515
+ await this.listenOn(0);
25441
25516
  }
25517
+ }
25518
+ listenOn(port) {
25442
25519
  return new Promise((resolve, reject) => {
25443
25520
  const server = http.createServer((_req, res) => {
25444
25521
  res.writeHead(404);
@@ -25462,6 +25539,8 @@ class GodotBridge extends EventEmitter {
25462
25539
  settled = true;
25463
25540
  this.httpServer = server;
25464
25541
  this.godotWss = godotWss;
25542
+ const bound = server.address();
25543
+ this.boundPort = typeof bound === "object" && bound !== null ? bound.port : port;
25465
25544
  this.log("info", `Editor bridge listening on ${this.host}:${this.port}`);
25466
25545
  resolve();
25467
25546
  });
@@ -25478,7 +25557,7 @@ class GodotBridge extends EventEmitter {
25478
25557
  godotWss.on("error", (error) => {
25479
25558
  this.log("error", `Godot WebSocket server error: ${error.message}`);
25480
25559
  });
25481
- server.listen(this.port, this.host);
25560
+ server.listen(port, this.host);
25482
25561
  });
25483
25562
  }
25484
25563
  async stop() {
@@ -25512,6 +25591,7 @@ class GodotBridge extends EventEmitter {
25512
25591
  this.httpServer = null;
25513
25592
  }
25514
25593
  await Promise.all(closeTasks);
25594
+ this.boundPort = null;
25515
25595
  this.connectionInfo = null;
25516
25596
  this.log("info", "WebSocket bridge stopped");
25517
25597
  }
@@ -25813,8 +25893,8 @@ class GodotBridge extends EventEmitter {
25813
25893
  }
25814
25894
  }
25815
25895
  var defaultBridge = null;
25816
- function getDefaultBridge() {
25817
- defaultBridge ??= new GodotBridge(resolveDefaultBridgePort(), resolveDefaultBridgeHost());
25896
+ function getDefaultBridge(mayMove = false) {
25897
+ defaultBridge ??= new GodotBridge(resolveDefaultBridgePort(), resolveDefaultBridgeHost(), DEFAULT_TIMEOUT_MS, mayMove);
25818
25898
  return defaultBridge;
25819
25899
  }
25820
25900
 
@@ -25827,7 +25907,7 @@ import { promisify } from "node:util";
25827
25907
  // src/detection.ts
25828
25908
  import { existsSync as existsSync3, readdirSync as readdirSync2, statSync } from "node:fs";
25829
25909
  import { homedir as homedir2 } from "node:os";
25830
- import { join as join3 } from "node:path";
25910
+ import { join as join4 } from "node:path";
25831
25911
  function resolveHomeDirectory() {
25832
25912
  try {
25833
25913
  return homedir2();
@@ -25851,7 +25931,7 @@ function scanDirectoryForGodotBinaries(directory, platform) {
25851
25931
  if (!pattern.test(name)) {
25852
25932
  continue;
25853
25933
  }
25854
- const fullPath = join3(directory, name);
25934
+ const fullPath = join4(directory, name);
25855
25935
  try {
25856
25936
  const stat = statSync(fullPath);
25857
25937
  if (stat.isFile()) {
@@ -25860,7 +25940,7 @@ function scanDirectoryForGodotBinaries(directory, platform) {
25860
25940
  } catch {}
25861
25941
  }
25862
25942
  matches.sort((a, b) => b.mtime - a.mtime);
25863
- return matches.map((m) => join3(directory, m.name));
25943
+ return matches.map((m) => join4(directory, m.name));
25864
25944
  }
25865
25945
  function conventionalPaths(platform, home) {
25866
25946
  const paths = ["godot"];
@@ -26003,9 +26083,9 @@ class GodotLocator {
26003
26083
 
26004
26084
  // src/headless.ts
26005
26085
  import { execFile as execFile2 } from "node:child_process";
26006
- import { mkdtempSync, rmSync, writeFileSync as writeFileSync2 } from "node:fs";
26086
+ import { mkdtempSync, rmSync as rmSync2, writeFileSync as writeFileSync3 } from "node:fs";
26007
26087
  import { tmpdir as tmpdir2 } from "node:os";
26008
- import { join as join4 } from "node:path";
26088
+ import { join as join5 } from "node:path";
26009
26089
  import { promisify as promisify2 } from "node:util";
26010
26090
  var run2 = promisify2(execFile2);
26011
26091
  function snakeCased(params) {
@@ -26046,9 +26126,9 @@ function reason(stdout, stderr) {
26046
26126
  return stdout.trim().split(/\r?\n/).at(-1) ?? "no output at all";
26047
26127
  }
26048
26128
  async function runOperation(engine, operation, params, projectPath) {
26049
- const paramsDir = mkdtempSync(join4(tmpdir2(), "gdharness-params-"));
26050
- const paramsFile = join4(paramsDir, `${operation}.json`);
26051
- writeFileSync2(paramsFile, JSON.stringify(snakeCased(params)), "utf8");
26129
+ const paramsDir = mkdtempSync(join5(tmpdir2(), "gdharness-params-"));
26130
+ const paramsFile = join5(paramsDir, `${operation}.json`);
26131
+ writeFileSync3(paramsFile, JSON.stringify(snakeCased(params)), "utf8");
26052
26132
  const args = [
26053
26133
  "--headless",
26054
26134
  "--path",
@@ -26078,7 +26158,7 @@ async function runOperation(engine, operation, params, projectPath) {
26078
26158
  messages: []
26079
26159
  };
26080
26160
  } finally {
26081
- rmSync(paramsDir, { recursive: true, force: true });
26161
+ rmSync2(paramsDir, { recursive: true, force: true });
26082
26162
  }
26083
26163
  const payload = lastJsonObject(stdout);
26084
26164
  if (payload === null) {
@@ -26373,7 +26453,7 @@ function parseJUnit(xml) {
26373
26453
  import { realpathSync } from "node:fs";
26374
26454
  import { readFile, realpath } from "node:fs/promises";
26375
26455
  import { createConnection as createConnection2 } from "node:net";
26376
- import { dirname, resolve as resolve2 } from "node:path";
26456
+ import { dirname as dirname2, resolve as resolve2 } from "node:path";
26377
26457
  import { fileURLToPath, pathToFileURL } from "node:url";
26378
26458
 
26379
26459
  // src/paths.ts
@@ -26551,7 +26631,7 @@ class GodotLSPClient {
26551
26631
  if (this.initialized) {
26552
26632
  return;
26553
26633
  }
26554
- const rootPath = this.rootPath ?? dirname(resolve2(filePath));
26634
+ const rootPath = this.rootPath ?? dirname2(resolve2(filePath));
26555
26635
  await this.initialize(rootPath);
26556
26636
  }
26557
26637
  async sendRequest(method, params) {
@@ -26919,7 +26999,7 @@ async function handleLSPTool(client, toolName, args) {
26919
26999
 
26920
27000
  // src/project-scan.ts
26921
27001
  import { readdirSync as readdirSync3, readFileSync as readFileSync4 } from "node:fs";
26922
- import { join as join5 } from "node:path";
27002
+ import { join as join6 } from "node:path";
26923
27003
  var SKIPPED = new Set([".git", ".godot", ".import", "node_modules"]);
26924
27004
  var ASSET_EXTENSIONS = new Set(["png", "jpg", "jpeg", "webp", "svg", "ttf", "otf", "wav", "mp3", "ogg"]);
26925
27005
  function projectStructure(projectPath) {
@@ -26930,7 +27010,7 @@ function projectStructure(projectPath) {
26930
27010
  continue;
26931
27011
  }
26932
27012
  if (entry.isDirectory()) {
26933
- visit(join5(directory, entry.name));
27013
+ visit(join6(directory, entry.name));
26934
27014
  } else if (entry.isFile()) {
26935
27015
  const extension = entry.name.split(".").pop()?.toLowerCase() ?? "";
26936
27016
  if (extension === "tscn") {
@@ -26972,7 +27052,7 @@ function searchProject(projectPath, options) {
26972
27052
  if (SKIPPED.has(entry.name)) {
26973
27053
  continue;
26974
27054
  }
26975
- const entryPath = join5(directory, entry.name);
27055
+ const entryPath = join6(directory, entry.name);
26976
27056
  if (entry.isDirectory()) {
26977
27057
  visit(entryPath);
26978
27058
  continue;
@@ -27226,7 +27306,7 @@ function setupResourceHandlers(mcp, getProjectPath) {
27226
27306
  import { existsSync as existsSync5, readdirSync as readdirSync4, readFileSync as readFileSync6, unlinkSync } from "node:fs";
27227
27307
  import { createConnection as createConnection3 } from "node:net";
27228
27308
  import { tmpdir as tmpdir3 } from "node:os";
27229
- import { join as join6, resolve as resolve4 } from "node:path";
27309
+ import { join as join7, resolve as resolve4 } from "node:path";
27230
27310
 
27231
27311
  // src/tool-args.ts
27232
27312
  function asParams(value) {
@@ -27288,13 +27368,13 @@ function runtimeDirectory(variables = process.env) {
27288
27368
  return explicit;
27289
27369
  }
27290
27370
  const perUser = envValue("XDG_RUNTIME_DIR", variables);
27291
- return join6(perUser ?? tmpdir3(), "gdharness");
27371
+ return join7(perUser ?? tmpdir3(), "gdharness");
27292
27372
  }
27293
27373
  function runtimeDirectories(variables = process.env) {
27294
27374
  const candidates = [runtimeDirectory(variables)];
27295
27375
  const fallbacks = process.platform === "win32" ? [envValue("SystemRoot", variables) ?? envValue("windir", variables) ?? "C:\\Windows"] : ["/tmp", "/var/tmp"];
27296
27376
  for (const base of fallbacks) {
27297
- candidates.push(join6(base, process.platform === "win32" ? "Temp" : "", "gdharness"));
27377
+ candidates.push(join7(base, process.platform === "win32" ? "Temp" : "", "gdharness"));
27298
27378
  }
27299
27379
  return [...new Set(candidates.map((path) => resolve4(path)))];
27300
27380
  }
@@ -27357,7 +27437,7 @@ function announcedIn(directory) {
27357
27437
  if (!match) {
27358
27438
  continue;
27359
27439
  }
27360
- const file = join6(directory, entry);
27440
+ const file = join7(directory, entry);
27361
27441
  const pid = Number.parseInt(match[1] ?? "", 10);
27362
27442
  const announced = processAlive(pid) ? parseAnnouncement(file, pid) : { kind: "rubbish" };
27363
27443
  if (announced.kind === "rubbish") {
@@ -27498,6 +27578,20 @@ function runtimeRequest(endpoint, command, params, timeoutMs) {
27498
27578
  });
27499
27579
  }
27500
27580
 
27581
+ // src/setup.ts
27582
+ import { cpSync, existsSync as existsSync6, mkdirSync as mkdirSync3, readFileSync as readFileSync7, rmSync as rmSync3, writeFileSync as writeFileSync4 } from "node:fs";
27583
+ import { dirname as dirname3, join as join8 } from "node:path";
27584
+ var ADDONS = ["gdharness_editor", "gdharness_runtime", "auto_reload"];
27585
+ var VERSION_MARKER = ".gdharness-version";
27586
+ function installedAddonVersion(projectPath) {
27587
+ const marker = join8(projectPath, "addons", ADDONS[0], VERSION_MARKER);
27588
+ if (!existsSync6(marker)) {
27589
+ return null;
27590
+ }
27591
+ const said = readFileSync7(marker, "utf8").trim();
27592
+ return said === "" ? null : said;
27593
+ }
27594
+
27501
27595
  // src/tool-definitions.ts
27502
27596
  var PROJECT_PATH = {
27503
27597
  type: "string",
@@ -28413,7 +28507,7 @@ function buildToolDefinitions() {
28413
28507
  var UPDATE_NOTICE_EVERY = 500;
28414
28508
  var FEEDBACK_NOTICE_EVERY = 250;
28415
28509
  var run3 = promisify3(execFile3);
28416
- var __dirname2 = dirname2(fileURLToPath2(import.meta.url));
28510
+ var __dirname2 = dirname4(fileURLToPath2(import.meta.url));
28417
28511
  var EDITOR_RESTART_TIMEOUT_MS = 90000;
28418
28512
  var BRIDGE_RETRY_MS = 2000;
28419
28513
  var PATH_SOLUTIONS = [
@@ -28491,11 +28585,11 @@ function realPathOr(path) {
28491
28585
  }
28492
28586
  }
28493
28587
  function hasMainScene(projectFile) {
28494
- const scene = parseProjectGodot(readFileSync7(projectFile, "utf8"))["application"]?.["run/main_scene"];
28588
+ const scene = parseProjectGodot(readFileSync8(projectFile, "utf8"))["application"]?.["run/main_scene"];
28495
28589
  return typeof scene === "string" && scene !== "";
28496
28590
  }
28497
28591
  function editorPlaysHeadless(projectFile) {
28498
- const runArgs = parseProjectGodot(readFileSync7(projectFile, "utf8"))["editor"]?.["run/main_run_args"];
28592
+ const runArgs = parseProjectGodot(readFileSync8(projectFile, "utf8"))["editor"]?.["run/main_run_args"];
28499
28593
  return typeof runArgs === "string" && /(?:^|\s)--headless(?:\s|$)/.test(runArgs);
28500
28594
  }
28501
28595
  function camelCased(params) {
@@ -28511,7 +28605,7 @@ function camelCased(params) {
28511
28605
  class GodotServer {
28512
28606
  mcp;
28513
28607
  locator = new GodotLocator;
28514
- operationsScript = join7(__dirname2, "godot", "operations", "godot_operations.gd");
28608
+ operationsScript = join9(__dirname2, "godot", "operations", "godot_operations.gd");
28515
28609
  godotBridge;
28516
28610
  tools = buildToolDefinitions();
28517
28611
  activeProcess = null;
@@ -28525,8 +28619,11 @@ class GodotServer {
28525
28619
  bridgeRetry = null;
28526
28620
  lastProjectPath = null;
28527
28621
  shutdownInitiated = false;
28622
+ ownProject;
28623
+ announcedAt = null;
28528
28624
  constructor() {
28529
- this.godotBridge = getDefaultBridge();
28625
+ this.ownProject = envValue("GDHARNESS_PROJECT") ?? null;
28626
+ this.godotBridge = getDefaultBridge(this.ownProject !== null);
28530
28627
  this.mcp = new McpServer({ name: "gdharness", version: SERVER_VERSION }, { capabilities: { tools: {}, resources: {} } });
28531
28628
  this.setupToolHandlers();
28532
28629
  setupResourceHandlers(this.mcp, () => this.lastProjectPath);
@@ -28574,6 +28671,7 @@ class GodotServer {
28574
28671
  this.bridgeStartupError = null;
28575
28672
  const bridgeStatus = this.godotBridge.getStatus();
28576
28673
  console.error(`[SERVER] Godot Editor Bridge started on ${bridgeStatus.host}:${bridgeStatus.port}`);
28674
+ this.announceTheBridge();
28577
28675
  } catch (bridgeError) {
28578
28676
  const code = bridgeError instanceof Error && "code" in bridgeError && typeof bridgeError.code === "string" ? bridgeError.code : null;
28579
28677
  const reason = errorMessage(bridgeError);
@@ -28606,6 +28704,18 @@ class GodotServer {
28606
28704
  this.stopTryingTheBridge();
28607
28705
  const bridgeStatus = this.godotBridge.getStatus();
28608
28706
  console.error(`[SERVER] Godot Editor Bridge came up on ${bridgeStatus.host}:${bridgeStatus.port}; editor tools are live.`);
28707
+ this.announceTheBridge();
28708
+ }
28709
+ announceTheBridge() {
28710
+ if (this.ownProject === null) {
28711
+ return;
28712
+ }
28713
+ const status = this.godotBridge.getStatus();
28714
+ this.announcedAt = announceBridge(this.ownProject, {
28715
+ host: status.host,
28716
+ port: status.port,
28717
+ version: SERVER_VERSION
28718
+ });
28609
28719
  }
28610
28720
  stopTryingTheBridge() {
28611
28721
  if (this.bridgeRetry !== null) {
@@ -28616,6 +28726,8 @@ class GodotServer {
28616
28726
  async cleanup() {
28617
28727
  this.logDebug("Cleaning up resources");
28618
28728
  this.stopTryingTheBridge();
28729
+ withdrawBridge(this.announcedAt);
28730
+ this.announcedAt = null;
28619
28731
  if (this.activeProcess) {
28620
28732
  this.activeProcess.process?.kill();
28621
28733
  this.activeProcess = null;
@@ -28731,28 +28843,44 @@ class GodotServer {
28731
28843
  if (this.callsSinceNotice < UPDATE_NOTICE_EVERY && this.noticedUpdate) {
28732
28844
  return answer;
28733
28845
  }
28846
+ const moved = this.projectHasMovedOn();
28847
+ if (moved !== null) {
28848
+ this.noticedUpdate = true;
28849
+ this.callsSinceNotice = 0;
28850
+ return this.saying(answer, {
28851
+ project_upgraded_under_this_server: {
28852
+ server_is: SERVER_VERSION,
28853
+ project_is: moved,
28854
+ what_to_do: "The project was upgraded while this server has been running, so it is still answering " + "as the version it was started as. Tell the user to reconnect the MCP server, which is " + "the only thing that replaces it: a server cannot restart itself."
28855
+ }
28856
+ });
28857
+ }
28734
28858
  const notice = this.updates.notice();
28735
28859
  if (notice === null) {
28736
28860
  return answer;
28737
28861
  }
28738
28862
  this.noticedUpdate = true;
28739
28863
  this.callsSinceNotice = 0;
28864
+ return this.saying(answer, {
28865
+ update_available: {
28866
+ ...notice,
28867
+ what_to_do: "Tell the user a newer gdharness is out, with what changed, and offer to take it. " + "Only run the upgrade command if they say yes: it restarts their editor and the " + "MCP server has to be reconnected afterwards."
28868
+ }
28869
+ });
28870
+ }
28871
+ saying(answer, block) {
28740
28872
  return {
28741
28873
  ...answer,
28742
- content: [
28743
- ...answer.content,
28744
- {
28745
- type: "text",
28746
- text: JSON.stringify({
28747
- update_available: {
28748
- ...notice,
28749
- what_to_do: "Tell the user a newer gdharness is out, with what changed, and offer to take it. " + "Only run the upgrade command if they say yes: it restarts their editor and the " + "MCP server has to be reconnected afterwards."
28750
- }
28751
- }, null, 2)
28752
- }
28753
- ]
28874
+ content: [...answer.content, { type: "text", text: JSON.stringify(block, null, 2) }]
28754
28875
  };
28755
28876
  }
28877
+ projectHasMovedOn() {
28878
+ if (this.ownProject === null) {
28879
+ return null;
28880
+ }
28881
+ const installed = installedAddonVersion(this.ownProject);
28882
+ return installed === null || installed === SERVER_VERSION ? null : installed;
28883
+ }
28756
28884
  withFeedbackNotice(answer) {
28757
28885
  this.callsSinceFeedback += 1;
28758
28886
  if (this.callsSinceFeedback < FEEDBACK_NOTICE_EVERY) {
@@ -28998,8 +29126,8 @@ class GodotServer {
28998
29126
  if (path === undefined) {
28999
29127
  return { ok: false, response: this.createErrorResponse("projectPath is required.") };
29000
29128
  }
29001
- const file = join7(path, "project.godot");
29002
- if (!existsSync6(file)) {
29129
+ const file = join9(path, "project.godot");
29130
+ if (!existsSync7(file)) {
29003
29131
  return {
29004
29132
  ok: false,
29005
29133
  response: this.createErrorResponse(`Not a Godot project: ${path}`, [
@@ -29144,7 +29272,7 @@ class GodotServer {
29144
29272
  if (!engine.ok) {
29145
29273
  return engine.response;
29146
29274
  }
29147
- const application = parseProjectGodot(readFileSync7(project.value.file, "utf8"))["application"] ?? {};
29275
+ const application = parseProjectGodot(readFileSync8(project.value.file, "utf8"))["application"] ?? {};
29148
29276
  const name = application["config/name"];
29149
29277
  const mainScene = application["run/main_scene"];
29150
29278
  const info = {
@@ -29233,7 +29361,7 @@ class GodotServer {
29233
29361
  log.finish();
29234
29362
  const problems = log.select({ severity: "warning", sinceLastCall: false, limit: 200 });
29235
29363
  const verdict = {
29236
- exported: exitCode === 0 && log.count("error") === 0 && existsSync6(output.absolutePath),
29364
+ exported: exitCode === 0 && log.count("error") === 0 && existsSync7(output.absolutePath),
29237
29365
  preset,
29238
29366
  outputPath: output.relativePath,
29239
29367
  debug,
@@ -29263,7 +29391,7 @@ class GodotServer {
29263
29391
  return contained.response;
29264
29392
  }
29265
29393
  const runner = "addons/gdUnit4/bin/GdUnitCmdTool.gd";
29266
- if (!existsSync6(join7(project.value.path, runner))) {
29394
+ if (!existsSync7(join9(project.value.path, runner))) {
29267
29395
  return this.createErrorResponse(`gdUnit4 is not installed in this project: no ${runner}.`, [
29268
29396
  "Install gdUnit4 under addons/gdUnit4, from https://github.com/godot-gdunit-labs/gdUnit4"
29269
29397
  ]);
@@ -29296,7 +29424,7 @@ class GodotServer {
29296
29424
  ];
29297
29425
  const timeoutMs = readPositiveNumber(args, "timeoutMs") ?? 600000;
29298
29426
  this.logDebug(`Running tests: ${engine.value} ${cmdArgs.join(" ")}`);
29299
- const userData = mkdtempSync2(join7(tmpdir4(), "gdharness-tests-"));
29427
+ const userData = mkdtempSync2(join9(tmpdir4(), "gdharness-tests-"));
29300
29428
  const run = this.spawnGame(engine.value, cmdArgs, userDataIn(userData));
29301
29429
  const hung = await new Promise((resolve) => {
29302
29430
  const timer = setTimeout(() => {
@@ -29312,20 +29440,20 @@ class GodotServer {
29312
29440
  resolve(false);
29313
29441
  });
29314
29442
  });
29315
- const reportsDir = join7(project.value.path, ".godot", "gdharness-reports");
29443
+ const reportsDir = join9(project.value.path, ".godot", "gdharness-reports");
29316
29444
  let report = null;
29317
29445
  let reportProblem = null;
29318
29446
  try {
29319
- const written = existsSync6(reportsDir) ? readdirSync5(reportsDir).filter((name) => name.startsWith("report_")).sort((a, b) => Number(a.slice("report_".length)) - Number(b.slice("report_".length))) : [];
29447
+ const written = existsSync7(reportsDir) ? readdirSync5(reportsDir).filter((name) => name.startsWith("report_")).sort((a, b) => Number(a.slice("report_".length)) - Number(b.slice("report_".length))) : [];
29320
29448
  const newest = written.at(-1);
29321
29449
  if (newest !== undefined) {
29322
- report = parseJUnit(readFileSync7(join7(reportsDir, newest, "results.xml"), "utf8"));
29450
+ report = parseJUnit(readFileSync8(join9(reportsDir, newest, "results.xml"), "utf8"));
29323
29451
  }
29324
29452
  } catch (error) {
29325
29453
  reportProblem = errorMessage(error);
29326
29454
  } finally {
29327
- rmSync2(reportsDir, { recursive: true, force: true });
29328
- rmSync2(userData, { recursive: true, force: true });
29455
+ rmSync4(reportsDir, { recursive: true, force: true });
29456
+ rmSync4(userData, { recursive: true, force: true });
29329
29457
  }
29330
29458
  const engineEntries = run.log.select({ severity: "warning", sinceLastCall: false, limit: 200 }).entries;
29331
29459
  const verdicts = {
@@ -29438,6 +29566,8 @@ class GodotServer {
29438
29566
  startupError: this.bridgeStartupError,
29439
29567
  staleNote: stale ? addonMismatch(status.addonVersion, SERVER_VERSION) : undefined,
29440
29568
  retryingBridge: this.bridgeRetry === null ? undefined : true,
29569
+ announcedAt: this.announcedAt ?? undefined,
29570
+ projectIs: this.projectHasMovedOn() ?? undefined,
29441
29571
  note: isPortConflict ? "Bridge port is already in use. Another gdharness instance owns the editor bridge, so this server cannot reach the editor. Usually the server this one replaced, still on its way out." : undefined,
29442
29572
  suggestion: isPortConflict ? `This server is asking for the port again every ${BRIDGE_RETRY_MS / 1000}s and takes it the moment the other one lets go, so editor tools come back on their own. Ask again, or end the older gdharness process to have it now.` : undefined
29443
29573
  };
@@ -29650,7 +29780,8 @@ class GodotServer {
29650
29780
  log,
29651
29781
  startedAt: Date.now(),
29652
29782
  exitCode: null,
29653
- throughEditor: true
29783
+ throughEditor: true,
29784
+ brokeOn: null
29654
29785
  };
29655
29786
  this.activeProcess = played;
29656
29787
  return this.jsonTextResponse({
@@ -29670,6 +29801,15 @@ class GodotServer {
29670
29801
  `) ? line : `${line}
29671
29802
  `);
29672
29803
  }
29804
+ this.recordWhatItBrokeOn(game);
29805
+ }
29806
+ recordWhatItBrokeOn(game) {
29807
+ const halt = this.dapClient?.whereItStopped() ?? null;
29808
+ if (halt?.reason !== "exception" || halt.text === "" || game.brokeOn === halt.text) {
29809
+ return;
29810
+ }
29811
+ game.brokeOn = halt.text;
29812
+ game.log.record("error", halt.text);
29673
29813
  }
29674
29814
  async endActiveGame() {
29675
29815
  const running = this.activeProcess;
@@ -29691,7 +29831,8 @@ class GodotServer {
29691
29831
  log,
29692
29832
  startedAt: Date.now(),
29693
29833
  exitCode: null,
29694
- throughEditor: false
29834
+ throughEditor: false,
29835
+ brokeOn: null
29695
29836
  };
29696
29837
  child.stdout.on("data", (data) => {
29697
29838
  log.append("stdout", data);
@@ -29756,6 +29897,7 @@ class GodotServer {
29756
29897
  contains: readNonEmptyString(args, "contains"),
29757
29898
  limit: readPositiveNumber(args, "limit") ?? 200
29758
29899
  });
29900
+ const halt = this.activeProcess.throughEditor ? this.dapClient?.whereItStopped() ?? null : null;
29759
29901
  return this.jsonTextResponse({
29760
29902
  running: this.activeProcess.exitCode === null,
29761
29903
  exitCode: this.activeProcess.exitCode,
@@ -29764,6 +29906,7 @@ class GodotServer {
29764
29906
  errors: this.activeProcess.log.count("error"),
29765
29907
  warnings: this.activeProcess.log.count("warning"),
29766
29908
  clean: this.activeProcess.log.count("error") === 0,
29909
+ heldAt: halt,
29767
29910
  omitted: selected.omitted,
29768
29911
  entries: selected.entries
29769
29912
  });
@@ -29833,8 +29976,8 @@ class GodotServer {
29833
29976
  return this.createErrorResponse(choice.problem);
29834
29977
  }
29835
29978
  const expectsScreenshot = command === "capture_screenshot" || command === "capture_viewport";
29836
- const screenshotDir = expectsScreenshot ? mkdtempSync2(join7(tmpdir4(), "gdharness-runtime-screenshot-")) : null;
29837
- const screenshotPath = screenshotDir ? join7(screenshotDir, "capture.png") : null;
29979
+ const screenshotDir = expectsScreenshot ? mkdtempSync2(join9(tmpdir4(), "gdharness-runtime-screenshot-")) : null;
29980
+ const screenshotPath = screenshotDir ? join9(screenshotDir, "capture.png") : null;
29838
29981
  try {
29839
29982
  const reply = await runtimeRequest(choice.endpoint, command, screenshotPath ? { ...params, output_path: screenshotPath } : params, timeoutMs);
29840
29983
  if (!reply.ok) {
@@ -29855,12 +29998,12 @@ class GodotServer {
29855
29998
  return {
29856
29999
  content: [
29857
30000
  { type: "text", text: `Screenshot captured: ${dimensions}` },
29858
- { type: "image", data: readFileSync7(screenshotPath).toString("base64"), mimeType: "image/png" }
30001
+ { type: "image", data: readFileSync8(screenshotPath).toString("base64"), mimeType: "image/png" }
29859
30002
  ]
29860
30003
  };
29861
30004
  } finally {
29862
30005
  if (screenshotDir) {
29863
- rmSync2(screenshotDir, { recursive: true, force: true });
30006
+ rmSync4(screenshotDir, { recursive: true, force: true });
29864
30007
  }
29865
30008
  }
29866
30009
  }