gdharness 0.7.0 → 0.8.0

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
@@ -15783,6 +15783,11 @@ var init_tool_definitions = __esm(() => {
15783
15783
  description: 'find: a case-insensitive glob on the node name, such as "Enemy*".'
15784
15784
  },
15785
15785
  group: { type: "string", ops: ["find"], description: "find: a group the node is in." },
15786
+ says: {
15787
+ type: "string",
15788
+ ops: ["find"],
15789
+ description: "find: part of what the node has written on it, case-insensitively, which is how a button is reached by the word on it rather than by a generated path. Its own text, so a row is found by the label in it, and hidden nodes match."
15790
+ },
15786
15791
  limit: {
15787
15792
  type: "number",
15788
15793
  ops: ["find", "text"],
@@ -15808,7 +15813,7 @@ var init_tool_definitions = __esm(() => {
15808
15813
  requires: []
15809
15814
  },
15810
15815
  find: {
15811
- summary: "the paths of every node matching className, script, namePattern or group, with property read off each",
15816
+ summary: "the paths of every node matching className, script, namePattern, group or says, with property read off each",
15812
15817
  requires: []
15813
15818
  },
15814
15819
  rect: {
@@ -15874,7 +15879,11 @@ var init_tool_definitions = __esm(() => {
15874
15879
  ops: ["click", "choose"],
15875
15880
  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."
15876
15881
  },
15877
- action: { type: "string", ops: ["action"], description: "action: the InputMap action name." },
15882
+ action: {
15883
+ type: "string",
15884
+ ops: ["action"],
15885
+ 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."
15886
+ },
15878
15887
  pressed: {
15879
15888
  type: "boolean",
15880
15889
  ops: ["action", "key", "mouse_click"],
@@ -37491,6 +37500,7 @@ class GodotServer {
37491
37500
  const script = readNonEmptyString(args, "script");
37492
37501
  const namePattern = readNonEmptyString(args, "namePattern");
37493
37502
  const group = readNonEmptyString(args, "group");
37503
+ const says = readNonEmptyString(args, "says");
37494
37504
  if (className !== undefined)
37495
37505
  filters["class"] = className;
37496
37506
  if (script !== undefined)
@@ -37499,8 +37509,10 @@ class GodotServer {
37499
37509
  filters["name"] = namePattern;
37500
37510
  if (group !== undefined)
37501
37511
  filters["group"] = group;
37512
+ if (says !== undefined)
37513
+ filters["says"] = says;
37502
37514
  if (Object.keys(filters).length === 0) {
37503
- return this.createErrorResponse("runtime_inspect find needs at least one of className, script, namePattern, group.");
37515
+ return this.createErrorResponse("runtime_inspect find needs at least one of className, script, namePattern, group, says.");
37504
37516
  }
37505
37517
  const property = readNonEmptyString(args, "property");
37506
37518
  return await this.handleRuntimeCommand("find_nodes", {
@@ -38905,13 +38917,15 @@ that is running, and the project on disk. ${TOOL_SPECS.length} tools, named \`do
38905
38917
  | Ask | Tool |
38906
38918
  | --- | --- |
38907
38919
  | What a screen says | \`runtime_inspect text\`, which reads what is drawn and leaves out what is hidden |
38920
+ | Find a control by the word on it | \`runtime_inspect find\` with \`says\`, rather than listing a screen and reading each one |
38908
38921
  | Where a control is, and whether it is visible | \`runtime_inspect\` \`find\`, \`rect\` |
38909
38922
  | What a property reads right now | \`runtime_inspect\` \`property\`, \`runtime_invoke\` |
38910
38923
  | Press a button | \`runtime_input click\`, which says what was under the pointer |
38911
38924
  | Fill in a field | \`runtime_input click\` on it, then \`runtime_input text\` with \`replace\`; a submitted field has to be clicked again |
38912
38925
  | Click somebody standing in a 3D room | \`runtime_input click\` on the Node3D, which aims at what it draws |
38913
38926
  | Pick something out of a dropdown | \`runtime_input choose\` on it, by what the item says |
38914
- | Answer a dialog, or press a bound key | \`runtime_input action\` or \`key\`, a whole press unless you hold it |
38927
+ | 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 |
38928
+ | Press a bound key | \`runtime_input action\` or \`key\`, a whole press unless you hold it |
38915
38929
  | Where a 3D node is on screen | \`runtime_inspect\` \`rect\`, rather than unprojecting by hand |
38916
38930
  | Wait for something | \`runtime_wait\`, never a sleep |
38917
38931
  | A picture, for a person who asked to see one | \`runtime_capture\` |
@@ -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
+ ## What a find can be narrowed by, which is also what it refuses to answer without. Named once so
14
+ ## the refusal lists the same set the walk reads.
15
+ const FILTERS: PackedStringArray = ["class", "script", "name", "group", "says"]
16
+
13
17
  ## The most lines one read answers with, unless asked for fewer. A screen is a few dozen; a
14
18
  ## thousand is a tree somebody pointed this at by mistake.
15
19
  const READ_LIMIT: int = 500
@@ -39,7 +43,8 @@ func get_tree(params: Dictionary) -> Dictionary:
39
43
 
40
44
  ## Nodes matching every filter given, as paths, so a caller can name what it wants without
41
45
  ## reading the whole tree to find it. `class` matches native classes and their subclasses, and
42
- ## the global name of a script class; `name` is a case-insensitive glob; `script` is a path.
46
+ ## the global name of a script class; `name` is a case-insensitive glob; `script` is a path;
47
+ ## `says` is what the node has written on it.
43
48
  ##
44
49
  ## `property` names one to read off each of them, which is the difference between one question and
45
50
  ## one call per answer. A panel of a dozen labels took thirteen calls to read, and a tree that
@@ -48,22 +53,16 @@ func get_tree(params: Dictionary) -> Dictionary:
48
53
  ## because null is what a node holding null answers.
49
54
  func find_nodes(params: Dictionary) -> Dictionary:
50
55
  var root_path: String = str(params.get("root", "/root"))
51
- var wanted_class: String = str(params.get("class", ""))
52
- var wanted_script: String = str(params.get("script", ""))
53
- var wanted_name: String = str(params.get("name", ""))
54
- var wanted_group: String = str(params.get("group", ""))
56
+ var wanted: Dictionary[String, String] = {}
57
+ for filter: String in FILTERS:
58
+ wanted[filter] = str(params.get(filter, ""))
55
59
  var wanted_property: String = str(params.get("property", ""))
56
60
  var limit: int = clampi(int(params.get("limit", FIND_LIMIT)), 1, FIND_LIMIT_CEILING)
57
61
 
58
- if (
59
- wanted_class.is_empty()
60
- and wanted_script.is_empty()
61
- and wanted_name.is_empty()
62
- and wanted_group.is_empty()
63
- ):
64
- return {"type": "error", "message": "find_nodes needs at least one of class, script, name, group"}
65
- if not wanted_script.is_empty() and not wanted_script.begins_with("res://"):
66
- wanted_script = "res://" + wanted_script
62
+ if not _anything_asked(wanted):
63
+ return {"type": "error", "message": "find_nodes needs at least one of " + ", ".join(FILTERS)}
64
+ if not wanted["script"].is_empty() and not wanted["script"].begins_with("res://"):
65
+ wanted["script"] = "res://" + wanted["script"]
67
66
 
68
67
  var root: Node = _host.get_tree().root.get_node_or_null(root_path)
69
68
  if root == null:
@@ -74,7 +73,7 @@ func find_nodes(params: Dictionary) -> Dictionary:
74
73
  var truncated: bool = false
75
74
  while not pending.is_empty():
76
75
  var node: Node = pending.pop_front()
77
- if _matches(node, wanted_class, wanted_script, wanted_name, wanted_group):
76
+ if _matches(node, wanted):
78
77
  if found.size() >= limit:
79
78
  truncated = true
80
79
  break
@@ -115,25 +114,37 @@ func _found(node: Node, wanted_property: String) -> Dictionary:
115
114
  return entry
116
115
 
117
116
 
118
- func _matches(
119
- node: Node, wanted_class: String, wanted_script: String, wanted_name: String, wanted_group: String
120
- ) -> bool:
121
- if not wanted_group.is_empty() and not node.is_in_group(wanted_group):
117
+ ## Whether the caller named anything to match on, since every filter left out matches everything
118
+ ## and a find with none of them is the whole tree by another name.
119
+ static func _anything_asked(wanted: Dictionary[String, String]) -> bool:
120
+ for filter: String in wanted:
121
+ if not wanted[filter].is_empty():
122
+ return true
123
+ return false
124
+
125
+
126
+ func _matches(node: Node, wanted: Dictionary[String, String]) -> bool:
127
+ if not wanted["group"].is_empty() and not node.is_in_group(wanted["group"]):
128
+ return false
129
+ if not wanted["name"].is_empty() and not str(node.name).matchn(wanted["name"]):
122
130
  return false
123
- if not wanted_name.is_empty() and not str(node.name).matchn(wanted_name):
131
+ # What this node says, rather than everything said underneath it. A row is then found by the
132
+ # label in it, and the path answered is that label's, which is where the words a caller is
133
+ # looking at actually are: matching every container above it would answer with the screen.
134
+ if not wanted["says"].is_empty() and not _said_by(node).containsn(wanted["says"]):
124
135
  return false
125
136
  var script: Variant = node.get_script()
126
- if not wanted_script.is_empty():
137
+ if not wanted["script"].is_empty():
127
138
  if not script is Script:
128
139
  return false
129
140
  var attached: Script = script
130
- if attached.resource_path != wanted_script:
141
+ if attached.resource_path != wanted["script"]:
131
142
  return false
132
- if not wanted_class.is_empty() and not node.is_class(wanted_class):
143
+ if not wanted["class"].is_empty() and not node.is_class(wanted["class"]):
133
144
  if not script is Script:
134
145
  return false
135
146
  var attached: Script = script
136
- if attached.get_global_name() != wanted_class:
147
+ if attached.get_global_name() != wanted["class"]:
137
148
  return false
138
149
  return true
139
150
 
package/build/index.js CHANGED
@@ -28336,6 +28336,11 @@ var TOOL_SPECS = [
28336
28336
  description: 'find: a case-insensitive glob on the node name, such as "Enemy*".'
28337
28337
  },
28338
28338
  group: { type: "string", ops: ["find"], description: "find: a group the node is in." },
28339
+ says: {
28340
+ type: "string",
28341
+ ops: ["find"],
28342
+ description: "find: part of what the node has written on it, case-insensitively, which is how a button is reached by the word on it rather than by a generated path. Its own text, so a row is found by the label in it, and hidden nodes match."
28343
+ },
28339
28344
  limit: {
28340
28345
  type: "number",
28341
28346
  ops: ["find", "text"],
@@ -28361,7 +28366,7 @@ var TOOL_SPECS = [
28361
28366
  requires: []
28362
28367
  },
28363
28368
  find: {
28364
- summary: "the paths of every node matching className, script, namePattern or group, with property read off each",
28369
+ summary: "the paths of every node matching className, script, namePattern, group or says, with property read off each",
28365
28370
  requires: []
28366
28371
  },
28367
28372
  rect: {
@@ -28427,7 +28432,11 @@ var TOOL_SPECS = [
28427
28432
  ops: ["click", "choose"],
28428
28433
  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."
28429
28434
  },
28430
- action: { type: "string", ops: ["action"], description: "action: the InputMap action name." },
28435
+ action: {
28436
+ type: "string",
28437
+ ops: ["action"],
28438
+ 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."
28439
+ },
28431
28440
  pressed: {
28432
28441
  type: "boolean",
28433
28442
  ops: ["action", "key", "mouse_click"],
@@ -30272,6 +30281,7 @@ class GodotServer {
30272
30281
  const script = readNonEmptyString(args, "script");
30273
30282
  const namePattern = readNonEmptyString(args, "namePattern");
30274
30283
  const group = readNonEmptyString(args, "group");
30284
+ const says = readNonEmptyString(args, "says");
30275
30285
  if (className !== undefined)
30276
30286
  filters["class"] = className;
30277
30287
  if (script !== undefined)
@@ -30280,8 +30290,10 @@ class GodotServer {
30280
30290
  filters["name"] = namePattern;
30281
30291
  if (group !== undefined)
30282
30292
  filters["group"] = group;
30293
+ if (says !== undefined)
30294
+ filters["says"] = says;
30283
30295
  if (Object.keys(filters).length === 0) {
30284
- return this.createErrorResponse("runtime_inspect find needs at least one of className, script, namePattern, group.");
30296
+ return this.createErrorResponse("runtime_inspect find needs at least one of className, script, namePattern, group, says.");
30285
30297
  }
30286
30298
  const property = readNonEmptyString(args, "property");
30287
30299
  return await this.handleRuntimeCommand("find_nodes", {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gdharness",
3
- "version": "0.7.0",
3
+ "version": "0.8.0",
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",