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/index.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import
|
|
3
|
-
import
|
|
4
|
-
import
|
|
2
|
+
import fs5, { existsSync, readFileSync, unlinkSync } from 'fs';
|
|
3
|
+
import path4, { join } from 'path';
|
|
4
|
+
import os3, { tmpdir } from 'os';
|
|
5
5
|
import { nanoid } from 'nanoid';
|
|
6
6
|
import { WebSocket, WebSocketServer } from 'ws';
|
|
7
7
|
import { EventEmitter } from 'events';
|
|
@@ -18,6 +18,7 @@ import { execSync } from 'child_process';
|
|
|
18
18
|
import { randomBytes } from 'crypto';
|
|
19
19
|
import Spinner from 'ink-spinner';
|
|
20
20
|
import OpenAI from 'openai';
|
|
21
|
+
import https from 'https';
|
|
21
22
|
|
|
22
23
|
var __defProp = Object.defineProperty;
|
|
23
24
|
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
@@ -55,14 +56,14 @@ function resolveProviderSettings(config) {
|
|
|
55
56
|
};
|
|
56
57
|
}
|
|
57
58
|
function ensureDir() {
|
|
58
|
-
if (!
|
|
59
|
-
|
|
59
|
+
if (!fs5.existsSync(DOMINUS_DIR)) {
|
|
60
|
+
fs5.mkdirSync(DOMINUS_DIR, { recursive: true });
|
|
60
61
|
}
|
|
61
62
|
}
|
|
62
63
|
function readJson(filePath, defaults) {
|
|
63
64
|
try {
|
|
64
|
-
if (
|
|
65
|
-
const raw =
|
|
65
|
+
if (fs5.existsSync(filePath)) {
|
|
66
|
+
const raw = fs5.readFileSync(filePath, "utf-8");
|
|
66
67
|
return { ...defaults, ...JSON.parse(raw) };
|
|
67
68
|
}
|
|
68
69
|
} catch {
|
|
@@ -71,7 +72,7 @@ function readJson(filePath, defaults) {
|
|
|
71
72
|
}
|
|
72
73
|
function writeJson(filePath, data) {
|
|
73
74
|
ensureDir();
|
|
74
|
-
|
|
75
|
+
fs5.writeFileSync(filePath, JSON.stringify(data, null, 2), "utf-8");
|
|
75
76
|
}
|
|
76
77
|
function loadConfig() {
|
|
77
78
|
ensureDir();
|
|
@@ -123,10 +124,10 @@ function createConfigAccess() {
|
|
|
123
124
|
var DOMINUS_DIR, CONFIG_PATH, DB_PATH, RULES_PATH, PROVIDERS, DEFAULTS, DEFAULT_RULES;
|
|
124
125
|
var init_config = __esm({
|
|
125
126
|
"src/config/config.ts"() {
|
|
126
|
-
DOMINUS_DIR =
|
|
127
|
-
CONFIG_PATH =
|
|
128
|
-
DB_PATH =
|
|
129
|
-
RULES_PATH =
|
|
127
|
+
DOMINUS_DIR = path4.join(os3.homedir(), ".dominus");
|
|
128
|
+
CONFIG_PATH = path4.join(DOMINUS_DIR, "config.json");
|
|
129
|
+
DB_PATH = path4.join(DOMINUS_DIR, "dominus.db");
|
|
130
|
+
RULES_PATH = path4.join(DOMINUS_DIR, "rules.json");
|
|
130
131
|
PROVIDERS = {
|
|
131
132
|
openai: {
|
|
132
133
|
name: "OpenAI",
|
|
@@ -459,6 +460,166 @@ var init_ws_server = __esm({
|
|
|
459
460
|
};
|
|
460
461
|
}
|
|
461
462
|
});
|
|
463
|
+
async function isPortInUse(port) {
|
|
464
|
+
return new Promise((resolve) => {
|
|
465
|
+
const ws = new WebSocket(`ws://localhost:${port}`);
|
|
466
|
+
const timer = setTimeout(() => {
|
|
467
|
+
ws.close();
|
|
468
|
+
resolve(false);
|
|
469
|
+
}, 1e3);
|
|
470
|
+
ws.on("open", () => {
|
|
471
|
+
clearTimeout(timer);
|
|
472
|
+
ws.close();
|
|
473
|
+
resolve(true);
|
|
474
|
+
});
|
|
475
|
+
ws.on("error", () => {
|
|
476
|
+
clearTimeout(timer);
|
|
477
|
+
resolve(false);
|
|
478
|
+
});
|
|
479
|
+
});
|
|
480
|
+
}
|
|
481
|
+
var DominusClient;
|
|
482
|
+
var init_ws_client = __esm({
|
|
483
|
+
"src/transport/ws-client.ts"() {
|
|
484
|
+
init_protocol();
|
|
485
|
+
DominusClient = class extends EventEmitter {
|
|
486
|
+
ws = null;
|
|
487
|
+
port;
|
|
488
|
+
studioConnected = false;
|
|
489
|
+
connectionInfo = null;
|
|
490
|
+
pendingRequests = /* @__PURE__ */ new Map();
|
|
491
|
+
constructor(port = 18088) {
|
|
492
|
+
super();
|
|
493
|
+
this.port = port;
|
|
494
|
+
}
|
|
495
|
+
connect() {
|
|
496
|
+
return new Promise((resolve, reject) => {
|
|
497
|
+
const url = `ws://localhost:${this.port}`;
|
|
498
|
+
const ws = new WebSocket(url);
|
|
499
|
+
ws.on("open", () => {
|
|
500
|
+
this.ws = ws;
|
|
501
|
+
ws.send(serializeMessage(createMessage("controller:connect", {})));
|
|
502
|
+
this.emit("server:started", { port: this.port, relay: true });
|
|
503
|
+
resolve();
|
|
504
|
+
});
|
|
505
|
+
ws.on("message", (raw) => {
|
|
506
|
+
try {
|
|
507
|
+
const msg = parseMessage(raw.toString());
|
|
508
|
+
this.handleMessage(msg);
|
|
509
|
+
} catch (err) {
|
|
510
|
+
this.emit("server:error", err);
|
|
511
|
+
}
|
|
512
|
+
});
|
|
513
|
+
ws.on("close", () => {
|
|
514
|
+
this.ws = null;
|
|
515
|
+
this.studioConnected = false;
|
|
516
|
+
this.emit("studio:disconnected");
|
|
517
|
+
for (const [id, pending] of this.pendingRequests) {
|
|
518
|
+
clearTimeout(pending.timer);
|
|
519
|
+
pending.reject(new Error("Relay connection lost"));
|
|
520
|
+
this.pendingRequests.delete(id);
|
|
521
|
+
}
|
|
522
|
+
});
|
|
523
|
+
ws.on("error", (err) => {
|
|
524
|
+
if (!this.ws) reject(err);
|
|
525
|
+
this.emit("server:error", err);
|
|
526
|
+
});
|
|
527
|
+
});
|
|
528
|
+
}
|
|
529
|
+
handleMessage(msg) {
|
|
530
|
+
if (msg.type === "controller:welcome") {
|
|
531
|
+
const p = msg.payload;
|
|
532
|
+
this.studioConnected = p.studioConnected;
|
|
533
|
+
if (this.studioConnected) {
|
|
534
|
+
this.emit("studio:connected", {});
|
|
535
|
+
}
|
|
536
|
+
return;
|
|
537
|
+
}
|
|
538
|
+
if (msg.type === "studio:connected") {
|
|
539
|
+
this.studioConnected = true;
|
|
540
|
+
const payload = msg.payload;
|
|
541
|
+
this.connectionInfo = {
|
|
542
|
+
connectedAt: Date.now(),
|
|
543
|
+
studioVersion: payload.studioVersion,
|
|
544
|
+
placeId: payload.placeId,
|
|
545
|
+
placeName: payload.placeName
|
|
546
|
+
};
|
|
547
|
+
this.emit("studio:connected", payload);
|
|
548
|
+
return;
|
|
549
|
+
}
|
|
550
|
+
if (msg.type === "studio:disconnected") {
|
|
551
|
+
this.studioConnected = false;
|
|
552
|
+
this.connectionInfo = null;
|
|
553
|
+
this.emit("studio:disconnected");
|
|
554
|
+
return;
|
|
555
|
+
}
|
|
556
|
+
if (msg.type === StudioMsg.RESPONSE) {
|
|
557
|
+
const pending = this.pendingRequests.get(msg.id);
|
|
558
|
+
if (pending) {
|
|
559
|
+
clearTimeout(pending.timer);
|
|
560
|
+
pending.resolve(msg);
|
|
561
|
+
this.pendingRequests.delete(msg.id);
|
|
562
|
+
}
|
|
563
|
+
return;
|
|
564
|
+
}
|
|
565
|
+
this.emit(msg.type, msg.payload);
|
|
566
|
+
this.emit("message", msg);
|
|
567
|
+
}
|
|
568
|
+
sendRequest(type, payload, timeoutMs = 1e4) {
|
|
569
|
+
return new Promise((resolve, reject) => {
|
|
570
|
+
if (!this.ws || this.ws.readyState !== WebSocket.OPEN) {
|
|
571
|
+
reject(new Error("Not connected to Dominus server"));
|
|
572
|
+
return;
|
|
573
|
+
}
|
|
574
|
+
if (!this.studioConnected) {
|
|
575
|
+
reject(new Error("Studio not connected"));
|
|
576
|
+
return;
|
|
577
|
+
}
|
|
578
|
+
const msg = createMessage(type, payload);
|
|
579
|
+
const timer = setTimeout(() => {
|
|
580
|
+
this.pendingRequests.delete(msg.id);
|
|
581
|
+
reject(new Error(`Request timed out: ${type}`));
|
|
582
|
+
}, timeoutMs);
|
|
583
|
+
this.pendingRequests.set(msg.id, {
|
|
584
|
+
resolve,
|
|
585
|
+
reject,
|
|
586
|
+
timer
|
|
587
|
+
});
|
|
588
|
+
this.ws.send(serializeMessage(msg));
|
|
589
|
+
});
|
|
590
|
+
}
|
|
591
|
+
sendNotification(type, payload) {
|
|
592
|
+
if (!this.ws || this.ws.readyState !== WebSocket.OPEN) return;
|
|
593
|
+
const msg = createMessage(type, payload);
|
|
594
|
+
this.ws.send(serializeMessage(msg));
|
|
595
|
+
}
|
|
596
|
+
isConnected() {
|
|
597
|
+
return this.studioConnected;
|
|
598
|
+
}
|
|
599
|
+
getConnectionInfo() {
|
|
600
|
+
if (!this.connectionInfo) return null;
|
|
601
|
+
return { ...this.connectionInfo, ws: this.ws };
|
|
602
|
+
}
|
|
603
|
+
getPort() {
|
|
604
|
+
return this.port;
|
|
605
|
+
}
|
|
606
|
+
stop() {
|
|
607
|
+
return new Promise((resolve) => {
|
|
608
|
+
for (const [, pending] of this.pendingRequests) {
|
|
609
|
+
clearTimeout(pending.timer);
|
|
610
|
+
pending.reject(new Error("Client shutting down"));
|
|
611
|
+
}
|
|
612
|
+
this.pendingRequests.clear();
|
|
613
|
+
if (this.ws) {
|
|
614
|
+
this.ws.close();
|
|
615
|
+
this.ws = null;
|
|
616
|
+
}
|
|
617
|
+
resolve();
|
|
618
|
+
});
|
|
619
|
+
}
|
|
620
|
+
};
|
|
621
|
+
}
|
|
622
|
+
});
|
|
462
623
|
|
|
463
624
|
// src/memory/store.ts
|
|
464
625
|
var store_exports = {};
|
|
@@ -481,14 +642,14 @@ __export(store_exports, {
|
|
|
481
642
|
function persist() {
|
|
482
643
|
if (!db) return;
|
|
483
644
|
const data = db.export();
|
|
484
|
-
|
|
645
|
+
fs5.writeFileSync(dbPath, Buffer.from(data));
|
|
485
646
|
}
|
|
486
647
|
async function initMemoryStore() {
|
|
487
648
|
if (db) return;
|
|
488
649
|
dbPath = getDbPath();
|
|
489
650
|
const SQL = await initSqlJs();
|
|
490
|
-
if (
|
|
491
|
-
const buffer =
|
|
651
|
+
if (fs5.existsSync(dbPath)) {
|
|
652
|
+
const buffer = fs5.readFileSync(dbPath);
|
|
492
653
|
db = new SQL.Database(buffer);
|
|
493
654
|
} else {
|
|
494
655
|
db = new SQL.Database();
|
|
@@ -779,10 +940,10 @@ var init_read_script = __esm({
|
|
|
779
940
|
if (!ctx.isStudioConnected()) {
|
|
780
941
|
return { success: false, error: "Studio is not connected" };
|
|
781
942
|
}
|
|
782
|
-
const
|
|
783
|
-
const response = await ctx.sendToStudio(CliMsg.GET_SCRIPT, { path:
|
|
943
|
+
const path5 = params.path;
|
|
944
|
+
const response = await ctx.sendToStudio(CliMsg.GET_SCRIPT, { path: path5 });
|
|
784
945
|
if (!response.payload) {
|
|
785
|
-
return { success: false, error: `Script not found: ${
|
|
946
|
+
return { success: false, error: `Script not found: ${path5}` };
|
|
786
947
|
}
|
|
787
948
|
return {
|
|
788
949
|
success: true,
|
|
@@ -828,14 +989,14 @@ var init_edit_script = __esm({
|
|
|
828
989
|
if (!ctx.isStudioConnected()) {
|
|
829
990
|
return { success: false, error: "Studio is not connected" };
|
|
830
991
|
}
|
|
831
|
-
const
|
|
992
|
+
const path5 = params.path;
|
|
832
993
|
const source = params.source;
|
|
833
|
-
const response = await ctx.sendToStudio(CliMsg.SET_SCRIPT, { path:
|
|
994
|
+
const response = await ctx.sendToStudio(CliMsg.SET_SCRIPT, { path: path5, source });
|
|
834
995
|
const payload = response.payload;
|
|
835
996
|
if (!payload.success) {
|
|
836
997
|
return { success: false, error: payload.error ?? "Failed to edit script" };
|
|
837
998
|
}
|
|
838
|
-
return { success: true, data: { path:
|
|
999
|
+
return { success: true, data: { path: path5, linesWritten: source.split("\n").length } };
|
|
839
1000
|
}
|
|
840
1001
|
};
|
|
841
1002
|
}
|
|
@@ -1521,8 +1682,8 @@ __export(clone_instance_exports, {
|
|
|
1521
1682
|
tool: () => tool18
|
|
1522
1683
|
});
|
|
1523
1684
|
function buildCloneCode(params) {
|
|
1524
|
-
const
|
|
1525
|
-
const parts =
|
|
1685
|
+
const path5 = params.path;
|
|
1686
|
+
const parts = path5.split(".");
|
|
1526
1687
|
let resolve = "game";
|
|
1527
1688
|
for (const p of parts) {
|
|
1528
1689
|
resolve += `["${p}"]`;
|
|
@@ -1663,8 +1824,8 @@ __export(build_multiple_exports, {
|
|
|
1663
1824
|
});
|
|
1664
1825
|
function generateBatchCode(parts, groupName, groupParent) {
|
|
1665
1826
|
const lines = [];
|
|
1666
|
-
function resolveParent(
|
|
1667
|
-
const segs =
|
|
1827
|
+
function resolveParent(path5) {
|
|
1828
|
+
const segs = path5.split(".");
|
|
1668
1829
|
let r = "game";
|
|
1669
1830
|
for (const s of segs) r += `["${s}"]`;
|
|
1670
1831
|
return r;
|
|
@@ -1928,11 +2089,11 @@ var init_upload_asset = __esm({
|
|
|
1928
2089
|
const assetType = params.assetType;
|
|
1929
2090
|
const name = params.name;
|
|
1930
2091
|
const description = params.description ?? "";
|
|
1931
|
-
if (!
|
|
2092
|
+
if (!fs5.existsSync(filePath)) {
|
|
1932
2093
|
return { success: false, error: `File not found: ${filePath}` };
|
|
1933
2094
|
}
|
|
1934
|
-
const fileBuffer =
|
|
1935
|
-
const fileName =
|
|
2095
|
+
const fileBuffer = fs5.readFileSync(filePath);
|
|
2096
|
+
const fileName = path4.basename(filePath);
|
|
1936
2097
|
const typeMapping = {
|
|
1937
2098
|
Image: "Decal",
|
|
1938
2099
|
Audio: "Audio",
|
|
@@ -1994,19 +2155,27 @@ __export(server_exports, {
|
|
|
1994
2155
|
async function startMcpServer() {
|
|
1995
2156
|
const config = loadConfig();
|
|
1996
2157
|
await initMemoryStore();
|
|
1997
|
-
|
|
1998
|
-
|
|
2158
|
+
const portTaken = await isPortInUse(config.port);
|
|
2159
|
+
if (portTaken) {
|
|
2160
|
+
const client = new DominusClient(config.port);
|
|
2161
|
+
await client.connect();
|
|
2162
|
+
bridge = client;
|
|
2163
|
+
} else {
|
|
2164
|
+
const server = new DominusServer(config.port);
|
|
2165
|
+
await server.start();
|
|
2166
|
+
bridge = server;
|
|
2167
|
+
}
|
|
1999
2168
|
const mcp = new McpServer({
|
|
2000
2169
|
name: "dominus",
|
|
2001
|
-
version: "0.1
|
|
2170
|
+
version: "0.2.1"
|
|
2002
2171
|
});
|
|
2003
2172
|
mcp.tool(
|
|
2004
2173
|
"read_script",
|
|
2005
2174
|
"Read the source code of a script in Roblox Studio",
|
|
2006
2175
|
{ path: z.string().describe('Full instance path, e.g. "ServerScriptService.GameManager"') },
|
|
2007
|
-
async ({ path:
|
|
2176
|
+
async ({ path: path5 }) => {
|
|
2008
2177
|
assertConnected();
|
|
2009
|
-
const res = await
|
|
2178
|
+
const res = await bridge.sendRequest(CliMsg.GET_SCRIPT, { path: path5 });
|
|
2010
2179
|
return { content: [{ type: "text", text: JSON.stringify(res.payload, null, 2) }] };
|
|
2011
2180
|
}
|
|
2012
2181
|
);
|
|
@@ -2017,9 +2186,9 @@ async function startMcpServer() {
|
|
|
2017
2186
|
path: z.string().describe("Full instance path"),
|
|
2018
2187
|
source: z.string().describe("New complete source code")
|
|
2019
2188
|
},
|
|
2020
|
-
async ({ path:
|
|
2189
|
+
async ({ path: path5, source }) => {
|
|
2021
2190
|
assertConnected();
|
|
2022
|
-
const res = await
|
|
2191
|
+
const res = await bridge.sendRequest(CliMsg.SET_SCRIPT, { path: path5, source });
|
|
2023
2192
|
return { content: [{ type: "text", text: JSON.stringify(res.payload) }] };
|
|
2024
2193
|
}
|
|
2025
2194
|
);
|
|
@@ -2029,7 +2198,7 @@ async function startMcpServer() {
|
|
|
2029
2198
|
{ code: z.string().describe("Luau code to execute") },
|
|
2030
2199
|
async ({ code }) => {
|
|
2031
2200
|
assertConnected();
|
|
2032
|
-
const res = await
|
|
2201
|
+
const res = await bridge.sendRequest(CliMsg.RUN_CODE, { code });
|
|
2033
2202
|
return { content: [{ type: "text", text: JSON.stringify(res.payload, null, 2) }] };
|
|
2034
2203
|
}
|
|
2035
2204
|
);
|
|
@@ -2042,7 +2211,7 @@ async function startMcpServer() {
|
|
|
2042
2211
|
},
|
|
2043
2212
|
async ({ rootPath, maxDepth }) => {
|
|
2044
2213
|
assertConnected();
|
|
2045
|
-
const res = await
|
|
2214
|
+
const res = await bridge.sendRequest(CliMsg.GET_EXPLORER, {
|
|
2046
2215
|
rootPath: rootPath ?? null,
|
|
2047
2216
|
maxDepth
|
|
2048
2217
|
});
|
|
@@ -2053,9 +2222,9 @@ async function startMcpServer() {
|
|
|
2053
2222
|
"get_properties",
|
|
2054
2223
|
"Get all properties of an instance in Roblox Studio",
|
|
2055
2224
|
{ path: z.string().describe("Full instance path") },
|
|
2056
|
-
async ({ path:
|
|
2225
|
+
async ({ path: path5 }) => {
|
|
2057
2226
|
assertConnected();
|
|
2058
|
-
const res = await
|
|
2227
|
+
const res = await bridge.sendRequest(CliMsg.GET_PROPERTIES, { path: path5 });
|
|
2059
2228
|
return { content: [{ type: "text", text: JSON.stringify(res.payload, null, 2) }] };
|
|
2060
2229
|
}
|
|
2061
2230
|
);
|
|
@@ -2066,9 +2235,9 @@ async function startMcpServer() {
|
|
|
2066
2235
|
path: z.string().describe("Full instance path"),
|
|
2067
2236
|
properties: z.record(z.unknown()).describe("Key-value pairs of properties to set")
|
|
2068
2237
|
},
|
|
2069
|
-
async ({ path:
|
|
2238
|
+
async ({ path: path5, properties }) => {
|
|
2070
2239
|
assertConnected();
|
|
2071
|
-
const res = await
|
|
2240
|
+
const res = await bridge.sendRequest(CliMsg.SET_PROPERTIES, { path: path5, properties });
|
|
2072
2241
|
return { content: [{ type: "text", text: JSON.stringify(res.payload) }] };
|
|
2073
2242
|
}
|
|
2074
2243
|
);
|
|
@@ -2083,7 +2252,7 @@ async function startMcpServer() {
|
|
|
2083
2252
|
},
|
|
2084
2253
|
async ({ className, parentPath, name, properties }) => {
|
|
2085
2254
|
assertConnected();
|
|
2086
|
-
const res = await
|
|
2255
|
+
const res = await bridge.sendRequest(CliMsg.INSERT_INSTANCE, {
|
|
2087
2256
|
className,
|
|
2088
2257
|
parentPath,
|
|
2089
2258
|
name,
|
|
@@ -2096,9 +2265,9 @@ async function startMcpServer() {
|
|
|
2096
2265
|
"delete_instance",
|
|
2097
2266
|
"Delete an instance from the Roblox Studio DataModel",
|
|
2098
2267
|
{ path: z.string().describe("Full instance path to delete") },
|
|
2099
|
-
async ({ path:
|
|
2268
|
+
async ({ path: path5 }) => {
|
|
2100
2269
|
assertConnected();
|
|
2101
|
-
const res = await
|
|
2270
|
+
const res = await bridge.sendRequest(CliMsg.DELETE_INSTANCE, { path: path5 });
|
|
2102
2271
|
return { content: [{ type: "text", text: JSON.stringify(res.payload) }] };
|
|
2103
2272
|
}
|
|
2104
2273
|
);
|
|
@@ -2108,7 +2277,7 @@ async function startMcpServer() {
|
|
|
2108
2277
|
{},
|
|
2109
2278
|
async () => {
|
|
2110
2279
|
assertConnected();
|
|
2111
|
-
const res = await
|
|
2280
|
+
const res = await bridge.sendRequest(CliMsg.GET_SELECTION, {});
|
|
2112
2281
|
return { content: [{ type: "text", text: JSON.stringify(res.payload, null, 2) }] };
|
|
2113
2282
|
}
|
|
2114
2283
|
);
|
|
@@ -2121,7 +2290,7 @@ async function startMcpServer() {
|
|
|
2121
2290
|
},
|
|
2122
2291
|
async ({ query, maxResults }) => {
|
|
2123
2292
|
assertConnected();
|
|
2124
|
-
const res = await
|
|
2293
|
+
const res = await bridge.sendRequest(CliMsg.SEARCH_SCRIPTS, { query, maxResults });
|
|
2125
2294
|
return { content: [{ type: "text", text: JSON.stringify(res.payload, null, 2) }] };
|
|
2126
2295
|
}
|
|
2127
2296
|
);
|
|
@@ -2134,7 +2303,7 @@ async function startMcpServer() {
|
|
|
2134
2303
|
},
|
|
2135
2304
|
async ({ limit, level }) => {
|
|
2136
2305
|
assertConnected();
|
|
2137
|
-
const res = await
|
|
2306
|
+
const res = await bridge.sendRequest(CliMsg.GET_OUTPUT, {
|
|
2138
2307
|
limit,
|
|
2139
2308
|
level: level ?? null
|
|
2140
2309
|
});
|
|
@@ -2147,7 +2316,7 @@ async function startMcpServer() {
|
|
|
2147
2316
|
{ className: z.string().describe('Roblox class name, e.g. "Frame", "TextLabel", "Part", "MeshPart"') },
|
|
2148
2317
|
async ({ className }) => {
|
|
2149
2318
|
assertConnected();
|
|
2150
|
-
const res = await
|
|
2319
|
+
const res = await bridge.sendRequest(CliMsg.GET_REFLECTION, { className });
|
|
2151
2320
|
return { content: [{ type: "text", text: JSON.stringify(res.payload, null, 2) }] };
|
|
2152
2321
|
}
|
|
2153
2322
|
);
|
|
@@ -2162,7 +2331,7 @@ async function startMcpServer() {
|
|
|
2162
2331
|
},
|
|
2163
2332
|
async ({ parent, tree }) => {
|
|
2164
2333
|
assertConnected();
|
|
2165
|
-
const res = await
|
|
2334
|
+
const res = await bridge.sendRequest(
|
|
2166
2335
|
CliMsg.BUILD_UI,
|
|
2167
2336
|
{ parent: parent ?? "StarterGui", tree }
|
|
2168
2337
|
);
|
|
@@ -2175,7 +2344,7 @@ async function startMcpServer() {
|
|
|
2175
2344
|
{ testName: z.string().optional().describe("Specific test name, or omit to run all") },
|
|
2176
2345
|
async ({ testName }) => {
|
|
2177
2346
|
assertConnected();
|
|
2178
|
-
const res = await
|
|
2347
|
+
const res = await bridge.sendRequest(CliMsg.RUN_TESTS, {
|
|
2179
2348
|
testName: testName ?? null
|
|
2180
2349
|
});
|
|
2181
2350
|
return { content: [{ type: "text", text: JSON.stringify(res.payload, null, 2) }] };
|
|
@@ -2187,7 +2356,7 @@ async function startMcpServer() {
|
|
|
2187
2356
|
{ args: z.unknown().optional().describe("Arguments passed to the test session. Server scripts get this via StudioTestService:GetTestArgs()") },
|
|
2188
2357
|
async ({ args }) => {
|
|
2189
2358
|
assertConnected();
|
|
2190
|
-
const res = await
|
|
2359
|
+
const res = await bridge.sendRequest(CliMsg.EXECUTE_RUN_TEST, { args: args ?? null }, 12e4);
|
|
2191
2360
|
return { content: [{ type: "text", text: JSON.stringify(res.payload, null, 2) }] };
|
|
2192
2361
|
}
|
|
2193
2362
|
);
|
|
@@ -2197,7 +2366,7 @@ async function startMcpServer() {
|
|
|
2197
2366
|
{ args: z.unknown().optional().describe("Arguments passed to the test session. Server scripts get this via StudioTestService:GetTestArgs()") },
|
|
2198
2367
|
async ({ args }) => {
|
|
2199
2368
|
assertConnected();
|
|
2200
|
-
const res = await
|
|
2369
|
+
const res = await bridge.sendRequest(CliMsg.EXECUTE_PLAY_TEST, { args: args ?? null }, 12e4);
|
|
2201
2370
|
return { content: [{ type: "text", text: JSON.stringify(res.payload, null, 2) }] };
|
|
2202
2371
|
}
|
|
2203
2372
|
);
|
|
@@ -2236,7 +2405,7 @@ async function startMcpServer() {
|
|
|
2236
2405
|
if (params.canCollide !== void 0) properties.CanCollide = params.canCollide;
|
|
2237
2406
|
if (params.shape && params.shape !== "Block") properties.Shape = params.shape;
|
|
2238
2407
|
const className = params.shape === "Wedge" ? "WedgePart" : params.shape === "CornerWedge" ? "CornerWedgePart" : "Part";
|
|
2239
|
-
const res = await
|
|
2408
|
+
const res = await bridge.sendRequest(CliMsg.INSERT_INSTANCE, {
|
|
2240
2409
|
className,
|
|
2241
2410
|
parentPath: params.parent ?? "Workspace",
|
|
2242
2411
|
name: params.name,
|
|
@@ -2275,7 +2444,7 @@ async function startMcpServer() {
|
|
|
2275
2444
|
lines.push(`end`);
|
|
2276
2445
|
}
|
|
2277
2446
|
lines.push(`print("Cloned: " .. clone:GetFullName())`);
|
|
2278
|
-
const res = await
|
|
2447
|
+
const res = await bridge.sendRequest(CliMsg.RUN_CODE, { code: lines.join("\n") });
|
|
2279
2448
|
return { content: [{ type: "text", text: JSON.stringify(res.payload, null, 2) }] };
|
|
2280
2449
|
}
|
|
2281
2450
|
);
|
|
@@ -2295,8 +2464,8 @@ async function startMcpServer() {
|
|
|
2295
2464
|
const parentParts = parent.split(".");
|
|
2296
2465
|
let parentResolve = "game";
|
|
2297
2466
|
for (const p of parentParts) parentResolve += `["${p}"]`;
|
|
2298
|
-
const resolves = params.paths.map((
|
|
2299
|
-
const parts =
|
|
2467
|
+
const resolves = params.paths.map((path5) => {
|
|
2468
|
+
const parts = path5.split(".");
|
|
2300
2469
|
let r = "game";
|
|
2301
2470
|
for (const part of parts) r += `["${part}"]`;
|
|
2302
2471
|
return r;
|
|
@@ -2308,7 +2477,7 @@ async function startMcpServer() {
|
|
|
2308
2477
|
...resolves.map((r) => `${r}.Parent = group`),
|
|
2309
2478
|
`print("Grouped ${params.paths.length} instances into: " .. group:GetFullName())`
|
|
2310
2479
|
].join("\n");
|
|
2311
|
-
const res = await
|
|
2480
|
+
const res = await bridge.sendRequest(CliMsg.RUN_CODE, { code });
|
|
2312
2481
|
return { content: [{ type: "text", text: JSON.stringify(res.payload, null, 2) }] };
|
|
2313
2482
|
}
|
|
2314
2483
|
);
|
|
@@ -2336,8 +2505,8 @@ async function startMcpServer() {
|
|
|
2336
2505
|
const parts = params.parts;
|
|
2337
2506
|
const groupName = params.groupName;
|
|
2338
2507
|
const groupParent = params.groupParent ?? "Workspace";
|
|
2339
|
-
function resolveP(
|
|
2340
|
-
const segs =
|
|
2508
|
+
function resolveP(path5) {
|
|
2509
|
+
const segs = path5.split(".");
|
|
2341
2510
|
let r = "game";
|
|
2342
2511
|
for (const s of segs) r += `["${s}"]`;
|
|
2343
2512
|
return r;
|
|
@@ -2367,7 +2536,7 @@ async function startMcpServer() {
|
|
|
2367
2536
|
lines.push(`${v}.Parent = ${groupName ? "_group" : resolveP(p.parent ?? "Workspace")}`);
|
|
2368
2537
|
}
|
|
2369
2538
|
lines.push(`print("Built ${parts.length} parts${groupName ? ` in ${groupName}` : ""}")`);
|
|
2370
|
-
const res = await
|
|
2539
|
+
const res = await bridge.sendRequest(CliMsg.RUN_CODE, { code: lines.join("\n") });
|
|
2371
2540
|
return { content: [{ type: "text", text: JSON.stringify(res.payload, null, 2) }] };
|
|
2372
2541
|
}
|
|
2373
2542
|
);
|
|
@@ -2403,16 +2572,16 @@ async function startMcpServer() {
|
|
|
2403
2572
|
"studio://status",
|
|
2404
2573
|
"studio://status",
|
|
2405
2574
|
async () => {
|
|
2406
|
-
const info =
|
|
2575
|
+
const info = bridge.getConnectionInfo();
|
|
2407
2576
|
return {
|
|
2408
2577
|
contents: [{
|
|
2409
2578
|
uri: "studio://status",
|
|
2410
2579
|
text: JSON.stringify({
|
|
2411
|
-
connected:
|
|
2580
|
+
connected: bridge.isConnected(),
|
|
2412
2581
|
placeName: info?.placeName,
|
|
2413
2582
|
placeId: info?.placeId,
|
|
2414
2583
|
studioVersion: info?.studioVersion,
|
|
2415
|
-
port:
|
|
2584
|
+
port: bridge.getPort()
|
|
2416
2585
|
}, null, 2),
|
|
2417
2586
|
mimeType: "application/json"
|
|
2418
2587
|
}]
|
|
@@ -2423,16 +2592,17 @@ async function startMcpServer() {
|
|
|
2423
2592
|
await mcp.connect(transport);
|
|
2424
2593
|
}
|
|
2425
2594
|
function assertConnected() {
|
|
2426
|
-
if (!
|
|
2595
|
+
if (!bridge.isConnected()) {
|
|
2427
2596
|
throw new Error(
|
|
2428
2597
|
"Roblox Studio is not connected. Open Studio and click the Dominus plugin Connect button."
|
|
2429
2598
|
);
|
|
2430
2599
|
}
|
|
2431
2600
|
}
|
|
2432
|
-
var
|
|
2601
|
+
var bridge;
|
|
2433
2602
|
var init_server = __esm({
|
|
2434
2603
|
"src/mcp/server.ts"() {
|
|
2435
2604
|
init_ws_server();
|
|
2605
|
+
init_ws_client();
|
|
2436
2606
|
init_protocol();
|
|
2437
2607
|
init_store();
|
|
2438
2608
|
init_config();
|
|
@@ -3475,163 +3645,7 @@ var App = ({ server, agent: initialAgent, version }) => {
|
|
|
3475
3645
|
|
|
3476
3646
|
// src/index.tsx
|
|
3477
3647
|
init_ws_server();
|
|
3478
|
-
|
|
3479
|
-
// src/transport/ws-client.ts
|
|
3480
|
-
init_protocol();
|
|
3481
|
-
var DominusClient = class extends EventEmitter {
|
|
3482
|
-
ws = null;
|
|
3483
|
-
port;
|
|
3484
|
-
studioConnected = false;
|
|
3485
|
-
connectionInfo = null;
|
|
3486
|
-
pendingRequests = /* @__PURE__ */ new Map();
|
|
3487
|
-
constructor(port = 18088) {
|
|
3488
|
-
super();
|
|
3489
|
-
this.port = port;
|
|
3490
|
-
}
|
|
3491
|
-
connect() {
|
|
3492
|
-
return new Promise((resolve, reject) => {
|
|
3493
|
-
const url = `ws://localhost:${this.port}`;
|
|
3494
|
-
const ws = new WebSocket(url);
|
|
3495
|
-
ws.on("open", () => {
|
|
3496
|
-
this.ws = ws;
|
|
3497
|
-
ws.send(serializeMessage(createMessage("controller:connect", {})));
|
|
3498
|
-
this.emit("server:started", { port: this.port, relay: true });
|
|
3499
|
-
resolve();
|
|
3500
|
-
});
|
|
3501
|
-
ws.on("message", (raw) => {
|
|
3502
|
-
try {
|
|
3503
|
-
const msg = parseMessage(raw.toString());
|
|
3504
|
-
this.handleMessage(msg);
|
|
3505
|
-
} catch (err) {
|
|
3506
|
-
this.emit("server:error", err);
|
|
3507
|
-
}
|
|
3508
|
-
});
|
|
3509
|
-
ws.on("close", () => {
|
|
3510
|
-
this.ws = null;
|
|
3511
|
-
this.studioConnected = false;
|
|
3512
|
-
this.emit("studio:disconnected");
|
|
3513
|
-
for (const [id, pending] of this.pendingRequests) {
|
|
3514
|
-
clearTimeout(pending.timer);
|
|
3515
|
-
pending.reject(new Error("Relay connection lost"));
|
|
3516
|
-
this.pendingRequests.delete(id);
|
|
3517
|
-
}
|
|
3518
|
-
});
|
|
3519
|
-
ws.on("error", (err) => {
|
|
3520
|
-
if (!this.ws) reject(err);
|
|
3521
|
-
this.emit("server:error", err);
|
|
3522
|
-
});
|
|
3523
|
-
});
|
|
3524
|
-
}
|
|
3525
|
-
handleMessage(msg) {
|
|
3526
|
-
if (msg.type === "controller:welcome") {
|
|
3527
|
-
const p = msg.payload;
|
|
3528
|
-
this.studioConnected = p.studioConnected;
|
|
3529
|
-
if (this.studioConnected) {
|
|
3530
|
-
this.emit("studio:connected", {});
|
|
3531
|
-
}
|
|
3532
|
-
return;
|
|
3533
|
-
}
|
|
3534
|
-
if (msg.type === "studio:connected") {
|
|
3535
|
-
this.studioConnected = true;
|
|
3536
|
-
const payload = msg.payload;
|
|
3537
|
-
this.connectionInfo = {
|
|
3538
|
-
connectedAt: Date.now(),
|
|
3539
|
-
studioVersion: payload.studioVersion,
|
|
3540
|
-
placeId: payload.placeId,
|
|
3541
|
-
placeName: payload.placeName
|
|
3542
|
-
};
|
|
3543
|
-
this.emit("studio:connected", payload);
|
|
3544
|
-
return;
|
|
3545
|
-
}
|
|
3546
|
-
if (msg.type === "studio:disconnected") {
|
|
3547
|
-
this.studioConnected = false;
|
|
3548
|
-
this.connectionInfo = null;
|
|
3549
|
-
this.emit("studio:disconnected");
|
|
3550
|
-
return;
|
|
3551
|
-
}
|
|
3552
|
-
if (msg.type === StudioMsg.RESPONSE) {
|
|
3553
|
-
const pending = this.pendingRequests.get(msg.id);
|
|
3554
|
-
if (pending) {
|
|
3555
|
-
clearTimeout(pending.timer);
|
|
3556
|
-
pending.resolve(msg);
|
|
3557
|
-
this.pendingRequests.delete(msg.id);
|
|
3558
|
-
}
|
|
3559
|
-
return;
|
|
3560
|
-
}
|
|
3561
|
-
this.emit(msg.type, msg.payload);
|
|
3562
|
-
this.emit("message", msg);
|
|
3563
|
-
}
|
|
3564
|
-
sendRequest(type, payload, timeoutMs = 1e4) {
|
|
3565
|
-
return new Promise((resolve, reject) => {
|
|
3566
|
-
if (!this.ws || this.ws.readyState !== WebSocket.OPEN) {
|
|
3567
|
-
reject(new Error("Not connected to Dominus server"));
|
|
3568
|
-
return;
|
|
3569
|
-
}
|
|
3570
|
-
if (!this.studioConnected) {
|
|
3571
|
-
reject(new Error("Studio not connected"));
|
|
3572
|
-
return;
|
|
3573
|
-
}
|
|
3574
|
-
const msg = createMessage(type, payload);
|
|
3575
|
-
const timer = setTimeout(() => {
|
|
3576
|
-
this.pendingRequests.delete(msg.id);
|
|
3577
|
-
reject(new Error(`Request timed out: ${type}`));
|
|
3578
|
-
}, timeoutMs);
|
|
3579
|
-
this.pendingRequests.set(msg.id, {
|
|
3580
|
-
resolve,
|
|
3581
|
-
reject,
|
|
3582
|
-
timer
|
|
3583
|
-
});
|
|
3584
|
-
this.ws.send(serializeMessage(msg));
|
|
3585
|
-
});
|
|
3586
|
-
}
|
|
3587
|
-
sendNotification(type, payload) {
|
|
3588
|
-
if (!this.ws || this.ws.readyState !== WebSocket.OPEN) return;
|
|
3589
|
-
const msg = createMessage(type, payload);
|
|
3590
|
-
this.ws.send(serializeMessage(msg));
|
|
3591
|
-
}
|
|
3592
|
-
isConnected() {
|
|
3593
|
-
return this.studioConnected;
|
|
3594
|
-
}
|
|
3595
|
-
getConnectionInfo() {
|
|
3596
|
-
if (!this.connectionInfo) return null;
|
|
3597
|
-
return { ...this.connectionInfo, ws: this.ws };
|
|
3598
|
-
}
|
|
3599
|
-
getPort() {
|
|
3600
|
-
return this.port;
|
|
3601
|
-
}
|
|
3602
|
-
stop() {
|
|
3603
|
-
return new Promise((resolve) => {
|
|
3604
|
-
for (const [, pending] of this.pendingRequests) {
|
|
3605
|
-
clearTimeout(pending.timer);
|
|
3606
|
-
pending.reject(new Error("Client shutting down"));
|
|
3607
|
-
}
|
|
3608
|
-
this.pendingRequests.clear();
|
|
3609
|
-
if (this.ws) {
|
|
3610
|
-
this.ws.close();
|
|
3611
|
-
this.ws = null;
|
|
3612
|
-
}
|
|
3613
|
-
resolve();
|
|
3614
|
-
});
|
|
3615
|
-
}
|
|
3616
|
-
};
|
|
3617
|
-
async function isPortInUse(port) {
|
|
3618
|
-
return new Promise((resolve) => {
|
|
3619
|
-
const ws = new WebSocket(`ws://localhost:${port}`);
|
|
3620
|
-
const timer = setTimeout(() => {
|
|
3621
|
-
ws.close();
|
|
3622
|
-
resolve(false);
|
|
3623
|
-
}, 1e3);
|
|
3624
|
-
ws.on("open", () => {
|
|
3625
|
-
clearTimeout(timer);
|
|
3626
|
-
ws.close();
|
|
3627
|
-
resolve(true);
|
|
3628
|
-
});
|
|
3629
|
-
ws.on("error", () => {
|
|
3630
|
-
clearTimeout(timer);
|
|
3631
|
-
resolve(false);
|
|
3632
|
-
});
|
|
3633
|
-
});
|
|
3634
|
-
}
|
|
3648
|
+
init_ws_client();
|
|
3635
3649
|
|
|
3636
3650
|
// src/ai/provider.ts
|
|
3637
3651
|
init_config();
|
|
@@ -4293,7 +4307,98 @@ function createDefaultRegistry() {
|
|
|
4293
4307
|
// src/index.tsx
|
|
4294
4308
|
init_store();
|
|
4295
4309
|
init_config();
|
|
4296
|
-
var
|
|
4310
|
+
var CACHE_DIR = path4.join(os3.homedir(), ".dominus");
|
|
4311
|
+
var CACHE_FILE = path4.join(CACHE_DIR, "update-check.json");
|
|
4312
|
+
var CHECK_INTERVAL_MS = 4 * 60 * 60 * 1e3;
|
|
4313
|
+
var NPM_PACKAGE = "dominus-cli";
|
|
4314
|
+
function readCache() {
|
|
4315
|
+
try {
|
|
4316
|
+
if (fs5.existsSync(CACHE_FILE)) {
|
|
4317
|
+
return JSON.parse(fs5.readFileSync(CACHE_FILE, "utf-8"));
|
|
4318
|
+
}
|
|
4319
|
+
} catch {
|
|
4320
|
+
}
|
|
4321
|
+
return { lastCheck: 0, latestVersion: null };
|
|
4322
|
+
}
|
|
4323
|
+
function writeCache(cache) {
|
|
4324
|
+
try {
|
|
4325
|
+
if (!fs5.existsSync(CACHE_DIR)) fs5.mkdirSync(CACHE_DIR, { recursive: true });
|
|
4326
|
+
fs5.writeFileSync(CACHE_FILE, JSON.stringify(cache), "utf-8");
|
|
4327
|
+
} catch {
|
|
4328
|
+
}
|
|
4329
|
+
}
|
|
4330
|
+
function fetchLatestVersion() {
|
|
4331
|
+
return new Promise((resolve) => {
|
|
4332
|
+
const req = https.get(
|
|
4333
|
+
`https://registry.npmjs.org/${NPM_PACKAGE}/latest`,
|
|
4334
|
+
{ headers: { Accept: "application/json" }, timeout: 5e3 },
|
|
4335
|
+
(res) => {
|
|
4336
|
+
if (res.statusCode !== 200) {
|
|
4337
|
+
resolve(null);
|
|
4338
|
+
res.resume();
|
|
4339
|
+
return;
|
|
4340
|
+
}
|
|
4341
|
+
let body = "";
|
|
4342
|
+
res.on("data", (c) => body += c);
|
|
4343
|
+
res.on("end", () => {
|
|
4344
|
+
try {
|
|
4345
|
+
resolve(JSON.parse(body).version ?? null);
|
|
4346
|
+
} catch {
|
|
4347
|
+
resolve(null);
|
|
4348
|
+
}
|
|
4349
|
+
});
|
|
4350
|
+
}
|
|
4351
|
+
);
|
|
4352
|
+
req.on("error", () => resolve(null));
|
|
4353
|
+
req.on("timeout", () => {
|
|
4354
|
+
req.destroy();
|
|
4355
|
+
resolve(null);
|
|
4356
|
+
});
|
|
4357
|
+
});
|
|
4358
|
+
}
|
|
4359
|
+
function compareVersions(current, latest) {
|
|
4360
|
+
const a = current.split(".").map(Number);
|
|
4361
|
+
const b = latest.split(".").map(Number);
|
|
4362
|
+
for (let i = 0; i < 3; i++) {
|
|
4363
|
+
if ((b[i] ?? 0) > (a[i] ?? 0)) return 1;
|
|
4364
|
+
if ((b[i] ?? 0) < (a[i] ?? 0)) return -1;
|
|
4365
|
+
}
|
|
4366
|
+
return 0;
|
|
4367
|
+
}
|
|
4368
|
+
async function checkForUpdate(currentVersion) {
|
|
4369
|
+
try {
|
|
4370
|
+
const cache = readCache();
|
|
4371
|
+
const now = Date.now();
|
|
4372
|
+
if (now - cache.lastCheck < CHECK_INTERVAL_MS && cache.latestVersion) {
|
|
4373
|
+
if (compareVersions(currentVersion, cache.latestVersion) > 0) {
|
|
4374
|
+
return { updateAvailable: true, currentVersion, latestVersion: cache.latestVersion };
|
|
4375
|
+
}
|
|
4376
|
+
return null;
|
|
4377
|
+
}
|
|
4378
|
+
const latest = await fetchLatestVersion();
|
|
4379
|
+
writeCache({ lastCheck: now, latestVersion: latest });
|
|
4380
|
+
if (latest && compareVersions(currentVersion, latest) > 0) {
|
|
4381
|
+
return { updateAvailable: true, currentVersion, latestVersion: latest };
|
|
4382
|
+
}
|
|
4383
|
+
} catch {
|
|
4384
|
+
}
|
|
4385
|
+
return null;
|
|
4386
|
+
}
|
|
4387
|
+
function formatUpdateMessage(info) {
|
|
4388
|
+
const line1 = `Update available: ${info.currentVersion} \u2192 ${info.latestVersion}`;
|
|
4389
|
+
const line2 = `Run: npm i -g ${NPM_PACKAGE}`;
|
|
4390
|
+
const width = Math.max(line1.length, line2.length) + 4;
|
|
4391
|
+
const pad = (s) => ` \u2502 ${s}${" ".repeat(width - s.length - 4)} \u2502`;
|
|
4392
|
+
return [
|
|
4393
|
+
"",
|
|
4394
|
+
` \u256D${"\u2500".repeat(width)}\u256E`,
|
|
4395
|
+
pad(line1),
|
|
4396
|
+
pad(line2),
|
|
4397
|
+
` \u2570${"\u2500".repeat(width)}\u256F`,
|
|
4398
|
+
""
|
|
4399
|
+
].join("\n");
|
|
4400
|
+
}
|
|
4401
|
+
var VERSION = "0.2.1";
|
|
4297
4402
|
var program = new Command();
|
|
4298
4403
|
program.name("dominus").description("AI-powered autonomous agent for Roblox Studio development").version(VERSION);
|
|
4299
4404
|
program.command("start", { isDefault: true }).description("Launch the Dominus TUI agent").option("-p, --port <port>", "WebSocket server port", "18088").action(async (opts) => {
|
|
@@ -4313,6 +4418,10 @@ program.command("start", { isDefault: true }).description("Launch the Dominus TU
|
|
|
4313
4418
|
setConfigValue("userName", firstName);
|
|
4314
4419
|
}
|
|
4315
4420
|
}
|
|
4421
|
+
checkForUpdate(VERSION).then((info) => {
|
|
4422
|
+
if (info) process.stderr.write(formatUpdateMessage(info));
|
|
4423
|
+
}).catch(() => {
|
|
4424
|
+
});
|
|
4316
4425
|
await launchApp(port);
|
|
4317
4426
|
});
|
|
4318
4427
|
var configCmd = program.command("config").description("Manage Dominus configuration");
|
|
@@ -4403,24 +4512,24 @@ rulesCmd.command("list").description("List all rules").action(() => {
|
|
|
4403
4512
|
});
|
|
4404
4513
|
program.command("install-plugin").description("Build & install the Dominus plugin into Roblox Studio").action(async () => {
|
|
4405
4514
|
const { execSync: execSync2 } = await import('child_process');
|
|
4406
|
-
const packageRoot =
|
|
4407
|
-
const pluginDir =
|
|
4408
|
-
const pluginProject =
|
|
4409
|
-
const studioPlugins =
|
|
4410
|
-
|
|
4515
|
+
const packageRoot = path4.resolve(import.meta.dirname ?? __dirname, "..");
|
|
4516
|
+
const pluginDir = path4.join(packageRoot, "plugin");
|
|
4517
|
+
const pluginProject = path4.join(pluginDir, "default.project.json");
|
|
4518
|
+
const studioPlugins = path4.join(
|
|
4519
|
+
os3.homedir(),
|
|
4411
4520
|
"AppData",
|
|
4412
4521
|
"Local",
|
|
4413
4522
|
"Roblox",
|
|
4414
4523
|
"Plugins"
|
|
4415
4524
|
);
|
|
4416
|
-
const destFile =
|
|
4417
|
-
if (!
|
|
4525
|
+
const destFile = path4.join(studioPlugins, "Dominus.rbxm");
|
|
4526
|
+
if (!fs5.existsSync(pluginProject)) {
|
|
4418
4527
|
console.log(" \u2716 Plugin source not found.");
|
|
4419
4528
|
console.log(` Expected at: ${pluginDir}`);
|
|
4420
4529
|
return;
|
|
4421
4530
|
}
|
|
4422
|
-
if (!
|
|
4423
|
-
|
|
4531
|
+
if (!fs5.existsSync(studioPlugins)) {
|
|
4532
|
+
fs5.mkdirSync(studioPlugins, { recursive: true });
|
|
4424
4533
|
}
|
|
4425
4534
|
let rojoAvailable = false;
|
|
4426
4535
|
try {
|
|
@@ -4447,49 +4556,49 @@ program.command("mcp").description("Start the Dominus MCP server (for Cursor, Cl
|
|
|
4447
4556
|
await startMcpServer2();
|
|
4448
4557
|
});
|
|
4449
4558
|
function getMcpTargets() {
|
|
4450
|
-
const home =
|
|
4451
|
-
const appData = process.env.APPDATA ||
|
|
4452
|
-
const localAppData = process.env.LOCALAPPDATA ||
|
|
4559
|
+
const home = os3.homedir();
|
|
4560
|
+
const appData = process.env.APPDATA || path4.join(home, "AppData", "Roaming");
|
|
4561
|
+
const localAppData = process.env.LOCALAPPDATA || path4.join(home, "AppData", "Local");
|
|
4453
4562
|
const cwd = process.cwd();
|
|
4454
4563
|
return [
|
|
4455
4564
|
{
|
|
4456
4565
|
name: "Cursor (project)",
|
|
4457
|
-
configPath:
|
|
4566
|
+
configPath: path4.join(cwd, ".cursor", "mcp.json"),
|
|
4458
4567
|
keyPath: ["mcpServers"]
|
|
4459
4568
|
},
|
|
4460
4569
|
{
|
|
4461
4570
|
name: "Cursor (global)",
|
|
4462
|
-
configPath:
|
|
4571
|
+
configPath: path4.join(home, ".cursor", "mcp.json"),
|
|
4463
4572
|
keyPath: ["mcpServers"]
|
|
4464
4573
|
},
|
|
4465
4574
|
{
|
|
4466
4575
|
name: "Claude Desktop",
|
|
4467
|
-
configPath:
|
|
4576
|
+
configPath: path4.join(appData, "Claude", "claude_desktop_config.json"),
|
|
4468
4577
|
keyPath: ["mcpServers"]
|
|
4469
4578
|
},
|
|
4470
4579
|
{
|
|
4471
4580
|
name: "Claude Code",
|
|
4472
|
-
configPath:
|
|
4581
|
+
configPath: path4.join(home, ".claude.json"),
|
|
4473
4582
|
keyPath: ["mcpServers"]
|
|
4474
4583
|
},
|
|
4475
4584
|
{
|
|
4476
4585
|
name: "Windsurf (project)",
|
|
4477
|
-
configPath:
|
|
4586
|
+
configPath: path4.join(cwd, ".windsurf", "mcp.json"),
|
|
4478
4587
|
keyPath: ["mcpServers"]
|
|
4479
4588
|
},
|
|
4480
4589
|
{
|
|
4481
4590
|
name: "Windsurf (global)",
|
|
4482
|
-
configPath:
|
|
4591
|
+
configPath: path4.join(home, ".codeium", "windsurf", "mcp_config.json"),
|
|
4483
4592
|
keyPath: ["mcpServers"]
|
|
4484
4593
|
},
|
|
4485
4594
|
{
|
|
4486
4595
|
name: "VS Code (project)",
|
|
4487
|
-
configPath:
|
|
4596
|
+
configPath: path4.join(cwd, ".vscode", "mcp.json"),
|
|
4488
4597
|
keyPath: ["servers"]
|
|
4489
4598
|
},
|
|
4490
4599
|
{
|
|
4491
4600
|
name: "VS Code (global)",
|
|
4492
|
-
configPath:
|
|
4601
|
+
configPath: path4.join(localAppData, "Code", "User", "settings.json"),
|
|
4493
4602
|
keyPath: ["mcp", "servers"]
|
|
4494
4603
|
}
|
|
4495
4604
|
];
|
|
@@ -4501,9 +4610,9 @@ var dominusEntry = {
|
|
|
4501
4610
|
};
|
|
4502
4611
|
function injectMcpConfig(target) {
|
|
4503
4612
|
let config = {};
|
|
4504
|
-
if (
|
|
4613
|
+
if (fs5.existsSync(target.configPath)) {
|
|
4505
4614
|
try {
|
|
4506
|
-
config = JSON.parse(
|
|
4615
|
+
config = JSON.parse(fs5.readFileSync(target.configPath, "utf-8"));
|
|
4507
4616
|
} catch {
|
|
4508
4617
|
config = {};
|
|
4509
4618
|
}
|
|
@@ -4519,12 +4628,12 @@ function injectMcpConfig(target) {
|
|
|
4519
4628
|
return { status: "already" };
|
|
4520
4629
|
}
|
|
4521
4630
|
obj["dominus"] = { ...dominusEntry };
|
|
4522
|
-
const dir =
|
|
4523
|
-
if (!
|
|
4524
|
-
|
|
4631
|
+
const dir = path4.dirname(target.configPath);
|
|
4632
|
+
if (!fs5.existsSync(dir)) {
|
|
4633
|
+
fs5.mkdirSync(dir, { recursive: true });
|
|
4525
4634
|
}
|
|
4526
|
-
|
|
4527
|
-
return { status:
|
|
4635
|
+
fs5.writeFileSync(target.configPath, JSON.stringify(config, null, 2), "utf-8");
|
|
4636
|
+
return { status: fs5.existsSync(target.configPath) ? "installed" : "created" };
|
|
4528
4637
|
}
|
|
4529
4638
|
program.command("mcp-install").description("Auto-install Dominus MCP into Cursor, VS Code, Claude Desktop, Windsurf, etc.").option("-a, --all", "Install to all detected editors").action(async (opts) => {
|
|
4530
4639
|
const targets = getMcpTargets();
|
|
@@ -4546,7 +4655,7 @@ program.command("mcp-install").description("Auto-install Dominus MCP into Cursor
|
|
|
4546
4655
|
}
|
|
4547
4656
|
const detected = targets.map((t) => ({
|
|
4548
4657
|
target: t,
|
|
4549
|
-
exists:
|
|
4658
|
+
exists: fs5.existsSync(path4.dirname(t.configPath))
|
|
4550
4659
|
}));
|
|
4551
4660
|
const toInstall = detected.filter((d) => d.exists);
|
|
4552
4661
|
if (toInstall.length === 0) {
|
|
@@ -4619,23 +4728,23 @@ program.command("mcp-setup").description("Print MCP configuration for manual set
|
|
|
4619
4728
|
console.log("");
|
|
4620
4729
|
});
|
|
4621
4730
|
function buildPluginXml(pluginDir, destFile) {
|
|
4622
|
-
const srcDir =
|
|
4623
|
-
if (!
|
|
4731
|
+
const srcDir = path4.join(pluginDir, "src");
|
|
4732
|
+
if (!fs5.existsSync(srcDir)) {
|
|
4624
4733
|
console.log(` \u2716 Plugin source directory not found: ${srcDir}`);
|
|
4625
4734
|
return;
|
|
4626
4735
|
}
|
|
4627
|
-
const files =
|
|
4736
|
+
const files = fs5.readdirSync(srcDir).filter((f) => f.endsWith(".lua"));
|
|
4628
4737
|
const mainFile = files.find((f) => f === "init.server.lua");
|
|
4629
4738
|
const moduleFiles = files.filter((f) => f !== "init.server.lua");
|
|
4630
4739
|
if (!mainFile) {
|
|
4631
4740
|
console.log(" \u2716 init.server.lua not found in plugin source");
|
|
4632
4741
|
return;
|
|
4633
4742
|
}
|
|
4634
|
-
const mainSource =
|
|
4743
|
+
const mainSource = fs5.readFileSync(path4.join(srcDir, mainFile), "utf-8");
|
|
4635
4744
|
let moduleItems = "";
|
|
4636
4745
|
for (const mf of moduleFiles) {
|
|
4637
4746
|
const modName = mf.replace(".lua", "");
|
|
4638
|
-
const modSource =
|
|
4747
|
+
const modSource = fs5.readFileSync(path4.join(srcDir, mf), "utf-8");
|
|
4639
4748
|
moduleItems += `
|
|
4640
4749
|
<Item class="ModuleScript" referent="${modName}">
|
|
4641
4750
|
<Properties>
|
|
@@ -4653,7 +4762,7 @@ function buildPluginXml(pluginDir, destFile) {
|
|
|
4653
4762
|
</Properties>${moduleItems}
|
|
4654
4763
|
</Item>
|
|
4655
4764
|
</roblox>`;
|
|
4656
|
-
|
|
4765
|
+
fs5.writeFileSync(destFile, rbxmx, "utf-8");
|
|
4657
4766
|
console.log(` \u2714 Plugin installed to: ${destFile}`);
|
|
4658
4767
|
console.log(" Restart Roblox Studio to load the plugin.");
|
|
4659
4768
|
}
|