dominus-cli 0.5.9 → 0.6.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/dist/index.js +234 -57
- package/dist/index.js.map +1 -1
- package/dist/mcp.js +204 -49
- package/dist/mcp.js.map +1 -1
- package/package.json +1 -1
package/dist/mcp.js
CHANGED
|
@@ -68,7 +68,12 @@ function serializeMessage(msg) {
|
|
|
68
68
|
// src/transport/ws-server.ts
|
|
69
69
|
var DominusServer = class extends EventEmitter {
|
|
70
70
|
wss = null;
|
|
71
|
-
|
|
71
|
+
/** All connected Studio instances, keyed by placeId (0 for unknown) */
|
|
72
|
+
studioClients = /* @__PURE__ */ new Map();
|
|
73
|
+
/** Reverse lookup: ws → placeId */
|
|
74
|
+
wsToPlaceId = /* @__PURE__ */ new Map();
|
|
75
|
+
/** Which placeId to target. 0 = auto-select if only one is connected. */
|
|
76
|
+
activePlaceId = 0;
|
|
72
77
|
controllers = /* @__PURE__ */ new Set();
|
|
73
78
|
pendingRequests = /* @__PURE__ */ new Map();
|
|
74
79
|
relayRequests = /* @__PURE__ */ new Map();
|
|
@@ -117,7 +122,9 @@ var DominusServer = class extends EventEmitter {
|
|
|
117
122
|
this.addController(ws);
|
|
118
123
|
ws.send(serializeMessage(createMessage("controller:welcome", {
|
|
119
124
|
studioConnected: this.isConnected(),
|
|
120
|
-
port: this.port
|
|
125
|
+
port: this.port,
|
|
126
|
+
connections: this.listConnections(),
|
|
127
|
+
activePlaceId: this.activePlaceId
|
|
121
128
|
})));
|
|
122
129
|
return;
|
|
123
130
|
}
|
|
@@ -125,7 +132,7 @@ var DominusServer = class extends EventEmitter {
|
|
|
125
132
|
if (this.controllers.has(ws)) {
|
|
126
133
|
this.handleControllerMessage(ws, msg);
|
|
127
134
|
} else {
|
|
128
|
-
this.handleStudioMessage(msg);
|
|
135
|
+
this.handleStudioMessage(ws, msg);
|
|
129
136
|
}
|
|
130
137
|
} catch (err) {
|
|
131
138
|
this.emit("server:error", err);
|
|
@@ -137,14 +144,22 @@ var DominusServer = class extends EventEmitter {
|
|
|
137
144
|
for (const [id, controllerWs] of this.relayRequests) {
|
|
138
145
|
if (controllerWs === ws) this.relayRequests.delete(id);
|
|
139
146
|
}
|
|
140
|
-
} else
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
this.
|
|
147
|
+
} else {
|
|
148
|
+
const placeId = this.wsToPlaceId.get(ws);
|
|
149
|
+
if (placeId !== void 0) {
|
|
150
|
+
const conn = this.studioClients.get(placeId);
|
|
151
|
+
this.studioClients.delete(placeId);
|
|
152
|
+
this.wsToPlaceId.delete(ws);
|
|
153
|
+
this.emit("studio:disconnected", { placeId, placeName: conn?.placeName });
|
|
154
|
+
this.broadcastToControllers("studio:disconnected", { placeId, placeName: conn?.placeName });
|
|
155
|
+
for (const [id, pending] of this.pendingRequests) {
|
|
156
|
+
clearTimeout(pending.timer);
|
|
157
|
+
pending.reject(new Error(`Studio disconnected (place ${placeId})`));
|
|
158
|
+
this.pendingRequests.delete(id);
|
|
159
|
+
}
|
|
160
|
+
if (this.activePlaceId === placeId) {
|
|
161
|
+
this.activePlaceId = 0;
|
|
162
|
+
}
|
|
148
163
|
}
|
|
149
164
|
}
|
|
150
165
|
});
|
|
@@ -153,19 +168,24 @@ var DominusServer = class extends EventEmitter {
|
|
|
153
168
|
});
|
|
154
169
|
}
|
|
155
170
|
setStudioClient(ws, msg) {
|
|
156
|
-
if (this.studioClient) {
|
|
157
|
-
this.studioClient.ws.close();
|
|
158
|
-
}
|
|
159
171
|
const payload = msg.payload;
|
|
160
|
-
|
|
172
|
+
const placeId = payload.placeId ?? 0;
|
|
173
|
+
const existing = this.studioClients.get(placeId);
|
|
174
|
+
if (existing && existing.ws !== ws) {
|
|
175
|
+
existing.ws.close();
|
|
176
|
+
this.wsToPlaceId.delete(existing.ws);
|
|
177
|
+
}
|
|
178
|
+
const conn = {
|
|
161
179
|
ws,
|
|
162
180
|
connectedAt: Date.now(),
|
|
163
181
|
studioVersion: payload.studioVersion,
|
|
164
|
-
placeId
|
|
182
|
+
placeId,
|
|
165
183
|
placeName: payload.placeName
|
|
166
184
|
};
|
|
167
|
-
this.
|
|
168
|
-
this.
|
|
185
|
+
this.studioClients.set(placeId, conn);
|
|
186
|
+
this.wsToPlaceId.set(ws, placeId);
|
|
187
|
+
this.emit("studio:connected", { ...payload, placeId });
|
|
188
|
+
this.broadcastToControllers("studio:connected", { ...payload, placeId });
|
|
169
189
|
}
|
|
170
190
|
addController(ws) {
|
|
171
191
|
this.controllers.add(ws);
|
|
@@ -180,25 +200,30 @@ var DominusServer = class extends EventEmitter {
|
|
|
180
200
|
}
|
|
181
201
|
}
|
|
182
202
|
handleControllerMessage(controllerWs, msg) {
|
|
183
|
-
|
|
203
|
+
const target = this.resolveTarget();
|
|
204
|
+
if (!target || target.ws.readyState !== WebSocket.OPEN) {
|
|
184
205
|
controllerWs.send(serializeMessage({
|
|
185
206
|
id: msg.id,
|
|
186
207
|
type: StudioMsg.RESPONSE,
|
|
187
|
-
payload: { error:
|
|
208
|
+
payload: { error: this.getTargetError() },
|
|
188
209
|
ts: Date.now()
|
|
189
210
|
}));
|
|
190
211
|
return;
|
|
191
212
|
}
|
|
192
213
|
this.relayRequests.set(msg.id, controllerWs);
|
|
193
|
-
|
|
214
|
+
target.ws.send(serializeMessage(msg));
|
|
194
215
|
}
|
|
195
|
-
handleStudioMessage(msg) {
|
|
216
|
+
handleStudioMessage(ws, msg) {
|
|
196
217
|
if (msg.type === StudioMsg.CONNECTED) {
|
|
197
218
|
const payload = msg.payload;
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
219
|
+
const placeId = this.wsToPlaceId.get(ws);
|
|
220
|
+
if (placeId !== void 0) {
|
|
221
|
+
const conn = this.studioClients.get(placeId);
|
|
222
|
+
if (conn) {
|
|
223
|
+
conn.studioVersion = payload.studioVersion;
|
|
224
|
+
conn.placeId = payload.placeId;
|
|
225
|
+
conn.placeName = payload.placeName;
|
|
226
|
+
}
|
|
202
227
|
}
|
|
203
228
|
this.emit("studio:connected", payload);
|
|
204
229
|
this.broadcastToControllers("studio:connected", payload);
|
|
@@ -223,10 +248,35 @@ var DominusServer = class extends EventEmitter {
|
|
|
223
248
|
this.emit("message", msg);
|
|
224
249
|
this.broadcastToControllers(msg.type, msg.payload);
|
|
225
250
|
}
|
|
251
|
+
/** Resolve which Studio connection to target */
|
|
252
|
+
resolveTarget() {
|
|
253
|
+
if (this.studioClients.size === 0) return null;
|
|
254
|
+
if (this.activePlaceId !== 0) {
|
|
255
|
+
const conn = this.studioClients.get(this.activePlaceId);
|
|
256
|
+
if (conn && conn.ws.readyState === WebSocket.OPEN) return conn;
|
|
257
|
+
return null;
|
|
258
|
+
}
|
|
259
|
+
if (this.studioClients.size === 1) {
|
|
260
|
+
const conn = this.studioClients.values().next().value;
|
|
261
|
+
if (conn.ws.readyState === WebSocket.OPEN) return conn;
|
|
262
|
+
return null;
|
|
263
|
+
}
|
|
264
|
+
return null;
|
|
265
|
+
}
|
|
266
|
+
/** Generate an appropriate error message when no target found */
|
|
267
|
+
getTargetError() {
|
|
268
|
+
if (this.studioClients.size === 0) return "No Studio connection";
|
|
269
|
+
if (this.activePlaceId !== 0) {
|
|
270
|
+
return `Active place ${this.activePlaceId} is not connected. Use set_active_place or list_connections.`;
|
|
271
|
+
}
|
|
272
|
+
const places = [...this.studioClients.values()].map((c) => `${c.placeName ?? "Unknown"} (${c.placeId})`);
|
|
273
|
+
return `Multiple Studio instances connected: ${places.join(", ")}. Use set_active_place to choose one.`;
|
|
274
|
+
}
|
|
226
275
|
sendRequest(type, payload, timeoutMs = 1e4) {
|
|
227
276
|
return new Promise((resolve, reject) => {
|
|
228
|
-
|
|
229
|
-
|
|
277
|
+
const target = this.resolveTarget();
|
|
278
|
+
if (!target || target.ws.readyState !== WebSocket.OPEN) {
|
|
279
|
+
reject(new Error(this.getTargetError()));
|
|
230
280
|
return;
|
|
231
281
|
}
|
|
232
282
|
const msg = createMessage(type, payload);
|
|
@@ -239,31 +289,45 @@ var DominusServer = class extends EventEmitter {
|
|
|
239
289
|
reject,
|
|
240
290
|
timer
|
|
241
291
|
});
|
|
242
|
-
|
|
292
|
+
target.ws.send(serializeMessage(msg));
|
|
243
293
|
});
|
|
244
294
|
}
|
|
245
295
|
sendNotification(type, payload) {
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
}
|
|
296
|
+
const target = this.resolveTarget();
|
|
297
|
+
if (!target || target.ws.readyState !== WebSocket.OPEN) return;
|
|
249
298
|
const msg = createMessage(type, payload);
|
|
250
|
-
|
|
299
|
+
target.ws.send(serializeMessage(msg));
|
|
251
300
|
}
|
|
252
301
|
isConnected() {
|
|
253
|
-
return this.
|
|
302
|
+
return this.resolveTarget() !== null;
|
|
254
303
|
}
|
|
255
304
|
getConnectionInfo() {
|
|
256
|
-
return this.
|
|
305
|
+
return this.resolveTarget();
|
|
306
|
+
}
|
|
307
|
+
listConnections() {
|
|
308
|
+
return [...this.studioClients.values()].filter((c) => c.ws.readyState === WebSocket.OPEN).map((c) => ({
|
|
309
|
+
placeId: c.placeId ?? 0,
|
|
310
|
+
placeName: c.placeName,
|
|
311
|
+
studioVersion: c.studioVersion,
|
|
312
|
+
connectedAt: c.connectedAt
|
|
313
|
+
}));
|
|
314
|
+
}
|
|
315
|
+
getActivePlaceId() {
|
|
316
|
+
return this.activePlaceId;
|
|
317
|
+
}
|
|
318
|
+
setActivePlaceId(placeId) {
|
|
319
|
+
this.activePlaceId = placeId;
|
|
257
320
|
}
|
|
258
321
|
getPort() {
|
|
259
322
|
return this.port;
|
|
260
323
|
}
|
|
261
324
|
stop() {
|
|
262
325
|
return new Promise((resolve) => {
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
this.studioClient = null;
|
|
326
|
+
for (const conn of this.studioClients.values()) {
|
|
327
|
+
conn.ws.close();
|
|
266
328
|
}
|
|
329
|
+
this.studioClients.clear();
|
|
330
|
+
this.wsToPlaceId.clear();
|
|
267
331
|
for (const ws of this.controllers) {
|
|
268
332
|
ws.close();
|
|
269
333
|
}
|
|
@@ -286,7 +350,8 @@ var DominusClient = class extends EventEmitter {
|
|
|
286
350
|
ws = null;
|
|
287
351
|
port;
|
|
288
352
|
studioConnected = false;
|
|
289
|
-
|
|
353
|
+
connections = /* @__PURE__ */ new Map();
|
|
354
|
+
activePlaceId = 0;
|
|
290
355
|
pendingRequests = /* @__PURE__ */ new Map();
|
|
291
356
|
constructor(port = 18088) {
|
|
292
357
|
super();
|
|
@@ -330,6 +395,20 @@ var DominusClient = class extends EventEmitter {
|
|
|
330
395
|
if (msg.type === "controller:welcome") {
|
|
331
396
|
const p = msg.payload;
|
|
332
397
|
this.studioConnected = p.studioConnected;
|
|
398
|
+
if (p.connections) {
|
|
399
|
+
this.connections.clear();
|
|
400
|
+
for (const c of p.connections) {
|
|
401
|
+
this.connections.set(c.placeId, {
|
|
402
|
+
connectedAt: c.connectedAt,
|
|
403
|
+
studioVersion: c.studioVersion,
|
|
404
|
+
placeId: c.placeId,
|
|
405
|
+
placeName: c.placeName
|
|
406
|
+
});
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
if (p.activePlaceId !== void 0) {
|
|
410
|
+
this.activePlaceId = p.activePlaceId;
|
|
411
|
+
}
|
|
333
412
|
if (this.studioConnected) {
|
|
334
413
|
this.emit("studio:connected", {});
|
|
335
414
|
}
|
|
@@ -338,19 +417,27 @@ var DominusClient = class extends EventEmitter {
|
|
|
338
417
|
if (msg.type === "studio:connected") {
|
|
339
418
|
this.studioConnected = true;
|
|
340
419
|
const payload = msg.payload;
|
|
341
|
-
|
|
420
|
+
const placeId = payload.placeId ?? 0;
|
|
421
|
+
this.connections.set(placeId, {
|
|
342
422
|
connectedAt: Date.now(),
|
|
343
423
|
studioVersion: payload.studioVersion,
|
|
344
|
-
placeId
|
|
424
|
+
placeId,
|
|
345
425
|
placeName: payload.placeName
|
|
346
|
-
};
|
|
426
|
+
});
|
|
347
427
|
this.emit("studio:connected", payload);
|
|
348
428
|
return;
|
|
349
429
|
}
|
|
350
430
|
if (msg.type === "studio:disconnected") {
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
this.
|
|
431
|
+
const payload = msg.payload;
|
|
432
|
+
const placeId = payload.placeId ?? 0;
|
|
433
|
+
this.connections.delete(placeId);
|
|
434
|
+
if (this.connections.size === 0) {
|
|
435
|
+
this.studioConnected = false;
|
|
436
|
+
}
|
|
437
|
+
if (this.activePlaceId === placeId) {
|
|
438
|
+
this.activePlaceId = 0;
|
|
439
|
+
}
|
|
440
|
+
this.emit("studio:disconnected", payload);
|
|
354
441
|
return;
|
|
355
442
|
}
|
|
356
443
|
if (msg.type === StudioMsg.RESPONSE) {
|
|
@@ -394,11 +481,27 @@ var DominusClient = class extends EventEmitter {
|
|
|
394
481
|
this.ws.send(serializeMessage(msg));
|
|
395
482
|
}
|
|
396
483
|
isConnected() {
|
|
397
|
-
return this.studioConnected;
|
|
484
|
+
return this.studioConnected && this.connections.size > 0;
|
|
398
485
|
}
|
|
399
486
|
getConnectionInfo() {
|
|
400
|
-
if (
|
|
401
|
-
|
|
487
|
+
if (this.connections.size === 0) return null;
|
|
488
|
+
const target = this.activePlaceId !== 0 ? this.connections.get(this.activePlaceId) : this.connections.values().next().value;
|
|
489
|
+
if (!target) return null;
|
|
490
|
+
return { ...target, ws: this.ws };
|
|
491
|
+
}
|
|
492
|
+
listConnections() {
|
|
493
|
+
return [...this.connections.values()].map((c) => ({
|
|
494
|
+
placeId: c.placeId ?? 0,
|
|
495
|
+
placeName: c.placeName,
|
|
496
|
+
studioVersion: c.studioVersion,
|
|
497
|
+
connectedAt: c.connectedAt
|
|
498
|
+
}));
|
|
499
|
+
}
|
|
500
|
+
getActivePlaceId() {
|
|
501
|
+
return this.activePlaceId;
|
|
502
|
+
}
|
|
503
|
+
setActivePlaceId(placeId) {
|
|
504
|
+
this.activePlaceId = placeId;
|
|
402
505
|
}
|
|
403
506
|
getPort() {
|
|
404
507
|
return this.port;
|
|
@@ -673,6 +776,7 @@ These are built-in lessons that ship with Dominus. Follow them exactly.
|
|
|
673
776
|
- **Inspecting user's selection \u2192 \`get_selection\`, then \`get_properties\` or \`serialize_ui\`**
|
|
674
777
|
- **Reading properties \u2192 \`get_properties\` (NEVER \`run_code\` with dump scripts)**
|
|
675
778
|
- **Browsing the tree \u2192 \`get_explorer\`**
|
|
779
|
+
- **Multiple Studios open \u2192 \`list_connections\` to see all, \`set_active_place\` to target one**
|
|
676
780
|
- Anything exotic or procedural \u2192 \`run_code\` as last resort
|
|
677
781
|
|
|
678
782
|
### NEVER use run_code for:
|
|
@@ -731,7 +835,7 @@ async function startMcpServer() {
|
|
|
731
835
|
}
|
|
732
836
|
const mcp = new McpServer({
|
|
733
837
|
name: "dominus",
|
|
734
|
-
version: "0.
|
|
838
|
+
version: "0.6.0"
|
|
735
839
|
}, {
|
|
736
840
|
instructions: INTERNAL_MEMORY
|
|
737
841
|
});
|
|
@@ -1319,6 +1423,57 @@ ${" ".repeat(_depth)}}`;
|
|
|
1319
1423
|
return { content: [{ type: "text", text: code }] };
|
|
1320
1424
|
}
|
|
1321
1425
|
);
|
|
1426
|
+
mcp.tool(
|
|
1427
|
+
"list_connections",
|
|
1428
|
+
"List all Roblox Studio instances currently connected to Dominus. Shows placeId, placeName, and connection time for each. Use this when working with multiple Studio windows.",
|
|
1429
|
+
{},
|
|
1430
|
+
async () => {
|
|
1431
|
+
const connections = bridge.listConnections();
|
|
1432
|
+
const activePlaceId = bridge.getActivePlaceId();
|
|
1433
|
+
return {
|
|
1434
|
+
content: [{
|
|
1435
|
+
type: "text",
|
|
1436
|
+
text: JSON.stringify({
|
|
1437
|
+
activePlaceId: activePlaceId || "auto",
|
|
1438
|
+
connections: connections.map((c) => ({
|
|
1439
|
+
...c,
|
|
1440
|
+
active: activePlaceId === 0 && connections.length === 1 ? true : c.placeId === activePlaceId
|
|
1441
|
+
}))
|
|
1442
|
+
}, null, 2)
|
|
1443
|
+
}]
|
|
1444
|
+
};
|
|
1445
|
+
}
|
|
1446
|
+
);
|
|
1447
|
+
mcp.tool(
|
|
1448
|
+
"set_active_place",
|
|
1449
|
+
"Set which Roblox Studio instance to target when multiple are connected. Pass placeId=0 to reset to auto mode (works when only one is connected). Call list_connections first to see available placeIds.",
|
|
1450
|
+
{
|
|
1451
|
+
placeId: z.number().describe("The placeId to target. Use 0 to reset to auto-select.")
|
|
1452
|
+
},
|
|
1453
|
+
async ({ placeId }) => {
|
|
1454
|
+
const connections = bridge.listConnections();
|
|
1455
|
+
if (placeId !== 0 && !connections.some((c) => c.placeId === placeId)) {
|
|
1456
|
+
const available = connections.map((c) => `${c.placeName ?? "Unknown"} (${c.placeId})`).join(", ");
|
|
1457
|
+
return {
|
|
1458
|
+
content: [{
|
|
1459
|
+
type: "text",
|
|
1460
|
+
text: `Place ${placeId} is not connected. Available: ${available || "none"}`
|
|
1461
|
+
}]
|
|
1462
|
+
};
|
|
1463
|
+
}
|
|
1464
|
+
bridge.setActivePlaceId(placeId);
|
|
1465
|
+
if (placeId === 0) {
|
|
1466
|
+
return { content: [{ type: "text", text: "Active place reset to auto-select." }] };
|
|
1467
|
+
}
|
|
1468
|
+
const target = connections.find((c) => c.placeId === placeId);
|
|
1469
|
+
return {
|
|
1470
|
+
content: [{
|
|
1471
|
+
type: "text",
|
|
1472
|
+
text: `Active place set to ${target?.placeName ?? "Unknown"} (${placeId})`
|
|
1473
|
+
}]
|
|
1474
|
+
};
|
|
1475
|
+
}
|
|
1476
|
+
);
|
|
1322
1477
|
mcp.tool(
|
|
1323
1478
|
"recall_memory",
|
|
1324
1479
|
"Search persistent memory for facts about the current Roblox project",
|