gdharness 1.1.37 → 1.1.39

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.
Files changed (3) hide show
  1. package/build/cli.js +150 -57
  2. package/build/index.js +146 -58
  3. package/package.json +1 -1
package/build/cli.js CHANGED
@@ -16541,6 +16541,16 @@ var init_types = __esm(() => {
16541
16541
  };
16542
16542
  });
16543
16543
 
16544
+ // src/answer-json.ts
16545
+ function answerJson(value) {
16546
+ return JSON.stringify(value, null, 2).replace(SCALAR_ARRAY, (_whole, items) => `[${items.split(/,\n\s+/).join(", ")}]`);
16547
+ }
16548
+ var SCALAR, SCALAR_ARRAY;
16549
+ var init_answer_json = __esm(() => {
16550
+ SCALAR = String.raw`(?:-?\d+(?:\.\d+)?(?:[eE][+-]?\d+)?|true|false|null)`;
16551
+ SCALAR_ARRAY = new RegExp(String.raw`\[\n\s+(${SCALAR}(?:,\n\s+${SCALAR})*)\n\s*\]`, "g");
16552
+ });
16553
+
16544
16554
  // src/resources.ts
16545
16555
  import { readFileSync as readFileSync8, realpathSync as realpathSync3 } from "node:fs";
16546
16556
  import { extname, resolve as resolve3 } from "node:path";
@@ -16727,7 +16737,7 @@ function readResourceText(uri, getProjectPath) {
16727
16737
  const parsedProject = parseProjectGodot(rawProject);
16728
16738
  return {
16729
16739
  mimeType: "application/json",
16730
- text: JSON.stringify(parsedProject, null, 2)
16740
+ text: answerJson(parsedProject)
16731
16741
  };
16732
16742
  }
16733
16743
  const filePath = resolveProjectFile(projectPath, parsedUri.resourcePath);
@@ -16770,6 +16780,7 @@ function setupResourceHandlers(mcp, getProjectPath) {
16770
16780
  var STATIC_RESOURCES, RESOURCE_TEMPLATES, RESOURCE_COUNT;
16771
16781
  var init_resources = __esm(() => {
16772
16782
  init_types();
16783
+ init_answer_json();
16773
16784
  init_errors();
16774
16785
  init_paths();
16775
16786
  STATIC_RESOURCES = [
@@ -17758,7 +17769,7 @@ var init_tool_definitions = __esm(() => {
17758
17769
  },
17759
17770
  {
17760
17771
  name: "script_edit",
17761
- description: "Creates a GDScript file, or adds functions, variables and signals to one. Every declaration written carries a type. Both load what they wrote under the project's own warning settings and answer with parses; the engine's reasons for a refusal come back under engine_messages. create answers with the extends and class_name the engine read, and a script declaring a class_name is put in the project's class list, with registered saying whether it is there. modify makes every addition or none, and answers with the line each one is on in the file written. rename renames a class or a member wherever the engine uses it and writes every file or none. A new name already taken is refused with nothing written, a member's included when a class inheriting it already uses the name in code, as a local or a parameter. changed lists each file with its changed lines as they now read, and leftAlone lists every other whole-word occurrence of the old name with what the tokenizer makes of it (code, string, comment, doc, nodePath, scene, text, or ignored for a file under a .gdignore directory, which is never changed) and, where it is not plain, why: a string that is the old name whole, such as find_children's type or a call's method name, which nothing renames or compiles and which is listed first; a node named like the class; a call on a value whose type the analyser could not tell. A member rename answers under languageServer how many requests it made and how long they took. A comment or a line of a text file with nothing more to say is given under leftAloneInProse by file and line instead, and code that is certainly another symbol of the same name, a bare name in a file where nothing inherits the member or one the language server resolved to another declaration, under leftAloneOtherSymbols by file, why and line. Read both before calling the rename done; nothing in them was changed. Afterwards an open editor scans the changed files and reloads every changed script, which is also the compile check, and its answer comes back under editor; with no editor the class cache is rebuilt and the changed scripts are compiled in a fresh engine, under classCache and compiled. written is true once the files are written, and ok is false when what came after the write failed: the files are left changed, and note says so.",
17772
+ description: "Creates a GDScript file, or adds functions, variables and signals to one. Every declaration written carries a type. Both load what they wrote under the project's own warning settings and answer with parses; the engine's reasons for a refusal come back under engine_messages. create answers with the extends and class_name the engine read, and a script declaring a class_name is put in the project's class list, with registered saying whether it is there. modify makes every addition or none, and answers with the line each one is on in the file written. rename renames a class or a member wherever the engine uses it and writes every file or none. A new name already taken is refused with nothing written, a member's included when a class inheriting it already uses the name in code, as a local or a parameter. changed lists each file with its changed lines as they now read, and leftAlone lists every other whole-word occurrence of the old name with what the tokenizer makes of it (code, string, comment, doc, nodePath, scene, text, or ignored for a file under a .gdignore directory, which is never changed) and, where it is not plain, why: a string that is the old name whole, such as find_children's type or a call's method name, which nothing renames or compiles and which is listed first; a node named like the class; a call on a value whose type the analyser could not tell. A member rename answers under languageServer how many requests it made and how long they took. A comment, documentation line, string or text file line with nothing more to say is given under leftAloneInProse by file, kind and line instead, and code that is certainly another symbol of the same name, a bare name in a file where nothing inherits the member or one the language server resolved to another declaration, under leftAloneOtherSymbols as each why with the files and lines it covers. summary.leftAlone counts each of the three lists beside the total. When those two grouped lists run past 300 lines, they are written with each line's text to a file under the project's .godot directory instead, and leftAloneDetail names the file and counts what is in it. Read both before calling the rename done; nothing in them was changed. Afterwards an open editor scans the changed files and reloads every changed script, which is also the compile check, and its answer comes back under editor; with no editor the class cache is rebuilt and the changed scripts are compiled in a fresh engine, under classCache and compiled. written is true once the files are written, and ok is false when what came after the write failed: the files are left changed, and note says so.",
17762
17773
  parameters: {
17763
17774
  projectPath: PROJECT_PATH,
17764
17775
  scriptPath: SCRIPT_PATH,
@@ -34666,7 +34677,7 @@ async function handleDAPTool(client, toolName, args) {
34666
34677
  }
34667
34678
  const result = await client.setBreakpoint(safeArgs.scriptPath, safeArgs.line);
34668
34679
  return {
34669
- content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
34680
+ content: [{ type: "text", text: answerJson(result) }]
34670
34681
  };
34671
34682
  }
34672
34683
  case "dap_remove_breakpoint": {
@@ -34675,7 +34686,7 @@ async function handleDAPTool(client, toolName, args) {
34675
34686
  }
34676
34687
  const result = await client.removeBreakpoint(safeArgs.scriptPath, safeArgs.line);
34677
34688
  return {
34678
- content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
34689
+ content: [{ type: "text", text: answerJson(result) }]
34679
34690
  };
34680
34691
  }
34681
34692
  case "dap_continue": {
@@ -34690,12 +34701,12 @@ async function handleDAPTool(client, toolName, args) {
34690
34701
  heldAgain,
34691
34702
  note: `The game had not stopped again ${client.stepPatienceMs / 1000}s after the step: the step is still running, it went on without stopping anywhere, or the game was not held when it was asked. debug_state stack answers once it stops.`
34692
34703
  };
34693
- return { content: [{ type: "text", text: JSON.stringify(answer, null, 2) }] };
34704
+ return { content: [{ type: "text", text: answerJson(answer) }] };
34694
34705
  }
34695
34706
  case "dap_get_stack_trace": {
34696
34707
  const stack = await client.getStackTrace();
34697
34708
  return {
34698
- content: [{ type: "text", text: JSON.stringify(stack, null, 2) }]
34709
+ content: [{ type: "text", text: answerJson(stack) }]
34699
34710
  };
34700
34711
  }
34701
34712
  case "dap_get_variables": {
@@ -34708,7 +34719,7 @@ async function handleDAPTool(client, toolName, args) {
34708
34719
  note: `The values of ${missing.join(", ")} did not arrive from the game within ${client.scopesPatienceMs / 1000}s, so they are unknown rather than empty. Ask again.`
34709
34720
  };
34710
34721
  return {
34711
- content: [{ type: "text", text: JSON.stringify(answer, null, 2) }]
34722
+ content: [{ type: "text", text: answerJson(answer) }]
34712
34723
  };
34713
34724
  }
34714
34725
  default:
@@ -34727,6 +34738,7 @@ async function handleDAPTool(client, toolName, args) {
34727
34738
  }
34728
34739
  var DEFAULT_DAP_PORT = 6006, DAP_REQUEST_TIMEOUT_MS = 1e4, SCOPES_TIMEOUT_MS = 20000, STEP_LANDS_WITHIN_MS = 1e4, VARIABLES_POLL_TIMEOUT_MS = 2000, ReadRefusal;
34729
34740
  var init_dap_client = __esm(() => {
34741
+ init_answer_json();
34730
34742
  init_errors();
34731
34743
  init_framing();
34732
34744
  init_ports();
@@ -39341,7 +39353,7 @@ function asToolResponse(payload) {
39341
39353
  content: [
39342
39354
  {
39343
39355
  type: "text",
39344
- text: JSON.stringify(payload, null, 2)
39356
+ text: answerJson(payload)
39345
39357
  }
39346
39358
  ]
39347
39359
  };
@@ -39438,6 +39450,7 @@ async function handleLSPTool(client, toolName, args) {
39438
39450
  }
39439
39451
  var DEFAULT_LSP_PORT = 6005, DIAGNOSTICS_TIMEOUT_MS = 5000, REQUEST_TIMEOUT_MS2 = 1e4, REFERENCES_TIMEOUT_MS = 180000, LSPTimeout, ArgumentRefusal, AnotherProjectsServer;
39440
39452
  var init_lsp_client = __esm(() => {
39453
+ init_answer_json();
39441
39454
  init_errors();
39442
39455
  init_framing();
39443
39456
  init_paths();
@@ -40074,7 +40087,14 @@ class PlanBuilder {
40074
40087
  }
40075
40088
  function isWholeString(text, offset, name) {
40076
40089
  const before = text[offset - 1];
40077
- return (before === '"' || before === "'") && text[offset + name.length] === before;
40090
+ if (before !== '"' && before !== "'" || text[offset + name.length] !== before) {
40091
+ return false;
40092
+ }
40093
+ let after = offset + name.length + 1;
40094
+ while (text[after] === " " || text[after] === "\t") {
40095
+ after += 1;
40096
+ }
40097
+ return text[after] !== ":";
40078
40098
  }
40079
40099
  function declaresItsOwn(script, name, except) {
40080
40100
  return script.occurrences(name).some((occurrence) => occurrence.kind === "code" && occurrence.offset !== except && occurrence.declaredAs !== null && occurrence.declaredAs !== "class_name");
@@ -40536,7 +40556,7 @@ function applyRewrites(rewrites) {
40536
40556
  throw error;
40537
40557
  }
40538
40558
  }
40539
- var DOC_LINK_KINDS, DOC_KIND, WHOLE_CLASS_NAME = "the whole string is the class's name, as find_children's type, a comparison with get_global_name() or a list of accepted type names takes it, and it was not changed: if it names this class, it no longer matches anything after the rename, and no compile says so", WHOLE_MEMBER_NAME = "the whole string is the member's name, as call, connect, has_method, get or set take it, and it was not changed: if it names this member, it no longer finds it after the rename, and no compile says so", NOT_INHERITED = "no class in this file inherits the member, so a bare name here is that class's own local, parameter or member, or a global", RESOLVED_ELSEWHERE = "the language server resolved this to another declaration of the name";
40559
+ var DOC_LINK_KINDS, DOC_KIND, WHOLE_CLASS_NAME = "the whole string is the class's name, as find_children's type, a comparison with get_global_name() or a list of accepted type names takes it, and it was not changed: if it names this class, it no longer matches anything after the rename, and no compile says so", WHOLE_MEMBER_NAME = "the whole string is the member's name, as call, connect, has_method, get, set or a subscript on an object take it, and it was not changed: if it names this member, it no longer finds it after the rename, and no compile says so", NOT_INHERITED = "no class in this file inherits the member, so a bare name here is that class's own local, parameter or member, or a global", RESOLVED_ELSEWHERE = "the language server resolved this to another declaration of the name";
40540
40560
  var init_rename = __esm(() => {
40541
40561
  init_gdscript_source();
40542
40562
  DOC_LINK_KINDS = new Set([
@@ -40560,7 +40580,7 @@ var init_rename = __esm(() => {
40560
40580
  });
40561
40581
 
40562
40582
  // src/rename-symbol.ts
40563
- import { existsSync as existsSync18, realpathSync as realpathSync5 } from "node:fs";
40583
+ import { existsSync as existsSync18, mkdirSync as mkdirSync11, readdirSync as readdirSync8, realpathSync as realpathSync5, rmSync as rmSync9, writeFileSync as writeFileSync13 } from "node:fs";
40564
40584
  import { basename as basename5, dirname as dirname15, join as join21, relative as relative4, resolve as resolve6 } from "node:path";
40565
40585
  function resourcePathOf(projectPath, absolute) {
40566
40586
  const inside = relative4(onDiskSpelling(projectPath), onDiskSpelling(absolute));
@@ -40807,6 +40827,7 @@ async function renameSymbol(request, services) {
40807
40827
  }, plan);
40808
40828
  }
40809
40829
  plan = mentionIgnored(plan, ignoredTexts(projectPath), symbol, moveTo === null ? null : declaringPath);
40830
+ const left = leftAloneReport(plan.mentions);
40810
40831
  const rewrites = rewritesOf(plan, texts, moveTo === null || request.newScriptPath === null ? null : { from: declaringPath, to: moveTo, absoluteTo: request.newScriptPath });
40811
40832
  const graph = new ClassGraph(scripts, declared);
40812
40833
  const rank = (path) => (declaringFiles.includes(path) ? 0 : 1000) + graph.ancestors(`${path}#`).length;
@@ -40817,14 +40838,15 @@ async function renameSymbol(request, services) {
40817
40838
  moved: moveTo === null ? null : { from: declaringPath, to: moveTo },
40818
40839
  scriptsInOrder,
40819
40840
  rewrites,
40841
+ detail: left.detail,
40820
40842
  report: {
40821
40843
  renamed: { what, from: symbol, to: newName, declaredIn: declaringPath, resolvedBy },
40822
40844
  ...took === null ? {} : { languageServer: took },
40823
40845
  ...overrides.length > 0 ? { overridesRenamed: overrides } : {},
40824
40846
  ...moveTo === null ? {} : { moved: { from: declaringPath, to: moveTo } },
40825
40847
  changed: changedReport(plan.edits, rewrites),
40826
- ...leftAloneReport(plan.mentions),
40827
- summary: summaryOf(plan.edits, plan.mentions, rewrites)
40848
+ ...left.lists,
40849
+ summary: summaryOf(plan.edits, plan.mentions, rewrites, left.counts)
40828
40850
  }
40829
40851
  };
40830
40852
  }
@@ -40923,36 +40945,96 @@ function leftAloneReport(mentions) {
40923
40945
  const full = [];
40924
40946
  const prose = new Map;
40925
40947
  const others = new Map;
40926
- const add = (group, line) => {
40927
- if (!group.lines.includes(line)) {
40928
- group.lines.push(line);
40948
+ const counts = { quoted: 0, inProse: 0, otherSymbols: 0 };
40949
+ const add = (lines, line) => {
40950
+ if (!lines.includes(line)) {
40951
+ lines.push(line);
40929
40952
  }
40930
40953
  };
40954
+ const textOf = new Map;
40955
+ for (const mention of mentions) {
40956
+ textOf.set(`${mention.file}:${String(mention.line)}`, mention.text);
40957
+ }
40931
40958
  for (const mention of mentions) {
40932
- if ((mention.kind === "comment" || mention.kind === "text") && mention.why === undefined) {
40959
+ if (PROSE_KINDS.has(mention.kind) && mention.why === undefined) {
40933
40960
  const key = `${mention.file}
40934
40961
  ${mention.kind}`;
40935
40962
  const group = prose.get(key) ?? { file: mention.file, kind: mention.kind, lines: [] };
40936
- add(group, mention.line);
40963
+ add(group.lines, mention.line);
40937
40964
  prose.set(key, group);
40965
+ counts.inProse += 1;
40938
40966
  } else if (mention.kind === "code" && (mention.why === NOT_INHERITED || mention.why === RESOLVED_ELSEWHERE)) {
40939
- const key = `${mention.file}
40940
- ${mention.why}`;
40941
- const group = others.get(key) ?? { file: mention.file, why: mention.why, lines: [] };
40942
- add(group, mention.line);
40943
- others.set(key, group);
40967
+ const files = others.get(mention.why) ?? new Map;
40968
+ const lines = files.get(mention.file) ?? [];
40969
+ add(lines, mention.line);
40970
+ files.set(mention.file, lines);
40971
+ others.set(mention.why, files);
40972
+ counts.otherSymbols += 1;
40944
40973
  } else {
40945
40974
  full.push(mention);
40975
+ counts.quoted += 1;
40946
40976
  }
40947
40977
  }
40948
40978
  const loudFirst = (mention) => mention.kind === "string" && mention.why !== undefined ? 0 : 1;
40979
+ const leftAlone = full.sort((a, b) => loudFirst(a) - loudFirst(b));
40980
+ const inProse = [...prose.values()];
40981
+ const otherSymbols = [...others.entries()].map(([why, files]) => ({
40982
+ why,
40983
+ files: [...files.entries()].map(([file, lines]) => ({ file, lines }))
40984
+ }));
40985
+ const grouped = inProse.reduce((sum, group) => sum + group.lines.length, 0) + otherSymbols.reduce((sum, reason) => sum + reason.files.reduce((n, one) => n + one.lines.length, 0), 0);
40986
+ if (grouped <= LEFT_ALONE_ANSWER_LINES) {
40987
+ return {
40988
+ lists: {
40989
+ leftAlone,
40990
+ ...inProse.length > 0 ? { leftAloneInProse: inProse } : {},
40991
+ ...otherSymbols.length > 0 ? { leftAloneOtherSymbols: otherSymbols } : {}
40992
+ },
40993
+ counts,
40994
+ detail: null
40995
+ };
40996
+ }
40997
+ const withText = (file, lines) => lines.map((line) => ({ line, text: textOf.get(`${file}:${String(line)}`) ?? "" }));
40949
40998
  return {
40950
- leftAlone: full.sort((a, b) => loudFirst(a) - loudFirst(b)),
40951
- ...prose.size > 0 ? { leftAloneInProse: [...prose.values()] } : {},
40952
- ...others.size > 0 ? { leftAloneOtherSymbols: [...others.values()] } : {}
40999
+ lists: {
41000
+ leftAlone,
41001
+ leftAloneDetail: {
41002
+ inProse: { files: new Set(inProse.map((group) => group.file)).size, occurrences: counts.inProse },
41003
+ otherSymbols: otherSymbols.map((reason) => ({
41004
+ why: reason.why,
41005
+ files: reason.files.length,
41006
+ occurrences: reason.files.reduce((n, one) => n + one.lines.length, 0)
41007
+ }))
41008
+ }
41009
+ },
41010
+ counts,
41011
+ detail: {
41012
+ leftAloneInProse: inProse.map((group) => ({
41013
+ file: group.file,
41014
+ kind: group.kind,
41015
+ lines: withText(group.file, group.lines)
41016
+ })),
41017
+ leftAloneOtherSymbols: otherSymbols.map((reason) => ({
41018
+ why: reason.why,
41019
+ files: reason.files.map((one) => ({ file: one.file, lines: withText(one.file, one.lines) }))
41020
+ }))
41021
+ }
40953
41022
  };
40954
41023
  }
40955
- function summaryOf(edits, mentions, rewrites) {
41024
+ function writeLeftAloneDetail(projectPath, symbol, detail) {
41025
+ const directory = join21(projectPath, ".godot", "gdharness-renames");
41026
+ mkdirSync11(directory, { recursive: true });
41027
+ detailWrites += 1;
41028
+ const path = join21(directory, `${String(Date.now())}-${String(detailWrites).padStart(6, "0")}-${String(process.pid)}-${symbol}.json`);
41029
+ writeFileSync13(path, `${JSON.stringify(detail, null, 2)}
41030
+ `, "utf8");
41031
+ const kept = readdirSync8(directory).filter((name) => name.endsWith(".json")).sort().reverse();
41032
+ for (const old of kept.slice(10)) {
41033
+ rmSync9(join21(directory, old), { force: true });
41034
+ }
41035
+ return path;
41036
+ }
41037
+ function summaryOf(edits, mentions, rewrites, left) {
40956
41038
  const count = (values) => {
40957
41039
  const counted = {};
40958
41040
  for (const value of values) {
@@ -40964,11 +41046,11 @@ function summaryOf(edits, mentions, rewrites) {
40964
41046
  filesChanged: rewrites.length,
40965
41047
  edits: edits.length,
40966
41048
  editsByKind: count(edits.map((edit) => edit.kind)),
40967
- leftAlone: mentions.length,
41049
+ leftAlone: { total: mentions.length, ...left },
40968
41050
  leftAloneByKind: count(mentions.map((mention) => mention.kind))
40969
41051
  };
40970
41052
  }
40971
- var WHAT;
41053
+ var WHAT, LEFT_ALONE_ANSWER_LINES = 300, detailWrites = 0, PROSE_KINDS;
40972
41054
  var init_rename_symbol = __esm(() => {
40973
41055
  init_gdscript_source();
40974
41056
  init_lsp_client();
@@ -40984,10 +41066,11 @@ var init_rename_symbol = __esm(() => {
40984
41066
  class: "inner class",
40985
41067
  enumValue: "enum value"
40986
41068
  };
41069
+ PROSE_KINDS = new Set(["comment", "doc", "string", "text"]);
40987
41070
  });
40988
41071
 
40989
41072
  // src/restart-note.ts
40990
- import { mkdirSync as mkdirSync11, readFileSync as readFileSync19, rmSync as rmSync9, writeFileSync as writeFileSync13 } from "node:fs";
41073
+ import { mkdirSync as mkdirSync12, readFileSync as readFileSync19, rmSync as rmSync10, writeFileSync as writeFileSync14 } from "node:fs";
40991
41074
  import { dirname as dirname16, join as join22 } from "node:path";
40992
41075
  function restartNotePath(projectPath) {
40993
41076
  return join22(projectPath, ".godot", "gdharness-restart.json");
@@ -40995,8 +41078,8 @@ function restartNotePath(projectPath) {
40995
41078
  function noteRestartBegun(note) {
40996
41079
  const path = restartNotePath(note.projectPath);
40997
41080
  try {
40998
- mkdirSync11(dirname16(path), { recursive: true });
40999
- writeFileSync13(path, `${JSON.stringify(note, null, 2)}
41081
+ mkdirSync12(dirname16(path), { recursive: true });
41082
+ writeFileSync14(path, `${JSON.stringify(note, null, 2)}
41000
41083
  `, "utf8");
41001
41084
  } catch (error) {
41002
41085
  console.error(`[SERVER] Could not record the restart at ${path}: ${errorMessage(error)}`);
@@ -41019,7 +41102,7 @@ function restartOwed(projectPath) {
41019
41102
  }
41020
41103
  function restartSettled(projectPath) {
41021
41104
  try {
41022
- rmSync9(restartNotePath(projectPath), { force: true });
41105
+ rmSync10(restartNotePath(projectPath), { force: true });
41023
41106
  } catch (error) {
41024
41107
  console.error(`[SERVER] Could not clear the restart note: ${errorMessage(error)}`);
41025
41108
  }
@@ -41032,14 +41115,14 @@ var init_restart_note = __esm(() => {
41032
41115
  import { execFile as execFile4 } from "node:child_process";
41033
41116
  import { createHash as createHash4 } from "node:crypto";
41034
41117
  import {
41035
- mkdirSync as mkdirSync12,
41118
+ mkdirSync as mkdirSync13,
41036
41119
  openSync as openSync2,
41037
- readdirSync as readdirSync8,
41120
+ readdirSync as readdirSync9,
41038
41121
  readFileSync as readFileSync20,
41039
41122
  realpathSync as realpathSync6,
41040
- rmSync as rmSync10,
41123
+ rmSync as rmSync11,
41041
41124
  statSync as statSync8,
41042
- writeFileSync as writeFileSync14
41125
+ writeFileSync as writeFileSync15
41043
41126
  } from "node:fs";
41044
41127
  import { basename as basename6, dirname as dirname17, join as join23, resolve as resolve7 } from "node:path";
41045
41128
  import { promisify as promisify4 } from "node:util";
@@ -41071,7 +41154,7 @@ function runRecordPath(projectPath, directory = runtimeDirectory()) {
41071
41154
  return join23(directory, "runs", `note-${projectKey(projectPath)}.json`);
41072
41155
  }
41073
41156
  function openTranscript(startedAt) {
41074
- mkdirSync12(runsDirectory(), { recursive: true });
41157
+ mkdirSync13(runsDirectory(), { recursive: true });
41075
41158
  const path = join23(runsDirectory(), `run-${startedAt}.log`);
41076
41159
  return { path, fd: openSync2(path, "a") };
41077
41160
  }
@@ -41082,8 +41165,8 @@ function editorNotePath(projectPath, directory = runtimeDirectory()) {
41082
41165
  return join23(directory, "runs", `editor-${projectKey(projectPath)}.json`);
41083
41166
  }
41084
41167
  function writeEditorRunNote(note) {
41085
- mkdirSync12(runsDirectory(), { recursive: true });
41086
- writeFileSync14(editorNotePath(note.projectPath), JSON.stringify(note, null, 2), "utf8");
41168
+ mkdirSync13(runsDirectory(), { recursive: true });
41169
+ writeFileSync15(editorNotePath(note.projectPath), JSON.stringify(note, null, 2), "utf8");
41087
41170
  }
41088
41171
  function readEditorRunNote(projectPath) {
41089
41172
  for (const directory of runtimeDirectories()) {
@@ -41133,7 +41216,7 @@ function everyRunRecord() {
41133
41216
  for (const directory of runtimeDirectories()) {
41134
41217
  let entries;
41135
41218
  try {
41136
- entries = readdirSync8(join23(directory, "runs"));
41219
+ entries = readdirSync9(join23(directory, "runs"));
41137
41220
  } catch {
41138
41221
  continue;
41139
41222
  }
@@ -41352,7 +41435,7 @@ function clearRunRecord(projectPath) {
41352
41435
  const ours = sharedRecord !== null && sharedRecord.projectPath !== "" && isSameDirectory(sharedRecord.projectPath, projectPath);
41353
41436
  for (const path of [runRecordPath(projectPath, directory), ...ours ? [shared] : []]) {
41354
41437
  try {
41355
- rmSync10(path, { force: true });
41438
+ rmSync11(path, { force: true });
41356
41439
  } catch {}
41357
41440
  }
41358
41441
  }
@@ -41369,7 +41452,7 @@ function sweepTranscripts(keepMs = 24 * 60 * 60 * 1000, now = Date.now()) {
41369
41452
  const kept = new Set(everyRunRecord().map((record) => record.transcript));
41370
41453
  let entries;
41371
41454
  try {
41372
- entries = readdirSync8(directory);
41455
+ entries = readdirSync9(directory);
41373
41456
  } catch {
41374
41457
  return;
41375
41458
  }
@@ -41382,7 +41465,7 @@ function sweepTranscripts(keepMs = 24 * 60 * 60 * 1000, now = Date.now()) {
41382
41465
  continue;
41383
41466
  }
41384
41467
  try {
41385
- rmSync10(path);
41468
+ rmSync11(path);
41386
41469
  } catch {}
41387
41470
  }
41388
41471
  }
@@ -41406,9 +41489,9 @@ import {
41406
41489
  copyFileSync,
41407
41490
  existsSync as existsSync19,
41408
41491
  fstatSync,
41409
- mkdirSync as mkdirSync13,
41492
+ mkdirSync as mkdirSync14,
41410
41493
  openSync as openSync3,
41411
- readdirSync as readdirSync9,
41494
+ readdirSync as readdirSync10,
41412
41495
  readFileSync as readFileSync21,
41413
41496
  readSync as readSync2,
41414
41497
  rmdirSync as rmdirSync2,
@@ -41862,7 +41945,7 @@ function exportAnswer(ending, asked, before, after) {
41862
41945
  ...problems.omitted === 0 ? {} : { entriesOmitted: problems.omitted }
41863
41946
  };
41864
41947
  if (verdict.exported) {
41865
- return { content: [{ type: "text", text: JSON.stringify(verdict, null, 2) }] };
41948
+ return { content: [{ type: "text", text: answerJson(verdict) }] };
41866
41949
  }
41867
41950
  const stale = !written && after !== null ? ` The ${asked.outputPath} there is from before this export, which did not write it.` : "";
41868
41951
  const why = ending.exitSignal === null ? stale : ` The engine was ${howItEnded(ending)}, so it has no exit code.${stale}`;
@@ -41872,7 +41955,7 @@ function exportAnswer(ending, asked, before, after) {
41872
41955
  type: "text",
41873
41956
  text: `Export with preset '${asked.preset}' did not produce ${asked.outputPath}.${why}`
41874
41957
  },
41875
- { type: "text", text: JSON.stringify(verdict, null, 2) }
41958
+ { type: "text", text: answerJson(verdict) }
41876
41959
  ],
41877
41960
  isError: true
41878
41961
  };
@@ -41976,7 +42059,7 @@ function elsewhereIn(projectPath, asked) {
41976
42059
  const root = (wanted.split("/")[0] ?? "").toLowerCase();
41977
42060
  let near = [];
41978
42061
  try {
41979
- near = readdirSync9(projectPath, { withFileTypes: true }).filter((entry) => entry.isDirectory() && entry.name.toLowerCase() !== root).map((entry) => entry.name).filter((name) => name.toLowerCase().startsWith("test") || root.startsWith(name.toLowerCase()));
42062
+ near = readdirSync10(projectPath, { withFileTypes: true }).filter((entry) => entry.isDirectory() && entry.name.toLowerCase() !== root).map((entry) => entry.name).filter((name) => name.toLowerCase().startsWith("test") || root.startsWith(name.toLowerCase()));
41980
42063
  } catch {
41981
42064
  return "";
41982
42065
  }
@@ -42078,7 +42161,7 @@ class GodotServer {
42078
42161
  return response;
42079
42162
  }
42080
42163
  jsonTextResponse(payload) {
42081
- return { content: [{ type: "text", text: JSON.stringify(payload, null, 2) }] };
42164
+ return { content: [{ type: "text", text: answerJson(payload) }] };
42082
42165
  }
42083
42166
  async run() {
42084
42167
  const located = await this.locator.find();
@@ -42411,7 +42494,7 @@ class GodotServer {
42411
42494
  saying(answer, block) {
42412
42495
  return {
42413
42496
  ...answer,
42414
- content: [...answer.content, { type: "text", text: JSON.stringify(block, null, 2) }]
42497
+ content: [...answer.content, { type: "text", text: answerJson(block) }]
42415
42498
  };
42416
42499
  }
42417
42500
  projectHasMovedOn() {
@@ -43384,7 +43467,7 @@ class GodotServer {
43384
43467
  if (!outcome.ok) {
43385
43468
  const response = this.createErrorResponse(outcome.message);
43386
43469
  if (Object.keys(extras).length > 0) {
43387
- response.content.push({ type: "text", text: JSON.stringify(extras, null, 2) });
43470
+ response.content.push({ type: "text", text: answerJson(extras) });
43388
43471
  }
43389
43472
  return response;
43390
43473
  }
@@ -43462,7 +43545,7 @@ class GodotServer {
43462
43545
  }
43463
43546
  const folder = dirname18(output.absolutePath);
43464
43547
  try {
43465
- mkdirSync13(folder, { recursive: true });
43548
+ mkdirSync14(folder, { recursive: true });
43466
43549
  } catch (error) {
43467
43550
  return this.createErrorResponse(`Cannot create ${dirname18(output.relativePath)} to export into: ${errorMessage(error)}`);
43468
43551
  }
@@ -43573,7 +43656,7 @@ class GodotServer {
43573
43656
  let report = null;
43574
43657
  let reportProblem = null;
43575
43658
  try {
43576
- const written = existsSync19(reportsDir) ? readdirSync9(reportsDir).filter((name) => name.startsWith("report_")).sort((a, b) => Number(a.slice("report_".length)) - Number(b.slice("report_".length))) : [];
43659
+ const written = existsSync19(reportsDir) ? readdirSync10(reportsDir).filter((name) => name.startsWith("report_")).sort((a, b) => Number(a.slice("report_".length)) - Number(b.slice("report_".length))) : [];
43577
43660
  const newest = written.at(-1);
43578
43661
  if (newest !== undefined) {
43579
43662
  report = parseJUnit(readFileSync21(join24(reportsDir, newest, "results.xml"), "utf8"));
@@ -46084,6 +46167,15 @@ class GodotServer {
46084
46167
  const said = outcome.conflicts === undefined ? "" : ` ${outcome.conflicts.join("; ")}.`;
46085
46168
  return this.createErrorResponse(`${outcome.reason}${said}`, outcome.advice ?? []);
46086
46169
  }
46170
+ if (outcome.detail !== null) {
46171
+ const file = writeLeftAloneDetail(projectPath, symbol, outcome.detail);
46172
+ const folded = asParams(outcome.report["leftAloneDetail"]);
46173
+ outcome.report["leftAloneDetail"] = {
46174
+ file,
46175
+ note: `Too many lines were left alone to list here, and none of them is a use: every comment, documentation line, string and text file line that only shares the word, and every other symbol of the same name, is in ${file} with its text.`,
46176
+ ...folded
46177
+ };
46178
+ }
46087
46179
  if (readBoolean(args, "preview") === true) {
46088
46180
  return this.jsonTextResponse({ preview: true, ...outcome.report });
46089
46181
  }
@@ -46547,6 +46639,7 @@ var init_server3 = __esm(() => {
46547
46639
  init_stdio2();
46548
46640
  init_types();
46549
46641
  init_alive();
46642
+ init_answer_json();
46550
46643
  init_boot_note();
46551
46644
  init_breakpoint_note();
46552
46645
  init_bridge_announce();
@@ -47815,9 +47908,9 @@ To rename a class or a member across the project, use \`script_edit rename\` rat
47815
47908
  replacement: it changes the uses the engine resolves, doc links and what scenes record, and lists
47816
47909
  every other occurrence of the word for you to read: a string that is the name whole, such as a
47817
47910
  \`find_children\` type, first under \`leftAlone\` since nothing compiles it, a file under a
47818
- \`.gdignore\` directory there too, a word in a comment or a text file by line under
47819
- \`leftAloneInProse\`, and a local or another class's member of the same name by line under
47820
- \`leftAloneOtherSymbols\`. Pass \`preview\` first on a large rename. If the answer has \`ok: false\`
47911
+ \`.gdignore\` directory there too, a word in a comment, a doc line, a string or a text file by
47912
+ line under \`leftAloneInProse\`, and a local or another class's member of the same name by line
47913
+ under \`leftAloneOtherSymbols\`; a common name's lines go to the file \`leftAloneDetail\` names. Pass \`preview\` first on a large rename. If the answer has \`ok: false\`
47821
47914
  beside \`written: true\`, the files were changed and a script did not compile afterwards.
47822
47915
 
47823
47916
  Editing a file while a game is running does not change the running game. \`auto_reload\` is an
package/build/index.js CHANGED
@@ -10238,9 +10238,9 @@ import {
10238
10238
  copyFileSync,
10239
10239
  existsSync as existsSync16,
10240
10240
  fstatSync,
10241
- mkdirSync as mkdirSync11,
10241
+ mkdirSync as mkdirSync12,
10242
10242
  openSync as openSync3,
10243
- readdirSync as readdirSync8,
10243
+ readdirSync as readdirSync9,
10244
10244
  readFileSync as readFileSync19,
10245
10245
  readSync as readSync2,
10246
10246
  rmdirSync as rmdirSync2,
@@ -24828,6 +24828,13 @@ function alive(pid) {
24828
24828
  }
24829
24829
  }
24830
24830
 
24831
+ // src/answer-json.ts
24832
+ var SCALAR = String.raw`(?:-?\d+(?:\.\d+)?(?:[eE][+-]?\d+)?|true|false|null)`;
24833
+ var SCALAR_ARRAY = new RegExp(String.raw`\[\n\s+(${SCALAR}(?:,\n\s+${SCALAR})*)\n\s*\]`, "g");
24834
+ function answerJson(value) {
24835
+ return JSON.stringify(value, null, 2).replace(SCALAR_ARRAY, (_whole, items) => `[${items.split(/,\n\s+/).join(", ")}]`);
24836
+ }
24837
+
24831
24838
  // src/boot-note.ts
24832
24839
  import { mkdirSync as mkdirSync2, readFileSync as readFileSync4, writeFileSync as writeFileSync2 } from "node:fs";
24833
24840
  import { dirname as dirname2, join as join3 } from "node:path";
@@ -27580,7 +27587,7 @@ async function handleDAPTool(client, toolName, args) {
27580
27587
  }
27581
27588
  const result = await client.setBreakpoint(safeArgs.scriptPath, safeArgs.line);
27582
27589
  return {
27583
- content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
27590
+ content: [{ type: "text", text: answerJson(result) }]
27584
27591
  };
27585
27592
  }
27586
27593
  case "dap_remove_breakpoint": {
@@ -27589,7 +27596,7 @@ async function handleDAPTool(client, toolName, args) {
27589
27596
  }
27590
27597
  const result = await client.removeBreakpoint(safeArgs.scriptPath, safeArgs.line);
27591
27598
  return {
27592
- content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
27599
+ content: [{ type: "text", text: answerJson(result) }]
27593
27600
  };
27594
27601
  }
27595
27602
  case "dap_continue": {
@@ -27604,12 +27611,12 @@ async function handleDAPTool(client, toolName, args) {
27604
27611
  heldAgain,
27605
27612
  note: `The game had not stopped again ${client.stepPatienceMs / 1000}s after the step: the step is still running, it went on without stopping anywhere, or the game was not held when it was asked. debug_state stack answers once it stops.`
27606
27613
  };
27607
- return { content: [{ type: "text", text: JSON.stringify(answer, null, 2) }] };
27614
+ return { content: [{ type: "text", text: answerJson(answer) }] };
27608
27615
  }
27609
27616
  case "dap_get_stack_trace": {
27610
27617
  const stack = await client.getStackTrace();
27611
27618
  return {
27612
- content: [{ type: "text", text: JSON.stringify(stack, null, 2) }]
27619
+ content: [{ type: "text", text: answerJson(stack) }]
27613
27620
  };
27614
27621
  }
27615
27622
  case "dap_get_variables": {
@@ -27622,7 +27629,7 @@ async function handleDAPTool(client, toolName, args) {
27622
27629
  note: `The values of ${missing.join(", ")} did not arrive from the game within ${client.scopesPatienceMs / 1000}s, so they are unknown rather than empty. Ask again.`
27623
27630
  };
27624
27631
  return {
27625
- content: [{ type: "text", text: JSON.stringify(answer, null, 2) }]
27632
+ content: [{ type: "text", text: answerJson(answer) }]
27626
27633
  };
27627
27634
  }
27628
27635
  default:
@@ -30082,7 +30089,7 @@ function asToolResponse(payload) {
30082
30089
  content: [
30083
30090
  {
30084
30091
  type: "text",
30085
- text: JSON.stringify(payload, null, 2)
30092
+ text: answerJson(payload)
30086
30093
  }
30087
30094
  ]
30088
30095
  };
@@ -30798,10 +30805,17 @@ class PlanBuilder {
30798
30805
  }
30799
30806
  function isWholeString(text, offset, name) {
30800
30807
  const before = text[offset - 1];
30801
- return (before === '"' || before === "'") && text[offset + name.length] === before;
30808
+ if (before !== '"' && before !== "'" || text[offset + name.length] !== before) {
30809
+ return false;
30810
+ }
30811
+ let after = offset + name.length + 1;
30812
+ while (text[after] === " " || text[after] === "\t") {
30813
+ after += 1;
30814
+ }
30815
+ return text[after] !== ":";
30802
30816
  }
30803
30817
  var WHOLE_CLASS_NAME = "the whole string is the class's name, as find_children's type, a comparison with get_global_name() or a list of accepted type names takes it, and it was not changed: if it names this class, it no longer matches anything after the rename, and no compile says so";
30804
- var WHOLE_MEMBER_NAME = "the whole string is the member's name, as call, connect, has_method, get or set take it, and it was not changed: if it names this member, it no longer finds it after the rename, and no compile says so";
30818
+ var WHOLE_MEMBER_NAME = "the whole string is the member's name, as call, connect, has_method, get, set or a subscript on an object take it, and it was not changed: if it names this member, it no longer finds it after the rename, and no compile says so";
30805
30819
  function declaresItsOwn(script, name, except) {
30806
30820
  return script.occurrences(name).some((occurrence) => occurrence.kind === "code" && occurrence.offset !== except && occurrence.declaredAs !== null && occurrence.declaredAs !== "class_name");
30807
30821
  }
@@ -31266,7 +31280,7 @@ function applyRewrites(rewrites) {
31266
31280
  }
31267
31281
 
31268
31282
  // src/rename-symbol.ts
31269
- import { existsSync as existsSync14, realpathSync as realpathSync5 } from "node:fs";
31283
+ import { existsSync as existsSync14, mkdirSync as mkdirSync8, readdirSync as readdirSync6, realpathSync as realpathSync5, rmSync as rmSync6, writeFileSync as writeFileSync10 } from "node:fs";
31270
31284
  import { basename as basename3, dirname as dirname10, join as join16, relative as relative3, resolve as resolve5 } from "node:path";
31271
31285
 
31272
31286
  // src/resources.ts
@@ -31484,7 +31498,7 @@ function readResourceText(uri, getProjectPath) {
31484
31498
  const parsedProject = parseProjectGodot(rawProject);
31485
31499
  return {
31486
31500
  mimeType: "application/json",
31487
- text: JSON.stringify(parsedProject, null, 2)
31501
+ text: answerJson(parsedProject)
31488
31502
  };
31489
31503
  }
31490
31504
  const filePath = resolveProjectFile(projectPath, parsedUri.resourcePath);
@@ -31780,6 +31794,7 @@ async function renameSymbol(request, services) {
31780
31794
  }, plan);
31781
31795
  }
31782
31796
  plan = mentionIgnored(plan, ignoredTexts(projectPath), symbol, moveTo === null ? null : declaringPath);
31797
+ const left = leftAloneReport(plan.mentions);
31783
31798
  const rewrites = rewritesOf(plan, texts, moveTo === null || request.newScriptPath === null ? null : { from: declaringPath, to: moveTo, absoluteTo: request.newScriptPath });
31784
31799
  const graph = new ClassGraph(scripts, declared);
31785
31800
  const rank = (path) => (declaringFiles.includes(path) ? 0 : 1000) + graph.ancestors(`${path}#`).length;
@@ -31790,14 +31805,15 @@ async function renameSymbol(request, services) {
31790
31805
  moved: moveTo === null ? null : { from: declaringPath, to: moveTo },
31791
31806
  scriptsInOrder,
31792
31807
  rewrites,
31808
+ detail: left.detail,
31793
31809
  report: {
31794
31810
  renamed: { what, from: symbol, to: newName, declaredIn: declaringPath, resolvedBy },
31795
31811
  ...took === null ? {} : { languageServer: took },
31796
31812
  ...overrides.length > 0 ? { overridesRenamed: overrides } : {},
31797
31813
  ...moveTo === null ? {} : { moved: { from: declaringPath, to: moveTo } },
31798
31814
  changed: changedReport(plan.edits, rewrites),
31799
- ...leftAloneReport(plan.mentions),
31800
- summary: summaryOf(plan.edits, plan.mentions, rewrites)
31815
+ ...left.lists,
31816
+ summary: summaryOf(plan.edits, plan.mentions, rewrites, left.counts)
31801
31817
  }
31802
31818
  };
31803
31819
  }
@@ -31896,36 +31912,99 @@ function leftAloneReport(mentions) {
31896
31912
  const full = [];
31897
31913
  const prose = new Map;
31898
31914
  const others = new Map;
31899
- const add = (group, line) => {
31900
- if (!group.lines.includes(line)) {
31901
- group.lines.push(line);
31915
+ const counts = { quoted: 0, inProse: 0, otherSymbols: 0 };
31916
+ const add = (lines, line) => {
31917
+ if (!lines.includes(line)) {
31918
+ lines.push(line);
31902
31919
  }
31903
31920
  };
31921
+ const textOf = new Map;
31922
+ for (const mention of mentions) {
31923
+ textOf.set(`${mention.file}:${String(mention.line)}`, mention.text);
31924
+ }
31904
31925
  for (const mention of mentions) {
31905
- if ((mention.kind === "comment" || mention.kind === "text") && mention.why === undefined) {
31926
+ if (PROSE_KINDS.has(mention.kind) && mention.why === undefined) {
31906
31927
  const key = `${mention.file}
31907
31928
  ${mention.kind}`;
31908
31929
  const group = prose.get(key) ?? { file: mention.file, kind: mention.kind, lines: [] };
31909
- add(group, mention.line);
31930
+ add(group.lines, mention.line);
31910
31931
  prose.set(key, group);
31932
+ counts.inProse += 1;
31911
31933
  } else if (mention.kind === "code" && (mention.why === NOT_INHERITED || mention.why === RESOLVED_ELSEWHERE)) {
31912
- const key = `${mention.file}
31913
- ${mention.why}`;
31914
- const group = others.get(key) ?? { file: mention.file, why: mention.why, lines: [] };
31915
- add(group, mention.line);
31916
- others.set(key, group);
31934
+ const files = others.get(mention.why) ?? new Map;
31935
+ const lines = files.get(mention.file) ?? [];
31936
+ add(lines, mention.line);
31937
+ files.set(mention.file, lines);
31938
+ others.set(mention.why, files);
31939
+ counts.otherSymbols += 1;
31917
31940
  } else {
31918
31941
  full.push(mention);
31942
+ counts.quoted += 1;
31919
31943
  }
31920
31944
  }
31921
31945
  const loudFirst = (mention) => mention.kind === "string" && mention.why !== undefined ? 0 : 1;
31946
+ const leftAlone = full.sort((a, b) => loudFirst(a) - loudFirst(b));
31947
+ const inProse = [...prose.values()];
31948
+ const otherSymbols = [...others.entries()].map(([why, files]) => ({
31949
+ why,
31950
+ files: [...files.entries()].map(([file, lines]) => ({ file, lines }))
31951
+ }));
31952
+ const grouped = inProse.reduce((sum, group) => sum + group.lines.length, 0) + otherSymbols.reduce((sum, reason) => sum + reason.files.reduce((n, one) => n + one.lines.length, 0), 0);
31953
+ if (grouped <= LEFT_ALONE_ANSWER_LINES) {
31954
+ return {
31955
+ lists: {
31956
+ leftAlone,
31957
+ ...inProse.length > 0 ? { leftAloneInProse: inProse } : {},
31958
+ ...otherSymbols.length > 0 ? { leftAloneOtherSymbols: otherSymbols } : {}
31959
+ },
31960
+ counts,
31961
+ detail: null
31962
+ };
31963
+ }
31964
+ const withText = (file, lines) => lines.map((line) => ({ line, text: textOf.get(`${file}:${String(line)}`) ?? "" }));
31922
31965
  return {
31923
- leftAlone: full.sort((a, b) => loudFirst(a) - loudFirst(b)),
31924
- ...prose.size > 0 ? { leftAloneInProse: [...prose.values()] } : {},
31925
- ...others.size > 0 ? { leftAloneOtherSymbols: [...others.values()] } : {}
31966
+ lists: {
31967
+ leftAlone,
31968
+ leftAloneDetail: {
31969
+ inProse: { files: new Set(inProse.map((group) => group.file)).size, occurrences: counts.inProse },
31970
+ otherSymbols: otherSymbols.map((reason) => ({
31971
+ why: reason.why,
31972
+ files: reason.files.length,
31973
+ occurrences: reason.files.reduce((n, one) => n + one.lines.length, 0)
31974
+ }))
31975
+ }
31976
+ },
31977
+ counts,
31978
+ detail: {
31979
+ leftAloneInProse: inProse.map((group) => ({
31980
+ file: group.file,
31981
+ kind: group.kind,
31982
+ lines: withText(group.file, group.lines)
31983
+ })),
31984
+ leftAloneOtherSymbols: otherSymbols.map((reason) => ({
31985
+ why: reason.why,
31986
+ files: reason.files.map((one) => ({ file: one.file, lines: withText(one.file, one.lines) }))
31987
+ }))
31988
+ }
31926
31989
  };
31927
31990
  }
31928
- function summaryOf(edits, mentions, rewrites) {
31991
+ var LEFT_ALONE_ANSWER_LINES = 300;
31992
+ var detailWrites = 0;
31993
+ function writeLeftAloneDetail(projectPath, symbol, detail) {
31994
+ const directory = join16(projectPath, ".godot", "gdharness-renames");
31995
+ mkdirSync8(directory, { recursive: true });
31996
+ detailWrites += 1;
31997
+ const path = join16(directory, `${String(Date.now())}-${String(detailWrites).padStart(6, "0")}-${String(process.pid)}-${symbol}.json`);
31998
+ writeFileSync10(path, `${JSON.stringify(detail, null, 2)}
31999
+ `, "utf8");
32000
+ const kept = readdirSync6(directory).filter((name) => name.endsWith(".json")).sort().reverse();
32001
+ for (const old of kept.slice(10)) {
32002
+ rmSync6(join16(directory, old), { force: true });
32003
+ }
32004
+ return path;
32005
+ }
32006
+ var PROSE_KINDS = new Set(["comment", "doc", "string", "text"]);
32007
+ function summaryOf(edits, mentions, rewrites, left) {
31929
32008
  const count = (values) => {
31930
32009
  const counted = {};
31931
32010
  for (const value of values) {
@@ -31937,13 +32016,13 @@ function summaryOf(edits, mentions, rewrites) {
31937
32016
  filesChanged: rewrites.length,
31938
32017
  edits: edits.length,
31939
32018
  editsByKind: count(edits.map((edit) => edit.kind)),
31940
- leftAlone: mentions.length,
32019
+ leftAlone: { total: mentions.length, ...left },
31941
32020
  leftAloneByKind: count(mentions.map((mention) => mention.kind))
31942
32021
  };
31943
32022
  }
31944
32023
 
31945
32024
  // src/restart-note.ts
31946
- import { mkdirSync as mkdirSync8, readFileSync as readFileSync16, rmSync as rmSync6, writeFileSync as writeFileSync10 } from "node:fs";
32025
+ import { mkdirSync as mkdirSync9, readFileSync as readFileSync16, rmSync as rmSync7, writeFileSync as writeFileSync11 } from "node:fs";
31947
32026
  import { dirname as dirname11, join as join17 } from "node:path";
31948
32027
  function restartNotePath(projectPath) {
31949
32028
  return join17(projectPath, ".godot", "gdharness-restart.json");
@@ -31951,8 +32030,8 @@ function restartNotePath(projectPath) {
31951
32030
  function noteRestartBegun(note) {
31952
32031
  const path = restartNotePath(note.projectPath);
31953
32032
  try {
31954
- mkdirSync8(dirname11(path), { recursive: true });
31955
- writeFileSync10(path, `${JSON.stringify(note, null, 2)}
32033
+ mkdirSync9(dirname11(path), { recursive: true });
32034
+ writeFileSync11(path, `${JSON.stringify(note, null, 2)}
31956
32035
  `, "utf8");
31957
32036
  } catch (error) {
31958
32037
  console.error(`[SERVER] Could not record the restart at ${path}: ${errorMessage(error)}`);
@@ -31975,7 +32054,7 @@ function restartOwed(projectPath) {
31975
32054
  }
31976
32055
  function restartSettled(projectPath) {
31977
32056
  try {
31978
- rmSync6(restartNotePath(projectPath), { force: true });
32057
+ rmSync7(restartNotePath(projectPath), { force: true });
31979
32058
  } catch (error) {
31980
32059
  console.error(`[SERVER] Could not clear the restart note: ${errorMessage(error)}`);
31981
32060
  }
@@ -31985,14 +32064,14 @@ function restartSettled(projectPath) {
31985
32064
  import { execFile as execFile4 } from "node:child_process";
31986
32065
  import { createHash as createHash3 } from "node:crypto";
31987
32066
  import {
31988
- mkdirSync as mkdirSync9,
32067
+ mkdirSync as mkdirSync10,
31989
32068
  openSync as openSync2,
31990
- readdirSync as readdirSync6,
32069
+ readdirSync as readdirSync7,
31991
32070
  readFileSync as readFileSync17,
31992
32071
  realpathSync as realpathSync6,
31993
- rmSync as rmSync7,
32072
+ rmSync as rmSync8,
31994
32073
  statSync as statSync6,
31995
- writeFileSync as writeFileSync11
32074
+ writeFileSync as writeFileSync12
31996
32075
  } from "node:fs";
31997
32076
  import { basename as basename4, dirname as dirname12, join as join18, resolve as resolve6 } from "node:path";
31998
32077
  import { promisify as promisify4 } from "node:util";
@@ -32026,7 +32105,7 @@ function runRecordPath(projectPath, directory = runtimeDirectory()) {
32026
32105
  }
32027
32106
  var SHARED_NOTE = "run.json";
32028
32107
  function openTranscript(startedAt) {
32029
- mkdirSync9(runsDirectory(), { recursive: true });
32108
+ mkdirSync10(runsDirectory(), { recursive: true });
32030
32109
  const path = join18(runsDirectory(), `run-${startedAt}.log`);
32031
32110
  return { path, fd: openSync2(path, "a") };
32032
32111
  }
@@ -32037,8 +32116,8 @@ function editorNotePath(projectPath, directory = runtimeDirectory()) {
32037
32116
  return join18(directory, "runs", `editor-${projectKey(projectPath)}.json`);
32038
32117
  }
32039
32118
  function writeEditorRunNote(note) {
32040
- mkdirSync9(runsDirectory(), { recursive: true });
32041
- writeFileSync11(editorNotePath(note.projectPath), JSON.stringify(note, null, 2), "utf8");
32119
+ mkdirSync10(runsDirectory(), { recursive: true });
32120
+ writeFileSync12(editorNotePath(note.projectPath), JSON.stringify(note, null, 2), "utf8");
32042
32121
  }
32043
32122
  function readEditorRunNote(projectPath) {
32044
32123
  for (const directory of runtimeDirectories()) {
@@ -32088,7 +32167,7 @@ function everyRunRecord() {
32088
32167
  for (const directory of runtimeDirectories()) {
32089
32168
  let entries;
32090
32169
  try {
32091
- entries = readdirSync6(join18(directory, "runs"));
32170
+ entries = readdirSync7(join18(directory, "runs"));
32092
32171
  } catch {
32093
32172
  continue;
32094
32173
  }
@@ -32308,7 +32387,7 @@ function clearRunRecord(projectPath) {
32308
32387
  const ours = sharedRecord !== null && sharedRecord.projectPath !== "" && isSameDirectory(sharedRecord.projectPath, projectPath);
32309
32388
  for (const path of [runRecordPath(projectPath, directory), ...ours ? [shared] : []]) {
32310
32389
  try {
32311
- rmSync7(path, { force: true });
32390
+ rmSync8(path, { force: true });
32312
32391
  } catch {}
32313
32392
  }
32314
32393
  }
@@ -32325,7 +32404,7 @@ function sweepTranscripts(keepMs = 24 * 60 * 60 * 1000, now = Date.now()) {
32325
32404
  const kept = new Set(everyRunRecord().map((record) => record.transcript));
32326
32405
  let entries;
32327
32406
  try {
32328
- entries = readdirSync6(directory);
32407
+ entries = readdirSync7(directory);
32329
32408
  } catch {
32330
32409
  return;
32331
32410
  }
@@ -32338,7 +32417,7 @@ function sweepTranscripts(keepMs = 24 * 60 * 60 * 1000, now = Date.now()) {
32338
32417
  continue;
32339
32418
  }
32340
32419
  try {
32341
- rmSync7(path);
32420
+ rmSync8(path);
32342
32421
  } catch {}
32343
32422
  }
32344
32423
  }
@@ -32349,13 +32428,13 @@ import { createHash as createHash4 } from "node:crypto";
32349
32428
  import {
32350
32429
  cpSync,
32351
32430
  existsSync as existsSync15,
32352
- mkdirSync as mkdirSync10,
32353
- readdirSync as readdirSync7,
32431
+ mkdirSync as mkdirSync11,
32432
+ readdirSync as readdirSync8,
32354
32433
  readFileSync as readFileSync18,
32355
32434
  rmdirSync,
32356
- rmSync as rmSync8,
32435
+ rmSync as rmSync9,
32357
32436
  statSync as statSync7,
32358
- writeFileSync as writeFileSync12
32437
+ writeFileSync as writeFileSync13
32359
32438
  } from "node:fs";
32360
32439
  import { dirname as dirname13, join as join19 } from "node:path";
32361
32440
  import { fileURLToPath as fileURLToPath3 } from "node:url";
@@ -32377,7 +32456,7 @@ function runtimeAddonDigest(from = shippedAddonsDirectory()) {
32377
32456
  function addonsDigest(names, from) {
32378
32457
  const hash = createHash4("sha256");
32379
32458
  const walk = (directory, relative) => {
32380
- for (const name of readdirSync7(directory).sort()) {
32459
+ for (const name of readdirSync8(directory).sort()) {
32381
32460
  if (name === VERSION_MARKER || name === DIGEST_MARKER) {
32382
32461
  continue;
32383
32462
  }
@@ -33111,7 +33190,7 @@ var TOOL_SPECS = [
33111
33190
  },
33112
33191
  {
33113
33192
  name: "script_edit",
33114
- description: "Creates a GDScript file, or adds functions, variables and signals to one. Every declaration written carries a type. Both load what they wrote under the project's own warning settings and answer with parses; the engine's reasons for a refusal come back under engine_messages. create answers with the extends and class_name the engine read, and a script declaring a class_name is put in the project's class list, with registered saying whether it is there. modify makes every addition or none, and answers with the line each one is on in the file written. rename renames a class or a member wherever the engine uses it and writes every file or none. A new name already taken is refused with nothing written, a member's included when a class inheriting it already uses the name in code, as a local or a parameter. changed lists each file with its changed lines as they now read, and leftAlone lists every other whole-word occurrence of the old name with what the tokenizer makes of it (code, string, comment, doc, nodePath, scene, text, or ignored for a file under a .gdignore directory, which is never changed) and, where it is not plain, why: a string that is the old name whole, such as find_children's type or a call's method name, which nothing renames or compiles and which is listed first; a node named like the class; a call on a value whose type the analyser could not tell. A member rename answers under languageServer how many requests it made and how long they took. A comment or a line of a text file with nothing more to say is given under leftAloneInProse by file and line instead, and code that is certainly another symbol of the same name, a bare name in a file where nothing inherits the member or one the language server resolved to another declaration, under leftAloneOtherSymbols by file, why and line. Read both before calling the rename done; nothing in them was changed. Afterwards an open editor scans the changed files and reloads every changed script, which is also the compile check, and its answer comes back under editor; with no editor the class cache is rebuilt and the changed scripts are compiled in a fresh engine, under classCache and compiled. written is true once the files are written, and ok is false when what came after the write failed: the files are left changed, and note says so.",
33193
+ description: "Creates a GDScript file, or adds functions, variables and signals to one. Every declaration written carries a type. Both load what they wrote under the project's own warning settings and answer with parses; the engine's reasons for a refusal come back under engine_messages. create answers with the extends and class_name the engine read, and a script declaring a class_name is put in the project's class list, with registered saying whether it is there. modify makes every addition or none, and answers with the line each one is on in the file written. rename renames a class or a member wherever the engine uses it and writes every file or none. A new name already taken is refused with nothing written, a member's included when a class inheriting it already uses the name in code, as a local or a parameter. changed lists each file with its changed lines as they now read, and leftAlone lists every other whole-word occurrence of the old name with what the tokenizer makes of it (code, string, comment, doc, nodePath, scene, text, or ignored for a file under a .gdignore directory, which is never changed) and, where it is not plain, why: a string that is the old name whole, such as find_children's type or a call's method name, which nothing renames or compiles and which is listed first; a node named like the class; a call on a value whose type the analyser could not tell. A member rename answers under languageServer how many requests it made and how long they took. A comment, documentation line, string or text file line with nothing more to say is given under leftAloneInProse by file, kind and line instead, and code that is certainly another symbol of the same name, a bare name in a file where nothing inherits the member or one the language server resolved to another declaration, under leftAloneOtherSymbols as each why with the files and lines it covers. summary.leftAlone counts each of the three lists beside the total. When those two grouped lists run past 300 lines, they are written with each line's text to a file under the project's .godot directory instead, and leftAloneDetail names the file and counts what is in it. Read both before calling the rename done; nothing in them was changed. Afterwards an open editor scans the changed files and reloads every changed script, which is also the compile check, and its answer comes back under editor; with no editor the class cache is rebuilt and the changed scripts are compiled in a fresh engine, under classCache and compiled. written is true once the files are written, and ok is false when what came after the write failed: the files are left changed, and note says so.",
33115
33194
  parameters: {
33116
33195
  projectPath: PROJECT_PATH,
33117
33196
  scriptPath: SCRIPT_PATH,
@@ -34397,7 +34476,7 @@ function exportAnswer(ending, asked, before, after) {
34397
34476
  ...problems.omitted === 0 ? {} : { entriesOmitted: problems.omitted }
34398
34477
  };
34399
34478
  if (verdict.exported) {
34400
- return { content: [{ type: "text", text: JSON.stringify(verdict, null, 2) }] };
34479
+ return { content: [{ type: "text", text: answerJson(verdict) }] };
34401
34480
  }
34402
34481
  const stale = !written && after !== null ? ` The ${asked.outputPath} there is from before this export, which did not write it.` : "";
34403
34482
  const why = ending.exitSignal === null ? stale : ` The engine was ${howItEnded(ending)}, so it has no exit code.${stale}`;
@@ -34407,7 +34486,7 @@ function exportAnswer(ending, asked, before, after) {
34407
34486
  type: "text",
34408
34487
  text: `Export with preset '${asked.preset}' did not produce ${asked.outputPath}.${why}`
34409
34488
  },
34410
- { type: "text", text: JSON.stringify(verdict, null, 2) }
34489
+ { type: "text", text: answerJson(verdict) }
34411
34490
  ],
34412
34491
  isError: true
34413
34492
  };
@@ -34554,7 +34633,7 @@ function elsewhereIn(projectPath, asked) {
34554
34633
  const root = (wanted.split("/")[0] ?? "").toLowerCase();
34555
34634
  let near = [];
34556
34635
  try {
34557
- near = readdirSync8(projectPath, { withFileTypes: true }).filter((entry) => entry.isDirectory() && entry.name.toLowerCase() !== root).map((entry) => entry.name).filter((name) => name.toLowerCase().startsWith("test") || root.startsWith(name.toLowerCase()));
34636
+ near = readdirSync9(projectPath, { withFileTypes: true }).filter((entry) => entry.isDirectory() && entry.name.toLowerCase() !== root).map((entry) => entry.name).filter((name) => name.toLowerCase().startsWith("test") || root.startsWith(name.toLowerCase()));
34558
34637
  } catch {
34559
34638
  return "";
34560
34639
  }
@@ -34656,7 +34735,7 @@ class GodotServer {
34656
34735
  return response;
34657
34736
  }
34658
34737
  jsonTextResponse(payload) {
34659
- return { content: [{ type: "text", text: JSON.stringify(payload, null, 2) }] };
34738
+ return { content: [{ type: "text", text: answerJson(payload) }] };
34660
34739
  }
34661
34740
  async run() {
34662
34741
  const located = await this.locator.find();
@@ -34989,7 +35068,7 @@ class GodotServer {
34989
35068
  saying(answer, block) {
34990
35069
  return {
34991
35070
  ...answer,
34992
- content: [...answer.content, { type: "text", text: JSON.stringify(block, null, 2) }]
35071
+ content: [...answer.content, { type: "text", text: answerJson(block) }]
34993
35072
  };
34994
35073
  }
34995
35074
  projectHasMovedOn() {
@@ -35962,7 +36041,7 @@ class GodotServer {
35962
36041
  if (!outcome.ok) {
35963
36042
  const response = this.createErrorResponse(outcome.message);
35964
36043
  if (Object.keys(extras).length > 0) {
35965
- response.content.push({ type: "text", text: JSON.stringify(extras, null, 2) });
36044
+ response.content.push({ type: "text", text: answerJson(extras) });
35966
36045
  }
35967
36046
  return response;
35968
36047
  }
@@ -36040,7 +36119,7 @@ class GodotServer {
36040
36119
  }
36041
36120
  const folder = dirname14(output.absolutePath);
36042
36121
  try {
36043
- mkdirSync11(folder, { recursive: true });
36122
+ mkdirSync12(folder, { recursive: true });
36044
36123
  } catch (error) {
36045
36124
  return this.createErrorResponse(`Cannot create ${dirname14(output.relativePath)} to export into: ${errorMessage(error)}`);
36046
36125
  }
@@ -36151,7 +36230,7 @@ class GodotServer {
36151
36230
  let report = null;
36152
36231
  let reportProblem = null;
36153
36232
  try {
36154
- const written = existsSync16(reportsDir) ? readdirSync8(reportsDir).filter((name) => name.startsWith("report_")).sort((a, b) => Number(a.slice("report_".length)) - Number(b.slice("report_".length))) : [];
36233
+ const written = existsSync16(reportsDir) ? readdirSync9(reportsDir).filter((name) => name.startsWith("report_")).sort((a, b) => Number(a.slice("report_".length)) - Number(b.slice("report_".length))) : [];
36155
36234
  const newest = written.at(-1);
36156
36235
  if (newest !== undefined) {
36157
36236
  report = parseJUnit(readFileSync19(join20(reportsDir, newest, "results.xml"), "utf8"));
@@ -38662,6 +38741,15 @@ class GodotServer {
38662
38741
  const said = outcome.conflicts === undefined ? "" : ` ${outcome.conflicts.join("; ")}.`;
38663
38742
  return this.createErrorResponse(`${outcome.reason}${said}`, outcome.advice ?? []);
38664
38743
  }
38744
+ if (outcome.detail !== null) {
38745
+ const file = writeLeftAloneDetail(projectPath, symbol, outcome.detail);
38746
+ const folded = asParams(outcome.report["leftAloneDetail"]);
38747
+ outcome.report["leftAloneDetail"] = {
38748
+ file,
38749
+ note: `Too many lines were left alone to list here, and none of them is a use: every comment, documentation line, string and text file line that only shares the word, and every other symbol of the same name, is in ${file} with its text.`,
38750
+ ...folded
38751
+ };
38752
+ }
38665
38753
  if (readBoolean(args, "preview") === true) {
38666
38754
  return this.jsonTextResponse({ preview: true, ...outcome.report });
38667
38755
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gdharness",
3
- "version": "1.1.37",
3
+ "version": "1.1.39",
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",