gdharness 0.9.7 → 0.10.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
@@ -7474,6 +7474,9 @@ import { join as join6 } from "node:path";
7474
7474
  function declaredClasses(projectPath) {
7475
7475
  const declared = new Map;
7476
7476
  const visit = (directory, prefix) => {
7477
+ if (existsSync6(join6(directory, ".gdignore"))) {
7478
+ return;
7479
+ }
7477
7480
  for (const entry of readdirSync2(directory, { withFileTypes: true })) {
7478
7481
  if (entry.name.startsWith(".")) {
7479
7482
  continue;
@@ -7511,6 +7514,12 @@ function staleClassNames(projectPath) {
7511
7514
  const cached = cachedClasses(projectPath);
7512
7515
  return cached === null ? [...declaredClasses(projectPath).keys()] : staleAgainst(cached, projectPath);
7513
7516
  }
7517
+ function unseenByEditor(projectPath, editorHolds) {
7518
+ const held = new Set(editorHolds);
7519
+ const declared = declaredClasses(projectPath);
7520
+ const source = cachedClasses(projectPath) ?? declared;
7521
+ return [...source].filter(([name]) => !held.has(name) && declared.has(name)).map(([className, path]) => ({ className, path: declared.get(className) ?? path }));
7522
+ }
7514
7523
  var init_class_cache = () => {};
7515
7524
 
7516
7525
  // src/headless.ts
@@ -15371,7 +15380,7 @@ var init_tool_definitions = __esm(() => {
15371
15380
  uid: { summary: "the UID of one file", requires: ["resourcePath"] },
15372
15381
  refresh_uids: { summary: "resave every resource so UID references are current", requires: [] },
15373
15382
  refresh_classes: {
15374
- summary: "rewrite .godot/global_script_class_cache.cfg from the class_name declarations on disk, for an editor whose list has gone stale",
15383
+ summary: 'rewrite .godot/global_script_class_cache.cfg from the class_name declarations on disk, and name under unseenByEditor any class the editor open on this project still cannot resolve: rewriting the file does not reach the list a running editor loaded, so "added: []" means the file was already right rather than that nothing is wrong',
15375
15384
  requires: []
15376
15385
  }
15377
15386
  }
@@ -15716,7 +15725,7 @@ var init_tool_definitions = __esm(() => {
15716
15725
  },
15717
15726
  {
15718
15727
  name: "editor_rescan",
15719
- description: "Makes the running editor scan the project filesystem, so files written outside it are picked up. The scan is a change-detecting walk rather than an unconditional reparse, so a file the editor has already recorded and reads as unchanged is one it does not look inside again: a class_name usually becomes visible this way and on one project did not, with the cache on disk still missing it after the scan said it was done. project_import refresh_classes is the one that rebuilds the class list from the declarations on disk, whatever the editor thinks it has seen. Needs the editor connected.",
15728
+ description: "Makes the running editor scan the project filesystem, so files written outside it are picked up. The scan is a change-detecting walk rather than an unconditional reparse, so a file another engine has already imported reads as settled and the walk does not look inside it: its class_name then stays out of the list the editor resolves against, however many times you scan. Any class in that state is named under unseenByEditor, which is what no check on disk can see, since the declaration and the cache are both correct there and only the editor disagrees. The cure is a change to the declaring script, or editor_launch restart; project_import refresh_classes rewrites the cache and does not touch what the editor is holding. Needs the editor connected.",
15720
15729
  parameters: {
15721
15730
  projectPath: PROJECT_PATH,
15722
15731
  timeoutMs: { type: "number", description: "How long to wait for the scan. Default 30000." }
@@ -36092,6 +36101,10 @@ function realPathOr(path) {
36092
36101
  return path;
36093
36102
  }
36094
36103
  }
36104
+ function samePath(one, other) {
36105
+ const settled = (path) => realPathOr(path).replaceAll("\\", "/").replace(/\/+$/, "").toLowerCase();
36106
+ return settled(one) === settled(other);
36107
+ }
36095
36108
  function aboveTheRunner(entry) {
36096
36109
  const runner = entry.detail.findIndex((line) => line.includes(RUNNER_DIRECTORY));
36097
36110
  if (runner < 0) {
@@ -36522,7 +36535,8 @@ class GodotServer {
36522
36535
  async dispatch(tool, op, args) {
36523
36536
  const headless = HEADLESS_OPERATIONS[tool]?.[op];
36524
36537
  if (headless !== undefined) {
36525
- return await this.headless(headless, args);
36538
+ const answered = await this.headless(headless, args);
36539
+ return headless === "refresh_class_cache" && this.godotBridge.isConnected() ? await this.alsoSayWhatTheEditorCannotSee(answered, args) : answered;
36526
36540
  }
36527
36541
  const { op: _op, ...arguments_ } = args;
36528
36542
  const bridge = async (command, extra = {}) => {
@@ -37630,13 +37644,59 @@ class GodotServer {
37630
37644
  const status = asParams(await this.godotBridge.invokeTool("rescan_filesystem", { ...args, statusOnly: true }));
37631
37645
  busy = Boolean(status["scanning"]) || Boolean(status["importing"]);
37632
37646
  }
37647
+ const checked = busy ? { unseen: [] } : await this.classesTheEditorCannotSee(args);
37648
+ const unseen = checked.unseen;
37633
37649
  return this.jsonTextResponse({
37634
- ok: !busy,
37650
+ ok: !busy && unseen.length === 0 && checked.unchecked === undefined,
37635
37651
  stillWorking: busy,
37636
37652
  waitedMs: Date.now() - started,
37637
- note: busy ? "The editor was still scanning or importing when the wait ran out, so new files may not be visible yet." : undefined
37653
+ unseenByEditor: unseen.length > 0 ? unseen : undefined,
37654
+ classesUnchecked: checked.unchecked,
37655
+ note: busy ? "The editor was still scanning or importing when the wait ran out, so new files may not be visible yet." : unseen.length > 0 ? "The scan finished and these classes are still not in the list the editor resolves against, so every use of them reads as an unknown identifier. Its walk skips a file another engine has already imported. Change the declaring script and rescan, or restart the editor with editor_launch restart." : undefined
37638
37656
  });
37639
37657
  }
37658
+ async alsoSayWhatTheEditorCannotSee(answered, args) {
37659
+ const first = answered.content[0];
37660
+ if (answered.isError || first?.type !== "text" || typeof first.text !== "string") {
37661
+ return answered;
37662
+ }
37663
+ const checked = await this.classesTheEditorCannotSee(args);
37664
+ if (checked.unseen.length === 0 && checked.unchecked === undefined) {
37665
+ return answered;
37666
+ }
37667
+ return this.jsonTextResponse({
37668
+ ...asParams(JSON.parse(first.text)),
37669
+ unseenByEditor: checked.unseen.length > 0 ? checked.unseen : undefined,
37670
+ classesUnchecked: checked.unchecked,
37671
+ note: checked.unseen.length > 0 ? "The cache on disk is right now, and the editor holding this project is still not resolving these: rewriting the file does not reach the list it already loaded. Change the declaring script and editor_rescan, or restart the editor with editor_launch restart." : undefined
37672
+ });
37673
+ }
37674
+ async classesTheEditorCannotSee(args) {
37675
+ const projectPath = typeof args["projectPath"] === "string" ? args["projectPath"] : "";
37676
+ if (projectPath === "") {
37677
+ return { unseen: [], unchecked: "no projectPath, so there was nothing to read the cache from" };
37678
+ }
37679
+ const open = this.godotBridge.getStatus().projectPath;
37680
+ if (open === undefined || !samePath(open, projectPath)) {
37681
+ return {
37682
+ unseen: [],
37683
+ unchecked: `the connected editor is open on ${open ?? "a project it did not name"}, not this one`
37684
+ };
37685
+ }
37686
+ let held;
37687
+ try {
37688
+ held = asParams(await this.godotBridge.invokeTool("global_classes", args));
37689
+ } catch (error) {
37690
+ return {
37691
+ unseen: [],
37692
+ unchecked: `the editor would not say what classes it is holding: ${errorMessage(error)}`
37693
+ };
37694
+ }
37695
+ if (!Array.isArray(held["classes"])) {
37696
+ return { unseen: [], unchecked: "the editor answered without a class list in it" };
37697
+ }
37698
+ return { unseen: unseenByEditor(projectPath, held["classes"].map(String)) };
37699
+ }
37640
37700
  async handleViaBridge(toolName, args) {
37641
37701
  if (!this.godotBridge.isConnected()) {
37642
37702
  return this.createErrorResponse("Editor not connected: no Godot editor with the gdharness editor addon has reached this server.", [
@@ -39226,6 +39286,12 @@ formatting. \`script_diagnostics\` and \`script_info\` come from the editor's ow
39226
39286
  \`editor_classes\` need nothing open: they run a short headless engine and are gone before the
39227
39287
  answer is printed.
39228
39288
 
39289
+ A wall of "Could not find type" on the project's own classes means the editor is not holding
39290
+ them. \`editor_rescan\` asks it to scan and names any class it still cannot resolve under
39291
+ \`unseenByEditor\`: its walk skips a file a headless engine has already imported, so the
39292
+ declaration and the cache can both be right while the editor stays blind to it. Change the
39293
+ declaring script, or restart the editor.
39294
+
39229
39295
  ## Refusals are useful
39230
39296
 
39231
39297
  An unknown op, an argument the tool does not name, or a missing required one is refused with the
@@ -9,6 +9,7 @@ const SceneTools = preload("tools/scene_tools.gd")
9
9
  const ResourceTools = preload("tools/resource_tools.gd")
10
10
  const AnimationTools = preload("tools/animation_tools.gd")
11
11
  const PlayTools = preload("tools/play_tools.gd")
12
+ const ClassTools = preload("tools/class_tools.gd")
12
13
 
13
14
  var _editor_plugin: EditorPlugin = null
14
15
 
@@ -16,6 +17,7 @@ var _scene_tools: SceneTools = null
16
17
  var _resource_tools: ResourceTools = null
17
18
  var _animation_tools: AnimationTools = null
18
19
  var _play_tools: PlayTools = null
20
+ var _class_tools: ClassTools = null
19
21
 
20
22
  var _tool_map: Dictionary = {}
21
23
  var _initialized: bool = false
@@ -28,6 +30,7 @@ func set_editor_plugin(plugin: EditorPlugin) -> void:
28
30
  _resource_tools.set_editor_plugin(plugin)
29
31
  _animation_tools.set_editor_plugin(plugin)
30
32
  _play_tools.set_editor_plugin(plugin)
33
+ _class_tools.set_editor_plugin(plugin)
31
34
 
32
35
 
33
36
  func _init_tools() -> void:
@@ -51,6 +54,10 @@ func _init_tools() -> void:
51
54
  _play_tools.name = "PlayTools"
52
55
  add_child(_play_tools)
53
56
 
57
+ _class_tools = ClassTools.new()
58
+ _class_tools.name = "ClassTools"
59
+ add_child(_class_tools)
60
+
54
61
  _tool_map = {
55
62
  # Scene tools
56
63
  "create_scene": [_scene_tools, "create_scene"],
@@ -66,6 +73,7 @@ func _init_tools() -> void:
66
73
  "disconnect_signal": [_scene_tools, "disconnect_signal"],
67
74
  "list_connections": [_scene_tools, "list_connections"],
68
75
  "rescan_filesystem": [_scene_tools, "rescan_filesystem"],
76
+ "global_classes": [_class_tools, "global_classes"],
69
77
  # Resource tools
70
78
  "create_resource": [_resource_tools, "create_resource"],
71
79
  "modify_resource": [_resource_tools, "modify_resource"],
@@ -0,0 +1,56 @@
1
+ @tool
2
+ extends Node
3
+
4
+ ## The global classes the running editor is holding.
5
+ ##
6
+ ## The editor fixes this list when it starts and adds to it only from a filesystem scan, and
7
+ ## that scan is change-detecting: a file another engine has already imported looks settled, so
8
+ ## the walk goes past it without reading the declaration inside. The class is then declared on
9
+ ## disk, listed in `.godot/global_script_class_cache.cfg`, and absent from the editor, whose
10
+ ## analyser reports every use of it as an unknown identifier. Rebuilding the cache does not
11
+ ## help, because nothing rereads it.
12
+ ##
13
+ ## Every check there was compares one file on disk against another, so all of them call that
14
+ ## state clean. The editor is the only thing that can say otherwise, so it is asked.
15
+ ##
16
+ ## Read off the editor's own filesystem entries rather than from
17
+ ## `ProjectSettings.get_global_class_list()`. The entries are what the scan itself recorded, so
18
+ ## nothing but a scan can change them; the settings list is loaded from the cache file when it is
19
+ ## first asked for, which makes it a poor witness against a cache that was just rewritten, and
20
+ ## rewriting the cache is the call most in need of an honest answer here.
21
+ ##
22
+ ## A file the scan skipped is either missing from the tree or recorded with no class name against
23
+ ## it, and both come back as a class the editor does not have, which is the whole of the question.
24
+
25
+ var _editor_plugin: EditorPlugin = null
26
+
27
+
28
+ func set_editor_plugin(plugin: EditorPlugin) -> void:
29
+ _editor_plugin = plugin
30
+
31
+
32
+ func global_classes(_args: Dictionary) -> Dictionary:
33
+ if not _editor_plugin:
34
+ return {"ok": false, "error": "Editor plugin unavailable"}
35
+
36
+ var filesystem: EditorFileSystem = EditorInterface.get_resource_filesystem()
37
+ var names: PackedStringArray = []
38
+ _walk(filesystem.get_filesystem(), names)
39
+ names.sort()
40
+ return {
41
+ "ok": true,
42
+ "classes": names,
43
+ "scanning": filesystem.is_scanning(),
44
+ "importing": filesystem.is_importing(),
45
+ }
46
+
47
+
48
+ func _walk(directory: EditorFileSystemDirectory, into: PackedStringArray) -> void:
49
+ if directory == null:
50
+ return
51
+ for index: int in range(directory.get_file_count()):
52
+ var declared: String = directory.get_file_script_class_name(index)
53
+ if not declared.is_empty():
54
+ into.append(declared)
55
+ for index: int in range(directory.get_subdir_count()):
56
+ _walk(directory.get_subdir(index), into)
@@ -0,0 +1 @@
1
+ uid://bodtvclhs3toc
@@ -45,6 +45,17 @@ func set_project_setting(params: Dictionary) -> Dictionary:
45
45
  old_value = ProjectSettings.get_setting(setting_path)
46
46
 
47
47
  var final_value: Variant = _values.deserialize_value(value)
48
+ var wanted: int = _wanted_type(setting_path, old_value if had_value else null)
49
+ if wanted > TYPE_NIL and final_value != null and typeof(final_value) != wanted:
50
+ if not _converts_faithfully(final_value, wanted):
51
+ return _log.failure(
52
+ (
53
+ "%s is declared as %s, and %s (%s) cannot be written as that without losing something."
54
+ % [setting_path, type_string(wanted), str(final_value), type_string(typeof(final_value))]
55
+ )
56
+ )
57
+ final_value = type_convert(final_value, wanted)
58
+
48
59
  ProjectSettings.set_setting(setting_path, final_value)
49
60
  var err: Error = ProjectSettings.save()
50
61
  if err != OK:
@@ -59,6 +70,33 @@ func set_project_setting(params: Dictionary) -> Dictionary:
59
70
  }
60
71
 
61
72
 
73
+ # The type a setting wants, or TYPE_NIL where the engine names none and there is nothing to go on.
74
+ #
75
+ # JSON carries one number type, so every number arrives as a float and an int setting was written
76
+ # to project.godot as "50.0". Every reader casts it back, so the tool, the editor and the running
77
+ # game all answer 50 afterwards and only the committed file says otherwise, where it reads as
78
+ # something having gone wrong.
79
+ #
80
+ # The engine's own property list is what is asked, rather than the value that is there: a setting
81
+ # already written as "50.0" reads back as a float, so the value on hand agrees with the mistake and
82
+ # no project that has made it once could be set right again. The property list still says int.
83
+ # A setting the engine does not declare has only its current value to go on, and a new one has
84
+ # neither, which is the caller's type to choose.
85
+ func _wanted_type(setting_path: String, current: Variant) -> int:
86
+ for info: Dictionary in ProjectSettings.get_property_list():
87
+ if str(info.get("name", "")) == setting_path:
88
+ return int(info.get("type", TYPE_NIL))
89
+ return typeof(current)
90
+
91
+
92
+ # What separates a narrowing from a guess: 50.0 through int and out again is 50.0, so it was an int
93
+ # all along, while 50.5 comes back as 50.0 and "loud" comes back as "0". Those are a caller meaning
94
+ # something the setting cannot hold, and are refused rather than written as whatever the cast
95
+ # happened to produce.
96
+ func _converts_faithfully(value: Variant, to: int) -> bool:
97
+ return type_convert(type_convert(value, to), typeof(value)) == value
98
+
99
+
62
100
  # Add an autoload singleton
63
101
  func add_autoload(params: Dictionary) -> Dictionary:
64
102
  var name: String = str(params.get("name", ""))
package/build/index.js CHANGED
@@ -24758,6 +24758,9 @@ import { join as join3 } from "node:path";
24758
24758
  function declaredClasses(projectPath) {
24759
24759
  const declared = new Map;
24760
24760
  const visit = (directory, prefix) => {
24761
+ if (existsSync2(join3(directory, ".gdignore"))) {
24762
+ return;
24763
+ }
24761
24764
  for (const entry of readdirSync(directory, { withFileTypes: true })) {
24762
24765
  if (entry.name.startsWith(".")) {
24763
24766
  continue;
@@ -24795,6 +24798,12 @@ function staleClassNames(projectPath) {
24795
24798
  const cached = cachedClasses(projectPath);
24796
24799
  return cached === null ? [...declaredClasses(projectPath).keys()] : staleAgainst(cached, projectPath);
24797
24800
  }
24801
+ function unseenByEditor(projectPath, editorHolds) {
24802
+ const held = new Set(editorHolds);
24803
+ const declared = declaredClasses(projectPath);
24804
+ const source = cachedClasses(projectPath) ?? declared;
24805
+ return [...source].filter(([name]) => !held.has(name) && declared.has(name)).map(([className, path]) => ({ className, path: declared.get(className) ?? path }));
24806
+ }
24798
24807
 
24799
24808
  // src/dap_client.ts
24800
24809
  import { createConnection } from "node:net";
@@ -27981,7 +27990,7 @@ var TOOL_SPECS = [
27981
27990
  uid: { summary: "the UID of one file", requires: ["resourcePath"] },
27982
27991
  refresh_uids: { summary: "resave every resource so UID references are current", requires: [] },
27983
27992
  refresh_classes: {
27984
- summary: "rewrite .godot/global_script_class_cache.cfg from the class_name declarations on disk, for an editor whose list has gone stale",
27993
+ summary: 'rewrite .godot/global_script_class_cache.cfg from the class_name declarations on disk, and name under unseenByEditor any class the editor open on this project still cannot resolve: rewriting the file does not reach the list a running editor loaded, so "added: []" means the file was already right rather than that nothing is wrong',
27985
27994
  requires: []
27986
27995
  }
27987
27996
  }
@@ -28326,7 +28335,7 @@ var TOOL_SPECS = [
28326
28335
  },
28327
28336
  {
28328
28337
  name: "editor_rescan",
28329
- description: "Makes the running editor scan the project filesystem, so files written outside it are picked up. The scan is a change-detecting walk rather than an unconditional reparse, so a file the editor has already recorded and reads as unchanged is one it does not look inside again: a class_name usually becomes visible this way and on one project did not, with the cache on disk still missing it after the scan said it was done. project_import refresh_classes is the one that rebuilds the class list from the declarations on disk, whatever the editor thinks it has seen. Needs the editor connected.",
28338
+ description: "Makes the running editor scan the project filesystem, so files written outside it are picked up. The scan is a change-detecting walk rather than an unconditional reparse, so a file another engine has already imported reads as settled and the walk does not look inside it: its class_name then stays out of the list the editor resolves against, however many times you scan. Any class in that state is named under unseenByEditor, which is what no check on disk can see, since the declaration and the cache are both correct there and only the editor disagrees. The cure is a change to the declaring script, or editor_launch restart; project_import refresh_classes rewrites the cache and does not touch what the editor is holding. Needs the editor connected.",
28330
28339
  parameters: {
28331
28340
  projectPath: PROJECT_PATH,
28332
28341
  timeoutMs: { type: "number", description: "How long to wait for the scan. Default 30000." }
@@ -28871,6 +28880,10 @@ function realPathOr(path) {
28871
28880
  return path;
28872
28881
  }
28873
28882
  }
28883
+ function samePath(one, other) {
28884
+ const settled = (path) => realPathOr(path).replaceAll("\\", "/").replace(/\/+$/, "").toLowerCase();
28885
+ return settled(one) === settled(other);
28886
+ }
28874
28887
  var RUNNER_DIRECTORY = "addons/gdUnit4/";
28875
28888
  function aboveTheRunner(entry) {
28876
28889
  const runner = entry.detail.findIndex((line) => line.includes(RUNNER_DIRECTORY));
@@ -29302,7 +29315,8 @@ class GodotServer {
29302
29315
  async dispatch(tool, op, args) {
29303
29316
  const headless = HEADLESS_OPERATIONS[tool]?.[op];
29304
29317
  if (headless !== undefined) {
29305
- return await this.headless(headless, args);
29318
+ const answered = await this.headless(headless, args);
29319
+ return headless === "refresh_class_cache" && this.godotBridge.isConnected() ? await this.alsoSayWhatTheEditorCannotSee(answered, args) : answered;
29306
29320
  }
29307
29321
  const { op: _op, ...arguments_ } = args;
29308
29322
  const bridge = async (command, extra = {}) => {
@@ -30410,13 +30424,59 @@ class GodotServer {
30410
30424
  const status = asParams(await this.godotBridge.invokeTool("rescan_filesystem", { ...args, statusOnly: true }));
30411
30425
  busy = Boolean(status["scanning"]) || Boolean(status["importing"]);
30412
30426
  }
30427
+ const checked = busy ? { unseen: [] } : await this.classesTheEditorCannotSee(args);
30428
+ const unseen = checked.unseen;
30413
30429
  return this.jsonTextResponse({
30414
- ok: !busy,
30430
+ ok: !busy && unseen.length === 0 && checked.unchecked === undefined,
30415
30431
  stillWorking: busy,
30416
30432
  waitedMs: Date.now() - started,
30417
- note: busy ? "The editor was still scanning or importing when the wait ran out, so new files may not be visible yet." : undefined
30433
+ unseenByEditor: unseen.length > 0 ? unseen : undefined,
30434
+ classesUnchecked: checked.unchecked,
30435
+ note: busy ? "The editor was still scanning or importing when the wait ran out, so new files may not be visible yet." : unseen.length > 0 ? "The scan finished and these classes are still not in the list the editor resolves against, so every use of them reads as an unknown identifier. Its walk skips a file another engine has already imported. Change the declaring script and rescan, or restart the editor with editor_launch restart." : undefined
30436
+ });
30437
+ }
30438
+ async alsoSayWhatTheEditorCannotSee(answered, args) {
30439
+ const first = answered.content[0];
30440
+ if (answered.isError || first?.type !== "text" || typeof first.text !== "string") {
30441
+ return answered;
30442
+ }
30443
+ const checked = await this.classesTheEditorCannotSee(args);
30444
+ if (checked.unseen.length === 0 && checked.unchecked === undefined) {
30445
+ return answered;
30446
+ }
30447
+ return this.jsonTextResponse({
30448
+ ...asParams(JSON.parse(first.text)),
30449
+ unseenByEditor: checked.unseen.length > 0 ? checked.unseen : undefined,
30450
+ classesUnchecked: checked.unchecked,
30451
+ note: checked.unseen.length > 0 ? "The cache on disk is right now, and the editor holding this project is still not resolving these: rewriting the file does not reach the list it already loaded. Change the declaring script and editor_rescan, or restart the editor with editor_launch restart." : undefined
30418
30452
  });
30419
30453
  }
30454
+ async classesTheEditorCannotSee(args) {
30455
+ const projectPath = typeof args["projectPath"] === "string" ? args["projectPath"] : "";
30456
+ if (projectPath === "") {
30457
+ return { unseen: [], unchecked: "no projectPath, so there was nothing to read the cache from" };
30458
+ }
30459
+ const open = this.godotBridge.getStatus().projectPath;
30460
+ if (open === undefined || !samePath(open, projectPath)) {
30461
+ return {
30462
+ unseen: [],
30463
+ unchecked: `the connected editor is open on ${open ?? "a project it did not name"}, not this one`
30464
+ };
30465
+ }
30466
+ let held;
30467
+ try {
30468
+ held = asParams(await this.godotBridge.invokeTool("global_classes", args));
30469
+ } catch (error) {
30470
+ return {
30471
+ unseen: [],
30472
+ unchecked: `the editor would not say what classes it is holding: ${errorMessage(error)}`
30473
+ };
30474
+ }
30475
+ if (!Array.isArray(held["classes"])) {
30476
+ return { unseen: [], unchecked: "the editor answered without a class list in it" };
30477
+ }
30478
+ return { unseen: unseenByEditor(projectPath, held["classes"].map(String)) };
30479
+ }
30420
30480
  async handleViaBridge(toolName, args) {
30421
30481
  if (!this.godotBridge.isConnected()) {
30422
30482
  return this.createErrorResponse("Editor not connected: no Godot editor with the gdharness editor addon has reached this server.", [
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gdharness",
3
- "version": "0.9.7",
3
+ "version": "0.10.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",