gdharness 0.6.4 → 0.6.6

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: []
@@ -15842,9 +15850,15 @@ var init_tool_definitions = __esm(() => {
15842
15850
  description: "click: the Control to click, at its centre, or the 3D node to click, where it is drawn. choose: the PopupMenu, or the OptionButton or MenuButton in front of one."
15843
15851
  },
15844
15852
  action: { type: "string", description: "action: the InputMap action name." },
15845
- pressed: { type: "boolean", description: "Press or release. Default true." },
15853
+ pressed: {
15854
+ type: "boolean",
15855
+ description: "action, key: leave it out and the press is a whole one, down and up a frame apart. true holds it down, false lets go of one being held."
15856
+ },
15846
15857
  strength: { type: "number", description: "action: 0 to 1. Default 1." },
15847
- keycode: { type: "string", description: 'key: the key name, such as "Space" or "A".' },
15858
+ keycode: {
15859
+ type: ["string", "number"],
15860
+ description: 'key: the key name, such as "Space" or "A", or its Godot keycode.'
15861
+ },
15848
15862
  text: {
15849
15863
  type: "string",
15850
15864
  description: "text: what to type. A newline is Enter and a tab is Tab. choose: the item to take, by what it says."
@@ -15859,9 +15873,8 @@ var init_tool_definitions = __esm(() => {
15859
15873
  x: { type: "number", description: "mouse_click, mouse_motion: window pixels." },
15860
15874
  y: { type: "number", description: "mouse_click, mouse_motion: window pixels." },
15861
15875
  button: {
15862
- type: "string",
15863
- enum: ["left", "right", "middle"],
15864
- description: "click, mouse_click: default left."
15876
+ type: ["string", "number"],
15877
+ description: "click, mouse_click: left, right, middle, wheel_up or wheel_down, or a button number. Default left."
15865
15878
  },
15866
15879
  doubleClick: { type: "boolean", description: "click, mouse_click: default false." },
15867
15880
  relativeX: { type: "number", description: "mouse_motion: movement since the last event." },
@@ -15877,8 +15890,8 @@ var init_tool_definitions = __esm(() => {
15877
15890
  summary: "take an item out of a menu, by what it says or by where it is in the list. A menu's items are drawn rather than built, so there is nothing to click: the item takes the focus and Enter presses it, which is the engine's own path and needs no window. Answers with what was chosen and what the button in front of it shows now",
15878
15891
  requires: ["nodePath"]
15879
15892
  },
15880
- action: { summary: "press or release an action", requires: ["action"] },
15881
- key: { summary: "press or release a key", requires: ["keycode"] },
15893
+ action: { summary: "press an action, or hold it", requires: ["action"] },
15894
+ key: { summary: "press a key, or hold it", requires: ["keycode"] },
15882
15895
  text: {
15883
15896
  summary: "type a string wherever the focus is, a character at a time",
15884
15897
  requires: ["text"]
@@ -35844,6 +35857,48 @@ import { basename as basename2, dirname as dirname9, join as join15, normalize a
35844
35857
  import { setTimeout as delay2 } from "node:timers/promises";
35845
35858
  import { fileURLToPath as fileURLToPath4 } from "node:url";
35846
35859
  import { promisify as promisify5 } from "node:util";
35860
+ function patienceForFrames(frames, atLeast) {
35861
+ return Math.max(atLeast, frames / SLOWEST_FRAME_RATE * 1000 + atLeast);
35862
+ }
35863
+ function isOfType(value, kind) {
35864
+ switch (kind) {
35865
+ case "string":
35866
+ return typeof value === "string";
35867
+ case "number":
35868
+ return typeof value === "number" && Number.isFinite(value);
35869
+ case "integer":
35870
+ return Number.isInteger(value);
35871
+ case "boolean":
35872
+ return typeof value === "boolean";
35873
+ case "array":
35874
+ return Array.isArray(value);
35875
+ case "object":
35876
+ return typeof value === "object" && value !== null && !Array.isArray(value);
35877
+ default:
35878
+ return true;
35879
+ }
35880
+ }
35881
+ function describeValue(value) {
35882
+ if (Array.isArray(value)) {
35883
+ return "a list";
35884
+ }
35885
+ return VALUE_NAMES[typeof value] ?? typeof value;
35886
+ }
35887
+ function wrongTypes(spec, args) {
35888
+ const complaints = [];
35889
+ for (const [name, value] of Object.entries(args)) {
35890
+ if (value === undefined || value === null) {
35891
+ continue;
35892
+ }
35893
+ const declared = spec.parameters[name]?.["type"];
35894
+ const wanted = typeof declared === "string" ? [declared] : Array.isArray(declared) ? declared : [];
35895
+ if (wanted.length === 0 || wanted.some((kind) => isOfType(value, kind))) {
35896
+ continue;
35897
+ }
35898
+ complaints.push(`${name} as ${wanted.join(" or ")}, not ${describeValue(value)}`);
35899
+ }
35900
+ return complaints;
35901
+ }
35847
35902
  function escapingPropertyValue(properties) {
35848
35903
  if (typeof properties !== "object" || properties === null) {
35849
35904
  return;
@@ -36182,6 +36237,10 @@ class GodotServer {
36182
36237
  response: this.createErrorResponse(`${spec.name} does not take ${unknown.join(", ")}. It takes: ${[...known].join(", ")}.`)
36183
36238
  };
36184
36239
  }
36240
+ const wrong = wrongTypes(spec, args);
36241
+ if (wrong.length > 0) {
36242
+ return { ok: false, response: this.createErrorResponse(`${spec.name} takes ${wrong.join("; ")}.`) };
36243
+ }
36185
36244
  let op = null;
36186
36245
  if (spec.operations) {
36187
36246
  const valid = Object.keys(spec.operations);
@@ -36323,6 +36382,12 @@ class GodotServer {
36323
36382
  });
36324
36383
  case "find":
36325
36384
  return await this.handleFindRuntimeNodes(args);
36385
+ case "text":
36386
+ return await this.handleRuntimeCommand("read_text", {
36387
+ projectPath: args["projectPath"],
36388
+ root: readNonEmptyString(args, "nodePath") ?? "/root",
36389
+ include_hidden: readBoolean(args, "includeHidden") ?? false
36390
+ });
36326
36391
  case "rect":
36327
36392
  return await this.handleRuntimeCommand("get_rect", {
36328
36393
  projectPath: args["projectPath"],
@@ -37367,8 +37432,11 @@ class GodotServer {
37367
37432
  const nodePath = readNonEmptyString(args, "nodePath") ?? "";
37368
37433
  const patience = Math.max(this.runtimeTimeoutMs(), timeoutMs + 5000);
37369
37434
  switch (op) {
37370
- case "frames":
37371
- return await this.handleRuntimeCommand("wait_frames", { projectPath: args["projectPath"], frames: readPositiveNumber(args, "frames") ?? 1 }, patience);
37435
+ case "frames": {
37436
+ const frames = readPositiveNumber(args, "frames") ?? 1;
37437
+ const waited = patienceForFrames(frames, patience);
37438
+ return await this.handleRuntimeCommand("wait_frames", { projectPath: args["projectPath"], frames }, waited);
37439
+ }
37372
37440
  case "signal":
37373
37441
  return await this.handleRuntimeCommand("wait_signal", {
37374
37442
  projectPath: args["projectPath"],
@@ -37391,7 +37459,7 @@ async function runGodotServer() {
37391
37459
  const server = new GodotServer;
37392
37460
  await server.run();
37393
37461
  }
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;
37462
+ var UPDATE_NOTICE_EVERY = 500, FEEDBACK_NOTICE_EVERY = 250, run5, __dirname2, EDITOR_RESTART_TIMEOUT_MS = 90000, SLOWEST_FRAME_RATE = 20, VALUE_NAMES, BRIDGE_RETRY_MS = 2000, PATH_SOLUTIONS, PROJECT_FILE_ARGUMENTS, DEBUG_STATE_CALLS, HEADLESS_OPERATIONS, PROJECT_INFO_SECTIONS;
37395
37463
  var init_server3 = __esm(() => {
37396
37464
  init_mcp();
37397
37465
  init_stdio2();
@@ -37418,6 +37486,12 @@ var init_server3 = __esm(() => {
37418
37486
  init_update_check();
37419
37487
  run5 = promisify5(execFile5);
37420
37488
  __dirname2 = dirname9(fileURLToPath4(import.meta.url));
37489
+ VALUE_NAMES = {
37490
+ string: "a string",
37491
+ number: "a number",
37492
+ boolean: "a boolean",
37493
+ object: "an object"
37494
+ };
37421
37495
  PATH_SOLUTIONS = [
37422
37496
  'Give the path relative to the project, such as "scenes/main.tscn" or "res://scenes/main.tscn"',
37423
37497
  "Point projectPath at the project the file belongs to"
@@ -38660,6 +38734,15 @@ import { basename, dirname as dirname4, join as join9 } from "node:path";
38660
38734
 
38661
38735
  // src/tool-reference.ts
38662
38736
  init_tool_definitions();
38737
+ function namedType(declared) {
38738
+ if (typeof declared === "string") {
38739
+ return declared;
38740
+ }
38741
+ if (Array.isArray(declared) && declared.every((kind) => typeof kind === "string")) {
38742
+ return declared.join(" or ");
38743
+ }
38744
+ return "any";
38745
+ }
38663
38746
  function renderToolsMarkdown() {
38664
38747
  const lines = [
38665
38748
  "# Tools",
@@ -38690,7 +38773,7 @@ function renderToolsMarkdown() {
38690
38773
  if (parameters.length > 0) {
38691
38774
  lines.push("Arguments:", "");
38692
38775
  for (const [name, schema] of parameters) {
38693
- const type = typeof schema["type"] === "string" ? schema["type"] : "any";
38776
+ const type = namedType(schema["type"]);
38694
38777
  const note = typeof schema["description"] === "string" ? ` ${schema["description"]}` : "";
38695
38778
  lines.push(`- \`${name}\` (${type}):${note}`);
38696
38779
  }
@@ -38740,12 +38823,14 @@ that is running, and the project on disk. ${TOOL_SPECS.length} tools, named \`do
38740
38823
 
38741
38824
  | Ask | Tool |
38742
38825
  | --- | --- |
38826
+ | What a screen says | \`runtime_inspect text\`, which reads what is drawn and leaves out what is hidden |
38743
38827
  | Where a control is, and whether it is visible | \`runtime_inspect\` \`find\`, \`rect\` |
38744
38828
  | What a property reads right now | \`runtime_inspect\` \`property\`, \`runtime_invoke\` |
38745
38829
  | Press a button | \`runtime_input click\`, which says what was under the pointer |
38746
38830
  | Fill in a field | \`runtime_input click\` on it, then \`runtime_input text\` |
38747
38831
  | Click somebody standing in a 3D room | \`runtime_input click\` on the Node3D, which aims at what it draws |
38748
38832
  | Pick something out of a dropdown | \`runtime_input choose\` on it, by what the item says |
38833
+ | Answer a dialog, or press a bound key | \`runtime_input action\` or \`key\`, a whole press unless you hold it |
38749
38834
  | Where a 3D node is on screen | \`runtime_inspect\` \`rect\`, rather than unprojecting by hand |
38750
38835
  | Wait for something | \`runtime_wait\`, never a sleep |
38751
38836
  | A picture, for a person who asked to see one | \`runtime_capture\` |
@@ -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,
@@ -30,9 +30,19 @@ func _init(host: Node, values: Values) -> void:
30
30
  _values = values
31
31
 
32
32
 
33
+ ## Presses [param params].action, and lets go of it again unless asked to hold it.
34
+ ##
35
+ ## The whole press by default, for the reason [method click] is the whole click: an action left
36
+ ## down is not a press that did nothing, it is a press that never ends, and everything reading
37
+ ## [method Input.is_action_pressed] goes on seeing it for the rest of the session. Answering a
38
+ ## dialog with `ui_accept` took two calls and left the first one held, which is the shape that
39
+ ## found this.
40
+ ##
41
+ ## `pressed` is how a caller says otherwise: true holds it down, false lets go of one being held.
33
42
  func inject_action(params: Dictionary) -> Dictionary:
34
43
  var action: String = String(params.get("action", ""))
35
- var pressed: bool = bool(params.get("pressed", true))
44
+ var held: bool = bool(params.get("pressed", true))
45
+ var whole: bool = not params.has("pressed")
36
46
  var strength: float = float(params.get("strength", 1.0))
37
47
 
38
48
  if action.is_empty():
@@ -41,18 +51,36 @@ func inject_action(params: Dictionary) -> Dictionary:
41
51
  if not InputMap.has_action(action):
42
52
  return {"type": "error", "message": "Action not found: " + action}
43
53
 
54
+ _say_action(action, whole or held, strength)
55
+ if whole:
56
+ # A frame between the halves, as a click has: a listener that acts on the press and a
57
+ # listener that acts on the release both get their own frame to do it in.
58
+ await _host.get_tree().process_frame
59
+ _say_action(action, false, strength)
60
+ await _host.get_tree().process_frame
61
+
62
+ return {
63
+ "type": "input_injected",
64
+ "input_type": "action",
65
+ "action": action,
66
+ "pressed": held and not whole,
67
+ "whole": whole,
68
+ }
69
+
70
+
71
+ func _say_action(action: String, down: bool, strength: float) -> void:
44
72
  var event: InputEventAction = InputEventAction.new()
45
73
  event.action = action
46
- event.pressed = pressed
74
+ event.pressed = down
47
75
  event.strength = strength
48
76
  Input.parse_input_event(event)
49
77
 
50
- return {"type": "input_injected", "input_type": "action", "action": action, "pressed": pressed}
51
-
52
78
 
79
+ ## Presses one key, and lets go of it again unless asked to hold it. See [method inject_action].
53
80
  func inject_key(params: Dictionary) -> Dictionary:
54
81
  var keycode_raw: Variant = params.get("keycode", 0)
55
- var pressed: bool = bool(params.get("pressed", true))
82
+ var held: bool = bool(params.get("pressed", true))
83
+ var whole: bool = not params.has("pressed")
56
84
  var key_label: String = String(params.get("key_label", ""))
57
85
 
58
86
  if keycode_raw is String:
@@ -62,7 +90,7 @@ func inject_key(params: Dictionary) -> Dictionary:
62
90
  var keycode: int = 0 if keycode_raw is String else int(keycode_raw)
63
91
 
64
92
  var event: InputEventKey = InputEventKey.new()
65
- event.pressed = pressed
93
+ event.pressed = whole or held
66
94
 
67
95
  if not key_label.is_empty():
68
96
  event.keycode = OS.find_keycode_from_string(key_label)
@@ -92,6 +120,12 @@ func inject_key(params: Dictionary) -> Dictionary:
92
120
  event.unicode = _glyph_of(event.keycode, event.shift_pressed)
93
121
 
94
122
  Input.parse_input_event(event)
123
+ if whole:
124
+ await _host.get_tree().process_frame
125
+ var up: InputEventKey = event.duplicate()
126
+ up.pressed = false
127
+ Input.parse_input_event(up)
128
+ await _host.get_tree().process_frame
95
129
 
96
130
  return {
97
131
  "type": "input_injected",
@@ -102,7 +136,8 @@ func inject_key(params: Dictionary) -> Dictionary:
102
136
  "ctrl": event.ctrl_pressed,
103
137
  "alt": event.alt_pressed,
104
138
  "unicode": event.unicode,
105
- "pressed": pressed
139
+ "pressed": held and not whole,
140
+ "whole": whole,
106
141
  }
107
142
 
108
143
 
@@ -190,6 +225,8 @@ func inject_mouse_click(params: Dictionary) -> Dictionary:
190
225
  return {"type": "error", "message": point}
191
226
  var position: Vector2 = point
192
227
  var button: int = _resolve_mouse_button(params.get("button", MOUSE_BUTTON_LEFT))
228
+ if button < 0:
229
+ return _no_such_button(params.get("button"))
193
230
  var pressed: bool = bool(params.get("pressed", true))
194
231
  var double: bool = bool(params.get("doubleClick", false))
195
232
 
@@ -334,6 +371,8 @@ func click(params: Dictionary) -> Dictionary:
334
371
  )
335
372
  }
336
373
  var button: int = _resolve_mouse_button(params.get("button", MOUSE_BUTTON_LEFT))
374
+ if button < 0:
375
+ return _no_such_button(params.get("button"))
337
376
  var double: bool = bool(params.get("double", false))
338
377
 
339
378
  # Pushed into the viewport rather than through Input: Input accumulates events and flushes
@@ -594,6 +633,8 @@ func _click_in_the_world(node_path: String, item: Node3D, params: Dictionary) ->
594
633
 
595
634
  var position: Vector2 = viewport.get_final_transform() * aim
596
635
  var button: int = _resolve_mouse_button(params.get("button", MOUSE_BUTTON_LEFT))
636
+ if button < 0:
637
+ return _no_such_button(params.get("button"))
597
638
  var double: bool = bool(params.get("double", false))
598
639
 
599
640
  viewport.push_input(_motion(position, Vector2.ZERO))
@@ -642,6 +683,23 @@ func _button(position: Vector2, button: int, pressed: bool, double: bool) -> Inp
642
683
  return event
643
684
 
644
685
 
686
+ ## What to say about a name that is not a mouse button, with the ones that are.
687
+ func _no_such_button(named: Variant) -> Dictionary:
688
+ return {
689
+ "type": "error",
690
+ "message":
691
+ (
692
+ "%s is not a mouse button. It takes: left, right, middle, wheel_up, wheel_down, or the number of one."
693
+ % JSON.stringify(named)
694
+ )
695
+ }
696
+
697
+
698
+ ## The button [param raw] names, or -1 for a name that is not one of them.
699
+ ##
700
+ ## A name nobody recognises used to come back as the left button, so a right click asked for by a
701
+ ## spelling this does not know went to the left button and reported the left button. The caller
702
+ ## reads the answer and believes it, which is the whole reason nothing here guesses.
645
703
  func _resolve_mouse_button(raw: Variant) -> int:
646
704
  if raw is String:
647
705
  var named: String = raw
@@ -657,5 +715,5 @@ func _resolve_mouse_button(raw: Variant) -> int:
657
715
  "wheel_down", "wheeldown":
658
716
  return MOUSE_BUTTON_WHEEL_DOWN
659
717
  _:
660
- return MOUSE_BUTTON_LEFT
718
+ return -1
661
719
  return int(raw)
@@ -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: []
@@ -28396,9 +28404,15 @@ var TOOL_SPECS = [
28396
28404
  description: "click: the Control to click, at its centre, or the 3D node to click, where it is drawn. choose: the PopupMenu, or the OptionButton or MenuButton in front of one."
28397
28405
  },
28398
28406
  action: { type: "string", description: "action: the InputMap action name." },
28399
- pressed: { type: "boolean", description: "Press or release. Default true." },
28407
+ pressed: {
28408
+ type: "boolean",
28409
+ description: "action, key: leave it out and the press is a whole one, down and up a frame apart. true holds it down, false lets go of one being held."
28410
+ },
28400
28411
  strength: { type: "number", description: "action: 0 to 1. Default 1." },
28401
- keycode: { type: "string", description: 'key: the key name, such as "Space" or "A".' },
28412
+ keycode: {
28413
+ type: ["string", "number"],
28414
+ description: 'key: the key name, such as "Space" or "A", or its Godot keycode.'
28415
+ },
28402
28416
  text: {
28403
28417
  type: "string",
28404
28418
  description: "text: what to type. A newline is Enter and a tab is Tab. choose: the item to take, by what it says."
@@ -28413,9 +28427,8 @@ var TOOL_SPECS = [
28413
28427
  x: { type: "number", description: "mouse_click, mouse_motion: window pixels." },
28414
28428
  y: { type: "number", description: "mouse_click, mouse_motion: window pixels." },
28415
28429
  button: {
28416
- type: "string",
28417
- enum: ["left", "right", "middle"],
28418
- description: "click, mouse_click: default left."
28430
+ type: ["string", "number"],
28431
+ description: "click, mouse_click: left, right, middle, wheel_up or wheel_down, or a button number. Default left."
28419
28432
  },
28420
28433
  doubleClick: { type: "boolean", description: "click, mouse_click: default false." },
28421
28434
  relativeX: { type: "number", description: "mouse_motion: movement since the last event." },
@@ -28431,8 +28444,8 @@ var TOOL_SPECS = [
28431
28444
  summary: "take an item out of a menu, by what it says or by where it is in the list. A menu's items are drawn rather than built, so there is nothing to click: the item takes the focus and Enter presses it, which is the engine's own path and needs no window. Answers with what was chosen and what the button in front of it shows now",
28432
28445
  requires: ["nodePath"]
28433
28446
  },
28434
- action: { summary: "press or release an action", requires: ["action"] },
28435
- key: { summary: "press or release a key", requires: ["keycode"] },
28447
+ action: { summary: "press an action, or hold it", requires: ["action"] },
28448
+ key: { summary: "press a key, or hold it", requires: ["keycode"] },
28436
28449
  text: {
28437
28450
  summary: "type a string wherever the focus is, a character at a time",
28438
28451
  requires: ["text"]
@@ -28563,6 +28576,55 @@ var FEEDBACK_NOTICE_EVERY = 250;
28563
28576
  var run3 = promisify3(execFile3);
28564
28577
  var __dirname2 = dirname4(fileURLToPath2(import.meta.url));
28565
28578
  var EDITOR_RESTART_TIMEOUT_MS = 90000;
28579
+ var SLOWEST_FRAME_RATE = 20;
28580
+ function patienceForFrames(frames, atLeast) {
28581
+ return Math.max(atLeast, frames / SLOWEST_FRAME_RATE * 1000 + atLeast);
28582
+ }
28583
+ function isOfType(value, kind) {
28584
+ switch (kind) {
28585
+ case "string":
28586
+ return typeof value === "string";
28587
+ case "number":
28588
+ return typeof value === "number" && Number.isFinite(value);
28589
+ case "integer":
28590
+ return Number.isInteger(value);
28591
+ case "boolean":
28592
+ return typeof value === "boolean";
28593
+ case "array":
28594
+ return Array.isArray(value);
28595
+ case "object":
28596
+ return typeof value === "object" && value !== null && !Array.isArray(value);
28597
+ default:
28598
+ return true;
28599
+ }
28600
+ }
28601
+ var VALUE_NAMES = {
28602
+ string: "a string",
28603
+ number: "a number",
28604
+ boolean: "a boolean",
28605
+ object: "an object"
28606
+ };
28607
+ function describeValue(value) {
28608
+ if (Array.isArray(value)) {
28609
+ return "a list";
28610
+ }
28611
+ return VALUE_NAMES[typeof value] ?? typeof value;
28612
+ }
28613
+ function wrongTypes(spec, args) {
28614
+ const complaints = [];
28615
+ for (const [name, value] of Object.entries(args)) {
28616
+ if (value === undefined || value === null) {
28617
+ continue;
28618
+ }
28619
+ const declared = spec.parameters[name]?.["type"];
28620
+ const wanted = typeof declared === "string" ? [declared] : Array.isArray(declared) ? declared : [];
28621
+ if (wanted.length === 0 || wanted.some((kind) => isOfType(value, kind))) {
28622
+ continue;
28623
+ }
28624
+ complaints.push(`${name} as ${wanted.join(" or ")}, not ${describeValue(value)}`);
28625
+ }
28626
+ return complaints;
28627
+ }
28566
28628
  var BRIDGE_RETRY_MS = 2000;
28567
28629
  var PATH_SOLUTIONS = [
28568
28630
  'Give the path relative to the project, such as "scenes/main.tscn" or "res://scenes/main.tscn"',
@@ -28955,6 +29017,10 @@ class GodotServer {
28955
29017
  response: this.createErrorResponse(`${spec.name} does not take ${unknown.join(", ")}. It takes: ${[...known].join(", ")}.`)
28956
29018
  };
28957
29019
  }
29020
+ const wrong = wrongTypes(spec, args);
29021
+ if (wrong.length > 0) {
29022
+ return { ok: false, response: this.createErrorResponse(`${spec.name} takes ${wrong.join("; ")}.`) };
29023
+ }
28958
29024
  let op = null;
28959
29025
  if (spec.operations) {
28960
29026
  const valid = Object.keys(spec.operations);
@@ -29096,6 +29162,12 @@ class GodotServer {
29096
29162
  });
29097
29163
  case "find":
29098
29164
  return await this.handleFindRuntimeNodes(args);
29165
+ case "text":
29166
+ return await this.handleRuntimeCommand("read_text", {
29167
+ projectPath: args["projectPath"],
29168
+ root: readNonEmptyString(args, "nodePath") ?? "/root",
29169
+ include_hidden: readBoolean(args, "includeHidden") ?? false
29170
+ });
29099
29171
  case "rect":
29100
29172
  return await this.handleRuntimeCommand("get_rect", {
29101
29173
  projectPath: args["projectPath"],
@@ -30140,8 +30212,11 @@ class GodotServer {
30140
30212
  const nodePath = readNonEmptyString(args, "nodePath") ?? "";
30141
30213
  const patience = Math.max(this.runtimeTimeoutMs(), timeoutMs + 5000);
30142
30214
  switch (op) {
30143
- case "frames":
30144
- return await this.handleRuntimeCommand("wait_frames", { projectPath: args["projectPath"], frames: readPositiveNumber(args, "frames") ?? 1 }, patience);
30215
+ case "frames": {
30216
+ const frames = readPositiveNumber(args, "frames") ?? 1;
30217
+ const waited = patienceForFrames(frames, patience);
30218
+ return await this.handleRuntimeCommand("wait_frames", { projectPath: args["projectPath"], frames }, waited);
30219
+ }
30145
30220
  case "signal":
30146
30221
  return await this.handleRuntimeCommand("wait_signal", {
30147
30222
  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.6",
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",