gdharness 0.10.1 → 0.10.2

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
@@ -155,6 +155,9 @@ function runArguments(options) {
155
155
  if (options.scene !== null) {
156
156
  args.push(`res://${options.scene}`);
157
157
  }
158
+ if (options.userArgs !== undefined && options.userArgs.length > 0) {
159
+ args.push("--", ...options.userArgs);
160
+ }
158
161
  return args;
159
162
  }
160
163
  function editorArguments(projectPath, ports) {
@@ -15680,10 +15683,15 @@ var init_tool_definitions = __esm(() => {
15680
15683
  },
15681
15684
  {
15682
15685
  name: "editor_run",
15683
- 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. 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.",
15686
+ 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.",
15684
15687
  parameters: {
15685
15688
  projectPath: PROJECT_PATH,
15686
15689
  scene: { type: "string", description: "A scene to run instead of the main scene." },
15690
+ args: {
15691
+ type: "array",
15692
+ items: { type: "string" },
15693
+ description: `The game's own arguments, what OS.get_cmdline_user_args() answers, such as ["--level=2"]. The separator is added here. A run with any is started by this server rather than by the editor, which fixes the game's command line when it opens the project, so the debug_* tools do not answer for it.`
15694
+ },
15687
15695
  headless: { type: "boolean", description: "start: force a window or no window." },
15688
15696
  frames: { type: "number", description: "check: frames to run before quitting. Default 3." },
15689
15697
  timeoutMs: {
@@ -36776,6 +36784,21 @@ class GodotServer {
36776
36784
  }
36777
36785
  return { ok: true, value: { path, file } };
36778
36786
  }
36787
+ gameArguments(args) {
36788
+ if (args["args"] === undefined) {
36789
+ return { ok: true, value: [] };
36790
+ }
36791
+ const given = readStringArray(args, "args");
36792
+ if (given === undefined) {
36793
+ return {
36794
+ ok: false,
36795
+ response: this.createErrorResponse("args must be a list of strings.", [
36796
+ `Pass the game's own flags, such as ["--level=2"]`
36797
+ ])
36798
+ };
36799
+ }
36800
+ return { ok: true, value: given };
36801
+ }
36779
36802
  async debuggedGame() {
36780
36803
  const game = this.activeProcess;
36781
36804
  if (game !== null && game.exitCode !== null) {
@@ -37436,6 +37459,10 @@ class GodotServer {
37436
37459
  if (sceneToRun && !sceneToRun.ok) {
37437
37460
  return this.createErrorResponse(sceneToRun.reason, PATH_SOLUTIONS);
37438
37461
  }
37462
+ const given = this.gameArguments(args);
37463
+ if (!given.ok) {
37464
+ return given.response;
37465
+ }
37439
37466
  if (!sceneToRun && !hasMainScene(project.value.file)) {
37440
37467
  return this.createErrorResponse("The project sets no main scene, so there is nothing to run.", [
37441
37468
  "Pass scene to run one scene",
@@ -37452,7 +37479,7 @@ class GodotServer {
37452
37479
  }
37453
37480
  const sceneArgument = sceneToRun?.ok ? sceneToRun.relativePath : null;
37454
37481
  if (op === "check") {
37455
- return await this.checkBoot(engine.value, project.value.path, sceneArgument, args);
37482
+ return await this.checkBoot(engine.value, project.value.path, sceneArgument, args, given.value);
37456
37483
  }
37457
37484
  if (this.activeProcess?.exitCode === null) {
37458
37485
  this.logDebug("Ending the running game before starting another");
@@ -37463,17 +37490,20 @@ class GodotServer {
37463
37490
  platform: process.platform,
37464
37491
  variables: process.env
37465
37492
  });
37466
- if (this.godotBridge.isConnected() && (!headless || editorPlaysHeadless(project.value.file))) {
37493
+ const editorWouldPlay = !headless || editorPlaysHeadless(project.value.file);
37494
+ if (this.godotBridge.isConnected() && editorWouldPlay && given.value.length === 0) {
37467
37495
  return await this.playThroughEditor(sceneArgument, refreshed.value, project.value.path, alreadyPlaying);
37468
37496
  }
37469
37497
  const cmdArgs = runArguments({
37470
37498
  projectPath: project.value.path,
37471
37499
  headless,
37472
- scene: sceneArgument
37500
+ scene: sceneArgument,
37501
+ userArgs: given.value
37473
37502
  });
37474
37503
  this.logDebug(`Running Godot project: ${engine.value} ${cmdArgs.join(" ")}`);
37475
37504
  const started = this.spawnGame(engine.value, cmdArgs);
37476
37505
  this.activeProcess = started;
37506
+ const spawnedInstead = this.godotBridge.isConnected() && editorWouldPlay && given.value.length > 0 ? " The editor cannot be handed arguments for a game it plays, so this one was started " + "here: the debug_* tools answer only for a game the editor is playing." : "";
37477
37507
  return this.jsonTextResponse({
37478
37508
  started: true,
37479
37509
  through: "gdharness",
@@ -37481,7 +37511,7 @@ class GodotServer {
37481
37511
  arguments: cmdArgs,
37482
37512
  refreshedClasses: refreshed.value,
37483
37513
  runtime: await this.runtimeUp(project.value.path, alreadyPlaying),
37484
- message: "Use editor_output for what it prints and editor_run stop to end it."
37514
+ message: `Use editor_output for what it prints and editor_run stop to end it.${spawnedInstead}`
37485
37515
  });
37486
37516
  }
37487
37517
  async runtimeUp(projectPath, before) {
@@ -37598,10 +37628,10 @@ class GodotServer {
37598
37628
  });
37599
37629
  return started;
37600
37630
  }
37601
- async checkBoot(godotPath, projectPath, scene, args) {
37631
+ async checkBoot(godotPath, projectPath, scene, args, userArgs) {
37602
37632
  const frames = readPositiveNumber(args, "frames") ?? 3;
37603
37633
  const timeoutMs = readPositiveNumber(args, "timeoutMs") ?? 60000;
37604
- const cmdArgs = runArguments({ projectPath, headless: true, scene, quitAfter: frames });
37634
+ const cmdArgs = runArguments({ projectPath, headless: true, scene, quitAfter: frames, userArgs });
37605
37635
  const boot = this.spawnGame(godotPath, cmdArgs);
37606
37636
  const hung = await new Promise((resolve) => {
37607
37637
  const timer = setTimeout(() => {
package/build/index.js CHANGED
@@ -26154,6 +26154,9 @@ function runArguments(options) {
26154
26154
  if (options.scene !== null) {
26155
26155
  args.push(`res://${options.scene}`);
26156
26156
  }
26157
+ if (options.userArgs !== undefined && options.userArgs.length > 0) {
26158
+ args.push("--", ...options.userArgs);
26159
+ }
26157
26160
  return args;
26158
26161
  }
26159
26162
  function editorArguments(projectPath, ports) {
@@ -28313,10 +28316,15 @@ var TOOL_SPECS = [
28313
28316
  },
28314
28317
  {
28315
28318
  name: "editor_run",
28316
- 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. 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.",
28319
+ 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.",
28317
28320
  parameters: {
28318
28321
  projectPath: PROJECT_PATH,
28319
28322
  scene: { type: "string", description: "A scene to run instead of the main scene." },
28323
+ args: {
28324
+ type: "array",
28325
+ items: { type: "string" },
28326
+ description: `The game's own arguments, what OS.get_cmdline_user_args() answers, such as ["--level=2"]. The separator is added here. A run with any is started by this server rather than by the editor, which fixes the game's command line when it opens the project, so the debug_* tools do not answer for it.`
28327
+ },
28320
28328
  headless: { type: "boolean", description: "start: force a window or no window." },
28321
28329
  frames: { type: "number", description: "check: frames to run before quitting. Default 3." },
28322
28330
  timeoutMs: {
@@ -29558,6 +29566,21 @@ class GodotServer {
29558
29566
  }
29559
29567
  return { ok: true, value: { path, file } };
29560
29568
  }
29569
+ gameArguments(args) {
29570
+ if (args["args"] === undefined) {
29571
+ return { ok: true, value: [] };
29572
+ }
29573
+ const given = readStringArray(args, "args");
29574
+ if (given === undefined) {
29575
+ return {
29576
+ ok: false,
29577
+ response: this.createErrorResponse("args must be a list of strings.", [
29578
+ `Pass the game's own flags, such as ["--level=2"]`
29579
+ ])
29580
+ };
29581
+ }
29582
+ return { ok: true, value: given };
29583
+ }
29561
29584
  async debuggedGame() {
29562
29585
  const game = this.activeProcess;
29563
29586
  if (game !== null && game.exitCode !== null) {
@@ -30218,6 +30241,10 @@ class GodotServer {
30218
30241
  if (sceneToRun && !sceneToRun.ok) {
30219
30242
  return this.createErrorResponse(sceneToRun.reason, PATH_SOLUTIONS);
30220
30243
  }
30244
+ const given = this.gameArguments(args);
30245
+ if (!given.ok) {
30246
+ return given.response;
30247
+ }
30221
30248
  if (!sceneToRun && !hasMainScene(project.value.file)) {
30222
30249
  return this.createErrorResponse("The project sets no main scene, so there is nothing to run.", [
30223
30250
  "Pass scene to run one scene",
@@ -30234,7 +30261,7 @@ class GodotServer {
30234
30261
  }
30235
30262
  const sceneArgument = sceneToRun?.ok ? sceneToRun.relativePath : null;
30236
30263
  if (op === "check") {
30237
- return await this.checkBoot(engine.value, project.value.path, sceneArgument, args);
30264
+ return await this.checkBoot(engine.value, project.value.path, sceneArgument, args, given.value);
30238
30265
  }
30239
30266
  if (this.activeProcess?.exitCode === null) {
30240
30267
  this.logDebug("Ending the running game before starting another");
@@ -30245,17 +30272,20 @@ class GodotServer {
30245
30272
  platform: process.platform,
30246
30273
  variables: process.env
30247
30274
  });
30248
- if (this.godotBridge.isConnected() && (!headless || editorPlaysHeadless(project.value.file))) {
30275
+ const editorWouldPlay = !headless || editorPlaysHeadless(project.value.file);
30276
+ if (this.godotBridge.isConnected() && editorWouldPlay && given.value.length === 0) {
30249
30277
  return await this.playThroughEditor(sceneArgument, refreshed.value, project.value.path, alreadyPlaying);
30250
30278
  }
30251
30279
  const cmdArgs = runArguments({
30252
30280
  projectPath: project.value.path,
30253
30281
  headless,
30254
- scene: sceneArgument
30282
+ scene: sceneArgument,
30283
+ userArgs: given.value
30255
30284
  });
30256
30285
  this.logDebug(`Running Godot project: ${engine.value} ${cmdArgs.join(" ")}`);
30257
30286
  const started = this.spawnGame(engine.value, cmdArgs);
30258
30287
  this.activeProcess = started;
30288
+ const spawnedInstead = this.godotBridge.isConnected() && editorWouldPlay && given.value.length > 0 ? " The editor cannot be handed arguments for a game it plays, so this one was started " + "here: the debug_* tools answer only for a game the editor is playing." : "";
30259
30289
  return this.jsonTextResponse({
30260
30290
  started: true,
30261
30291
  through: "gdharness",
@@ -30263,7 +30293,7 @@ class GodotServer {
30263
30293
  arguments: cmdArgs,
30264
30294
  refreshedClasses: refreshed.value,
30265
30295
  runtime: await this.runtimeUp(project.value.path, alreadyPlaying),
30266
- message: "Use editor_output for what it prints and editor_run stop to end it."
30296
+ message: `Use editor_output for what it prints and editor_run stop to end it.${spawnedInstead}`
30267
30297
  });
30268
30298
  }
30269
30299
  async runtimeUp(projectPath, before) {
@@ -30380,10 +30410,10 @@ class GodotServer {
30380
30410
  });
30381
30411
  return started;
30382
30412
  }
30383
- async checkBoot(godotPath, projectPath, scene, args) {
30413
+ async checkBoot(godotPath, projectPath, scene, args, userArgs) {
30384
30414
  const frames = readPositiveNumber(args, "frames") ?? 3;
30385
30415
  const timeoutMs = readPositiveNumber(args, "timeoutMs") ?? 60000;
30386
- const cmdArgs = runArguments({ projectPath, headless: true, scene, quitAfter: frames });
30416
+ const cmdArgs = runArguments({ projectPath, headless: true, scene, quitAfter: frames, userArgs });
30387
30417
  const boot = this.spawnGame(godotPath, cmdArgs);
30388
30418
  const hung = await new Promise((resolve) => {
30389
30419
  const timer = setTimeout(() => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gdharness",
3
- "version": "0.10.1",
3
+ "version": "0.10.2",
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",