gdharness 0.9.0 → 0.9.2

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,7 +10126,7 @@ 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 existsSync7, mkdtempSync as mkdtempSync2, readdirSync as readdirSync5, readFileSync as readFileSync8, realpathSync as realpathSync2, rmSync as rmSync4 } from "node:fs";
10129
+ import { existsSync as existsSync7, mkdtempSync as mkdtempSync2, readdirSync as readdirSync5, readFileSync as readFileSync9, realpathSync as realpathSync2, rmSync as rmSync4 } from "node:fs";
10130
10130
  import { tmpdir as tmpdir4 } from "node:os";
10131
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";
@@ -24697,12 +24697,30 @@ class StdioServerTransport {
24697
24697
  }
24698
24698
 
24699
24699
  // src/bridge-announce.ts
24700
- import { mkdirSync as mkdirSync2, rmSync, writeFileSync as writeFileSync2 } from "node:fs";
24700
+ import { mkdirSync as mkdirSync2, readFileSync as readFileSync3, rmSync, writeFileSync as writeFileSync2 } from "node:fs";
24701
24701
  import { dirname, join as join2 } from "node:path";
24702
24702
  function announcementPath(projectPath) {
24703
24703
  return join2(projectPath, ".godot", "gdharness-bridge.json");
24704
24704
  }
24705
24705
  var BRIDGE_ANNOUNCE_PROTOCOL = 1;
24706
+ function readAnnouncement(path) {
24707
+ try {
24708
+ const parsed = JSON.parse(readFileSync3(path, "utf8"));
24709
+ if (typeof parsed !== "object" || parsed === null) {
24710
+ return null;
24711
+ }
24712
+ const said = parsed;
24713
+ if (typeof said.port !== "number" || typeof said.pid !== "number") {
24714
+ return null;
24715
+ }
24716
+ return said;
24717
+ } catch {
24718
+ return null;
24719
+ }
24720
+ }
24721
+ function announcementIsOurs(path) {
24722
+ return readAnnouncement(path)?.pid === process.pid;
24723
+ }
24706
24724
  function announceBridge(projectPath, bridge) {
24707
24725
  const path = announcementPath(projectPath);
24708
24726
  try {
@@ -24724,7 +24742,7 @@ function announceBridge(projectPath, bridge) {
24724
24742
  }
24725
24743
  }
24726
24744
  function withdrawBridge(path) {
24727
- if (path === null) {
24745
+ if (path === null || !announcementIsOurs(path)) {
24728
24746
  return;
24729
24747
  }
24730
24748
  try {
@@ -24735,7 +24753,7 @@ function withdrawBridge(path) {
24735
24753
  }
24736
24754
 
24737
24755
  // src/class-cache.ts
24738
- import { existsSync as existsSync2, readdirSync, readFileSync as readFileSync3 } from "node:fs";
24756
+ import { existsSync as existsSync2, readdirSync, readFileSync as readFileSync4 } from "node:fs";
24739
24757
  import { join as join3 } from "node:path";
24740
24758
  function declaredClasses(projectPath) {
24741
24759
  const declared = new Map;
@@ -24748,7 +24766,7 @@ function declaredClasses(projectPath) {
24748
24766
  if (entry.isDirectory()) {
24749
24767
  visit(path, `${prefix}${entry.name}/`);
24750
24768
  } else if (entry.isFile() && entry.name.endsWith(".gd")) {
24751
- const found = /^class_name\s+([A-Za-z_][A-Za-z0-9_]*)/m.exec(readFileSync3(path, "utf8"));
24769
+ const found = /^class_name\s+([A-Za-z_][A-Za-z0-9_]*)/m.exec(readFileSync4(path, "utf8"));
24752
24770
  if (found?.[1]) {
24753
24771
  declared.set(found[1], `res://${prefix}${entry.name}`);
24754
24772
  }
@@ -24764,7 +24782,7 @@ function cachedClasses(projectPath) {
24764
24782
  return null;
24765
24783
  }
24766
24784
  const listed = new Map;
24767
- const text = readFileSync3(cache, "utf8");
24785
+ const text = readFileSync4(cache, "utf8");
24768
24786
  for (const entry of text.matchAll(/"class":\s*&"([^"]+)"[\s\S]*?"path":\s*"([^"]+)"/g)) {
24769
24787
  listed.set(entry[1] ?? "", entry[2] ?? "");
24770
24788
  }
@@ -25494,12 +25512,64 @@ var import_subprotocol = __toESM(require_subprotocol(), 1);
25494
25512
  var import_websocket = __toESM(require_websocket(), 1);
25495
25513
  var import_websocket_server = __toESM(require_websocket_server(), 1);
25496
25514
 
25515
+ // src/paths.ts
25516
+ import { isAbsolute, relative, resolve, sep } from "node:path";
25517
+ var RESOURCE_SCHEME = "res://";
25518
+ var OTHER_SCHEME = /^[a-zA-Z][a-zA-Z\d+.-]+:\/\//;
25519
+ function isSameDirectory(onePath, otherPath) {
25520
+ return relative(resolve(onePath), resolve(otherPath)) === "";
25521
+ }
25522
+ function isWithinRoot(rootPath, candidatePath) {
25523
+ const step = relative(resolve(rootPath), resolve(candidatePath));
25524
+ return step !== ".." && !step.startsWith(`..${sep}`) && !isAbsolute(step);
25525
+ }
25526
+ function resolveWithinProject(projectPath, candidatePath) {
25527
+ const trimmed = candidatePath.trim();
25528
+ if (trimmed === "") {
25529
+ return { ok: false, reason: "Path is empty." };
25530
+ }
25531
+ if (trimmed.includes("\x00")) {
25532
+ return { ok: false, reason: "Path contains a null byte." };
25533
+ }
25534
+ const withoutScheme = trimmed.startsWith(RESOURCE_SCHEME) ? trimmed.slice(RESOURCE_SCHEME.length) : trimmed;
25535
+ const scheme = OTHER_SCHEME.exec(withoutScheme);
25536
+ if (scheme) {
25537
+ return {
25538
+ ok: false,
25539
+ reason: `Path '${trimmed}' uses the '${scheme[0]}' scheme, which names somewhere other than this project.`
25540
+ };
25541
+ }
25542
+ if (withoutScheme.startsWith("/") || withoutScheme.startsWith("\\") || isAbsolute(withoutScheme)) {
25543
+ return {
25544
+ ok: false,
25545
+ reason: `Path '${trimmed}' is absolute; give a path inside the project, such as 'scenes/main.tscn'.`
25546
+ };
25547
+ }
25548
+ if (withoutScheme.includes(":")) {
25549
+ return {
25550
+ ok: false,
25551
+ reason: `Path '${trimmed}' contains ':', which no file inside a project is named with.`
25552
+ };
25553
+ }
25554
+ const root = resolve(projectPath);
25555
+ const absolutePath = resolve(root, withoutScheme);
25556
+ if (!isWithinRoot(root, absolutePath)) {
25557
+ return { ok: false, reason: `Path '${trimmed}' resolves outside the project directory.` };
25558
+ }
25559
+ const relativePath = relative(root, absolutePath).split(sep).join("/");
25560
+ if (relativePath === "") {
25561
+ return { ok: false, reason: `Path '${trimmed}' is the project directory itself, not a file in it.` };
25562
+ }
25563
+ return { ok: true, absolutePath, relativePath };
25564
+ }
25565
+
25497
25566
  // src/godot-bridge.ts
25498
25567
  var DEFAULT_PORT = 6505;
25499
25568
  var DEFAULT_HOST = "127.0.0.1";
25500
25569
  var DEFAULT_TIMEOUT_MS = 30000;
25501
25570
  var KEEPALIVE_INTERVAL_MS = 1e4;
25502
25571
  var SECOND_CONNECTION_CLOSE_CODE = 4000;
25572
+ var OTHER_PROJECT_CLOSE_CODE = 4001;
25503
25573
  function resolveDefaultBridgePort() {
25504
25574
  return portFromEnv("GDHARNESS_BRIDGE_PORT", DEFAULT_PORT);
25505
25575
  }
@@ -25523,13 +25593,16 @@ class GodotBridge extends EventEmitter {
25523
25593
  boundPort = null;
25524
25594
  host;
25525
25595
  timeoutMs;
25526
- mayMove;
25527
- constructor(port = DEFAULT_PORT, host = DEFAULT_HOST, timeoutMs = DEFAULT_TIMEOUT_MS, mayMove = false) {
25596
+ ownProject;
25597
+ constructor(port = DEFAULT_PORT, host = DEFAULT_HOST, timeoutMs = DEFAULT_TIMEOUT_MS, ownProject = null) {
25528
25598
  super();
25529
25599
  this.wantedPort = port;
25530
25600
  this.host = host;
25531
25601
  this.timeoutMs = timeoutMs;
25532
- this.mayMove = mayMove;
25602
+ this.ownProject = ownProject;
25603
+ }
25604
+ get mayMove() {
25605
+ return this.ownProject !== null;
25533
25606
  }
25534
25607
  get port() {
25535
25608
  return this.boundPort ?? this.wantedPort;
@@ -25775,6 +25848,10 @@ class GodotBridge extends EventEmitter {
25775
25848
  }
25776
25849
  case "godot_ready":
25777
25850
  if (this.connectionInfo) {
25851
+ if (this.belongsElsewhere(message.project_path)) {
25852
+ this.sendElsewhere(message.project_path);
25853
+ return;
25854
+ }
25778
25855
  this.connectionInfo.projectPath = message.project_path;
25779
25856
  this.connectionInfo.addonVersion = message.addon_version ?? "";
25780
25857
  this.connectionInfo.editorPid = message.editor_pid;
@@ -25857,6 +25934,21 @@ class GodotBridge extends EventEmitter {
25857
25934
  clearInterval(this.pingInterval);
25858
25935
  this.pingInterval = null;
25859
25936
  }
25937
+ belongsElsewhere(theirProject) {
25938
+ if (this.ownProject === null || theirProject.trim() === "") {
25939
+ return false;
25940
+ }
25941
+ return !isSameDirectory(this.ownProject, theirProject);
25942
+ }
25943
+ sendElsewhere(theirProject) {
25944
+ const said = `this server serves ${this.ownProject ?? ""}, and that editor has ${theirProject} open`;
25945
+ this.log("warn", `Turning away an editor from another project: ${said}`);
25946
+ const stranger = this.socket;
25947
+ this.socket = null;
25948
+ this.connectionInfo = null;
25949
+ this.stopKeepalive();
25950
+ stranger?.close(OTHER_PROJECT_CLOSE_CODE, said);
25951
+ }
25860
25952
  handleDisconnect(disconnectedSocket, reason) {
25861
25953
  if (disconnectedSocket && this.socket && disconnectedSocket !== this.socket) {
25862
25954
  this.log("debug", "Ignoring stale Godot socket disconnect event");
@@ -25933,8 +26025,8 @@ class GodotBridge extends EventEmitter {
25933
26025
  }
25934
26026
  }
25935
26027
  var defaultBridge = null;
25936
- function getDefaultBridge(mayMove = false) {
25937
- defaultBridge ??= new GodotBridge(resolveDefaultBridgePort(), resolveDefaultBridgeHost(), DEFAULT_TIMEOUT_MS, mayMove);
26028
+ function getDefaultBridge(ownProject = null) {
26029
+ defaultBridge ??= new GodotBridge(resolveDefaultBridgePort(), resolveDefaultBridgeHost(), DEFAULT_TIMEOUT_MS, ownProject);
25938
26030
  return defaultBridge;
25939
26031
  }
25940
26032
 
@@ -26495,56 +26587,6 @@ import { readFile, realpath } from "node:fs/promises";
26495
26587
  import { createConnection as createConnection2 } from "node:net";
26496
26588
  import { dirname as dirname2, resolve as resolve2 } from "node:path";
26497
26589
  import { fileURLToPath, pathToFileURL } from "node:url";
26498
-
26499
- // src/paths.ts
26500
- import { isAbsolute, relative, resolve, sep } from "node:path";
26501
- var RESOURCE_SCHEME = "res://";
26502
- var OTHER_SCHEME = /^[a-zA-Z][a-zA-Z\d+.-]+:\/\//;
26503
- function isWithinRoot(rootPath, candidatePath) {
26504
- const step = relative(resolve(rootPath), resolve(candidatePath));
26505
- return step !== ".." && !step.startsWith(`..${sep}`) && !isAbsolute(step);
26506
- }
26507
- function resolveWithinProject(projectPath, candidatePath) {
26508
- const trimmed = candidatePath.trim();
26509
- if (trimmed === "") {
26510
- return { ok: false, reason: "Path is empty." };
26511
- }
26512
- if (trimmed.includes("\x00")) {
26513
- return { ok: false, reason: "Path contains a null byte." };
26514
- }
26515
- const withoutScheme = trimmed.startsWith(RESOURCE_SCHEME) ? trimmed.slice(RESOURCE_SCHEME.length) : trimmed;
26516
- const scheme = OTHER_SCHEME.exec(withoutScheme);
26517
- if (scheme) {
26518
- return {
26519
- ok: false,
26520
- reason: `Path '${trimmed}' uses the '${scheme[0]}' scheme, which names somewhere other than this project.`
26521
- };
26522
- }
26523
- if (withoutScheme.startsWith("/") || withoutScheme.startsWith("\\") || isAbsolute(withoutScheme)) {
26524
- return {
26525
- ok: false,
26526
- reason: `Path '${trimmed}' is absolute; give a path inside the project, such as 'scenes/main.tscn'.`
26527
- };
26528
- }
26529
- if (withoutScheme.includes(":")) {
26530
- return {
26531
- ok: false,
26532
- reason: `Path '${trimmed}' contains ':', which no file inside a project is named with.`
26533
- };
26534
- }
26535
- const root = resolve(projectPath);
26536
- const absolutePath = resolve(root, withoutScheme);
26537
- if (!isWithinRoot(root, absolutePath)) {
26538
- return { ok: false, reason: `Path '${trimmed}' resolves outside the project directory.` };
26539
- }
26540
- const relativePath = relative(root, absolutePath).split(sep).join("/");
26541
- if (relativePath === "") {
26542
- return { ok: false, reason: `Path '${trimmed}' is the project directory itself, not a file in it.` };
26543
- }
26544
- return { ok: true, absolutePath, relativePath };
26545
- }
26546
-
26547
- // src/lsp_client.ts
26548
26590
  var DEFAULT_LSP_PORT = 6005;
26549
26591
  var DIAGNOSTICS_TIMEOUT_MS = 5000;
26550
26592
  function diagnosticsKey(uri) {
@@ -27038,7 +27080,7 @@ async function handleLSPTool(client, toolName, args) {
27038
27080
  }
27039
27081
 
27040
27082
  // src/project-scan.ts
27041
- import { readdirSync as readdirSync3, readFileSync as readFileSync4 } from "node:fs";
27083
+ import { readdirSync as readdirSync3, readFileSync as readFileSync5 } from "node:fs";
27042
27084
  import { join as join6 } from "node:path";
27043
27085
  var SKIPPED = new Set([".git", ".godot", ".import", "node_modules"]);
27044
27086
  var ASSET_EXTENSIONS = new Set(["png", "jpg", "jpeg", "webp", "svg", "ttf", "otf", "wav", "mp3", "ogg"]);
@@ -27103,7 +27145,7 @@ function searchProject(projectPath, options) {
27103
27145
  }
27104
27146
  result.summary.files_searched += 1;
27105
27147
  const matches = [];
27106
- for (const [index, line] of readFileSync4(entryPath, "utf8").split(`
27148
+ for (const [index, line] of readFileSync5(entryPath, "utf8").split(`
27107
27149
  `).entries()) {
27108
27150
  if (full()) {
27109
27151
  break;
@@ -27126,7 +27168,7 @@ function searchProject(projectPath, options) {
27126
27168
  }
27127
27169
 
27128
27170
  // src/resources.ts
27129
- import { readFileSync as readFileSync5 } from "node:fs";
27171
+ import { readFileSync as readFileSync6 } from "node:fs";
27130
27172
  import { extname, resolve as resolve3 } from "node:path";
27131
27173
  var STATIC_RESOURCES = [
27132
27174
  {
@@ -27297,7 +27339,7 @@ function readResourceText(uri, getProjectPath) {
27297
27339
  const parsedUri = parseGodotUri(uri);
27298
27340
  if (parsedUri.kind === "project-info") {
27299
27341
  const projectFilePath = resolveProjectFile(projectPath, "project.godot");
27300
- const rawProject = readFileSync5(projectFilePath, "utf-8");
27342
+ const rawProject = readFileSync6(projectFilePath, "utf-8");
27301
27343
  const parsedProject = parseProjectGodot(rawProject);
27302
27344
  return {
27303
27345
  mimeType: "application/json",
@@ -27306,7 +27348,7 @@ function readResourceText(uri, getProjectPath) {
27306
27348
  }
27307
27349
  const filePath = resolveProjectFile(projectPath, parsedUri.resourcePath);
27308
27350
  ensureAllowedExtension(parsedUri.kind, filePath);
27309
- const text = readFileSync5(filePath, "utf-8");
27351
+ const text = readFileSync6(filePath, "utf-8");
27310
27352
  return {
27311
27353
  mimeType: parsedUri.kind === "script" ? "text/x-gdscript" : "text/plain",
27312
27354
  text
@@ -27343,7 +27385,7 @@ function setupResourceHandlers(mcp, getProjectPath) {
27343
27385
  }
27344
27386
 
27345
27387
  // src/runtime-client.ts
27346
- import { existsSync as existsSync5, readdirSync as readdirSync4, readFileSync as readFileSync6, unlinkSync } from "node:fs";
27388
+ import { existsSync as existsSync5, readdirSync as readdirSync4, readFileSync as readFileSync7, unlinkSync } from "node:fs";
27347
27389
  import { createConnection as createConnection3 } from "node:net";
27348
27390
  import { tmpdir as tmpdir3 } from "node:os";
27349
27391
  import { join as join7, resolve as resolve4 } from "node:path";
@@ -27429,7 +27471,7 @@ function processAlive(pid) {
27429
27471
  function parseAnnouncement(file, pid) {
27430
27472
  let fields;
27431
27473
  try {
27432
- fields = asParams(JSON.parse(readFileSync6(file, "utf8")));
27474
+ fields = asParams(JSON.parse(readFileSync7(file, "utf8")));
27433
27475
  } catch {
27434
27476
  return { kind: "rubbish" };
27435
27477
  }
@@ -27619,7 +27661,7 @@ function runtimeRequest(endpoint, command, params, timeoutMs) {
27619
27661
  }
27620
27662
 
27621
27663
  // src/setup.ts
27622
- import { cpSync, existsSync as existsSync6, mkdirSync as mkdirSync3, readFileSync as readFileSync7, rmSync as rmSync3, writeFileSync as writeFileSync4 } from "node:fs";
27664
+ import { cpSync, existsSync as existsSync6, mkdirSync as mkdirSync3, readFileSync as readFileSync8, rmSync as rmSync3, writeFileSync as writeFileSync4 } from "node:fs";
27623
27665
  import { dirname as dirname3, join as join8 } from "node:path";
27624
27666
  var ADDONS = ["gdharness_editor", "gdharness_runtime", "auto_reload"];
27625
27667
  var VERSION_MARKER = ".gdharness-version";
@@ -27628,7 +27670,7 @@ function installedAddonVersion(projectPath) {
27628
27670
  if (!existsSync6(marker)) {
27629
27671
  return null;
27630
27672
  }
27631
- const said = readFileSync7(marker, "utf8").trim();
27673
+ const said = readFileSync8(marker, "utf8").trim();
27632
27674
  return said === "" ? null : said;
27633
27675
  }
27634
27676
 
@@ -27811,7 +27853,8 @@ var TOOL_SPECS = [
27811
27853
  projectPath: PROJECT_PATH,
27812
27854
  setting: { type: "string", description: 'Setting path, such as "display/window/size/viewport_width".' },
27813
27855
  value: {
27814
- description: 'The value to write. Engine types may be tagged, {"_type": "Vector2", "x": 1, "y": 2}.'
27856
+ blank: true,
27857
+ description: 'The value to write. Engine types may be tagged, {"_type": "Vector2", "x": 1, "y": 2}. "" writes an empty string.'
27815
27858
  },
27816
27859
  name: { type: "string", description: "Autoload name." },
27817
27860
  path: { type: "string", description: "Autoload script or scene inside the project." },
@@ -28316,7 +28359,7 @@ var TOOL_SPECS = [
28316
28359
  property: {
28317
28360
  type: "string",
28318
28361
  ops: ["property", "find"],
28319
- description: "property: which one to read. find: read this one off every node matched, so a panel of labels is one call rather than one per label."
28362
+ description: 'property: which one to read. find: read this one off every node matched, so a panel of labels is one call rather than one per label. Colons read through the objects a node holds, "_game:clock:speed", which is where a game keeps what is worth asking about; a step that is not there is named.'
28320
28363
  },
28321
28364
  depth: { type: "number", ops: ["tree"], description: "tree: levels to descend. Default 3." },
28322
28365
  includeProperties: {
@@ -28333,7 +28376,7 @@ var TOOL_SPECS = [
28333
28376
  namePattern: {
28334
28377
  type: "string",
28335
28378
  ops: ["find"],
28336
- description: 'find: a case-insensitive glob on the node name, such as "Enemy*".'
28379
+ description: 'find: a case-insensitive glob on the node name, such as "Enemy*". Matched against the whole name, so a bare word finds only a node called exactly that; an answer of none says how many names contain it and what glob would have found them.'
28337
28380
  },
28338
28381
  group: { type: "string", ops: ["find"], description: "find: a group the node is in." },
28339
28382
  says: {
@@ -28374,7 +28417,7 @@ var TOOL_SPECS = [
28374
28417
  requires: ["nodePath"]
28375
28418
  },
28376
28419
  property: {
28377
- summary: "what one property reads on a node, refusing a property the node does not have",
28420
+ summary: "what one property reads on a node, or through the objects it holds, refusing a name nothing along the way has",
28378
28421
  requires: ["nodePath", "property"]
28379
28422
  },
28380
28423
  metrics: { summary: "frame time, memory, draw calls and the rest", requires: [] }
@@ -28387,9 +28430,21 @@ var TOOL_SPECS = [
28387
28430
  parameters: {
28388
28431
  projectPath: RUNNING_PROJECT_PATH,
28389
28432
  nodePath: { type: "string", description: 'Absolute node path, such as "/root/Main/Player".' },
28390
- property: { type: "string", ops: ["set"] },
28391
- value: { ops: ["set"], description: "set: the value, fitted to the property's type." },
28392
- method: { type: "string", ops: ["call"] },
28433
+ property: {
28434
+ type: "string",
28435
+ ops: ["set"],
28436
+ description: 'set: which one to write. Colons write through the objects a node holds, "_game:run:day", and the answer reads back off the same holder, so a write a typed container refused shows as an unchanged value.'
28437
+ },
28438
+ value: {
28439
+ ops: ["set"],
28440
+ blank: true,
28441
+ description: `set: the value, fitted to the property's type. "" writes an empty string.`
28442
+ },
28443
+ method: {
28444
+ type: "string",
28445
+ ops: ["call"],
28446
+ description: 'call: which one to call. Colons call through the objects a node holds, "_game:run:advance", the same way a property is written through them.'
28447
+ },
28393
28448
  args: {
28394
28449
  type: "array",
28395
28450
  ops: ["call"],
@@ -28398,8 +28453,14 @@ var TOOL_SPECS = [
28398
28453
  },
28399
28454
  requires: ["nodePath"],
28400
28455
  operations: {
28401
- set: { summary: "set a property", requires: ["property", "value"] },
28402
- call: { summary: "call a method and return its result", requires: ["method"] }
28456
+ set: {
28457
+ summary: "set a property, on a node or on an object it holds",
28458
+ requires: ["property", "value"]
28459
+ },
28460
+ call: {
28461
+ summary: "call a method, on a node or on an object it holds, and return its result",
28462
+ requires: ["method"]
28463
+ }
28403
28464
  }
28404
28465
  },
28405
28466
  {
@@ -28451,7 +28512,8 @@ var TOOL_SPECS = [
28451
28512
  text: {
28452
28513
  type: "string",
28453
28514
  ops: ["text", "choose"],
28454
- description: "text: what to type. A newline is Enter and a tab is Tab. choose: the item to take, by what it says."
28515
+ blank: true,
28516
+ description: 'text: what to type. A newline is Enter and a tab is Tab, a space is a space, and "" with replace empties the field. choose: the item to take, by what it says.'
28455
28517
  },
28456
28518
  replace: {
28457
28519
  type: "boolean",
@@ -28625,7 +28687,7 @@ function buildToolDefinitions() {
28625
28687
  };
28626
28688
  }
28627
28689
  for (const [name, schema] of Object.entries(spec.parameters)) {
28628
- const { ops: _ops, ...carried } = schema;
28690
+ const { ops: _ops, blank: _blank, ...carried } = schema;
28629
28691
  properties[name] = carried;
28630
28692
  }
28631
28693
  const required = [...spec.requires];
@@ -28701,6 +28763,7 @@ function wrongTypes(spec, args) {
28701
28763
  return complaints;
28702
28764
  }
28703
28765
  var BRIDGE_RETRY_MS = 2000;
28766
+ var SUCCESSOR_CHECK_MS = 1e4;
28704
28767
  var PATH_SOLUTIONS = [
28705
28768
  'Give the path relative to the project, such as "scenes/main.tscn" or "res://scenes/main.tscn"',
28706
28769
  "Point projectPath at the project the file belongs to"
@@ -28788,11 +28851,11 @@ function aboveTheRunner(entry) {
28788
28851
  };
28789
28852
  }
28790
28853
  function hasMainScene(projectFile) {
28791
- const scene = parseProjectGodot(readFileSync8(projectFile, "utf8"))["application"]?.["run/main_scene"];
28854
+ const scene = parseProjectGodot(readFileSync9(projectFile, "utf8"))["application"]?.["run/main_scene"];
28792
28855
  return typeof scene === "string" && scene !== "";
28793
28856
  }
28794
28857
  function editorPlaysHeadless(projectFile) {
28795
- const runArgs = parseProjectGodot(readFileSync8(projectFile, "utf8"))["editor"]?.["run/main_run_args"];
28858
+ const runArgs = parseProjectGodot(readFileSync9(projectFile, "utf8"))["editor"]?.["run/main_run_args"];
28796
28859
  return typeof runArgs === "string" && /(?:^|\s)--headless(?:\s|$)/.test(runArgs);
28797
28860
  }
28798
28861
  function camelCased(params) {
@@ -28820,13 +28883,14 @@ class GodotServer {
28820
28883
  dapClient = null;
28821
28884
  bridgeStartupError = null;
28822
28885
  bridgeRetry = null;
28886
+ successorWatch = null;
28823
28887
  lastProjectPath = null;
28824
28888
  shutdownInitiated = false;
28825
28889
  ownProject;
28826
28890
  announcedAt = null;
28827
28891
  constructor() {
28828
28892
  this.ownProject = envValue("GDHARNESS_PROJECT") ?? null;
28829
- this.godotBridge = getDefaultBridge(this.ownProject !== null);
28893
+ this.godotBridge = getDefaultBridge(this.ownProject);
28830
28894
  this.mcp = new McpServer({ name: "gdharness", version: SERVER_VERSION }, { capabilities: { tools: {}, resources: {} } });
28831
28895
  this.setupToolHandlers();
28832
28896
  setupResourceHandlers(this.mcp, () => this.lastProjectPath);
@@ -28919,6 +28983,7 @@ class GodotServer {
28919
28983
  port: status.port,
28920
28984
  version: SERVER_VERSION
28921
28985
  });
28986
+ this.watchForASuccessor();
28922
28987
  }
28923
28988
  stopTryingTheBridge() {
28924
28989
  if (this.bridgeRetry !== null) {
@@ -28926,9 +28991,46 @@ class GodotServer {
28926
28991
  this.bridgeRetry = null;
28927
28992
  }
28928
28993
  }
28994
+ watchForASuccessor() {
28995
+ if (this.ownProject === null || this.successorWatch !== null) {
28996
+ return;
28997
+ }
28998
+ const watch = setInterval(() => {
28999
+ const successor = this.supersededBy();
29000
+ if (successor === null) {
29001
+ return;
29002
+ }
29003
+ console.error(`[SERVER] pid ${successor} now serves ${this.ownProject}; this server is finished and is stopping.`);
29004
+ this.handleShutdown(`superseded by ${successor}`, 0);
29005
+ }, SUCCESSOR_CHECK_MS);
29006
+ watch.unref();
29007
+ this.successorWatch = watch;
29008
+ }
29009
+ supersededBy() {
29010
+ if (this.ownProject === null) {
29011
+ return null;
29012
+ }
29013
+ const announced = readAnnouncement(announcementPath(this.ownProject));
29014
+ if (announced === null || announced.pid === process.pid) {
29015
+ return null;
29016
+ }
29017
+ try {
29018
+ process.kill(announced.pid, 0);
29019
+ return announced.pid;
29020
+ } catch {
29021
+ return null;
29022
+ }
29023
+ }
29024
+ stopWatchingForASuccessor() {
29025
+ if (this.successorWatch !== null) {
29026
+ clearInterval(this.successorWatch);
29027
+ this.successorWatch = null;
29028
+ }
29029
+ }
28929
29030
  async cleanup() {
28930
29031
  this.logDebug("Cleaning up resources");
28931
29032
  this.stopTryingTheBridge();
29033
+ this.stopWatchingForASuccessor();
28932
29034
  withdrawBridge(this.announcedAt);
28933
29035
  this.announcedAt = null;
28934
29036
  if (this.activeProcess) {
@@ -29141,7 +29243,11 @@ class GodotServer {
29141
29243
  const required = [...spec.requires, ...op !== null ? spec.operations?.[op]?.requires ?? [] : []];
29142
29244
  const missing = required.filter((field) => {
29143
29245
  const value = Object.hasOwn(args, field) ? args[field] : undefined;
29144
- return value === undefined || value === null || typeof value === "string" && value.trim() === "";
29246
+ if (value === undefined || value === null) {
29247
+ return true;
29248
+ }
29249
+ const blank = typeof value === "string" && value.trim() === "";
29250
+ return blank && spec.parameters[field]?.blank !== true;
29145
29251
  });
29146
29252
  if (missing.length > 0) {
29147
29253
  const where = op !== null ? `${spec.name} ${op}` : spec.name;
@@ -29506,7 +29612,7 @@ class GodotServer {
29506
29612
  if (!engine.ok) {
29507
29613
  return engine.response;
29508
29614
  }
29509
- const application = parseProjectGodot(readFileSync8(project.value.file, "utf8"))["application"] ?? {};
29615
+ const application = parseProjectGodot(readFileSync9(project.value.file, "utf8"))["application"] ?? {};
29510
29616
  const name = application["config/name"];
29511
29617
  const mainScene = application["run/main_scene"];
29512
29618
  const info = {
@@ -29681,7 +29787,7 @@ class GodotServer {
29681
29787
  const written = existsSync7(reportsDir) ? readdirSync5(reportsDir).filter((name) => name.startsWith("report_")).sort((a, b) => Number(a.slice("report_".length)) - Number(b.slice("report_".length))) : [];
29682
29788
  const newest = written.at(-1);
29683
29789
  if (newest !== undefined) {
29684
- report = parseJUnit(readFileSync8(join9(reportsDir, newest, "results.xml"), "utf8"));
29790
+ report = parseJUnit(readFileSync9(join9(reportsDir, newest, "results.xml"), "utf8"));
29685
29791
  }
29686
29792
  } catch (error) {
29687
29793
  reportProblem = errorMessage(error);
@@ -30271,7 +30377,7 @@ class GodotServer {
30271
30377
  return {
30272
30378
  content: [
30273
30379
  { type: "text", text: `Screenshot captured: ${dimensions}` },
30274
- { type: "image", data: readFileSync8(screenshotPath).toString("base64"), mimeType: "image/png" }
30380
+ { type: "image", data: readFileSync9(screenshotPath).toString("base64"), mimeType: "image/png" }
30275
30381
  ]
30276
30382
  };
30277
30383
  } finally {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gdharness",
3
- "version": "0.9.0",
3
+ "version": "0.9.2",
4
4
  "mcpName": "io.github.Aureliolo/gdharness",
5
5
  "description": "A harness for driving a Godot 4 project from an agent: editor addons, a runtime bridge into the running game, an MCP server in front of them, and a CLI that installs and diagnoses the Godot side.",
6
6
  "type": "module",