gdharness 1.0.14 → 1.0.15

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
@@ -16398,7 +16398,11 @@ var init_tool_definitions = __esm(() => {
16398
16398
  description: "viewport: the Viewport node. Default the root viewport."
16399
16399
  },
16400
16400
  width: { type: "number", description: "Scale the image to this width." },
16401
- height: { type: "number", description: "Scale the image to this height." }
16401
+ height: { type: "number", description: "Scale the image to this height." },
16402
+ outputPath: {
16403
+ type: "string",
16404
+ description: "Also save the picture as a PNG at this absolute path, for somebody to open later; the answer says where. Refused unless it ends in .png, its directory exists, it is outside the project, and no file is there already."
16405
+ }
16402
16406
  },
16403
16407
  requires: [],
16404
16408
  operations: {
@@ -16421,7 +16425,7 @@ var init_tool_definitions = __esm(() => {
16421
16425
  action: {
16422
16426
  type: "string",
16423
16427
  ops: ["action"],
16424
- description: "action: the InputMap action name. An engine dialog is not answered this way: AcceptDialog reads the Escape key itself and never asks the InputMap, so ui_cancel goes in and the question stays up. Dismiss one with key Escape, or click its button."
16428
+ description: "action: the InputMap action name. An engine dialog is not answered this way: AcceptDialog reads the Escape key itself and never asks the InputMap, so ui_cancel goes in and the question stays up. Dismiss one with the key op and keycode Escape, or click its button."
16425
16429
  },
16426
16430
  pressed: {
16427
16431
  type: "boolean",
@@ -16524,7 +16528,11 @@ var init_tool_definitions = __esm(() => {
16524
16528
  ops: ["frames"],
16525
16529
  description: "frames: how many to let pass, 1 to 600. More than that is refused."
16526
16530
  },
16527
- nodePath: { type: "string", ops: ["signal", "until"], description: "signal, until: the node." },
16531
+ nodePath: {
16532
+ type: "string",
16533
+ ops: ["signal", "until"],
16534
+ description: "signal, until: the node. Needed for a signal and for a property; for says it defaults to /root, the whole screen."
16535
+ },
16528
16536
  signal: { type: "string", ops: ["signal"], description: "signal: the signal name." },
16529
16537
  property: {
16530
16538
  type: "string",
@@ -16535,7 +16543,7 @@ var init_tool_definitions = __esm(() => {
16535
16543
  says: {
16536
16544
  type: "string",
16537
16545
  ops: ["until"],
16538
- description: "until: wait for these words to appear anywhere under nodePath instead of for a property, which is how a panel that rebuilds its labels is waited on at all: the labels are named afresh each redraw and the panel is what stays put. Case-insensitive, part of a line, on a node the player can see unless includeHidden says otherwise; a label with a line break is matched with the break in the words, and a backslash followed by n counts as one. Instead of, not as well as: a call carrying this and a property is refused, because they ask about different things and answering one of them silently is how a caller watches a screen believing they are watching a property."
16546
+ description: "until: wait for these words to appear anywhere under nodePath, or anywhere in the game when no nodePath is given, instead of for a property, which is how a panel that rebuilds its labels is waited on at all: the labels are named afresh each redraw and the panel is what stays put. Case-insensitive, part of a line, on a node the player can see unless includeHidden says otherwise; a label with a line break is matched with the break in the words, and a backslash followed by n counts as one. Instead of, not as well as: a call carrying this and a property is refused, because they ask about different things and answering one of them silently is how a caller watches a screen believing they are watching a property."
16539
16547
  },
16540
16548
  includeHidden: {
16541
16549
  type: "boolean",
@@ -16557,7 +16565,7 @@ var init_tool_definitions = __esm(() => {
16557
16565
  },
16558
16566
  until: {
16559
16567
  summary: "wait for a property to read as a value, or for words to appear under a node, and answer with what it found",
16560
- requires: ["nodePath"]
16568
+ requires: []
16561
16569
  }
16562
16570
  }
16563
16571
  },
@@ -38025,6 +38033,8 @@ import { randomUUID as randomUUID2 } from "node:crypto";
38025
38033
  import {
38026
38034
  appendFileSync,
38027
38035
  closeSync as closeSync2,
38036
+ constants,
38037
+ copyFileSync,
38028
38038
  existsSync as existsSync15,
38029
38039
  fstatSync,
38030
38040
  mkdirSync as mkdirSync13,
@@ -38037,10 +38047,34 @@ import {
38037
38047
  statSync as statSync8
38038
38048
  } from "node:fs";
38039
38049
  import { tmpdir as tmpdir5 } from "node:os";
38040
- import { basename as basename4, dirname as dirname15, isAbsolute as isAbsolute3, join as join22, normalize as normalize3, relative as relative3 } from "node:path";
38050
+ import { basename as basename4, dirname as dirname15, extname as extname2, isAbsolute as isAbsolute3, join as join22, normalize as normalize3, relative as relative3 } from "node:path";
38041
38051
  import { setTimeout as delay3 } from "node:timers/promises";
38042
38052
  import { fileURLToPath as fileURLToPath5 } from "node:url";
38043
38053
  import { promisify as promisify7 } from "node:util";
38054
+ function captureDestinationRefusal(outputPath, projects) {
38055
+ if (/^(res|user):\/\//.test(outputPath)) {
38056
+ return `outputPath ${outputPath} is a Godot path; a capture is saved outside the project, at an absolute path on this machine.`;
38057
+ }
38058
+ if (!isAbsolute3(outputPath)) {
38059
+ return `outputPath ${outputPath} is not absolute; name the whole path, so where it is written does not depend on this server's directory.`;
38060
+ }
38061
+ if (extname2(outputPath).toLowerCase() !== ".png") {
38062
+ return `outputPath ${outputPath} does not end in .png, which is what a capture is.`;
38063
+ }
38064
+ const directory = realPathOr(dirname15(outputPath));
38065
+ if (!existsSync15(directory)) {
38066
+ return `outputPath's directory ${dirname15(outputPath)} does not exist; a capture creates no directories.`;
38067
+ }
38068
+ const where = join22(directory, basename4(outputPath));
38069
+ const inside = projects.find((project) => project !== "" && isWithinRoot(realPathOr(project), where));
38070
+ if (inside !== undefined) {
38071
+ return `outputPath ${outputPath} is inside the project at ${inside}; a capture is saved outside it, so it never becomes part of the game.`;
38072
+ }
38073
+ if (existsSync15(where)) {
38074
+ return `outputPath ${outputPath} already exists; a capture is not written over a file. Name a new one.`;
38075
+ }
38076
+ return null;
38077
+ }
38044
38078
  function patienceForFrames(frames, atLeast) {
38045
38079
  return Math.max(atLeast, frames / SLOWEST_FRAME_RATE * 1000 + atLeast);
38046
38080
  }
@@ -41333,7 +41367,7 @@ class GodotServer {
41333
41367
  return `The game this server started (${which}) is still coming up and has not announced its runtime after a wait of ${Math.min(budgetMs, remaining)}ms, so there is nothing to talk to yet. It is still running: ask again in a moment rather than starting another.`;
41334
41368
  }
41335
41369
  async handleRuntimeCommand(command, args, timeoutMs = this.runtimeTimeoutMs()) {
41336
- const { op: _op, projectPath, pid, ...params } = asParams(args);
41370
+ const { op: _op, projectPath, pid, outputPath, ...params } = asParams(args);
41337
41371
  const waited = typeof pid === "number" ? null : await this.ownRunOnItsWay(typeof projectPath === "string" ? projectPath : this.ownProject, timeoutMs);
41338
41372
  if (waited !== null) {
41339
41373
  return this.createErrorResponse(waited, [
@@ -41360,6 +41394,16 @@ class GodotServer {
41360
41394
  ]);
41361
41395
  }
41362
41396
  const expectsScreenshot = command === "capture_screenshot" || command === "capture_viewport";
41397
+ const saveTo = expectsScreenshot && typeof outputPath === "string" ? outputPath : null;
41398
+ if (saveTo !== null) {
41399
+ const refusal = captureDestinationRefusal(saveTo, [
41400
+ choice.endpoint.project.path,
41401
+ ...this.ownProject === null ? [] : [this.ownProject]
41402
+ ]);
41403
+ if (refusal !== null) {
41404
+ return this.createErrorResponse(refusal);
41405
+ }
41406
+ }
41363
41407
  const screenshotDir = expectsScreenshot ? mkdtempSync3(join22(tmpdir5(), "gdharness-runtime-screenshot-")) : null;
41364
41408
  const screenshotPath = screenshotDir ? join22(screenshotDir, "capture.png") : null;
41365
41409
  try {
@@ -41379,11 +41423,18 @@ class GodotServer {
41379
41423
  return this.createErrorResponse(`Rejected screenshot file path outside the managed capture path: '${returnedPath}'`);
41380
41424
  }
41381
41425
  const dimensions = `${readNumber(payload, "width") ?? 0}x${readNumber(payload, "height") ?? 0} ${readString(payload, "format") ?? "unknown"}`;
41426
+ if (saveTo !== null) {
41427
+ try {
41428
+ copyFileSync(screenshotPath, saveTo, constants.COPYFILE_EXCL);
41429
+ } catch (error) {
41430
+ return this.createErrorResponse(`The capture was taken and could not be saved at ${saveTo}: ${errorMessage(error)}`);
41431
+ }
41432
+ }
41382
41433
  return {
41383
41434
  content: [
41384
41435
  {
41385
41436
  type: "text",
41386
- text: `Screenshot captured: ${dimensions}${chosenAmongSeveral ? ` from pid ${own}, the game this server holds` : ""}`
41437
+ text: `Screenshot captured: ${dimensions}${chosenAmongSeveral ? ` from pid ${own}, the game this server holds` : ""}${saveTo === null ? "" : `. Saved to ${saveTo}`}`
41387
41438
  },
41388
41439
  { type: "image", data: readFileSync20(screenshotPath).toString("base64"), mimeType: "image/png" }
41389
41440
  ]
@@ -41430,7 +41481,11 @@ class GodotServer {
41430
41481
  return await this.handleRuntimeCommand("wait_frames", { ...whichGame(args), frames }, waited);
41431
41482
  }
41432
41483
  const timeoutMs = readPositiveNumber(args, "timeoutMs") ?? 5000;
41433
- const nodePath = readNonEmptyString(args, "nodePath") ?? "";
41484
+ const asked = readNonEmptyString(args, "nodePath");
41485
+ if (asked === undefined && op === "until" && readNonEmptyString(args, "says") === undefined) {
41486
+ return this.createErrorResponse("runtime_wait until needs nodePath to wait for a property: the property belongs to that node.", ["With says instead of property, nodePath can be left out and the whole game is searched"]);
41487
+ }
41488
+ const nodePath = asked ?? "/root";
41434
41489
  const patience = Math.max(this.runtimeTimeoutMs(), timeoutMs + 5000);
41435
41490
  return op === "signal" ? await this.handleRuntimeCommand("wait_signal", {
41436
41491
  ...whichGame(args),
@@ -43098,12 +43153,12 @@ that is running, and the project on disk. ${TOOL_SPECS.length} tools, named \`do
43098
43153
  | What a property on an object a node holds reads | the same two, with colons: \`_game:clock:speed\` |
43099
43154
  | Which control holds the focus, or anything only a method answers | the same path with a step written as a call, \`get_viewport():gui_get_focus_owner\`, which calls a method taking no arguments and walks into what it returned |
43100
43155
  | Press a button | \`runtime_input click\`, which says what was under the pointer |
43101
- | Press the next thing after a click that changed screen | \`runtime_wait until\` with \`says\` and a word the new screen shows, then the click. A click sent straight after a screen change lands on a control the new screen is about to free, answers \`landed: true\`, and does nothing; a frame count is a number nobody can pick correctly across a cold start and a warm one, and waiting for the words answered in about 130ms where twenty frames had not been enough |
43156
+ | Press the next thing after a click that changed screen | \`runtime_wait until\` with \`says\` and a word the new screen shows (no \`nodePath\` needed: without one the whole game is searched), then the click. A click sent straight after a screen change lands on a control the new screen is about to free, answers \`landed: true\`, and does nothing; a frame count is a number nobody can pick correctly across a cold start and a warm one, and waiting for the words answered in about 130ms where twenty frames had not been enough |
43102
43157
  | Fill in a field | \`runtime_input click\` on it, then \`runtime_input text\` with \`replace\`; a submitted field has to be clicked again |
43103
43158
  | Click somebody standing in a 3D room | \`runtime_input click\` on the Node3D, which aims at what it draws |
43104
43159
  | Pick something out of a dropdown | \`runtime_input choose\` on it, by what the item says, rather than a click and keys: the menu opens as a window of its own that an injected key never reaches, so Enter closes it with nothing chosen |
43105
- | Answer a dialog | \`runtime_input click\` on its button, or \`key\` Escape to dismiss it: an [AcceptDialog] reads that key itself and never asks the InputMap, so \`ui_cancel\` leaves it standing |
43106
- | Press a bound key | \`runtime_input action\` or \`key\`, a whole press unless you hold it |
43160
+ | Answer a dialog | \`runtime_input click\` on its button, or \`runtime_input key\` with \`keycode\` \`Escape\` to dismiss it: an [AcceptDialog] reads that key itself and never asks the InputMap, so \`ui_cancel\` leaves it standing |
43161
+ | Press a bound key | \`runtime_input action\` with the action's name, or \`runtime_input key\` with \`keycode\`, a whole press unless you hold it |
43107
43162
  | Where a 3D node is on screen | \`runtime_inspect\` \`rect\`, rather than unprojecting by hand |
43108
43163
  | Wait for something in a running game | \`runtime_wait\`, never a sleep; \`until\` with \`says\` for a panel that rebuilds its own labels. The words have to be shown: a button that exists hidden through an animation does not satisfy it until it appears, unless \`includeHidden\` says otherwise |
43109
43164
  | What every label on a screen says | \`runtime_inspect tree\` with \`properties\` naming \`text\`, which reads it off every node that has one; \`includeProperties\` is every stored property of every node and ran to seventy-six thousand characters for one row |
package/build/index.js CHANGED
@@ -10182,6 +10182,8 @@ import { randomUUID as randomUUID2 } from "node:crypto";
10182
10182
  import {
10183
10183
  appendFileSync,
10184
10184
  closeSync as closeSync2,
10185
+ constants,
10186
+ copyFileSync,
10185
10187
  existsSync as existsSync10,
10186
10188
  fstatSync,
10187
10189
  mkdirSync as mkdirSync10,
@@ -10194,7 +10196,7 @@ import {
10194
10196
  statSync as statSync7
10195
10197
  } from "node:fs";
10196
10198
  import { tmpdir as tmpdir4 } from "node:os";
10197
- import { basename as basename3, dirname as dirname10, isAbsolute as isAbsolute3, join as join16, normalize as normalize2, relative as relative3 } from "node:path";
10199
+ import { basename as basename3, dirname as dirname10, extname as extname2, isAbsolute as isAbsolute3, join as join16, normalize as normalize2, relative as relative3 } from "node:path";
10198
10200
  import { setTimeout as delay3 } from "node:timers/promises";
10199
10201
  import { fileURLToPath as fileURLToPath3 } from "node:url";
10200
10202
  import { promisify as promisify5 } from "node:util";
@@ -30472,7 +30474,11 @@ var TOOL_SPECS = [
30472
30474
  description: "viewport: the Viewport node. Default the root viewport."
30473
30475
  },
30474
30476
  width: { type: "number", description: "Scale the image to this width." },
30475
- height: { type: "number", description: "Scale the image to this height." }
30477
+ height: { type: "number", description: "Scale the image to this height." },
30478
+ outputPath: {
30479
+ type: "string",
30480
+ description: "Also save the picture as a PNG at this absolute path, for somebody to open later; the answer says where. Refused unless it ends in .png, its directory exists, it is outside the project, and no file is there already."
30481
+ }
30476
30482
  },
30477
30483
  requires: [],
30478
30484
  operations: {
@@ -30495,7 +30501,7 @@ var TOOL_SPECS = [
30495
30501
  action: {
30496
30502
  type: "string",
30497
30503
  ops: ["action"],
30498
- description: "action: the InputMap action name. An engine dialog is not answered this way: AcceptDialog reads the Escape key itself and never asks the InputMap, so ui_cancel goes in and the question stays up. Dismiss one with key Escape, or click its button."
30504
+ description: "action: the InputMap action name. An engine dialog is not answered this way: AcceptDialog reads the Escape key itself and never asks the InputMap, so ui_cancel goes in and the question stays up. Dismiss one with the key op and keycode Escape, or click its button."
30499
30505
  },
30500
30506
  pressed: {
30501
30507
  type: "boolean",
@@ -30598,7 +30604,11 @@ var TOOL_SPECS = [
30598
30604
  ops: ["frames"],
30599
30605
  description: "frames: how many to let pass, 1 to 600. More than that is refused."
30600
30606
  },
30601
- nodePath: { type: "string", ops: ["signal", "until"], description: "signal, until: the node." },
30607
+ nodePath: {
30608
+ type: "string",
30609
+ ops: ["signal", "until"],
30610
+ description: "signal, until: the node. Needed for a signal and for a property; for says it defaults to /root, the whole screen."
30611
+ },
30602
30612
  signal: { type: "string", ops: ["signal"], description: "signal: the signal name." },
30603
30613
  property: {
30604
30614
  type: "string",
@@ -30609,7 +30619,7 @@ var TOOL_SPECS = [
30609
30619
  says: {
30610
30620
  type: "string",
30611
30621
  ops: ["until"],
30612
- description: "until: wait for these words to appear anywhere under nodePath instead of for a property, which is how a panel that rebuilds its labels is waited on at all: the labels are named afresh each redraw and the panel is what stays put. Case-insensitive, part of a line, on a node the player can see unless includeHidden says otherwise; a label with a line break is matched with the break in the words, and a backslash followed by n counts as one. Instead of, not as well as: a call carrying this and a property is refused, because they ask about different things and answering one of them silently is how a caller watches a screen believing they are watching a property."
30622
+ description: "until: wait for these words to appear anywhere under nodePath, or anywhere in the game when no nodePath is given, instead of for a property, which is how a panel that rebuilds its labels is waited on at all: the labels are named afresh each redraw and the panel is what stays put. Case-insensitive, part of a line, on a node the player can see unless includeHidden says otherwise; a label with a line break is matched with the break in the words, and a backslash followed by n counts as one. Instead of, not as well as: a call carrying this and a property is refused, because they ask about different things and answering one of them silently is how a caller watches a screen believing they are watching a property."
30613
30623
  },
30614
30624
  includeHidden: {
30615
30625
  type: "boolean",
@@ -30631,7 +30641,7 @@ var TOOL_SPECS = [
30631
30641
  },
30632
30642
  until: {
30633
30643
  summary: "wait for a property to read as a value, or for words to appear under a node, and answer with what it found",
30634
- requires: ["nodePath"]
30644
+ requires: []
30635
30645
  }
30636
30646
  }
30637
30647
  },
@@ -30733,6 +30743,30 @@ var __dirname2 = dirname10(fileURLToPath3(import.meta.url));
30733
30743
  var EDITOR_RESTART_TIMEOUT_MS = 90000;
30734
30744
  var HOLD_PING_MS = 2000;
30735
30745
  var SLOWEST_FRAME_RATE = 20;
30746
+ function captureDestinationRefusal(outputPath, projects) {
30747
+ if (/^(res|user):\/\//.test(outputPath)) {
30748
+ return `outputPath ${outputPath} is a Godot path; a capture is saved outside the project, at an absolute path on this machine.`;
30749
+ }
30750
+ if (!isAbsolute3(outputPath)) {
30751
+ return `outputPath ${outputPath} is not absolute; name the whole path, so where it is written does not depend on this server's directory.`;
30752
+ }
30753
+ if (extname2(outputPath).toLowerCase() !== ".png") {
30754
+ return `outputPath ${outputPath} does not end in .png, which is what a capture is.`;
30755
+ }
30756
+ const directory = realPathOr(dirname10(outputPath));
30757
+ if (!existsSync10(directory)) {
30758
+ return `outputPath's directory ${dirname10(outputPath)} does not exist; a capture creates no directories.`;
30759
+ }
30760
+ const where = join16(directory, basename3(outputPath));
30761
+ const inside = projects.find((project) => project !== "" && isWithinRoot(realPathOr(project), where));
30762
+ if (inside !== undefined) {
30763
+ return `outputPath ${outputPath} is inside the project at ${inside}; a capture is saved outside it, so it never becomes part of the game.`;
30764
+ }
30765
+ if (existsSync10(where)) {
30766
+ return `outputPath ${outputPath} already exists; a capture is not written over a file. Name a new one.`;
30767
+ }
30768
+ return null;
30769
+ }
30736
30770
  function patienceForFrames(frames, atLeast) {
30737
30771
  return Math.max(atLeast, frames / SLOWEST_FRAME_RATE * 1000 + atLeast);
30738
30772
  }
@@ -34071,7 +34105,7 @@ class GodotServer {
34071
34105
  return `The game this server started (${which}) is still coming up and has not announced its runtime after a wait of ${Math.min(budgetMs, remaining)}ms, so there is nothing to talk to yet. It is still running: ask again in a moment rather than starting another.`;
34072
34106
  }
34073
34107
  async handleRuntimeCommand(command, args, timeoutMs = this.runtimeTimeoutMs()) {
34074
- const { op: _op, projectPath, pid, ...params } = asParams(args);
34108
+ const { op: _op, projectPath, pid, outputPath, ...params } = asParams(args);
34075
34109
  const waited = typeof pid === "number" ? null : await this.ownRunOnItsWay(typeof projectPath === "string" ? projectPath : this.ownProject, timeoutMs);
34076
34110
  if (waited !== null) {
34077
34111
  return this.createErrorResponse(waited, [
@@ -34098,6 +34132,16 @@ class GodotServer {
34098
34132
  ]);
34099
34133
  }
34100
34134
  const expectsScreenshot = command === "capture_screenshot" || command === "capture_viewport";
34135
+ const saveTo = expectsScreenshot && typeof outputPath === "string" ? outputPath : null;
34136
+ if (saveTo !== null) {
34137
+ const refusal = captureDestinationRefusal(saveTo, [
34138
+ choice.endpoint.project.path,
34139
+ ...this.ownProject === null ? [] : [this.ownProject]
34140
+ ]);
34141
+ if (refusal !== null) {
34142
+ return this.createErrorResponse(refusal);
34143
+ }
34144
+ }
34101
34145
  const screenshotDir = expectsScreenshot ? mkdtempSync2(join16(tmpdir4(), "gdharness-runtime-screenshot-")) : null;
34102
34146
  const screenshotPath = screenshotDir ? join16(screenshotDir, "capture.png") : null;
34103
34147
  try {
@@ -34117,11 +34161,18 @@ class GodotServer {
34117
34161
  return this.createErrorResponse(`Rejected screenshot file path outside the managed capture path: '${returnedPath}'`);
34118
34162
  }
34119
34163
  const dimensions = `${readNumber(payload, "width") ?? 0}x${readNumber(payload, "height") ?? 0} ${readString(payload, "format") ?? "unknown"}`;
34164
+ if (saveTo !== null) {
34165
+ try {
34166
+ copyFileSync(screenshotPath, saveTo, constants.COPYFILE_EXCL);
34167
+ } catch (error) {
34168
+ return this.createErrorResponse(`The capture was taken and could not be saved at ${saveTo}: ${errorMessage(error)}`);
34169
+ }
34170
+ }
34120
34171
  return {
34121
34172
  content: [
34122
34173
  {
34123
34174
  type: "text",
34124
- text: `Screenshot captured: ${dimensions}${chosenAmongSeveral ? ` from pid ${own}, the game this server holds` : ""}`
34175
+ text: `Screenshot captured: ${dimensions}${chosenAmongSeveral ? ` from pid ${own}, the game this server holds` : ""}${saveTo === null ? "" : `. Saved to ${saveTo}`}`
34125
34176
  },
34126
34177
  { type: "image", data: readFileSync16(screenshotPath).toString("base64"), mimeType: "image/png" }
34127
34178
  ]
@@ -34168,7 +34219,11 @@ class GodotServer {
34168
34219
  return await this.handleRuntimeCommand("wait_frames", { ...whichGame(args), frames }, waited);
34169
34220
  }
34170
34221
  const timeoutMs = readPositiveNumber(args, "timeoutMs") ?? 5000;
34171
- const nodePath = readNonEmptyString(args, "nodePath") ?? "";
34222
+ const asked = readNonEmptyString(args, "nodePath");
34223
+ if (asked === undefined && op === "until" && readNonEmptyString(args, "says") === undefined) {
34224
+ return this.createErrorResponse("runtime_wait until needs nodePath to wait for a property: the property belongs to that node.", ["With says instead of property, nodePath can be left out and the whole game is searched"]);
34225
+ }
34226
+ const nodePath = asked ?? "/root";
34172
34227
  const patience = Math.max(this.runtimeTimeoutMs(), timeoutMs + 5000);
34173
34228
  return op === "signal" ? await this.handleRuntimeCommand("wait_signal", {
34174
34229
  ...whichGame(args),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gdharness",
3
- "version": "1.0.14",
3
+ "version": "1.0.15",
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",