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/index.js +366 -257
- package/dist/index.js.map +1 -1
- package/dist/mcp.js +190 -28
- package/dist/mcp.js.map +1 -1
- package/package.json +1 -1
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
|
|
615
|
+
var bridge;
|
|
462
616
|
async function startMcpServer() {
|
|
463
617
|
const config = loadConfig();
|
|
464
618
|
await initMemoryStore();
|
|
465
|
-
|
|
466
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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 =
|
|
1036
|
+
const info = bridge.getConnectionInfo();
|
|
875
1037
|
return {
|
|
876
1038
|
contents: [{
|
|
877
1039
|
uri: "studio://status",
|
|
878
1040
|
text: JSON.stringify({
|
|
879
|
-
connected:
|
|
1041
|
+
connected: bridge.isConnected(),
|
|
880
1042
|
placeName: info?.placeName,
|
|
881
1043
|
placeId: info?.placeId,
|
|
882
1044
|
studioVersion: info?.studioVersion,
|
|
883
|
-
port:
|
|
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 (!
|
|
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
|
);
|