dominus-cli 0.1.0 → 0.2.1

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/mcp.js CHANGED
@@ -271,6 +271,160 @@ var DominusServer = class extends EventEmitter {
271
271
  });
272
272
  }
273
273
  };
274
+ var DominusClient = class extends EventEmitter {
275
+ ws = null;
276
+ port;
277
+ studioConnected = false;
278
+ connectionInfo = null;
279
+ pendingRequests = /* @__PURE__ */ new Map();
280
+ constructor(port = 18088) {
281
+ super();
282
+ this.port = port;
283
+ }
284
+ connect() {
285
+ return new Promise((resolve, reject) => {
286
+ const url = `ws://localhost:${this.port}`;
287
+ const ws = new WebSocket(url);
288
+ ws.on("open", () => {
289
+ this.ws = ws;
290
+ ws.send(serializeMessage(createMessage("controller:connect", {})));
291
+ this.emit("server:started", { port: this.port, relay: true });
292
+ resolve();
293
+ });
294
+ ws.on("message", (raw) => {
295
+ try {
296
+ const msg = parseMessage(raw.toString());
297
+ this.handleMessage(msg);
298
+ } catch (err) {
299
+ this.emit("server:error", err);
300
+ }
301
+ });
302
+ ws.on("close", () => {
303
+ this.ws = null;
304
+ this.studioConnected = false;
305
+ this.emit("studio:disconnected");
306
+ for (const [id, pending] of this.pendingRequests) {
307
+ clearTimeout(pending.timer);
308
+ pending.reject(new Error("Relay connection lost"));
309
+ this.pendingRequests.delete(id);
310
+ }
311
+ });
312
+ ws.on("error", (err) => {
313
+ if (!this.ws) reject(err);
314
+ this.emit("server:error", err);
315
+ });
316
+ });
317
+ }
318
+ handleMessage(msg) {
319
+ if (msg.type === "controller:welcome") {
320
+ const p = msg.payload;
321
+ this.studioConnected = p.studioConnected;
322
+ if (this.studioConnected) {
323
+ this.emit("studio:connected", {});
324
+ }
325
+ return;
326
+ }
327
+ if (msg.type === "studio:connected") {
328
+ this.studioConnected = true;
329
+ const payload = msg.payload;
330
+ this.connectionInfo = {
331
+ connectedAt: Date.now(),
332
+ studioVersion: payload.studioVersion,
333
+ placeId: payload.placeId,
334
+ placeName: payload.placeName
335
+ };
336
+ this.emit("studio:connected", payload);
337
+ return;
338
+ }
339
+ if (msg.type === "studio:disconnected") {
340
+ this.studioConnected = false;
341
+ this.connectionInfo = null;
342
+ this.emit("studio:disconnected");
343
+ return;
344
+ }
345
+ if (msg.type === StudioMsg.RESPONSE) {
346
+ const pending = this.pendingRequests.get(msg.id);
347
+ if (pending) {
348
+ clearTimeout(pending.timer);
349
+ pending.resolve(msg);
350
+ this.pendingRequests.delete(msg.id);
351
+ }
352
+ return;
353
+ }
354
+ this.emit(msg.type, msg.payload);
355
+ this.emit("message", msg);
356
+ }
357
+ sendRequest(type, payload, timeoutMs = 1e4) {
358
+ return new Promise((resolve, reject) => {
359
+ if (!this.ws || this.ws.readyState !== WebSocket.OPEN) {
360
+ reject(new Error("Not connected to Dominus server"));
361
+ return;
362
+ }
363
+ if (!this.studioConnected) {
364
+ reject(new Error("Studio not connected"));
365
+ return;
366
+ }
367
+ const msg = createMessage(type, payload);
368
+ const timer = setTimeout(() => {
369
+ this.pendingRequests.delete(msg.id);
370
+ reject(new Error(`Request timed out: ${type}`));
371
+ }, timeoutMs);
372
+ this.pendingRequests.set(msg.id, {
373
+ resolve,
374
+ reject,
375
+ timer
376
+ });
377
+ this.ws.send(serializeMessage(msg));
378
+ });
379
+ }
380
+ sendNotification(type, payload) {
381
+ if (!this.ws || this.ws.readyState !== WebSocket.OPEN) return;
382
+ const msg = createMessage(type, payload);
383
+ this.ws.send(serializeMessage(msg));
384
+ }
385
+ isConnected() {
386
+ return this.studioConnected;
387
+ }
388
+ getConnectionInfo() {
389
+ if (!this.connectionInfo) return null;
390
+ return { ...this.connectionInfo, ws: this.ws };
391
+ }
392
+ getPort() {
393
+ return this.port;
394
+ }
395
+ stop() {
396
+ return new Promise((resolve) => {
397
+ for (const [, pending] of this.pendingRequests) {
398
+ clearTimeout(pending.timer);
399
+ pending.reject(new Error("Client shutting down"));
400
+ }
401
+ this.pendingRequests.clear();
402
+ if (this.ws) {
403
+ this.ws.close();
404
+ this.ws = null;
405
+ }
406
+ resolve();
407
+ });
408
+ }
409
+ };
410
+ async function isPortInUse(port) {
411
+ return new Promise((resolve) => {
412
+ const ws = new WebSocket(`ws://localhost:${port}`);
413
+ const timer = setTimeout(() => {
414
+ ws.close();
415
+ resolve(false);
416
+ }, 1e3);
417
+ ws.on("open", () => {
418
+ clearTimeout(timer);
419
+ ws.close();
420
+ resolve(true);
421
+ });
422
+ ws.on("error", () => {
423
+ clearTimeout(timer);
424
+ resolve(false);
425
+ });
426
+ });
427
+ }
274
428
  var DOMINUS_DIR = path.join(os.homedir(), ".dominus");
275
429
  var CONFIG_PATH = path.join(DOMINUS_DIR, "config.json");
276
430
  var DB_PATH = path.join(DOMINUS_DIR, "dominus.db");
@@ -458,15 +612,23 @@ function getScriptIndex(projectId) {
458
612
  }
459
613
 
460
614
  // src/mcp/server.ts
461
- var wsServer;
615
+ var bridge;
462
616
  async function startMcpServer() {
463
617
  const config = loadConfig();
464
618
  await initMemoryStore();
465
- wsServer = new DominusServer(config.port);
466
- await wsServer.start();
619
+ const portTaken = await isPortInUse(config.port);
620
+ if (portTaken) {
621
+ const client = new DominusClient(config.port);
622
+ await client.connect();
623
+ bridge = client;
624
+ } else {
625
+ const server = new DominusServer(config.port);
626
+ await server.start();
627
+ bridge = server;
628
+ }
467
629
  const mcp = new McpServer({
468
630
  name: "dominus",
469
- version: "0.1.0"
631
+ version: "0.2.1"
470
632
  });
471
633
  mcp.tool(
472
634
  "read_script",
@@ -474,7 +636,7 @@ async function startMcpServer() {
474
636
  { path: z.string().describe('Full instance path, e.g. "ServerScriptService.GameManager"') },
475
637
  async ({ path: path2 }) => {
476
638
  assertConnected();
477
- const res = await wsServer.sendRequest(CliMsg.GET_SCRIPT, { path: path2 });
639
+ const res = await bridge.sendRequest(CliMsg.GET_SCRIPT, { path: path2 });
478
640
  return { content: [{ type: "text", text: JSON.stringify(res.payload, null, 2) }] };
479
641
  }
480
642
  );
@@ -487,7 +649,7 @@ async function startMcpServer() {
487
649
  },
488
650
  async ({ path: path2, source }) => {
489
651
  assertConnected();
490
- const res = await wsServer.sendRequest(CliMsg.SET_SCRIPT, { path: path2, source });
652
+ const res = await bridge.sendRequest(CliMsg.SET_SCRIPT, { path: path2, source });
491
653
  return { content: [{ type: "text", text: JSON.stringify(res.payload) }] };
492
654
  }
493
655
  );
@@ -497,7 +659,7 @@ async function startMcpServer() {
497
659
  { code: z.string().describe("Luau code to execute") },
498
660
  async ({ code }) => {
499
661
  assertConnected();
500
- const res = await wsServer.sendRequest(CliMsg.RUN_CODE, { code });
662
+ const res = await bridge.sendRequest(CliMsg.RUN_CODE, { code });
501
663
  return { content: [{ type: "text", text: JSON.stringify(res.payload, null, 2) }] };
502
664
  }
503
665
  );
@@ -510,7 +672,7 @@ async function startMcpServer() {
510
672
  },
511
673
  async ({ rootPath, maxDepth }) => {
512
674
  assertConnected();
513
- const res = await wsServer.sendRequest(CliMsg.GET_EXPLORER, {
675
+ const res = await bridge.sendRequest(CliMsg.GET_EXPLORER, {
514
676
  rootPath: rootPath ?? null,
515
677
  maxDepth
516
678
  });
@@ -523,7 +685,7 @@ async function startMcpServer() {
523
685
  { path: z.string().describe("Full instance path") },
524
686
  async ({ path: path2 }) => {
525
687
  assertConnected();
526
- const res = await wsServer.sendRequest(CliMsg.GET_PROPERTIES, { path: path2 });
688
+ const res = await bridge.sendRequest(CliMsg.GET_PROPERTIES, { path: path2 });
527
689
  return { content: [{ type: "text", text: JSON.stringify(res.payload, null, 2) }] };
528
690
  }
529
691
  );
@@ -536,7 +698,7 @@ async function startMcpServer() {
536
698
  },
537
699
  async ({ path: path2, properties }) => {
538
700
  assertConnected();
539
- const res = await wsServer.sendRequest(CliMsg.SET_PROPERTIES, { path: path2, properties });
701
+ const res = await bridge.sendRequest(CliMsg.SET_PROPERTIES, { path: path2, properties });
540
702
  return { content: [{ type: "text", text: JSON.stringify(res.payload) }] };
541
703
  }
542
704
  );
@@ -551,7 +713,7 @@ async function startMcpServer() {
551
713
  },
552
714
  async ({ className, parentPath, name, properties }) => {
553
715
  assertConnected();
554
- const res = await wsServer.sendRequest(CliMsg.INSERT_INSTANCE, {
716
+ const res = await bridge.sendRequest(CliMsg.INSERT_INSTANCE, {
555
717
  className,
556
718
  parentPath,
557
719
  name,
@@ -566,7 +728,7 @@ async function startMcpServer() {
566
728
  { path: z.string().describe("Full instance path to delete") },
567
729
  async ({ path: path2 }) => {
568
730
  assertConnected();
569
- const res = await wsServer.sendRequest(CliMsg.DELETE_INSTANCE, { path: path2 });
731
+ const res = await bridge.sendRequest(CliMsg.DELETE_INSTANCE, { path: path2 });
570
732
  return { content: [{ type: "text", text: JSON.stringify(res.payload) }] };
571
733
  }
572
734
  );
@@ -576,7 +738,7 @@ async function startMcpServer() {
576
738
  {},
577
739
  async () => {
578
740
  assertConnected();
579
- const res = await wsServer.sendRequest(CliMsg.GET_SELECTION, {});
741
+ const res = await bridge.sendRequest(CliMsg.GET_SELECTION, {});
580
742
  return { content: [{ type: "text", text: JSON.stringify(res.payload, null, 2) }] };
581
743
  }
582
744
  );
@@ -589,7 +751,7 @@ async function startMcpServer() {
589
751
  },
590
752
  async ({ query, maxResults }) => {
591
753
  assertConnected();
592
- const res = await wsServer.sendRequest(CliMsg.SEARCH_SCRIPTS, { query, maxResults });
754
+ const res = await bridge.sendRequest(CliMsg.SEARCH_SCRIPTS, { query, maxResults });
593
755
  return { content: [{ type: "text", text: JSON.stringify(res.payload, null, 2) }] };
594
756
  }
595
757
  );
@@ -602,7 +764,7 @@ async function startMcpServer() {
602
764
  },
603
765
  async ({ limit, level }) => {
604
766
  assertConnected();
605
- const res = await wsServer.sendRequest(CliMsg.GET_OUTPUT, {
767
+ const res = await bridge.sendRequest(CliMsg.GET_OUTPUT, {
606
768
  limit,
607
769
  level: level ?? null
608
770
  });
@@ -615,7 +777,7 @@ async function startMcpServer() {
615
777
  { className: z.string().describe('Roblox class name, e.g. "Frame", "TextLabel", "Part", "MeshPart"') },
616
778
  async ({ className }) => {
617
779
  assertConnected();
618
- const res = await wsServer.sendRequest(CliMsg.GET_REFLECTION, { className });
780
+ const res = await bridge.sendRequest(CliMsg.GET_REFLECTION, { className });
619
781
  return { content: [{ type: "text", text: JSON.stringify(res.payload, null, 2) }] };
620
782
  }
621
783
  );
@@ -630,7 +792,7 @@ async function startMcpServer() {
630
792
  },
631
793
  async ({ parent, tree }) => {
632
794
  assertConnected();
633
- const res = await wsServer.sendRequest(
795
+ const res = await bridge.sendRequest(
634
796
  CliMsg.BUILD_UI,
635
797
  { parent: parent ?? "StarterGui", tree }
636
798
  );
@@ -643,7 +805,7 @@ async function startMcpServer() {
643
805
  { testName: z.string().optional().describe("Specific test name, or omit to run all") },
644
806
  async ({ testName }) => {
645
807
  assertConnected();
646
- const res = await wsServer.sendRequest(CliMsg.RUN_TESTS, {
808
+ const res = await bridge.sendRequest(CliMsg.RUN_TESTS, {
647
809
  testName: testName ?? null
648
810
  });
649
811
  return { content: [{ type: "text", text: JSON.stringify(res.payload, null, 2) }] };
@@ -655,7 +817,7 @@ async function startMcpServer() {
655
817
  { args: z.unknown().optional().describe("Arguments passed to the test session. Server scripts get this via StudioTestService:GetTestArgs()") },
656
818
  async ({ args }) => {
657
819
  assertConnected();
658
- const res = await wsServer.sendRequest(CliMsg.EXECUTE_RUN_TEST, { args: args ?? null }, 12e4);
820
+ const res = await bridge.sendRequest(CliMsg.EXECUTE_RUN_TEST, { args: args ?? null }, 12e4);
659
821
  return { content: [{ type: "text", text: JSON.stringify(res.payload, null, 2) }] };
660
822
  }
661
823
  );
@@ -665,7 +827,7 @@ async function startMcpServer() {
665
827
  { args: z.unknown().optional().describe("Arguments passed to the test session. Server scripts get this via StudioTestService:GetTestArgs()") },
666
828
  async ({ args }) => {
667
829
  assertConnected();
668
- const res = await wsServer.sendRequest(CliMsg.EXECUTE_PLAY_TEST, { args: args ?? null }, 12e4);
830
+ const res = await bridge.sendRequest(CliMsg.EXECUTE_PLAY_TEST, { args: args ?? null }, 12e4);
669
831
  return { content: [{ type: "text", text: JSON.stringify(res.payload, null, 2) }] };
670
832
  }
671
833
  );
@@ -704,7 +866,7 @@ async function startMcpServer() {
704
866
  if (params.canCollide !== void 0) properties.CanCollide = params.canCollide;
705
867
  if (params.shape && params.shape !== "Block") properties.Shape = params.shape;
706
868
  const className = params.shape === "Wedge" ? "WedgePart" : params.shape === "CornerWedge" ? "CornerWedgePart" : "Part";
707
- const res = await wsServer.sendRequest(CliMsg.INSERT_INSTANCE, {
869
+ const res = await bridge.sendRequest(CliMsg.INSERT_INSTANCE, {
708
870
  className,
709
871
  parentPath: params.parent ?? "Workspace",
710
872
  name: params.name,
@@ -743,7 +905,7 @@ async function startMcpServer() {
743
905
  lines.push(`end`);
744
906
  }
745
907
  lines.push(`print("Cloned: " .. clone:GetFullName())`);
746
- const res = await wsServer.sendRequest(CliMsg.RUN_CODE, { code: lines.join("\n") });
908
+ const res = await bridge.sendRequest(CliMsg.RUN_CODE, { code: lines.join("\n") });
747
909
  return { content: [{ type: "text", text: JSON.stringify(res.payload, null, 2) }] };
748
910
  }
749
911
  );
@@ -776,7 +938,7 @@ async function startMcpServer() {
776
938
  ...resolves.map((r) => `${r}.Parent = group`),
777
939
  `print("Grouped ${params.paths.length} instances into: " .. group:GetFullName())`
778
940
  ].join("\n");
779
- const res = await wsServer.sendRequest(CliMsg.RUN_CODE, { code });
941
+ const res = await bridge.sendRequest(CliMsg.RUN_CODE, { code });
780
942
  return { content: [{ type: "text", text: JSON.stringify(res.payload, null, 2) }] };
781
943
  }
782
944
  );
@@ -835,7 +997,7 @@ async function startMcpServer() {
835
997
  lines.push(`${v}.Parent = ${groupName ? "_group" : resolveP(p.parent ?? "Workspace")}`);
836
998
  }
837
999
  lines.push(`print("Built ${parts.length} parts${groupName ? ` in ${groupName}` : ""}")`);
838
- const res = await wsServer.sendRequest(CliMsg.RUN_CODE, { code: lines.join("\n") });
1000
+ const res = await bridge.sendRequest(CliMsg.RUN_CODE, { code: lines.join("\n") });
839
1001
  return { content: [{ type: "text", text: JSON.stringify(res.payload, null, 2) }] };
840
1002
  }
841
1003
  );
@@ -871,16 +1033,16 @@ async function startMcpServer() {
871
1033
  "studio://status",
872
1034
  "studio://status",
873
1035
  async () => {
874
- const info = wsServer.getConnectionInfo();
1036
+ const info = bridge.getConnectionInfo();
875
1037
  return {
876
1038
  contents: [{
877
1039
  uri: "studio://status",
878
1040
  text: JSON.stringify({
879
- connected: wsServer.isConnected(),
1041
+ connected: bridge.isConnected(),
880
1042
  placeName: info?.placeName,
881
1043
  placeId: info?.placeId,
882
1044
  studioVersion: info?.studioVersion,
883
- port: wsServer.getPort()
1045
+ port: bridge.getPort()
884
1046
  }, null, 2),
885
1047
  mimeType: "application/json"
886
1048
  }]
@@ -891,7 +1053,7 @@ async function startMcpServer() {
891
1053
  await mcp.connect(transport);
892
1054
  }
893
1055
  function assertConnected() {
894
- if (!wsServer.isConnected()) {
1056
+ if (!bridge.isConnected()) {
895
1057
  throw new Error(
896
1058
  "Roblox Studio is not connected. Open Studio and click the Dominus plugin Connect button."
897
1059
  );