gdharness 0.6.4 → 0.6.5
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:
|
|
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: []
|
|
@@ -35844,6 +35852,9 @@ import { basename as basename2, dirname as dirname9, join as join15, normalize a
|
|
|
35844
35852
|
import { setTimeout as delay2 } from "node:timers/promises";
|
|
35845
35853
|
import { fileURLToPath as fileURLToPath4 } from "node:url";
|
|
35846
35854
|
import { promisify as promisify5 } from "node:util";
|
|
35855
|
+
function patienceForFrames(frames, atLeast) {
|
|
35856
|
+
return Math.max(atLeast, frames / SLOWEST_FRAME_RATE * 1000 + atLeast);
|
|
35857
|
+
}
|
|
35847
35858
|
function escapingPropertyValue(properties) {
|
|
35848
35859
|
if (typeof properties !== "object" || properties === null) {
|
|
35849
35860
|
return;
|
|
@@ -36323,6 +36334,12 @@ class GodotServer {
|
|
|
36323
36334
|
});
|
|
36324
36335
|
case "find":
|
|
36325
36336
|
return await this.handleFindRuntimeNodes(args);
|
|
36337
|
+
case "text":
|
|
36338
|
+
return await this.handleRuntimeCommand("read_text", {
|
|
36339
|
+
projectPath: args["projectPath"],
|
|
36340
|
+
root: readNonEmptyString(args, "nodePath") ?? "/root",
|
|
36341
|
+
include_hidden: readBoolean(args, "includeHidden") ?? false
|
|
36342
|
+
});
|
|
36326
36343
|
case "rect":
|
|
36327
36344
|
return await this.handleRuntimeCommand("get_rect", {
|
|
36328
36345
|
projectPath: args["projectPath"],
|
|
@@ -37367,8 +37384,11 @@ class GodotServer {
|
|
|
37367
37384
|
const nodePath = readNonEmptyString(args, "nodePath") ?? "";
|
|
37368
37385
|
const patience = Math.max(this.runtimeTimeoutMs(), timeoutMs + 5000);
|
|
37369
37386
|
switch (op) {
|
|
37370
|
-
case "frames":
|
|
37371
|
-
|
|
37387
|
+
case "frames": {
|
|
37388
|
+
const frames = readPositiveNumber(args, "frames") ?? 1;
|
|
37389
|
+
const waited = patienceForFrames(frames, patience);
|
|
37390
|
+
return await this.handleRuntimeCommand("wait_frames", { projectPath: args["projectPath"], frames }, waited);
|
|
37391
|
+
}
|
|
37372
37392
|
case "signal":
|
|
37373
37393
|
return await this.handleRuntimeCommand("wait_signal", {
|
|
37374
37394
|
projectPath: args["projectPath"],
|
|
@@ -37391,7 +37411,7 @@ async function runGodotServer() {
|
|
|
37391
37411
|
const server = new GodotServer;
|
|
37392
37412
|
await server.run();
|
|
37393
37413
|
}
|
|
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;
|
|
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;
|
|
37395
37415
|
var init_server3 = __esm(() => {
|
|
37396
37416
|
init_mcp();
|
|
37397
37417
|
init_stdio2();
|
|
@@ -38740,6 +38760,7 @@ that is running, and the project on disk. ${TOOL_SPECS.length} tools, named \`do
|
|
|
38740
38760
|
|
|
38741
38761
|
| Ask | Tool |
|
|
38742
38762
|
| --- | --- |
|
|
38763
|
+
| What a screen says | \`runtime_inspect text\`, which reads what is drawn and leaves out what is hidden |
|
|
38743
38764
|
| Where a control is, and whether it is visible | \`runtime_inspect\` \`find\`, \`rect\` |
|
|
38744
38765
|
| What a property reads right now | \`runtime_inspect\` \`property\`, \`runtime_invoke\` |
|
|
38745
38766
|
| Press a button | \`runtime_input click\`, which says what was under the pointer |
|
|
@@ -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,
|
|
@@ -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:
|
|
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: []
|
|
@@ -28563,6 +28571,10 @@ var FEEDBACK_NOTICE_EVERY = 250;
|
|
|
28563
28571
|
var run3 = promisify3(execFile3);
|
|
28564
28572
|
var __dirname2 = dirname4(fileURLToPath2(import.meta.url));
|
|
28565
28573
|
var EDITOR_RESTART_TIMEOUT_MS = 90000;
|
|
28574
|
+
var SLOWEST_FRAME_RATE = 20;
|
|
28575
|
+
function patienceForFrames(frames, atLeast) {
|
|
28576
|
+
return Math.max(atLeast, frames / SLOWEST_FRAME_RATE * 1000 + atLeast);
|
|
28577
|
+
}
|
|
28566
28578
|
var BRIDGE_RETRY_MS = 2000;
|
|
28567
28579
|
var PATH_SOLUTIONS = [
|
|
28568
28580
|
'Give the path relative to the project, such as "scenes/main.tscn" or "res://scenes/main.tscn"',
|
|
@@ -29096,6 +29108,12 @@ class GodotServer {
|
|
|
29096
29108
|
});
|
|
29097
29109
|
case "find":
|
|
29098
29110
|
return await this.handleFindRuntimeNodes(args);
|
|
29111
|
+
case "text":
|
|
29112
|
+
return await this.handleRuntimeCommand("read_text", {
|
|
29113
|
+
projectPath: args["projectPath"],
|
|
29114
|
+
root: readNonEmptyString(args, "nodePath") ?? "/root",
|
|
29115
|
+
include_hidden: readBoolean(args, "includeHidden") ?? false
|
|
29116
|
+
});
|
|
29099
29117
|
case "rect":
|
|
29100
29118
|
return await this.handleRuntimeCommand("get_rect", {
|
|
29101
29119
|
projectPath: args["projectPath"],
|
|
@@ -30140,8 +30158,11 @@ class GodotServer {
|
|
|
30140
30158
|
const nodePath = readNonEmptyString(args, "nodePath") ?? "";
|
|
30141
30159
|
const patience = Math.max(this.runtimeTimeoutMs(), timeoutMs + 5000);
|
|
30142
30160
|
switch (op) {
|
|
30143
|
-
case "frames":
|
|
30144
|
-
|
|
30161
|
+
case "frames": {
|
|
30162
|
+
const frames = readPositiveNumber(args, "frames") ?? 1;
|
|
30163
|
+
const waited = patienceForFrames(frames, patience);
|
|
30164
|
+
return await this.handleRuntimeCommand("wait_frames", { projectPath: args["projectPath"], frames }, waited);
|
|
30165
|
+
}
|
|
30145
30166
|
case "signal":
|
|
30146
30167
|
return await this.handleRuntimeCommand("wait_signal", {
|
|
30147
30168
|
projectPath: args["projectPath"],
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "gdharness",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.5",
|
|
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",
|