gdharness 1.2.33 → 1.2.35

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/index.js CHANGED
@@ -25071,6 +25071,12 @@ function isWithinRoot(rootPath, candidatePath) {
25071
25071
  const step = relative(resolve(rootPath), resolve(candidatePath));
25072
25072
  return step !== ".." && !step.startsWith(`..${sep}`) && !isAbsolute(step);
25073
25073
  }
25074
+ var KEPT_ID = /^[A-Za-z0-9_]+$/;
25075
+ function keptInside(given) {
25076
+ const at = given.indexOf("::");
25077
+ const inner = at === -1 ? "" : given.slice(at + 2);
25078
+ return KEPT_ID.test(inner) ? { file: given.slice(0, at), inner } : { file: given, inner: undefined };
25079
+ }
25074
25080
  function resolveWithinProject(projectPath, candidatePath, options = {}) {
25075
25081
  const trimmed = candidatePath.trim();
25076
25082
  if (trimmed === "") {
@@ -25099,6 +25105,13 @@ function resolveWithinProject(projectPath, candidatePath, options = {}) {
25099
25105
  reason: `Path '${trimmed}' is absolute; give a path inside the project, such as 'scenes/main.tscn'.`
25100
25106
  };
25101
25107
  }
25108
+ const kept = keptInside(trimmed);
25109
+ if (kept.inner !== undefined) {
25110
+ return {
25111
+ ok: false,
25112
+ reason: `Path '${trimmed}' names the resource ${kept.inner} kept inside ${kept.file}, not a file. resource_edit read reads one by that path; this argument takes a file.`
25113
+ };
25114
+ }
25102
25115
  if (withoutScheme.includes(":")) {
25103
25116
  return {
25104
25117
  ok: false,
@@ -28704,9 +28717,14 @@ class GameLog {
28704
28717
  const owner = this.lastHeadline[source];
28705
28718
  if (owner !== null && /^\s/.test(line)) {
28706
28719
  owner.detail.push(line.trim());
28720
+ owner.continues = false;
28707
28721
  return;
28708
28722
  }
28709
28723
  const headline = announced(line);
28724
+ if (owner?.continues === true && headline === null) {
28725
+ owner.detail.push(line.trim());
28726
+ return;
28727
+ }
28710
28728
  const detail = [];
28711
28729
  const entry = {
28712
28730
  index: this.entries.length,
@@ -28719,7 +28737,7 @@ class GameLog {
28719
28737
  if (headline?.byScript === true) {
28720
28738
  this.byScript.add(entry.index);
28721
28739
  }
28722
- this.lastHeadline[source] = headline ? { detail } : null;
28740
+ this.lastHeadline[source] = headline ? { detail, continues: source === "stderr" && headline.text.endsWith(":") } : null;
28723
28741
  }
28724
28742
  raisedByScript(entry) {
28725
28743
  return this.byScript.has(entry.index);
@@ -33301,7 +33319,7 @@ var HEADLESS_OPERATIONS = dictionary({
33301
33319
  uid: "get_uid",
33302
33320
  refresh_classes: "refresh_class_cache"
33303
33321
  }),
33304
- project_export: dictionary({ list: "list_export_presets" }),
33322
+ project_export: dictionary({ list: "list_export_presets", add_preset: "add_export_preset" }),
33305
33323
  script_edit: dictionary({ create: "create_script", modify: "modify_script" }),
33306
33324
  script_info: dictionary({ structure: "get_script_info" }),
33307
33325
  editor_classes: dictionary({
@@ -35040,7 +35058,7 @@ var TOOL_SPECS = [
35040
35058
  {
35041
35059
  name: "project_settings",
35042
35060
  title: "Project settings",
35043
- description: "Reads or writes project.godot: settings, autoloads, the main scene, input actions, plugins and audio buses. A write with an editor open on the project is handed to the editor, which takes the settings the write changed or names from the file (named under editorAdopted), including a setting the file already held, since the editor can hold another value after the file was put back, or reloads the bus layout, so it answers the new values and does not save the old ones back over them. editorNote is an editor that could not take the write, one on an addon from before this, which still holds the old values until editor_launch restart. An enabled plugin's code still loads on the editor's next start. set answers old_value and new_value, with was_new true when project.godot did not set the setting before, and was_default true when what it held was the engine's own default, which old_value then is. A set of the value the setting already holds, in the same type, writes nothing and answers unchanged true and saved false. Every write keeps the sections and keys of project.godot in the order the file had them, so a change touches only its own lines. get answers exists, true for any setting the engine knows, and at_default, true when the value is the engine's own default and so has no line in project.godot. Each setting under prefix carries at_default too, so a family read separates what the project sets from the engine's defaults; it is absent for a setting the engine registers no default for, such as an input action the project added, which only the project can have set.",
35061
+ description: "Reads or writes project.godot: settings, autoloads, the main scene, input actions, plugins and audio buses. A write with an editor open on the project is handed to the editor, which takes the settings the write changed or names from the file (named under editorAdopted), including a setting the file already held, since the editor can hold another value after the file was put back, or reloads the bus layout, so it answers the new values and does not save the old ones back over them. editorNote is an editor that could not take the write, one on an addon from before this, which still holds the old values until editor_launch restart. An enabled plugin's code still loads on the editor's next start. set answers old_value and new_value, with was_new true when project.godot did not set the setting before, and was_default true when what it held was the engine's own default, which old_value then is. add_input_action on an action that exists, one of the engine's ui_ actions included, replaces its events and deadzone, and answers was_new false with what it held under old_events and old_deadzone. A set of the value the setting already holds, in the same type, writes nothing and answers unchanged true and saved false. Every write keeps the sections and keys of project.godot in the order the file had them, so a change touches only its own lines. get answers exists, true for any setting the engine knows, and at_default, true when the value is the engine's own default and so has no line in project.godot. Each setting under prefix carries at_default too, so a family read separates what the project sets from the engine's defaults; it is absent for a setting the engine registers no default for, such as an input action the project added, which only the project can have set.",
35044
35062
  parameters: {
35045
35063
  projectPath: PROJECT_PATH2,
35046
35064
  setting: {
@@ -35138,8 +35156,8 @@ var TOOL_SPECS = [
35138
35156
  requires: ["scenePath"]
35139
35157
  },
35140
35158
  add_input_action: {
35141
- effect: "additive",
35142
- summary: "register an input action and its events",
35159
+ effect: "destructive",
35160
+ summary: "register an input action and its events, or rebind one that exists",
35143
35161
  requires: ["actionName", "events"]
35144
35162
  },
35145
35163
  remove_input_action: {
@@ -35277,20 +35295,36 @@ var TOOL_SPECS = [
35277
35295
  {
35278
35296
  name: "project_export",
35279
35297
  title: "Export",
35280
- description: "Export presets and exports.",
35298
+ description: "Export presets and exports. add_preset writes a preset to export_presets.cfg with the keys the editor's Project > Export writes for a new one, every platform option left at its default, and makes it the platform's runnable preset when the platform has none. It answers where this engine's export templates belong and whether the preset's platform has them, since an export needs them; list says the same for every preset. An editor open on the project does not see a preset added this way until it restarts. run names, as alsoWritten, what else the export wrote beside its output, such as the .pck a Windows or Linux build cannot start without.",
35281
35299
  parameters: {
35282
35300
  projectPath: PROJECT_PATH2,
35283
- preset: { type: "string", description: "Preset name from export_presets.cfg." },
35301
+ preset: {
35302
+ type: "string",
35303
+ ops: ["run", "add_preset"],
35304
+ description: "Preset name from export_presets.cfg. add_preset: the name of the new one."
35305
+ },
35306
+ platform: {
35307
+ type: "string",
35308
+ ops: ["add_preset"],
35309
+ enum: ["Windows Desktop", "Linux", "macOS", "Android", "iOS", "Web"],
35310
+ description: "add_preset: the platform the preset exports for."
35311
+ },
35284
35312
  outputPath: {
35285
35313
  type: "string",
35286
- description: `Where the export is written, inside the project. The directory is created if it is not there: Godot's command-line exporter refuses a missing one with "The given export path doesn't exist", which reads as a wrong path in the preset.`
35314
+ ops: ["run", "add_preset"],
35315
+ description: `Where the export is written, inside the project. The directory is created if it is not there: Godot's command-line exporter refuses a missing one with "The given export path doesn't exist", which reads as a wrong path in the preset. add_preset: the path the preset exports to from the editor, optional.`
35287
35316
  },
35288
- debug: { type: "boolean", description: "run: a debug export. Default false." }
35317
+ debug: { type: "boolean", ops: ["run"], description: "run: a debug export. Default false." }
35289
35318
  },
35290
35319
  requires: ["projectPath"],
35291
35320
  operations: {
35292
35321
  list: { effect: "read", summary: "the presets in export_presets.cfg", requires: [] },
35293
- run: { effect: "destructive", summary: "export with a preset", requires: ["preset", "outputPath"] }
35322
+ run: { effect: "destructive", summary: "export with a preset", requires: ["preset", "outputPath"] },
35323
+ add_preset: {
35324
+ effect: "additive",
35325
+ summary: "add an export preset to export_presets.cfg",
35326
+ requires: ["preset", "platform"]
35327
+ }
35294
35328
  }
35295
35329
  },
35296
35330
  {
@@ -35487,7 +35521,7 @@ var TOOL_SPECS = [
35487
35521
  {
35488
35522
  name: "scene_animation",
35489
35523
  title: "Animations",
35490
- description: "Animations in an AnimationPlayer and states in an AnimationTree state machine. read answers with every track of an animation in the shape add_track takes, so a track read off one animation can be added to another. Needs the editor connected.",
35524
+ description: "Animations in an AnimationPlayer and states in an AnimationTree state machine. read answers with every track of an animation in the shape add_track takes, so a track read off one animation can be added to another. read and read_states need no editor: one serving the project answers and says whether it holds unsaved changes to the scene, and with none a headless engine reads the file without running the scene's scripts. The ops that write need the editor connected.",
35491
35525
  parameters: {
35492
35526
  projectPath: PROJECT_PATH2,
35493
35527
  scenePath: SCENE_PATH,
@@ -35519,7 +35553,11 @@ var TOOL_SPECS = [
35519
35553
  ops: ["read_states", "add_state", "connect_states"],
35520
35554
  description: "The AnimationTree node."
35521
35555
  },
35522
- stateName: { type: "string", ops: ["add_state"], description: "add_state: the state to add." },
35556
+ stateName: {
35557
+ type: "string",
35558
+ ops: ["add_state"],
35559
+ description: "add_state: the state to add. It goes right of the rightmost state in the editor's graph, answered under position."
35560
+ },
35523
35561
  stateMachinePath: {
35524
35562
  type: "string",
35525
35563
  ops: ["read_states", "add_state", "connect_states"],
@@ -35694,7 +35732,7 @@ var TOOL_SPECS = [
35694
35732
  projectPath: PROJECT_PATH2,
35695
35733
  resourcePath: {
35696
35734
  ...RESOURCE_PATH,
35697
- description: `${RESOURCE_FILE} move: a directory too.`
35735
+ description: `${RESOURCE_FILE} move: a directory too. read: also a resource kept inside a scene or resource file, by the path a read answers a property holding it with, such as "res://hero.tscn::Animation_jump".`
35698
35736
  },
35699
35737
  resourceType: {
35700
35738
  type: "string",
@@ -37548,7 +37586,7 @@ function exportPresetRefusal(projectPath, preset) {
37548
37586
  try {
37549
37587
  text = readFileSync24(join26(projectPath, "export_presets.cfg"), "utf8");
37550
37588
  } catch {
37551
- return `This project has no export_presets.cfg, so it has no preset '${preset}' to export with. The editor writes the file when a preset is added in Project > Export.`;
37589
+ return `This project has no export_presets.cfg, so it has no preset '${preset}' to export with. project_export add_preset writes one, as the editor's Project > Export does.`;
37552
37590
  }
37553
37591
  const names = [];
37554
37592
  let inPreset = false;
@@ -37566,9 +37604,9 @@ function exportPresetRefusal(projectPath, preset) {
37566
37604
  if (names.includes(preset)) {
37567
37605
  return null;
37568
37606
  }
37569
- return names.length === 0 ? `export_presets.cfg names no preset, so there is no '${preset}' to export with. The editor adds one in Project > Export.` : `This project has no export preset named '${preset}'. Its presets are ${names.map((name) => `'${name}'`).join(", ")}.`;
37607
+ return names.length === 0 ? `export_presets.cfg names no preset, so there is no '${preset}' to export with. project_export add_preset adds one.` : `This project has no export preset named '${preset}'. Its presets are ${names.map((name) => `'${name}'`).join(", ")}.`;
37570
37608
  }
37571
- function exportAnswer(ending, asked, before, after) {
37609
+ function exportAnswer(ending, asked, before, after, beside = []) {
37572
37610
  const { log } = ending;
37573
37611
  const written = after !== null && after !== before;
37574
37612
  const notCopied = librariesNotCopied(log.all);
@@ -37580,6 +37618,7 @@ function exportAnswer(ending, asked, before, after) {
37580
37618
  failure: ending.failure ?? undefined,
37581
37619
  errors: log.count("error"),
37582
37620
  warnings: log.count("warning"),
37621
+ ...beside.length === 0 ? {} : { alsoWritten: beside },
37583
37622
  ...notCopied.length === 0 ? {} : {
37584
37623
  librariesNotLoaded: notCopied,
37585
37624
  extensionNote: `The export could not load ${notCopied.join(" and ")}: on Windows an engine running as the editor, which an export is, loads a GDExtension from a ~ copy beside the library, and another engine on this project held that copy: it could not be moved aside before the export, or another engine made a new one in between. Export again once that engine has loaded or exited.`
@@ -37602,6 +37641,20 @@ function exportAnswer(ending, asked, before, after) {
37602
37641
  isError: true
37603
37642
  };
37604
37643
  }
37644
+ function writtenBeside(before, after, output, within) {
37645
+ return [...after].filter(([name, at]) => name !== output && before.get(name) !== at).map(([name]) => within === "." ? name : `${within}/${name}`).sort();
37646
+ }
37647
+ function filesIn(folder) {
37648
+ const found = new Map;
37649
+ try {
37650
+ for (const entry of readdirSync11(folder, { withFileTypes: true })) {
37651
+ if (entry.isFile()) {
37652
+ found.set(entry.name, statSync12(join26(folder, entry.name)).mtimeMs);
37653
+ }
37654
+ }
37655
+ } catch {}
37656
+ return found;
37657
+ }
37605
37658
  function scanWaitAnswer(scanned, started = "game") {
37606
37659
  if (scanned.waitedMs === 0) {
37607
37660
  return {};
@@ -37839,6 +37892,9 @@ var PATH_SOLUTIONS = [
37839
37892
  'Give the path relative to the project, such as "scenes/main.tscn" or "res://scenes/main.tscn"',
37840
37893
  "Point projectPath at the project the file belongs to"
37841
37894
  ];
37895
+ var OUTPUT_PATH_SOLUTIONS = [
37896
+ 'Give outputPath inside the project, relative to it, such as "build/game.exe" or "res://build/game.exe"; copy the build elsewhere once it is exported'
37897
+ ];
37842
37898
  var PROJECT_FILE_ARGUMENTS = [
37843
37899
  "scenePath",
37844
37900
  "instancePath",
@@ -38893,6 +38949,9 @@ class GodotServer {
38893
38949
  if (tool === "project_import" && op === "set_options") {
38894
38950
  return await this.handleSetImportOptions(args);
38895
38951
  }
38952
+ if (tool === "project_export" && op === "add_preset") {
38953
+ return await this.handleAddExportPreset(args);
38954
+ }
38896
38955
  const headless = HEADLESS_OPERATIONS[tool]?.[op];
38897
38956
  if (headless !== undefined) {
38898
38957
  const { from: asked, ...answerable } = args;
@@ -38950,7 +39009,11 @@ class GodotServer {
38950
39009
  case "scene_create":
38951
39010
  return op === "create" ? await bridge("create_scene") : await bridge("save_scene");
38952
39011
  case "scene_tree":
38953
- return await this.readFromFile(args, "scenePath", "read_scene_tree", async () => await bridge("list_scene_nodes"));
39012
+ return await this.readFromFile(args, "scenePath", "read_scene_tree", (file) => ({
39013
+ scenePath: file,
39014
+ depth: readNumber(args, "depth") ?? -1,
39015
+ includeProperties: readBoolean(args, "includeProperties") ?? false
39016
+ }), async () => await bridge("list_scene_nodes"));
38954
39017
  case "scene_node":
38955
39018
  switch (op) {
38956
39019
  case "add":
@@ -38980,9 +39043,17 @@ class GodotServer {
38980
39043
  case "scene_animation":
38981
39044
  switch (op) {
38982
39045
  case "read":
38983
- return await bridge("read_animations");
39046
+ return await this.readFromFile(args, "scenePath", "read_animations", (file) => ({
39047
+ scenePath: file,
39048
+ playerNodePath: readNonEmptyString(args, "playerNodePath") ?? ".",
39049
+ animationName: readString(args, "animationName") ?? ""
39050
+ }), async () => await bridge("read_animations"));
38984
39051
  case "read_states":
38985
- return await bridge("read_animation_states");
39052
+ return await this.readFromFile(args, "scenePath", "read_animation_states", (file) => ({
39053
+ scenePath: file,
39054
+ animTreePath: readString(args, "animTreePath") ?? "",
39055
+ stateMachinePath: readString(args, "stateMachinePath") ?? ""
39056
+ }), async () => await bridge("read_animation_states"));
38986
39057
  case "create":
38987
39058
  return await bridge("create_animation");
38988
39059
  case "add_track":
@@ -39009,8 +39080,14 @@ class GodotServer {
39009
39080
  switch (op) {
39010
39081
  case "move":
39011
39082
  return await this.handleMoveFile(args);
39012
- case "read":
39013
- return await this.readFromFile(args, "resourcePath", "read_resource", async () => await bridge("read_resource"));
39083
+ case "read": {
39084
+ const { file, inner } = keptInside(readString(args, "resourcePath") ?? "");
39085
+ const kept = inner === undefined ? {} : { resourcePath: file, subResource: inner };
39086
+ return await this.readFromFile({ ...args, ...kept }, "resourcePath", "read_resource", (found) => ({
39087
+ resourcePath: inner === undefined ? found : `${found}::${inner}`,
39088
+ includeDefaults: readBoolean(args, "includeDefaults") ?? false
39089
+ }), async () => await bridge("read_resource", kept));
39090
+ }
39014
39091
  case "create":
39015
39092
  return await bridge("create_resource");
39016
39093
  case "modify":
@@ -39938,6 +40015,30 @@ class GodotServer {
39938
40015
  return this.createErrorResponse(`Search failed: ${errorMessage(error)}`);
39939
40016
  }
39940
40017
  }
40018
+ async handleAddExportPreset(args) {
40019
+ const project = this.project(args);
40020
+ if (!project.ok) {
40021
+ return project.response;
40022
+ }
40023
+ const given = readNonEmptyString(args, "outputPath");
40024
+ const output = given === undefined ? null : resolveWithinProject(project.value.path, given);
40025
+ if (output !== null && !output.ok) {
40026
+ return this.createErrorResponse(output.reason, OUTPUT_PATH_SOLUTIONS);
40027
+ }
40028
+ const outcome = await this.operation("add_export_preset", {
40029
+ preset: readString(args, "preset") ?? "",
40030
+ platform: readString(args, "platform") ?? "",
40031
+ outputPath: output?.relativePath ?? ""
40032
+ }, project.value.path);
40033
+ if (!outcome.ok || !this.editorHasOpen(project.value.path)) {
40034
+ return this.answer(outcome);
40035
+ }
40036
+ return this.jsonTextResponse({
40037
+ ...outcome.payload,
40038
+ editorNote: "The editor open on this project read export_presets.cfg when it started and does not list this preset, and saving presets in its Export dialog writes its own list over the file. editor_launch restart loads the file; an export through project_export run reads the file and needs no restart.",
40039
+ ...engineExtras(outcome)
40040
+ });
40041
+ }
39941
40042
  async handleExportProject(args) {
39942
40043
  const project = this.project(args);
39943
40044
  if (!project.ok) {
@@ -39946,11 +40047,14 @@ class GodotServer {
39946
40047
  const preset = readNonEmptyString(args, "preset") ?? "";
39947
40048
  const output = resolveWithinProject(project.value.path, readString(args, "outputPath") ?? "");
39948
40049
  if (!output.ok) {
39949
- return this.createErrorResponse(output.reason, PATH_SOLUTIONS);
40050
+ return this.createErrorResponse(output.reason, OUTPUT_PATH_SOLUTIONS);
39950
40051
  }
39951
40052
  const noPreset = exportPresetRefusal(project.value.path, preset);
39952
40053
  if (noPreset !== null) {
39953
- return this.createErrorResponse(noPreset, ["project_export list reads the presets the project has"]);
40054
+ return this.createErrorResponse(noPreset, [
40055
+ "project_export list reads the presets the project has",
40056
+ "project_export add_preset adds one"
40057
+ ]);
39954
40058
  }
39955
40059
  const engine = await this.engine();
39956
40060
  if (!engine.ok) {
@@ -39977,6 +40081,7 @@ class GodotServer {
39977
40081
  this.logDebug(`Exporting: ${engine.value} ${exportArgs.join(" ")}`);
39978
40082
  const writtenAt = () => existsSync20(output.absolutePath) ? statSync12(output.absolutePath).mtimeMs : null;
39979
40083
  const before = writtenAt();
40084
+ const listedBefore = filesIn(folder);
39980
40085
  const freed = freeLibraryCopies(project.value.path);
39981
40086
  if (freed.setAside.length > 0 || freed.swept.length > 0) {
39982
40087
  this.logDebug(`Before the export: set aside ${freed.setAside.join(", ")}; swept ${freed.swept.join(", ")}`);
@@ -39990,7 +40095,8 @@ class GodotServer {
39990
40095
  if (ending.exitSignal === null && ending.exitCode === null) {
39991
40096
  return this.createErrorResponse(`Export could not be run: ${ending.failure ?? "the engine gave no exit status"}`);
39992
40097
  }
39993
- return exportAnswer(ending, { preset, outputPath: output.relativePath, debug }, before, writtenAt());
40098
+ const beside = writtenBeside(listedBefore, filesIn(folder), basename7(output.absolutePath), dirname18(output.relativePath).replaceAll("\\", "/"));
40099
+ return exportAnswer(ending, { preset, outputPath: output.relativePath, debug }, before, writtenAt(), beside);
39994
40100
  }
39995
40101
  reportTestProgress(log) {
39996
40102
  const progress = callProgress();
@@ -43400,7 +43506,7 @@ class GodotServer {
43400
43506
  ...leftChanged === undefined ? {} : { note: leftChanged }
43401
43507
  });
43402
43508
  }
43403
- async readFromFile(args, pathKey, operation, throughTheEditor) {
43509
+ async readFromFile(args, pathKey, operation, paramsFor, throughTheEditor) {
43404
43510
  const project = this.project(args);
43405
43511
  if (!project.ok) {
43406
43512
  return project.response;
@@ -43418,12 +43524,7 @@ class GodotServer {
43418
43524
  if (file === null) {
43419
43525
  return this.createErrorResponse(`${pathKey} must name a file inside the project.`);
43420
43526
  }
43421
- const params = operation === "read_scene_tree" ? {
43422
- scenePath: file,
43423
- depth: readNumber(args, "depth") ?? -1,
43424
- includeProperties: readBoolean(args, "includeProperties") ?? false
43425
- } : { resourcePath: file, includeDefaults: readBoolean(args, "includeDefaults") ?? false };
43426
- const outcome = await this.operation(operation, params, projectPath);
43527
+ const outcome = await this.operation(operation, paramsFor(file), projectPath);
43427
43528
  if (!outcome.ok) {
43428
43529
  return this.answer(outcome);
43429
43530
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gdharness",
3
- "version": "1.2.33",
3
+ "version": "1.2.35",
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",