gdharness 0.13.44 → 1.0.1
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/README.md
CHANGED
|
@@ -26,9 +26,8 @@ property reads right now, what the console printed, what broke and on which line
|
|
|
26
26
|
Hand this to your agent:
|
|
27
27
|
|
|
28
28
|
```text
|
|
29
|
-
Install gdharness
|
|
30
|
-
|
|
31
|
-
recommend back to me.
|
|
29
|
+
Install gdharness here by following https://aureliolo.github.io/gdharness/agent.md,
|
|
30
|
+
then tell me what it asked you to recommend back to me.
|
|
32
31
|
```
|
|
33
32
|
|
|
34
33
|
Or do it yourself, and pick your harness:
|
|
@@ -53,16 +52,16 @@ emptily, so a call that succeeded is a call that was understood.
|
|
|
53
52
|
|
|
54
53
|
## At a glance
|
|
55
54
|
|
|
56
|
-
| |
|
|
57
|
-
| --------- |
|
|
58
|
-
| Needs | Godot 4.7 or newer, Node 22 or newer. Runs under Bun 1.4 too.
|
|
59
|
-
| Surface | 30 tools named `domain_verb`, and 4 `godot://` resources
|
|
60
|
-
| Reaches | The editor that is open, a game it is playing, and the project on disk
|
|
61
|
-
| Harnesses | 35, written inside the project wherever the harness has a project config
|
|
62
|
-
| Skill | Written to `.agents/skills`, which every major harness reads
|
|
63
|
-
| Install | npm, or a Sigstore-signed archive with an SBOM, SLSA Build Level 3
|
|
64
|
-
| Proven | Every tool driven against Godot 4.7.2 in CI, on Windows, Linux and macOS
|
|
65
|
-
| Status | 0
|
|
55
|
+
| | |
|
|
56
|
+
| --------- | ------------------------------------------------------------------------------------ |
|
|
57
|
+
| Needs | Godot 4.7 or newer, Node 22 or newer. Runs under Bun 1.4 too. |
|
|
58
|
+
| Surface | 30 tools named `domain_verb`, and 4 `godot://` resources |
|
|
59
|
+
| Reaches | The editor that is open, a game it is playing, and the project on disk |
|
|
60
|
+
| Harnesses | 35, written inside the project wherever the harness has a project config |
|
|
61
|
+
| Skill | Written to `.agents/skills`, which every major harness reads |
|
|
62
|
+
| Install | npm, or a Sigstore-signed archive with an SBOM, SLSA Build Level 3 |
|
|
63
|
+
| Proven | Every tool driven against Godot 4.7.2 in CI, on Windows, Linux and macOS |
|
|
64
|
+
| Status | 1.0: every release is a patch. [What a bump means](.github/CONTRIBUTING.md#versions) |
|
|
66
65
|
|
|
67
66
|
Older 4.x is likely to work and is not tested.
|
|
68
67
|
|
package/build/cli.js
CHANGED
|
@@ -15319,10 +15319,22 @@ function opTakes(spec, op, name) {
|
|
|
15319
15319
|
function argumentsOf(spec, op) {
|
|
15320
15320
|
return Object.keys(spec.parameters).filter((name) => opTakes(spec, op, name));
|
|
15321
15321
|
}
|
|
15322
|
-
function
|
|
15323
|
-
|
|
15322
|
+
function callsWithoutProjectPath() {
|
|
15323
|
+
const calls = [];
|
|
15324
|
+
for (const spec of TOOL_SPECS) {
|
|
15325
|
+
if (!Object.hasOwn(spec.parameters, "projectPath")) {
|
|
15326
|
+
calls.push(spec.name);
|
|
15327
|
+
continue;
|
|
15328
|
+
}
|
|
15329
|
+
for (const op of Object.keys(spec.operations ?? {})) {
|
|
15330
|
+
if (!opTakes(spec, op, "projectPath")) {
|
|
15331
|
+
calls.push(`${spec.name} ${op}`);
|
|
15332
|
+
}
|
|
15333
|
+
}
|
|
15334
|
+
}
|
|
15335
|
+
return calls.sort();
|
|
15324
15336
|
}
|
|
15325
|
-
function projectPathSentence(without =
|
|
15337
|
+
function projectPathSentence(without = callsWithoutProjectPath()) {
|
|
15326
15338
|
if (without.length === 0) {
|
|
15327
15339
|
return "Every call takes `projectPath`";
|
|
15328
15340
|
}
|
|
@@ -16327,12 +16339,12 @@ var init_tool_definitions = __esm(() => {
|
|
|
16327
16339
|
relativeX: {
|
|
16328
16340
|
type: "number",
|
|
16329
16341
|
ops: ["mouse_motion"],
|
|
16330
|
-
description: "mouse_motion: movement
|
|
16342
|
+
description: "mouse_motion: the movement the event carries. Left out, it is the distance from where the pointer last was, which is what a control that drags reads; give it to send a motion the position does not show."
|
|
16331
16343
|
},
|
|
16332
16344
|
relativeY: {
|
|
16333
16345
|
type: "number",
|
|
16334
16346
|
ops: ["mouse_motion"],
|
|
16335
|
-
description: "mouse_motion:
|
|
16347
|
+
description: "mouse_motion: as relativeX."
|
|
16336
16348
|
}
|
|
16337
16349
|
},
|
|
16338
16350
|
requires: [],
|
|
@@ -16346,13 +16358,22 @@ var init_tool_definitions = __esm(() => {
|
|
|
16346
16358
|
requires: ["nodePath"]
|
|
16347
16359
|
},
|
|
16348
16360
|
action: { summary: "press an action, or hold it", requires: ["action"] },
|
|
16349
|
-
key: {
|
|
16361
|
+
key: {
|
|
16362
|
+
summary: "press a key, or hold it. Not for a menu a click has opened: an OptionButton or PopupMenu pops up as a window of its own that a key sent to the game never reaches, so an arrow moves nothing in it and Enter closes it with nothing chosen. choose is what selects there",
|
|
16363
|
+
requires: ["keycode"]
|
|
16364
|
+
},
|
|
16350
16365
|
text: {
|
|
16351
16366
|
summary: "type a string into the field being edited, a character at a time, and say what it landed in",
|
|
16352
16367
|
requires: ["text"]
|
|
16353
16368
|
},
|
|
16354
|
-
mouse_click: {
|
|
16355
|
-
|
|
16369
|
+
mouse_click: {
|
|
16370
|
+
summary: "one mouse button event at a position, carrying the buttons held once it has happened. A drag is a press held with pressed true, motions, and a release with pressed false",
|
|
16371
|
+
requires: ["x", "y"]
|
|
16372
|
+
},
|
|
16373
|
+
mouse_motion: {
|
|
16374
|
+
summary: "move the mouse to a position, carrying the distance from where it last was unless relativeX and relativeY say otherwise, and the buttons held, so a control that drags moves under a run of these between a held mouse_click and its release",
|
|
16375
|
+
requires: ["x", "y"]
|
|
16376
|
+
}
|
|
16356
16377
|
}
|
|
16357
16378
|
},
|
|
16358
16379
|
{
|
|
@@ -42068,7 +42089,7 @@ that is running, and the project on disk. ${TOOL_SPECS.length} tools, named \`do
|
|
|
42068
42089
|
| 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 |
|
|
42069
42090
|
| Fill in a field | \`runtime_input click\` on it, then \`runtime_input text\` with \`replace\`; a submitted field has to be clicked again |
|
|
42070
42091
|
| Click somebody standing in a 3D room | \`runtime_input click\` on the Node3D, which aims at what it draws |
|
|
42071
|
-
| Pick something out of a dropdown | \`runtime_input choose\` on it, by what the item says |
|
|
42092
|
+
| 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 |
|
|
42072
42093
|
| 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 |
|
|
42073
42094
|
| Press a bound key | \`runtime_input action\` or \`key\`, a whole press unless you hold it |
|
|
42074
42095
|
| Where a 3D node is on screen | \`runtime_inspect\` \`rect\`, rather than unprojecting by hand |
|
|
@@ -341,15 +341,29 @@ func inject_mouse_click(params: Dictionary) -> Dictionary:
|
|
|
341
341
|
}
|
|
342
342
|
|
|
343
343
|
|
|
344
|
+
## Moves the pointer to a position, with the movement the event carries taken from where the
|
|
345
|
+
## pointer last was unless the caller says otherwise.
|
|
346
|
+
##
|
|
347
|
+
## A real pointer never arrives without a `relative`, and a control that drags reads that field
|
|
348
|
+
## rather than the position, so a motion carrying none leaves a grip where it was however far the
|
|
349
|
+
## position moved. The viewport remembers where the last motion put the pointer, injected or not,
|
|
350
|
+
## so the difference from there is what a real move would have carried. A relative the caller gives
|
|
351
|
+
## is kept, since a game may want to read a motion the position does not show.
|
|
344
352
|
func inject_mouse_motion(params: Dictionary) -> Dictionary:
|
|
345
353
|
var point: Variant = _read_point(params, "x", "y", "position")
|
|
346
354
|
if point is String:
|
|
347
355
|
return {"type": "error", "message": point}
|
|
348
356
|
var position: Vector2 = point
|
|
349
|
-
var
|
|
350
|
-
if
|
|
351
|
-
|
|
352
|
-
|
|
357
|
+
var relative: Vector2 = position - _host.get_viewport().get_mouse_position()
|
|
358
|
+
if params.has("relativeX") or params.has("relativeY"):
|
|
359
|
+
relative = Vector2(
|
|
360
|
+
Read.as_float(params.get("relativeX", 0.0)), Read.as_float(params.get("relativeY", 0.0))
|
|
361
|
+
)
|
|
362
|
+
elif params.has("relative"):
|
|
363
|
+
var movement: Variant = _read_point(params, "relativeX", "relativeY", "relative")
|
|
364
|
+
if movement is String:
|
|
365
|
+
return {"type": "error", "message": movement}
|
|
366
|
+
relative = movement
|
|
353
367
|
|
|
354
368
|
Input.parse_input_event(_motion(position, relative))
|
|
355
369
|
|
|
@@ -790,14 +804,21 @@ func _click_in_the_world(node_path: String, item: Node3D, params: Dictionary) ->
|
|
|
790
804
|
}
|
|
791
805
|
|
|
792
806
|
|
|
807
|
+
## The buttons a real pointer event carries are the ones held as it happens, and Input keeps that
|
|
808
|
+
## from every button event it is given, injected ones included, so a motion between a held click
|
|
809
|
+
## and its release is a drag to a control that reads the mask rather than remembering the click.
|
|
793
810
|
func _motion(position: Vector2, relative: Vector2) -> InputEventMouseMotion:
|
|
794
811
|
var event: InputEventMouseMotion = InputEventMouseMotion.new()
|
|
795
812
|
event.position = position
|
|
796
813
|
event.global_position = position
|
|
797
814
|
event.relative = relative
|
|
815
|
+
event.screen_relative = relative
|
|
816
|
+
event.button_mask = Input.get_mouse_button_mask()
|
|
798
817
|
return event
|
|
799
818
|
|
|
800
819
|
|
|
820
|
+
## A button event carries the buttons held once it has happened: the pressed one among them, the
|
|
821
|
+
## released one out. A wheel step is a button with no mask, as it is from a real mouse.
|
|
801
822
|
func _button(position: Vector2, button: int, pressed: bool, double: bool) -> InputEventMouseButton:
|
|
802
823
|
var event: InputEventMouseButton = InputEventMouseButton.new()
|
|
803
824
|
event.position = position
|
|
@@ -805,9 +826,20 @@ func _button(position: Vector2, button: int, pressed: bool, double: bool) -> Inp
|
|
|
805
826
|
event.button_index = button as MouseButton
|
|
806
827
|
event.pressed = pressed
|
|
807
828
|
event.double_click = double
|
|
829
|
+
var held: int = Input.get_mouse_button_mask()
|
|
830
|
+
var own: int = _mask_of(button)
|
|
831
|
+
event.button_mask = (held | own) if pressed else (held & ~own)
|
|
808
832
|
return event
|
|
809
833
|
|
|
810
834
|
|
|
835
|
+
## The mask bit for a button, which is the engine's own mapping: left 1, right 2, middle 4, the
|
|
836
|
+
## two extra buttons 128 and 256. The wheel steps between have none.
|
|
837
|
+
func _mask_of(button: int) -> int:
|
|
838
|
+
if button >= MOUSE_BUTTON_WHEEL_UP and button <= MOUSE_BUTTON_WHEEL_RIGHT:
|
|
839
|
+
return 0
|
|
840
|
+
return 1 << (button - 1)
|
|
841
|
+
|
|
842
|
+
|
|
811
843
|
## What to say about a name that is not a mouse button, with the ones that are.
|
|
812
844
|
func _no_such_button(named: Variant) -> Dictionary:
|
|
813
845
|
return {
|
|
@@ -247,11 +247,27 @@ func _matches_apart_from_name(node: Node, wanted: Dictionary[String, String]) ->
|
|
|
247
247
|
if not script is Script:
|
|
248
248
|
return false
|
|
249
249
|
var attached: Script = script
|
|
250
|
-
if attached
|
|
250
|
+
if not _script_is(attached, wanted["class"]):
|
|
251
251
|
return false
|
|
252
252
|
return true
|
|
253
253
|
|
|
254
254
|
|
|
255
|
+
## Whether [param attached] is the script class [param wanted], the way a typed `is` reads it: its
|
|
256
|
+
## own class_name, or one it extends at any distance.
|
|
257
|
+
##
|
|
258
|
+
## The class_name alone answered 0 for `Card` over a tree of rows whose scripts extend Card two
|
|
259
|
+
## steps down, IntakeRow extends DocketCard extends Card, while `is_class` reaches every native
|
|
260
|
+
## subclass. A caller naming a class means what extends it, whichever side of the line the class
|
|
261
|
+
## is declared on.
|
|
262
|
+
static func _script_is(attached: Script, wanted: String) -> bool:
|
|
263
|
+
var walk: Script = attached
|
|
264
|
+
while walk != null:
|
|
265
|
+
if walk.get_global_name() == wanted:
|
|
266
|
+
return true
|
|
267
|
+
walk = walk.get_base_script()
|
|
268
|
+
return false
|
|
269
|
+
|
|
270
|
+
|
|
255
271
|
## Where a node is on screen: a Control's rectangle, a Node2D's position, or the place a 3D node
|
|
256
272
|
## is drawn in, in both the canvas coordinates the node reports and the window pixels input
|
|
257
273
|
## arrives in. The two differ whenever the project stretches its viewport, which is what made a
|
package/build/index.js
CHANGED
|
@@ -29942,12 +29942,12 @@ var TOOL_SPECS = [
|
|
|
29942
29942
|
relativeX: {
|
|
29943
29943
|
type: "number",
|
|
29944
29944
|
ops: ["mouse_motion"],
|
|
29945
|
-
description: "mouse_motion: movement
|
|
29945
|
+
description: "mouse_motion: the movement the event carries. Left out, it is the distance from where the pointer last was, which is what a control that drags reads; give it to send a motion the position does not show."
|
|
29946
29946
|
},
|
|
29947
29947
|
relativeY: {
|
|
29948
29948
|
type: "number",
|
|
29949
29949
|
ops: ["mouse_motion"],
|
|
29950
|
-
description: "mouse_motion:
|
|
29950
|
+
description: "mouse_motion: as relativeX."
|
|
29951
29951
|
}
|
|
29952
29952
|
},
|
|
29953
29953
|
requires: [],
|
|
@@ -29961,13 +29961,22 @@ var TOOL_SPECS = [
|
|
|
29961
29961
|
requires: ["nodePath"]
|
|
29962
29962
|
},
|
|
29963
29963
|
action: { summary: "press an action, or hold it", requires: ["action"] },
|
|
29964
|
-
key: {
|
|
29964
|
+
key: {
|
|
29965
|
+
summary: "press a key, or hold it. Not for a menu a click has opened: an OptionButton or PopupMenu pops up as a window of its own that a key sent to the game never reaches, so an arrow moves nothing in it and Enter closes it with nothing chosen. choose is what selects there",
|
|
29966
|
+
requires: ["keycode"]
|
|
29967
|
+
},
|
|
29965
29968
|
text: {
|
|
29966
29969
|
summary: "type a string into the field being edited, a character at a time, and say what it landed in",
|
|
29967
29970
|
requires: ["text"]
|
|
29968
29971
|
},
|
|
29969
|
-
mouse_click: {
|
|
29970
|
-
|
|
29972
|
+
mouse_click: {
|
|
29973
|
+
summary: "one mouse button event at a position, carrying the buttons held once it has happened. A drag is a press held with pressed true, motions, and a release with pressed false",
|
|
29974
|
+
requires: ["x", "y"]
|
|
29975
|
+
},
|
|
29976
|
+
mouse_motion: {
|
|
29977
|
+
summary: "move the mouse to a position, carrying the distance from where it last was unless relativeX and relativeY say otherwise, and the buttons held, so a control that drags moves under a run of these between a held mouse_click and its release",
|
|
29978
|
+
requires: ["x", "y"]
|
|
29979
|
+
}
|
|
29971
29980
|
}
|
|
29972
29981
|
},
|
|
29973
29982
|
{
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "gdharness",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "1.0.1",
|
|
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",
|