gdharness 1.1.51 → 1.1.52

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 +181 -40
  2. package/build/index.js +179 -39
  3. package/package.json +1 -1
package/build/cli.js CHANGED
@@ -8544,7 +8544,8 @@ function sameCodeNote(addonVersion, serverVersion, addonDigest, shippedDigest) {
8544
8544
  if (addonVersion === undefined || addonVersion === serverVersion || addonDigest === undefined || addonDigest === "" || addonDigest !== shippedDigest) {
8545
8545
  return;
8546
8546
  }
8547
- return `The editor loaded the ${addonVersion} addons, which are the same code this ${serverVersion} server ships, so nothing needs restarting; addonVersion changes at the next editor start.`;
8547
+ const same = `The editor loaded the ${addonVersion} addons, which are the same code this ${serverVersion} server ships, so nothing needs restarting`;
8548
+ return isNewer(addonVersion, serverVersion) ? `${same}. This server is the older of the two, which a reconnect in your harness settles once the project pins ${addonVersion}.` : `${same}; addonVersion is the version the editor loaded when it started.`;
8548
8549
  }
8549
8550
  function addonMismatch(addonVersion, serverVersion, addonDigest, shippedDigest, onDisk) {
8550
8551
  if (!editorIsStale(addonVersion, serverVersion, addonDigest, shippedDigest)) {
@@ -17826,8 +17827,16 @@ var init_tool_definitions = __esm(() => {
17826
17827
  type: "boolean",
17827
17828
  description: "structure: also list what the script inherits, walking extends through the project's other scripts. Each such member carries inherited_from, the file declaring it, and the answer gains inherits_from, the chain that was walked. A name the script overrides is listed at both its lines rather than once. A native base is not walked, since it declares nothing in a file: editor_classes info answers for those. Default false."
17828
17829
  },
17829
- line: { type: "integer", minimum: 0, description: "completion, hover: zero-based line." },
17830
- character: { type: "integer", minimum: 0, description: "completion, hover: zero-based column." }
17830
+ line: {
17831
+ type: "integer",
17832
+ minimum: 1,
17833
+ description: "completion, hover: the line, counted from 1 as the editor shows it."
17834
+ },
17835
+ character: {
17836
+ type: "integer",
17837
+ minimum: 1,
17838
+ description: "completion, hover: the column, counted from 1 as the editor shows it, with a tab counted as one. symbols gives each name at the line and column hover is asked at."
17839
+ }
17831
17840
  },
17832
17841
  requires: ["projectPath", "scriptPath"],
17833
17842
  operations: {
@@ -17835,7 +17844,10 @@ var init_tool_definitions = __esm(() => {
17835
17844
  summary: "functions, variables, signals, class_name and extends, read from the file",
17836
17845
  requires: []
17837
17846
  },
17838
- symbols: { summary: "document symbols from the language server", requires: [] },
17847
+ symbols: {
17848
+ summary: "document symbols from the language server, each with its kind, the line and column of its name, and the lastLine of its declaration",
17849
+ requires: []
17850
+ },
17839
17851
  completion: { summary: "completions at a position", requires: ["line", "character"] },
17840
17852
  hover: { summary: "hover text at a position", requires: ["line", "character"] }
17841
17853
  },
@@ -17843,13 +17855,13 @@ var init_tool_definitions = __esm(() => {
17843
17855
  },
17844
17856
  {
17845
17857
  name: "script_diagnostics",
17846
- description: "Errors and warnings for a script, or for several, from the editor's language server, and whether each is clean. Needs the editor running. Diagnostics are checked against the project's own files, and the two that the files disprove are named rather than passed on: a member the class cache's file declares, a method, property, constant, static function or enum member, comes back under contradictedByTheFile, and a class this project declares that the diagnostic could not resolve comes back under typesTheEditorHasNotLoaded, with inTheClassCache saying whether a launched game would resolve it. staleAnalysis then says what to do, and for both states it is editor_rescan first: it asks the editor already running, starts nothing, and has cleared each in every reproduction measured. Two exceptions are named when they apply. For an enum member, constant or static function of a global class the reload comes first: in the one project that has reproduced a stale enum member, editor_rescan with reloadScript on the declaring script cleared it and a plain rescan did not. And an editor whose last scan wrote the class cache shorter than the file holds a list behind the files and writes it on every scan, so on that editor the rescan loads nothing and staleAnalysis says editor_launch restart instead. What follows differs: for a stale analysed type, editor_rescan with reloadScript recompiles the held copy and editor_launch restart is the last resort that costs a window; for a class missing from the cache, project_import refresh_classes rewrites the cache from disk at the cost of a short headless engine, which matters beside a running bench.",
17858
+ description: "Errors and warnings for a script, or for several, from the editor's language server, and whether each is clean. Needs the editor running. Each diagnostic carries its severity (error or warning), its message, and where it is as the editor shows it: line and column counted from 1, and endLine and endColumn just past its end. Diagnostics are checked against the project's own files, and the two that the files disprove are named rather than passed on: a member the class cache's file declares, a method, property, constant, static function or enum member, comes back under contradictedByTheFile, and a class this project declares that the diagnostic could not resolve comes back under typesTheEditorHasNotLoaded, with inTheClassCache saying whether a launched game would resolve it. staleAnalysis then says what to do, and for both states it is editor_rescan first: it asks the editor already running, starts nothing, and has cleared each in every reproduction measured. Two exceptions are named when they apply. For an enum member, constant or static function of a global class the reload comes first: in the one project that has reproduced a stale enum member, editor_rescan with reloadScript on the declaring script cleared it and a plain rescan did not. And an editor whose last scan wrote the class cache shorter than the file holds a list behind the files and writes it on every scan, so on that editor the rescan loads nothing and staleAnalysis says editor_launch restart instead. What follows differs: for a stale analysed type, editor_rescan with reloadScript recompiles the held copy and editor_launch restart is the last resort that costs a window; for a class missing from the cache, project_import refresh_classes rewrites the cache from disk at the cost of a short headless engine, which matters beside a running bench.",
17847
17859
  parameters: {
17848
17860
  projectPath: PROJECT_PATH,
17849
17861
  scriptPath: {
17850
17862
  type: ["string", "array"],
17851
17863
  items: { type: "string" },
17852
- description: 'A script inside the project, a folder, or a list of either, such as "scripts/player.gd" or ["scripts/economy", "ui/shop.gd"]. A folder is its .gd files at any depth, skipping what the engine skips. One script is answered as it always was; several are answered together under scripts, each in that same shape, with clean, errors and warnings for all of them beside it, and a script that could not be read is named there with why while the rest are still asked about. At most 300 scripts a call.'
17864
+ description: 'A script inside the project, a folder, or a list of either, such as "scripts/player.gd" or ["scripts/economy", "ui/shop.gd"]. A folder is its .gd files at any depth, skipping what the engine skips. One script is answered as it always was. Several are answered together, with clean, errors and warnings for all of them: a script with nothing to report is named in nothingReported, and every other is answered under scripts in that same shape, including a script that could not be read, named with why while the rest are still asked about. Any number of scripts is one call, which reports progress as it goes.'
17853
17865
  }
17854
17866
  },
17855
17867
  requires: ["projectPath", "scriptPath"]
@@ -39112,6 +39124,10 @@ import { readFile, realpath } from "node:fs/promises";
39112
39124
  import { createConnection as createConnection3 } from "node:net";
39113
39125
  import { dirname as dirname14, join as join20, resolve as resolve5 } from "node:path";
39114
39126
  import { fileURLToPath as fileURLToPath3, pathToFileURL } from "node:url";
39127
+ function initializeTimeoutMs() {
39128
+ const override = Number.parseInt(envValue("GDHARNESS_LSP_INITIALIZE_TIMEOUT_MS") ?? "", 10);
39129
+ return Number.isInteger(override) && override > 0 ? override : 180000;
39130
+ }
39115
39131
  function diagnosticsKey(uri) {
39116
39132
  let decoded;
39117
39133
  try {
@@ -39145,7 +39161,11 @@ class GodotLSPClient {
39145
39161
  documentVersions = new Map;
39146
39162
  documentTurns = new Map;
39147
39163
  timeouts;
39148
- constructor(port = portFromEnv("GDHARNESS_LSP_PORT", DEFAULT_LSP_PORT), host = "127.0.0.1", timeouts = { requestMs: REQUEST_TIMEOUT_MS2, referencesMs: REFERENCES_TIMEOUT_MS }) {
39164
+ constructor(port = portFromEnv("GDHARNESS_LSP_PORT", DEFAULT_LSP_PORT), host = "127.0.0.1", timeouts = {
39165
+ requestMs: REQUEST_TIMEOUT_MS2,
39166
+ referencesMs: REFERENCES_TIMEOUT_MS,
39167
+ initializeMs: initializeTimeoutMs()
39168
+ }) {
39149
39169
  this.port = port;
39150
39170
  this.host = host;
39151
39171
  this.timeouts = timeouts;
@@ -39455,7 +39475,7 @@ class GodotLSPClient {
39455
39475
  name: resolvedRootPath
39456
39476
  }
39457
39477
  ]
39458
- });
39478
+ }, this.timeouts.initializeMs);
39459
39479
  const served = this.servedWorkspace;
39460
39480
  if (served !== null && !isSameDirectory(served, resolvedRootPath)) {
39461
39481
  throw new AnotherProjectsServer(this.port, served, resolvedRootPath);
@@ -39641,10 +39661,78 @@ function normalizeLSPError(error, port) {
39641
39661
  if (error.message.includes("ECONNREFUSED") || error.message.includes("Failed to connect to Godot LSP") || error.message.includes("socket closed")) {
39642
39662
  return `Godot LSP is unavailable on port ${port}. Start the Godot editor and enable Language Server in Editor Settings, or set GDHARNESS_LSP_PORT to the port it serves.`;
39643
39663
  }
39664
+ if (error instanceof LSPTimeout && error.method === "initialize") {
39665
+ return `The language server on port ${port} took the connection and did not answer initialize within ${String(error.seconds)}s. Godot reads every script in the project before it answers the first initialize after its editor starts, and goes on reading after this call stops waiting, so call again once the editor has settled.`;
39666
+ }
39644
39667
  return error.message;
39645
39668
  }
39646
39669
  return String(error);
39647
39670
  }
39671
+ function asRecord(value) {
39672
+ return value !== null && typeof value === "object" && !Array.isArray(value) ? value : {};
39673
+ }
39674
+ function named(value, names) {
39675
+ return typeof value === "number" && Number.isInteger(value) ? names[value - 1] ?? value : value;
39676
+ }
39677
+ function shownPosition(position) {
39678
+ const { line, character } = asRecord(position);
39679
+ return typeof line === "number" && typeof character === "number" ? { line: line + 1, column: character + 1 } : null;
39680
+ }
39681
+ function shownRange(range) {
39682
+ const start = shownPosition(asRecord(range)["start"]);
39683
+ const end = shownPosition(asRecord(range)["end"]);
39684
+ return {
39685
+ ...start === null ? {} : { line: start.line, column: start.column },
39686
+ ...end === null ? {} : { endLine: end.line, endColumn: end.column }
39687
+ };
39688
+ }
39689
+ function shownDiagnostic(entry) {
39690
+ const { range, severity, message, code } = asRecord(entry);
39691
+ return {
39692
+ ...shownRange(range),
39693
+ severity: named(severity, SEVERITIES),
39694
+ message,
39695
+ ...code === undefined || code === null || code === 0 || code === "" ? {} : { code }
39696
+ };
39697
+ }
39698
+ function shownSymbol(entry) {
39699
+ const symbol = asRecord(entry);
39700
+ const whole = asRecord(symbol["range"] ?? asRecord(symbol["location"])["range"]);
39701
+ const at = shownPosition(asRecord(symbol["selectionRange"] ?? whole)["start"]);
39702
+ const last = shownPosition(whole["end"]);
39703
+ const children = Array.isArray(symbol["children"]) ? symbol["children"] : [];
39704
+ const { documentation, native_class: nativeClass, deprecated, detail } = symbol;
39705
+ return {
39706
+ name: symbol["name"],
39707
+ kind: named(symbol["kind"], SYMBOL_KINDS),
39708
+ ...typeof detail === "string" && detail !== "" ? { detail } : {},
39709
+ ...at === null ? {} : { line: at.line, column: at.column },
39710
+ ...last === null ? {} : { lastLine: last.line },
39711
+ ...typeof documentation === "string" && documentation !== "" ? { documentation } : {},
39712
+ ...typeof nativeClass === "string" && nativeClass !== "" ? { nativeClass } : {},
39713
+ ...deprecated === true ? { deprecated } : {},
39714
+ ...children.length > 0 ? { children: children.map(shownSymbol) } : {}
39715
+ };
39716
+ }
39717
+ function shownCompletion(entry) {
39718
+ const { data: _request, kind, ...rest } = asRecord(entry);
39719
+ return { ...rest, kind: named(kind, COMPLETION_KINDS) };
39720
+ }
39721
+ function shownHover(hover) {
39722
+ if (hover === null || typeof hover !== "object") {
39723
+ return hover;
39724
+ }
39725
+ const { range, ...rest } = asRecord(hover);
39726
+ return { ...rest, ...shownRange(range) };
39727
+ }
39728
+ function protocolPosition(args) {
39729
+ const line = args["line"];
39730
+ const character = args["character"];
39731
+ if (typeof line !== "number" || typeof character !== "number" || !Number.isInteger(line) || !Number.isInteger(character) || line < 1 || character < 1) {
39732
+ throw new ArgumentRefusal(`line and character are counted from 1, as the editor shows them; got line ${String(line)} and character ${String(character)}.`);
39733
+ }
39734
+ return { line: line - 1, character: character - 1 };
39735
+ }
39648
39736
  async function resolveLSPPaths(projectPathValue, scriptPathValue) {
39649
39737
  const requestedProjectPath = resolve5(projectPathValue);
39650
39738
  let projectPath;
@@ -39691,29 +39779,21 @@ async function handleLSPTool(client, toolName, args) {
39691
39779
  switch (toolName) {
39692
39780
  case "lsp_get_diagnostics": {
39693
39781
  const diagnostics = await client.getDiagnostics(scriptPath, content);
39694
- return asToolResponse({ diagnostics });
39782
+ return asToolResponse({ diagnostics: diagnostics.map(shownDiagnostic) });
39695
39783
  }
39696
39784
  case "lsp_get_completions": {
39697
- const line = Number(parsedArgs["line"]);
39698
- const character = Number(parsedArgs["character"]);
39699
- if (!Number.isFinite(line) || !Number.isFinite(character)) {
39700
- throw new ArgumentRefusal("Arguments line and character must be numbers.");
39701
- }
39785
+ const { line, character } = protocolPosition(parsedArgs);
39702
39786
  const completions = await client.getCompletions(scriptPath, content, line, character);
39703
- return asToolResponse({ completions });
39787
+ return asToolResponse({ completions: completions.map(shownCompletion) });
39704
39788
  }
39705
39789
  case "lsp_get_hover": {
39706
- const line = Number(parsedArgs["line"]);
39707
- const character = Number(parsedArgs["character"]);
39708
- if (!Number.isFinite(line) || !Number.isFinite(character)) {
39709
- throw new ArgumentRefusal("Arguments line and character must be numbers.");
39710
- }
39790
+ const { line, character } = protocolPosition(parsedArgs);
39711
39791
  const hover = await client.getHover(scriptPath, content, line, character);
39712
- return asToolResponse({ hover });
39792
+ return asToolResponse({ hover: shownHover(hover) });
39713
39793
  }
39714
39794
  case "lsp_get_symbols": {
39715
39795
  const symbols = await client.getDocumentSymbols(scriptPath, content);
39716
- return asToolResponse({ symbols });
39796
+ return asToolResponse({ symbols: symbols.map(shownSymbol) });
39717
39797
  }
39718
39798
  default:
39719
39799
  return asToolResponse({ error: `Unknown LSP tool: ${toolName}` });
@@ -39723,11 +39803,12 @@ async function handleLSPTool(client, toolName, args) {
39723
39803
  error: normalizeLSPError(error, client.port),
39724
39804
  ...error instanceof AnotherProjectsServer ? { serves: error.serves } : {},
39725
39805
  ...error instanceof ArgumentRefusal ? { refusedArguments: true } : {},
39726
- ...error instanceof NoDiagnosticsPublished ? { aboutThisFile: true } : {}
39806
+ ...error instanceof NoDiagnosticsPublished ? { aboutThisFile: true } : {},
39807
+ ...error instanceof LSPTimeout && error.method === "initialize" ? { stillReading: true } : {}
39727
39808
  });
39728
39809
  }
39729
39810
  }
39730
- var DEFAULT_LSP_PORT = 6005, DIAGNOSTICS_TIMEOUT_MS = 5000, REQUEST_TIMEOUT_MS2 = 1e4, REFERENCES_TIMEOUT_MS = 180000, LSPTimeout, ArgumentRefusal, NoDiagnosticsPublished, AnotherProjectsServer;
39811
+ var DEFAULT_LSP_PORT = 6005, DIAGNOSTICS_TIMEOUT_MS = 5000, REQUEST_TIMEOUT_MS2 = 1e4, REFERENCES_TIMEOUT_MS = 180000, LSPTimeout, ArgumentRefusal, NoDiagnosticsPublished, AnotherProjectsServer, SEVERITIES, SYMBOL_KINDS, COMPLETION_KINDS;
39731
39812
  var init_lsp_client = __esm(() => {
39732
39813
  init_answer_json();
39733
39814
  init_errors();
@@ -39754,6 +39835,62 @@ var init_lsp_client = __esm(() => {
39754
39835
  this.serves = serves;
39755
39836
  }
39756
39837
  };
39838
+ SEVERITIES = ["error", "warning", "information", "hint"];
39839
+ SYMBOL_KINDS = [
39840
+ "file",
39841
+ "module",
39842
+ "namespace",
39843
+ "package",
39844
+ "class",
39845
+ "method",
39846
+ "property",
39847
+ "field",
39848
+ "constructor",
39849
+ "enum",
39850
+ "interface",
39851
+ "function",
39852
+ "variable",
39853
+ "constant",
39854
+ "string",
39855
+ "number",
39856
+ "boolean",
39857
+ "array",
39858
+ "object",
39859
+ "key",
39860
+ "null",
39861
+ "enum member",
39862
+ "struct",
39863
+ "event",
39864
+ "operator",
39865
+ "type parameter"
39866
+ ];
39867
+ COMPLETION_KINDS = [
39868
+ "text",
39869
+ "method",
39870
+ "function",
39871
+ "constructor",
39872
+ "field",
39873
+ "variable",
39874
+ "class",
39875
+ "interface",
39876
+ "module",
39877
+ "property",
39878
+ "unit",
39879
+ "value",
39880
+ "enum",
39881
+ "keyword",
39882
+ "snippet",
39883
+ "color",
39884
+ "file",
39885
+ "reference",
39886
+ "folder",
39887
+ "enum member",
39888
+ "constant",
39889
+ "struct",
39890
+ "event",
39891
+ "operator",
39892
+ "type parameter"
39893
+ ];
39757
39894
  });
39758
39895
 
39759
39896
  // src/outside.ts
@@ -42628,12 +42765,6 @@ function scriptsToDiagnose(projectPath, given) {
42628
42765
  if (unique.length === 0) {
42629
42766
  return { ok: false, reason: `No .gd script is under ${empty.join(" or ")}.` };
42630
42767
  }
42631
- if (unique.length > MOST_DIAGNOSED) {
42632
- return {
42633
- ok: false,
42634
- reason: `scriptPath names ${unique.length} scripts, and one call asks about at most ${MOST_DIAGNOSED}, each a round trip through the language server: name narrower folders, or call again for the rest.`
42635
- };
42636
- }
42637
42768
  return { ok: true, value: unique };
42638
42769
  }
42639
42770
  function isDirectory(path) {
@@ -44499,11 +44630,16 @@ class GodotServer {
44499
44630
  return "refusal" in one ? one.refusal : this.jsonTextResponse(this.markedIfStale(one.answer));
44500
44631
  }
44501
44632
  const progress = callProgress();
44502
- const answers = [];
44633
+ const cancelled = callSignal();
44634
+ const reported = [];
44635
+ const nothingReported = [];
44503
44636
  let errors = 0;
44504
44637
  let warnings = 0;
44505
44638
  let unread = 0;
44506
44639
  for (const [at, scriptPath] of scripts.value.entries()) {
44640
+ if (cancelled?.aborted === true) {
44641
+ return this.createErrorResponse(`Cancelled after ${at} of ${scripts.value.length} scripts.`);
44642
+ }
44507
44643
  progress?.(at + 1, `${at + 1} of ${scripts.value.length}: ${scriptPath}`);
44508
44644
  const one = await this.diagnosticsOf({ ...args, scriptPath });
44509
44645
  if ("refusal" in one) {
@@ -44511,20 +44647,25 @@ class GodotServer {
44511
44647
  return one.refusal;
44512
44648
  }
44513
44649
  unread += 1;
44514
- answers.push({ scriptPath, error: one.refusal.content[0]?.text ?? "not read" });
44650
+ reported.push({ scriptPath, error: one.refusal.content[0]?.text ?? "not read" });
44515
44651
  continue;
44516
44652
  }
44517
44653
  errors += Number(one.answer["errors"]);
44518
44654
  warnings += Number(one.answer["warnings"]);
44519
- answers.push(one.answer);
44655
+ if (readArray(one.answer, "diagnostics")?.length === 0) {
44656
+ nothingReported.push(scriptPath);
44657
+ } else {
44658
+ reported.push(one.answer);
44659
+ }
44520
44660
  }
44521
44661
  return this.jsonTextResponse(this.markedIfStale({
44522
44662
  clean: errors === 0 && unread === 0,
44523
- scriptsChecked: answers.length,
44663
+ scriptsChecked: scripts.value.length,
44524
44664
  errors,
44525
44665
  warnings,
44526
44666
  ...unread > 0 ? { scriptsNotRead: unread } : {},
44527
- scripts: answers
44667
+ scripts: reported,
44668
+ nothingReported
44528
44669
  }));
44529
44670
  }
44530
44671
  markedIfStale(answer) {
@@ -44538,6 +44679,9 @@ class GodotServer {
44538
44679
  if (payload["refusedArguments"] === true) {
44539
44680
  return { refusal: this.createErrorResponse(reason), perScript: true };
44540
44681
  }
44682
+ if (payload["stillReading"] === true) {
44683
+ return { refusal: this.createErrorResponse(`Diagnostics unavailable: ${reason}`), perScript: false };
44684
+ }
44541
44685
  if (typeof payload["serves"] === "string") {
44542
44686
  return {
44543
44687
  refusal: this.createErrorResponse(`Diagnostics unavailable: ${reason}`, [
@@ -44557,10 +44701,7 @@ class GodotServer {
44557
44701
  };
44558
44702
  }
44559
44703
  const diagnostics = readArray(payload, "diagnostics") ?? [];
44560
- const errors = diagnostics.filter((entry) => {
44561
- const severity = asParams(entry)["severity"];
44562
- return severity === 1 || severity === "error" || severity === "ERROR";
44563
- }).length;
44704
+ const errors = diagnostics.filter((entry) => asParams(entry)["severity"] === "error").length;
44564
44705
  return {
44565
44706
  answer: {
44566
44707
  scriptPath: readString(args, "scriptPath"),
@@ -47403,7 +47544,7 @@ async function runGodotServer() {
47403
47544
  const server = new GodotServer;
47404
47545
  await server.run();
47405
47546
  }
47406
- var UPDATE_NOTICE_EVERY = 500, FEEDBACK_NOTICE_EVERY = 250, run4, __dirname2, GREETING_WAIT_MS = 1e4, HOLD_PING_MS = 2000, SLOWEST_FRAME_RATE = 20, PLAY_STARTS_WITHIN_MS = 30000, STOP_WAIT_MS = 1e4, KEEPER_ANSWER_MS = 15000, EXIT_REPORTED_WITHIN_MS = 2000, CONSOLE_WRAPPER, LONGEST_BOOT_NOTED_MS, MOST_GROUPS = 20, VALUE_NAMES, BRIDGE_RETRY_MS = 2000, SUCCESSOR_CHECK_MS = 1e4, CONSOLE_WATCH_MS = 250, BOUNDARY_WAIT_MS = 2000, HANDOVER_MS, HANDOVER_POLL_MS = 250, CACHE_WRITE_MS = 2000, SCAN_WAIT_MS = 30000, STREAMS_GRACE_MS = 2000, PLAYING_STATUS_MS = 1000, SCAN_START_MS = 2000, SCAN_SETTLED_MS = 500, REIMPORT_TIMEOUT_MS = 300000, AUDIO_BUS_OPS, STALLED_AFTER_MS = 30000, TEST_HEARTBEAT_MS = 20000, DEPENDED_FAILED = "Failed to compile depended scripts", WAIT_FOR_RUN_MS = 600000, RUN_POLL_MS = 250, PATH_SOLUTIONS, PROJECT_FILE_ARGUMENTS, DEBUG_STATE_CALLS, PROJECT_INFO_SECTIONS2, RUNNER_DIRECTORY = "addons/gdUnit4/", MOST_DIAGNOSED = 300;
47547
+ var UPDATE_NOTICE_EVERY = 500, FEEDBACK_NOTICE_EVERY = 250, run4, __dirname2, GREETING_WAIT_MS = 1e4, HOLD_PING_MS = 2000, SLOWEST_FRAME_RATE = 20, PLAY_STARTS_WITHIN_MS = 30000, STOP_WAIT_MS = 1e4, KEEPER_ANSWER_MS = 15000, EXIT_REPORTED_WITHIN_MS = 2000, CONSOLE_WRAPPER, LONGEST_BOOT_NOTED_MS, MOST_GROUPS = 20, VALUE_NAMES, BRIDGE_RETRY_MS = 2000, SUCCESSOR_CHECK_MS = 1e4, CONSOLE_WATCH_MS = 250, BOUNDARY_WAIT_MS = 2000, HANDOVER_MS, HANDOVER_POLL_MS = 250, CACHE_WRITE_MS = 2000, SCAN_WAIT_MS = 30000, STREAMS_GRACE_MS = 2000, PLAYING_STATUS_MS = 1000, SCAN_START_MS = 2000, SCAN_SETTLED_MS = 500, REIMPORT_TIMEOUT_MS = 300000, AUDIO_BUS_OPS, STALLED_AFTER_MS = 30000, TEST_HEARTBEAT_MS = 20000, DEPENDED_FAILED = "Failed to compile depended scripts", WAIT_FOR_RUN_MS = 600000, RUN_POLL_MS = 250, PATH_SOLUTIONS, PROJECT_FILE_ARGUMENTS, DEBUG_STATE_CALLS, PROJECT_INFO_SECTIONS2, RUNNER_DIRECTORY = "addons/gdUnit4/";
47407
47548
  var init_server3 = __esm(() => {
47408
47549
  init_mcp();
47409
47550
  init_stdio2();
package/build/index.js CHANGED
@@ -10069,7 +10069,8 @@ function sameCodeNote(addonVersion, serverVersion, addonDigest, shippedDigest) {
10069
10069
  if (addonVersion === undefined || addonVersion === serverVersion || addonDigest === undefined || addonDigest === "" || addonDigest !== shippedDigest) {
10070
10070
  return;
10071
10071
  }
10072
- return `The editor loaded the ${addonVersion} addons, which are the same code this ${serverVersion} server ships, so nothing needs restarting; addonVersion changes at the next editor start.`;
10072
+ const same = `The editor loaded the ${addonVersion} addons, which are the same code this ${serverVersion} server ships, so nothing needs restarting`;
10073
+ return isNewer(addonVersion, serverVersion) ? `${same}. This server is the older of the two, which a reconnect in your harness settles once the project pins ${addonVersion}.` : `${same}; addonVersion is the version the editor loaded when it started.`;
10073
10074
  }
10074
10075
  function addonMismatch(addonVersion, serverVersion, addonDigest, shippedDigest, onDisk) {
10075
10076
  if (!editorIsStale(addonVersion, serverVersion, addonDigest, shippedDigest)) {
@@ -29715,6 +29716,10 @@ var DEFAULT_LSP_PORT = 6005;
29715
29716
  var DIAGNOSTICS_TIMEOUT_MS = 5000;
29716
29717
  var REQUEST_TIMEOUT_MS2 = 1e4;
29717
29718
  var REFERENCES_TIMEOUT_MS = 180000;
29719
+ function initializeTimeoutMs() {
29720
+ const override = Number.parseInt(envValue("GDHARNESS_LSP_INITIALIZE_TIMEOUT_MS") ?? "", 10);
29721
+ return Number.isInteger(override) && override > 0 ? override : 180000;
29722
+ }
29718
29723
 
29719
29724
  class LSPTimeout extends Error {
29720
29725
  method;
@@ -29772,7 +29777,11 @@ class GodotLSPClient {
29772
29777
  documentVersions = new Map;
29773
29778
  documentTurns = new Map;
29774
29779
  timeouts;
29775
- constructor(port = portFromEnv("GDHARNESS_LSP_PORT", DEFAULT_LSP_PORT), host = "127.0.0.1", timeouts = { requestMs: REQUEST_TIMEOUT_MS2, referencesMs: REFERENCES_TIMEOUT_MS }) {
29780
+ constructor(port = portFromEnv("GDHARNESS_LSP_PORT", DEFAULT_LSP_PORT), host = "127.0.0.1", timeouts = {
29781
+ requestMs: REQUEST_TIMEOUT_MS2,
29782
+ referencesMs: REFERENCES_TIMEOUT_MS,
29783
+ initializeMs: initializeTimeoutMs()
29784
+ }) {
29776
29785
  this.port = port;
29777
29786
  this.host = host;
29778
29787
  this.timeouts = timeouts;
@@ -30082,7 +30091,7 @@ class GodotLSPClient {
30082
30091
  name: resolvedRootPath
30083
30092
  }
30084
30093
  ]
30085
- });
30094
+ }, this.timeouts.initializeMs);
30086
30095
  const served = this.servedWorkspace;
30087
30096
  if (served !== null && !isSameDirectory(served, resolvedRootPath)) {
30088
30097
  throw new AnotherProjectsServer(this.port, served, resolvedRootPath);
@@ -30268,10 +30277,134 @@ function normalizeLSPError(error, port) {
30268
30277
  if (error.message.includes("ECONNREFUSED") || error.message.includes("Failed to connect to Godot LSP") || error.message.includes("socket closed")) {
30269
30278
  return `Godot LSP is unavailable on port ${port}. Start the Godot editor and enable Language Server in Editor Settings, or set GDHARNESS_LSP_PORT to the port it serves.`;
30270
30279
  }
30280
+ if (error instanceof LSPTimeout && error.method === "initialize") {
30281
+ return `The language server on port ${port} took the connection and did not answer initialize within ${String(error.seconds)}s. Godot reads every script in the project before it answers the first initialize after its editor starts, and goes on reading after this call stops waiting, so call again once the editor has settled.`;
30282
+ }
30271
30283
  return error.message;
30272
30284
  }
30273
30285
  return String(error);
30274
30286
  }
30287
+ var SEVERITIES = ["error", "warning", "information", "hint"];
30288
+ var SYMBOL_KINDS = [
30289
+ "file",
30290
+ "module",
30291
+ "namespace",
30292
+ "package",
30293
+ "class",
30294
+ "method",
30295
+ "property",
30296
+ "field",
30297
+ "constructor",
30298
+ "enum",
30299
+ "interface",
30300
+ "function",
30301
+ "variable",
30302
+ "constant",
30303
+ "string",
30304
+ "number",
30305
+ "boolean",
30306
+ "array",
30307
+ "object",
30308
+ "key",
30309
+ "null",
30310
+ "enum member",
30311
+ "struct",
30312
+ "event",
30313
+ "operator",
30314
+ "type parameter"
30315
+ ];
30316
+ var COMPLETION_KINDS = [
30317
+ "text",
30318
+ "method",
30319
+ "function",
30320
+ "constructor",
30321
+ "field",
30322
+ "variable",
30323
+ "class",
30324
+ "interface",
30325
+ "module",
30326
+ "property",
30327
+ "unit",
30328
+ "value",
30329
+ "enum",
30330
+ "keyword",
30331
+ "snippet",
30332
+ "color",
30333
+ "file",
30334
+ "reference",
30335
+ "folder",
30336
+ "enum member",
30337
+ "constant",
30338
+ "struct",
30339
+ "event",
30340
+ "operator",
30341
+ "type parameter"
30342
+ ];
30343
+ function asRecord(value) {
30344
+ return value !== null && typeof value === "object" && !Array.isArray(value) ? value : {};
30345
+ }
30346
+ function named(value, names) {
30347
+ return typeof value === "number" && Number.isInteger(value) ? names[value - 1] ?? value : value;
30348
+ }
30349
+ function shownPosition(position) {
30350
+ const { line, character } = asRecord(position);
30351
+ return typeof line === "number" && typeof character === "number" ? { line: line + 1, column: character + 1 } : null;
30352
+ }
30353
+ function shownRange(range) {
30354
+ const start = shownPosition(asRecord(range)["start"]);
30355
+ const end = shownPosition(asRecord(range)["end"]);
30356
+ return {
30357
+ ...start === null ? {} : { line: start.line, column: start.column },
30358
+ ...end === null ? {} : { endLine: end.line, endColumn: end.column }
30359
+ };
30360
+ }
30361
+ function shownDiagnostic(entry) {
30362
+ const { range, severity, message, code } = asRecord(entry);
30363
+ return {
30364
+ ...shownRange(range),
30365
+ severity: named(severity, SEVERITIES),
30366
+ message,
30367
+ ...code === undefined || code === null || code === 0 || code === "" ? {} : { code }
30368
+ };
30369
+ }
30370
+ function shownSymbol(entry) {
30371
+ const symbol = asRecord(entry);
30372
+ const whole = asRecord(symbol["range"] ?? asRecord(symbol["location"])["range"]);
30373
+ const at = shownPosition(asRecord(symbol["selectionRange"] ?? whole)["start"]);
30374
+ const last = shownPosition(whole["end"]);
30375
+ const children = Array.isArray(symbol["children"]) ? symbol["children"] : [];
30376
+ const { documentation, native_class: nativeClass, deprecated, detail } = symbol;
30377
+ return {
30378
+ name: symbol["name"],
30379
+ kind: named(symbol["kind"], SYMBOL_KINDS),
30380
+ ...typeof detail === "string" && detail !== "" ? { detail } : {},
30381
+ ...at === null ? {} : { line: at.line, column: at.column },
30382
+ ...last === null ? {} : { lastLine: last.line },
30383
+ ...typeof documentation === "string" && documentation !== "" ? { documentation } : {},
30384
+ ...typeof nativeClass === "string" && nativeClass !== "" ? { nativeClass } : {},
30385
+ ...deprecated === true ? { deprecated } : {},
30386
+ ...children.length > 0 ? { children: children.map(shownSymbol) } : {}
30387
+ };
30388
+ }
30389
+ function shownCompletion(entry) {
30390
+ const { data: _request, kind, ...rest } = asRecord(entry);
30391
+ return { ...rest, kind: named(kind, COMPLETION_KINDS) };
30392
+ }
30393
+ function shownHover(hover) {
30394
+ if (hover === null || typeof hover !== "object") {
30395
+ return hover;
30396
+ }
30397
+ const { range, ...rest } = asRecord(hover);
30398
+ return { ...rest, ...shownRange(range) };
30399
+ }
30400
+ function protocolPosition(args) {
30401
+ const line = args["line"];
30402
+ const character = args["character"];
30403
+ if (typeof line !== "number" || typeof character !== "number" || !Number.isInteger(line) || !Number.isInteger(character) || line < 1 || character < 1) {
30404
+ throw new ArgumentRefusal(`line and character are counted from 1, as the editor shows them; got line ${String(line)} and character ${String(character)}.`);
30405
+ }
30406
+ return { line: line - 1, character: character - 1 };
30407
+ }
30275
30408
  async function resolveLSPPaths(projectPathValue, scriptPathValue) {
30276
30409
  const requestedProjectPath = resolve3(projectPathValue);
30277
30410
  let projectPath;
@@ -30318,29 +30451,21 @@ async function handleLSPTool(client, toolName, args) {
30318
30451
  switch (toolName) {
30319
30452
  case "lsp_get_diagnostics": {
30320
30453
  const diagnostics = await client.getDiagnostics(scriptPath, content);
30321
- return asToolResponse({ diagnostics });
30454
+ return asToolResponse({ diagnostics: diagnostics.map(shownDiagnostic) });
30322
30455
  }
30323
30456
  case "lsp_get_completions": {
30324
- const line = Number(parsedArgs["line"]);
30325
- const character = Number(parsedArgs["character"]);
30326
- if (!Number.isFinite(line) || !Number.isFinite(character)) {
30327
- throw new ArgumentRefusal("Arguments line and character must be numbers.");
30328
- }
30457
+ const { line, character } = protocolPosition(parsedArgs);
30329
30458
  const completions = await client.getCompletions(scriptPath, content, line, character);
30330
- return asToolResponse({ completions });
30459
+ return asToolResponse({ completions: completions.map(shownCompletion) });
30331
30460
  }
30332
30461
  case "lsp_get_hover": {
30333
- const line = Number(parsedArgs["line"]);
30334
- const character = Number(parsedArgs["character"]);
30335
- if (!Number.isFinite(line) || !Number.isFinite(character)) {
30336
- throw new ArgumentRefusal("Arguments line and character must be numbers.");
30337
- }
30462
+ const { line, character } = protocolPosition(parsedArgs);
30338
30463
  const hover = await client.getHover(scriptPath, content, line, character);
30339
- return asToolResponse({ hover });
30464
+ return asToolResponse({ hover: shownHover(hover) });
30340
30465
  }
30341
30466
  case "lsp_get_symbols": {
30342
30467
  const symbols = await client.getDocumentSymbols(scriptPath, content);
30343
- return asToolResponse({ symbols });
30468
+ return asToolResponse({ symbols: symbols.map(shownSymbol) });
30344
30469
  }
30345
30470
  default:
30346
30471
  return asToolResponse({ error: `Unknown LSP tool: ${toolName}` });
@@ -30350,7 +30475,8 @@ async function handleLSPTool(client, toolName, args) {
30350
30475
  error: normalizeLSPError(error, client.port),
30351
30476
  ...error instanceof AnotherProjectsServer ? { serves: error.serves } : {},
30352
30477
  ...error instanceof ArgumentRefusal ? { refusedArguments: true } : {},
30353
- ...error instanceof NoDiagnosticsPublished ? { aboutThisFile: true } : {}
30478
+ ...error instanceof NoDiagnosticsPublished ? { aboutThisFile: true } : {},
30479
+ ...error instanceof LSPTimeout && error.method === "initialize" ? { stillReading: true } : {}
30354
30480
  });
30355
30481
  }
30356
30482
  }
@@ -33507,8 +33633,16 @@ var TOOL_SPECS = [
33507
33633
  type: "boolean",
33508
33634
  description: "structure: also list what the script inherits, walking extends through the project's other scripts. Each such member carries inherited_from, the file declaring it, and the answer gains inherits_from, the chain that was walked. A name the script overrides is listed at both its lines rather than once. A native base is not walked, since it declares nothing in a file: editor_classes info answers for those. Default false."
33509
33635
  },
33510
- line: { type: "integer", minimum: 0, description: "completion, hover: zero-based line." },
33511
- character: { type: "integer", minimum: 0, description: "completion, hover: zero-based column." }
33636
+ line: {
33637
+ type: "integer",
33638
+ minimum: 1,
33639
+ description: "completion, hover: the line, counted from 1 as the editor shows it."
33640
+ },
33641
+ character: {
33642
+ type: "integer",
33643
+ minimum: 1,
33644
+ description: "completion, hover: the column, counted from 1 as the editor shows it, with a tab counted as one. symbols gives each name at the line and column hover is asked at."
33645
+ }
33512
33646
  },
33513
33647
  requires: ["projectPath", "scriptPath"],
33514
33648
  operations: {
@@ -33516,7 +33650,10 @@ var TOOL_SPECS = [
33516
33650
  summary: "functions, variables, signals, class_name and extends, read from the file",
33517
33651
  requires: []
33518
33652
  },
33519
- symbols: { summary: "document symbols from the language server", requires: [] },
33653
+ symbols: {
33654
+ summary: "document symbols from the language server, each with its kind, the line and column of its name, and the lastLine of its declaration",
33655
+ requires: []
33656
+ },
33520
33657
  completion: { summary: "completions at a position", requires: ["line", "character"] },
33521
33658
  hover: { summary: "hover text at a position", requires: ["line", "character"] }
33522
33659
  },
@@ -33524,13 +33661,13 @@ var TOOL_SPECS = [
33524
33661
  },
33525
33662
  {
33526
33663
  name: "script_diagnostics",
33527
- description: "Errors and warnings for a script, or for several, from the editor's language server, and whether each is clean. Needs the editor running. Diagnostics are checked against the project's own files, and the two that the files disprove are named rather than passed on: a member the class cache's file declares, a method, property, constant, static function or enum member, comes back under contradictedByTheFile, and a class this project declares that the diagnostic could not resolve comes back under typesTheEditorHasNotLoaded, with inTheClassCache saying whether a launched game would resolve it. staleAnalysis then says what to do, and for both states it is editor_rescan first: it asks the editor already running, starts nothing, and has cleared each in every reproduction measured. Two exceptions are named when they apply. For an enum member, constant or static function of a global class the reload comes first: in the one project that has reproduced a stale enum member, editor_rescan with reloadScript on the declaring script cleared it and a plain rescan did not. And an editor whose last scan wrote the class cache shorter than the file holds a list behind the files and writes it on every scan, so on that editor the rescan loads nothing and staleAnalysis says editor_launch restart instead. What follows differs: for a stale analysed type, editor_rescan with reloadScript recompiles the held copy and editor_launch restart is the last resort that costs a window; for a class missing from the cache, project_import refresh_classes rewrites the cache from disk at the cost of a short headless engine, which matters beside a running bench.",
33664
+ description: "Errors and warnings for a script, or for several, from the editor's language server, and whether each is clean. Needs the editor running. Each diagnostic carries its severity (error or warning), its message, and where it is as the editor shows it: line and column counted from 1, and endLine and endColumn just past its end. Diagnostics are checked against the project's own files, and the two that the files disprove are named rather than passed on: a member the class cache's file declares, a method, property, constant, static function or enum member, comes back under contradictedByTheFile, and a class this project declares that the diagnostic could not resolve comes back under typesTheEditorHasNotLoaded, with inTheClassCache saying whether a launched game would resolve it. staleAnalysis then says what to do, and for both states it is editor_rescan first: it asks the editor already running, starts nothing, and has cleared each in every reproduction measured. Two exceptions are named when they apply. For an enum member, constant or static function of a global class the reload comes first: in the one project that has reproduced a stale enum member, editor_rescan with reloadScript on the declaring script cleared it and a plain rescan did not. And an editor whose last scan wrote the class cache shorter than the file holds a list behind the files and writes it on every scan, so on that editor the rescan loads nothing and staleAnalysis says editor_launch restart instead. What follows differs: for a stale analysed type, editor_rescan with reloadScript recompiles the held copy and editor_launch restart is the last resort that costs a window; for a class missing from the cache, project_import refresh_classes rewrites the cache from disk at the cost of a short headless engine, which matters beside a running bench.",
33528
33665
  parameters: {
33529
33666
  projectPath: PROJECT_PATH,
33530
33667
  scriptPath: {
33531
33668
  type: ["string", "array"],
33532
33669
  items: { type: "string" },
33533
- description: 'A script inside the project, a folder, or a list of either, such as "scripts/player.gd" or ["scripts/economy", "ui/shop.gd"]. A folder is its .gd files at any depth, skipping what the engine skips. One script is answered as it always was; several are answered together under scripts, each in that same shape, with clean, errors and warnings for all of them beside it, and a script that could not be read is named there with why while the rest are still asked about. At most 300 scripts a call.'
33670
+ description: 'A script inside the project, a folder, or a list of either, such as "scripts/player.gd" or ["scripts/economy", "ui/shop.gd"]. A folder is its .gd files at any depth, skipping what the engine skips. One script is answered as it always was. Several are answered together, with clean, errors and warnings for all of them: a script with nothing to report is named in nothingReported, and every other is answered under scripts in that same shape, including a script that could not be read, named with why while the rest are still asked about. Any number of scripts is one call, which reports progress as it goes.'
33534
33671
  }
33535
33672
  },
33536
33673
  requires: ["projectPath", "scriptPath"]
@@ -35072,7 +35209,6 @@ function testPathsIn(projectPath, given) {
35072
35209
  value: unique.filter((path) => !unique.some((other) => other !== path && within(path, other))).map((path) => `res://${path}`)
35073
35210
  };
35074
35211
  }
35075
- var MOST_DIAGNOSED = 300;
35076
35212
  function scriptsToDiagnose(projectPath, given) {
35077
35213
  const listed = Array.isArray(given);
35078
35214
  if (!listed) {
@@ -35116,12 +35252,6 @@ function scriptsToDiagnose(projectPath, given) {
35116
35252
  if (unique.length === 0) {
35117
35253
  return { ok: false, reason: `No .gd script is under ${empty.join(" or ")}.` };
35118
35254
  }
35119
- if (unique.length > MOST_DIAGNOSED) {
35120
- return {
35121
- ok: false,
35122
- reason: `scriptPath names ${unique.length} scripts, and one call asks about at most ${MOST_DIAGNOSED}, each a round trip through the language server: name narrower folders, or call again for the rest.`
35123
- };
35124
- }
35125
35255
  return { ok: true, value: unique };
35126
35256
  }
35127
35257
  function isDirectory(path) {
@@ -36987,11 +37117,16 @@ class GodotServer {
36987
37117
  return "refusal" in one ? one.refusal : this.jsonTextResponse(this.markedIfStale(one.answer));
36988
37118
  }
36989
37119
  const progress = callProgress();
36990
- const answers = [];
37120
+ const cancelled = callSignal();
37121
+ const reported = [];
37122
+ const nothingReported = [];
36991
37123
  let errors = 0;
36992
37124
  let warnings = 0;
36993
37125
  let unread = 0;
36994
37126
  for (const [at, scriptPath] of scripts.value.entries()) {
37127
+ if (cancelled?.aborted === true) {
37128
+ return this.createErrorResponse(`Cancelled after ${at} of ${scripts.value.length} scripts.`);
37129
+ }
36995
37130
  progress?.(at + 1, `${at + 1} of ${scripts.value.length}: ${scriptPath}`);
36996
37131
  const one = await this.diagnosticsOf({ ...args, scriptPath });
36997
37132
  if ("refusal" in one) {
@@ -36999,20 +37134,25 @@ class GodotServer {
36999
37134
  return one.refusal;
37000
37135
  }
37001
37136
  unread += 1;
37002
- answers.push({ scriptPath, error: one.refusal.content[0]?.text ?? "not read" });
37137
+ reported.push({ scriptPath, error: one.refusal.content[0]?.text ?? "not read" });
37003
37138
  continue;
37004
37139
  }
37005
37140
  errors += Number(one.answer["errors"]);
37006
37141
  warnings += Number(one.answer["warnings"]);
37007
- answers.push(one.answer);
37142
+ if (readArray(one.answer, "diagnostics")?.length === 0) {
37143
+ nothingReported.push(scriptPath);
37144
+ } else {
37145
+ reported.push(one.answer);
37146
+ }
37008
37147
  }
37009
37148
  return this.jsonTextResponse(this.markedIfStale({
37010
37149
  clean: errors === 0 && unread === 0,
37011
- scriptsChecked: answers.length,
37150
+ scriptsChecked: scripts.value.length,
37012
37151
  errors,
37013
37152
  warnings,
37014
37153
  ...unread > 0 ? { scriptsNotRead: unread } : {},
37015
- scripts: answers
37154
+ scripts: reported,
37155
+ nothingReported
37016
37156
  }));
37017
37157
  }
37018
37158
  markedIfStale(answer) {
@@ -37026,6 +37166,9 @@ class GodotServer {
37026
37166
  if (payload["refusedArguments"] === true) {
37027
37167
  return { refusal: this.createErrorResponse(reason), perScript: true };
37028
37168
  }
37169
+ if (payload["stillReading"] === true) {
37170
+ return { refusal: this.createErrorResponse(`Diagnostics unavailable: ${reason}`), perScript: false };
37171
+ }
37029
37172
  if (typeof payload["serves"] === "string") {
37030
37173
  return {
37031
37174
  refusal: this.createErrorResponse(`Diagnostics unavailable: ${reason}`, [
@@ -37045,10 +37188,7 @@ class GodotServer {
37045
37188
  };
37046
37189
  }
37047
37190
  const diagnostics = readArray(payload, "diagnostics") ?? [];
37048
- const errors = diagnostics.filter((entry) => {
37049
- const severity = asParams(entry)["severity"];
37050
- return severity === 1 || severity === "error" || severity === "ERROR";
37051
- }).length;
37191
+ const errors = diagnostics.filter((entry) => asParams(entry)["severity"] === "error").length;
37052
37192
  return {
37053
37193
  answer: {
37054
37194
  scriptPath: readString(args, "scriptPath"),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gdharness",
3
- "version": "1.1.51",
3
+ "version": "1.1.52",
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",