gopeak 2.3.4 → 2.3.6

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
@@ -39,6 +39,14 @@ npm install -g gopeak
39
39
  gopeak
40
40
  ```
41
41
 
42
+ Optional shell hooks for update notifications are now **opt-in**:
43
+
44
+ ```bash
45
+ gopeak setup
46
+ ```
47
+
48
+ > `gopeak setup` only modifies supported bash/zsh rc files when you run it explicitly. `npm install` no longer installs shell hooks automatically.
49
+
42
50
  ### 2) Add MCP client config
43
51
 
44
52
  ```json
@@ -136,6 +144,20 @@ In `compact` mode, 78 additional tools are organized into **22 groups** that act
136
144
  > "Use `tool.groups` to reset all active groups."
137
145
 
138
146
  The server sends `notifications/tools/list_changed` so MCP clients (Claude Code, Claude Desktop) automatically refresh the tool list.
147
+ If your MCP client caches tools aggressively and does not refresh after activation, reconnect the client or call the newly activated tool directly once to force a fresh `tools/list` round-trip.
148
+
149
+ ### Typed property values for scene tools
150
+
151
+ Bridge-backed scene tools (`add_node`, `set_node_properties`) now coerce common vector payloads such as `{ "x": 100, "y": 200 }` and `[100, 200]` for typed properties like `position` and `scale`. Tagged values are still the safest cross-tool form:
152
+
153
+ ```json
154
+ {
155
+ "position": { "type": "Vector2", "x": 100, "y": 200 },
156
+ "scale": { "type": "Vector2", "x": 2, "y": 2 }
157
+ }
158
+ ```
159
+
160
+ The internal headless serializer uses `_type`, but MCP callers should prefer `type` when they need an explicit cross-tool Godot value tag.
139
161
 
140
162
  ### Don't worry about tokens
141
163
 
@@ -168,6 +190,12 @@ npm install -g gopeak
168
190
  gopeak
169
191
  ```
170
192
 
193
+ Optional shell hooks for update notifications remain available via:
194
+
195
+ ```bash
196
+ gopeak setup
197
+ ```
198
+
171
199
  ### C) From source
172
200
 
173
201
  ```bash
@@ -201,10 +229,12 @@ GitHub Actions runs on push/PR and executes:
201
229
  2. `npx tsc --noEmit`
202
230
  3. `npm run smoke`
203
231
 
204
- Run the same checks locally:
232
+ Run the same checks locally before opening a PR:
205
233
 
206
234
  ```bash
207
235
  npm run ci
236
+ npm run test:dynamic-groups
237
+ npm run test:integration
208
238
  ```
209
239
 
210
240
  ---
@@ -337,6 +367,7 @@ Visualize your entire project architecture with `visualizer.map` (`map_project`
337
367
  - **Project path invalid** → confirm `project.godot` exists
338
368
  - **Runtime tools not working** → install/enable runtime addon plugin
339
369
  - **Need a tool that is not visible** → run `tool.catalog` to search and auto-activate matching groups, or use `tool.groups` to activate a specific group
370
+ - **`get_editor_status` says disconnected while the Godot editor shows connected** → check whether another `gopeak`/MCP server instance already owns bridge port `6505`; the status payload now reports the startup error and suggests stopping duplicate servers
340
371
 
341
372
  ---
342
373
 
@@ -81,49 +81,92 @@ func _find_node(root: Node, path: String) -> Node:
81
81
  return root.get_node_or_null(path)
82
82
 
83
83
 
84
- func _parse_value(value):
85
- if typeof(value) == TYPE_DICTIONARY and value.has("type"):
86
- match value["type"]:
87
- "Vector2":
88
- return Vector2(value.get("x", 0), value.get("y", 0))
89
- "Vector3":
90
- return Vector3(value.get("x", 0), value.get("y", 0), value.get("z", 0))
91
- "Color":
92
- return Color(value.get("r", 1), value.get("g", 1), value.get("b", 1), value.get("a", 1))
93
- "Vector2i":
94
- return Vector2i(value.get("x", 0), value.get("y", 0))
95
- "Vector3i":
96
- return Vector3i(value.get("x", 0), value.get("y", 0), value.get("z", 0))
97
- "Rect2":
98
- return Rect2(value.get("x", 0), value.get("y", 0), value.get("width", 0), value.get("height", 0))
99
- "Transform2D":
100
- if value.has("x") and value.has("y") and value.has("origin"):
101
- var xx: Dictionary = value["x"]
102
- var yy: Dictionary = value["y"]
103
- var oo: Dictionary = value["origin"]
104
- return Transform2D(
105
- Vector2(xx.get("x", 1), xx.get("y", 0)),
106
- Vector2(yy.get("x", 0), yy.get("y", 1)),
107
- Vector2(oo.get("x", 0), oo.get("y", 0))
108
- )
109
- "Transform3D":
110
- if value.has("basis") and value.has("origin"):
111
- var b: Dictionary = value["basis"]
112
- var o: Dictionary = value["origin"]
113
- var basis := Basis(
114
- Vector3(b.get("x", {}).get("x", 1), b.get("x", {}).get("y", 0), b.get("x", {}).get("z", 0)),
115
- Vector3(b.get("y", {}).get("x", 0), b.get("y", {}).get("y", 1), b.get("y", {}).get("z", 0)),
116
- Vector3(b.get("z", {}).get("x", 0), b.get("z", {}).get("y", 0), b.get("z", {}).get("z", 1))
117
- )
118
- return Transform3D(basis, Vector3(o.get("x", 0), o.get("y", 0), o.get("z", 0)))
119
- "NodePath":
120
- return NodePath(value.get("path", ""))
121
- "Resource":
122
- var resource_path: String = str(value.get("path", ""))
123
- if resource_path.is_empty():
124
- return null
125
- return load(resource_path)
84
+ func _parse_value(value, expected_type: int = TYPE_NIL):
85
+ if typeof(value) == TYPE_DICTIONARY:
86
+ var type_tag := ""
87
+ if value.has("type"):
88
+ type_tag = str(value["type"])
89
+ elif value.has("_type"):
90
+ type_tag = str(value["_type"])
91
+
92
+ if not type_tag.is_empty():
93
+ match type_tag:
94
+ "Vector2":
95
+ return Vector2(value.get("x", 0), value.get("y", 0))
96
+ "Vector3":
97
+ return Vector3(value.get("x", 0), value.get("y", 0), value.get("z", 0))
98
+ "Color":
99
+ return Color(value.get("r", 1), value.get("g", 1), value.get("b", 1), value.get("a", 1))
100
+ "Vector2i":
101
+ return Vector2i(value.get("x", 0), value.get("y", 0))
102
+ "Vector3i":
103
+ return Vector3i(value.get("x", 0), value.get("y", 0), value.get("z", 0))
104
+ "Rect2":
105
+ return Rect2(value.get("x", 0), value.get("y", 0), value.get("width", 0), value.get("height", 0))
106
+ "Transform2D":
107
+ if value.has("x") and value.has("y") and value.has("origin"):
108
+ var xx: Dictionary = value["x"]
109
+ var yy: Dictionary = value["y"]
110
+ var oo: Dictionary = value["origin"]
111
+ return Transform2D(
112
+ Vector2(xx.get("x", 1), xx.get("y", 0)),
113
+ Vector2(yy.get("x", 0), yy.get("y", 1)),
114
+ Vector2(oo.get("x", 0), oo.get("y", 0))
115
+ )
116
+ "Transform3D":
117
+ if value.has("basis") and value.has("origin"):
118
+ var b: Dictionary = value["basis"]
119
+ var o: Dictionary = value["origin"]
120
+ var basis := Basis(
121
+ Vector3(b.get("x", {}).get("x", 1), b.get("x", {}).get("y", 0), b.get("x", {}).get("z", 0)),
122
+ Vector3(b.get("y", {}).get("x", 0), b.get("y", {}).get("y", 1), b.get("y", {}).get("z", 0)),
123
+ Vector3(b.get("z", {}).get("x", 0), b.get("z", {}).get("y", 0), b.get("z", {}).get("z", 1))
124
+ )
125
+ return Transform3D(basis, Vector3(o.get("x", 0), o.get("y", 0), o.get("z", 0)))
126
+ "NodePath":
127
+ return NodePath(value.get("path", ""))
128
+ "Resource":
129
+ var resource_path: String = str(value.get("path", ""))
130
+ if resource_path.is_empty():
131
+ return null
132
+ return load(resource_path)
133
+
134
+ match expected_type:
135
+ TYPE_VECTOR2:
136
+ if value.has("x") and value.has("y"):
137
+ return Vector2(value.get("x", 0), value.get("y", 0))
138
+ TYPE_VECTOR2I:
139
+ if value.has("x") and value.has("y"):
140
+ return Vector2i(value.get("x", 0), value.get("y", 0))
141
+ TYPE_VECTOR3:
142
+ if value.has("x") and value.has("y") and value.has("z"):
143
+ return Vector3(value.get("x", 0), value.get("y", 0), value.get("z", 0))
144
+ TYPE_VECTOR3I:
145
+ if value.has("x") and value.has("y") and value.has("z"):
146
+ return Vector3i(value.get("x", 0), value.get("y", 0), value.get("z", 0))
147
+ TYPE_COLOR:
148
+ if value.has("r") and value.has("g") and value.has("b"):
149
+ return Color(value.get("r", 1), value.get("g", 1), value.get("b", 1), value.get("a", 1))
150
+ TYPE_RECT2:
151
+ if value.has("x") and value.has("y") and value.has("width") and value.has("height"):
152
+ return Rect2(value.get("x", 0), value.get("y", 0), value.get("width", 0), value.get("height", 0))
153
+ TYPE_NODE_PATH:
154
+ if value.has("path"):
155
+ return NodePath(value.get("path", ""))
126
156
  if typeof(value) == TYPE_ARRAY:
157
+ match expected_type:
158
+ TYPE_VECTOR2:
159
+ if value.size() >= 2:
160
+ return Vector2(value[0], value[1])
161
+ TYPE_VECTOR2I:
162
+ if value.size() >= 2:
163
+ return Vector2i(value[0], value[1])
164
+ TYPE_VECTOR3:
165
+ if value.size() >= 3:
166
+ return Vector3(value[0], value[1], value[2])
167
+ TYPE_VECTOR3I:
168
+ if value.size() >= 3:
169
+ return Vector3i(value[0], value[1], value[2])
127
170
  var result: Array = []
128
171
  for item in value:
129
172
  result.append(_parse_value(item))
@@ -131,6 +174,13 @@ func _parse_value(value):
131
174
  return value
132
175
 
133
176
 
177
+ func _get_property_type(node: Node, prop_name: String) -> int:
178
+ for prop in node.get_property_list():
179
+ if str(prop.get("name", "")) == prop_name:
180
+ return int(prop.get("type", TYPE_NIL))
181
+ return TYPE_NIL
182
+
183
+
134
184
  func _serialize_value(value) -> Variant:
135
185
  match typeof(value):
136
186
  TYPE_VECTOR2:
@@ -174,7 +224,8 @@ func _serialize_value(value) -> Variant:
174
224
 
175
225
  func _set_node_properties(node: Node, properties: Dictionary) -> void:
176
226
  for prop_name in properties:
177
- var val = _parse_value(properties[prop_name])
227
+ var expected_type := _get_property_type(node, str(prop_name))
228
+ var val = _parse_value(properties[prop_name], expected_type)
178
229
  node.set(prop_name, val)
179
230
 
180
231
 
@@ -45,6 +45,9 @@ func _process(_delta: float) -> void:
45
45
  continue
46
46
 
47
47
  client.poll()
48
+ if client.get_status() != StreamPeerTCP.STATUS_CONNECTED:
49
+ clients_to_remove.append(client)
50
+ continue
48
51
  var available = client.get_available_bytes()
49
52
  if available > 0:
50
53
  var data = client.get_utf8_string(available)
@@ -335,10 +335,13 @@ export class GodotBridge extends EventEmitter {
335
335
  nextSocket.on('close', (code, reasonBuffer) => {
336
336
  const reason = reasonBuffer.toString();
337
337
  this.log('warn', `Godot disconnected (code=${code}, reason=${reason || 'none'})`);
338
- this.handleDisconnect(new Error('Godot disconnected during request'));
338
+ this.handleDisconnect(nextSocket, new Error('Godot disconnected during request'));
339
339
  });
340
340
  nextSocket.on('error', (error) => {
341
341
  this.log('error', `WebSocket error: ${error.message}`);
342
+ if (nextSocket.readyState === WebSocket.CLOSED || nextSocket.readyState === WebSocket.CLOSING) {
343
+ this.handleDisconnect(nextSocket, error);
344
+ }
342
345
  });
343
346
  }
344
347
  handleRawMessage(data) {
@@ -463,7 +466,11 @@ export class GodotBridge extends EventEmitter {
463
466
  clearInterval(this.pingInterval);
464
467
  this.pingInterval = null;
465
468
  }
466
- handleDisconnect(reason) {
469
+ handleDisconnect(disconnectedSocket, reason) {
470
+ if (disconnectedSocket && this.socket && disconnectedSocket !== this.socket) {
471
+ this.log('debug', 'Ignoring stale Godot socket disconnect event');
472
+ return;
473
+ }
467
474
  this.stopKeepalive();
468
475
  this.socket = null;
469
476
  this.connectionInfo = null;