gdharness 0.6.4 → 0.6.5

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
@@ -8962,7 +8962,7 @@ var init_versions = __esm(() => {
8962
8962
  version = {
8963
8963
  major: 4,
8964
8964
  minor: 6,
8965
- patch: 4
8965
+ patch: 5
8966
8966
  };
8967
8967
  });
8968
8968
 
@@ -15744,12 +15744,12 @@ var init_tool_definitions = __esm(() => {
15744
15744
  },
15745
15745
  {
15746
15746
  name: "runtime_inspect",
15747
- description: "Questions about the running game: the scene tree, the nodes matching a query, where one node is on screen, what one property reads, or the performance metrics. Needs the game running with the runtime addon.",
15747
+ description: "Questions about the running game: what is written on the screen, the scene tree, the nodes matching a query, where one node is on screen, what one property reads, or the performance metrics. Needs the game running with the runtime addon.",
15748
15748
  parameters: {
15749
15749
  projectPath: RUNNING_PROJECT_PATH,
15750
15750
  nodePath: {
15751
15751
  type: "string",
15752
- description: "tree, find: where to start, default /root. rect: the node to place. property: the node to read."
15752
+ description: "tree, find, text: where to start, default /root. rect: the node to place. property: the node to read."
15753
15753
  },
15754
15754
  property: {
15755
15755
  type: "string",
@@ -15771,6 +15771,10 @@ var init_tool_definitions = __esm(() => {
15771
15771
  },
15772
15772
  group: { type: "string", description: "find: a group the node is in." },
15773
15773
  limit: { type: "number", description: "find: the most nodes to answer with. Default 100." },
15774
+ includeHidden: {
15775
+ type: "boolean",
15776
+ description: "text: read hidden nodes as well, for checking that something is not showing. Default false."
15777
+ },
15774
15778
  metrics: {
15775
15779
  type: "array",
15776
15780
  items: { type: "string" },
@@ -15780,6 +15784,10 @@ var init_tool_definitions = __esm(() => {
15780
15784
  requires: [],
15781
15785
  operations: {
15782
15786
  tree: { summary: "the live scene tree", requires: [] },
15787
+ text: {
15788
+ summary: "every line of text under nodePath, in the order somebody reads the screen, leaving out what is hidden and everything under it",
15789
+ requires: []
15790
+ },
15783
15791
  find: {
15784
15792
  summary: "the paths of every node matching className, script, namePattern or group, with property read off each",
15785
15793
  requires: []
@@ -35844,6 +35852,9 @@ import { basename as basename2, dirname as dirname9, join as join15, normalize a
35844
35852
  import { setTimeout as delay2 } from "node:timers/promises";
35845
35853
  import { fileURLToPath as fileURLToPath4 } from "node:url";
35846
35854
  import { promisify as promisify5 } from "node:util";
35855
+ function patienceForFrames(frames, atLeast) {
35856
+ return Math.max(atLeast, frames / SLOWEST_FRAME_RATE * 1000 + atLeast);
35857
+ }
35847
35858
  function escapingPropertyValue(properties) {
35848
35859
  if (typeof properties !== "object" || properties === null) {
35849
35860
  return;
@@ -36323,6 +36334,12 @@ class GodotServer {
36323
36334
  });
36324
36335
  case "find":
36325
36336
  return await this.handleFindRuntimeNodes(args);
36337
+ case "text":
36338
+ return await this.handleRuntimeCommand("read_text", {
36339
+ projectPath: args["projectPath"],
36340
+ root: readNonEmptyString(args, "nodePath") ?? "/root",
36341
+ include_hidden: readBoolean(args, "includeHidden") ?? false
36342
+ });
36326
36343
  case "rect":
36327
36344
  return await this.handleRuntimeCommand("get_rect", {
36328
36345
  projectPath: args["projectPath"],
@@ -37367,8 +37384,11 @@ class GodotServer {
37367
37384
  const nodePath = readNonEmptyString(args, "nodePath") ?? "";
37368
37385
  const patience = Math.max(this.runtimeTimeoutMs(), timeoutMs + 5000);
37369
37386
  switch (op) {
37370
- case "frames":
37371
- return await this.handleRuntimeCommand("wait_frames", { projectPath: args["projectPath"], frames: readPositiveNumber(args, "frames") ?? 1 }, patience);
37387
+ case "frames": {
37388
+ const frames = readPositiveNumber(args, "frames") ?? 1;
37389
+ const waited = patienceForFrames(frames, patience);
37390
+ return await this.handleRuntimeCommand("wait_frames", { projectPath: args["projectPath"], frames }, waited);
37391
+ }
37372
37392
  case "signal":
37373
37393
  return await this.handleRuntimeCommand("wait_signal", {
37374
37394
  projectPath: args["projectPath"],
@@ -37391,7 +37411,7 @@ async function runGodotServer() {
37391
37411
  const server = new GodotServer;
37392
37412
  await server.run();
37393
37413
  }
37394
- var UPDATE_NOTICE_EVERY = 500, FEEDBACK_NOTICE_EVERY = 250, run5, __dirname2, EDITOR_RESTART_TIMEOUT_MS = 90000, BRIDGE_RETRY_MS = 2000, PATH_SOLUTIONS, PROJECT_FILE_ARGUMENTS, DEBUG_STATE_CALLS, HEADLESS_OPERATIONS, PROJECT_INFO_SECTIONS;
37414
+ var UPDATE_NOTICE_EVERY = 500, FEEDBACK_NOTICE_EVERY = 250, run5, __dirname2, EDITOR_RESTART_TIMEOUT_MS = 90000, SLOWEST_FRAME_RATE = 20, BRIDGE_RETRY_MS = 2000, PATH_SOLUTIONS, PROJECT_FILE_ARGUMENTS, DEBUG_STATE_CALLS, HEADLESS_OPERATIONS, PROJECT_INFO_SECTIONS;
37395
37415
  var init_server3 = __esm(() => {
37396
37416
  init_mcp();
37397
37417
  init_stdio2();
@@ -38740,6 +38760,7 @@ that is running, and the project on disk. ${TOOL_SPECS.length} tools, named \`do
38740
38760
 
38741
38761
  | Ask | Tool |
38742
38762
  | --- | --- |
38763
+ | What a screen says | \`runtime_inspect text\`, which reads what is drawn and leaves out what is hidden |
38743
38764
  | Where a control is, and whether it is visible | \`runtime_inspect\` \`find\`, \`rect\` |
38744
38765
  | What a property reads right now | \`runtime_inspect\` \`property\`, \`runtime_invoke\` |
38745
38766
  | Press a button | \`runtime_input click\`, which says what was under the pointer |
@@ -57,6 +57,7 @@ func _init() -> void:
57
57
  "ping": _ping,
58
58
  "get_tree": _queries.get_tree,
59
59
  "find_nodes": _queries.find_nodes,
60
+ "read_text": _queries.read_text,
60
61
  "get_rect": _queries.get_rect,
61
62
  "get_property": _queries.get_property,
62
63
  "set_property": _queries.set_property,
@@ -10,6 +10,10 @@ const Values = preload("runtime_values.gd")
10
10
  const FIND_LIMIT: int = 100
11
11
  const FIND_LIMIT_CEILING: int = 1000
12
12
 
13
+ ## The most lines one read answers with. A screen is a few dozen; a thousand is a tree somebody
14
+ ## pointed this at by mistake.
15
+ const READ_LIMIT: int = 500
16
+
13
17
  var _host: Node
14
18
  var _values: Values
15
19
 
@@ -138,6 +142,60 @@ func _matches(
138
142
  ## is drawn in, in both the canvas coordinates the node reports and the window pixels input
139
143
  ## arrives in. The two differ whenever the project stretches its viewport, which is what made a
140
144
  ## rect unusable for a click.
145
+ ## Every line of text under a node, in the order somebody reads the screen.
146
+ ##
147
+ ## The question a caller asks most often, and the one that cost the most to answer: reading a panel
148
+ ## was a find for every label with the path of each beside it, a few hundred characters apiece to
149
+ ## carry a sentence of six words. Long is the smaller half of it. A find answers off nodes nobody
150
+ ## can see, and a panel keeps its empty state in the tree beside its rows, so "Nothing posted" came
151
+ ## back next to the three things posted and was believed twice in one session.
152
+ ##
153
+ ## A hidden node is left out and so is everything under it, because what a player reads is what is
154
+ ## drawn. [param include_hidden] asks for the lot instead, which is what a caller checking that
155
+ ## something is not showing wants.
156
+ func read_text(params: Dictionary) -> Dictionary:
157
+ var root_path: String = str(params.get("root", "/root"))
158
+ var include_hidden: bool = bool(params.get("include_hidden", false))
159
+
160
+ var root: Node = _host.get_tree().root.get_node_or_null(root_path)
161
+ if root == null:
162
+ return {"type": "error", "message": "Node not found: " + root_path}
163
+
164
+ var lines: PackedStringArray = PackedStringArray()
165
+ _read_into(root, include_hidden, lines)
166
+ return {
167
+ "type": "text",
168
+ "root": root_path,
169
+ "lines": lines,
170
+ "count": lines.size(),
171
+ "truncated": lines.size() >= READ_LIMIT,
172
+ }
173
+
174
+
175
+ ## Walks [param node] depth first, which is the order the screen is laid out in and the order a
176
+ ## person reads it.
177
+ func _read_into(node: Node, include_hidden: bool, into: PackedStringArray) -> void:
178
+ if into.size() >= READ_LIMIT:
179
+ return
180
+ var control: CanvasItem = node as CanvasItem
181
+ if not include_hidden and control != null and not control.visible:
182
+ return
183
+ var said: String = _said_by(node)
184
+ if not said.is_empty():
185
+ into.append(said)
186
+ for child: Node in node.get_children():
187
+ _read_into(child, include_hidden, into)
188
+
189
+
190
+ ## What one node says, or "" for a node that says nothing. Anything with a `text` property, which
191
+ ## is every label, button and field the interface is built out of.
192
+ static func _said_by(node: Node) -> String:
193
+ for property: Dictionary in node.get_property_list():
194
+ if str(property.get("name", "")) == "text" and int(property.get("type", 0)) == TYPE_STRING:
195
+ return str(node.get("text")).strip_edges()
196
+ return ""
197
+
198
+
141
199
  func get_rect(params: Dictionary) -> Dictionary:
142
200
  var node_path: String = str(params.get("path", ""))
143
201
  if node_path.is_empty():
package/build/index.js CHANGED
@@ -15311,7 +15311,7 @@ ${content.join(`
15311
15311
  var version = {
15312
15312
  major: 4,
15313
15313
  minor: 6,
15314
- patch: 4
15314
+ patch: 5
15315
15315
  };
15316
15316
 
15317
15317
  // node_modules/zod/v4/core/schemas.js
@@ -28298,12 +28298,12 @@ var TOOL_SPECS = [
28298
28298
  },
28299
28299
  {
28300
28300
  name: "runtime_inspect",
28301
- description: "Questions about the running game: the scene tree, the nodes matching a query, where one node is on screen, what one property reads, or the performance metrics. Needs the game running with the runtime addon.",
28301
+ description: "Questions about the running game: what is written on the screen, the scene tree, the nodes matching a query, where one node is on screen, what one property reads, or the performance metrics. Needs the game running with the runtime addon.",
28302
28302
  parameters: {
28303
28303
  projectPath: RUNNING_PROJECT_PATH,
28304
28304
  nodePath: {
28305
28305
  type: "string",
28306
- description: "tree, find: where to start, default /root. rect: the node to place. property: the node to read."
28306
+ description: "tree, find, text: where to start, default /root. rect: the node to place. property: the node to read."
28307
28307
  },
28308
28308
  property: {
28309
28309
  type: "string",
@@ -28325,6 +28325,10 @@ var TOOL_SPECS = [
28325
28325
  },
28326
28326
  group: { type: "string", description: "find: a group the node is in." },
28327
28327
  limit: { type: "number", description: "find: the most nodes to answer with. Default 100." },
28328
+ includeHidden: {
28329
+ type: "boolean",
28330
+ description: "text: read hidden nodes as well, for checking that something is not showing. Default false."
28331
+ },
28328
28332
  metrics: {
28329
28333
  type: "array",
28330
28334
  items: { type: "string" },
@@ -28334,6 +28338,10 @@ var TOOL_SPECS = [
28334
28338
  requires: [],
28335
28339
  operations: {
28336
28340
  tree: { summary: "the live scene tree", requires: [] },
28341
+ text: {
28342
+ summary: "every line of text under nodePath, in the order somebody reads the screen, leaving out what is hidden and everything under it",
28343
+ requires: []
28344
+ },
28337
28345
  find: {
28338
28346
  summary: "the paths of every node matching className, script, namePattern or group, with property read off each",
28339
28347
  requires: []
@@ -28563,6 +28571,10 @@ var FEEDBACK_NOTICE_EVERY = 250;
28563
28571
  var run3 = promisify3(execFile3);
28564
28572
  var __dirname2 = dirname4(fileURLToPath2(import.meta.url));
28565
28573
  var EDITOR_RESTART_TIMEOUT_MS = 90000;
28574
+ var SLOWEST_FRAME_RATE = 20;
28575
+ function patienceForFrames(frames, atLeast) {
28576
+ return Math.max(atLeast, frames / SLOWEST_FRAME_RATE * 1000 + atLeast);
28577
+ }
28566
28578
  var BRIDGE_RETRY_MS = 2000;
28567
28579
  var PATH_SOLUTIONS = [
28568
28580
  'Give the path relative to the project, such as "scenes/main.tscn" or "res://scenes/main.tscn"',
@@ -29096,6 +29108,12 @@ class GodotServer {
29096
29108
  });
29097
29109
  case "find":
29098
29110
  return await this.handleFindRuntimeNodes(args);
29111
+ case "text":
29112
+ return await this.handleRuntimeCommand("read_text", {
29113
+ projectPath: args["projectPath"],
29114
+ root: readNonEmptyString(args, "nodePath") ?? "/root",
29115
+ include_hidden: readBoolean(args, "includeHidden") ?? false
29116
+ });
29099
29117
  case "rect":
29100
29118
  return await this.handleRuntimeCommand("get_rect", {
29101
29119
  projectPath: args["projectPath"],
@@ -30140,8 +30158,11 @@ class GodotServer {
30140
30158
  const nodePath = readNonEmptyString(args, "nodePath") ?? "";
30141
30159
  const patience = Math.max(this.runtimeTimeoutMs(), timeoutMs + 5000);
30142
30160
  switch (op) {
30143
- case "frames":
30144
- return await this.handleRuntimeCommand("wait_frames", { projectPath: args["projectPath"], frames: readPositiveNumber(args, "frames") ?? 1 }, patience);
30161
+ case "frames": {
30162
+ const frames = readPositiveNumber(args, "frames") ?? 1;
30163
+ const waited = patienceForFrames(frames, patience);
30164
+ return await this.handleRuntimeCommand("wait_frames", { projectPath: args["projectPath"], frames }, waited);
30165
+ }
30145
30166
  case "signal":
30146
30167
  return await this.handleRuntimeCommand("wait_signal", {
30147
30168
  projectPath: args["projectPath"],
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gdharness",
3
- "version": "0.6.4",
3
+ "version": "0.6.5",
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",