gdharness 0.13.8 → 0.13.9

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) {
@@ -36567,8 +36584,7 @@ import {
36567
36584
  readdirSync as readdirSync7,
36568
36585
  readFileSync as readFileSync15,
36569
36586
  readSync,
36570
- rmSync as rmSync9,
36571
- statSync as statSync4
36587
+ rmSync as rmSync9
36572
36588
  } from "node:fs";
36573
36589
  import { tmpdir as tmpdir5 } from "node:os";
36574
36590
  import { basename as basename3, dirname as dirname9, join as join17, normalize as normalize3 } from "node:path";
@@ -37122,9 +37138,10 @@ class GodotServer {
37122
37138
  if (op !== null) {
37123
37139
  const elsewhere = Object.keys(args).filter((key) => key !== "op" && !opTakes(spec, op, key));
37124
37140
  if (elsewhere.length > 0) {
37141
+ const takes = argumentsOf(spec, op);
37125
37142
  return {
37126
37143
  ok: false,
37127
- response: this.createErrorResponse(`${spec.name} ${op} does not take ${elsewhere.join(", ")}. ${op} takes: ${argumentsOf(spec, op).join(", ")}.`)
37144
+ response: this.createErrorResponse(`${spec.name} ${op} does not take ${elsewhere.join(", ")}. ${takes.length === 0 ? `${op} takes no arguments.` : `${op} takes: ${takes.join(", ")}.`}`)
37128
37145
  };
37129
37146
  }
37130
37147
  }
@@ -37874,9 +37891,27 @@ class GodotServer {
37874
37891
  if (this.dapClient !== null && this.dapClient.port !== port) {
37875
37892
  this.dapClient = null;
37876
37893
  }
37877
- this.dapClient ??= new GodotDAPClient(port);
37894
+ if (this.dapClient === null) {
37895
+ this.dapClient = new GodotDAPClient(port);
37896
+ this.dapClient.setOutputSink((line) => {
37897
+ this.writeToTranscript(line);
37898
+ });
37899
+ }
37878
37900
  return this.dapClient;
37879
37901
  }
37902
+ writeToTranscript(line) {
37903
+ const run = this.activeProcess;
37904
+ if (run?.throughEditor !== true || run.transcript === null) {
37905
+ return;
37906
+ }
37907
+ try {
37908
+ appendFileSync(run.transcript, line.endsWith(`
37909
+ `) ? line : `${line}
37910
+ `);
37911
+ } catch (error) {
37912
+ this.logDebug(`Could not write an editor-played run's transcript: ${errorMessage(error)}`);
37913
+ }
37914
+ }
37880
37915
  editorServes(field, variable, fallback) {
37881
37916
  const named = portFromEnvOrNull(variable);
37882
37917
  if (named !== null) {
@@ -38235,20 +38270,11 @@ class GodotServer {
38235
38270
  if (!game.throughEditor || !this.dapClient) {
38236
38271
  return;
38237
38272
  }
38238
- const heard = [];
38239
- for (const line of this.dapClient.getOutput(true)) {
38240
- const ended = line.endsWith(`
38273
+ if (game.transcript === null) {
38274
+ for (const line of this.dapClient.getOutput(true)) {
38275
+ game.log.append("stdout", line.endsWith(`
38241
38276
  `) ? 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)}`);
38277
+ `);
38252
38278
  }
38253
38279
  }
38254
38280
  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) {
@@ -28792,7 +28802,7 @@ var TOOL_SPECS = [
28792
28802
  {
28793
28803
  name: "editor_launch",
28794
28804
  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 },
28805
+ parameters: { projectPath: { ...PROJECT_PATH, ops: ["open"] } },
28796
28806
  requires: [],
28797
28807
  operations: {
28798
28808
  open: { summary: "open the editor on a project", requires: ["projectPath"] },
@@ -28804,7 +28814,7 @@ var TOOL_SPECS = [
28804
28814
  name: "editor_run",
28805
28815
  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
28816
  parameters: {
28807
- projectPath: PROJECT_PATH,
28817
+ projectPath: { ...PROJECT_PATH, ops: ["start", "check"] },
28808
28818
  scene: {
28809
28819
  type: "string",
28810
28820
  ops: ["start", "check"],
@@ -28833,11 +28843,17 @@ var TOOL_SPECS = [
28833
28843
  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
28844
  }
28835
28845
  },
28836
- requires: ["projectPath"],
28846
+ requires: [],
28837
28847
  operations: {
28838
- start: { summary: "run the project until it quits or is stopped", requires: [] },
28848
+ start: {
28849
+ summary: "run the project until it quits or is stopped",
28850
+ requires: ["projectPath"]
28851
+ },
28839
28852
  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: [] },
28853
+ check: {
28854
+ summary: "boot headless, quit after a few frames, and report the verdict",
28855
+ requires: ["projectPath"]
28856
+ },
28841
28857
  wait: { summary: "wait for the run to end, then answer as editor_output does", requires: [] }
28842
28858
  },
28843
28859
  defaultOperation: "start"
@@ -29876,9 +29892,10 @@ class GodotServer {
29876
29892
  if (op !== null) {
29877
29893
  const elsewhere = Object.keys(args).filter((key) => key !== "op" && !opTakes(spec, op, key));
29878
29894
  if (elsewhere.length > 0) {
29895
+ const takes = argumentsOf(spec, op);
29879
29896
  return {
29880
29897
  ok: false,
29881
- response: this.createErrorResponse(`${spec.name} ${op} does not take ${elsewhere.join(", ")}. ${op} takes: ${argumentsOf(spec, op).join(", ")}.`)
29898
+ response: this.createErrorResponse(`${spec.name} ${op} does not take ${elsewhere.join(", ")}. ${takes.length === 0 ? `${op} takes no arguments.` : `${op} takes: ${takes.join(", ")}.`}`)
29882
29899
  };
29883
29900
  }
29884
29901
  }
@@ -30628,9 +30645,27 @@ class GodotServer {
30628
30645
  if (this.dapClient !== null && this.dapClient.port !== port) {
30629
30646
  this.dapClient = null;
30630
30647
  }
30631
- this.dapClient ??= new GodotDAPClient(port);
30648
+ if (this.dapClient === null) {
30649
+ this.dapClient = new GodotDAPClient(port);
30650
+ this.dapClient.setOutputSink((line) => {
30651
+ this.writeToTranscript(line);
30652
+ });
30653
+ }
30632
30654
  return this.dapClient;
30633
30655
  }
30656
+ writeToTranscript(line) {
30657
+ const run = this.activeProcess;
30658
+ if (run?.throughEditor !== true || run.transcript === null) {
30659
+ return;
30660
+ }
30661
+ try {
30662
+ appendFileSync(run.transcript, line.endsWith(`
30663
+ `) ? line : `${line}
30664
+ `);
30665
+ } catch (error) {
30666
+ this.logDebug(`Could not write an editor-played run's transcript: ${errorMessage(error)}`);
30667
+ }
30668
+ }
30634
30669
  editorServes(field, variable, fallback) {
30635
30670
  const named = portFromEnvOrNull(variable);
30636
30671
  if (named !== null) {
@@ -30989,20 +31024,11 @@ class GodotServer {
30989
31024
  if (!game.throughEditor || !this.dapClient) {
30990
31025
  return;
30991
31026
  }
30992
- const heard = [];
30993
- for (const line of this.dapClient.getOutput(true)) {
30994
- const ended = line.endsWith(`
31027
+ if (game.transcript === null) {
31028
+ for (const line of this.dapClient.getOutput(true)) {
31029
+ game.log.append("stdout", line.endsWith(`
30995
31030
  `) ? 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)}`);
31031
+ `);
31006
31032
  }
31007
31033
  }
31008
31034
  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.9",
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",