gdharness 0.13.2 → 0.13.4

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/cli.js CHANGED
@@ -7493,7 +7493,7 @@ var init_server_version = __esm(() => {
7493
7493
  });
7494
7494
 
7495
7495
  // src/class-cache.ts
7496
- import { existsSync as existsSync6, readdirSync as readdirSync2, readFileSync as readFileSync5 } from "node:fs";
7496
+ import { existsSync as existsSync6, readdirSync as readdirSync2, readFileSync as readFileSync5, statSync as statSync2 } from "node:fs";
7497
7497
  import { join as join6 } from "node:path";
7498
7498
  function declaredClasses(projectPath) {
7499
7499
  const declared = new Map;
@@ -7531,6 +7531,14 @@ function cachedClasses(projectPath) {
7531
7531
  }
7532
7532
  return listed;
7533
7533
  }
7534
+ function cacheWrittenAt(projectPath) {
7535
+ const cache = join6(projectPath, ".godot", "global_script_class_cache.cfg");
7536
+ try {
7537
+ return statSync2(cache).mtimeMs;
7538
+ } catch {
7539
+ return null;
7540
+ }
7541
+ }
7534
7542
  function staleAgainst(cached, projectPath) {
7535
7543
  return [...declaredClasses(projectPath)].filter(([name, path]) => cached.get(name) !== path).map(([name]) => name);
7536
7544
  }
@@ -14744,9 +14752,17 @@ var init_types = __esm(() => {
14744
14752
  });
14745
14753
 
14746
14754
  // src/paths.ts
14755
+ import { realpathSync } from "node:fs";
14747
14756
  import { isAbsolute, relative, resolve, sep } from "node:path";
14757
+ function realPathOr(path) {
14758
+ try {
14759
+ return realpathSync.native(path);
14760
+ } catch {
14761
+ return path;
14762
+ }
14763
+ }
14748
14764
  function isSameDirectory(onePath, otherPath) {
14749
- return relative(resolve(onePath), resolve(otherPath)) === "";
14765
+ return relative(realPathOr(resolve(onePath)), realPathOr(resolve(otherPath))) === "";
14750
14766
  }
14751
14767
  function isWithinRoot(rootPath, candidatePath) {
14752
14768
  const step = relative(resolve(rootPath), resolve(candidatePath));
@@ -15127,8 +15143,16 @@ function buildToolDefinitions() {
15127
15143
  };
15128
15144
  });
15129
15145
  }
15130
- var PROJECT_PATH, RUNNING_PROJECT_PATH, SCENE_PATH, SCRIPT_PATH, RESOURCE_PATH, NODE_PATH, PROPERTIES, XY, ANIMATION_TRACK, SCRIPT_MODIFICATIONS, INPUT_EVENTS, TILESET_SOURCES, TILEMAP_CELLS, COLOUR, TOOL_SPECS, SPECS_BY_NAME;
15146
+ var PROJECT_INFO_SECTIONS, PROJECT_PATH, RUNNING_PROJECT_PATH, SCENE_PATH, SCRIPT_PATH, RESOURCE_PATH, NODE_PATH, PROPERTIES, XY, ANIMATION_TRACK, SCRIPT_MODIFICATIONS, INPUT_EVENTS, TILESET_SOURCES, TILEMAP_CELLS, COLOUR, TOOL_SPECS, SPECS_BY_NAME;
15131
15147
  var init_tool_definitions = __esm(() => {
15148
+ PROJECT_INFO_SECTIONS = [
15149
+ "autoloads",
15150
+ "plugins",
15151
+ "export_presets",
15152
+ "audio_buses",
15153
+ "health",
15154
+ "validation"
15155
+ ];
15132
15156
  PROJECT_PATH = {
15133
15157
  type: "string",
15134
15158
  description: "Absolute path to the project directory, the one holding project.godot."
@@ -15155,7 +15179,7 @@ var init_tool_definitions = __esm(() => {
15155
15179
  };
15156
15180
  PROPERTIES = {
15157
15181
  type: "object",
15158
- description: 'Properties to set, keyed by Godot property name. Vectors, colours and the like may be written as {"x": 1, "y": 2} or tagged {"_type": "Vector2", "x": 1, "y": 2}.',
15182
+ description: 'Properties to set, keyed by Godot property name. Vectors, colours and the like may be written as {"x": 1, "y": 2} or tagged {"_type": "Vector2", "x": 1, "y": 2}, which is the form a read answers with, so a value read off one node can be written straight to another.',
15159
15183
  additionalProperties: true
15160
15184
  };
15161
15185
  XY = {
@@ -15278,10 +15302,7 @@ var init_tool_definitions = __esm(() => {
15278
15302
  projectPath: PROJECT_PATH,
15279
15303
  include: {
15280
15304
  type: "array",
15281
- items: {
15282
- type: "string",
15283
- enum: ["autoloads", "plugins", "export_presets", "audio_buses", "health", "validation"]
15284
- },
15305
+ items: { type: "string", enum: PROJECT_INFO_SECTIONS },
15285
15306
  description: "Extra sections: registered autoloads, addons and whether each is enabled, export presets, the audio bus layout, a health report, or export validation."
15286
15307
  },
15287
15308
  preset: { type: "string", description: "For validation: the export preset to validate against." },
@@ -15299,6 +15320,12 @@ var init_tool_definitions = __esm(() => {
15299
15320
  parameters: {
15300
15321
  projectPath: PROJECT_PATH,
15301
15322
  setting: { type: "string", description: 'Setting path, such as "display/window/size/viewport_width".' },
15323
+ from: {
15324
+ type: "string",
15325
+ enum: ["disk", "editor"],
15326
+ ops: ["get"],
15327
+ description: "get: where to read. Default disk, which starts a short-lived engine and reads project.godot, needs no editor and is what an automatic check can reproduce. editor asks the open editor instead, which costs no engine and answers what the editor holds, including changes nobody has saved. Asking for editor with none connected is refused rather than answered from disk, so an answer never means the other one quietly."
15328
+ },
15302
15329
  prefix: {
15303
15330
  type: "string",
15304
15331
  ops: ["get"],
@@ -15790,7 +15817,7 @@ var init_tool_definitions = __esm(() => {
15790
15817
  },
15791
15818
  {
15792
15819
  name: "editor_rescan",
15793
- description: "Makes the running editor scan the project filesystem, so files written outside it are picked up. The scan is a change-detecting walk rather than an unconditional reparse, so a file another engine has already imported reads as settled and the walk does not look inside it: its class_name then stays out of the list the editor resolves against, however many times you scan. Any class in that state is named under unseenByEditor, which is what no check on disk can see, since the declaration and the cache are both correct there and only the editor disagrees. The cure is a change to the declaring script, or editor_launch restart; project_import refresh_classes rewrites the cache and does not touch what the editor is holding. The scan also writes .godot/global_script_class_cache.cfg from the list the editor is holding, so a class it cannot resolve is one the scan drops out of that file: unseenByEditor names it before a fresh engine, a CI run or a clone inherits it. Needs the editor connected.",
15820
+ description: "Makes the running editor scan the project filesystem, so files written outside it are picked up. The scan is a change-detecting walk rather than an unconditional reparse, so a file another engine has already imported reads as settled and the walk does not look inside it: its class_name then stays out of the list the editor resolves against, however many times you scan. Any class in that state is named under unseenByEditor, which is what no check on disk can see, since the declaration and the cache are both correct there and only the editor disagrees. The cure is both halves in order: a change to the declaring script, saved, and then a rescan. Measured downstream, neither works alone, and a change on its own reads as the cure having failed. editor_launch restart also does it, and project_import refresh_classes does not: it rewrites the cache and does not touch what the editor is holding. A scan writes .godot/global_script_class_cache.cfg from the list the editor is holding, so a class the editor cannot resolve can go out of that file with it, which one project saw and two could not reproduce; where it happens the answer names what the file lost under cacheLost, before a fresh engine, a CI run or a clone inherits it. Needs the editor connected.",
15794
15821
  parameters: {
15795
15822
  projectPath: PROJECT_PATH,
15796
15823
  timeoutMs: { type: "number", description: "How long to wait for the scan. Default 30000." }
@@ -16151,7 +16178,7 @@ var init_tool_definitions = __esm(() => {
16151
16178
  });
16152
16179
 
16153
16180
  // src/headless-operations.ts
16154
- var HEADLESS_OPERATIONS;
16181
+ var HEADLESS_OPERATIONS, EDITOR_READS;
16155
16182
  var init_headless_operations = __esm(() => {
16156
16183
  HEADLESS_OPERATIONS = dictionary({
16157
16184
  project_settings: dictionary({
@@ -16185,6 +16212,9 @@ var init_headless_operations = __esm(() => {
16185
16212
  inheritance: "inspect_inheritance"
16186
16213
  })
16187
16214
  });
16215
+ EDITOR_READS = dictionary({
16216
+ project_settings: dictionary({ get: "get_project_setting" })
16217
+ });
16188
16218
  });
16189
16219
 
16190
16220
  // node_modules/zod/v3/helpers/util.js
@@ -30955,6 +30985,36 @@ var init_bridge_announce = __esm(() => {
30955
30985
  init_errors();
30956
30986
  });
30957
30987
 
30988
+ // src/config-pin.ts
30989
+ import { existsSync as existsSync9, readFileSync as readFileSync10 } from "node:fs";
30990
+ import { join as join12 } from "node:path";
30991
+ function versionsNamedInConfig(projectPath) {
30992
+ const config = join12(projectPath, ".mcp.json");
30993
+ if (!existsSync9(config)) {
30994
+ return [];
30995
+ }
30996
+ let text;
30997
+ try {
30998
+ text = readFileSync10(config, "utf8");
30999
+ JSON.parse(text);
31000
+ } catch {
31001
+ return [];
31002
+ }
31003
+ return [...new Set([...text.matchAll(PINNED)].map((found) => found[1] ?? ""))].filter((version) => version !== "");
31004
+ }
31005
+ function configDisagrees(projectPath, serverVersion) {
31006
+ const named = versionsNamedInConfig(projectPath);
31007
+ const others = named.filter((version) => version !== serverVersion);
31008
+ if (others.length === 0) {
31009
+ return null;
31010
+ }
31011
+ return `.mcp.json in this project names gdharness@${others.join(" and gdharness@")}, and this server ` + `is ${serverVersion}. Whichever is meant to be right, nothing will change version until the ` + "MCP server is reconnected, and a pin kept anywhere else does not reach this file.";
31012
+ }
31013
+ var PINNED;
31014
+ var init_config_pin = __esm(() => {
31015
+ PINNED = /\bgdharness@(\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?)\b/g;
31016
+ });
31017
+
30958
31018
  // src/framing.ts
30959
31019
  function frame(message) {
30960
31020
  const body = Buffer.from(JSON.stringify(message), "utf8");
@@ -34932,7 +34992,7 @@ var init_godot_bridge = __esm(() => {
34932
34992
 
34933
34993
  // src/godot-path.ts
34934
34994
  import { execFile as execFile4 } from "node:child_process";
34935
- import { existsSync as existsSync9 } from "node:fs";
34995
+ import { existsSync as existsSync10 } from "node:fs";
34936
34996
  import { normalize as normalize2 } from "node:path";
34937
34997
  import { promisify as promisify4 } from "node:util";
34938
34998
  var run4, GodotLocator2;
@@ -34974,7 +35034,7 @@ var init_godot_path = __esm(() => {
34974
35034
  return known;
34975
35035
  }
34976
35036
  let ok = false;
34977
- if (path === "godot" || existsSync9(path)) {
35037
+ if (path === "godot" || existsSync10(path)) {
34978
35038
  try {
34979
35039
  await run4(path, ["--version"]);
34980
35040
  ok = true;
@@ -35319,7 +35379,7 @@ var init_junit = __esm(() => {
35319
35379
  });
35320
35380
 
35321
35381
  // src/lsp_client.ts
35322
- import { realpathSync } from "node:fs";
35382
+ import { realpathSync as realpathSync2 } from "node:fs";
35323
35383
  import { readFile, realpath } from "node:fs/promises";
35324
35384
  import { createConnection as createConnection2 } from "node:net";
35325
35385
  import { dirname as dirname7, resolve as resolve3 } from "node:path";
@@ -35335,7 +35395,7 @@ function diagnosticsKey(uri) {
35335
35395
  return decoded;
35336
35396
  }
35337
35397
  try {
35338
- return realpathSync.native(fileURLToPath3(decoded)).toLowerCase();
35398
+ return realpathSync2.native(fileURLToPath3(decoded)).toLowerCase();
35339
35399
  } catch {
35340
35400
  return decoded.toLowerCase();
35341
35401
  }
@@ -35895,13 +35955,13 @@ var init_process_time = __esm(() => {
35895
35955
  });
35896
35956
 
35897
35957
  // src/project-scan.ts
35898
- import { existsSync as existsSync10, readdirSync as readdirSync4, readFileSync as readFileSync10 } from "node:fs";
35899
- import { join as join12 } from "node:path";
35958
+ import { existsSync as existsSync11, readdirSync as readdirSync4, readFileSync as readFileSync11 } from "node:fs";
35959
+ import { join as join13 } from "node:path";
35900
35960
  function skipped(name) {
35901
35961
  return name.startsWith(".") || SKIPPED.has(name);
35902
35962
  }
35903
35963
  function steppedOver(directory) {
35904
- return existsSync10(join12(directory, ".gdignore"));
35964
+ return existsSync11(join13(directory, ".gdignore"));
35905
35965
  }
35906
35966
  function projectStructure(projectPath) {
35907
35967
  const structure = { scenes: 0, scripts: 0, assets: 0, other: 0 };
@@ -35914,7 +35974,7 @@ function projectStructure(projectPath) {
35914
35974
  continue;
35915
35975
  }
35916
35976
  if (entry.isDirectory()) {
35917
- visit(join12(directory, entry.name));
35977
+ visit(join13(directory, entry.name));
35918
35978
  } else if (entry.isFile()) {
35919
35979
  const extension = entry.name.split(".").pop()?.toLowerCase() ?? "";
35920
35980
  if (extension === "tscn") {
@@ -35959,7 +36019,7 @@ function searchProject(projectPath, options) {
35959
36019
  if (skipped(entry.name)) {
35960
36020
  continue;
35961
36021
  }
35962
- const entryPath = join12(directory, entry.name);
36022
+ const entryPath = join13(directory, entry.name);
35963
36023
  if (entry.isDirectory()) {
35964
36024
  visit(entryPath);
35965
36025
  continue;
@@ -35970,7 +36030,7 @@ function searchProject(projectPath, options) {
35970
36030
  }
35971
36031
  result.summary.files_searched += 1;
35972
36032
  const matches = [];
35973
- for (const [index, line] of readFileSync10(entryPath, "utf8").split(`
36033
+ for (const [index, line] of readFileSync11(entryPath, "utf8").split(`
35974
36034
  `).entries()) {
35975
36035
  if (full()) {
35976
36036
  break;
@@ -35998,10 +36058,10 @@ var init_project_scan = __esm(() => {
35998
36058
  });
35999
36059
 
36000
36060
  // src/runtime-client.ts
36001
- import { existsSync as existsSync11, readdirSync as readdirSync5, readFileSync as readFileSync11, unlinkSync } from "node:fs";
36061
+ import { existsSync as existsSync12, readdirSync as readdirSync5, readFileSync as readFileSync12, unlinkSync } from "node:fs";
36002
36062
  import { createConnection as createConnection3 } from "node:net";
36003
36063
  import { tmpdir as tmpdir4 } from "node:os";
36004
- import { join as join13, resolve as resolve4 } from "node:path";
36064
+ import { join as join14, resolve as resolve4 } from "node:path";
36005
36065
  import { setTimeout as delay2 } from "node:timers/promises";
36006
36066
  function runtimeDirectory(variables = process.env) {
36007
36067
  const explicit = envValue("GDHARNESS_RUNTIME_DIR", variables);
@@ -36009,13 +36069,13 @@ function runtimeDirectory(variables = process.env) {
36009
36069
  return explicit;
36010
36070
  }
36011
36071
  const perUser = envValue("XDG_RUNTIME_DIR", variables);
36012
- return join13(perUser ?? tmpdir4(), "gdharness");
36072
+ return join14(perUser ?? tmpdir4(), "gdharness");
36013
36073
  }
36014
36074
  function runtimeDirectories(variables = process.env) {
36015
36075
  const candidates = [runtimeDirectory(variables)];
36016
36076
  const fallbacks = process.platform === "win32" ? [envValue("SystemRoot", variables) ?? envValue("windir", variables) ?? "C:\\Windows"] : ["/tmp", "/var/tmp"];
36017
36077
  for (const base of fallbacks) {
36018
- candidates.push(join13(base, process.platform === "win32" ? "Temp" : "", "gdharness"));
36078
+ candidates.push(join14(base, process.platform === "win32" ? "Temp" : "", "gdharness"));
36019
36079
  }
36020
36080
  return [...new Set(candidates.map((path) => resolve4(path)))];
36021
36081
  }
@@ -36030,7 +36090,7 @@ function processAlive(pid) {
36030
36090
  function parseAnnouncement(file, pid) {
36031
36091
  let fields;
36032
36092
  try {
36033
- fields = asParams(JSON.parse(readFileSync11(file, "utf8")));
36093
+ fields = asParams(JSON.parse(readFileSync12(file, "utf8")));
36034
36094
  } catch {
36035
36095
  return { kind: "rubbish" };
36036
36096
  }
@@ -36069,7 +36129,7 @@ function discoverRuntimes(directories = runtimeDirectories()) {
36069
36129
  return runtimesAnnounced(directories).running;
36070
36130
  }
36071
36131
  function announcedIn(directory) {
36072
- if (!existsSync11(directory)) {
36132
+ if (!existsSync12(directory)) {
36073
36133
  return [];
36074
36134
  }
36075
36135
  const found = [];
@@ -36078,7 +36138,7 @@ function announcedIn(directory) {
36078
36138
  if (!match) {
36079
36139
  continue;
36080
36140
  }
36081
- const file = join13(directory, entry);
36141
+ const file = join14(directory, entry);
36082
36142
  const pid = Number.parseInt(match[1] ?? "", 10);
36083
36143
  const announced = processAlive(pid) ? parseAnnouncement(file, pid) : { kind: "rubbish" };
36084
36144
  if (announced.kind === "rubbish") {
@@ -36240,20 +36300,20 @@ var init_runtime_client = __esm(() => {
36240
36300
 
36241
36301
  // src/run-record.ts
36242
36302
  import { execFileSync } from "node:child_process";
36243
- import { mkdirSync as mkdirSync6, openSync, readdirSync as readdirSync6, readFileSync as readFileSync12, rmSync as rmSync7, statSync as statSync2, writeFileSync as writeFileSync8 } from "node:fs";
36244
- import { basename as basename2, join as join14 } from "node:path";
36303
+ import { mkdirSync as mkdirSync6, openSync, readdirSync as readdirSync6, readFileSync as readFileSync13, rmSync as rmSync7, statSync as statSync3, writeFileSync as writeFileSync8 } from "node:fs";
36304
+ import { basename as basename2, join as join15 } from "node:path";
36245
36305
  function runsDirectory() {
36246
- return join14(runtimeDirectory(), "runs");
36306
+ return join15(runtimeDirectory(), "runs");
36247
36307
  }
36248
36308
  function recordPath() {
36249
- return join14(runsDirectory(), "run.json");
36309
+ return join15(runsDirectory(), "run.json");
36250
36310
  }
36251
36311
  function recordPaths() {
36252
- return runtimeDirectories().map((directory) => join14(directory, "runs", "run.json"));
36312
+ return runtimeDirectories().map((directory) => join15(directory, "runs", "run.json"));
36253
36313
  }
36254
36314
  function openTranscript(startedAt) {
36255
36315
  mkdirSync6(runsDirectory(), { recursive: true });
36256
- const path = join14(runsDirectory(), `run-${startedAt}.log`);
36316
+ const path = join15(runsDirectory(), `run-${startedAt}.log`);
36257
36317
  return { path, fd: openSync(path, "a") };
36258
36318
  }
36259
36319
  function writeRunRecord(record) {
@@ -36282,7 +36342,7 @@ function readRunRecord() {
36282
36342
  function recordAt(path) {
36283
36343
  let parsed;
36284
36344
  try {
36285
- parsed = JSON.parse(readFileSync12(path, "utf8"));
36345
+ parsed = JSON.parse(readFileSync13(path, "utf8"));
36286
36346
  } catch {
36287
36347
  return null;
36288
36348
  }
@@ -36318,7 +36378,7 @@ function runningAs(pid) {
36318
36378
  }
36319
36379
  try {
36320
36380
  if (process.platform === "linux") {
36321
- const raw = readFileSync12(`/proc/${pid}/cmdline`, "utf8").replaceAll("\x00", " ").trim();
36381
+ const raw = readFileSync13(`/proc/${pid}/cmdline`, "utf8").replaceAll("\x00", " ").trim();
36322
36382
  return raw === "" ? null : { kind: "commandLine", text: raw };
36323
36383
  }
36324
36384
  const args = execFileSync("ps", ["-p", String(pid), "-o", "args="], {
@@ -36393,7 +36453,7 @@ function clearRunRecord(ours = () => true) {
36393
36453
  }
36394
36454
  function ageMs(path, now) {
36395
36455
  try {
36396
- return now - statSync2(path).mtimeMs;
36456
+ return now - statSync3(path).mtimeMs;
36397
36457
  } catch {
36398
36458
  return 0;
36399
36459
  }
@@ -36411,7 +36471,7 @@ function sweepTranscripts(keepMs = 24 * 60 * 60 * 1000, now = Date.now()) {
36411
36471
  if (!entry.startsWith("run-") || !entry.endsWith(".log")) {
36412
36472
  continue;
36413
36473
  }
36414
- const path = join14(directory, entry);
36474
+ const path = join15(directory, entry);
36415
36475
  if (path === keep || ageMs(path, now) < keepMs) {
36416
36476
  continue;
36417
36477
  }
@@ -36425,14 +36485,14 @@ var init_run_record = __esm(() => {
36425
36485
  });
36426
36486
 
36427
36487
  // src/setup.ts
36428
- import { cpSync as cpSync2, existsSync as existsSync12, mkdirSync as mkdirSync7, readFileSync as readFileSync13, rmSync as rmSync8, writeFileSync as writeFileSync9 } from "node:fs";
36429
- import { dirname as dirname8, join as join15 } from "node:path";
36488
+ import { cpSync as cpSync2, existsSync as existsSync13, mkdirSync as mkdirSync7, readFileSync as readFileSync14, rmSync as rmSync8, writeFileSync as writeFileSync9 } from "node:fs";
36489
+ import { dirname as dirname8, join as join16 } from "node:path";
36430
36490
  function installedAddonVersion(projectPath) {
36431
- const marker = join15(projectPath, "addons", ADDONS2[0], VERSION_MARKER2);
36432
- if (!existsSync12(marker)) {
36491
+ const marker = join16(projectPath, "addons", ADDONS2[0], VERSION_MARKER2);
36492
+ if (!existsSync13(marker)) {
36433
36493
  return null;
36434
36494
  }
36435
- const said = readFileSync13(marker, "utf8").trim();
36495
+ const said = readFileSync14(marker, "utf8").trim();
36436
36496
  return said === "" ? null : said;
36437
36497
  }
36438
36498
  var ADDONS2, RUNTIME_AUTOLOAD2, VERSION_MARKER2 = ".gdharness-version";
@@ -36453,19 +36513,18 @@ import { execFile as execFile6, spawn } from "node:child_process";
36453
36513
  import { randomUUID as randomUUID2 } from "node:crypto";
36454
36514
  import {
36455
36515
  closeSync,
36456
- existsSync as existsSync13,
36516
+ existsSync as existsSync14,
36457
36517
  fstatSync,
36458
36518
  mkdirSync as mkdirSync8,
36459
36519
  mkdtempSync as mkdtempSync3,
36460
36520
  openSync as openSync2,
36461
36521
  readdirSync as readdirSync7,
36462
- readFileSync as readFileSync14,
36522
+ readFileSync as readFileSync15,
36463
36523
  readSync,
36464
- realpathSync as realpathSync2,
36465
36524
  rmSync as rmSync9
36466
36525
  } from "node:fs";
36467
36526
  import { tmpdir as tmpdir5 } from "node:os";
36468
- import { basename as basename3, dirname as dirname9, join as join16, normalize as normalize3 } from "node:path";
36527
+ import { basename as basename3, dirname as dirname9, join as join17, normalize as normalize3 } from "node:path";
36469
36528
  import { setTimeout as delay3 } from "node:timers/promises";
36470
36529
  import { fileURLToPath as fileURLToPath4 } from "node:url";
36471
36530
  import { promisify as promisify6 } from "node:util";
@@ -36563,6 +36622,30 @@ function wrongTypes(spec, args) {
36563
36622
  }
36564
36623
  return complaints;
36565
36624
  }
36625
+ function unlistedValues(schema, value, path) {
36626
+ if (typeof schema !== "object" || schema === null) {
36627
+ return [];
36628
+ }
36629
+ const fields = schema;
36630
+ const listed = fields["enum"];
36631
+ if (Array.isArray(listed) && typeof value === "string") {
36632
+ return listed.includes(value) ? [] : [`${path} as one of ${listed.join(", ")}, not ${JSON.stringify(value)}`];
36633
+ }
36634
+ if (Array.isArray(value)) {
36635
+ return value.flatMap((element, index) => unlistedValues(fields["items"], element, `${path}[${index}]`));
36636
+ }
36637
+ const properties = fields["properties"];
36638
+ if (isPlainObject3(properties) && isPlainObject3(value)) {
36639
+ return Object.entries(value).flatMap(([field, held]) => Object.hasOwn(properties, field) ? unlistedValues(properties[field], held, `${path}.${field}`) : []);
36640
+ }
36641
+ return [];
36642
+ }
36643
+ function isPlainObject3(value) {
36644
+ return typeof value === "object" && value !== null && !Array.isArray(value);
36645
+ }
36646
+ function outsideTheirLists(spec, args) {
36647
+ return Object.entries(args).flatMap(([name, value]) => unlistedValues(spec.parameters[name], value, name));
36648
+ }
36566
36649
  function escapingPropertyValue(properties) {
36567
36650
  if (typeof properties !== "object" || properties === null) {
36568
36651
  return;
@@ -36577,17 +36660,6 @@ function escapingPropertyValue(properties) {
36577
36660
  }
36578
36661
  return;
36579
36662
  }
36580
- function realPathOr(path) {
36581
- try {
36582
- return realpathSync2.native(path);
36583
- } catch {
36584
- return path;
36585
- }
36586
- }
36587
- function samePath(one, other) {
36588
- const settled = (path) => realPathOr(path).replaceAll("\\", "/").replace(/\/+$/, "").toLowerCase();
36589
- return settled(one) === settled(other);
36590
- }
36591
36663
  function aboveTheRunner(entry) {
36592
36664
  const runner = entry.detail.findIndex((line) => line.includes(RUNNER_DIRECTORY));
36593
36665
  if (runner < 0) {
@@ -36601,7 +36673,7 @@ function aboveTheRunner(entry) {
36601
36673
  }
36602
36674
  function elsewhereIn(projectPath, asked) {
36603
36675
  const wanted = asked.replace(/^res:\/\//, "").replace(/\/+$/, "");
36604
- if (wanted === "" || existsSync13(join16(projectPath, wanted))) {
36676
+ if (wanted === "" || existsSync14(join17(projectPath, wanted))) {
36605
36677
  return "";
36606
36678
  }
36607
36679
  const root = (wanted.split("/")[0] ?? "").toLowerCase();
@@ -36615,11 +36687,11 @@ function elsewhereIn(projectPath, asked) {
36615
36687
  return near.length === 0 ? "" : ` The project does have ${there}.`;
36616
36688
  }
36617
36689
  function hasMainScene(projectFile) {
36618
- const scene = parseProjectGodot(readFileSync14(projectFile, "utf8"))["application"]?.["run/main_scene"];
36690
+ const scene = parseProjectGodot(readFileSync15(projectFile, "utf8"))["application"]?.["run/main_scene"];
36619
36691
  return typeof scene === "string" && scene !== "";
36620
36692
  }
36621
36693
  function editorPlaysHeadless(projectFile) {
36622
- const runArgs = parseProjectGodot(readFileSync14(projectFile, "utf8"))["editor"]?.["run/main_run_args"];
36694
+ const runArgs = parseProjectGodot(readFileSync15(projectFile, "utf8"))["editor"]?.["run/main_run_args"];
36623
36695
  return typeof runArgs === "string" && /(?:^|\s)--headless(?:\s|$)/.test(runArgs);
36624
36696
  }
36625
36697
  function camelCased(params) {
@@ -36635,7 +36707,7 @@ function camelCased(params) {
36635
36707
  class GodotServer {
36636
36708
  mcp;
36637
36709
  locator = new GodotLocator2;
36638
- operationsScript = join16(__dirname2, "godot", "operations", "godot_operations.gd");
36710
+ operationsScript = join17(__dirname2, "godot", "operations", "godot_operations.gd");
36639
36711
  godotBridge;
36640
36712
  tools = buildToolDefinitions();
36641
36713
  activeProcess = null;
@@ -36909,6 +36981,18 @@ class GodotServer {
36909
36981
  }
36910
36982
  });
36911
36983
  }
36984
+ const disagreement = this.configDisagreement();
36985
+ if (disagreement !== null) {
36986
+ this.noticedUpdate = true;
36987
+ this.callsSinceNotice = 0;
36988
+ return this.saying(answer, {
36989
+ config_names_another_version: {
36990
+ server_is: SERVER_VERSION,
36991
+ said: disagreement,
36992
+ what_to_do: "Tell the user which version their config names and which one is answering, and that " + "the two will not meet until .mcp.json is right and the MCP server is reconnected. Do " + "not edit their config without asking: which of the two they meant is theirs to say."
36993
+ }
36994
+ });
36995
+ }
36912
36996
  const notice = this.updates.notice();
36913
36997
  if (notice === null) {
36914
36998
  return answer;
@@ -36935,6 +37019,9 @@ class GodotServer {
36935
37019
  const installed = installedAddonVersion(this.ownProject);
36936
37020
  return installed === null || installed === SERVER_VERSION ? null : installed;
36937
37021
  }
37022
+ configDisagreement() {
37023
+ return this.ownProject === null ? null : configDisagrees(this.ownProject, SERVER_VERSION);
37024
+ }
36938
37025
  withFeedbackNotice(answer) {
36939
37026
  this.callsSinceFeedback += 1;
36940
37027
  if (this.callsSinceFeedback < FEEDBACK_NOTICE_EVERY) {
@@ -36960,7 +37047,7 @@ class GodotServer {
36960
37047
  response: this.createErrorResponse(`${where} does not take ${unknown.join(", ")}. ${takes} This is gdharness ${SERVER_VERSION}, which is what decides whether an argument exists.`)
36961
37048
  };
36962
37049
  }
36963
- const wrong = wrongTypes(spec, args);
37050
+ const wrong = [...wrongTypes(spec, args), ...outsideTheirLists(spec, args)];
36964
37051
  if (wrong.length > 0) {
36965
37052
  return { ok: false, response: this.createErrorResponse(`${spec.name} takes ${wrong.join("; ")}.`) };
36966
37053
  }
@@ -37012,7 +37099,25 @@ class GodotServer {
37012
37099
  async dispatch(tool, op, args) {
37013
37100
  const headless = HEADLESS_OPERATIONS[tool]?.[op];
37014
37101
  if (headless !== undefined) {
37015
- const answered = await this.headless(headless, args);
37102
+ const { from: asked, ...answerable } = args;
37103
+ if (asked === "editor") {
37104
+ const editorSide = EDITOR_READS[tool]?.[op];
37105
+ if (editorSide === undefined) {
37106
+ return this.createErrorResponse(`${tool} ${op} cannot be read from the editor, only from disk.`, [
37107
+ 'Leave from unset, or pass from: "disk"'
37108
+ ]);
37109
+ }
37110
+ if (!this.godotBridge.isConnected()) {
37111
+ return this.createErrorResponse(`${tool} ${op} was asked of the editor, and no editor has reached this server.`, [
37112
+ "editor_status says whether the bridge is up and whether an editor may yet connect",
37113
+ 'Open the project with editor_launch, or pass from: "disk" to read the file instead'
37114
+ ]);
37115
+ }
37116
+ const { op: _dropped, ...asks } = answerable;
37117
+ const contained = this.containProjectFiles(asks);
37118
+ return contained.ok ? await this.handleViaBridge(editorSide, contained.value) : contained.response;
37119
+ }
37120
+ const answered = await this.headless(headless, answerable);
37016
37121
  return headless === "refresh_class_cache" && this.godotBridge.isConnected() ? await this.alsoSayWhatTheEditorCannotSee(answered, args) : answered;
37017
37122
  }
37018
37123
  const { op: _op, ...arguments_ } = args;
@@ -37227,8 +37332,8 @@ class GodotServer {
37227
37332
  if (path === undefined) {
37228
37333
  return { ok: false, response: this.createErrorResponse("projectPath is required.") };
37229
37334
  }
37230
- const file = join16(path, "project.godot");
37231
- if (!existsSync13(file)) {
37335
+ const file = join17(path, "project.godot");
37336
+ if (!existsSync14(file)) {
37232
37337
  return {
37233
37338
  ok: false,
37234
37339
  response: this.createErrorResponse(`Not a Godot project: ${path}`, [
@@ -37385,9 +37490,9 @@ class GodotServer {
37385
37490
  }
37386
37491
  async handleProjectInfo(args) {
37387
37492
  const include = readStringArray(args, "include") ?? [];
37388
- const unknown = include.filter((name) => PROJECT_INFO_SECTIONS[name] === undefined);
37493
+ const unknown = include.filter((name) => PROJECT_INFO_SECTIONS2[name] === undefined);
37389
37494
  if (unknown.length > 0) {
37390
- return this.createErrorResponse(`project_info cannot include ${unknown.join(", ")}. Sections: ${Object.keys(PROJECT_INFO_SECTIONS).join(", ")}.`);
37495
+ return this.createErrorResponse(`project_info cannot include ${unknown.join(", ")}. Sections: ${Object.keys(PROJECT_INFO_SECTIONS2).join(", ")}.`);
37391
37496
  }
37392
37497
  const project = this.project(args);
37393
37498
  if (!project.ok) {
@@ -37397,7 +37502,7 @@ class GodotServer {
37397
37502
  if (!engine.ok) {
37398
37503
  return engine.response;
37399
37504
  }
37400
- const application = parseProjectGodot(readFileSync14(project.value.file, "utf8"))["application"] ?? {};
37505
+ const application = parseProjectGodot(readFileSync15(project.value.file, "utf8"))["application"] ?? {};
37401
37506
  const name = application["config/name"];
37402
37507
  const mainScene = application["run/main_scene"];
37403
37508
  const info = {
@@ -37409,7 +37514,7 @@ class GodotServer {
37409
37514
  };
37410
37515
  const detailed = args["detail"] === "full";
37411
37516
  for (const section of include) {
37412
- const spec = PROJECT_INFO_SECTIONS[section];
37517
+ const spec = PROJECT_INFO_SECTIONS2[section];
37413
37518
  if (spec === undefined) {
37414
37519
  continue;
37415
37520
  }
@@ -37465,11 +37570,11 @@ class GodotServer {
37465
37570
  return this.createErrorResponse(`Cannot create ${dirname9(output.relativePath)} to export into: ${errorMessage(error)}`);
37466
37571
  }
37467
37572
  const debug = readBoolean(args, "debug") ?? false;
37468
- const exportLogs = mkdtempSync3(join16(tmpdir5(), "gdharness-export-"));
37573
+ const exportLogs = mkdtempSync3(join17(tmpdir5(), "gdharness-export-"));
37469
37574
  const exportArgs = [
37470
37575
  "--headless",
37471
37576
  "--log-file",
37472
- join16(exportLogs, "engine.log"),
37577
+ join17(exportLogs, "engine.log"),
37473
37578
  "--path",
37474
37579
  project.value.path,
37475
37580
  debug ? "--export-debug" : "--export-release",
@@ -37497,7 +37602,7 @@ class GodotServer {
37497
37602
  log.finish();
37498
37603
  const problems = log.select({ severity: "warning", sinceLastCall: false, limit: 200 });
37499
37604
  const verdict = {
37500
- exported: exitCode === 0 && log.count("error") === 0 && existsSync13(output.absolutePath),
37605
+ exported: exitCode === 0 && log.count("error") === 0 && existsSync14(output.absolutePath),
37501
37606
  preset,
37502
37607
  outputPath: output.relativePath,
37503
37608
  debug,
@@ -37527,7 +37632,7 @@ class GodotServer {
37527
37632
  return contained.response;
37528
37633
  }
37529
37634
  const runner = "addons/gdUnit4/bin/GdUnitCmdTool.gd";
37530
- if (!existsSync13(join16(project.value.path, runner))) {
37635
+ if (!existsSync14(join17(project.value.path, runner))) {
37531
37636
  return this.createErrorResponse(`gdUnit4 is not installed in this project: no ${runner}.`, [
37532
37637
  "Install gdUnit4 under addons/gdUnit4, from https://github.com/godot-gdunit-labs/gdUnit4"
37533
37638
  ]);
@@ -37562,7 +37667,7 @@ class GodotServer {
37562
37667
  ];
37563
37668
  const timeoutMs = readPositiveNumber(args, "timeoutMs") ?? 600000;
37564
37669
  this.logDebug(`Running tests: ${engine.value} ${cmdArgs.join(" ")}`);
37565
- const userData = mkdtempSync3(join16(tmpdir5(), "gdharness-tests-"));
37670
+ const userData = mkdtempSync3(join17(tmpdir5(), "gdharness-tests-"));
37566
37671
  const run = this.spawnGame(engine.value, cmdArgs, userDataIn(userData));
37567
37672
  const hung = await new Promise((resolve) => {
37568
37673
  const timer = setTimeout(() => {
@@ -37578,14 +37683,14 @@ class GodotServer {
37578
37683
  resolve(false);
37579
37684
  });
37580
37685
  });
37581
- const reportsDir = join16(project.value.path, ".godot", ...ours.split("/"));
37686
+ const reportsDir = join17(project.value.path, ".godot", ...ours.split("/"));
37582
37687
  let report = null;
37583
37688
  let reportProblem = null;
37584
37689
  try {
37585
- const written = existsSync13(reportsDir) ? readdirSync7(reportsDir).filter((name) => name.startsWith("report_")).sort((a, b) => Number(a.slice("report_".length)) - Number(b.slice("report_".length))) : [];
37690
+ const written = existsSync14(reportsDir) ? readdirSync7(reportsDir).filter((name) => name.startsWith("report_")).sort((a, b) => Number(a.slice("report_".length)) - Number(b.slice("report_".length))) : [];
37586
37691
  const newest = written.at(-1);
37587
37692
  if (newest !== undefined) {
37588
- report = parseJUnit(readFileSync14(join16(reportsDir, newest, "results.xml"), "utf8"));
37693
+ report = parseJUnit(readFileSync15(join17(reportsDir, newest, "results.xml"), "utf8"));
37589
37694
  }
37590
37695
  } catch (error) {
37591
37696
  reportProblem = errorMessage(error);
@@ -37748,6 +37853,7 @@ class GodotServer {
37748
37853
  retryingBridge: this.bridgeRetry === null ? undefined : true,
37749
37854
  announcedAt: this.announcedAt ?? undefined,
37750
37855
  projectIs: this.projectHasMovedOn() ?? undefined,
37856
+ configIs: this.configDisagreement() ?? undefined,
37751
37857
  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,
37752
37858
  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
37753
37859
  };
@@ -38003,7 +38109,7 @@ class GodotServer {
38003
38109
  return runIsUp(run, editorSays);
38004
38110
  }
38005
38111
  async runtimeUp(projectPath, before, budgetMs) {
38006
- if (!existsSync13(join16(projectPath, RUNTIME_AUTOLOAD2.path))) {
38112
+ if (!existsSync14(join17(projectPath, RUNTIME_AUTOLOAD2.path))) {
38007
38113
  return runtimeVerdict(null, { addon: false, budgetMs, heldAt: null, running: false });
38008
38114
  }
38009
38115
  const endpoint = await announcedSince(projectPath, before, {
@@ -38258,7 +38364,7 @@ class GodotServer {
38258
38364
  if (mine === null) {
38259
38365
  return false;
38260
38366
  }
38261
- return project !== "" && samePath(mine, project);
38367
+ return project !== "" && isSameDirectory(mine, project);
38262
38368
  }
38263
38369
  currentRun() {
38264
38370
  return this.activeProcess ?? this.adoptRecordedRun();
@@ -38389,6 +38495,11 @@ class GodotServer {
38389
38495
  async handleRescanFilesystem(args) {
38390
38496
  const timeoutMs = readPositiveNumber(args, "timeoutMs") ?? 30000;
38391
38497
  const started = Date.now();
38498
+ const projectPath = typeof args["projectPath"] === "string" ? args["projectPath"] : "";
38499
+ const before = projectPath === "" ? null : cachedClasses(projectPath);
38500
+ const writtenBefore = projectPath === "" ? null : cacheWrittenAt(projectPath);
38501
+ const blind = projectPath === "" ? { unseen: [] } : await this.classesTheEditorCannotSee(args);
38502
+ const atRisk = before === null ? [] : blind.unseen.filter((one) => before.has(one.className));
38392
38503
  const first = await this.handleViaBridge("rescan_filesystem", args);
38393
38504
  if (first.isError) {
38394
38505
  return first;
@@ -38397,17 +38508,27 @@ class GodotServer {
38397
38508
  while (busy && Date.now() - started < timeoutMs) {
38398
38509
  await new Promise((settle) => setTimeout(settle, 100));
38399
38510
  const status = asParams(await this.godotBridge.invokeTool("rescan_filesystem", { ...args, statusOnly: true }));
38400
- busy = Boolean(status["scanning"]) || Boolean(status["importing"]);
38511
+ const pending = Boolean(status["pending"]) && Date.now() - started < SCAN_START_MS;
38512
+ busy = Boolean(status["scanning"]) || Boolean(status["importing"]) || pending;
38513
+ }
38514
+ if (!busy && atRisk.length > 0) {
38515
+ const until = Date.now() + CACHE_WRITE_MS;
38516
+ while (Date.now() < until && cacheWrittenAt(projectPath) === writtenBefore) {
38517
+ await new Promise((settle) => setTimeout(settle, 100));
38518
+ }
38401
38519
  }
38520
+ const after = projectPath === "" || busy ? null : cachedClasses(projectPath);
38521
+ const lost = before === null || after === null ? [] : [...before.keys()].filter((name) => !after.has(name));
38402
38522
  const checked = busy ? { unseen: [] } : await this.classesTheEditorCannotSee(args);
38403
38523
  const unseen = checked.unseen;
38404
38524
  return this.jsonTextResponse({
38405
- ok: !busy && unseen.length === 0 && checked.unchecked === undefined,
38525
+ ok: !busy && unseen.length === 0 && lost.length === 0 && checked.unchecked === undefined,
38406
38526
  stillWorking: busy,
38407
38527
  waitedMs: Date.now() - started,
38408
38528
  unseenByEditor: unseen.length > 0 ? unseen : undefined,
38529
+ cacheLost: lost.length > 0 ? lost : undefined,
38409
38530
  classesUnchecked: checked.unchecked,
38410
- note: busy ? "The editor was still scanning or importing when the wait ran out, so new files may not be visible yet." : unseen.length > 0 ? "The scan finished and these classes are still not in the list the editor resolves against, so every use of them reads as an unknown identifier. Its walk skips a file another engine has already imported. Change the declaring script and rescan, or restart the editor with editor_launch restart." : undefined
38531
+ note: busy ? "The editor was still scanning or importing when the wait ran out, so new files may not be visible yet." : lost.length > 0 ? "The scan wrote the class cache from the list this editor is holding, and that list is shorter than the file was: these classes were in it before the scan and are not now. Every engine that reads the cache next, including a test run, will report them as unknown identifiers in files nobody touched. Run project_import refresh_classes to put them back, and do not rescan again until the editor has been restarted with editor_launch restart, because it will write the same short list over it." : unseen.length > 0 ? "The scan finished and these classes are still not in the list the editor resolves against, so every use of them reads as an unknown identifier. Its walk skips a file another engine has already imported. Change the declaring script and rescan, or restart the editor with editor_launch restart." : undefined
38411
38532
  });
38412
38533
  }
38413
38534
  async alsoSayWhatTheEditorCannotSee(answered, args) {
@@ -38432,7 +38553,7 @@ class GodotServer {
38432
38553
  return { unseen: [], unchecked: "no projectPath, so there was nothing to read the cache from" };
38433
38554
  }
38434
38555
  const open = this.godotBridge.getStatus().projectPath;
38435
- if (open === undefined || !samePath(open, projectPath)) {
38556
+ if (open === undefined || !isSameDirectory(open, projectPath)) {
38436
38557
  return {
38437
38558
  unseen: [],
38438
38559
  unchecked: `the connected editor is open on ${open ?? "a project it did not name"}, not this one`
@@ -38460,6 +38581,14 @@ class GodotServer {
38460
38581
  "editor_status says whether the bridge is up and what it is listening on"
38461
38582
  ]);
38462
38583
  }
38584
+ const open = this.godotBridge.getStatus().projectPath;
38585
+ const meant = readNonEmptyString(args, "projectPath");
38586
+ if (open !== undefined && open !== "" && meant !== undefined && !isSameDirectory(open, meant)) {
38587
+ return this.createErrorResponse(`This call names ${meant}, and the editor on this bridge has ${open} open.`, [
38588
+ "editor_status names the project this server's editor is showing",
38589
+ "One server serves one editor: start a second server for the other project"
38590
+ ]);
38591
+ }
38463
38592
  try {
38464
38593
  return this.jsonTextResponse(markIfStale(await this.godotBridge.invokeTool(toolName, args), this.godotBridge.getStatus().addonVersion, SERVER_VERSION));
38465
38594
  } catch (error) {
@@ -38478,8 +38607,8 @@ class GodotServer {
38478
38607
  return this.createErrorResponse(choice.problem);
38479
38608
  }
38480
38609
  const expectsScreenshot = command === "capture_screenshot" || command === "capture_viewport";
38481
- const screenshotDir = expectsScreenshot ? mkdtempSync3(join16(tmpdir5(), "gdharness-runtime-screenshot-")) : null;
38482
- const screenshotPath = screenshotDir ? join16(screenshotDir, "capture.png") : null;
38610
+ const screenshotDir = expectsScreenshot ? mkdtempSync3(join17(tmpdir5(), "gdharness-runtime-screenshot-")) : null;
38611
+ const screenshotPath = screenshotDir ? join17(screenshotDir, "capture.png") : null;
38483
38612
  try {
38484
38613
  const reply = await runtimeRequest(choice.endpoint, command, screenshotPath ? { ...params, output_path: screenshotPath } : params, timeoutMs);
38485
38614
  if (!reply.ok) {
@@ -38500,7 +38629,7 @@ class GodotServer {
38500
38629
  return {
38501
38630
  content: [
38502
38631
  { type: "text", text: `Screenshot captured: ${dimensions}` },
38503
- { type: "image", data: readFileSync14(screenshotPath).toString("base64"), mimeType: "image/png" }
38632
+ { type: "image", data: readFileSync15(screenshotPath).toString("base64"), mimeType: "image/png" }
38504
38633
  ]
38505
38634
  };
38506
38635
  } finally {
@@ -38568,13 +38697,14 @@ async function runGodotServer() {
38568
38697
  const server = new GodotServer;
38569
38698
  await server.run();
38570
38699
  }
38571
- var UPDATE_NOTICE_EVERY = 500, FEEDBACK_NOTICE_EVERY = 250, run6, __dirname2, EDITOR_RESTART_TIMEOUT_MS = 90000, SLOWEST_FRAME_RATE = 20, VALUE_NAMES, BRIDGE_RETRY_MS = 2000, SUCCESSOR_CHECK_MS = 1e4, WAIT_FOR_RUN_MS = 600000, RUN_POLL_MS = 250, PATH_SOLUTIONS, PROJECT_FILE_ARGUMENTS, DEBUG_STATE_CALLS, PROJECT_INFO_SECTIONS, RUNNER_DIRECTORY = "addons/gdUnit4/";
38700
+ var UPDATE_NOTICE_EVERY = 500, FEEDBACK_NOTICE_EVERY = 250, run6, __dirname2, EDITOR_RESTART_TIMEOUT_MS = 90000, SLOWEST_FRAME_RATE = 20, VALUE_NAMES, BRIDGE_RETRY_MS = 2000, SUCCESSOR_CHECK_MS = 1e4, CACHE_WRITE_MS = 2000, SCAN_START_MS = 2000, WAIT_FOR_RUN_MS = 600000, RUN_POLL_MS = 250, PATH_SOLUTIONS, PROJECT_FILE_ARGUMENTS, DEBUG_STATE_CALLS, PROJECT_INFO_SECTIONS2, RUNNER_DIRECTORY = "addons/gdUnit4/";
38572
38701
  var init_server3 = __esm(() => {
38573
38702
  init_mcp();
38574
38703
  init_stdio2();
38575
38704
  init_types();
38576
38705
  init_bridge_announce();
38577
38706
  init_class_cache();
38707
+ init_config_pin();
38578
38708
  init_dap_client();
38579
38709
  init_errors();
38580
38710
  init_game_log();
@@ -38621,7 +38751,7 @@ var init_server3 = __esm(() => {
38621
38751
  output: "dap_get_output",
38622
38752
  variables: "dap_get_variables"
38623
38753
  });
38624
- PROJECT_INFO_SECTIONS = dictionary({
38754
+ PROJECT_INFO_SECTIONS2 = dictionary({
38625
38755
  autoloads: { operation: "list_autoloads", params: () => ({}) },
38626
38756
  plugins: { operation: "list_plugins", params: () => ({}) },
38627
38757
  export_presets: { operation: "list_export_presets", params: () => ({}) },
@@ -38635,8 +38765,8 @@ var init_server3 = __esm(() => {
38635
38765
  });
38636
38766
 
38637
38767
  // src/cli.ts
38638
- import { existsSync as existsSync14 } from "fs";
38639
- import { join as join17, resolve as resolve5 } from "path";
38768
+ import { existsSync as existsSync15 } from "fs";
38769
+ import { join as join18, resolve as resolve5 } from "path";
38640
38770
 
38641
38771
  // src/errors.ts
38642
38772
  class Refusal extends Error {
@@ -39973,6 +40103,11 @@ function headlessSection() {
39973
40103
  "",
39974
40104
  "Everything else needs the editor (`editor_*`, `debug_*`, `scene_*`, `resource_*`) or a running",
39975
40105
  "game (`runtime_*`).",
40106
+ "",
40107
+ 'One of these can be pointed the other way: `project_settings get` takes `from: "editor"` to ask',
40108
+ "the open editor instead of the file, which is a different reading and costs no engine start. A",
40109
+ "gate leaves it alone, because there is no editor on a build machine and the call is refused",
40110
+ "rather than answered from the file.",
39976
40111
  ""
39977
40112
  ];
39978
40113
  }
@@ -40241,7 +40376,7 @@ class UsageError extends Refusal {
40241
40376
  function projectArgument(at) {
40242
40377
  const named = args.slice(at).find((value) => !value.startsWith("--"));
40243
40378
  const projectPath = resolve5(named ?? ".");
40244
- if (!existsSync14(join17(projectPath, "project.godot"))) {
40379
+ if (!existsSync15(join18(projectPath, "project.godot"))) {
40245
40380
  throw new UsageError(`Not a Godot project: ${projectPath} holds no project.godot.`);
40246
40381
  }
40247
40382
  return projectPath;
@@ -40275,7 +40410,7 @@ async function registerRuntime(godot, projectPath) {
40275
40410
  return;
40276
40411
  }
40277
40412
  console.log(`${RUNTIME_AUTOLOAD.name} autoload left at ${named}, which this project registers itself`);
40278
- if (!existsSync14(join17(projectPath, (named ?? "").replace(/^res:\/\//, "")))) {
40413
+ if (!existsSync15(join18(projectPath, (named ?? "").replace(/^res:\/\//, "")))) {
40279
40414
  console.log(` that file is not in this project, so it boots with a missing script: restore it, or point the entry at res://${RUNTIME_AUTOLOAD.path}`);
40280
40415
  }
40281
40416
  }
@@ -40407,7 +40542,7 @@ async function setup() {
40407
40542
  console.log("no harness configured; name one yourself, and gdharness harnesses lists them all");
40408
40543
  }
40409
40544
  if (!args.includes("--no-skill")) {
40410
- const directories = skillDirectories(chosen.harnesses, projectPath, existsSync14);
40545
+ const directories = skillDirectories(chosen.harnesses, projectPath, existsSync15);
40411
40546
  for (const written of writeSkill(directories, getLocalVersion())) {
40412
40547
  console.log(`skill: ${written.replaced ? "replaced" : "written"} ${written.path}`);
40413
40548
  }
@@ -40496,7 +40631,7 @@ async function upgrade() {
40496
40631
  if (already.length === 0) {
40497
40632
  console.log("no harness config names gdharness, so none was re-pinned");
40498
40633
  }
40499
- for (const written of writeSkill(skillDirectories(already, projectPath, existsSync14), version)) {
40634
+ for (const written of writeSkill(skillDirectories(already, projectPath, existsSync15), version)) {
40500
40635
  console.log(`skill: ${written.replaced ? "replaced" : "written"} ${written.path}`);
40501
40636
  }
40502
40637
  console.log(`