gdharness 0.11.0 → 0.12.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/README.md
CHANGED
|
@@ -61,6 +61,29 @@ parts connect, and every tool, op and argument.
|
|
|
61
61
|
[SECURITY.md](.github/SECURITY.md) is how to report something.
|
|
62
62
|
[CONTRIBUTING.md](.github/CONTRIBUTING.md) is what a change has to clear.
|
|
63
63
|
|
|
64
|
+
## Versions
|
|
65
|
+
|
|
66
|
+
0.x, and the number is telling the truth: the tools, their arguments and the shape of their
|
|
67
|
+
answers are still moving, because the projects using this keep finding answers that were wrong or
|
|
68
|
+
missing. Pin an exact version and upgrade deliberately.
|
|
69
|
+
|
|
70
|
+
Built and measured against Godot 4.7.2 on Windows, Linux and macOS. The editor, engine, runtime
|
|
71
|
+
and integration suites run against a real engine on all three before anything is tagged. Older
|
|
72
|
+
4.x is likely to work and is not tested.
|
|
73
|
+
|
|
74
|
+
What each kind of release means here:
|
|
75
|
+
|
|
76
|
+
- **Patch**: an answer that was wrong is now right. The output can change, because a wrong answer
|
|
77
|
+
corrected is a fix rather than a feature, and leaving one wrong to protect whoever parsed it is
|
|
78
|
+
how a tool stops being worth asking.
|
|
79
|
+
- **Minor**: a tool, an op, an argument or a field was added, or behaviour a caller can see
|
|
80
|
+
changed.
|
|
81
|
+
- **Major**: a tool, an op, an argument or a field was renamed or taken away. Nothing else earns
|
|
82
|
+
one.
|
|
83
|
+
|
|
84
|
+
1.0 is not a date, it is a condition: the tool surface holding still for a fortnight, and the
|
|
85
|
+
issues arriving being things it cannot do yet rather than things it answers wrongly.
|
|
86
|
+
|
|
64
87
|
## Project
|
|
65
88
|
|
|
66
89
|
Fork of [GoPeak](https://github.com/HaD0Yun/Doyunha-Gopeak) v2.3.9, September 2026, MIT: the
|
package/build/cli.js
CHANGED
|
@@ -15725,8 +15725,14 @@ var init_tool_definitions = __esm(() => {
|
|
|
15725
15725
|
type: "boolean",
|
|
15726
15726
|
description: "Only entries printed since the previous editor_output. Default false."
|
|
15727
15727
|
},
|
|
15728
|
-
contains: {
|
|
15729
|
-
|
|
15728
|
+
contains: {
|
|
15729
|
+
type: "string",
|
|
15730
|
+
description: "Only entries mentioning this text, matched against everything the run has printed rather than against the entries this answer would otherwise carry: a line is found however much was printed after it."
|
|
15731
|
+
},
|
|
15732
|
+
limit: {
|
|
15733
|
+
type: "number",
|
|
15734
|
+
description: "The most entries to answer with, newest kept. Default 200, and omitted says how many matching entries that left out. A run long enough to pass it needs this raised, not just filtered: filtering narrows what counts as matching, and the newest of those is still all one answer carries."
|
|
15735
|
+
}
|
|
15730
15736
|
},
|
|
15731
15737
|
requires: []
|
|
15732
15738
|
},
|
|
@@ -15793,7 +15799,7 @@ var init_tool_definitions = __esm(() => {
|
|
|
15793
15799
|
property: {
|
|
15794
15800
|
type: "string",
|
|
15795
15801
|
ops: ["property", "find"],
|
|
15796
|
-
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
|
|
15802
|
+
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 what a node holds, "_game:clock:speed", which is where a game keeps what is worth asking about. A number or a key steps into a list or a map, "_game:run:roster:0:traits", which is how the lists a game keeps its state in are walked: a roster, a board, an in-tray. A negative number counts from the end. A step that is not there is named, and says what was there instead.'
|
|
15797
15803
|
},
|
|
15798
15804
|
depth: { type: "number", ops: ["tree"], description: "tree: levels to descend. Default 3." },
|
|
15799
15805
|
includeProperties: {
|
|
@@ -15867,7 +15873,7 @@ var init_tool_definitions = __esm(() => {
|
|
|
15867
15873
|
property: {
|
|
15868
15874
|
type: "string",
|
|
15869
15875
|
ops: ["set"],
|
|
15870
|
-
description: 'set: which one to write. Colons write through
|
|
15876
|
+
description: 'set: which one to write. Colons write through what a node holds, "_game:run:day", and a number or a key steps into a list or a map on the way, "_game:run:roster:0:name", a negative number counting from the end. The answer reads back off the same holder, so a write a typed container refused shows as an unchanged value.'
|
|
15871
15877
|
},
|
|
15872
15878
|
value: {
|
|
15873
15879
|
ops: ["set"],
|
|
@@ -15877,7 +15883,7 @@ var init_tool_definitions = __esm(() => {
|
|
|
15877
15883
|
method: {
|
|
15878
15884
|
type: "string",
|
|
15879
15885
|
ops: ["call"],
|
|
15880
|
-
description: 'call: which one to call. Colons call through
|
|
15886
|
+
description: 'call: which one to call. Colons call through what a node holds, "_game:run:advance", the same way a property is written through them, and a list or a map is stepped into by index or key, "_game:run:roster:0:retire".'
|
|
15881
15887
|
},
|
|
15882
15888
|
args: {
|
|
15883
15889
|
type: "array",
|
|
@@ -36097,6 +36103,9 @@ function runsDirectory() {
|
|
|
36097
36103
|
function recordPath() {
|
|
36098
36104
|
return join14(runsDirectory(), "run.json");
|
|
36099
36105
|
}
|
|
36106
|
+
function recordPaths() {
|
|
36107
|
+
return runtimeDirectories().map((directory) => join14(directory, "runs", "run.json"));
|
|
36108
|
+
}
|
|
36100
36109
|
function openTranscript(startedAt) {
|
|
36101
36110
|
mkdirSync6(runsDirectory(), { recursive: true });
|
|
36102
36111
|
const path = join14(runsDirectory(), `run-${startedAt}.log`);
|
|
@@ -36107,9 +36116,18 @@ function writeRunRecord(record) {
|
|
|
36107
36116
|
writeFileSync8(recordPath(), JSON.stringify(record, null, 2), "utf8");
|
|
36108
36117
|
}
|
|
36109
36118
|
function readRunRecord() {
|
|
36119
|
+
for (const path of recordPaths()) {
|
|
36120
|
+
const record = recordAt(path);
|
|
36121
|
+
if (record !== null) {
|
|
36122
|
+
return record;
|
|
36123
|
+
}
|
|
36124
|
+
}
|
|
36125
|
+
return null;
|
|
36126
|
+
}
|
|
36127
|
+
function recordAt(path) {
|
|
36110
36128
|
let parsed;
|
|
36111
36129
|
try {
|
|
36112
|
-
parsed = JSON.parse(readFileSync12(
|
|
36130
|
+
parsed = JSON.parse(readFileSync12(path, "utf8"));
|
|
36113
36131
|
} catch {
|
|
36114
36132
|
return null;
|
|
36115
36133
|
}
|
|
@@ -36132,10 +36150,16 @@ function readRunRecord() {
|
|
|
36132
36150
|
arguments: Array.isArray(args) ? args.filter((value) => typeof value === "string") : []
|
|
36133
36151
|
};
|
|
36134
36152
|
}
|
|
36135
|
-
function clearRunRecord() {
|
|
36136
|
-
|
|
36137
|
-
|
|
36138
|
-
|
|
36153
|
+
function clearRunRecord(ours = () => true) {
|
|
36154
|
+
for (const path of recordPaths()) {
|
|
36155
|
+
const record = recordAt(path);
|
|
36156
|
+
if (record === null || !ours(record)) {
|
|
36157
|
+
continue;
|
|
36158
|
+
}
|
|
36159
|
+
try {
|
|
36160
|
+
rmSync7(path);
|
|
36161
|
+
} catch {}
|
|
36162
|
+
}
|
|
36139
36163
|
}
|
|
36140
36164
|
function ageMs(path, now) {
|
|
36141
36165
|
try {
|
|
@@ -37764,7 +37788,7 @@ class GodotServer {
|
|
|
37764
37788
|
async endActiveGame() {
|
|
37765
37789
|
const running = this.activeProcess;
|
|
37766
37790
|
this.activeProcess = null;
|
|
37767
|
-
clearRunRecord();
|
|
37791
|
+
clearRunRecord((record) => this.couldBeOurs(record.projectPath));
|
|
37768
37792
|
if (!running) {
|
|
37769
37793
|
return;
|
|
37770
37794
|
}
|
|
@@ -172,12 +172,12 @@ func _found(node: Node, wanted_property: String) -> Dictionary:
|
|
|
172
172
|
if reached.has("message"):
|
|
173
173
|
entry["has_property"] = false
|
|
174
174
|
return entry
|
|
175
|
-
var holder:
|
|
175
|
+
var holder: Variant = reached["holder"]
|
|
176
176
|
var named: String = reached["name"]
|
|
177
|
-
var has: bool =
|
|
177
|
+
var has: bool = _can_read(holder, named)
|
|
178
178
|
entry["has_property"] = has
|
|
179
179
|
if has:
|
|
180
|
-
entry["value"] = _values.serialize(holder
|
|
180
|
+
entry["value"] = _values.serialize(_read(holder, named))
|
|
181
181
|
return entry
|
|
182
182
|
|
|
183
183
|
|
|
@@ -500,18 +500,18 @@ func get_property(params: Dictionary) -> Dictionary:
|
|
|
500
500
|
if reached.has("message"):
|
|
501
501
|
return reached
|
|
502
502
|
|
|
503
|
-
var holder:
|
|
503
|
+
var holder: Variant = reached["holder"]
|
|
504
504
|
var named: String = reached["name"]
|
|
505
|
-
# Asked of the
|
|
506
|
-
#
|
|
507
|
-
if not
|
|
508
|
-
return {"type": "error", "message":
|
|
505
|
+
# Asked of the holder rather than read and compared to null, because a property the holder does
|
|
506
|
+
# not have and a property that is null both read as null.
|
|
507
|
+
if not _can_read(holder, named):
|
|
508
|
+
return {"type": "error", "message": _nothing_there(holder, named, str(reached["called"]))}
|
|
509
509
|
|
|
510
510
|
return {
|
|
511
511
|
"type": "property",
|
|
512
512
|
"path": node_path,
|
|
513
513
|
"property": property,
|
|
514
|
-
"value": _values.serialize(holder
|
|
514
|
+
"value": _values.serialize(_read(holder, named)),
|
|
515
515
|
}
|
|
516
516
|
|
|
517
517
|
|
|
@@ -532,21 +532,115 @@ func get_property(params: Dictionary) -> Dictionary:
|
|
|
532
532
|
## clocks" is a typo found, and "no property _game:clocks:speed" is a puzzle.
|
|
533
533
|
func _reached(node: Node, node_path: String, reaching: String) -> Dictionary:
|
|
534
534
|
var parts: PackedStringArray = reaching.split(":")
|
|
535
|
-
var holder:
|
|
535
|
+
var holder: Variant = node
|
|
536
536
|
var called: String = node_path
|
|
537
537
|
for step: int in parts.size() - 1:
|
|
538
538
|
var named: String = parts[step]
|
|
539
|
-
if not
|
|
540
|
-
return {"type": "error", "message":
|
|
541
|
-
|
|
539
|
+
if not _can_read(holder, named):
|
|
540
|
+
return {"type": "error", "message": _nothing_there(holder, named, called)}
|
|
541
|
+
holder = _read(holder, named)
|
|
542
542
|
called = "%s:%s" % [called, named]
|
|
543
|
-
if not
|
|
543
|
+
if not _can_hold(holder):
|
|
544
544
|
var wanted: String = parts[step + 1]
|
|
545
545
|
return {"type": "error", "message": "%s holds no object to read %s off" % [called, wanted]}
|
|
546
|
-
holder = next
|
|
547
546
|
return {"holder": holder, "name": parts[parts.size() - 1], "called": called}
|
|
548
547
|
|
|
549
548
|
|
|
549
|
+
## Whether a step can be taken into [param value] at all: an object, a list or a map can be walked
|
|
550
|
+
## into, and a number or a string is where a path ends whether the caller meant it to or not.
|
|
551
|
+
static func _can_hold(value: Variant) -> bool:
|
|
552
|
+
return value is Object or value is Array or value is Dictionary
|
|
553
|
+
|
|
554
|
+
|
|
555
|
+
## Whether [param named] is something [param holder] has.
|
|
556
|
+
##
|
|
557
|
+
## Three kinds of holder, because the state a game keeps is not all properties of objects: a roster
|
|
558
|
+
## is a list and an in-tray is a map, and a path that could not step into either stopped at the
|
|
559
|
+
## first one. An index reads the way a property does, and a negative one counts from the end the
|
|
560
|
+
## way GDScript's own does, so reading the last of something does not mean asking how many first.
|
|
561
|
+
static func _can_read(holder: Variant, named: String) -> bool:
|
|
562
|
+
if holder is Array:
|
|
563
|
+
var items: Array = holder
|
|
564
|
+
return _index_in(named, items.size()) != -1
|
|
565
|
+
if holder is Dictionary:
|
|
566
|
+
var map: Dictionary = holder
|
|
567
|
+
return map.has(named) or map.has(StringName(named))
|
|
568
|
+
if holder is Object:
|
|
569
|
+
return _has_property(holder, named)
|
|
570
|
+
return false
|
|
571
|
+
|
|
572
|
+
|
|
573
|
+
## Put [param value] where [param named] points. Only ever called once [method _can_read] agrees,
|
|
574
|
+
## and lists and maps are reference types, so writing into one reaches the game's own copy.
|
|
575
|
+
static func _write(holder: Variant, named: String, value: Variant) -> void:
|
|
576
|
+
if holder is Array:
|
|
577
|
+
var items: Array = holder
|
|
578
|
+
items[_index_in(named, items.size())] = value
|
|
579
|
+
return
|
|
580
|
+
if holder is Dictionary:
|
|
581
|
+
var map: Dictionary = holder
|
|
582
|
+
# Written back under the key it was found under, since a map keyed by StringName and one
|
|
583
|
+
# keyed by String both read the same way and a write to the wrong one adds a second entry.
|
|
584
|
+
if map.has(named):
|
|
585
|
+
map[named] = value
|
|
586
|
+
else:
|
|
587
|
+
map[StringName(named)] = value
|
|
588
|
+
return
|
|
589
|
+
var object: Object = holder
|
|
590
|
+
object.set(named, value)
|
|
591
|
+
|
|
592
|
+
|
|
593
|
+
## What [param holder] holds under [param named]. Only ever called once [method _can_read] agrees.
|
|
594
|
+
static func _read(holder: Variant, named: String) -> Variant:
|
|
595
|
+
if holder is Array:
|
|
596
|
+
var items: Array = holder
|
|
597
|
+
return items[_index_in(named, items.size())]
|
|
598
|
+
if holder is Dictionary:
|
|
599
|
+
var map: Dictionary = holder
|
|
600
|
+
if map.has(named):
|
|
601
|
+
return map[named]
|
|
602
|
+
return map[StringName(named)]
|
|
603
|
+
var object: Object = holder
|
|
604
|
+
return object.get(named)
|
|
605
|
+
|
|
606
|
+
|
|
607
|
+
## Where [param named] lands in a list of [param size], or -1 for a step that is not in it.
|
|
608
|
+
##
|
|
609
|
+
## -1 for "nowhere" is safe because every answer this gives is an index into a list that long, so a
|
|
610
|
+
## real one is never negative by the time it is returned.
|
|
611
|
+
static func _index_in(named: String, size: int) -> int:
|
|
612
|
+
if not named.is_valid_int():
|
|
613
|
+
return -1
|
|
614
|
+
var index: int = int(named)
|
|
615
|
+
if index < 0:
|
|
616
|
+
index += size
|
|
617
|
+
return index if index >= 0 and index < size else -1
|
|
618
|
+
|
|
619
|
+
|
|
620
|
+
## Why a step could not be taken, said in the terms of what was actually there.
|
|
621
|
+
##
|
|
622
|
+
## "has no property 0" about a list is a true sentence that sends somebody looking for a property,
|
|
623
|
+
## so a list says how long it is and a map says what it is keyed by. The step is named rather than
|
|
624
|
+
## the whole path, because the step is the typo.
|
|
625
|
+
static func _nothing_there(holder: Variant, named: String, called: String) -> String:
|
|
626
|
+
if holder is Array:
|
|
627
|
+
var items: Array = holder
|
|
628
|
+
return "%s is a list of %d, so there is no %s in it" % [called, items.size(), named]
|
|
629
|
+
if holder is Dictionary:
|
|
630
|
+
var map: Dictionary = holder
|
|
631
|
+
var keys: Array = map.keys()
|
|
632
|
+
# Eight of them, because a map keyed by something unexpected is told by the first few and a
|
|
633
|
+
# map of two hundred would bury the sentence saying which key was missing.
|
|
634
|
+
var some: PackedStringArray = []
|
|
635
|
+
for key: Variant in keys.slice(0, 8):
|
|
636
|
+
some.append(str(key))
|
|
637
|
+
var rest: String = ""
|
|
638
|
+
if keys.size() > 8:
|
|
639
|
+
rest = " and %d more" % [keys.size() - 8]
|
|
640
|
+
return "%s has no key %s; it is keyed by %s%s" % [called, named, ", ".join(some), rest]
|
|
641
|
+
return "%s has no property %s" % [called, named]
|
|
642
|
+
|
|
643
|
+
|
|
550
644
|
## Whether [param holder] declares [param named]. Its own function because three ops ask it and a
|
|
551
645
|
## property read back as null answers it wrongly.
|
|
552
646
|
static func _has_property(holder: Object, named: String) -> bool:
|
|
@@ -576,17 +670,19 @@ func set_property(params: Dictionary) -> Dictionary:
|
|
|
576
670
|
if reached.has("message"):
|
|
577
671
|
return reached
|
|
578
672
|
|
|
579
|
-
var holder:
|
|
673
|
+
var holder: Variant = reached["holder"]
|
|
580
674
|
var named: String = reached["name"]
|
|
581
|
-
|
|
582
|
-
|
|
675
|
+
if not _can_read(holder, named):
|
|
676
|
+
return {"type": "error", "message": _nothing_there(holder, named, str(reached["called"]))}
|
|
677
|
+
var old_value: Variant = _read(holder, named)
|
|
678
|
+
_write(holder, named, _values.fitted(value, typeof(old_value)))
|
|
583
679
|
|
|
584
680
|
return {
|
|
585
681
|
"type": "property_set",
|
|
586
682
|
"path": node_path,
|
|
587
683
|
"property": property,
|
|
588
684
|
"old_value": _values.serialize(old_value),
|
|
589
|
-
"new_value": _values.serialize(holder
|
|
685
|
+
"new_value": _values.serialize(_read(holder, named))
|
|
590
686
|
}
|
|
591
687
|
|
|
592
688
|
|
|
@@ -610,6 +706,19 @@ func call_method(params: Dictionary) -> Dictionary:
|
|
|
610
706
|
if reached.has("message"):
|
|
611
707
|
return reached
|
|
612
708
|
|
|
709
|
+
# A list or a map can be stepped through on the way, and is not a thing with methods when the
|
|
710
|
+
# path stops on one: said in those terms rather than as "has no method", which reads as a
|
|
711
|
+
# misspelling of a method that was never going to be there.
|
|
712
|
+
if not reached["holder"] is Object:
|
|
713
|
+
var sort: String = "a list" if reached["holder"] is Array else "a map"
|
|
714
|
+
return {
|
|
715
|
+
"type": "error",
|
|
716
|
+
"message":
|
|
717
|
+
(
|
|
718
|
+
"%s is %s, which has no methods: name the element to call it on, as in %s:0:%s"
|
|
719
|
+
% [reached["called"], sort, reached["called"], reached["name"]]
|
|
720
|
+
)
|
|
721
|
+
}
|
|
613
722
|
var holder: Object = reached["holder"]
|
|
614
723
|
var named: String = reached["name"]
|
|
615
724
|
if not holder.has_method(named):
|
|
@@ -136,7 +136,36 @@ func _serialize_object(value: Object) -> Dictionary:
|
|
|
136
136
|
var node: Node = value
|
|
137
137
|
if node.is_inside_tree():
|
|
138
138
|
return {"_type": "Node", "class": node.get_class(), "path": str(node.get_path())}
|
|
139
|
-
return
|
|
139
|
+
return _describe_object(value)
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
## A plain object, said in enough detail to tell it from the one beside it.
|
|
143
|
+
##
|
|
144
|
+
## `{"class": "RefCounted"}` was all of it, so a roster of twelve people came back as the word
|
|
145
|
+
## RefCounted twelve times: correct, useless, and indistinguishable from a list of twelve of
|
|
146
|
+
## anything else. The state worth looking at in a game is lists of these, so each one carries what
|
|
147
|
+
## it is (the `class_name` its script declares, or the script's path when it has no name), what it
|
|
148
|
+
## says about itself when the game has given it a `_to_string`, and an id that is at least
|
|
149
|
+
## different per object. None of it is invented: every field is left out when there is nothing to
|
|
150
|
+
## put in it, because a key holding "" is a worse answer than a key that is not there.
|
|
151
|
+
func _describe_object(value: Object) -> Dictionary:
|
|
152
|
+
var described: Dictionary = {"_type": "Object", "class": value.get_class()}
|
|
153
|
+
# Asked as a Variant and narrowed, because get_script answers one and an object without a
|
|
154
|
+
# script answers null: casting that to Script is the unsafe cast the engine refuses to compile.
|
|
155
|
+
var attached: Variant = value.get_script()
|
|
156
|
+
if attached is Script:
|
|
157
|
+
var script: Script = attached
|
|
158
|
+
var declared: String = String(script.get_global_name())
|
|
159
|
+
if not declared.is_empty():
|
|
160
|
+
described["script_class"] = declared
|
|
161
|
+
elif not script.resource_path.is_empty():
|
|
162
|
+
described["script"] = script.resource_path
|
|
163
|
+
# Only when the game wrote one. The default is "<RefCounted#31098236>", which is the id again
|
|
164
|
+
# in a costume and reads as if the object had been asked and had nothing to say.
|
|
165
|
+
if value.has_method("_to_string"):
|
|
166
|
+
described["says"] = value.to_string()
|
|
167
|
+
described["id"] = value.get_instance_id()
|
|
168
|
+
return described
|
|
140
169
|
|
|
141
170
|
|
|
142
171
|
## Rebuilds a Godot value from the shape serialize gave it.
|
package/build/index.js
CHANGED
|
@@ -27798,6 +27798,9 @@ function runsDirectory() {
|
|
|
27798
27798
|
function recordPath() {
|
|
27799
27799
|
return join8(runsDirectory(), "run.json");
|
|
27800
27800
|
}
|
|
27801
|
+
function recordPaths() {
|
|
27802
|
+
return runtimeDirectories().map((directory) => join8(directory, "runs", "run.json"));
|
|
27803
|
+
}
|
|
27801
27804
|
function openTranscript(startedAt) {
|
|
27802
27805
|
mkdirSync3(runsDirectory(), { recursive: true });
|
|
27803
27806
|
const path = join8(runsDirectory(), `run-${startedAt}.log`);
|
|
@@ -27808,9 +27811,18 @@ function writeRunRecord(record) {
|
|
|
27808
27811
|
writeFileSync4(recordPath(), JSON.stringify(record, null, 2), "utf8");
|
|
27809
27812
|
}
|
|
27810
27813
|
function readRunRecord() {
|
|
27814
|
+
for (const path of recordPaths()) {
|
|
27815
|
+
const record = recordAt(path);
|
|
27816
|
+
if (record !== null) {
|
|
27817
|
+
return record;
|
|
27818
|
+
}
|
|
27819
|
+
}
|
|
27820
|
+
return null;
|
|
27821
|
+
}
|
|
27822
|
+
function recordAt(path) {
|
|
27811
27823
|
let parsed;
|
|
27812
27824
|
try {
|
|
27813
|
-
parsed = JSON.parse(readFileSync8(
|
|
27825
|
+
parsed = JSON.parse(readFileSync8(path, "utf8"));
|
|
27814
27826
|
} catch {
|
|
27815
27827
|
return null;
|
|
27816
27828
|
}
|
|
@@ -27833,10 +27845,16 @@ function readRunRecord() {
|
|
|
27833
27845
|
arguments: Array.isArray(args) ? args.filter((value) => typeof value === "string") : []
|
|
27834
27846
|
};
|
|
27835
27847
|
}
|
|
27836
|
-
function clearRunRecord() {
|
|
27837
|
-
|
|
27838
|
-
|
|
27839
|
-
|
|
27848
|
+
function clearRunRecord(ours = () => true) {
|
|
27849
|
+
for (const path of recordPaths()) {
|
|
27850
|
+
const record = recordAt(path);
|
|
27851
|
+
if (record === null || !ours(record)) {
|
|
27852
|
+
continue;
|
|
27853
|
+
}
|
|
27854
|
+
try {
|
|
27855
|
+
rmSync3(path);
|
|
27856
|
+
} catch {}
|
|
27857
|
+
}
|
|
27840
27858
|
}
|
|
27841
27859
|
function ageMs(path, now) {
|
|
27842
27860
|
try {
|
|
@@ -28511,8 +28529,14 @@ var TOOL_SPECS = [
|
|
|
28511
28529
|
type: "boolean",
|
|
28512
28530
|
description: "Only entries printed since the previous editor_output. Default false."
|
|
28513
28531
|
},
|
|
28514
|
-
contains: {
|
|
28515
|
-
|
|
28532
|
+
contains: {
|
|
28533
|
+
type: "string",
|
|
28534
|
+
description: "Only entries mentioning this text, matched against everything the run has printed rather than against the entries this answer would otherwise carry: a line is found however much was printed after it."
|
|
28535
|
+
},
|
|
28536
|
+
limit: {
|
|
28537
|
+
type: "number",
|
|
28538
|
+
description: "The most entries to answer with, newest kept. Default 200, and omitted says how many matching entries that left out. A run long enough to pass it needs this raised, not just filtered: filtering narrows what counts as matching, and the newest of those is still all one answer carries."
|
|
28539
|
+
}
|
|
28516
28540
|
},
|
|
28517
28541
|
requires: []
|
|
28518
28542
|
},
|
|
@@ -28579,7 +28603,7 @@ var TOOL_SPECS = [
|
|
|
28579
28603
|
property: {
|
|
28580
28604
|
type: "string",
|
|
28581
28605
|
ops: ["property", "find"],
|
|
28582
|
-
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
|
|
28606
|
+
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 what a node holds, "_game:clock:speed", which is where a game keeps what is worth asking about. A number or a key steps into a list or a map, "_game:run:roster:0:traits", which is how the lists a game keeps its state in are walked: a roster, a board, an in-tray. A negative number counts from the end. A step that is not there is named, and says what was there instead.'
|
|
28583
28607
|
},
|
|
28584
28608
|
depth: { type: "number", ops: ["tree"], description: "tree: levels to descend. Default 3." },
|
|
28585
28609
|
includeProperties: {
|
|
@@ -28653,7 +28677,7 @@ var TOOL_SPECS = [
|
|
|
28653
28677
|
property: {
|
|
28654
28678
|
type: "string",
|
|
28655
28679
|
ops: ["set"],
|
|
28656
|
-
description: 'set: which one to write. Colons write through
|
|
28680
|
+
description: 'set: which one to write. Colons write through what a node holds, "_game:run:day", and a number or a key steps into a list or a map on the way, "_game:run:roster:0:name", a negative number counting from the end. The answer reads back off the same holder, so a write a typed container refused shows as an unchanged value.'
|
|
28657
28681
|
},
|
|
28658
28682
|
value: {
|
|
28659
28683
|
ops: ["set"],
|
|
@@ -28663,7 +28687,7 @@ var TOOL_SPECS = [
|
|
|
28663
28687
|
method: {
|
|
28664
28688
|
type: "string",
|
|
28665
28689
|
ops: ["call"],
|
|
28666
|
-
description: 'call: which one to call. Colons call through
|
|
28690
|
+
description: 'call: which one to call. Colons call through what a node holds, "_game:run:advance", the same way a property is written through them, and a list or a map is stepped into by index or key, "_game:run:roster:0:retire".'
|
|
28667
28691
|
},
|
|
28668
28692
|
args: {
|
|
28669
28693
|
type: "array",
|
|
@@ -30546,7 +30570,7 @@ class GodotServer {
|
|
|
30546
30570
|
async endActiveGame() {
|
|
30547
30571
|
const running = this.activeProcess;
|
|
30548
30572
|
this.activeProcess = null;
|
|
30549
|
-
clearRunRecord();
|
|
30573
|
+
clearRunRecord((record) => this.couldBeOurs(record.projectPath));
|
|
30550
30574
|
if (!running) {
|
|
30551
30575
|
return;
|
|
30552
30576
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "gdharness",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.12.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",
|