gdharness 0.9.0 → 0.9.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/build/cli.js
CHANGED
|
@@ -15066,7 +15066,7 @@ function buildToolDefinitions() {
|
|
|
15066
15066
|
};
|
|
15067
15067
|
}
|
|
15068
15068
|
for (const [name, schema] of Object.entries(spec.parameters)) {
|
|
15069
|
-
const { ops: _ops, ...carried } = schema;
|
|
15069
|
+
const { ops: _ops, blank: _blank, ...carried } = schema;
|
|
15070
15070
|
properties[name] = carried;
|
|
15071
15071
|
}
|
|
15072
15072
|
const required = [...spec.requires];
|
|
@@ -15258,7 +15258,8 @@ var init_tool_definitions = __esm(() => {
|
|
|
15258
15258
|
projectPath: PROJECT_PATH,
|
|
15259
15259
|
setting: { type: "string", description: 'Setting path, such as "display/window/size/viewport_width".' },
|
|
15260
15260
|
value: {
|
|
15261
|
-
|
|
15261
|
+
blank: true,
|
|
15262
|
+
description: 'The value to write. Engine types may be tagged, {"_type": "Vector2", "x": 1, "y": 2}. "" writes an empty string.'
|
|
15262
15263
|
},
|
|
15263
15264
|
name: { type: "string", description: "Autoload name." },
|
|
15264
15265
|
path: { type: "string", description: "Autoload script or scene inside the project." },
|
|
@@ -15763,7 +15764,7 @@ var init_tool_definitions = __esm(() => {
|
|
|
15763
15764
|
property: {
|
|
15764
15765
|
type: "string",
|
|
15765
15766
|
ops: ["property", "find"],
|
|
15766
|
-
description:
|
|
15767
|
+
description: 'property: which one to read. find: read this one off every node matched, so a panel of labels is one call rather than one per label. Colons read through the objects a node holds, "_game:clock:speed", which is where a game keeps what is worth asking about; a step that is not there is named.'
|
|
15767
15768
|
},
|
|
15768
15769
|
depth: { type: "number", ops: ["tree"], description: "tree: levels to descend. Default 3." },
|
|
15769
15770
|
includeProperties: {
|
|
@@ -15780,7 +15781,7 @@ var init_tool_definitions = __esm(() => {
|
|
|
15780
15781
|
namePattern: {
|
|
15781
15782
|
type: "string",
|
|
15782
15783
|
ops: ["find"],
|
|
15783
|
-
description: 'find: a case-insensitive glob on the node name, such as "Enemy*".'
|
|
15784
|
+
description: 'find: a case-insensitive glob on the node name, such as "Enemy*". Matched against the whole name, so a bare word finds only a node called exactly that; an answer of none says how many names contain it and what glob would have found them.'
|
|
15784
15785
|
},
|
|
15785
15786
|
group: { type: "string", ops: ["find"], description: "find: a group the node is in." },
|
|
15786
15787
|
says: {
|
|
@@ -15821,7 +15822,7 @@ var init_tool_definitions = __esm(() => {
|
|
|
15821
15822
|
requires: ["nodePath"]
|
|
15822
15823
|
},
|
|
15823
15824
|
property: {
|
|
15824
|
-
summary: "what one property reads on a node, refusing a
|
|
15825
|
+
summary: "what one property reads on a node, or through the objects it holds, refusing a name nothing along the way has",
|
|
15825
15826
|
requires: ["nodePath", "property"]
|
|
15826
15827
|
},
|
|
15827
15828
|
metrics: { summary: "frame time, memory, draw calls and the rest", requires: [] }
|
|
@@ -15834,8 +15835,16 @@ var init_tool_definitions = __esm(() => {
|
|
|
15834
15835
|
parameters: {
|
|
15835
15836
|
projectPath: RUNNING_PROJECT_PATH,
|
|
15836
15837
|
nodePath: { type: "string", description: 'Absolute node path, such as "/root/Main/Player".' },
|
|
15837
|
-
property: {
|
|
15838
|
-
|
|
15838
|
+
property: {
|
|
15839
|
+
type: "string",
|
|
15840
|
+
ops: ["set"],
|
|
15841
|
+
description: 'set: which one to write. Colons write through the objects a node holds, "_game:run:day", and the answer reads back off the same holder, so a write a typed container refused shows as an unchanged value.'
|
|
15842
|
+
},
|
|
15843
|
+
value: {
|
|
15844
|
+
ops: ["set"],
|
|
15845
|
+
blank: true,
|
|
15846
|
+
description: `set: the value, fitted to the property's type. "" writes an empty string.`
|
|
15847
|
+
},
|
|
15839
15848
|
method: { type: "string", ops: ["call"] },
|
|
15840
15849
|
args: {
|
|
15841
15850
|
type: "array",
|
|
@@ -15845,7 +15854,10 @@ var init_tool_definitions = __esm(() => {
|
|
|
15845
15854
|
},
|
|
15846
15855
|
requires: ["nodePath"],
|
|
15847
15856
|
operations: {
|
|
15848
|
-
set: {
|
|
15857
|
+
set: {
|
|
15858
|
+
summary: "set a property, on a node or on an object it holds",
|
|
15859
|
+
requires: ["property", "value"]
|
|
15860
|
+
},
|
|
15849
15861
|
call: { summary: "call a method and return its result", requires: ["method"] }
|
|
15850
15862
|
}
|
|
15851
15863
|
},
|
|
@@ -15898,7 +15910,8 @@ var init_tool_definitions = __esm(() => {
|
|
|
15898
15910
|
text: {
|
|
15899
15911
|
type: "string",
|
|
15900
15912
|
ops: ["text", "choose"],
|
|
15901
|
-
|
|
15913
|
+
blank: true,
|
|
15914
|
+
description: 'text: what to type. A newline is Enter and a tab is Tab, a space is a space, and "" with replace empties the field. choose: the item to take, by what it says.'
|
|
15902
15915
|
},
|
|
15903
15916
|
replace: {
|
|
15904
15917
|
type: "boolean",
|
|
@@ -36360,7 +36373,11 @@ class GodotServer {
|
|
|
36360
36373
|
const required = [...spec.requires, ...op !== null ? spec.operations?.[op]?.requires ?? [] : []];
|
|
36361
36374
|
const missing = required.filter((field) => {
|
|
36362
36375
|
const value = Object.hasOwn(args, field) ? args[field] : undefined;
|
|
36363
|
-
|
|
36376
|
+
if (value === undefined || value === null) {
|
|
36377
|
+
return true;
|
|
36378
|
+
}
|
|
36379
|
+
const blank = typeof value === "string" && value.trim() === "";
|
|
36380
|
+
return blank && spec.parameters[field]?.blank !== true;
|
|
36364
36381
|
});
|
|
36365
36382
|
if (missing.length > 0) {
|
|
36366
36383
|
const where = op !== null ? `${spec.name} ${op}` : spec.name;
|
|
@@ -38926,6 +38943,7 @@ that is running, and the project on disk. ${TOOL_SPECS.length} tools, named \`do
|
|
|
38926
38943
|
| Find a control by the word on it | \`runtime_inspect find\` with \`says\`, rather than listing a screen and reading each one |
|
|
38927
38944
|
| Where a control is, and whether it is visible | \`runtime_inspect\` \`find\`, \`rect\` |
|
|
38928
38945
|
| What a property reads right now | \`runtime_inspect\` \`property\`, \`runtime_invoke\` |
|
|
38946
|
+
| What a property on an object a node holds reads | the same two, with colons: \`_game:clock:speed\` |
|
|
38929
38947
|
| Press a button | \`runtime_input click\`, which says what was under the pointer |
|
|
38930
38948
|
| Fill in a field | \`runtime_input click\` on it, then \`runtime_input text\` with \`replace\`; a submitted field has to be clicked again |
|
|
38931
38949
|
| Click somebody standing in a 3D room | \`runtime_input click\` on the Node3D, which aims at what it draws |
|
|
@@ -162,8 +162,9 @@ func inject_key(params: Dictionary) -> Dictionary:
|
|
|
162
162
|
## back now rather than echoed from the request, so a call that typed somewhere unhelpful says so.
|
|
163
163
|
func inject_text(params: Dictionary) -> Dictionary:
|
|
164
164
|
var text: String = String(params.get("text", ""))
|
|
165
|
-
|
|
166
|
-
|
|
165
|
+
var over: bool = bool(params.get("replace", false))
|
|
166
|
+
if text.is_empty() and not over:
|
|
167
|
+
return {"type": "error", "message": "text needs something to type, or replace to empty a field"}
|
|
167
168
|
|
|
168
169
|
var viewport: Viewport = _host.get_tree().root
|
|
169
170
|
var focused: Control = viewport.gui_get_focus_owner()
|
|
@@ -171,14 +172,13 @@ func inject_text(params: Dictionary) -> Dictionary:
|
|
|
171
172
|
if not shut.is_empty():
|
|
172
173
|
return {"type": "error", "message": shut}
|
|
173
174
|
|
|
174
|
-
var replaced: bool =
|
|
175
|
+
var replaced: bool = over and _select_everything(focused)
|
|
176
|
+
# Emptying a field is a real thing to ask for and the one shape of filling one in that types no
|
|
177
|
+
# characters. The selection is standing, so the key a player presses over one is what clears it.
|
|
178
|
+
if replaced and text.is_empty():
|
|
179
|
+
_press(viewport, _held_down(KEY_DELETE))
|
|
175
180
|
for index: int in text.length():
|
|
176
|
-
|
|
177
|
-
viewport.push_input(down)
|
|
178
|
-
# The release as well, so nothing is left held down behind the caller.
|
|
179
|
-
var up: InputEventKey = down.duplicate()
|
|
180
|
-
up.pressed = false
|
|
181
|
-
viewport.push_input(up)
|
|
181
|
+
_press(viewport, _typed(text.unicode_at(index)))
|
|
182
182
|
|
|
183
183
|
# Where it went, which is the one thing a caller cannot see from here. Null is a game reading
|
|
184
184
|
# keys for itself with nothing focused, which is a real thing to be typing at.
|
|
@@ -253,6 +253,24 @@ static func _shut_to_typing(focused: Control) -> String:
|
|
|
253
253
|
|
|
254
254
|
|
|
255
255
|
## The key press that produces [param glyph], as a keyboard would send it.
|
|
256
|
+
## A whole press: down, then the release, so nothing is left held down behind the caller.
|
|
257
|
+
static func _press(viewport: Viewport, down: InputEventKey) -> void:
|
|
258
|
+
viewport.push_input(down)
|
|
259
|
+
var up: InputEventKey = down.duplicate()
|
|
260
|
+
up.pressed = false
|
|
261
|
+
viewport.push_input(up)
|
|
262
|
+
|
|
263
|
+
|
|
264
|
+
## A key held down by its keycode, for the ones that stand for an edit rather than a character.
|
|
265
|
+
static func _held_down(code: Key) -> InputEventKey:
|
|
266
|
+
var event: InputEventKey = InputEventKey.new()
|
|
267
|
+
event.pressed = true
|
|
268
|
+
event.keycode = code
|
|
269
|
+
event.physical_keycode = code
|
|
270
|
+
event.key_label = code
|
|
271
|
+
return event
|
|
272
|
+
|
|
273
|
+
|
|
256
274
|
func _typed(glyph: int) -> InputEventKey:
|
|
257
275
|
var event: InputEventKey = InputEventKey.new()
|
|
258
276
|
event.pressed = true
|
|
@@ -71,13 +71,20 @@ func find_nodes(params: Dictionary) -> Dictionary:
|
|
|
71
71
|
var found: Array[Dictionary] = []
|
|
72
72
|
var pending: Array[Node] = [root]
|
|
73
73
|
var truncated: bool = false
|
|
74
|
+
# Counted while the tree is already being walked, for the answer below: how many nodes this
|
|
75
|
+
# find would have matched if the name had been read the way it was probably meant.
|
|
76
|
+
var literal: bool = _is_literal(wanted["name"])
|
|
77
|
+
var nearly: int = 0
|
|
74
78
|
while not pending.is_empty():
|
|
75
79
|
var node: Node = pending.pop_front()
|
|
76
|
-
|
|
80
|
+
var rest: bool = _matches_apart_from_name(node, wanted)
|
|
81
|
+
if rest and _named(node, wanted["name"]):
|
|
77
82
|
if found.size() >= limit:
|
|
78
83
|
truncated = true
|
|
79
84
|
break
|
|
80
85
|
found.append(_found(node, wanted_property))
|
|
86
|
+
elif rest and literal and str(node.name).containsn(wanted["name"]):
|
|
87
|
+
nearly += 1
|
|
81
88
|
# Internal children included, which they were not. A ConfirmationDialog builds its Yes and
|
|
82
89
|
# its No as internal nodes, and a ScrollContainer its bars, so a find over a screen for
|
|
83
90
|
# every Button came back without the two buttons the player is being asked to press:
|
|
@@ -88,7 +95,17 @@ func find_nodes(params: Dictionary) -> Dictionary:
|
|
|
88
95
|
for index: int in range(children.size() - 1, -1, -1):
|
|
89
96
|
pending.push_front(children[index])
|
|
90
97
|
|
|
91
|
-
|
|
98
|
+
var answer: Dictionary = {"type": "nodes", "count": found.size(), "truncated": truncated, "nodes": found}
|
|
99
|
+
# Nothing found is the one answer that cannot be told apart from having asked the wrong
|
|
100
|
+
# question, and a name written without a wildcard is the way an agent writes "contains".
|
|
101
|
+
# Said only when it changes the answer, so a genuine nothing stays a plain nothing.
|
|
102
|
+
if found.is_empty() and nearly > 0:
|
|
103
|
+
var holding: String = "names contain" if nearly > 1 else "name contains"
|
|
104
|
+
answer["note"] = (
|
|
105
|
+
'name is matched as a glob against the whole name; %d node %s "%s", which "*%s*" would find'
|
|
106
|
+
% [nearly, holding, wanted["name"], wanted["name"]]
|
|
107
|
+
)
|
|
108
|
+
return answer
|
|
92
109
|
|
|
93
110
|
|
|
94
111
|
## One match, with the named property on it when one was named.
|
|
@@ -102,15 +119,20 @@ func _found(node: Node, wanted_property: String) -> Dictionary:
|
|
|
102
119
|
var entry: Dictionary = _serialize_node(node, false)
|
|
103
120
|
if wanted_property.is_empty():
|
|
104
121
|
return entry
|
|
105
|
-
var has: bool = false
|
|
106
|
-
for prop: Dictionary in node.get_property_list():
|
|
107
|
-
if str(prop["name"]) == wanted_property:
|
|
108
|
-
has = true
|
|
109
|
-
break
|
|
110
122
|
entry["property"] = wanted_property
|
|
123
|
+
# Through a path as well, for the reason [method _reached] gives. A step that is not there on
|
|
124
|
+
# this node reads as not having the property rather than as a refusal, which is the rule this
|
|
125
|
+
# whole function is written to: a find matches nodes of several classes on purpose.
|
|
126
|
+
var reached: Dictionary = _reached(node, str(entry["path"]), wanted_property)
|
|
127
|
+
if reached.has("message"):
|
|
128
|
+
entry["has_property"] = false
|
|
129
|
+
return entry
|
|
130
|
+
var holder: Object = reached["holder"]
|
|
131
|
+
var named: String = reached["property"]
|
|
132
|
+
var has: bool = _has_property(holder, named)
|
|
111
133
|
entry["has_property"] = has
|
|
112
134
|
if has:
|
|
113
|
-
entry["value"] = _values.serialize(
|
|
135
|
+
entry["value"] = _values.serialize(holder.get(named))
|
|
114
136
|
return entry
|
|
115
137
|
|
|
116
138
|
|
|
@@ -123,11 +145,24 @@ static func _anything_asked(wanted: Dictionary[String, String]) -> bool:
|
|
|
123
145
|
return false
|
|
124
146
|
|
|
125
147
|
|
|
126
|
-
|
|
148
|
+
## Whether [param node] carries the name a find asked for. Empty matches everything, which is what
|
|
149
|
+
## makes leaving a filter out the same as not having one.
|
|
150
|
+
static func _named(node: Node, pattern: String) -> bool:
|
|
151
|
+
return pattern.is_empty() or str(node.name).matchn(pattern)
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
## Whether [param pattern] is a name written as a whole word rather than as a glob, which is how a
|
|
155
|
+
## caller writes one when they mean "contains". [method String.matchn] answers nothing to it, and
|
|
156
|
+
## nothing is also what a name that is simply not there answers.
|
|
157
|
+
static func _is_literal(pattern: String) -> bool:
|
|
158
|
+
return not pattern.is_empty() and not pattern.contains("*") and not pattern.contains("?")
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
## Every filter but the name, so a find that came back empty can say how many nodes the name was
|
|
162
|
+
## the only thing standing between it and.
|
|
163
|
+
func _matches_apart_from_name(node: Node, wanted: Dictionary[String, String]) -> bool:
|
|
127
164
|
if not wanted["group"].is_empty() and not node.is_in_group(wanted["group"]):
|
|
128
165
|
return false
|
|
129
|
-
if not wanted["name"].is_empty() and not str(node.name).matchn(wanted["name"]):
|
|
130
|
-
return false
|
|
131
166
|
# What this node says, rather than everything said underneath it. A row is then found by the
|
|
132
167
|
# label in it, and the path answered is that label's, which is where the words a caller is
|
|
133
168
|
# looking at actually are: matching every container above it would answer with the screen.
|
|
@@ -395,24 +430,63 @@ func get_property(params: Dictionary) -> Dictionary:
|
|
|
395
430
|
if node == null:
|
|
396
431
|
return {"type": "error", "message": "Node not found: " + node_path}
|
|
397
432
|
|
|
433
|
+
var reached: Dictionary = _reached(node, node_path, property)
|
|
434
|
+
if reached.has("message"):
|
|
435
|
+
return reached
|
|
436
|
+
|
|
437
|
+
var holder: Object = reached["holder"]
|
|
438
|
+
var named: String = reached["property"]
|
|
398
439
|
# Asked of the property list rather than read and compared to null, because a property the
|
|
399
|
-
#
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
if str(entry["name"]) == property:
|
|
403
|
-
known = true
|
|
404
|
-
break
|
|
405
|
-
if not known:
|
|
406
|
-
return {"type": "error", "message": "%s has no property %s" % [node_path, property]}
|
|
440
|
+
# holder does not have and a property that is null both read as null.
|
|
441
|
+
if not _has_property(holder, named):
|
|
442
|
+
return {"type": "error", "message": "%s has no property %s" % [reached["called"], named]}
|
|
407
443
|
|
|
408
444
|
return {
|
|
409
445
|
"type": "property",
|
|
410
446
|
"path": node_path,
|
|
411
447
|
"property": property,
|
|
412
|
-
"value": _values.serialize(
|
|
448
|
+
"value": _values.serialize(holder.get(named)),
|
|
413
449
|
}
|
|
414
450
|
|
|
415
451
|
|
|
452
|
+
## What [param property] is a property of, which is the node itself until the name has a colon in
|
|
453
|
+
## it, and the last name along that path.
|
|
454
|
+
##
|
|
455
|
+
## A game's state does not sit on nodes, it hangs off them: the speed of the clock is a property of
|
|
456
|
+
## a [RefCounted] held by a [RefCounted] held by the root, and that is what an agent asks about.
|
|
457
|
+
## Colons, because that is the separator [method Object.get_indexed] already takes. Walked a step
|
|
458
|
+
## at a time rather than handed to that method, which answers null for a path that goes wrong
|
|
459
|
+
## halfway along and for one that ends on null.
|
|
460
|
+
##
|
|
461
|
+
## Answers with a `message` instead when a step along the way is not there or holds something that
|
|
462
|
+
## is not an object, naming the step rather than the whole path: "/root/Main:_game has no property
|
|
463
|
+
## clocks" is a typo found, and "no property _game:clocks:speed" is a puzzle.
|
|
464
|
+
func _reached(node: Node, node_path: String, property: String) -> Dictionary:
|
|
465
|
+
var parts: PackedStringArray = property.split(":")
|
|
466
|
+
var holder: Object = node
|
|
467
|
+
var called: String = node_path
|
|
468
|
+
for step: int in parts.size() - 1:
|
|
469
|
+
var named: String = parts[step]
|
|
470
|
+
if not _has_property(holder, named):
|
|
471
|
+
return {"type": "error", "message": "%s has no property %s" % [called, named]}
|
|
472
|
+
var next: Variant = holder.get(named)
|
|
473
|
+
called = "%s:%s" % [called, named]
|
|
474
|
+
if not next is Object:
|
|
475
|
+
var wanted: String = parts[step + 1]
|
|
476
|
+
return {"type": "error", "message": "%s holds no object to read %s off" % [called, wanted]}
|
|
477
|
+
holder = next
|
|
478
|
+
return {"holder": holder, "property": parts[parts.size() - 1], "called": called}
|
|
479
|
+
|
|
480
|
+
|
|
481
|
+
## Whether [param holder] declares [param named]. Its own function because three ops ask it and a
|
|
482
|
+
## property read back as null answers it wrongly.
|
|
483
|
+
static func _has_property(holder: Object, named: String) -> bool:
|
|
484
|
+
for entry: Dictionary in holder.get_property_list():
|
|
485
|
+
if str(entry["name"]) == named:
|
|
486
|
+
return true
|
|
487
|
+
return false
|
|
488
|
+
|
|
489
|
+
|
|
416
490
|
func set_property(params: Dictionary) -> Dictionary:
|
|
417
491
|
var node_path: String = str(params.get("path", ""))
|
|
418
492
|
var property: String = str(params.get("property", ""))
|
|
@@ -425,15 +499,24 @@ func set_property(params: Dictionary) -> Dictionary:
|
|
|
425
499
|
if node == null:
|
|
426
500
|
return {"type": "error", "message": "Node not found: " + node_path}
|
|
427
501
|
|
|
428
|
-
|
|
429
|
-
|
|
502
|
+
# Through a path as well, for the reason [method _reached] gives, and read back off the same
|
|
503
|
+
# holder afterwards: a set that does not take says so by answering with the old value, which is
|
|
504
|
+
# how a typed container refusing a write is told from one accepting it.
|
|
505
|
+
var reached: Dictionary = _reached(node, node_path, property)
|
|
506
|
+
if reached.has("message"):
|
|
507
|
+
return reached
|
|
508
|
+
|
|
509
|
+
var holder: Object = reached["holder"]
|
|
510
|
+
var named: String = reached["property"]
|
|
511
|
+
var old_value: Variant = holder.get(named)
|
|
512
|
+
holder.set(named, _values.fitted(value, typeof(old_value)))
|
|
430
513
|
|
|
431
514
|
return {
|
|
432
515
|
"type": "property_set",
|
|
433
516
|
"path": node_path,
|
|
434
517
|
"property": property,
|
|
435
518
|
"old_value": _values.serialize(old_value),
|
|
436
|
-
"new_value": _values.serialize(
|
|
519
|
+
"new_value": _values.serialize(holder.get(named))
|
|
437
520
|
}
|
|
438
521
|
|
|
439
522
|
|
package/build/index.js
CHANGED
|
@@ -27811,7 +27811,8 @@ var TOOL_SPECS = [
|
|
|
27811
27811
|
projectPath: PROJECT_PATH,
|
|
27812
27812
|
setting: { type: "string", description: 'Setting path, such as "display/window/size/viewport_width".' },
|
|
27813
27813
|
value: {
|
|
27814
|
-
|
|
27814
|
+
blank: true,
|
|
27815
|
+
description: 'The value to write. Engine types may be tagged, {"_type": "Vector2", "x": 1, "y": 2}. "" writes an empty string.'
|
|
27815
27816
|
},
|
|
27816
27817
|
name: { type: "string", description: "Autoload name." },
|
|
27817
27818
|
path: { type: "string", description: "Autoload script or scene inside the project." },
|
|
@@ -28316,7 +28317,7 @@ var TOOL_SPECS = [
|
|
|
28316
28317
|
property: {
|
|
28317
28318
|
type: "string",
|
|
28318
28319
|
ops: ["property", "find"],
|
|
28319
|
-
description:
|
|
28320
|
+
description: 'property: which one to read. find: read this one off every node matched, so a panel of labels is one call rather than one per label. Colons read through the objects a node holds, "_game:clock:speed", which is where a game keeps what is worth asking about; a step that is not there is named.'
|
|
28320
28321
|
},
|
|
28321
28322
|
depth: { type: "number", ops: ["tree"], description: "tree: levels to descend. Default 3." },
|
|
28322
28323
|
includeProperties: {
|
|
@@ -28333,7 +28334,7 @@ var TOOL_SPECS = [
|
|
|
28333
28334
|
namePattern: {
|
|
28334
28335
|
type: "string",
|
|
28335
28336
|
ops: ["find"],
|
|
28336
|
-
description: 'find: a case-insensitive glob on the node name, such as "Enemy*".'
|
|
28337
|
+
description: 'find: a case-insensitive glob on the node name, such as "Enemy*". Matched against the whole name, so a bare word finds only a node called exactly that; an answer of none says how many names contain it and what glob would have found them.'
|
|
28337
28338
|
},
|
|
28338
28339
|
group: { type: "string", ops: ["find"], description: "find: a group the node is in." },
|
|
28339
28340
|
says: {
|
|
@@ -28374,7 +28375,7 @@ var TOOL_SPECS = [
|
|
|
28374
28375
|
requires: ["nodePath"]
|
|
28375
28376
|
},
|
|
28376
28377
|
property: {
|
|
28377
|
-
summary: "what one property reads on a node, refusing a
|
|
28378
|
+
summary: "what one property reads on a node, or through the objects it holds, refusing a name nothing along the way has",
|
|
28378
28379
|
requires: ["nodePath", "property"]
|
|
28379
28380
|
},
|
|
28380
28381
|
metrics: { summary: "frame time, memory, draw calls and the rest", requires: [] }
|
|
@@ -28387,8 +28388,16 @@ var TOOL_SPECS = [
|
|
|
28387
28388
|
parameters: {
|
|
28388
28389
|
projectPath: RUNNING_PROJECT_PATH,
|
|
28389
28390
|
nodePath: { type: "string", description: 'Absolute node path, such as "/root/Main/Player".' },
|
|
28390
|
-
property: {
|
|
28391
|
-
|
|
28391
|
+
property: {
|
|
28392
|
+
type: "string",
|
|
28393
|
+
ops: ["set"],
|
|
28394
|
+
description: 'set: which one to write. Colons write through the objects a node holds, "_game:run:day", and the answer reads back off the same holder, so a write a typed container refused shows as an unchanged value.'
|
|
28395
|
+
},
|
|
28396
|
+
value: {
|
|
28397
|
+
ops: ["set"],
|
|
28398
|
+
blank: true,
|
|
28399
|
+
description: `set: the value, fitted to the property's type. "" writes an empty string.`
|
|
28400
|
+
},
|
|
28392
28401
|
method: { type: "string", ops: ["call"] },
|
|
28393
28402
|
args: {
|
|
28394
28403
|
type: "array",
|
|
@@ -28398,7 +28407,10 @@ var TOOL_SPECS = [
|
|
|
28398
28407
|
},
|
|
28399
28408
|
requires: ["nodePath"],
|
|
28400
28409
|
operations: {
|
|
28401
|
-
set: {
|
|
28410
|
+
set: {
|
|
28411
|
+
summary: "set a property, on a node or on an object it holds",
|
|
28412
|
+
requires: ["property", "value"]
|
|
28413
|
+
},
|
|
28402
28414
|
call: { summary: "call a method and return its result", requires: ["method"] }
|
|
28403
28415
|
}
|
|
28404
28416
|
},
|
|
@@ -28451,7 +28463,8 @@ var TOOL_SPECS = [
|
|
|
28451
28463
|
text: {
|
|
28452
28464
|
type: "string",
|
|
28453
28465
|
ops: ["text", "choose"],
|
|
28454
|
-
|
|
28466
|
+
blank: true,
|
|
28467
|
+
description: 'text: what to type. A newline is Enter and a tab is Tab, a space is a space, and "" with replace empties the field. choose: the item to take, by what it says.'
|
|
28455
28468
|
},
|
|
28456
28469
|
replace: {
|
|
28457
28470
|
type: "boolean",
|
|
@@ -28625,7 +28638,7 @@ function buildToolDefinitions() {
|
|
|
28625
28638
|
};
|
|
28626
28639
|
}
|
|
28627
28640
|
for (const [name, schema] of Object.entries(spec.parameters)) {
|
|
28628
|
-
const { ops: _ops, ...carried } = schema;
|
|
28641
|
+
const { ops: _ops, blank: _blank, ...carried } = schema;
|
|
28629
28642
|
properties[name] = carried;
|
|
28630
28643
|
}
|
|
28631
28644
|
const required = [...spec.requires];
|
|
@@ -29141,7 +29154,11 @@ class GodotServer {
|
|
|
29141
29154
|
const required = [...spec.requires, ...op !== null ? spec.operations?.[op]?.requires ?? [] : []];
|
|
29142
29155
|
const missing = required.filter((field) => {
|
|
29143
29156
|
const value = Object.hasOwn(args, field) ? args[field] : undefined;
|
|
29144
|
-
|
|
29157
|
+
if (value === undefined || value === null) {
|
|
29158
|
+
return true;
|
|
29159
|
+
}
|
|
29160
|
+
const blank = typeof value === "string" && value.trim() === "";
|
|
29161
|
+
return blank && spec.parameters[field]?.blank !== true;
|
|
29145
29162
|
});
|
|
29146
29163
|
if (missing.length > 0) {
|
|
29147
29164
|
const where = op !== null ? `${spec.name} ${op}` : spec.name;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "gdharness",
|
|
3
|
-
"version": "0.9.
|
|
3
|
+
"version": "0.9.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",
|