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 CHANGED
@@ -252,7 +252,12 @@ var init_ws_server = __esm({
252
252
  init_protocol();
253
253
  DominusServer = class extends EventEmitter {
254
254
  wss = null;
255
- studioClient = null;
255
+ /** All connected Studio instances, keyed by placeId (0 for unknown) */
256
+ studioClients = /* @__PURE__ */ new Map();
257
+ /** Reverse lookup: ws → placeId */
258
+ wsToPlaceId = /* @__PURE__ */ new Map();
259
+ /** Which placeId to target. 0 = auto-select if only one is connected. */
260
+ activePlaceId = 0;
256
261
  controllers = /* @__PURE__ */ new Set();
257
262
  pendingRequests = /* @__PURE__ */ new Map();
258
263
  relayRequests = /* @__PURE__ */ new Map();
@@ -301,7 +306,9 @@ var init_ws_server = __esm({
301
306
  this.addController(ws);
302
307
  ws.send(serializeMessage(createMessage("controller:welcome", {
303
308
  studioConnected: this.isConnected(),
304
- port: this.port
309
+ port: this.port,
310
+ connections: this.listConnections(),
311
+ activePlaceId: this.activePlaceId
305
312
  })));
306
313
  return;
307
314
  }
@@ -309,7 +316,7 @@ var init_ws_server = __esm({
309
316
  if (this.controllers.has(ws)) {
310
317
  this.handleControllerMessage(ws, msg);
311
318
  } else {
312
- this.handleStudioMessage(msg);
319
+ this.handleStudioMessage(ws, msg);
313
320
  }
314
321
  } catch (err) {
315
322
  this.emit("server:error", err);
@@ -321,14 +328,22 @@ var init_ws_server = __esm({
321
328
  for (const [id, controllerWs] of this.relayRequests) {
322
329
  if (controllerWs === ws) this.relayRequests.delete(id);
323
330
  }
324
- } else if (this.studioClient?.ws === ws) {
325
- this.studioClient = null;
326
- this.emit("studio:disconnected");
327
- this.broadcastToControllers("studio:disconnected", {});
328
- for (const [id, pending] of this.pendingRequests) {
329
- clearTimeout(pending.timer);
330
- pending.reject(new Error("Studio disconnected"));
331
- this.pendingRequests.delete(id);
331
+ } else {
332
+ const placeId = this.wsToPlaceId.get(ws);
333
+ if (placeId !== void 0) {
334
+ const conn = this.studioClients.get(placeId);
335
+ this.studioClients.delete(placeId);
336
+ this.wsToPlaceId.delete(ws);
337
+ this.emit("studio:disconnected", { placeId, placeName: conn?.placeName });
338
+ this.broadcastToControllers("studio:disconnected", { placeId, placeName: conn?.placeName });
339
+ for (const [id, pending] of this.pendingRequests) {
340
+ clearTimeout(pending.timer);
341
+ pending.reject(new Error(`Studio disconnected (place ${placeId})`));
342
+ this.pendingRequests.delete(id);
343
+ }
344
+ if (this.activePlaceId === placeId) {
345
+ this.activePlaceId = 0;
346
+ }
332
347
  }
333
348
  }
334
349
  });
@@ -337,19 +352,24 @@ var init_ws_server = __esm({
337
352
  });
338
353
  }
339
354
  setStudioClient(ws, msg) {
340
- if (this.studioClient) {
341
- this.studioClient.ws.close();
342
- }
343
355
  const payload = msg.payload;
344
- this.studioClient = {
356
+ const placeId = payload.placeId ?? 0;
357
+ const existing = this.studioClients.get(placeId);
358
+ if (existing && existing.ws !== ws) {
359
+ existing.ws.close();
360
+ this.wsToPlaceId.delete(existing.ws);
361
+ }
362
+ const conn = {
345
363
  ws,
346
364
  connectedAt: Date.now(),
347
365
  studioVersion: payload.studioVersion,
348
- placeId: payload.placeId,
366
+ placeId,
349
367
  placeName: payload.placeName
350
368
  };
351
- this.emit("studio:connected", payload);
352
- this.broadcastToControllers("studio:connected", payload);
369
+ this.studioClients.set(placeId, conn);
370
+ this.wsToPlaceId.set(ws, placeId);
371
+ this.emit("studio:connected", { ...payload, placeId });
372
+ this.broadcastToControllers("studio:connected", { ...payload, placeId });
353
373
  }
354
374
  addController(ws) {
355
375
  this.controllers.add(ws);
@@ -364,25 +384,30 @@ var init_ws_server = __esm({
364
384
  }
365
385
  }
366
386
  handleControllerMessage(controllerWs, msg) {
367
- if (!this.studioClient || this.studioClient.ws.readyState !== WebSocket.OPEN) {
387
+ const target = this.resolveTarget();
388
+ if (!target || target.ws.readyState !== WebSocket.OPEN) {
368
389
  controllerWs.send(serializeMessage({
369
390
  id: msg.id,
370
391
  type: StudioMsg.RESPONSE,
371
- payload: { error: "Studio not connected" },
392
+ payload: { error: this.getTargetError() },
372
393
  ts: Date.now()
373
394
  }));
374
395
  return;
375
396
  }
376
397
  this.relayRequests.set(msg.id, controllerWs);
377
- this.studioClient.ws.send(serializeMessage(msg));
398
+ target.ws.send(serializeMessage(msg));
378
399
  }
379
- handleStudioMessage(msg) {
400
+ handleStudioMessage(ws, msg) {
380
401
  if (msg.type === StudioMsg.CONNECTED) {
381
402
  const payload = msg.payload;
382
- if (this.studioClient) {
383
- this.studioClient.studioVersion = payload.studioVersion;
384
- this.studioClient.placeId = payload.placeId;
385
- this.studioClient.placeName = payload.placeName;
403
+ const placeId = this.wsToPlaceId.get(ws);
404
+ if (placeId !== void 0) {
405
+ const conn = this.studioClients.get(placeId);
406
+ if (conn) {
407
+ conn.studioVersion = payload.studioVersion;
408
+ conn.placeId = payload.placeId;
409
+ conn.placeName = payload.placeName;
410
+ }
386
411
  }
387
412
  this.emit("studio:connected", payload);
388
413
  this.broadcastToControllers("studio:connected", payload);
@@ -407,10 +432,35 @@ var init_ws_server = __esm({
407
432
  this.emit("message", msg);
408
433
  this.broadcastToControllers(msg.type, msg.payload);
409
434
  }
435
+ /** Resolve which Studio connection to target */
436
+ resolveTarget() {
437
+ if (this.studioClients.size === 0) return null;
438
+ if (this.activePlaceId !== 0) {
439
+ const conn = this.studioClients.get(this.activePlaceId);
440
+ if (conn && conn.ws.readyState === WebSocket.OPEN) return conn;
441
+ return null;
442
+ }
443
+ if (this.studioClients.size === 1) {
444
+ const conn = this.studioClients.values().next().value;
445
+ if (conn.ws.readyState === WebSocket.OPEN) return conn;
446
+ return null;
447
+ }
448
+ return null;
449
+ }
450
+ /** Generate an appropriate error message when no target found */
451
+ getTargetError() {
452
+ if (this.studioClients.size === 0) return "No Studio connection";
453
+ if (this.activePlaceId !== 0) {
454
+ return `Active place ${this.activePlaceId} is not connected. Use set_active_place or list_connections.`;
455
+ }
456
+ const places = [...this.studioClients.values()].map((c) => `${c.placeName ?? "Unknown"} (${c.placeId})`);
457
+ return `Multiple Studio instances connected: ${places.join(", ")}. Use set_active_place to choose one.`;
458
+ }
410
459
  sendRequest(type, payload, timeoutMs = 1e4) {
411
460
  return new Promise((resolve, reject) => {
412
- if (!this.studioClient || this.studioClient.ws.readyState !== WebSocket.OPEN) {
413
- reject(new Error("No Studio connection"));
461
+ const target = this.resolveTarget();
462
+ if (!target || target.ws.readyState !== WebSocket.OPEN) {
463
+ reject(new Error(this.getTargetError()));
414
464
  return;
415
465
  }
416
466
  const msg = createMessage(type, payload);
@@ -423,31 +473,45 @@ var init_ws_server = __esm({
423
473
  reject,
424
474
  timer
425
475
  });
426
- this.studioClient.ws.send(serializeMessage(msg));
476
+ target.ws.send(serializeMessage(msg));
427
477
  });
428
478
  }
429
479
  sendNotification(type, payload) {
430
- if (!this.studioClient || this.studioClient.ws.readyState !== WebSocket.OPEN) {
431
- return;
432
- }
480
+ const target = this.resolveTarget();
481
+ if (!target || target.ws.readyState !== WebSocket.OPEN) return;
433
482
  const msg = createMessage(type, payload);
434
- this.studioClient.ws.send(serializeMessage(msg));
483
+ target.ws.send(serializeMessage(msg));
435
484
  }
436
485
  isConnected() {
437
- return this.studioClient !== null && this.studioClient.ws.readyState === WebSocket.OPEN;
486
+ return this.resolveTarget() !== null;
438
487
  }
439
488
  getConnectionInfo() {
440
- return this.studioClient;
489
+ return this.resolveTarget();
490
+ }
491
+ listConnections() {
492
+ return [...this.studioClients.values()].filter((c) => c.ws.readyState === WebSocket.OPEN).map((c) => ({
493
+ placeId: c.placeId ?? 0,
494
+ placeName: c.placeName,
495
+ studioVersion: c.studioVersion,
496
+ connectedAt: c.connectedAt
497
+ }));
498
+ }
499
+ getActivePlaceId() {
500
+ return this.activePlaceId;
501
+ }
502
+ setActivePlaceId(placeId) {
503
+ this.activePlaceId = placeId;
441
504
  }
442
505
  getPort() {
443
506
  return this.port;
444
507
  }
445
508
  stop() {
446
509
  return new Promise((resolve) => {
447
- if (this.studioClient) {
448
- this.studioClient.ws.close();
449
- this.studioClient = null;
510
+ for (const conn of this.studioClients.values()) {
511
+ conn.ws.close();
450
512
  }
513
+ this.studioClients.clear();
514
+ this.wsToPlaceId.clear();
451
515
  for (const ws of this.controllers) {
452
516
  ws.close();
453
517
  }
@@ -494,7 +558,8 @@ var init_ws_client = __esm({
494
558
  ws = null;
495
559
  port;
496
560
  studioConnected = false;
497
- connectionInfo = null;
561
+ connections = /* @__PURE__ */ new Map();
562
+ activePlaceId = 0;
498
563
  pendingRequests = /* @__PURE__ */ new Map();
499
564
  constructor(port = 18088) {
500
565
  super();
@@ -538,6 +603,20 @@ var init_ws_client = __esm({
538
603
  if (msg.type === "controller:welcome") {
539
604
  const p = msg.payload;
540
605
  this.studioConnected = p.studioConnected;
606
+ if (p.connections) {
607
+ this.connections.clear();
608
+ for (const c of p.connections) {
609
+ this.connections.set(c.placeId, {
610
+ connectedAt: c.connectedAt,
611
+ studioVersion: c.studioVersion,
612
+ placeId: c.placeId,
613
+ placeName: c.placeName
614
+ });
615
+ }
616
+ }
617
+ if (p.activePlaceId !== void 0) {
618
+ this.activePlaceId = p.activePlaceId;
619
+ }
541
620
  if (this.studioConnected) {
542
621
  this.emit("studio:connected", {});
543
622
  }
@@ -546,19 +625,27 @@ var init_ws_client = __esm({
546
625
  if (msg.type === "studio:connected") {
547
626
  this.studioConnected = true;
548
627
  const payload = msg.payload;
549
- this.connectionInfo = {
628
+ const placeId = payload.placeId ?? 0;
629
+ this.connections.set(placeId, {
550
630
  connectedAt: Date.now(),
551
631
  studioVersion: payload.studioVersion,
552
- placeId: payload.placeId,
632
+ placeId,
553
633
  placeName: payload.placeName
554
- };
634
+ });
555
635
  this.emit("studio:connected", payload);
556
636
  return;
557
637
  }
558
638
  if (msg.type === "studio:disconnected") {
559
- this.studioConnected = false;
560
- this.connectionInfo = null;
561
- this.emit("studio:disconnected");
639
+ const payload = msg.payload;
640
+ const placeId = payload.placeId ?? 0;
641
+ this.connections.delete(placeId);
642
+ if (this.connections.size === 0) {
643
+ this.studioConnected = false;
644
+ }
645
+ if (this.activePlaceId === placeId) {
646
+ this.activePlaceId = 0;
647
+ }
648
+ this.emit("studio:disconnected", payload);
562
649
  return;
563
650
  }
564
651
  if (msg.type === StudioMsg.RESPONSE) {
@@ -602,11 +689,27 @@ var init_ws_client = __esm({
602
689
  this.ws.send(serializeMessage(msg));
603
690
  }
604
691
  isConnected() {
605
- return this.studioConnected;
692
+ return this.studioConnected && this.connections.size > 0;
606
693
  }
607
694
  getConnectionInfo() {
608
- if (!this.connectionInfo) return null;
609
- return { ...this.connectionInfo, ws: this.ws };
695
+ if (this.connections.size === 0) return null;
696
+ const target = this.activePlaceId !== 0 ? this.connections.get(this.activePlaceId) : this.connections.values().next().value;
697
+ if (!target) return null;
698
+ return { ...target, ws: this.ws };
699
+ }
700
+ listConnections() {
701
+ return [...this.connections.values()].map((c) => ({
702
+ placeId: c.placeId ?? 0,
703
+ placeName: c.placeName,
704
+ studioVersion: c.studioVersion,
705
+ connectedAt: c.connectedAt
706
+ }));
707
+ }
708
+ getActivePlaceId() {
709
+ return this.activePlaceId;
710
+ }
711
+ setActivePlaceId(placeId) {
712
+ this.activePlaceId = placeId;
610
713
  }
611
714
  getPort() {
612
715
  return this.port;
@@ -683,6 +786,7 @@ These are built-in lessons that ship with Dominus. Follow them exactly.
683
786
  - **Inspecting user's selection \u2192 \`get_selection\`, then \`get_properties\` or \`serialize_ui\`**
684
787
  - **Reading properties \u2192 \`get_properties\` (NEVER \`run_code\` with dump scripts)**
685
788
  - **Browsing the tree \u2192 \`get_explorer\`**
789
+ - **Multiple Studios open \u2192 \`list_connections\` to see all, \`set_active_place\` to target one**
686
790
  - Anything exotic or procedural \u2192 \`run_code\` as last resort
687
791
 
688
792
  ### NEVER use run_code for:
@@ -2605,7 +2709,7 @@ async function startMcpServer() {
2605
2709
  }
2606
2710
  const mcp = new McpServer({
2607
2711
  name: "dominus",
2608
- version: "0.5.8"
2712
+ version: "0.6.0"
2609
2713
  }, {
2610
2714
  instructions: INTERNAL_MEMORY
2611
2715
  });
@@ -3193,6 +3297,57 @@ ${" ".repeat(_depth)}}`;
3193
3297
  return { content: [{ type: "text", text: code }] };
3194
3298
  }
3195
3299
  );
3300
+ mcp.tool(
3301
+ "list_connections",
3302
+ "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.",
3303
+ {},
3304
+ async () => {
3305
+ const connections = bridge.listConnections();
3306
+ const activePlaceId = bridge.getActivePlaceId();
3307
+ return {
3308
+ content: [{
3309
+ type: "text",
3310
+ text: JSON.stringify({
3311
+ activePlaceId: activePlaceId || "auto",
3312
+ connections: connections.map((c) => ({
3313
+ ...c,
3314
+ active: activePlaceId === 0 && connections.length === 1 ? true : c.placeId === activePlaceId
3315
+ }))
3316
+ }, null, 2)
3317
+ }]
3318
+ };
3319
+ }
3320
+ );
3321
+ mcp.tool(
3322
+ "set_active_place",
3323
+ "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.",
3324
+ {
3325
+ placeId: z.number().describe("The placeId to target. Use 0 to reset to auto-select.")
3326
+ },
3327
+ async ({ placeId }) => {
3328
+ const connections = bridge.listConnections();
3329
+ if (placeId !== 0 && !connections.some((c) => c.placeId === placeId)) {
3330
+ const available = connections.map((c) => `${c.placeName ?? "Unknown"} (${c.placeId})`).join(", ");
3331
+ return {
3332
+ content: [{
3333
+ type: "text",
3334
+ text: `Place ${placeId} is not connected. Available: ${available || "none"}`
3335
+ }]
3336
+ };
3337
+ }
3338
+ bridge.setActivePlaceId(placeId);
3339
+ if (placeId === 0) {
3340
+ return { content: [{ type: "text", text: "Active place reset to auto-select." }] };
3341
+ }
3342
+ const target = connections.find((c) => c.placeId === placeId);
3343
+ return {
3344
+ content: [{
3345
+ type: "text",
3346
+ text: `Active place set to ${target?.placeName ?? "Unknown"} (${placeId})`
3347
+ }]
3348
+ };
3349
+ }
3350
+ );
3196
3351
  mcp.tool(
3197
3352
  "recall_memory",
3198
3353
  "Search persistent memory for facts about the current Roblox project",
@@ -3487,11 +3642,20 @@ var Setup = ({ onComplete }) => {
3487
3642
  var StatusBar = React6.memo(({
3488
3643
  connected,
3489
3644
  placeName,
3645
+ connectionCount,
3490
3646
  model,
3491
3647
  sessionTokens
3492
3648
  }) => {
3493
3649
  const statusColor = connected ? "green" : "red";
3494
- const statusText = connected ? `\u25CF Connected${placeName ? ` \u2014 ${placeName}` : ""}` : "\u25CB Disconnected";
3650
+ const count = connectionCount ?? (connected ? 1 : 0);
3651
+ let statusText;
3652
+ if (!connected) {
3653
+ statusText = "\u25CB Disconnected";
3654
+ } else if (count > 1) {
3655
+ statusText = `\u25CF ${count} Studios${placeName ? ` \u2014 active: ${placeName}` : ""}`;
3656
+ } else {
3657
+ statusText = `\u25CF Connected${placeName ? ` \u2014 ${placeName}` : ""}`;
3658
+ }
3495
3659
  const shortModel = model.split("/").pop() ?? model;
3496
3660
  return /* @__PURE__ */ jsxs(
3497
3661
  Box,
@@ -3909,6 +4073,7 @@ var ToolCallView = React6.memo(({ calls }) => {
3909
4073
  var Layout = React6.memo(({
3910
4074
  connected,
3911
4075
  placeName,
4076
+ connectionCount,
3912
4077
  model,
3913
4078
  chatEntries,
3914
4079
  outputEntries,
@@ -3922,7 +4087,7 @@ var Layout = React6.memo(({
3922
4087
  onClearImage
3923
4088
  }) => {
3924
4089
  return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", width: "100%", children: [
3925
- /* @__PURE__ */ jsx(StatusBar, { connected, placeName, model }),
4090
+ /* @__PURE__ */ jsx(StatusBar, { connected, placeName, connectionCount, model }),
3926
4091
  /* @__PURE__ */ jsxs(Box, { flexGrow: 1, children: [
3927
4092
  /* @__PURE__ */ jsx(ExplorerPane, { tree: explorerTree }),
3928
4093
  /* @__PURE__ */ jsxs(Box, { flexDirection: "column", flexGrow: 1, children: [
@@ -3948,10 +4113,12 @@ var Layout = React6.memo(({
3948
4113
  init_protocol();
3949
4114
  function useStudio(server) {
3950
4115
  const connInfo = server.getConnectionInfo();
4116
+ const connections = server.listConnections();
3951
4117
  const [state, setState] = useState({
3952
4118
  connected: server.isConnected(),
3953
4119
  placeName: connInfo?.placeName,
3954
4120
  placeId: connInfo?.placeId,
4121
+ connectionCount: connections.length,
3955
4122
  explorerTree: [],
3956
4123
  outputLog: []
3957
4124
  });
@@ -3960,27 +4127,36 @@ function useStudio(server) {
3960
4127
  useEffect(() => {
3961
4128
  if (server.isConnected() && !stateRef.current.connected) {
3962
4129
  const info = server.getConnectionInfo();
4130
+ const conns = server.listConnections();
3963
4131
  setState((prev) => ({
3964
4132
  ...prev,
3965
4133
  connected: true,
3966
4134
  placeName: info?.placeName,
3967
- placeId: info?.placeId
4135
+ placeId: info?.placeId,
4136
+ connectionCount: conns.length
3968
4137
  }));
3969
4138
  }
3970
4139
  const onConnected = (payload) => {
4140
+ const conns = server.listConnections();
4141
+ const info = server.getConnectionInfo();
3971
4142
  setState((prev) => ({
3972
4143
  ...prev,
3973
4144
  connected: true,
3974
- placeName: payload.placeName ?? prev.placeName,
3975
- placeId: payload.placeId ?? prev.placeId
4145
+ placeName: info?.placeName ?? payload.placeName ?? prev.placeName,
4146
+ placeId: info?.placeId ?? payload.placeId ?? prev.placeId,
4147
+ connectionCount: conns.length
3976
4148
  }));
3977
4149
  };
3978
4150
  const onDisconnected = () => {
4151
+ const conns = server.listConnections();
4152
+ const info = server.getConnectionInfo();
4153
+ const stillConnected = conns.length > 0;
3979
4154
  setState((prev) => ({
3980
4155
  ...prev,
3981
- connected: false,
3982
- placeName: void 0,
3983
- placeId: void 0
4156
+ connected: stillConnected,
4157
+ placeName: stillConnected ? info?.placeName : void 0,
4158
+ placeId: stillConnected ? info?.placeId : void 0,
4159
+ connectionCount: conns.length
3984
4160
  }));
3985
4161
  };
3986
4162
  const onExplorer = (payload) => {
@@ -4282,6 +4458,7 @@ var App = ({ server, agent: initialAgent, version }) => {
4282
4458
  {
4283
4459
  connected: studio.connected,
4284
4460
  placeName: studio.placeName,
4461
+ connectionCount: studio.connectionCount,
4285
4462
  model: agent?.getModel() ?? resolveProviderSettings(config).model,
4286
4463
  chatEntries,
4287
4464
  outputEntries: studio.outputLog,