gdharness 0.5.6 → 0.5.8

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
@@ -15821,7 +15821,7 @@ var init_tool_definitions = __esm(() => {
15821
15821
  },
15822
15822
  {
15823
15823
  name: "runtime_input",
15824
- description: "Input to the running game: a whole click on a Control named by path, or a raw action, key, mouse button or mouse motion. All of it works headless, where the window is 64 by 64 and the GUI only takes what is inside it.",
15824
+ description: "Input to the running game: a whole click on a Control named by path, typing into whatever has the focus, or a raw action, key, mouse button or mouse motion. All of it works headless, where the window is 64 by 64 and the GUI only takes what is inside it.",
15825
15825
  parameters: {
15826
15826
  projectPath: RUNNING_PROJECT_PATH,
15827
15827
  nodePath: { type: "string", description: "click: the Control to click, at its centre." },
@@ -15829,6 +15829,10 @@ var init_tool_definitions = __esm(() => {
15829
15829
  pressed: { type: "boolean", description: "Press or release. Default true." },
15830
15830
  strength: { type: "number", description: "action: 0 to 1. Default 1." },
15831
15831
  keycode: { type: "string", description: 'key: the key name, such as "Space" or "A".' },
15832
+ text: {
15833
+ type: "string",
15834
+ description: "text: what to type. A newline is Enter and a tab is Tab."
15835
+ },
15832
15836
  shift: { type: "boolean" },
15833
15837
  ctrl: { type: "boolean" },
15834
15838
  alt: { type: "boolean" },
@@ -15851,6 +15855,10 @@ var init_tool_definitions = __esm(() => {
15851
15855
  },
15852
15856
  action: { summary: "press or release an action", requires: ["action"] },
15853
15857
  key: { summary: "press or release a key", requires: ["keycode"] },
15858
+ text: {
15859
+ summary: "type a string wherever the focus is, a character at a time",
15860
+ requires: ["text"]
15861
+ },
15854
15862
  mouse_click: { summary: "one mouse button event at a position", requires: ["x", "y"] },
15855
15863
  mouse_motion: { summary: "move the mouse to a position", requires: ["x", "y"] }
15856
15864
  }
@@ -34164,6 +34172,8 @@ var init_godot_bridge = __esm(() => {
34164
34172
  server.once("error", (error) => {
34165
34173
  if (!settled) {
34166
34174
  settled = true;
34175
+ server.close();
34176
+ godotWss.close();
34167
34177
  reject(error);
34168
34178
  return;
34169
34179
  }
@@ -35730,6 +35740,7 @@ class GodotServer {
35730
35740
  lspClient = null;
35731
35741
  dapClient = null;
35732
35742
  bridgeStartupError = null;
35743
+ bridgeRetry = null;
35733
35744
  lastProjectPath = null;
35734
35745
  shutdownInitiated = false;
35735
35746
  constructor() {
@@ -35786,11 +35797,43 @@ class GodotServer {
35786
35797
  const reason = errorMessage(bridgeError);
35787
35798
  this.bridgeStartupError = code && !reason.includes(code) ? `${code}: ${reason}` : reason;
35788
35799
  console.error(`[SERVER] Warning: Godot Editor Bridge failed to start: ${this.bridgeStartupError}`);
35789
- console.error("[SERVER] Continuing without bridge-backed editor tools.");
35800
+ console.error("[SERVER] Continuing without bridge-backed editor tools, and trying again.");
35801
+ this.keepTryingTheBridge();
35802
+ }
35803
+ }
35804
+ keepTryingTheBridge() {
35805
+ if (this.bridgeRetry !== null || this.shutdownInitiated) {
35806
+ return;
35807
+ }
35808
+ this.bridgeRetry = setInterval(() => {
35809
+ this.tryTheBridgeAgain();
35810
+ }, BRIDGE_RETRY_MS);
35811
+ this.bridgeRetry.unref();
35812
+ }
35813
+ async tryTheBridgeAgain() {
35814
+ if (this.shutdownInitiated) {
35815
+ this.stopTryingTheBridge();
35816
+ return;
35817
+ }
35818
+ try {
35819
+ await this.godotBridge.start();
35820
+ } catch {
35821
+ return;
35822
+ }
35823
+ this.bridgeStartupError = null;
35824
+ this.stopTryingTheBridge();
35825
+ const bridgeStatus = this.godotBridge.getStatus();
35826
+ console.error(`[SERVER] Godot Editor Bridge came up on ${bridgeStatus.host}:${bridgeStatus.port}; editor tools are live.`);
35827
+ }
35828
+ stopTryingTheBridge() {
35829
+ if (this.bridgeRetry !== null) {
35830
+ clearInterval(this.bridgeRetry);
35831
+ this.bridgeRetry = null;
35790
35832
  }
35791
35833
  }
35792
35834
  async cleanup() {
35793
35835
  this.logDebug("Cleaning up resources");
35836
+ this.stopTryingTheBridge();
35794
35837
  if (this.activeProcess) {
35795
35838
  this.activeProcess.process?.kill();
35796
35839
  this.activeProcess = null;
@@ -36612,8 +36655,9 @@ class GodotServer {
36612
36655
  bridgeAvailable: this.bridgeStartupError === null,
36613
36656
  startupError: this.bridgeStartupError,
36614
36657
  staleNote: stale ? addonMismatch(status.addonVersion, SERVER_VERSION) : undefined,
36615
- note: isPortConflict ? "Bridge port is already in use. Another gdharness instance may own the editor bridge, so this server cannot report that editor connection." : undefined,
36616
- suggestion: isPortConflict ? "Stop duplicate gdharness/MCP server instances or re-run the command from the same server process that owns the bridge port." : undefined
36658
+ retryingBridge: this.bridgeRetry === null ? undefined : true,
36659
+ note: isPortConflict ? "Bridge port is already in use. Another gdharness instance owns the editor bridge, so this server cannot reach the editor. Usually the server this one replaced, still on its way out." : undefined,
36660
+ suggestion: isPortConflict ? `This server is asking for the port again every ${BRIDGE_RETRY_MS / 1000}s and takes it the moment the other one lets go, so editor tools come back on their own. Ask again, or end the older gdharness process to have it now.` : undefined
36617
36661
  };
36618
36662
  }
36619
36663
  async editorPlayingState() {
@@ -37091,7 +37135,7 @@ async function runGodotServer() {
37091
37135
  const server = new GodotServer;
37092
37136
  await server.run();
37093
37137
  }
37094
- var UPDATE_NOTICE_EVERY = 500, FEEDBACK_NOTICE_EVERY = 250, run5, __dirname2, EDITOR_RESTART_TIMEOUT_MS = 90000, PATH_SOLUTIONS, PROJECT_FILE_ARGUMENTS, DEBUG_STATE_CALLS, HEADLESS_OPERATIONS, PROJECT_INFO_SECTIONS;
37138
+ 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;
37095
37139
  var init_server3 = __esm(() => {
37096
37140
  init_mcp();
37097
37141
  init_stdio2();
@@ -38433,6 +38477,7 @@ that is running, and the project on disk. ${TOOL_SPECS.length} tools, named \`do
38433
38477
  | Where a control is, and whether it is visible | \`runtime_inspect\` \`find\`, \`rect\` |
38434
38478
  | What a property reads right now | \`runtime_inspect\` \`property\`, \`runtime_invoke\` |
38435
38479
  | Press a button | \`runtime_input click\`, which says what was under the pointer |
38480
+ | Fill in a field | \`runtime_input click\` on it, then \`runtime_input text\` |
38436
38481
  | Wait for something | \`runtime_wait\`, never a sleep |
38437
38482
  | A picture, for a person who asked to see one | \`runtime_capture\` |
38438
38483
 
@@ -0,0 +1 @@
1
+ uid://dfde0vm55ws7m
@@ -19,7 +19,8 @@ var server_url: String = DEFAULT_URL
19
19
  var _is_connected: bool = false
20
20
  var _reconnect_timer: Timer
21
21
  var _current_reconnect_delay: float = RECONNECT_DELAY
22
- var _should_reconnect: bool = true
22
+ ## Nothing reconnects until somebody has asked to connect once.
23
+ var _should_reconnect: bool = false
23
24
  var _project_path: String
24
25
  var _initialized: bool = false
25
26
 
@@ -43,6 +44,11 @@ func _process(_delta: float) -> void:
43
44
  if socket.get_ready_state() == WebSocketPeer.STATE_CLOSED:
44
45
  if _is_connected:
45
46
  _handle_disconnect()
47
+ elif _should_reconnect and _reconnect_timer.is_stopped():
48
+ # A connection refused never opened, so it reaches CLOSED without passing through
49
+ # _handle_disconnect and nothing would ask again. An editor opened before the server
50
+ # is the ordinary way that happens, and it then sat there for the rest of the day.
51
+ _schedule_reconnect()
46
52
  return
47
53
 
48
54
  socket.poll()
@@ -0,0 +1 @@
1
+ uid://t1telvjuccb0
@@ -0,0 +1 @@
1
+ uid://bxebcmjteyrwl
@@ -0,0 +1 @@
1
+ uid://dmjxtcqxfrure
@@ -0,0 +1 @@
1
+ uid://bbmtwsaoi4bh0
@@ -0,0 +1 @@
1
+ uid://bpusjmmbajpxq
@@ -0,0 +1 @@
1
+ uid://d27rjl7bpjvss
@@ -0,0 +1 @@
1
+ uid://bdfggiknkfi0f
@@ -66,6 +66,7 @@ func _init() -> void:
66
66
  "capture_viewport": _capture.capture_viewport,
67
67
  "inject_action": _input.inject_action,
68
68
  "inject_key": _input.inject_key,
69
+ "inject_text": _input.inject_text,
69
70
  "inject_mouse_click": _input.inject_mouse_click,
70
71
  "inject_mouse_motion": _input.inject_mouse_motion,
71
72
  "click": _input.click,
@@ -0,0 +1 @@
1
+ uid://c5crv4n4t7c5
@@ -0,0 +1 @@
1
+ uid://cxhjoq814ox42
@@ -5,6 +5,13 @@ extends RefCounted
5
5
 
6
6
  const Values = preload("runtime_values.gd")
7
7
 
8
+ ## The distance from a capital letter to its small one in Unicode. A keycode holds the capital.
9
+ const TO_SMALL: int = 32
10
+
11
+ ## The two characters a field reads as keys rather than as text.
12
+ const NEWLINE: int = 10
13
+ const TAB: int = 9
14
+
8
15
  var _host: Node
9
16
  var _values: Values
10
17
 
@@ -69,6 +76,12 @@ func inject_key(params: Dictionary) -> Dictionary:
69
76
  event.ctrl_pressed = bool(params.get("ctrl", false))
70
77
  event.alt_pressed = bool(params.get("alt", false))
71
78
 
79
+ # The fourth thing a real event carries, and the only one a text field reads: LineEdit and
80
+ # TextEdit insert `unicode` and never consult the keycode, so an injected key could press any
81
+ # action in the map and still type nothing into a search box. Godot's keycodes for printable
82
+ # keys are the code points themselves, which is what makes this a mapping and not a table.
83
+ event.unicode = _glyph_of(event.keycode, event.shift_pressed)
84
+
72
85
  Input.parse_input_event(event)
73
86
 
74
87
  return {
@@ -79,10 +92,73 @@ func inject_key(params: Dictionary) -> Dictionary:
79
92
  "shift": event.shift_pressed,
80
93
  "ctrl": event.ctrl_pressed,
81
94
  "alt": event.alt_pressed,
95
+ "unicode": event.unicode,
82
96
  "pressed": pressed
83
97
  }
84
98
 
85
99
 
100
+ ## Types [param text] wherever the focus is, one key event per character.
101
+ ##
102
+ ## A key on its own cannot do this and should not try: which character a key produces is the
103
+ ## keyboard layout's business, and shift over a digit is an exclamation mark on one layout and
104
+ ## something else on the next. Given the character instead there is nothing to guess, so a field
105
+ ## can be filled with anything a player could type, this project's own two typefaces included.
106
+ ##
107
+ ## A newline and a tab are the two characters a field reads as keys rather than as text, so they
108
+ ## are sent as those keys and carry no character of their own: typing a name and submitting it is
109
+ ## one call rather than two.
110
+ ##
111
+ ## Pushed into the viewport for the reason [method click] is, and it matters more here: the focus
112
+ ## is what decides where a character lands, so a caller that clicked a field and then typed would
113
+ ## otherwise have both waiting in the same queue with nothing said about the order.
114
+ func inject_text(params: Dictionary) -> Dictionary:
115
+ var text: String = String(params.get("text", ""))
116
+ if text.is_empty():
117
+ return {"type": "error", "message": "text required"}
118
+
119
+ var viewport: Viewport = _host.get_tree().root
120
+ for index: int in text.length():
121
+ var down: InputEventKey = _typed(text.unicode_at(index))
122
+ viewport.push_input(down)
123
+ # The release as well, so nothing is left held down behind the caller.
124
+ var up: InputEventKey = down.duplicate()
125
+ up.pressed = false
126
+ viewport.push_input(up)
127
+
128
+ return {"type": "input_injected", "input_type": "text", "text": text, "characters": text.length()}
129
+
130
+
131
+ ## The key press that produces [param glyph], as a keyboard would send it.
132
+ func _typed(glyph: int) -> InputEventKey:
133
+ var event: InputEventKey = InputEventKey.new()
134
+ event.pressed = true
135
+ if glyph == NEWLINE:
136
+ event.keycode = KEY_ENTER
137
+ elif glyph == TAB:
138
+ event.keycode = KEY_TAB
139
+ else:
140
+ var capital: int = String.chr(glyph).to_upper().unicode_at(0)
141
+ event.keycode = capital as Key
142
+ event.shift_pressed = capital != glyph
143
+ event.unicode = glyph
144
+ event.physical_keycode = event.keycode
145
+ event.key_label = event.keycode
146
+ return event
147
+
148
+
149
+ ## The character a key produces, or nothing for a key that produces none.
150
+ ##
151
+ ## Godot's keycodes below [constant KEY_SPECIAL] are the Unicode code points of the keys that
152
+ ## print something, so the mapping is the value itself. Letters are held as their capitals, which
153
+ ## is the one place the shift a caller asked for changes the answer rather than the key.
154
+ func _glyph_of(keycode: Key, shifted: bool) -> int:
155
+ if keycode >= KEY_SPECIAL:
156
+ return 0
157
+ if keycode >= KEY_A and keycode <= KEY_Z and not shifted:
158
+ return keycode + TO_SMALL
159
+ return keycode
160
+
161
+
86
162
  ## A point the tool schema sends as two flat numbers, or the older form of one [x, y] value.
87
163
  ## Answers a Vector2, or the String that says what was wrong with it.
88
164
  func _read_point(params: Dictionary, x_key: String, y_key: String, pair_key: String) -> Variant:
@@ -0,0 +1 @@
1
+ uid://d1shdw1mlyig7
@@ -0,0 +1 @@
1
+ uid://bueql5kulh1dm
@@ -28,7 +28,16 @@ const SERIALISERS: Dictionary = {
28
28
 
29
29
  ## Converts a Godot value into something JSON can carry. A type with no entry in the table
30
30
  ## passes through as itself, which is what the JSON-native ones want.
31
+ ##
32
+ ## A method or property typed as a class answers a null with a Variant of type OBJECT that has
33
+ ## nothing behind it, and so does one whose object has been freed. Asking either for its class is
34
+ ## a script error, and a script error here costs far more than the one answer: the request is
35
+ ## never replied to at all, and an editor playing the game stops it dead on the error, so every
36
+ ## question after it times out as well. `find_child` for a name nothing has cost a whole session
37
+ ## that way.
31
38
  func serialize(value: Variant) -> Variant:
39
+ if typeof(value) == TYPE_OBJECT and not is_instance_valid(value):
40
+ return null
32
41
  var serialiser: String = SERIALISERS.get(typeof(value), "")
33
42
  return call(serialiser, value) if not serialiser.is_empty() else value
34
43
 
@@ -0,0 +1 @@
1
+ uid://bamdnyhpw2umv
@@ -0,0 +1 @@
1
+ uid://cpx5hgxdn8dwd
@@ -27,7 +27,15 @@ const SERIALISERS: Dictionary = {
27
27
 
28
28
  # Converts a Godot value into something JSON can carry. A type with no entry in the table
29
29
  # passes through as itself, which is what the JSON-native ones want.
30
+ #
31
+ # A property that holds no object is Object-typed and null rather than nil, which is most of what
32
+ # a node's property list is, and an object that has been freed is the same shape again. Both are
33
+ # refused here rather than inside _serialize_object, because a freed one never reaches it: the
34
+ # call itself fails on the argument, with "previously freed is not a subclass of the expected
35
+ # argument class", and a guard behind that is a guard that never runs.
30
36
  func serialize_value(value: Variant) -> Variant:
37
+ if typeof(value) == TYPE_OBJECT and not is_instance_valid(value):
38
+ return null
31
39
  var serialiser: String = SERIALISERS.get(typeof(value), "")
32
40
  return call(serialiser, value) if not serialiser.is_empty() else value
33
41
 
@@ -140,12 +148,7 @@ func _serialize_dictionary(value: Dictionary) -> Dictionary:
140
148
 
141
149
 
142
150
  # A pathless Resource says so rather than inventing an empty path for the caller to load.
143
- #
144
- # A property that holds no object is Object-typed and null rather than nil, which is most of
145
- # what a node's property list is, so this is the branch that runs the most.
146
151
  func _serialize_object(value: Object) -> Variant:
147
- if not is_instance_valid(value):
148
- return null
149
152
  if not value is Resource:
150
153
  return {"_type": "Object", "class": value.get_class()}
151
154
  var resource: Resource = value
package/build/index.js CHANGED
@@ -25468,6 +25468,8 @@ class GodotBridge extends EventEmitter {
25468
25468
  server.once("error", (error) => {
25469
25469
  if (!settled) {
25470
25470
  settled = true;
25471
+ server.close();
25472
+ godotWss.close();
25471
25473
  reject(error);
25472
25474
  return;
25473
25475
  }
@@ -28249,7 +28251,7 @@ var TOOL_SPECS = [
28249
28251
  },
28250
28252
  {
28251
28253
  name: "runtime_input",
28252
- description: "Input to the running game: a whole click on a Control named by path, or a raw action, key, mouse button or mouse motion. All of it works headless, where the window is 64 by 64 and the GUI only takes what is inside it.",
28254
+ description: "Input to the running game: a whole click on a Control named by path, typing into whatever has the focus, or a raw action, key, mouse button or mouse motion. All of it works headless, where the window is 64 by 64 and the GUI only takes what is inside it.",
28253
28255
  parameters: {
28254
28256
  projectPath: RUNNING_PROJECT_PATH,
28255
28257
  nodePath: { type: "string", description: "click: the Control to click, at its centre." },
@@ -28257,6 +28259,10 @@ var TOOL_SPECS = [
28257
28259
  pressed: { type: "boolean", description: "Press or release. Default true." },
28258
28260
  strength: { type: "number", description: "action: 0 to 1. Default 1." },
28259
28261
  keycode: { type: "string", description: 'key: the key name, such as "Space" or "A".' },
28262
+ text: {
28263
+ type: "string",
28264
+ description: "text: what to type. A newline is Enter and a tab is Tab."
28265
+ },
28260
28266
  shift: { type: "boolean" },
28261
28267
  ctrl: { type: "boolean" },
28262
28268
  alt: { type: "boolean" },
@@ -28279,6 +28285,10 @@ var TOOL_SPECS = [
28279
28285
  },
28280
28286
  action: { summary: "press or release an action", requires: ["action"] },
28281
28287
  key: { summary: "press or release a key", requires: ["keycode"] },
28288
+ text: {
28289
+ summary: "type a string wherever the focus is, a character at a time",
28290
+ requires: ["text"]
28291
+ },
28282
28292
  mouse_click: { summary: "one mouse button event at a position", requires: ["x", "y"] },
28283
28293
  mouse_motion: { summary: "move the mouse to a position", requires: ["x", "y"] }
28284
28294
  }
@@ -28405,6 +28415,7 @@ var FEEDBACK_NOTICE_EVERY = 250;
28405
28415
  var run3 = promisify3(execFile3);
28406
28416
  var __dirname2 = dirname2(fileURLToPath2(import.meta.url));
28407
28417
  var EDITOR_RESTART_TIMEOUT_MS = 90000;
28418
+ var BRIDGE_RETRY_MS = 2000;
28408
28419
  var PATH_SOLUTIONS = [
28409
28420
  'Give the path relative to the project, such as "scenes/main.tscn" or "res://scenes/main.tscn"',
28410
28421
  "Point projectPath at the project the file belongs to"
@@ -28511,6 +28522,7 @@ class GodotServer {
28511
28522
  lspClient = null;
28512
28523
  dapClient = null;
28513
28524
  bridgeStartupError = null;
28525
+ bridgeRetry = null;
28514
28526
  lastProjectPath = null;
28515
28527
  shutdownInitiated = false;
28516
28528
  constructor() {
@@ -28567,11 +28579,43 @@ class GodotServer {
28567
28579
  const reason = errorMessage(bridgeError);
28568
28580
  this.bridgeStartupError = code && !reason.includes(code) ? `${code}: ${reason}` : reason;
28569
28581
  console.error(`[SERVER] Warning: Godot Editor Bridge failed to start: ${this.bridgeStartupError}`);
28570
- console.error("[SERVER] Continuing without bridge-backed editor tools.");
28582
+ console.error("[SERVER] Continuing without bridge-backed editor tools, and trying again.");
28583
+ this.keepTryingTheBridge();
28584
+ }
28585
+ }
28586
+ keepTryingTheBridge() {
28587
+ if (this.bridgeRetry !== null || this.shutdownInitiated) {
28588
+ return;
28589
+ }
28590
+ this.bridgeRetry = setInterval(() => {
28591
+ this.tryTheBridgeAgain();
28592
+ }, BRIDGE_RETRY_MS);
28593
+ this.bridgeRetry.unref();
28594
+ }
28595
+ async tryTheBridgeAgain() {
28596
+ if (this.shutdownInitiated) {
28597
+ this.stopTryingTheBridge();
28598
+ return;
28599
+ }
28600
+ try {
28601
+ await this.godotBridge.start();
28602
+ } catch {
28603
+ return;
28604
+ }
28605
+ this.bridgeStartupError = null;
28606
+ this.stopTryingTheBridge();
28607
+ const bridgeStatus = this.godotBridge.getStatus();
28608
+ console.error(`[SERVER] Godot Editor Bridge came up on ${bridgeStatus.host}:${bridgeStatus.port}; editor tools are live.`);
28609
+ }
28610
+ stopTryingTheBridge() {
28611
+ if (this.bridgeRetry !== null) {
28612
+ clearInterval(this.bridgeRetry);
28613
+ this.bridgeRetry = null;
28571
28614
  }
28572
28615
  }
28573
28616
  async cleanup() {
28574
28617
  this.logDebug("Cleaning up resources");
28618
+ this.stopTryingTheBridge();
28575
28619
  if (this.activeProcess) {
28576
28620
  this.activeProcess.process?.kill();
28577
28621
  this.activeProcess = null;
@@ -29393,8 +29437,9 @@ class GodotServer {
29393
29437
  bridgeAvailable: this.bridgeStartupError === null,
29394
29438
  startupError: this.bridgeStartupError,
29395
29439
  staleNote: stale ? addonMismatch(status.addonVersion, SERVER_VERSION) : undefined,
29396
- note: isPortConflict ? "Bridge port is already in use. Another gdharness instance may own the editor bridge, so this server cannot report that editor connection." : undefined,
29397
- suggestion: isPortConflict ? "Stop duplicate gdharness/MCP server instances or re-run the command from the same server process that owns the bridge port." : undefined
29440
+ retryingBridge: this.bridgeRetry === null ? undefined : true,
29441
+ note: isPortConflict ? "Bridge port is already in use. Another gdharness instance owns the editor bridge, so this server cannot reach the editor. Usually the server this one replaced, still on its way out." : undefined,
29442
+ suggestion: isPortConflict ? `This server is asking for the port again every ${BRIDGE_RETRY_MS / 1000}s and takes it the moment the other one lets go, so editor tools come back on their own. Ask again, or end the older gdharness process to have it now.` : undefined
29398
29443
  };
29399
29444
  }
29400
29445
  async editorPlayingState() {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gdharness",
3
- "version": "0.5.6",
3
+ "version": "0.5.8",
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",