gdharness 0.13.8 → 0.13.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/build/cli.js CHANGED
@@ -15740,7 +15740,7 @@ var init_tool_definitions = __esm(() => {
15740
15740
  {
15741
15741
  name: "editor_launch",
15742
15742
  description: "Opens the Godot editor on a project, in a window on this machine, or restarts the one already connected. An editor goes on serving the addon it read at startup, so restart is what puts a gdharness upgrade into effect; it saves open scenes on the way out and answers with the version that came back. Only an editor with a window can be restarted, because the engine hands back none of the arguments it was started with. editor_status says which editor is connected and whether it is holding an old addon. A restart saves project.godot on the way out, and Godot writes only what differs from its own defaults, so a key named deliberately at its default value is dropped: settingsDropped names any that went, because nothing else will say so until something depends on one.",
15743
- parameters: { projectPath: PROJECT_PATH },
15743
+ parameters: { projectPath: { ...PROJECT_PATH, ops: ["open"] } },
15744
15744
  requires: [],
15745
15745
  operations: {
15746
15746
  open: { summary: "open the editor on a project", requires: ["projectPath"] },
@@ -15752,7 +15752,7 @@ var init_tool_definitions = __esm(() => {
15752
15752
  name: "editor_run",
15753
15753
  description: "The run: starting the project, stopping it, or booting it once to see whether it comes up clean. start keeps it running and collecting output until it quits or is stopped, windowed where there is a display and headless where there is not, unless headless says otherwise; only runtime_capture needs the window. A run that quits on its own is kept, so a scene that prints an answer and quits is start, then editor_output until running is false. What runs is a scene: a SceneTree script is not an entry point here, so put the script on the root of a scene of its own and name that in scene. args hands the game its own flags, the ones it reads with OS.get_cmdline_user_args(), and a run carrying any is started by this server rather than by the editor. check boots it headless for a few frames, waits for it to quit, and answers with the verdict: whether it came up, and every error and warning it printed on the way. A start waits for the game to become something the runtime_* tools can talk to and says which it is under runtime: listening with the port it took, or why not, so the first call after a start does not have to be made twice. When it is not listening, mayYetAnnounce says whether that is final: false is a runtime that is not coming, true is a game still on its way up, which editor_status will see and runtimeWaitMs waits longer for. A start this server made also answers with transcript, the file both the run's streams are written to, so a watch on it can be armed off the start rather than off a second call.",
15754
15754
  parameters: {
15755
- projectPath: PROJECT_PATH,
15755
+ projectPath: { ...PROJECT_PATH, ops: ["start", "check"] },
15756
15756
  scene: {
15757
15757
  type: "string",
15758
15758
  ops: ["start", "check"],
@@ -15781,11 +15781,17 @@ var init_tool_definitions = __esm(() => {
15781
15781
  description: "check: how long to give the boot before it is called hung. Default 60000. wait: how long to wait for the run to end before answering anyway. Default 600000."
15782
15782
  }
15783
15783
  },
15784
- requires: ["projectPath"],
15784
+ requires: [],
15785
15785
  operations: {
15786
- start: { summary: "run the project until it quits or is stopped", requires: [] },
15786
+ start: {
15787
+ summary: "run the project until it quits or is stopped",
15788
+ requires: ["projectPath"]
15789
+ },
15787
15790
  stop: { summary: "end the run and answer with what it printed last", requires: [] },
15788
- check: { summary: "boot headless, quit after a few frames, and report the verdict", requires: [] },
15791
+ check: {
15792
+ summary: "boot headless, quit after a few frames, and report the verdict",
15793
+ requires: ["projectPath"]
15794
+ },
15789
15795
  wait: { summary: "wait for the run to end, then answer as editor_output does", requires: [] }
15790
15796
  },
15791
15797
  defaultOperation: "start"
@@ -31168,6 +31174,7 @@ class GodotDAPClient {
31168
31174
  pendingRequests;
31169
31175
  reader = new FrameReader;
31170
31176
  outputBuffer = [];
31177
+ onOutput = null;
31171
31178
  maxOutputLines = 1000;
31172
31179
  initialized = false;
31173
31180
  attached = false;
@@ -31335,6 +31342,13 @@ class GodotDAPClient {
31335
31342
  if (outputText.length > 0) {
31336
31343
  const lines = outputText.split(/\r?\n/).filter((line) => line.length > 0);
31337
31344
  this.outputBuffer.push(...lines);
31345
+ if (this.onOutput !== null) {
31346
+ for (const line of lines) {
31347
+ try {
31348
+ this.onOutput(line);
31349
+ } catch {}
31350
+ }
31351
+ }
31338
31352
  }
31339
31353
  if (this.outputBuffer.length > this.maxOutputLines) {
31340
31354
  this.outputBuffer = this.outputBuffer.slice(this.outputBuffer.length - this.maxOutputLines);
@@ -31391,6 +31405,9 @@ class GodotDAPClient {
31391
31405
  await this.sendRequest("configurationDone", {});
31392
31406
  this.attached = true;
31393
31407
  }
31408
+ setOutputSink(sink) {
31409
+ this.onOutput = sink;
31410
+ }
31394
31411
  getOutput(clear = false) {
31395
31412
  const lines = [...this.outputBuffer];
31396
31413
  if (clear) {
@@ -36435,6 +36452,34 @@ function runningAs(pid) {
36435
36452
  return null;
36436
36453
  }
36437
36454
  }
36455
+ function listeningPid(port) {
36456
+ try {
36457
+ if (process.platform === "win32") {
36458
+ const said = execFileSync("powershell", [
36459
+ "-NoProfile",
36460
+ "-NonInteractive",
36461
+ "-Command",
36462
+ `(Get-NetTCPConnection -State Listen -LocalPort ${port} -ErrorAction SilentlyContinue | Select-Object -First 1).OwningProcess`
36463
+ ], { encoding: "utf8", timeout: 15000 }).trim();
36464
+ return /^\d+$/.test(said) ? Number(said) : null;
36465
+ }
36466
+ for (const [command, args, pattern] of [
36467
+ ["lsof", ["-nP", `-iTCP:${port}`, "-sTCP:LISTEN", "-t"], /^(\d+)/],
36468
+ ["ss", ["-ltnpH", `sport = :${port}`], /pid=(\d+)/]
36469
+ ]) {
36470
+ try {
36471
+ const said = execFileSync(command, [...args], { encoding: "utf8", timeout: 15000 }).trim();
36472
+ const found = pattern.exec(said);
36473
+ if (found?.[1] !== undefined) {
36474
+ return Number(found[1]);
36475
+ }
36476
+ } catch {}
36477
+ }
36478
+ return null;
36479
+ } catch {
36480
+ return null;
36481
+ }
36482
+ }
36438
36483
  function windowsCommandLine(pid) {
36439
36484
  try {
36440
36485
  const answer = execFileSync("powershell", [
@@ -36567,8 +36612,7 @@ import {
36567
36612
  readdirSync as readdirSync7,
36568
36613
  readFileSync as readFileSync15,
36569
36614
  readSync,
36570
- rmSync as rmSync9,
36571
- statSync as statSync4
36615
+ rmSync as rmSync9
36572
36616
  } from "node:fs";
36573
36617
  import { tmpdir as tmpdir5 } from "node:os";
36574
36618
  import { basename as basename3, dirname as dirname9, join as join17, normalize as normalize3 } from "node:path";
@@ -37122,9 +37166,10 @@ class GodotServer {
37122
37166
  if (op !== null) {
37123
37167
  const elsewhere = Object.keys(args).filter((key) => key !== "op" && !opTakes(spec, op, key));
37124
37168
  if (elsewhere.length > 0) {
37169
+ const takes = argumentsOf(spec, op);
37125
37170
  return {
37126
37171
  ok: false,
37127
- response: this.createErrorResponse(`${spec.name} ${op} does not take ${elsewhere.join(", ")}. ${op} takes: ${argumentsOf(spec, op).join(", ")}.`)
37172
+ response: this.createErrorResponse(`${spec.name} ${op} does not take ${elsewhere.join(", ")}. ${takes.length === 0 ? `${op} takes no arguments.` : `${op} takes: ${takes.join(", ")}.`}`)
37128
37173
  };
37129
37174
  }
37130
37175
  }
@@ -37869,14 +37914,45 @@ class GodotServer {
37869
37914
  async handleDAP(toolName, args) {
37870
37915
  return handleDAPTool(this.dap(), toolName, args);
37871
37916
  }
37917
+ adapterBelongsElsewhere() {
37918
+ const status = this.godotBridge.getStatus();
37919
+ const ours = status.editorPid;
37920
+ if (ours === undefined) {
37921
+ return null;
37922
+ }
37923
+ const port = this.editorServes("dapPort", "GDHARNESS_DAP_PORT", DEFAULT_DAP_PORT);
37924
+ const holder = listeningPid(port);
37925
+ if (holder === null || holder === ours) {
37926
+ return null;
37927
+ }
37928
+ return `The debug adapter on port ${port} belongs to process ${holder}, and the editor on this server's bridge is process ${ours}. Godot gives every editor the same debug adapter port by default, so the one this editor names in its settings is not necessarily the one it holds. Playing the game now would put another editor's console and another project's debugger under this project's name.`;
37929
+ }
37872
37930
  dap() {
37873
37931
  const port = this.editorServes("dapPort", "GDHARNESS_DAP_PORT", DEFAULT_DAP_PORT);
37874
37932
  if (this.dapClient !== null && this.dapClient.port !== port) {
37875
37933
  this.dapClient = null;
37876
37934
  }
37877
- this.dapClient ??= new GodotDAPClient(port);
37935
+ if (this.dapClient === null) {
37936
+ this.dapClient = new GodotDAPClient(port);
37937
+ this.dapClient.setOutputSink((line) => {
37938
+ this.writeToTranscript(line);
37939
+ });
37940
+ }
37878
37941
  return this.dapClient;
37879
37942
  }
37943
+ writeToTranscript(line) {
37944
+ const run = this.activeProcess;
37945
+ if (run?.throughEditor !== true || run.transcript === null) {
37946
+ return;
37947
+ }
37948
+ try {
37949
+ appendFileSync(run.transcript, line.endsWith(`
37950
+ `) ? line : `${line}
37951
+ `);
37952
+ } catch (error) {
37953
+ this.logDebug(`Could not write an editor-played run's transcript: ${errorMessage(error)}`);
37954
+ }
37955
+ }
37880
37956
  editorServes(field, variable, fallback) {
37881
37957
  const named = portFromEnvOrNull(variable);
37882
37958
  if (named !== null) {
@@ -38198,6 +38274,14 @@ class GodotServer {
38198
38274
  "GDHARNESS_DAP_PORT points this server at another one"
38199
38275
  ]);
38200
38276
  }
38277
+ const stranger = this.adapterBelongsElsewhere();
38278
+ if (stranger !== null) {
38279
+ return this.createErrorResponse(stranger, [
38280
+ "editor_status names the editor on the bridge and the port it says it serves",
38281
+ "GDHARNESS_DAP_PORT points this server at the right one",
38282
+ "Closing the other editor, or moving its debug adapter port in its editor settings, frees this one"
38283
+ ]);
38284
+ }
38201
38285
  const answer = await this.handleViaBridge("play_scene", scene === null ? {} : { scenePath: `res://${scene}` });
38202
38286
  if (answer.isError === true) {
38203
38287
  return answer;
@@ -38235,20 +38319,11 @@ class GodotServer {
38235
38319
  if (!game.throughEditor || !this.dapClient) {
38236
38320
  return;
38237
38321
  }
38238
- const heard = [];
38239
- for (const line of this.dapClient.getOutput(true)) {
38240
- const ended = line.endsWith(`
38322
+ if (game.transcript === null) {
38323
+ for (const line of this.dapClient.getOutput(true)) {
38324
+ game.log.append("stdout", line.endsWith(`
38241
38325
  `) ? line : `${line}
38242
- `;
38243
- game.log.append("stdout", ended);
38244
- heard.push(ended);
38245
- }
38246
- if (heard.length > 0 && game.transcript !== null) {
38247
- try {
38248
- appendFileSync(game.transcript, heard.join(""));
38249
- game.readOffset = statSync4(game.transcript).size;
38250
- } catch (error) {
38251
- this.logDebug(`Could not write an editor-played run's transcript: ${errorMessage(error)}`);
38326
+ `);
38252
38327
  }
38253
38328
  }
38254
38329
  if (!this.dapClient.isConnected()) {
package/build/index.js CHANGED
@@ -10152,8 +10152,7 @@ import {
10152
10152
  readdirSync as readdirSync6,
10153
10153
  readFileSync as readFileSync11,
10154
10154
  readSync,
10155
- rmSync as rmSync5,
10156
- statSync as statSync4
10155
+ rmSync as rmSync5
10157
10156
  } from "node:fs";
10158
10157
  import { tmpdir as tmpdir4 } from "node:os";
10159
10158
  import { basename as basename2, dirname as dirname4, join as join11, normalize as normalize2 } from "node:path";
@@ -25011,6 +25010,7 @@ class GodotDAPClient {
25011
25010
  pendingRequests;
25012
25011
  reader = new FrameReader;
25013
25012
  outputBuffer = [];
25013
+ onOutput = null;
25014
25014
  maxOutputLines = 1000;
25015
25015
  initialized = false;
25016
25016
  attached = false;
@@ -25178,6 +25178,13 @@ class GodotDAPClient {
25178
25178
  if (outputText.length > 0) {
25179
25179
  const lines = outputText.split(/\r?\n/).filter((line) => line.length > 0);
25180
25180
  this.outputBuffer.push(...lines);
25181
+ if (this.onOutput !== null) {
25182
+ for (const line of lines) {
25183
+ try {
25184
+ this.onOutput(line);
25185
+ } catch {}
25186
+ }
25187
+ }
25181
25188
  }
25182
25189
  if (this.outputBuffer.length > this.maxOutputLines) {
25183
25190
  this.outputBuffer = this.outputBuffer.slice(this.outputBuffer.length - this.maxOutputLines);
@@ -25234,6 +25241,9 @@ class GodotDAPClient {
25234
25241
  await this.sendRequest("configurationDone", {});
25235
25242
  this.attached = true;
25236
25243
  }
25244
+ setOutputSink(sink) {
25245
+ this.onOutput = sink;
25246
+ }
25237
25247
  getOutput(clear = false) {
25238
25248
  const lines = [...this.outputBuffer];
25239
25249
  if (clear) {
@@ -28089,6 +28099,34 @@ function runningAs(pid) {
28089
28099
  return null;
28090
28100
  }
28091
28101
  }
28102
+ function listeningPid(port) {
28103
+ try {
28104
+ if (process.platform === "win32") {
28105
+ const said = execFileSync("powershell", [
28106
+ "-NoProfile",
28107
+ "-NonInteractive",
28108
+ "-Command",
28109
+ `(Get-NetTCPConnection -State Listen -LocalPort ${port} -ErrorAction SilentlyContinue | Select-Object -First 1).OwningProcess`
28110
+ ], { encoding: "utf8", timeout: 15000 }).trim();
28111
+ return /^\d+$/.test(said) ? Number(said) : null;
28112
+ }
28113
+ for (const [command, args, pattern] of [
28114
+ ["lsof", ["-nP", `-iTCP:${port}`, "-sTCP:LISTEN", "-t"], /^(\d+)/],
28115
+ ["ss", ["-ltnpH", `sport = :${port}`], /pid=(\d+)/]
28116
+ ]) {
28117
+ try {
28118
+ const said = execFileSync(command, [...args], { encoding: "utf8", timeout: 15000 }).trim();
28119
+ const found = pattern.exec(said);
28120
+ if (found?.[1] !== undefined) {
28121
+ return Number(found[1]);
28122
+ }
28123
+ } catch {}
28124
+ }
28125
+ return null;
28126
+ } catch {
28127
+ return null;
28128
+ }
28129
+ }
28092
28130
  function windowsCommandLine(pid) {
28093
28131
  try {
28094
28132
  const answer = execFileSync("powershell", [
@@ -28792,7 +28830,7 @@ var TOOL_SPECS = [
28792
28830
  {
28793
28831
  name: "editor_launch",
28794
28832
  description: "Opens the Godot editor on a project, in a window on this machine, or restarts the one already connected. An editor goes on serving the addon it read at startup, so restart is what puts a gdharness upgrade into effect; it saves open scenes on the way out and answers with the version that came back. Only an editor with a window can be restarted, because the engine hands back none of the arguments it was started with. editor_status says which editor is connected and whether it is holding an old addon. A restart saves project.godot on the way out, and Godot writes only what differs from its own defaults, so a key named deliberately at its default value is dropped: settingsDropped names any that went, because nothing else will say so until something depends on one.",
28795
- parameters: { projectPath: PROJECT_PATH },
28833
+ parameters: { projectPath: { ...PROJECT_PATH, ops: ["open"] } },
28796
28834
  requires: [],
28797
28835
  operations: {
28798
28836
  open: { summary: "open the editor on a project", requires: ["projectPath"] },
@@ -28804,7 +28842,7 @@ var TOOL_SPECS = [
28804
28842
  name: "editor_run",
28805
28843
  description: "The run: starting the project, stopping it, or booting it once to see whether it comes up clean. start keeps it running and collecting output until it quits or is stopped, windowed where there is a display and headless where there is not, unless headless says otherwise; only runtime_capture needs the window. A run that quits on its own is kept, so a scene that prints an answer and quits is start, then editor_output until running is false. What runs is a scene: a SceneTree script is not an entry point here, so put the script on the root of a scene of its own and name that in scene. args hands the game its own flags, the ones it reads with OS.get_cmdline_user_args(), and a run carrying any is started by this server rather than by the editor. check boots it headless for a few frames, waits for it to quit, and answers with the verdict: whether it came up, and every error and warning it printed on the way. A start waits for the game to become something the runtime_* tools can talk to and says which it is under runtime: listening with the port it took, or why not, so the first call after a start does not have to be made twice. When it is not listening, mayYetAnnounce says whether that is final: false is a runtime that is not coming, true is a game still on its way up, which editor_status will see and runtimeWaitMs waits longer for. A start this server made also answers with transcript, the file both the run's streams are written to, so a watch on it can be armed off the start rather than off a second call.",
28806
28844
  parameters: {
28807
- projectPath: PROJECT_PATH,
28845
+ projectPath: { ...PROJECT_PATH, ops: ["start", "check"] },
28808
28846
  scene: {
28809
28847
  type: "string",
28810
28848
  ops: ["start", "check"],
@@ -28833,11 +28871,17 @@ var TOOL_SPECS = [
28833
28871
  description: "check: how long to give the boot before it is called hung. Default 60000. wait: how long to wait for the run to end before answering anyway. Default 600000."
28834
28872
  }
28835
28873
  },
28836
- requires: ["projectPath"],
28874
+ requires: [],
28837
28875
  operations: {
28838
- start: { summary: "run the project until it quits or is stopped", requires: [] },
28876
+ start: {
28877
+ summary: "run the project until it quits or is stopped",
28878
+ requires: ["projectPath"]
28879
+ },
28839
28880
  stop: { summary: "end the run and answer with what it printed last", requires: [] },
28840
- check: { summary: "boot headless, quit after a few frames, and report the verdict", requires: [] },
28881
+ check: {
28882
+ summary: "boot headless, quit after a few frames, and report the verdict",
28883
+ requires: ["projectPath"]
28884
+ },
28841
28885
  wait: { summary: "wait for the run to end, then answer as editor_output does", requires: [] }
28842
28886
  },
28843
28887
  defaultOperation: "start"
@@ -29876,9 +29920,10 @@ class GodotServer {
29876
29920
  if (op !== null) {
29877
29921
  const elsewhere = Object.keys(args).filter((key) => key !== "op" && !opTakes(spec, op, key));
29878
29922
  if (elsewhere.length > 0) {
29923
+ const takes = argumentsOf(spec, op);
29879
29924
  return {
29880
29925
  ok: false,
29881
- response: this.createErrorResponse(`${spec.name} ${op} does not take ${elsewhere.join(", ")}. ${op} takes: ${argumentsOf(spec, op).join(", ")}.`)
29926
+ response: this.createErrorResponse(`${spec.name} ${op} does not take ${elsewhere.join(", ")}. ${takes.length === 0 ? `${op} takes no arguments.` : `${op} takes: ${takes.join(", ")}.`}`)
29882
29927
  };
29883
29928
  }
29884
29929
  }
@@ -30623,14 +30668,45 @@ class GodotServer {
30623
30668
  async handleDAP(toolName, args) {
30624
30669
  return handleDAPTool(this.dap(), toolName, args);
30625
30670
  }
30671
+ adapterBelongsElsewhere() {
30672
+ const status = this.godotBridge.getStatus();
30673
+ const ours = status.editorPid;
30674
+ if (ours === undefined) {
30675
+ return null;
30676
+ }
30677
+ const port = this.editorServes("dapPort", "GDHARNESS_DAP_PORT", DEFAULT_DAP_PORT);
30678
+ const holder = listeningPid(port);
30679
+ if (holder === null || holder === ours) {
30680
+ return null;
30681
+ }
30682
+ return `The debug adapter on port ${port} belongs to process ${holder}, and the editor on this server's bridge is process ${ours}. Godot gives every editor the same debug adapter port by default, so the one this editor names in its settings is not necessarily the one it holds. Playing the game now would put another editor's console and another project's debugger under this project's name.`;
30683
+ }
30626
30684
  dap() {
30627
30685
  const port = this.editorServes("dapPort", "GDHARNESS_DAP_PORT", DEFAULT_DAP_PORT);
30628
30686
  if (this.dapClient !== null && this.dapClient.port !== port) {
30629
30687
  this.dapClient = null;
30630
30688
  }
30631
- this.dapClient ??= new GodotDAPClient(port);
30689
+ if (this.dapClient === null) {
30690
+ this.dapClient = new GodotDAPClient(port);
30691
+ this.dapClient.setOutputSink((line) => {
30692
+ this.writeToTranscript(line);
30693
+ });
30694
+ }
30632
30695
  return this.dapClient;
30633
30696
  }
30697
+ writeToTranscript(line) {
30698
+ const run = this.activeProcess;
30699
+ if (run?.throughEditor !== true || run.transcript === null) {
30700
+ return;
30701
+ }
30702
+ try {
30703
+ appendFileSync(run.transcript, line.endsWith(`
30704
+ `) ? line : `${line}
30705
+ `);
30706
+ } catch (error) {
30707
+ this.logDebug(`Could not write an editor-played run's transcript: ${errorMessage(error)}`);
30708
+ }
30709
+ }
30634
30710
  editorServes(field, variable, fallback) {
30635
30711
  const named = portFromEnvOrNull(variable);
30636
30712
  if (named !== null) {
@@ -30952,6 +31028,14 @@ class GodotServer {
30952
31028
  "GDHARNESS_DAP_PORT points this server at another one"
30953
31029
  ]);
30954
31030
  }
31031
+ const stranger = this.adapterBelongsElsewhere();
31032
+ if (stranger !== null) {
31033
+ return this.createErrorResponse(stranger, [
31034
+ "editor_status names the editor on the bridge and the port it says it serves",
31035
+ "GDHARNESS_DAP_PORT points this server at the right one",
31036
+ "Closing the other editor, or moving its debug adapter port in its editor settings, frees this one"
31037
+ ]);
31038
+ }
30955
31039
  const answer = await this.handleViaBridge("play_scene", scene === null ? {} : { scenePath: `res://${scene}` });
30956
31040
  if (answer.isError === true) {
30957
31041
  return answer;
@@ -30989,20 +31073,11 @@ class GodotServer {
30989
31073
  if (!game.throughEditor || !this.dapClient) {
30990
31074
  return;
30991
31075
  }
30992
- const heard = [];
30993
- for (const line of this.dapClient.getOutput(true)) {
30994
- const ended = line.endsWith(`
31076
+ if (game.transcript === null) {
31077
+ for (const line of this.dapClient.getOutput(true)) {
31078
+ game.log.append("stdout", line.endsWith(`
30995
31079
  `) ? line : `${line}
30996
- `;
30997
- game.log.append("stdout", ended);
30998
- heard.push(ended);
30999
- }
31000
- if (heard.length > 0 && game.transcript !== null) {
31001
- try {
31002
- appendFileSync(game.transcript, heard.join(""));
31003
- game.readOffset = statSync4(game.transcript).size;
31004
- } catch (error) {
31005
- this.logDebug(`Could not write an editor-played run's transcript: ${errorMessage(error)}`);
31080
+ `);
31006
31081
  }
31007
31082
  }
31008
31083
  if (!this.dapClient.isConnected()) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gdharness",
3
- "version": "0.13.8",
3
+ "version": "0.13.10",
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",