gdharness 0.6.5 → 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
@@ -15850,9 +15850,15 @@ var init_tool_definitions = __esm(() => {
15850
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."
15851
15851
  },
15852
15852
  action: { type: "string", description: "action: the InputMap action name." },
15853
- 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
+ },
15854
15857
  strength: { type: "number", description: "action: 0 to 1. Default 1." },
15855
- 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
+ },
15856
15862
  text: {
15857
15863
  type: "string",
15858
15864
  description: "text: what to type. A newline is Enter and a tab is Tab. choose: the item to take, by what it says."
@@ -15867,9 +15873,8 @@ var init_tool_definitions = __esm(() => {
15867
15873
  x: { type: "number", description: "mouse_click, mouse_motion: window pixels." },
15868
15874
  y: { type: "number", description: "mouse_click, mouse_motion: window pixels." },
15869
15875
  button: {
15870
- type: "string",
15871
- enum: ["left", "right", "middle"],
15872
- 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."
15873
15878
  },
15874
15879
  doubleClick: { type: "boolean", description: "click, mouse_click: default false." },
15875
15880
  relativeX: { type: "number", description: "mouse_motion: movement since the last event." },
@@ -15885,8 +15890,8 @@ var init_tool_definitions = __esm(() => {
15885
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",
15886
15891
  requires: ["nodePath"]
15887
15892
  },
15888
- action: { summary: "press or release an action", requires: ["action"] },
15889
- 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"] },
15890
15895
  text: {
15891
15896
  summary: "type a string wherever the focus is, a character at a time",
15892
15897
  requires: ["text"]
@@ -35855,6 +35860,45 @@ import { promisify as promisify5 } from "node:util";
35855
35860
  function patienceForFrames(frames, atLeast) {
35856
35861
  return Math.max(atLeast, frames / SLOWEST_FRAME_RATE * 1000 + atLeast);
35857
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
+ }
35858
35902
  function escapingPropertyValue(properties) {
35859
35903
  if (typeof properties !== "object" || properties === null) {
35860
35904
  return;
@@ -36193,6 +36237,10 @@ class GodotServer {
36193
36237
  response: this.createErrorResponse(`${spec.name} does not take ${unknown.join(", ")}. It takes: ${[...known].join(", ")}.`)
36194
36238
  };
36195
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
+ }
36196
36244
  let op = null;
36197
36245
  if (spec.operations) {
36198
36246
  const valid = Object.keys(spec.operations);
@@ -37411,7 +37459,7 @@ async function runGodotServer() {
37411
37459
  const server = new GodotServer;
37412
37460
  await server.run();
37413
37461
  }
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;
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;
37415
37463
  var init_server3 = __esm(() => {
37416
37464
  init_mcp();
37417
37465
  init_stdio2();
@@ -37438,6 +37486,12 @@ var init_server3 = __esm(() => {
37438
37486
  init_update_check();
37439
37487
  run5 = promisify5(execFile5);
37440
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
+ };
37441
37495
  PATH_SOLUTIONS = [
37442
37496
  'Give the path relative to the project, such as "scenes/main.tscn" or "res://scenes/main.tscn"',
37443
37497
  "Point projectPath at the project the file belongs to"
@@ -38680,6 +38734,15 @@ import { basename, dirname as dirname4, join as join9 } from "node:path";
38680
38734
 
38681
38735
  // src/tool-reference.ts
38682
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
+ }
38683
38746
  function renderToolsMarkdown() {
38684
38747
  const lines = [
38685
38748
  "# Tools",
@@ -38710,7 +38773,7 @@ function renderToolsMarkdown() {
38710
38773
  if (parameters.length > 0) {
38711
38774
  lines.push("Arguments:", "");
38712
38775
  for (const [name, schema] of parameters) {
38713
- const type = typeof schema["type"] === "string" ? schema["type"] : "any";
38776
+ const type = namedType(schema["type"]);
38714
38777
  const note = typeof schema["description"] === "string" ? ` ${schema["description"]}` : "";
38715
38778
  lines.push(`- \`${name}\` (${type}):${note}`);
38716
38779
  }
@@ -38767,6 +38830,7 @@ that is running, and the project on disk. ${TOOL_SPECS.length} tools, named \`do
38767
38830
  | Fill in a field | \`runtime_input click\` on it, then \`runtime_input text\` |
38768
38831
  | Click somebody standing in a 3D room | \`runtime_input click\` on the Node3D, which aims at what it draws |
38769
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 |
38770
38834
  | Where a 3D node is on screen | \`runtime_inspect\` \`rect\`, rather than unprojecting by hand |
38771
38835
  | Wait for something | \`runtime_wait\`, never a sleep |
38772
38836
  | A picture, for a person who asked to see one | \`runtime_capture\` |
@@ -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)
package/build/index.js CHANGED
@@ -28404,9 +28404,15 @@ var TOOL_SPECS = [
28404
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."
28405
28405
  },
28406
28406
  action: { type: "string", description: "action: the InputMap action name." },
28407
- 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
+ },
28408
28411
  strength: { type: "number", description: "action: 0 to 1. Default 1." },
28409
- 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
+ },
28410
28416
  text: {
28411
28417
  type: "string",
28412
28418
  description: "text: what to type. A newline is Enter and a tab is Tab. choose: the item to take, by what it says."
@@ -28421,9 +28427,8 @@ var TOOL_SPECS = [
28421
28427
  x: { type: "number", description: "mouse_click, mouse_motion: window pixels." },
28422
28428
  y: { type: "number", description: "mouse_click, mouse_motion: window pixels." },
28423
28429
  button: {
28424
- type: "string",
28425
- enum: ["left", "right", "middle"],
28426
- 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."
28427
28432
  },
28428
28433
  doubleClick: { type: "boolean", description: "click, mouse_click: default false." },
28429
28434
  relativeX: { type: "number", description: "mouse_motion: movement since the last event." },
@@ -28439,8 +28444,8 @@ var TOOL_SPECS = [
28439
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",
28440
28445
  requires: ["nodePath"]
28441
28446
  },
28442
- action: { summary: "press or release an action", requires: ["action"] },
28443
- 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"] },
28444
28449
  text: {
28445
28450
  summary: "type a string wherever the focus is, a character at a time",
28446
28451
  requires: ["text"]
@@ -28575,6 +28580,51 @@ var SLOWEST_FRAME_RATE = 20;
28575
28580
  function patienceForFrames(frames, atLeast) {
28576
28581
  return Math.max(atLeast, frames / SLOWEST_FRAME_RATE * 1000 + atLeast);
28577
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
+ }
28578
28628
  var BRIDGE_RETRY_MS = 2000;
28579
28629
  var PATH_SOLUTIONS = [
28580
28630
  'Give the path relative to the project, such as "scenes/main.tscn" or "res://scenes/main.tscn"',
@@ -28967,6 +29017,10 @@ class GodotServer {
28967
29017
  response: this.createErrorResponse(`${spec.name} does not take ${unknown.join(", ")}. It takes: ${[...known].join(", ")}.`)
28968
29018
  };
28969
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
+ }
28970
29024
  let op = null;
28971
29025
  if (spec.operations) {
28972
29026
  const valid = Object.keys(spec.operations);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gdharness",
3
- "version": "0.6.5",
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",