gdharness 1.2.42 → 1.3.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.
@@ -518,7 +518,7 @@ func _handle_message(json_string: String) -> void:
518
518
 
519
519
 
520
520
  ## Says [param tool_name] was not run because the editor is scanning or importing, for the server to
521
- ## ask again once that is over. An older server reads it as an ordinary refusal.
521
+ ## ask again once that is over.
522
522
  func send_busy(request_id: String, tool_name: String) -> void:
523
523
  _send_message(
524
524
  {
@@ -600,18 +600,13 @@ func shown(value: Variant) -> String:
600
600
  ## not mine". A tag that names a type but lacks the keys to build it is left unhandled on purpose,
601
601
  ## so the caller can still read it against the type the property declares.
602
602
  func _parse_tagged_dictionary(value: Dictionary) -> Array:
603
- var type_tag: String = str(value.get("_type", value.get("type", "")))
603
+ var type_tag: String = str(value.get("_type", ""))
604
604
  if type_tag == "Resource":
605
605
  return _parse_resource_tag(value)
606
606
 
607
607
  # The shared builder is the same one the answer was written by, so every shape it writes comes
608
- # back as itself. `type` as the tag is read as `_type`: it is what these tools answered with
609
- # before the serialisers were made one, and a caller who kept an answer can still send it.
610
- var tagged: Dictionary = value
611
- if not value.has("_type") and not type_tag.is_empty():
612
- tagged = value.duplicate()
613
- tagged["_type"] = type_tag
614
- var rebuilt: Variant = _values.deserialize_value(tagged)
608
+ # back as itself.
609
+ var rebuilt: Variant = _values.deserialize_value(value)
615
610
  if not rebuilt is Dictionary:
616
611
  return [true, rebuilt]
617
612
 
@@ -660,7 +655,7 @@ func _parse_new_resource(type_tag: String, value: Dictionary) -> Array:
660
655
  var wanted: Dictionary = {}
661
656
  for key: Variant in value:
662
657
  var property: String = str(key)
663
- if property != "_type" and property != "type":
658
+ if property != "_type":
664
659
  wanted[property] = value[key]
665
660
  var refused: String = write_all(built, wanted)
666
661
  if not refused.is_empty():
@@ -304,10 +304,10 @@ func read_resource(args: Dictionary) -> Dictionary:
304
304
 
305
305
 
306
306
  func create_shader(args: Dictionary) -> Dictionary:
307
- var shader_path: String = _ensure_res_path(str(args.get("shaderPath", "")))
307
+ var shader_path: String = _ensure_res_path(str(args.get("resourcePath", "")))
308
308
  var shader_type: String = str(args.get("shaderType", "canvas_item"))
309
309
  if shader_path == "res://":
310
- return {"ok": false, "error": "shaderPath is required"}
310
+ return {"ok": false, "error": "resourcePath is required"}
311
311
  var taken: String = _taken(shader_path, "resource_edit op=create_shader", "Edit its text")
312
312
  if not taken.is_empty():
313
313
  return {"ok": false, "error": taken}
@@ -336,9 +336,9 @@ func create_shader(args: Dictionary) -> Dictionary:
336
336
 
337
337
 
338
338
  func create_tileset(args: Dictionary) -> Dictionary:
339
- var tileset_path: String = _ensure_res_path(str(args.get("tilesetPath", "")))
339
+ var tileset_path: String = _ensure_res_path(str(args.get("resourcePath", "")))
340
340
  if tileset_path == "res://":
341
- return {"ok": false, "error": "tilesetPath is required"}
341
+ return {"ok": false, "error": "resourcePath is required"}
342
342
  var taken: String = _taken(tileset_path, "resource_edit op=create_tileset")
343
343
  if not taken.is_empty():
344
344
  return {"ok": false, "error": taken}
@@ -478,7 +478,7 @@ func _source_ids(tile_set: TileSet) -> Array[int]:
478
478
 
479
479
  func set_tilemap_cells(args: Dictionary) -> Dictionary:
480
480
  var scene_path: String = _ensure_res_path(str(args.get("scenePath", "")))
481
- var node_path: String = str(args.get("tilemapNodePath", ""))
481
+ var node_path: String = str(args.get("nodePath", ""))
482
482
  if scene_path == "res://":
483
483
  return {"ok": false, "error": "scenePath is required"}
484
484
 
@@ -702,9 +702,9 @@ func _save_theme(theme: Theme, theme_path: String, item: String) -> String:
702
702
 
703
703
 
704
704
  func set_theme_color(args: Dictionary) -> Dictionary:
705
- var theme_path: String = _ensure_res_path(str(args.get("themePath", "")))
705
+ var theme_path: String = _ensure_res_path(str(args.get("resourcePath", "")))
706
706
  if theme_path == "res://":
707
- return {"ok": false, "error": "themePath is required"}
707
+ return {"ok": false, "error": "resourcePath is required"}
708
708
 
709
709
  var color_name: String = str(args.get("colorName", ""))
710
710
  var control_type: String = str(args.get("controlType", ""))
@@ -745,9 +745,9 @@ func set_theme_color(args: Dictionary) -> Dictionary:
745
745
 
746
746
 
747
747
  func set_theme_font_size(args: Dictionary) -> Dictionary:
748
- var theme_path: String = _ensure_res_path(str(args.get("themePath", "")))
748
+ var theme_path: String = _ensure_res_path(str(args.get("resourcePath", "")))
749
749
  if theme_path == "res://":
750
- return {"ok": false, "error": "themePath is required"}
750
+ return {"ok": false, "error": "resourcePath is required"}
751
751
 
752
752
  var font_size_name: String = str(args.get("fontSizeName", ""))
753
753
  var control_type: String = str(args.get("controlType", ""))
@@ -410,7 +410,7 @@ func duplicate_node(args: Dictionary) -> Dictionary:
410
410
  var scene_path: String = _to_scene_res_path(project_path, str(args.get("scenePath", "")))
411
411
  var node_path: String = str(args.get("nodePath", ""))
412
412
  var new_name: String = str(args.get("newName", ""))
413
- var parent_path: String = str(args.get("parentPath", ""))
413
+ var parent_path: String = str(args.get("parentNodePath", ""))
414
414
 
415
415
  if node_path.is_empty() or new_name.is_empty():
416
416
  return {"ok": false, "error": "Missing nodePath or newName"}
@@ -23,7 +23,7 @@ const Typing = preload("runtime_typing.gd")
23
23
  const Values = preload("runtime_values.gd")
24
24
  const Waits = preload("runtime_waits.gd")
25
25
 
26
- const PROTOCOL: int = 2
26
+ const PROTOCOL: int = 3
27
27
  const DEFAULT_BIND_ADDRESS: String = "127.0.0.1"
28
28
  const BIND_ADDRESS_SETTING: String = "gdharness/runtime/bind_address"
29
29
  ## 0 asks the operating system for a free port, which is the default and what the server
@@ -502,7 +502,7 @@ func _announce(bind_address: String) -> void:
502
502
  # Written whole under a name no server reads and moved into place, so no look ever meets
503
503
  # the file empty: a server sweeping the directory between an open and a write took the
504
504
  # announcement for a dead game's leftover, and the directory is shared by every server on
505
- # the machine, older ones included.
505
+ # the machine.
506
506
  var staged: String = path + ".tmp"
507
507
  var file: FileAccess = FileAccess.open(staged, FileAccess.WRITE)
508
508
  if file == null:
@@ -36,6 +36,35 @@ func capture_viewport(params: Dictionary) -> Dictionary:
36
36
  if not node is Viewport:
37
37
  return {"type": "error", "message": "Node is not a Viewport: " + viewport_path}
38
38
  var viewport: Viewport = node
39
+ # The root's own state is refused in _capture. Another window of the game's that is hidden or
40
+ # minimised holds no frame, and asking it for its image makes the engine log the empty read into
41
+ # the game's output, so the run would stop unclean for an error the capture raised.
42
+ if viewport is Window and viewport != _host.get_tree().root:
43
+ var window: Window = viewport
44
+ if not window.visible:
45
+ return {
46
+ "type": "error",
47
+ "message":
48
+ (
49
+ (
50
+ "The Window at %s is hidden, so nothing is drawn to it. Show it (its visible "
51
+ + "property) and ask again."
52
+ )
53
+ % viewport_path
54
+ ),
55
+ }
56
+ if not window.can_draw():
57
+ return {
58
+ "type": "error",
59
+ "message":
60
+ (
61
+ (
62
+ "Nothing is being drawn to the Window at %s, which is minimised. Restore it and "
63
+ + "ask again."
64
+ )
65
+ % viewport_path
66
+ ),
67
+ }
39
68
  if not viewport is SubViewport:
40
69
  return await _capture(viewport, params)
41
70
  # Drawn now rather than taken as it stands. A SubViewport the game does not redraw every frame
@@ -284,7 +284,7 @@ func _glyph_of(keycode: Key, shifted: bool) -> int:
284
284
  ## `pressed` is how a caller asks for one half, the way a drag is made: true holds the button down,
285
285
  ## false lets go of one being held.
286
286
  func inject_mouse_click(params: Dictionary) -> Dictionary:
287
- var point: Variant = Mouse.read_point(params, "x", "y", "position")
287
+ var point: Variant = Mouse.read_point(params, "x", "y")
288
288
  if point is String:
289
289
  return {"type": "error", "message": point}
290
290
  var position: Vector2 = point
@@ -349,7 +349,7 @@ func inject_mouse_click(params: Dictionary) -> Dictionary:
349
349
  ## would have carried. A relative the caller gives is kept, since a game may want to read a motion
350
350
  ## the position does not show.
351
351
  func inject_mouse_motion(params: Dictionary) -> Dictionary:
352
- var point: Variant = Mouse.read_point(params, "x", "y", "position")
352
+ var point: Variant = Mouse.read_point(params, "x", "y")
353
353
  if point is String:
354
354
  return {"type": "error", "message": point}
355
355
  var position: Vector2 = point
@@ -358,11 +358,6 @@ func inject_mouse_motion(params: Dictionary) -> Dictionary:
358
358
  relative = Vector2(
359
359
  Read.as_float(params.get("relativeX", 0.0)), Read.as_float(params.get("relativeY", 0.0))
360
360
  )
361
- elif params.has("relative"):
362
- var movement: Variant = Mouse.read_point(params, "relativeX", "relativeY", "relative")
363
- if movement is String:
364
- return {"type": "error", "message": movement}
365
- relative = movement
366
361
 
367
362
  var supplied: bool = _follow_in_the_window(position)
368
363
  Input.parse_input_event(_motion(position, relative))
@@ -6,20 +6,12 @@ extends RefCounted
6
6
  const Read = preload("reading.gd")
7
7
 
8
8
 
9
- ## A point the tool schema sends as two flat numbers, or the older form of one [x, y] value.
10
- ## Answers a Vector2, or the String that says what was wrong with it.
11
- static func read_point(params: Dictionary, x_key: String, y_key: String, pair_key: String) -> Variant:
12
- if params.has(x_key) and params.has(y_key):
13
- return Vector2(Read.as_float(params[x_key]), Read.as_float(params[y_key]))
14
- var raw: Variant = params.get(pair_key, Vector2.ZERO)
15
- if raw is Vector2:
16
- return raw
17
- if raw is Array:
18
- var pair: Array = raw
19
- if pair.size() < 2:
20
- return "%s array must contain [x, y]" % pair_key
21
- return Vector2(Read.as_float(pair[0]), Read.as_float(pair[1]))
22
- return "%s must be Vector2 or [x, y]" % pair_key
9
+ ## A point the tool schema sends as two flat numbers. Answers a Vector2, or the String that says
10
+ ## which was missing.
11
+ static func read_point(params: Dictionary, x_key: String, y_key: String) -> Variant:
12
+ if not params.has(x_key) or not params.has(y_key):
13
+ return "%s and %s are both needed." % [x_key, y_key]
14
+ return Vector2(Read.as_float(params[x_key]), Read.as_float(params[y_key]))
23
15
 
24
16
 
25
17
  ## The points of a drag: x, y, then each of via, then toX, toY. Answers the String saying what is
@@ -600,18 +600,13 @@ func shown(value: Variant) -> String:
600
600
  ## not mine". A tag that names a type but lacks the keys to build it is left unhandled on purpose,
601
601
  ## so the caller can still read it against the type the property declares.
602
602
  func _parse_tagged_dictionary(value: Dictionary) -> Array:
603
- var type_tag: String = str(value.get("_type", value.get("type", "")))
603
+ var type_tag: String = str(value.get("_type", ""))
604
604
  if type_tag == "Resource":
605
605
  return _parse_resource_tag(value)
606
606
 
607
607
  # The shared builder is the same one the answer was written by, so every shape it writes comes
608
- # back as itself. `type` as the tag is read as `_type`: it is what these tools answered with
609
- # before the serialisers were made one, and a caller who kept an answer can still send it.
610
- var tagged: Dictionary = value
611
- if not value.has("_type") and not type_tag.is_empty():
612
- tagged = value.duplicate()
613
- tagged["_type"] = type_tag
614
- var rebuilt: Variant = _values.deserialize_value(tagged)
608
+ # back as itself.
609
+ var rebuilt: Variant = _values.deserialize_value(value)
615
610
  if not rebuilt is Dictionary:
616
611
  return [true, rebuilt]
617
612
 
@@ -660,7 +655,7 @@ func _parse_new_resource(type_tag: String, value: Dictionary) -> Array:
660
655
  var wanted: Dictionary = {}
661
656
  for key: Variant in value:
662
657
  var property: String = str(key)
663
- if property != "_type" and property != "type":
658
+ if property != "_type":
664
659
  wanted[property] = value[key]
665
660
  var refused: String = write_all(built, wanted)
666
661
  if not refused.is_empty():
package/build/keeper.js CHANGED
@@ -1428,17 +1428,23 @@ function recordAt(path) {
1428
1428
  return null;
1429
1429
  }
1430
1430
  const args = fields["arguments"];
1431
+ const startedBy = fields["startedBy"];
1432
+ const projectPath = fields["projectPath"];
1433
+ const command = fields["command"];
1434
+ if (typeof startedBy !== "number" || typeof projectPath !== "string" || projectPath === "" || typeof command !== "string" || !Array.isArray(args)) {
1435
+ return null;
1436
+ }
1431
1437
  return {
1432
1438
  pid,
1433
1439
  transcript,
1434
1440
  startedAt,
1435
- ...typeof fields["startedBy"] === "number" ? { startedBy: fields["startedBy"] } : {},
1436
- projectPath: typeof fields["projectPath"] === "string" ? fields["projectPath"] : "",
1441
+ startedBy,
1442
+ projectPath,
1437
1443
  ...typeof fields["servedBy"] === "string" && fields["servedBy"] !== "" ? { servedBy: fields["servedBy"] } : {},
1438
1444
  ...typeof fields["keeper"] === "string" && fields["keeper"] !== "" ? { keeper: fields["keeper"] } : {},
1439
1445
  ...typeof fields["profile"] === "string" && fields["profile"] !== "" ? { profile: fields["profile"] } : {},
1440
- arguments: Array.isArray(args) ? args.filter((value) => typeof value === "string") : [],
1441
- ...typeof fields["command"] === "string" ? { command: fields["command"] } : {},
1446
+ arguments: args.filter((value) => typeof value === "string"),
1447
+ command,
1442
1448
  ...typeof fields["exitCode"] === "number" ? { exitCode: fields["exitCode"] } : {},
1443
1449
  ...typeof fields["exitSignal"] === "string" ? { exitSignal: fields["exitSignal"] } : {},
1444
1450
  ...typeof fields["endedAt"] === "number" ? { endedAt: fields["endedAt"] } : {}
package/package.json CHANGED
@@ -1,10 +1,9 @@
1
1
  {
2
2
  "name": "gdharness",
3
- "version": "1.2.42",
3
+ "version": "1.3.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",
7
- "main": "./build/index.js",
8
7
  "bin": {
9
8
  "gdharness": "build/cli.js"
10
9
  },