dominus-cli 0.6.0 → 2.0.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 +2714 -512
- package/dist/index.js.map +1 -1
- package/dist/mcp.js +3786 -294
- package/dist/mcp.js.map +1 -1
- package/package.json +1 -1
- package/plugin/Dominus.rbxm +0 -0
- package/plugin/src/Properties.lua +69 -9
package/dist/mcp.js
CHANGED
|
@@ -1,71 +1,2456 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
import { nanoid } from 'nanoid';
|
|
3
|
+
import fs3 from 'fs';
|
|
4
|
+
import path2 from 'path';
|
|
5
|
+
import os from 'os';
|
|
6
|
+
import initSqlJs from 'sql.js';
|
|
2
7
|
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
3
8
|
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
4
9
|
import { z } from 'zod';
|
|
5
10
|
import { WebSocket, WebSocketServer } from 'ws';
|
|
6
11
|
import { EventEmitter } from 'events';
|
|
7
|
-
import
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
+
import OpenAI from 'openai';
|
|
13
|
+
|
|
14
|
+
var __defProp = Object.defineProperty;
|
|
15
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
16
|
+
var __esm = (fn, res) => function __init() {
|
|
17
|
+
return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
|
|
18
|
+
};
|
|
19
|
+
var __export = (target, all) => {
|
|
20
|
+
for (var name in all)
|
|
21
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
22
|
+
};
|
|
23
|
+
function createMessage(type, payload) {
|
|
24
|
+
return { id: nanoid(), type, payload, ts: Date.now() };
|
|
25
|
+
}
|
|
26
|
+
function parseMessage(raw) {
|
|
27
|
+
try {
|
|
28
|
+
return JSON.parse(raw);
|
|
29
|
+
} catch {
|
|
30
|
+
throw new Error(`Failed to parse WebSocket message: ${raw.slice(0, 100)}`);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
function serializeMessage(msg) {
|
|
34
|
+
return JSON.stringify(msg);
|
|
35
|
+
}
|
|
36
|
+
var StudioMsg, CliMsg;
|
|
37
|
+
var init_protocol = __esm({
|
|
38
|
+
"src/transport/protocol.ts"() {
|
|
39
|
+
StudioMsg = {
|
|
40
|
+
CONNECTED: "studio:connected",
|
|
41
|
+
DISCONNECTED: "studio:disconnected",
|
|
42
|
+
EXPLORER: "studio:explorer",
|
|
43
|
+
OUTPUT: "studio:output",
|
|
44
|
+
ERROR: "studio:error",
|
|
45
|
+
SELECTION: "studio:selection",
|
|
46
|
+
SCRIPT_CONTENT: "studio:script:content",
|
|
47
|
+
SCRIPT_CHANGED: "studio:script:changed",
|
|
48
|
+
RESPONSE: "studio:response",
|
|
49
|
+
REFLECTION: "studio:reflection",
|
|
50
|
+
TEST_RESULTS: "studio:test:results"
|
|
51
|
+
};
|
|
52
|
+
CliMsg = {
|
|
53
|
+
GET_EXPLORER: "cli:get:explorer",
|
|
54
|
+
GET_SCRIPT: "cli:get:script",
|
|
55
|
+
SET_SCRIPT: "cli:set:script",
|
|
56
|
+
RUN_CODE: "cli:run",
|
|
57
|
+
GET_PROPERTIES: "cli:get:properties",
|
|
58
|
+
GET_DESCENDANTS_PROPERTIES: "cli:get:descendants:properties",
|
|
59
|
+
SET_PROPERTIES: "cli:set:properties",
|
|
60
|
+
INSERT_INSTANCE: "cli:insert",
|
|
61
|
+
DELETE_INSTANCE: "cli:delete",
|
|
62
|
+
GET_SELECTION: "cli:get:selection",
|
|
63
|
+
SELECT: "cli:select",
|
|
64
|
+
SEARCH_SCRIPTS: "cli:search:scripts",
|
|
65
|
+
GET_OUTPUT: "cli:get:output",
|
|
66
|
+
RUN_TESTS: "cli:run:tests",
|
|
67
|
+
EXECUTE_RUN_TEST: "cli:test:run",
|
|
68
|
+
EXECUTE_PLAY_TEST: "cli:test:play",
|
|
69
|
+
END_TEST: "cli:test:end",
|
|
70
|
+
BUILD_UI: "cli:build:ui",
|
|
71
|
+
GET_REFLECTION: "cli:get:reflection",
|
|
72
|
+
UPLOAD_ASSET: "cli:upload:asset",
|
|
73
|
+
SERIALIZE_UI: "cli:serialize:ui",
|
|
74
|
+
MOVE_INSTANCE: "cli:move:instance",
|
|
75
|
+
UNDO: "cli:undo",
|
|
76
|
+
REDO: "cli:redo",
|
|
77
|
+
BULK_SET_PROPERTIES: "cli:bulk:set:properties",
|
|
78
|
+
FIND_REPLACE_SCRIPTS: "cli:find:replace:scripts"
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
});
|
|
82
|
+
function resolveProviderSettings(config) {
|
|
83
|
+
const providerInfo = PROVIDERS[config.provider];
|
|
84
|
+
return {
|
|
85
|
+
baseUrl: config.apiBaseUrl || providerInfo?.baseUrl || PROVIDERS.openrouter.baseUrl,
|
|
86
|
+
model: config.model || providerInfo?.defaultModel || "gpt-4o"
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
function ensureDir() {
|
|
90
|
+
if (!fs3.existsSync(DOMINUS_DIR)) {
|
|
91
|
+
fs3.mkdirSync(DOMINUS_DIR, { recursive: true });
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
function readJson(filePath, defaults) {
|
|
95
|
+
try {
|
|
96
|
+
if (fs3.existsSync(filePath)) {
|
|
97
|
+
const raw = fs3.readFileSync(filePath, "utf-8");
|
|
98
|
+
return { ...defaults, ...JSON.parse(raw) };
|
|
99
|
+
}
|
|
100
|
+
} catch {
|
|
101
|
+
}
|
|
102
|
+
return { ...defaults };
|
|
103
|
+
}
|
|
104
|
+
function writeJson(filePath, data) {
|
|
105
|
+
ensureDir();
|
|
106
|
+
fs3.writeFileSync(filePath, JSON.stringify(data, null, 2), "utf-8");
|
|
107
|
+
}
|
|
108
|
+
function loadConfig() {
|
|
109
|
+
ensureDir();
|
|
110
|
+
return readJson(CONFIG_PATH, DEFAULTS);
|
|
111
|
+
}
|
|
112
|
+
function saveConfig(config) {
|
|
113
|
+
writeJson(CONFIG_PATH, config);
|
|
114
|
+
}
|
|
115
|
+
function getDbPath() {
|
|
116
|
+
ensureDir();
|
|
117
|
+
return DB_PATH;
|
|
118
|
+
}
|
|
119
|
+
function createConfigAccess() {
|
|
120
|
+
return {
|
|
121
|
+
get(key) {
|
|
122
|
+
const config = loadConfig();
|
|
123
|
+
return config[key];
|
|
124
|
+
},
|
|
125
|
+
set(key, value) {
|
|
126
|
+
const config = loadConfig();
|
|
127
|
+
config[key] = value;
|
|
128
|
+
saveConfig(config);
|
|
129
|
+
},
|
|
130
|
+
getAll() {
|
|
131
|
+
return loadConfig();
|
|
132
|
+
}
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
var DOMINUS_DIR, CONFIG_PATH, DB_PATH, PROVIDERS, DEFAULTS;
|
|
136
|
+
var init_config = __esm({
|
|
137
|
+
"src/config/config.ts"() {
|
|
138
|
+
DOMINUS_DIR = path2.join(os.homedir(), ".dominus");
|
|
139
|
+
CONFIG_PATH = path2.join(DOMINUS_DIR, "config.json");
|
|
140
|
+
DB_PATH = path2.join(DOMINUS_DIR, "dominus.db");
|
|
141
|
+
path2.join(DOMINUS_DIR, "rules.json");
|
|
142
|
+
PROVIDERS = {
|
|
143
|
+
openai: {
|
|
144
|
+
name: "OpenAI",
|
|
145
|
+
baseUrl: "https://api.openai.com/v1",
|
|
146
|
+
envVar: "OPENAI_API_KEY",
|
|
147
|
+
defaultModel: "gpt-5.3-codex"
|
|
148
|
+
},
|
|
149
|
+
anthropic: {
|
|
150
|
+
name: "Anthropic",
|
|
151
|
+
baseUrl: "https://api.anthropic.com/v1/",
|
|
152
|
+
envVar: "ANTHROPIC_API_KEY",
|
|
153
|
+
defaultModel: "claude-sonnet-4-6"
|
|
154
|
+
},
|
|
155
|
+
gemini: {
|
|
156
|
+
name: "Google Gemini",
|
|
157
|
+
baseUrl: "https://generativelanguage.googleapis.com/v1beta/openai",
|
|
158
|
+
envVar: "GEMINI_API_KEY",
|
|
159
|
+
defaultModel: "gemini-3.1-pro"
|
|
160
|
+
},
|
|
161
|
+
xai: {
|
|
162
|
+
name: "xAI (Grok)",
|
|
163
|
+
baseUrl: "https://api.x.ai/v1",
|
|
164
|
+
envVar: "XAI_API_KEY",
|
|
165
|
+
defaultModel: "grok-4.2"
|
|
166
|
+
},
|
|
167
|
+
openrouter: {
|
|
168
|
+
name: "OpenRouter",
|
|
169
|
+
baseUrl: "https://openrouter.ai/api/v1",
|
|
170
|
+
envVar: "OPENROUTER_API_KEY",
|
|
171
|
+
defaultModel: "anthropic/claude-sonnet-4-6"
|
|
172
|
+
},
|
|
173
|
+
custom: {
|
|
174
|
+
name: "Custom",
|
|
175
|
+
baseUrl: "",
|
|
176
|
+
envVar: "",
|
|
177
|
+
defaultModel: ""
|
|
178
|
+
}
|
|
179
|
+
};
|
|
180
|
+
DEFAULTS = {
|
|
181
|
+
provider: "openai",
|
|
182
|
+
apiBaseUrl: "",
|
|
183
|
+
model: "",
|
|
184
|
+
port: 18088,
|
|
185
|
+
maxToolIterations: 25,
|
|
186
|
+
maxVerifyRetries: 3,
|
|
187
|
+
tokenBudget: 128e3,
|
|
188
|
+
autoIndex: true,
|
|
189
|
+
autoLearn: true,
|
|
190
|
+
userName: "Developer"
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
});
|
|
194
|
+
function persist() {
|
|
195
|
+
if (!db) return;
|
|
196
|
+
const data = db.export();
|
|
197
|
+
fs3.writeFileSync(dbPath, Buffer.from(data));
|
|
198
|
+
}
|
|
199
|
+
async function initMemoryStore() {
|
|
200
|
+
if (db) return;
|
|
201
|
+
dbPath = getDbPath();
|
|
202
|
+
const SQL = await initSqlJs();
|
|
203
|
+
if (fs3.existsSync(dbPath)) {
|
|
204
|
+
const buffer = fs3.readFileSync(dbPath);
|
|
205
|
+
db = new SQL.Database(buffer);
|
|
206
|
+
} else {
|
|
207
|
+
db = new SQL.Database();
|
|
208
|
+
}
|
|
209
|
+
db.run(SCHEMA);
|
|
210
|
+
persist();
|
|
211
|
+
}
|
|
212
|
+
function getDb() {
|
|
213
|
+
if (!db) throw new Error("Memory store not initialized. Call initMemoryStore() first.");
|
|
214
|
+
return db;
|
|
215
|
+
}
|
|
216
|
+
function storeFact(fact) {
|
|
217
|
+
const d = getDb();
|
|
218
|
+
d.run(
|
|
219
|
+
`INSERT INTO facts (project_id, content, category, source, relevance, created_at, accessed_at)
|
|
220
|
+
VALUES (?, ?, ?, ?, ?, ?, ?)`,
|
|
221
|
+
[
|
|
222
|
+
fact.projectId,
|
|
223
|
+
fact.content,
|
|
224
|
+
fact.category,
|
|
225
|
+
fact.source ?? null,
|
|
226
|
+
fact.relevance,
|
|
227
|
+
fact.createdAt,
|
|
228
|
+
fact.accessedAt
|
|
229
|
+
]
|
|
230
|
+
);
|
|
231
|
+
persist();
|
|
232
|
+
const stmt = d.prepare("SELECT last_insert_rowid()");
|
|
233
|
+
stmt.step();
|
|
234
|
+
const row = stmt.get();
|
|
235
|
+
stmt.free();
|
|
236
|
+
return row?.[0] ?? 0;
|
|
237
|
+
}
|
|
238
|
+
function recallFacts(projectId, query, limit = 10) {
|
|
239
|
+
const d = getDb();
|
|
240
|
+
const keywords = query.toLowerCase().split(/\s+/).filter((w) => w.length > 2);
|
|
241
|
+
if (keywords.length === 0) {
|
|
242
|
+
const result2 = d.exec(
|
|
243
|
+
`SELECT * FROM facts WHERE project_id = ? ORDER BY relevance DESC, accessed_at DESC LIMIT ?`,
|
|
244
|
+
[projectId, limit]
|
|
245
|
+
);
|
|
246
|
+
return rowsToFacts(result2);
|
|
247
|
+
}
|
|
248
|
+
const likeClauses = keywords.map(() => `LOWER(content) LIKE ?`).join(" OR ");
|
|
249
|
+
const likeParams = keywords.map((k) => `%${k}%`);
|
|
250
|
+
const result = d.exec(
|
|
251
|
+
`SELECT * FROM facts WHERE project_id = ? AND (${likeClauses})
|
|
252
|
+
ORDER BY relevance DESC, accessed_at DESC LIMIT ?`,
|
|
253
|
+
[projectId, ...likeParams, limit]
|
|
254
|
+
);
|
|
255
|
+
const facts = rowsToFacts(result);
|
|
256
|
+
for (const fact of facts) {
|
|
257
|
+
if (fact.id) {
|
|
258
|
+
d.run(`UPDATE facts SET accessed_at = ? WHERE id = ?`, [Date.now(), fact.id]);
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
if (facts.length > 0) persist();
|
|
262
|
+
return facts;
|
|
263
|
+
}
|
|
264
|
+
function rowsToFacts(result) {
|
|
265
|
+
if (!result[0]) return [];
|
|
266
|
+
return result[0].values.map((row) => ({
|
|
267
|
+
id: row[0],
|
|
268
|
+
projectId: row[1],
|
|
269
|
+
content: row[2],
|
|
270
|
+
category: row[3],
|
|
271
|
+
source: row[4],
|
|
272
|
+
relevance: row[5],
|
|
273
|
+
createdAt: row[6],
|
|
274
|
+
accessedAt: row[7]
|
|
275
|
+
}));
|
|
276
|
+
}
|
|
277
|
+
function getProjectSummaries(projectId, limit = 20) {
|
|
278
|
+
const d = getDb();
|
|
279
|
+
const result = d.exec(
|
|
280
|
+
`SELECT * FROM summaries WHERE project_id = ? ORDER BY created_at DESC LIMIT ?`,
|
|
281
|
+
[projectId, limit]
|
|
282
|
+
);
|
|
283
|
+
if (!result[0]) return [];
|
|
284
|
+
return result[0].values.map((row) => ({
|
|
285
|
+
id: row[0],
|
|
286
|
+
projectId: row[1],
|
|
287
|
+
sessionId: row[2],
|
|
288
|
+
summary: row[3],
|
|
289
|
+
createdAt: row[4]
|
|
290
|
+
}));
|
|
291
|
+
}
|
|
292
|
+
function getScriptIndex(projectId) {
|
|
293
|
+
const d = getDb();
|
|
294
|
+
const result = d.exec(
|
|
295
|
+
`SELECT path, summary, class_name FROM script_index WHERE project_id = ? ORDER BY path`,
|
|
296
|
+
[projectId]
|
|
297
|
+
);
|
|
298
|
+
if (!result[0]) return [];
|
|
299
|
+
return result[0].values.map((row) => ({
|
|
300
|
+
path: row[0],
|
|
301
|
+
summary: row[1] || void 0,
|
|
302
|
+
className: row[2]
|
|
303
|
+
}));
|
|
304
|
+
}
|
|
305
|
+
var db, dbPath, SCHEMA;
|
|
306
|
+
var init_store = __esm({
|
|
307
|
+
"src/memory/store.ts"() {
|
|
308
|
+
init_config();
|
|
309
|
+
db = null;
|
|
310
|
+
SCHEMA = `
|
|
311
|
+
CREATE TABLE IF NOT EXISTS facts (
|
|
312
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
313
|
+
project_id TEXT NOT NULL,
|
|
314
|
+
content TEXT NOT NULL,
|
|
315
|
+
category TEXT NOT NULL,
|
|
316
|
+
source TEXT,
|
|
317
|
+
relevance REAL DEFAULT 1.0,
|
|
318
|
+
created_at INTEGER NOT NULL,
|
|
319
|
+
accessed_at INTEGER NOT NULL
|
|
320
|
+
);
|
|
321
|
+
|
|
322
|
+
CREATE TABLE IF NOT EXISTS summaries (
|
|
323
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
324
|
+
project_id TEXT NOT NULL,
|
|
325
|
+
session_id TEXT NOT NULL,
|
|
326
|
+
summary TEXT NOT NULL,
|
|
327
|
+
created_at INTEGER NOT NULL
|
|
328
|
+
);
|
|
329
|
+
|
|
330
|
+
CREATE TABLE IF NOT EXISTS messages (
|
|
331
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
332
|
+
session_id TEXT NOT NULL,
|
|
333
|
+
role TEXT NOT NULL,
|
|
334
|
+
content TEXT NOT NULL,
|
|
335
|
+
tool_name TEXT,
|
|
336
|
+
created_at INTEGER NOT NULL
|
|
337
|
+
);
|
|
338
|
+
|
|
339
|
+
CREATE TABLE IF NOT EXISTS script_index (
|
|
340
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
341
|
+
project_id TEXT NOT NULL,
|
|
342
|
+
path TEXT NOT NULL,
|
|
343
|
+
class_name TEXT NOT NULL,
|
|
344
|
+
summary TEXT,
|
|
345
|
+
line_count INTEGER,
|
|
346
|
+
last_hash TEXT,
|
|
347
|
+
updated_at INTEGER NOT NULL
|
|
348
|
+
);
|
|
349
|
+
|
|
350
|
+
CREATE INDEX IF NOT EXISTS idx_facts_project ON facts(project_id);
|
|
351
|
+
CREATE INDEX IF NOT EXISTS idx_facts_category ON facts(project_id, category);
|
|
352
|
+
CREATE INDEX IF NOT EXISTS idx_scripts_project ON script_index(project_id);
|
|
353
|
+
CREATE INDEX IF NOT EXISTS idx_messages_session ON messages(session_id);
|
|
354
|
+
CREATE INDEX IF NOT EXISTS idx_summaries_project ON summaries(project_id);
|
|
355
|
+
`;
|
|
356
|
+
}
|
|
357
|
+
});
|
|
358
|
+
async function getRobloxApiReference(name, opts = {}) {
|
|
359
|
+
const normalizedName = normalizeLookupName(name);
|
|
360
|
+
const kinds = opts.kind && opts.kind !== "any" ? [opts.kind] : Object.keys(KIND_FOLDERS);
|
|
361
|
+
for (const kind of kinds) {
|
|
362
|
+
const doc = await readReferenceYaml(normalizedName, kind, opts);
|
|
363
|
+
if (!doc) continue;
|
|
364
|
+
const reference = parseReferenceYaml(doc.content, {
|
|
365
|
+
kind,
|
|
366
|
+
sourceUrl: doc.sourceUrl
|
|
367
|
+
});
|
|
368
|
+
markDefinedOn(reference, reference.name);
|
|
369
|
+
if (opts.includeInherited && reference.kind === "class") {
|
|
370
|
+
const inherited = await collectInheritedMembers(reference, opts, /* @__PURE__ */ new Set([reference.name]));
|
|
371
|
+
reference.inheritedClasses = inherited.classes;
|
|
372
|
+
reference.inheritedMembers = inherited.members;
|
|
373
|
+
}
|
|
374
|
+
return reference;
|
|
375
|
+
}
|
|
376
|
+
throw new Error(`Roblox API reference not found for "${name}"`);
|
|
377
|
+
}
|
|
378
|
+
async function collectInheritedMembers(reference, opts, visited) {
|
|
379
|
+
const classes = [];
|
|
380
|
+
const members = {};
|
|
381
|
+
for (const parent of reference.inherits) {
|
|
382
|
+
if (!parent || visited.has(parent)) continue;
|
|
383
|
+
visited.add(parent);
|
|
384
|
+
const doc = await readReferenceYaml(parent, "class", opts);
|
|
385
|
+
if (!doc) continue;
|
|
386
|
+
const parentReference = parseReferenceYaml(doc.content, {
|
|
387
|
+
kind: "class",
|
|
388
|
+
sourceUrl: doc.sourceUrl
|
|
389
|
+
});
|
|
390
|
+
markDefinedOn(parentReference, parentReference.name);
|
|
391
|
+
classes.push(parentReference.name);
|
|
392
|
+
mergeMembers(members, parentReference.members);
|
|
393
|
+
const inherited = await collectInheritedMembers(parentReference, opts, visited);
|
|
394
|
+
classes.push(...inherited.classes);
|
|
395
|
+
mergeMembers(members, inherited.members);
|
|
396
|
+
}
|
|
397
|
+
return { classes, members };
|
|
398
|
+
}
|
|
399
|
+
function markDefinedOn(reference, className) {
|
|
400
|
+
for (const section of Object.values(reference.members)) {
|
|
401
|
+
for (const member of section) {
|
|
402
|
+
member.definedOn = className;
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
function mergeMembers(target, source) {
|
|
407
|
+
for (const [section, members] of Object.entries(source)) {
|
|
408
|
+
target[section] ??= [];
|
|
409
|
+
target[section].push(...members);
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
async function searchRobloxApi(query, opts = {}) {
|
|
413
|
+
const limit = Math.max(1, Math.min(opts.limit ?? 10, 50));
|
|
414
|
+
const normalizedQuery = normalizeSearchText(query);
|
|
415
|
+
const kinds = opts.kind && opts.kind !== "any" ? [opts.kind] : Object.keys(KIND_FOLDERS);
|
|
416
|
+
const entries = await listReferenceEntries(opts);
|
|
417
|
+
return entries.filter((entry) => kinds.includes(entry.kind)).map((entry) => ({
|
|
418
|
+
...entry,
|
|
419
|
+
score: scoreSearchResult(normalizedQuery, entry.name, entry.kind)
|
|
420
|
+
})).filter((entry) => entry.score > 0).sort((a, b) => b.score - a.score || a.name.localeCompare(b.name)).slice(0, limit);
|
|
421
|
+
}
|
|
422
|
+
function parseReferenceYaml(raw, opts = {}) {
|
|
423
|
+
const kind = opts.kind ?? normalizeKind(readTopScalar(raw, "type")) ?? "class";
|
|
424
|
+
const members = {};
|
|
425
|
+
for (const section of MEMBER_SECTIONS) {
|
|
426
|
+
const parsed = parseMemberSection(raw, section);
|
|
427
|
+
if (parsed.length > 0) {
|
|
428
|
+
members[section] = parsed;
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
return {
|
|
432
|
+
name: readTopScalar(raw, "name") || "Unknown",
|
|
433
|
+
kind,
|
|
434
|
+
summary: cleanDocText(readTopScalar(raw, "summary")),
|
|
435
|
+
description: cleanDocText(readTopScalar(raw, "description")),
|
|
436
|
+
inherits: readTopList(raw, "inherits"),
|
|
437
|
+
tags: readTopList(raw, "tags"),
|
|
438
|
+
deprecationMessage: cleanDocText(readTopScalar(raw, "deprecation_message")),
|
|
439
|
+
sourceUrl: opts.sourceUrl ?? "",
|
|
440
|
+
members
|
|
441
|
+
};
|
|
442
|
+
}
|
|
443
|
+
function normalizeKind(value) {
|
|
444
|
+
if (value === "class" || value === "datatype" || value === "enum" || value === "global" || value === "library") {
|
|
445
|
+
return value;
|
|
446
|
+
}
|
|
447
|
+
return void 0;
|
|
448
|
+
}
|
|
449
|
+
async function readReferenceYaml(name, kind, opts) {
|
|
450
|
+
const localRoot = resolveDocsRoot(opts.docsRoot);
|
|
451
|
+
const folder = KIND_FOLDERS[kind];
|
|
452
|
+
const fileName = `${name}.yaml`;
|
|
453
|
+
if (localRoot) {
|
|
454
|
+
const localPath = path2.join(localRoot, folder, fileName);
|
|
455
|
+
if (fs3.existsSync(localPath)) {
|
|
456
|
+
return {
|
|
457
|
+
content: fs3.readFileSync(localPath, "utf-8"),
|
|
458
|
+
sourceUrl: toCreatorDocsUrl(kind, name)
|
|
459
|
+
};
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
const sourceUrl = `${GITHUB_RAW_BASE}/${folder}/${encodeURIComponent(fileName)}`;
|
|
463
|
+
try {
|
|
464
|
+
return {
|
|
465
|
+
content: await fetchText(sourceUrl),
|
|
466
|
+
sourceUrl: toCreatorDocsUrl(kind, name)
|
|
467
|
+
};
|
|
468
|
+
} catch {
|
|
469
|
+
return null;
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
async function listReferenceEntries(opts) {
|
|
473
|
+
const localRoot = resolveDocsRoot(opts.docsRoot);
|
|
474
|
+
if (localRoot) {
|
|
475
|
+
const entries = [];
|
|
476
|
+
for (const [kind, folder] of Object.entries(KIND_FOLDERS)) {
|
|
477
|
+
const dir = path2.join(localRoot, folder);
|
|
478
|
+
if (!fs3.existsSync(dir)) continue;
|
|
479
|
+
for (const file of fs3.readdirSync(dir)) {
|
|
480
|
+
if (!file.endsWith(".yaml")) continue;
|
|
481
|
+
const name = path2.basename(file, ".yaml");
|
|
482
|
+
entries.push({ name, kind, score: 0, sourceUrl: toCreatorDocsUrl(kind, name) });
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
return entries;
|
|
486
|
+
}
|
|
487
|
+
const tree = await getRemoteTree();
|
|
488
|
+
const prefix = "content/en-us/reference/engine/";
|
|
489
|
+
return tree.filter((entry) => entry.type === "blob").map((entry) => entry.path).filter((entryPath) => entryPath.startsWith(prefix) && entryPath.endsWith(".yaml")).flatMap((entryPath) => {
|
|
490
|
+
const rest = entryPath.slice(prefix.length);
|
|
491
|
+
const [folder, file] = rest.split("/");
|
|
492
|
+
const kind = folderToKind(folder);
|
|
493
|
+
if (!kind || !file) return [];
|
|
494
|
+
const name = path2.basename(file, ".yaml");
|
|
495
|
+
return [{ name, kind, score: 0, sourceUrl: toCreatorDocsUrl(kind, name) }];
|
|
496
|
+
});
|
|
497
|
+
}
|
|
498
|
+
async function getRemoteTree() {
|
|
499
|
+
if (remoteTreeCache) return remoteTreeCache;
|
|
500
|
+
const payload = JSON.parse(await fetchText(GITHUB_TREE_URL));
|
|
501
|
+
remoteTreeCache = payload.tree ?? [];
|
|
502
|
+
return remoteTreeCache;
|
|
503
|
+
}
|
|
504
|
+
function folderToKind(folder) {
|
|
505
|
+
for (const [kind, value] of Object.entries(KIND_FOLDERS)) {
|
|
506
|
+
if (value === folder) return kind;
|
|
507
|
+
}
|
|
508
|
+
return void 0;
|
|
509
|
+
}
|
|
510
|
+
function resolveDocsRoot(explicitRoot) {
|
|
511
|
+
const candidates = [
|
|
512
|
+
explicitRoot,
|
|
513
|
+
process.env.ROBLOX_CREATOR_DOCS_PATH,
|
|
514
|
+
path2.join(process.cwd(), ".tmp", "creator-docs")
|
|
515
|
+
].filter(Boolean);
|
|
516
|
+
for (const candidate of candidates) {
|
|
517
|
+
const normalized = normalizeDocsRoot(candidate);
|
|
518
|
+
if (normalized && fs3.existsSync(normalized)) return normalized;
|
|
519
|
+
}
|
|
520
|
+
return null;
|
|
521
|
+
}
|
|
522
|
+
function normalizeDocsRoot(root) {
|
|
523
|
+
const engineRoot = path2.join(root, "content", "en-us", "reference", "engine");
|
|
524
|
+
if (fs3.existsSync(engineRoot)) return engineRoot;
|
|
525
|
+
const directClasses = path2.join(root, "classes");
|
|
526
|
+
if (fs3.existsSync(directClasses)) return root;
|
|
527
|
+
return null;
|
|
528
|
+
}
|
|
529
|
+
function normalizeLookupName(name) {
|
|
530
|
+
const trimmed = name.trim();
|
|
531
|
+
const ownerName = trimmed.split(":")[0];
|
|
532
|
+
const lastToken = ownerName.split(".").pop() ?? ownerName;
|
|
533
|
+
return lastToken.replace(/[^A-Za-z0-9_]/g, "");
|
|
534
|
+
}
|
|
535
|
+
function normalizeSearchText(value) {
|
|
536
|
+
return value.trim().toLowerCase().replace(/[^a-z0-9_]+/g, "");
|
|
537
|
+
}
|
|
538
|
+
function scoreSearchResult(query, name, kind) {
|
|
539
|
+
if (!query) return 1;
|
|
540
|
+
const normalizedName = normalizeSearchText(name);
|
|
541
|
+
if (normalizedName === query) return 100;
|
|
542
|
+
if (normalizedName.startsWith(query)) return 75;
|
|
543
|
+
if (normalizedName.includes(query)) return 50;
|
|
544
|
+
const kindScore = kind.includes(query) ? 10 : 0;
|
|
545
|
+
return kindScore;
|
|
546
|
+
}
|
|
547
|
+
function toCreatorDocsUrl(kind, name) {
|
|
548
|
+
return `https://create.roblox.com/docs/reference/engine/${KIND_FOLDERS[kind]}/${name}`;
|
|
549
|
+
}
|
|
550
|
+
async function fetchText(url) {
|
|
551
|
+
const response = await fetch(url, {
|
|
552
|
+
headers: { "User-Agent": "Dominus Roblox API docs lookup" }
|
|
553
|
+
});
|
|
554
|
+
if (!response.ok) {
|
|
555
|
+
throw new Error(`Failed to fetch ${url}: ${response.status} ${response.statusText}`);
|
|
556
|
+
}
|
|
557
|
+
return response.text();
|
|
558
|
+
}
|
|
559
|
+
function readTopScalar(raw, key) {
|
|
560
|
+
const lines = raw.split(/\r?\n/);
|
|
561
|
+
for (let i = 0; i < lines.length; i++) {
|
|
562
|
+
const match = lines[i].match(new RegExp(`^${escapeRegExp(key)}:\\s*(.*)$`));
|
|
563
|
+
if (!match) continue;
|
|
564
|
+
return readYamlValue(lines, i, 0, match[1]);
|
|
565
|
+
}
|
|
566
|
+
return "";
|
|
567
|
+
}
|
|
568
|
+
function readTopList(raw, key) {
|
|
569
|
+
const lines = raw.split(/\r?\n/);
|
|
570
|
+
for (let i = 0; i < lines.length; i++) {
|
|
571
|
+
const match = lines[i].match(new RegExp(`^${escapeRegExp(key)}:\\s*(.*)$`));
|
|
572
|
+
if (!match) continue;
|
|
573
|
+
const inline = match[1].trim();
|
|
574
|
+
if (inline === "[]" || inline === "") {
|
|
575
|
+
if (inline === "[]") return [];
|
|
576
|
+
return readIndentedList(lines, i + 1, 2);
|
|
577
|
+
}
|
|
578
|
+
return [unquote(inline)];
|
|
579
|
+
}
|
|
580
|
+
return [];
|
|
581
|
+
}
|
|
582
|
+
function parseMemberSection(raw, section) {
|
|
583
|
+
const sectionLines = extractTopSection(raw, section);
|
|
584
|
+
if (sectionLines.length === 0) return [];
|
|
585
|
+
const items = splitMemberItems(sectionLines);
|
|
586
|
+
return items.map((item) => parseMemberItem(item, section)).filter((item) => Boolean(item));
|
|
587
|
+
}
|
|
588
|
+
function extractTopSection(raw, section) {
|
|
589
|
+
const lines = raw.split(/\r?\n/);
|
|
590
|
+
const start = lines.findIndex((line) => line === `${section}:`);
|
|
591
|
+
if (start === -1) return [];
|
|
592
|
+
const result = [];
|
|
593
|
+
for (let i = start + 1; i < lines.length; i++) {
|
|
594
|
+
if (/^[A-Za-z_][A-Za-z0-9_]*:/.test(lines[i])) break;
|
|
595
|
+
result.push(lines[i]);
|
|
596
|
+
}
|
|
597
|
+
return result;
|
|
598
|
+
}
|
|
599
|
+
function splitMemberItems(lines) {
|
|
600
|
+
const items = [];
|
|
601
|
+
let current = [];
|
|
602
|
+
for (const line of lines) {
|
|
603
|
+
if (/^ - /.test(line)) {
|
|
604
|
+
if (current.length > 0) items.push(current);
|
|
605
|
+
current = [line];
|
|
606
|
+
continue;
|
|
607
|
+
}
|
|
608
|
+
if (current.length > 0) current.push(line);
|
|
609
|
+
}
|
|
610
|
+
if (current.length > 0) items.push(current);
|
|
611
|
+
return items;
|
|
612
|
+
}
|
|
613
|
+
function parseMemberItem(lines, section) {
|
|
614
|
+
const name = readMemberScalar(lines, "name");
|
|
615
|
+
if (!name) return null;
|
|
616
|
+
const member = {
|
|
617
|
+
name,
|
|
618
|
+
memberType: section,
|
|
619
|
+
type: readMemberScalar(lines, "type") || readMemberScalar(lines, "return_type"),
|
|
620
|
+
summary: cleanDocText(readMemberScalar(lines, "summary")),
|
|
621
|
+
description: cleanDocText(readMemberScalar(lines, "description")),
|
|
622
|
+
parameters: readNestedObjects(lines, "parameters"),
|
|
623
|
+
returns: readNestedObjects(lines, "returns"),
|
|
624
|
+
tags: readMemberList(lines, "tags"),
|
|
625
|
+
security: readSecurity(lines),
|
|
626
|
+
threadSafety: readMemberScalar(lines, "thread_safety"),
|
|
627
|
+
category: readMemberScalar(lines, "category"),
|
|
628
|
+
deprecationMessage: cleanDocText(readMemberScalar(lines, "deprecation_message"))
|
|
629
|
+
};
|
|
630
|
+
const value = Number(readMemberScalar(lines, "value"));
|
|
631
|
+
if (Number.isFinite(value)) member.value = value;
|
|
632
|
+
return member;
|
|
633
|
+
}
|
|
634
|
+
function readMemberScalar(lines, key) {
|
|
635
|
+
for (let i = 0; i < lines.length; i++) {
|
|
636
|
+
const firstLinePattern = new RegExp(`^ - ${escapeRegExp(key)}:\\s*(.*)$`);
|
|
637
|
+
const fieldPattern = new RegExp(`^ ${escapeRegExp(key)}:\\s*(.*)$`);
|
|
638
|
+
const firstLineMatch = lines[i].match(firstLinePattern);
|
|
639
|
+
const fieldMatch = lines[i].match(fieldPattern);
|
|
640
|
+
const match = firstLineMatch ?? fieldMatch;
|
|
641
|
+
if (!match) continue;
|
|
642
|
+
return readYamlValue(lines, i, firstLineMatch ? 2 : 4, match[1]);
|
|
643
|
+
}
|
|
644
|
+
return "";
|
|
645
|
+
}
|
|
646
|
+
function readMemberList(lines, key) {
|
|
647
|
+
for (let i = 0; i < lines.length; i++) {
|
|
648
|
+
const match = lines[i].match(new RegExp(`^ ${escapeRegExp(key)}:\\s*(.*)$`));
|
|
649
|
+
if (!match) continue;
|
|
650
|
+
const inline = match[1].trim();
|
|
651
|
+
if (inline === "[]") return [];
|
|
652
|
+
if (inline) return [unquote(inline)];
|
|
653
|
+
return readIndentedList(lines, i + 1, 6);
|
|
654
|
+
}
|
|
655
|
+
return [];
|
|
656
|
+
}
|
|
657
|
+
function readNestedObjects(lines, key) {
|
|
658
|
+
const start = lines.findIndex((line) => line === ` ${key}:`);
|
|
659
|
+
if (start === -1) return [];
|
|
660
|
+
const nested = [];
|
|
661
|
+
for (let i = start + 1; i < lines.length; i++) {
|
|
662
|
+
if (/^ [A-Za-z_][A-Za-z0-9_]*:/.test(lines[i])) break;
|
|
663
|
+
nested.push(lines[i]);
|
|
664
|
+
}
|
|
665
|
+
const items = [];
|
|
666
|
+
let current = [];
|
|
667
|
+
for (const line of nested) {
|
|
668
|
+
if (/^ - /.test(line)) {
|
|
669
|
+
if (current.length > 0) items.push(current);
|
|
670
|
+
current = [line];
|
|
671
|
+
continue;
|
|
672
|
+
}
|
|
673
|
+
if (current.length > 0) current.push(line);
|
|
674
|
+
}
|
|
675
|
+
if (current.length > 0) items.push(current);
|
|
676
|
+
return items.map((item) => ({
|
|
677
|
+
name: readNestedScalar(item, "name"),
|
|
678
|
+
type: readNestedScalar(item, "type"),
|
|
679
|
+
summary: cleanDocText(readNestedScalar(item, "summary")),
|
|
680
|
+
default: readNestedScalar(item, "default")
|
|
681
|
+
}));
|
|
682
|
+
}
|
|
683
|
+
function readNestedScalar(lines, key) {
|
|
684
|
+
for (let i = 0; i < lines.length; i++) {
|
|
685
|
+
const firstLinePattern = new RegExp(`^ - ${escapeRegExp(key)}:\\s*(.*)$`);
|
|
686
|
+
const fieldPattern = new RegExp(`^ ${escapeRegExp(key)}:\\s*(.*)$`);
|
|
687
|
+
const firstLineMatch = lines[i].match(firstLinePattern);
|
|
688
|
+
const fieldMatch = lines[i].match(fieldPattern);
|
|
689
|
+
const match = firstLineMatch ?? fieldMatch;
|
|
690
|
+
if (!match) continue;
|
|
691
|
+
return readYamlValue(lines, i, firstLineMatch ? 6 : 8, match[1]);
|
|
692
|
+
}
|
|
693
|
+
return "";
|
|
694
|
+
}
|
|
695
|
+
function readSecurity(lines) {
|
|
696
|
+
const start = lines.findIndex((line) => line === " security:");
|
|
697
|
+
if (start === -1) return void 0;
|
|
698
|
+
return {
|
|
699
|
+
read: readIndentedScalar(lines, start + 1, "read", 6),
|
|
700
|
+
write: readIndentedScalar(lines, start + 1, "write", 6)
|
|
701
|
+
};
|
|
702
|
+
}
|
|
703
|
+
function readIndentedScalar(lines, start, key, indent) {
|
|
704
|
+
const pattern = new RegExp(`^\\s{${indent}}${escapeRegExp(key)}:\\s*(.*)$`);
|
|
705
|
+
for (let i = start; i < lines.length; i++) {
|
|
706
|
+
if (indentOf(lines[i]) < indent) break;
|
|
707
|
+
const match = lines[i].match(pattern);
|
|
708
|
+
if (match) return unquote(match[1].trim());
|
|
709
|
+
}
|
|
710
|
+
return "";
|
|
711
|
+
}
|
|
712
|
+
function readYamlValue(lines, index, indent, rawValue) {
|
|
713
|
+
const value = rawValue.trim();
|
|
714
|
+
if (value === "|-" || value === "|") {
|
|
715
|
+
return readBlock(lines, index + 1, indent + 2);
|
|
716
|
+
}
|
|
717
|
+
if (value === "''" || value === '""') return "";
|
|
718
|
+
return unquote(value);
|
|
719
|
+
}
|
|
720
|
+
function readBlock(lines, start, minIndent) {
|
|
721
|
+
const block = [];
|
|
722
|
+
for (let i = start; i < lines.length; i++) {
|
|
723
|
+
const line = lines[i];
|
|
724
|
+
if (line.trim() && indentOf(line) < minIndent) break;
|
|
725
|
+
block.push(line.slice(Math.min(minIndent, line.length)));
|
|
726
|
+
}
|
|
727
|
+
return block.join("\n").trim();
|
|
728
|
+
}
|
|
729
|
+
function readIndentedList(lines, start, indent) {
|
|
730
|
+
const pattern = new RegExp(`^\\s{${indent}}-\\s*(.*)$`);
|
|
731
|
+
const values = [];
|
|
732
|
+
for (let i = start; i < lines.length; i++) {
|
|
733
|
+
if (lines[i].trim() && indentOf(lines[i]) < indent) break;
|
|
734
|
+
const match = lines[i].match(pattern);
|
|
735
|
+
if (match) values.push(unquote(match[1].trim()));
|
|
736
|
+
}
|
|
737
|
+
return values;
|
|
738
|
+
}
|
|
739
|
+
function cleanDocText(value) {
|
|
740
|
+
return value.replace(/\r/g, "").replace(/[ \t]+\n/g, "\n").trim();
|
|
741
|
+
}
|
|
742
|
+
function indentOf(line) {
|
|
743
|
+
return line.match(/^ */)?.[0].length ?? 0;
|
|
744
|
+
}
|
|
745
|
+
function unquote(value) {
|
|
746
|
+
if (value.startsWith("'") && value.endsWith("'") || value.startsWith('"') && value.endsWith('"')) {
|
|
747
|
+
return value.slice(1, -1);
|
|
748
|
+
}
|
|
749
|
+
return value;
|
|
750
|
+
}
|
|
751
|
+
function escapeRegExp(value) {
|
|
752
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
753
|
+
}
|
|
754
|
+
var GITHUB_RAW_BASE, GITHUB_TREE_URL, KIND_FOLDERS, MEMBER_SECTIONS, remoteTreeCache;
|
|
755
|
+
var init_roblox_api = __esm({
|
|
756
|
+
"src/docs/roblox-api.ts"() {
|
|
757
|
+
GITHUB_RAW_BASE = "https://raw.githubusercontent.com/Roblox/creator-docs/main/content/en-us/reference/engine";
|
|
758
|
+
GITHUB_TREE_URL = "https://api.github.com/repos/Roblox/creator-docs/git/trees/main?recursive=1";
|
|
759
|
+
KIND_FOLDERS = {
|
|
760
|
+
class: "classes",
|
|
761
|
+
datatype: "datatypes",
|
|
762
|
+
enum: "enums",
|
|
763
|
+
global: "globals",
|
|
764
|
+
library: "libraries"
|
|
765
|
+
};
|
|
766
|
+
MEMBER_SECTIONS = [
|
|
767
|
+
"properties",
|
|
768
|
+
"methods",
|
|
769
|
+
"events",
|
|
770
|
+
"callbacks",
|
|
771
|
+
"constructors",
|
|
772
|
+
"functions",
|
|
773
|
+
"items",
|
|
774
|
+
"constants",
|
|
775
|
+
"math_operations"
|
|
776
|
+
];
|
|
777
|
+
remoteTreeCache = null;
|
|
778
|
+
}
|
|
779
|
+
});
|
|
780
|
+
|
|
781
|
+
// src/tools/studio/read-script.ts
|
|
782
|
+
var read_script_exports = {};
|
|
783
|
+
__export(read_script_exports, {
|
|
784
|
+
tool: () => tool
|
|
785
|
+
});
|
|
786
|
+
var tool;
|
|
787
|
+
var init_read_script = __esm({
|
|
788
|
+
"src/tools/studio/read-script.ts"() {
|
|
789
|
+
init_protocol();
|
|
790
|
+
tool = {
|
|
791
|
+
name: "read_script",
|
|
792
|
+
description: 'Read the source code of a script in Roblox Studio. Provide the full instance path (e.g., "ServerScriptService.GameManager").',
|
|
793
|
+
parameters: {
|
|
794
|
+
type: "object",
|
|
795
|
+
properties: {
|
|
796
|
+
path: {
|
|
797
|
+
type: "string",
|
|
798
|
+
description: 'Full instance path of the script (e.g., "ServerScriptService.GameManager")'
|
|
799
|
+
}
|
|
800
|
+
},
|
|
801
|
+
required: ["path"]
|
|
802
|
+
},
|
|
803
|
+
async execute(params, ctx) {
|
|
804
|
+
if (!ctx.isStudioConnected()) {
|
|
805
|
+
return { success: false, error: "Studio is not connected" };
|
|
806
|
+
}
|
|
807
|
+
const path4 = params.path;
|
|
808
|
+
const response = await ctx.sendToStudio(CliMsg.GET_SCRIPT, { path: path4 });
|
|
809
|
+
if (!response.payload) {
|
|
810
|
+
return { success: false, error: `Script not found: ${path4}` };
|
|
811
|
+
}
|
|
812
|
+
return {
|
|
813
|
+
success: true,
|
|
814
|
+
data: {
|
|
815
|
+
path: response.payload.path,
|
|
816
|
+
className: response.payload.className,
|
|
817
|
+
source: response.payload.source,
|
|
818
|
+
lineCount: response.payload.lineCount
|
|
819
|
+
}
|
|
820
|
+
};
|
|
821
|
+
}
|
|
822
|
+
};
|
|
823
|
+
}
|
|
824
|
+
});
|
|
825
|
+
|
|
826
|
+
// src/tools/studio/edit-script.ts
|
|
827
|
+
var edit_script_exports = {};
|
|
828
|
+
__export(edit_script_exports, {
|
|
829
|
+
tool: () => tool2
|
|
830
|
+
});
|
|
831
|
+
var tool2;
|
|
832
|
+
var init_edit_script = __esm({
|
|
833
|
+
"src/tools/studio/edit-script.ts"() {
|
|
834
|
+
init_protocol();
|
|
835
|
+
tool2 = {
|
|
836
|
+
name: "edit_script",
|
|
837
|
+
description: "Replace the entire source code of a script in Roblox Studio. Provide the full path and the new source code.",
|
|
838
|
+
parameters: {
|
|
839
|
+
type: "object",
|
|
840
|
+
properties: {
|
|
841
|
+
path: {
|
|
842
|
+
type: "string",
|
|
843
|
+
description: "Full instance path of the script"
|
|
844
|
+
},
|
|
845
|
+
source: {
|
|
846
|
+
type: "string",
|
|
847
|
+
description: "The new complete source code for the script"
|
|
848
|
+
}
|
|
849
|
+
},
|
|
850
|
+
required: ["path", "source"]
|
|
851
|
+
},
|
|
852
|
+
async execute(params, ctx) {
|
|
853
|
+
if (!ctx.isStudioConnected()) {
|
|
854
|
+
return { success: false, error: "Studio is not connected" };
|
|
855
|
+
}
|
|
856
|
+
const path4 = params.path;
|
|
857
|
+
const source = params.source;
|
|
858
|
+
const response = await ctx.sendToStudio(CliMsg.SET_SCRIPT, { path: path4, source });
|
|
859
|
+
const payload = response.payload;
|
|
860
|
+
if (!payload.success) {
|
|
861
|
+
return { success: false, error: payload.error ?? "Failed to edit script" };
|
|
862
|
+
}
|
|
863
|
+
return { success: true, data: { path: path4, linesWritten: source.split("\n").length } };
|
|
864
|
+
}
|
|
865
|
+
};
|
|
866
|
+
}
|
|
867
|
+
});
|
|
868
|
+
|
|
869
|
+
// src/tools/studio/run-code.ts
|
|
870
|
+
var run_code_exports = {};
|
|
871
|
+
__export(run_code_exports, {
|
|
872
|
+
tool: () => tool3
|
|
873
|
+
});
|
|
874
|
+
var tool3;
|
|
875
|
+
var init_run_code = __esm({
|
|
876
|
+
"src/tools/studio/run-code.ts"() {
|
|
877
|
+
init_protocol();
|
|
878
|
+
tool3 = {
|
|
879
|
+
name: "run_code",
|
|
880
|
+
description: "Execute arbitrary Luau code in Roblox Studio and return the output. The code runs in the Studio plugin context with full API access.",
|
|
881
|
+
parameters: {
|
|
882
|
+
type: "object",
|
|
883
|
+
properties: {
|
|
884
|
+
code: {
|
|
885
|
+
type: "string",
|
|
886
|
+
description: "Luau code to execute in Studio"
|
|
887
|
+
}
|
|
888
|
+
},
|
|
889
|
+
required: ["code"]
|
|
890
|
+
},
|
|
891
|
+
async execute(params, ctx) {
|
|
892
|
+
if (!ctx.isStudioConnected()) {
|
|
893
|
+
return { success: false, error: "Studio is not connected" };
|
|
894
|
+
}
|
|
895
|
+
const code = params.code;
|
|
896
|
+
const response = await ctx.sendToStudio(CliMsg.RUN_CODE, { code });
|
|
897
|
+
const payload = response.payload;
|
|
898
|
+
if (payload.error) {
|
|
899
|
+
return { success: false, error: payload.error, data: { output: payload.output } };
|
|
900
|
+
}
|
|
901
|
+
return {
|
|
902
|
+
success: true,
|
|
903
|
+
data: { output: payload.output, duration: payload.duration }
|
|
904
|
+
};
|
|
905
|
+
}
|
|
906
|
+
};
|
|
907
|
+
}
|
|
908
|
+
});
|
|
909
|
+
|
|
910
|
+
// src/tools/studio/get-explorer.ts
|
|
911
|
+
var get_explorer_exports = {};
|
|
912
|
+
__export(get_explorer_exports, {
|
|
913
|
+
tool: () => tool4
|
|
914
|
+
});
|
|
915
|
+
var tool4;
|
|
916
|
+
var init_get_explorer = __esm({
|
|
917
|
+
"src/tools/studio/get-explorer.ts"() {
|
|
918
|
+
init_protocol();
|
|
919
|
+
tool4 = {
|
|
920
|
+
name: "get_explorer",
|
|
921
|
+
description: "Get the DataModel explorer tree from Roblox Studio. Optionally filter by a root path and max depth.",
|
|
922
|
+
parameters: {
|
|
923
|
+
type: "object",
|
|
924
|
+
properties: {
|
|
925
|
+
rootPath: {
|
|
926
|
+
type: "string",
|
|
927
|
+
description: 'Root path to start from (e.g., "ServerScriptService"). Omit for entire tree.'
|
|
928
|
+
},
|
|
929
|
+
maxDepth: {
|
|
930
|
+
type: "number",
|
|
931
|
+
description: "Maximum depth of the tree (default: 3)",
|
|
932
|
+
default: 3
|
|
933
|
+
}
|
|
934
|
+
}
|
|
935
|
+
},
|
|
936
|
+
async execute(params, ctx) {
|
|
937
|
+
if (!ctx.isStudioConnected()) {
|
|
938
|
+
return { success: false, error: "Studio is not connected" };
|
|
939
|
+
}
|
|
940
|
+
const response = await ctx.sendToStudio(CliMsg.GET_EXPLORER, {
|
|
941
|
+
rootPath: params.rootPath ?? null,
|
|
942
|
+
maxDepth: params.maxDepth ?? 3
|
|
943
|
+
});
|
|
944
|
+
return { success: true, data: response.payload };
|
|
945
|
+
}
|
|
946
|
+
};
|
|
947
|
+
}
|
|
948
|
+
});
|
|
949
|
+
|
|
950
|
+
// src/tools/studio/get-properties.ts
|
|
951
|
+
var get_properties_exports = {};
|
|
952
|
+
__export(get_properties_exports, {
|
|
953
|
+
tool: () => tool5
|
|
954
|
+
});
|
|
955
|
+
var tool5;
|
|
956
|
+
var init_get_properties = __esm({
|
|
957
|
+
"src/tools/studio/get-properties.ts"() {
|
|
958
|
+
init_protocol();
|
|
959
|
+
tool5 = {
|
|
960
|
+
name: "get_properties",
|
|
961
|
+
description: "Get all properties of a single instance in Roblox Studio. By default uses compact mode which strips read-only, nil, deprecated, and default-valued properties. For inspecting UI trees, prefer serialize_ui or get_descendants_properties.",
|
|
962
|
+
parameters: {
|
|
963
|
+
type: "object",
|
|
964
|
+
properties: {
|
|
965
|
+
path: {
|
|
966
|
+
type: "string",
|
|
967
|
+
description: 'Full instance path (e.g., "Workspace.Part")'
|
|
968
|
+
},
|
|
969
|
+
compact: {
|
|
970
|
+
type: "boolean",
|
|
971
|
+
description: "Strip read-only, nil, deprecated, and default-valued properties (default true). Set false for full dump."
|
|
972
|
+
}
|
|
973
|
+
},
|
|
974
|
+
required: ["path"]
|
|
975
|
+
},
|
|
976
|
+
async execute(params, ctx) {
|
|
977
|
+
if (!ctx.isStudioConnected()) {
|
|
978
|
+
return { success: false, error: "Studio is not connected" };
|
|
979
|
+
}
|
|
980
|
+
const compact = params.compact !== false;
|
|
981
|
+
const response = await ctx.sendToStudio(
|
|
982
|
+
CliMsg.GET_PROPERTIES,
|
|
983
|
+
{ path: params.path, compact }
|
|
984
|
+
);
|
|
985
|
+
return { success: true, data: response.payload };
|
|
986
|
+
}
|
|
987
|
+
};
|
|
988
|
+
}
|
|
989
|
+
});
|
|
990
|
+
|
|
991
|
+
// src/tools/studio/get-descendants-properties.ts
|
|
992
|
+
var get_descendants_properties_exports = {};
|
|
993
|
+
__export(get_descendants_properties_exports, {
|
|
994
|
+
tool: () => tool6
|
|
995
|
+
});
|
|
996
|
+
var tool6;
|
|
997
|
+
var init_get_descendants_properties = __esm({
|
|
998
|
+
"src/tools/studio/get-descendants-properties.ts"() {
|
|
999
|
+
init_protocol();
|
|
1000
|
+
tool6 = {
|
|
1001
|
+
name: "get_descendants_properties",
|
|
1002
|
+
description: "Easily get all descendants of a UI instance (or any instance) and all of their properties. By default uses compact mode which strips read-only, nil, deprecated, and default-valued properties to keep responses small.",
|
|
1003
|
+
parameters: {
|
|
1004
|
+
type: "object",
|
|
1005
|
+
properties: {
|
|
1006
|
+
path: {
|
|
1007
|
+
type: "string",
|
|
1008
|
+
description: 'Full instance path (e.g., "Workspace.Part", "StarterGui.ScreenGui")'
|
|
1009
|
+
},
|
|
1010
|
+
compact: {
|
|
1011
|
+
type: "boolean",
|
|
1012
|
+
description: "When true (default), strips read-only, nil, deprecated, and default-valued properties to massively reduce response size. Set to false for full property dump."
|
|
1013
|
+
},
|
|
1014
|
+
outputFile: {
|
|
1015
|
+
type: "string",
|
|
1016
|
+
description: "Optional file path to write the JSON result to, useful for very large trees where context limits apply."
|
|
1017
|
+
}
|
|
1018
|
+
},
|
|
1019
|
+
required: ["path"]
|
|
1020
|
+
},
|
|
1021
|
+
async execute(params, ctx) {
|
|
1022
|
+
if (!ctx.isStudioConnected()) {
|
|
1023
|
+
return { success: false, error: "Studio is not connected" };
|
|
1024
|
+
}
|
|
1025
|
+
const compact = params.compact !== false;
|
|
1026
|
+
const response = await ctx.sendToStudio(
|
|
1027
|
+
CliMsg.GET_DESCENDANTS_PROPERTIES,
|
|
1028
|
+
{ path: params.path, compact },
|
|
1029
|
+
1e3 * 60 * 5
|
|
1030
|
+
// 5 minute timeout since getting ALL descendants can take a very long time
|
|
1031
|
+
);
|
|
1032
|
+
if (params.outputFile && typeof params.outputFile === "string") {
|
|
1033
|
+
const fs5 = await import('fs/promises');
|
|
1034
|
+
const path4 = await import('path');
|
|
1035
|
+
const outputPath = path4.resolve(process.cwd(), params.outputFile);
|
|
1036
|
+
await fs5.writeFile(outputPath, JSON.stringify(response.payload, null, 2), "utf-8");
|
|
1037
|
+
return { success: true, data: `Successfully saved descendants to ${outputPath}` };
|
|
1038
|
+
}
|
|
1039
|
+
return { success: true, data: response.payload };
|
|
1040
|
+
}
|
|
1041
|
+
};
|
|
1042
|
+
}
|
|
1043
|
+
});
|
|
1044
|
+
|
|
1045
|
+
// src/tools/studio/set-properties.ts
|
|
1046
|
+
var set_properties_exports = {};
|
|
1047
|
+
__export(set_properties_exports, {
|
|
1048
|
+
tool: () => tool7
|
|
1049
|
+
});
|
|
1050
|
+
var tool7;
|
|
1051
|
+
var init_set_properties = __esm({
|
|
1052
|
+
"src/tools/studio/set-properties.ts"() {
|
|
1053
|
+
init_protocol();
|
|
1054
|
+
tool7 = {
|
|
1055
|
+
name: "set_properties",
|
|
1056
|
+
description: "Set properties on an instance in Roblox Studio.",
|
|
1057
|
+
parameters: {
|
|
1058
|
+
type: "object",
|
|
1059
|
+
properties: {
|
|
1060
|
+
path: {
|
|
1061
|
+
type: "string",
|
|
1062
|
+
description: 'Full instance path (e.g., "Workspace.Part")'
|
|
1063
|
+
},
|
|
1064
|
+
properties: {
|
|
1065
|
+
type: "object",
|
|
1066
|
+
description: 'Key-value pairs of properties to set (e.g., {"Name": "MyPart", "BrickColor": "Bright red"})'
|
|
1067
|
+
}
|
|
1068
|
+
},
|
|
1069
|
+
required: ["path", "properties"]
|
|
1070
|
+
},
|
|
1071
|
+
async execute(params, ctx) {
|
|
1072
|
+
if (!ctx.isStudioConnected()) {
|
|
1073
|
+
return { success: false, error: "Studio is not connected" };
|
|
1074
|
+
}
|
|
1075
|
+
const response = await ctx.sendToStudio(CliMsg.SET_PROPERTIES, {
|
|
1076
|
+
path: params.path,
|
|
1077
|
+
properties: params.properties
|
|
1078
|
+
});
|
|
1079
|
+
const payload = response.payload;
|
|
1080
|
+
if (!payload.success) {
|
|
1081
|
+
return { success: false, error: payload.error ?? "Failed to set properties" };
|
|
1082
|
+
}
|
|
1083
|
+
return { success: true, data: { path: params.path, updated: Object.keys(params.properties) } };
|
|
1084
|
+
}
|
|
1085
|
+
};
|
|
1086
|
+
}
|
|
1087
|
+
});
|
|
1088
|
+
|
|
1089
|
+
// src/tools/studio/insert-instance.ts
|
|
1090
|
+
var insert_instance_exports = {};
|
|
1091
|
+
__export(insert_instance_exports, {
|
|
1092
|
+
tool: () => tool8
|
|
1093
|
+
});
|
|
1094
|
+
var tool8;
|
|
1095
|
+
var init_insert_instance = __esm({
|
|
1096
|
+
"src/tools/studio/insert-instance.ts"() {
|
|
1097
|
+
init_protocol();
|
|
1098
|
+
tool8 = {
|
|
1099
|
+
name: "insert_instance",
|
|
1100
|
+
description: "Insert a new instance into the Roblox Studio DataModel.",
|
|
1101
|
+
parameters: {
|
|
1102
|
+
type: "object",
|
|
1103
|
+
properties: {
|
|
1104
|
+
className: {
|
|
1105
|
+
type: "string",
|
|
1106
|
+
description: 'Class name of the instance to create (e.g., "Part", "Script", "ModuleScript")'
|
|
1107
|
+
},
|
|
1108
|
+
parentPath: {
|
|
1109
|
+
type: "string",
|
|
1110
|
+
description: 'Full path of the parent instance (e.g., "Workspace", "ServerScriptService")'
|
|
1111
|
+
},
|
|
1112
|
+
name: {
|
|
1113
|
+
type: "string",
|
|
1114
|
+
description: "Name for the new instance"
|
|
1115
|
+
},
|
|
1116
|
+
properties: {
|
|
1117
|
+
type: "object",
|
|
1118
|
+
description: "Optional properties to set on the new instance"
|
|
1119
|
+
}
|
|
1120
|
+
},
|
|
1121
|
+
required: ["className", "parentPath", "name"]
|
|
1122
|
+
},
|
|
1123
|
+
async execute(params, ctx) {
|
|
1124
|
+
if (!ctx.isStudioConnected()) {
|
|
1125
|
+
return { success: false, error: "Studio is not connected" };
|
|
1126
|
+
}
|
|
1127
|
+
const response = await ctx.sendToStudio(CliMsg.INSERT_INSTANCE, {
|
|
1128
|
+
className: params.className,
|
|
1129
|
+
parentPath: params.parentPath,
|
|
1130
|
+
name: params.name,
|
|
1131
|
+
properties: params.properties ?? {}
|
|
1132
|
+
});
|
|
1133
|
+
const payload = response.payload;
|
|
1134
|
+
if (!payload.success) {
|
|
1135
|
+
return { success: false, error: payload.error ?? "Failed to insert instance" };
|
|
1136
|
+
}
|
|
1137
|
+
return { success: true, data: { path: payload.path } };
|
|
1138
|
+
}
|
|
1139
|
+
};
|
|
1140
|
+
}
|
|
1141
|
+
});
|
|
1142
|
+
|
|
1143
|
+
// src/tools/studio/delete-instance.ts
|
|
1144
|
+
var delete_instance_exports = {};
|
|
1145
|
+
__export(delete_instance_exports, {
|
|
1146
|
+
tool: () => tool9
|
|
1147
|
+
});
|
|
1148
|
+
var tool9;
|
|
1149
|
+
var init_delete_instance = __esm({
|
|
1150
|
+
"src/tools/studio/delete-instance.ts"() {
|
|
1151
|
+
init_protocol();
|
|
1152
|
+
tool9 = {
|
|
1153
|
+
name: "delete_instance",
|
|
1154
|
+
description: "Delete an instance from the Roblox Studio DataModel by path.",
|
|
1155
|
+
parameters: {
|
|
1156
|
+
type: "object",
|
|
1157
|
+
properties: {
|
|
1158
|
+
path: {
|
|
1159
|
+
type: "string",
|
|
1160
|
+
description: 'Full instance path to delete (e.g., "Workspace.OldPart")'
|
|
1161
|
+
}
|
|
1162
|
+
},
|
|
1163
|
+
required: ["path"]
|
|
1164
|
+
},
|
|
1165
|
+
async execute(params, ctx) {
|
|
1166
|
+
if (!ctx.isStudioConnected()) {
|
|
1167
|
+
return { success: false, error: "Studio is not connected" };
|
|
1168
|
+
}
|
|
1169
|
+
const response = await ctx.sendToStudio(CliMsg.DELETE_INSTANCE, { path: params.path });
|
|
1170
|
+
const payload = response.payload;
|
|
1171
|
+
if (!payload.success) {
|
|
1172
|
+
return { success: false, error: payload.error ?? "Failed to delete instance" };
|
|
1173
|
+
}
|
|
1174
|
+
return { success: true, data: { deleted: params.path } };
|
|
1175
|
+
}
|
|
1176
|
+
};
|
|
1177
|
+
}
|
|
1178
|
+
});
|
|
1179
|
+
|
|
1180
|
+
// src/tools/studio/get-output.ts
|
|
1181
|
+
var get_output_exports = {};
|
|
1182
|
+
__export(get_output_exports, {
|
|
1183
|
+
tool: () => tool10
|
|
1184
|
+
});
|
|
1185
|
+
var tool10;
|
|
1186
|
+
var init_get_output = __esm({
|
|
1187
|
+
"src/tools/studio/get-output.ts"() {
|
|
1188
|
+
init_protocol();
|
|
1189
|
+
tool10 = {
|
|
1190
|
+
name: "get_output",
|
|
1191
|
+
description: "Get recent output log entries from Roblox Studio (prints, warnings, errors).",
|
|
1192
|
+
parameters: {
|
|
1193
|
+
type: "object",
|
|
1194
|
+
properties: {
|
|
1195
|
+
limit: {
|
|
1196
|
+
type: "number",
|
|
1197
|
+
description: "Maximum number of log entries to return (default: 50)",
|
|
1198
|
+
default: 50
|
|
1199
|
+
},
|
|
1200
|
+
level: {
|
|
1201
|
+
type: "string",
|
|
1202
|
+
description: "Filter by log level",
|
|
1203
|
+
enum: ["info", "warn", "error"]
|
|
1204
|
+
}
|
|
1205
|
+
}
|
|
1206
|
+
},
|
|
1207
|
+
async execute(params, ctx) {
|
|
1208
|
+
if (!ctx.isStudioConnected()) {
|
|
1209
|
+
return { success: false, error: "Studio is not connected" };
|
|
1210
|
+
}
|
|
1211
|
+
const response = await ctx.sendToStudio(CliMsg.GET_OUTPUT, {
|
|
1212
|
+
limit: params.limit ?? 50,
|
|
1213
|
+
level: params.level ?? null
|
|
1214
|
+
});
|
|
1215
|
+
return { success: true, data: response.payload };
|
|
1216
|
+
}
|
|
1217
|
+
};
|
|
1218
|
+
}
|
|
1219
|
+
});
|
|
1220
|
+
|
|
1221
|
+
// src/tools/studio/get-selection.ts
|
|
1222
|
+
var get_selection_exports = {};
|
|
1223
|
+
__export(get_selection_exports, {
|
|
1224
|
+
tool: () => tool11
|
|
1225
|
+
});
|
|
1226
|
+
var tool11;
|
|
1227
|
+
var init_get_selection = __esm({
|
|
1228
|
+
"src/tools/studio/get-selection.ts"() {
|
|
1229
|
+
init_protocol();
|
|
1230
|
+
tool11 = {
|
|
1231
|
+
name: "get_selection",
|
|
1232
|
+
description: "Get the currently selected instances in Roblox Studio.",
|
|
1233
|
+
parameters: {
|
|
1234
|
+
type: "object",
|
|
1235
|
+
properties: {}
|
|
1236
|
+
},
|
|
1237
|
+
async execute(_params, ctx) {
|
|
1238
|
+
if (!ctx.isStudioConnected()) {
|
|
1239
|
+
return { success: false, error: "Studio is not connected" };
|
|
1240
|
+
}
|
|
1241
|
+
const response = await ctx.sendToStudio(CliMsg.GET_SELECTION, {});
|
|
1242
|
+
return { success: true, data: response.payload };
|
|
1243
|
+
}
|
|
1244
|
+
};
|
|
1245
|
+
}
|
|
1246
|
+
});
|
|
1247
|
+
|
|
1248
|
+
// src/tools/studio/search-scripts.ts
|
|
1249
|
+
var search_scripts_exports = {};
|
|
1250
|
+
__export(search_scripts_exports, {
|
|
1251
|
+
tool: () => tool12
|
|
1252
|
+
});
|
|
1253
|
+
var tool12;
|
|
1254
|
+
var init_search_scripts = __esm({
|
|
1255
|
+
"src/tools/studio/search-scripts.ts"() {
|
|
1256
|
+
init_protocol();
|
|
1257
|
+
tool12 = {
|
|
1258
|
+
name: "search_scripts",
|
|
1259
|
+
description: "Search across all scripts in Roblox Studio for a text pattern. Returns matching lines with context.",
|
|
1260
|
+
parameters: {
|
|
1261
|
+
type: "object",
|
|
1262
|
+
properties: {
|
|
1263
|
+
query: {
|
|
1264
|
+
type: "string",
|
|
1265
|
+
description: "Text or pattern to search for in script sources"
|
|
1266
|
+
},
|
|
1267
|
+
maxResults: {
|
|
1268
|
+
type: "number",
|
|
1269
|
+
description: "Maximum number of script results (default: 20)",
|
|
1270
|
+
default: 20
|
|
1271
|
+
}
|
|
1272
|
+
},
|
|
1273
|
+
required: ["query"]
|
|
1274
|
+
},
|
|
1275
|
+
async execute(params, ctx) {
|
|
1276
|
+
if (!ctx.isStudioConnected()) {
|
|
1277
|
+
return { success: false, error: "Studio is not connected" };
|
|
1278
|
+
}
|
|
1279
|
+
const response = await ctx.sendToStudio(CliMsg.SEARCH_SCRIPTS, {
|
|
1280
|
+
query: params.query,
|
|
1281
|
+
maxResults: params.maxResults ?? 20
|
|
1282
|
+
});
|
|
1283
|
+
return { success: true, data: response.payload };
|
|
1284
|
+
}
|
|
1285
|
+
};
|
|
1286
|
+
}
|
|
1287
|
+
});
|
|
1288
|
+
|
|
1289
|
+
// src/tools/studio/run-tests.ts
|
|
1290
|
+
var run_tests_exports = {};
|
|
1291
|
+
__export(run_tests_exports, {
|
|
1292
|
+
tool: () => tool13
|
|
1293
|
+
});
|
|
1294
|
+
var tool13;
|
|
1295
|
+
var init_run_tests = __esm({
|
|
1296
|
+
"src/tools/studio/run-tests.ts"() {
|
|
1297
|
+
init_protocol();
|
|
1298
|
+
tool13 = {
|
|
1299
|
+
name: "run_tests",
|
|
1300
|
+
description: "Run tests in Roblox Studio using StudioTestService. Can run all tests or a specific test by name.",
|
|
1301
|
+
parameters: {
|
|
1302
|
+
type: "object",
|
|
1303
|
+
properties: {
|
|
1304
|
+
testName: {
|
|
1305
|
+
type: "string",
|
|
1306
|
+
description: "Specific test name to run. Omit to run all tests."
|
|
1307
|
+
}
|
|
1308
|
+
}
|
|
1309
|
+
},
|
|
1310
|
+
async execute(params, ctx) {
|
|
1311
|
+
if (!ctx.isStudioConnected()) {
|
|
1312
|
+
return { success: false, error: "Studio is not connected" };
|
|
1313
|
+
}
|
|
1314
|
+
const response = await ctx.sendToStudio(CliMsg.RUN_TESTS, {
|
|
1315
|
+
testName: params.testName ?? null
|
|
1316
|
+
});
|
|
1317
|
+
const results = response.payload.results ?? [];
|
|
1318
|
+
const passed = results.filter((r) => r.passed).length;
|
|
1319
|
+
const failed = results.filter((r) => !r.passed).length;
|
|
1320
|
+
return {
|
|
1321
|
+
success: failed === 0,
|
|
1322
|
+
data: { total: results.length, passed, failed, results }
|
|
1323
|
+
};
|
|
1324
|
+
}
|
|
1325
|
+
};
|
|
1326
|
+
}
|
|
1327
|
+
});
|
|
1328
|
+
|
|
1329
|
+
// src/tools/studio/get-class-info.ts
|
|
1330
|
+
var get_class_info_exports = {};
|
|
1331
|
+
__export(get_class_info_exports, {
|
|
1332
|
+
tool: () => tool14
|
|
1333
|
+
});
|
|
1334
|
+
var tool14;
|
|
1335
|
+
var init_get_class_info = __esm({
|
|
1336
|
+
"src/tools/studio/get-class-info.ts"() {
|
|
1337
|
+
init_protocol();
|
|
1338
|
+
tool14 = {
|
|
1339
|
+
name: "get_class_info",
|
|
1340
|
+
description: "Look up the full schema of a Roblox class using ReflectionService. Returns all properties (with their types), methods (with parameters), and events. ALWAYS call this before creating or modifying GUI/UI elements (Frame, TextLabel, ImageLabel, etc.) to know the correct property names and expected types (e.g., Size is UDim2 for GUI, Vector3 for Parts).",
|
|
1341
|
+
parameters: {
|
|
1342
|
+
type: "object",
|
|
1343
|
+
properties: {
|
|
1344
|
+
className: {
|
|
1345
|
+
type: "string",
|
|
1346
|
+
description: 'Roblox class name, e.g. "Frame", "TextLabel", "Part", "MeshPart", "Camera"'
|
|
1347
|
+
}
|
|
1348
|
+
},
|
|
1349
|
+
required: ["className"]
|
|
1350
|
+
},
|
|
1351
|
+
async execute(params, ctx) {
|
|
1352
|
+
if (!ctx.isStudioConnected()) {
|
|
1353
|
+
return { success: false, error: "Studio is not connected" };
|
|
1354
|
+
}
|
|
1355
|
+
const response = await ctx.sendToStudio(CliMsg.GET_REFLECTION, { className: params.className });
|
|
1356
|
+
const payload = response.payload;
|
|
1357
|
+
if (payload.error) {
|
|
1358
|
+
return { success: false, error: payload.error };
|
|
1359
|
+
}
|
|
1360
|
+
return { success: true, data: payload };
|
|
1361
|
+
}
|
|
1362
|
+
};
|
|
1363
|
+
}
|
|
1364
|
+
});
|
|
1365
|
+
|
|
1366
|
+
// src/tools/studio/create-ui.ts
|
|
1367
|
+
var create_ui_exports = {};
|
|
1368
|
+
__export(create_ui_exports, {
|
|
1369
|
+
tool: () => tool15
|
|
1370
|
+
});
|
|
1371
|
+
var tool15;
|
|
1372
|
+
var init_create_ui = __esm({
|
|
1373
|
+
"src/tools/studio/create-ui.ts"() {
|
|
1374
|
+
init_protocol();
|
|
1375
|
+
tool15 = {
|
|
1376
|
+
name: "create_ui",
|
|
1377
|
+
description: 'Create an entire UI tree in Roblox Studio in a SINGLE call. Pass a declarative JSON tree with ClassName, Name, properties, and Children. All types are auto-coerced natively in Luau \u2014 UDim2, Color3, Font, enums all work. This is 10x faster than run_code for UI because it handles everything in one round-trip with no Luau syntax issues. Replaces any existing instance with the same name.\n\nExample: create a pastel hello world screen:\n{\n "parent": "StarterGui",\n "tree": {\n "ClassName": "ScreenGui",\n "Name": "HelloWorld",\n "ResetOnSpawn": false,\n "Children": [{\n "ClassName": "Frame",\n "Name": "Background",\n "Size": [1, 0, 1, 0],\n "BackgroundColor3": "#F0E6FF",\n "Children": [\n { "ClassName": "UICorner", "CornerRadius": [0, 12] },\n { "ClassName": "TextLabel", "Name": "Title", "Text": "Hello World!",\n "Size": [0.5, 0, 0.1, 0], "Position": [0.25, 0, 0.45, 0],\n "BackgroundTransparency": 1, "TextColor3": "#6C5CE7",\n "TextScaled": true, "Font": "GothamBold" }\n ]\n }]\n }\n}',
|
|
1378
|
+
parameters: {
|
|
1379
|
+
type: "object",
|
|
1380
|
+
properties: {
|
|
1381
|
+
parent: {
|
|
1382
|
+
type: "string",
|
|
1383
|
+
description: 'Parent path: "StarterGui", "PlayerGui", or any instance path. Default: StarterGui'
|
|
1384
|
+
},
|
|
1385
|
+
tree: {
|
|
1386
|
+
type: "object",
|
|
1387
|
+
description: 'The root UI node. Each node has: ClassName (string), Name (string), Children (array of child nodes), and properties. Properties may be specified either flattened at the top level of the node, OR nested under a "properties" / "Properties" / "props" key \u2014 both work. Property values are auto-coerced:\n UDim2: [xScale, xOffset, yScale, yOffset] or {XScale, XOffset, YScale, YOffset}\n Color3: "#hex", "rgb(r,g,b)", [r,g,b], or BrickColor name\n UDim: [scale, offset] or number\n Vector2: [x, y] or {x, y} (auto-disambiguated from UDim by property name)\n NumberRange: [min, max] or number\n Rect: [minX, minY, maxX, maxY] or {MinX, MinY, MaxX, MaxY}\n Font: "GothamBold", "SourceSans", etc. (Enum.Font names)\n Enums: string name like "Center", "Left", "Fit"\n FontFace: {Family, Weight, Style}\n 2-element arrays are auto-disambiguated: AnchorPoint\u2192Vector2, CornerRadius/Padding\u2192UDim, etc.'
|
|
1388
|
+
},
|
|
1389
|
+
animate: {
|
|
1390
|
+
type: "boolean",
|
|
1391
|
+
description: "If true, task.wait() will be injected between creation of nodes so that the UI writes out dynamically."
|
|
1392
|
+
}
|
|
1393
|
+
},
|
|
1394
|
+
required: ["tree"]
|
|
1395
|
+
},
|
|
1396
|
+
async execute(params, ctx) {
|
|
1397
|
+
if (!ctx.isStudioConnected()) {
|
|
1398
|
+
return { success: false, error: "Studio is not connected" };
|
|
1399
|
+
}
|
|
1400
|
+
const response = await ctx.sendToStudio(CliMsg.BUILD_UI, {
|
|
1401
|
+
parent: params.parent ?? "StarterGui",
|
|
1402
|
+
animate: params.animate,
|
|
1403
|
+
tree: params.tree
|
|
1404
|
+
});
|
|
1405
|
+
const payload = response.payload;
|
|
1406
|
+
if (!payload.success) {
|
|
1407
|
+
return { success: false, error: payload.error ?? "UI build failed" };
|
|
1408
|
+
}
|
|
1409
|
+
return {
|
|
1410
|
+
success: true,
|
|
1411
|
+
data: { path: payload.path, instanceCount: payload.instanceCount }
|
|
1412
|
+
};
|
|
1413
|
+
}
|
|
1414
|
+
};
|
|
1415
|
+
}
|
|
1416
|
+
});
|
|
1417
|
+
|
|
1418
|
+
// src/tools/studio/execute-run-test.ts
|
|
1419
|
+
var execute_run_test_exports = {};
|
|
1420
|
+
__export(execute_run_test_exports, {
|
|
1421
|
+
tool: () => tool16
|
|
1422
|
+
});
|
|
1423
|
+
var tool16;
|
|
1424
|
+
var init_execute_run_test = __esm({
|
|
1425
|
+
"src/tools/studio/execute-run-test.ts"() {
|
|
1426
|
+
init_protocol();
|
|
1427
|
+
tool16 = {
|
|
1428
|
+
name: "execute_run_test",
|
|
1429
|
+
description: "Start a Run mode test session in Roblox Studio using StudioTestService:ExecuteRunModeAsync(). Passes args that server scripts can retrieve via GetTestArgs(). Yields until the session ends (via EndTest or manual stop). Returns the value passed to EndTest(). Use this for headless automated tests \u2014 no player character, just server scripts running.",
|
|
1430
|
+
parameters: {
|
|
1431
|
+
type: "object",
|
|
1432
|
+
properties: {
|
|
1433
|
+
args: {
|
|
1434
|
+
description: "Arguments to pass to the test session. Server scripts retrieve this with StudioTestService:GetTestArgs(). Can be a string (e.g. test suite name), table, or any serializable value."
|
|
1435
|
+
}
|
|
1436
|
+
}
|
|
1437
|
+
},
|
|
1438
|
+
async execute(params, ctx) {
|
|
1439
|
+
if (!ctx.isStudioConnected()) {
|
|
1440
|
+
return { success: false, error: "Studio is not connected" };
|
|
1441
|
+
}
|
|
1442
|
+
const response = await ctx.sendToStudio(
|
|
1443
|
+
CliMsg.EXECUTE_RUN_TEST,
|
|
1444
|
+
{ args: params.args ?? null },
|
|
1445
|
+
12e4
|
|
1446
|
+
);
|
|
1447
|
+
const payload = response.payload;
|
|
1448
|
+
if (!payload.success) {
|
|
1449
|
+
return { success: false, error: payload.error ?? "Run mode test failed" };
|
|
1450
|
+
}
|
|
1451
|
+
return {
|
|
1452
|
+
success: true,
|
|
1453
|
+
data: {
|
|
1454
|
+
result: payload.result,
|
|
1455
|
+
duration: payload.duration
|
|
1456
|
+
}
|
|
1457
|
+
};
|
|
1458
|
+
}
|
|
1459
|
+
};
|
|
1460
|
+
}
|
|
1461
|
+
});
|
|
1462
|
+
|
|
1463
|
+
// src/tools/studio/execute-play-test.ts
|
|
1464
|
+
var execute_play_test_exports = {};
|
|
1465
|
+
__export(execute_play_test_exports, {
|
|
1466
|
+
tool: () => tool17
|
|
1467
|
+
});
|
|
1468
|
+
var tool17;
|
|
1469
|
+
var init_execute_play_test = __esm({
|
|
1470
|
+
"src/tools/studio/execute-play-test.ts"() {
|
|
1471
|
+
init_protocol();
|
|
1472
|
+
tool17 = {
|
|
1473
|
+
name: "execute_play_test",
|
|
1474
|
+
description: "Start a solo Play mode test session in Roblox Studio using StudioTestService:ExecutePlayModeAsync(). Passes args that server scripts can retrieve via GetTestArgs(). Yields until the session ends (via EndTest or manual stop). Returns the value passed to EndTest(). Use this for tests that need a player character and full client-server simulation.",
|
|
1475
|
+
parameters: {
|
|
1476
|
+
type: "object",
|
|
1477
|
+
properties: {
|
|
1478
|
+
args: {
|
|
1479
|
+
description: "Arguments to pass to the test session. Server scripts retrieve this with StudioTestService:GetTestArgs(). Can be a string (e.g. test suite name), table, or any serializable value."
|
|
1480
|
+
}
|
|
1481
|
+
}
|
|
1482
|
+
},
|
|
1483
|
+
async execute(params, ctx) {
|
|
1484
|
+
if (!ctx.isStudioConnected()) {
|
|
1485
|
+
return { success: false, error: "Studio is not connected" };
|
|
1486
|
+
}
|
|
1487
|
+
const response = await ctx.sendToStudio(
|
|
1488
|
+
CliMsg.EXECUTE_PLAY_TEST,
|
|
1489
|
+
{ args: params.args ?? null },
|
|
1490
|
+
12e4
|
|
1491
|
+
);
|
|
1492
|
+
const payload = response.payload;
|
|
1493
|
+
if (!payload.success) {
|
|
1494
|
+
return { success: false, error: payload.error ?? "Play mode test failed" };
|
|
1495
|
+
}
|
|
1496
|
+
return {
|
|
1497
|
+
success: true,
|
|
1498
|
+
data: {
|
|
1499
|
+
result: payload.result,
|
|
1500
|
+
duration: payload.duration
|
|
1501
|
+
}
|
|
1502
|
+
};
|
|
1503
|
+
}
|
|
1504
|
+
};
|
|
1505
|
+
}
|
|
1506
|
+
});
|
|
1507
|
+
|
|
1508
|
+
// src/tools/studio/create-part.ts
|
|
1509
|
+
var create_part_exports = {};
|
|
1510
|
+
__export(create_part_exports, {
|
|
1511
|
+
tool: () => tool18
|
|
1512
|
+
});
|
|
1513
|
+
var tool18;
|
|
1514
|
+
var init_create_part = __esm({
|
|
1515
|
+
"src/tools/studio/create-part.ts"() {
|
|
1516
|
+
init_protocol();
|
|
1517
|
+
tool18 = {
|
|
1518
|
+
name: "create_part",
|
|
1519
|
+
description: "Create a Part in Roblox Studio with position, size, color, material and more \u2014 all in one call. Much faster than insert_instance + set_properties separately.",
|
|
1520
|
+
parameters: {
|
|
1521
|
+
type: "object",
|
|
1522
|
+
properties: {
|
|
1523
|
+
name: { type: "string", description: "Name for the part" },
|
|
1524
|
+
parent: {
|
|
1525
|
+
type: "string",
|
|
1526
|
+
description: 'Parent path (default: "Workspace")'
|
|
1527
|
+
},
|
|
1528
|
+
shape: {
|
|
1529
|
+
type: "string",
|
|
1530
|
+
enum: ["Block", "Ball", "Cylinder", "Wedge", "CornerWedge"],
|
|
1531
|
+
description: "Part shape (default: Block)"
|
|
1532
|
+
},
|
|
1533
|
+
position: {
|
|
1534
|
+
type: "object",
|
|
1535
|
+
properties: { x: { type: "number" }, y: { type: "number" }, z: { type: "number" } },
|
|
1536
|
+
description: "World position {x, y, z}"
|
|
1537
|
+
},
|
|
1538
|
+
size: {
|
|
1539
|
+
type: "object",
|
|
1540
|
+
properties: { x: { type: "number" }, y: { type: "number" }, z: { type: "number" } },
|
|
1541
|
+
description: "Size in studs {x, y, z} (default: {4, 1, 2})"
|
|
1542
|
+
},
|
|
1543
|
+
color: {
|
|
1544
|
+
type: "string",
|
|
1545
|
+
description: 'Color as hex "#FF0000", BrickColor name "Bright red", or RGB "rgb(255,0,0)"'
|
|
1546
|
+
},
|
|
1547
|
+
material: {
|
|
1548
|
+
type: "string",
|
|
1549
|
+
description: "Material name: Plastic, Wood, Grass, Ice, Metal, Concrete, Sand, Neon, Glass, etc."
|
|
1550
|
+
},
|
|
1551
|
+
transparency: { type: "number", description: "0 (opaque) to 1 (invisible)" },
|
|
1552
|
+
anchored: { type: "boolean", description: "Whether the part is anchored (default: true)" },
|
|
1553
|
+
canCollide: { type: "boolean", description: "Whether the part has collision" }
|
|
1554
|
+
},
|
|
1555
|
+
required: ["name"]
|
|
1556
|
+
},
|
|
1557
|
+
async execute(params, ctx) {
|
|
1558
|
+
if (!ctx.isStudioConnected()) {
|
|
1559
|
+
return { success: false, error: "Studio is not connected" };
|
|
1560
|
+
}
|
|
1561
|
+
const parent = params.parent || "Workspace";
|
|
1562
|
+
const name = params.name;
|
|
1563
|
+
const pos = params.position;
|
|
1564
|
+
const size = params.size;
|
|
1565
|
+
const properties = {};
|
|
1566
|
+
if (pos) properties.Position = { X: pos.x ?? 0, Y: pos.y ?? 0, Z: pos.z ?? 0 };
|
|
1567
|
+
if (size) properties.Size = { X: size.x ?? 4, Y: size.y ?? 1, Z: size.z ?? 2 };
|
|
1568
|
+
if (params.color) {
|
|
1569
|
+
const c = params.color;
|
|
1570
|
+
if (c.startsWith("#")) {
|
|
1571
|
+
properties.Color = c;
|
|
1572
|
+
} else if (c.startsWith("rgb")) {
|
|
1573
|
+
const m = c.match(/(\d+)/g);
|
|
1574
|
+
if (m && m.length >= 3) {
|
|
1575
|
+
properties.Color = { red: parseInt(m[0]), green: parseInt(m[1]), blue: parseInt(m[2]) };
|
|
1576
|
+
}
|
|
1577
|
+
} else {
|
|
1578
|
+
properties.BrickColor = c;
|
|
1579
|
+
}
|
|
1580
|
+
}
|
|
1581
|
+
if (params.material) properties.Material = params.material;
|
|
1582
|
+
if (params.transparency !== void 0) properties.Transparency = params.transparency;
|
|
1583
|
+
if (params.anchored !== void 0) properties.Anchored = params.anchored;
|
|
1584
|
+
else properties.Anchored = true;
|
|
1585
|
+
if (params.canCollide !== void 0) properties.CanCollide = params.canCollide;
|
|
1586
|
+
if (params.shape && params.shape !== "Block") properties.Shape = params.shape;
|
|
1587
|
+
const className = params.shape === "Wedge" ? "WedgePart" : params.shape === "CornerWedge" ? "CornerWedgePart" : "Part";
|
|
1588
|
+
const res = await ctx.sendToStudio(CliMsg.INSERT_INSTANCE, {
|
|
1589
|
+
className,
|
|
1590
|
+
parentPath: parent,
|
|
1591
|
+
name,
|
|
1592
|
+
properties
|
|
1593
|
+
});
|
|
1594
|
+
const payload = res.payload;
|
|
1595
|
+
if (!payload.success) {
|
|
1596
|
+
return { success: false, error: payload.error ?? "Failed to create part" };
|
|
1597
|
+
}
|
|
1598
|
+
return {
|
|
1599
|
+
success: true,
|
|
1600
|
+
data: { path: payload.path, properties: Object.keys(properties) }
|
|
1601
|
+
};
|
|
1602
|
+
}
|
|
1603
|
+
};
|
|
1604
|
+
}
|
|
1605
|
+
});
|
|
1606
|
+
|
|
1607
|
+
// src/tools/studio/clone-instance.ts
|
|
1608
|
+
var clone_instance_exports = {};
|
|
1609
|
+
__export(clone_instance_exports, {
|
|
1610
|
+
tool: () => tool19
|
|
1611
|
+
});
|
|
1612
|
+
function buildCloneCode(params) {
|
|
1613
|
+
const path4 = params.path;
|
|
1614
|
+
const parts = path4.split(".");
|
|
1615
|
+
let resolve = "game";
|
|
1616
|
+
for (const p of parts) {
|
|
1617
|
+
resolve += `["${p}"]`;
|
|
1618
|
+
}
|
|
1619
|
+
const lines = [
|
|
1620
|
+
`local orig = ${resolve}`,
|
|
1621
|
+
`local clone = orig:Clone()`
|
|
1622
|
+
];
|
|
1623
|
+
if (params.newName) {
|
|
1624
|
+
lines.push(`clone.Name = "${params.newName}"`);
|
|
1625
|
+
}
|
|
1626
|
+
if (params.newParent) {
|
|
1627
|
+
const parentParts = params.newParent.split(".");
|
|
1628
|
+
let parentResolve = "game";
|
|
1629
|
+
for (const p of parentParts) {
|
|
1630
|
+
parentResolve += `["${p}"]`;
|
|
1631
|
+
}
|
|
1632
|
+
lines.push(`clone.Parent = ${parentResolve}`);
|
|
1633
|
+
} else {
|
|
1634
|
+
lines.push(`clone.Parent = orig.Parent`);
|
|
1635
|
+
}
|
|
1636
|
+
const offset = params.offset;
|
|
1637
|
+
if (offset) {
|
|
1638
|
+
lines.push(`if clone:IsA("BasePart") then`);
|
|
1639
|
+
lines.push(` clone.Position = clone.Position + Vector3.new(${offset.x ?? 0}, ${offset.y ?? 0}, ${offset.z ?? 0})`);
|
|
1640
|
+
lines.push(`end`);
|
|
1641
|
+
}
|
|
1642
|
+
lines.push(`print("Cloned: " .. clone:GetFullName())`);
|
|
1643
|
+
return lines.join("\n");
|
|
1644
|
+
}
|
|
1645
|
+
var tool19;
|
|
1646
|
+
var init_clone_instance = __esm({
|
|
1647
|
+
"src/tools/studio/clone-instance.ts"() {
|
|
1648
|
+
init_protocol();
|
|
1649
|
+
tool19 = {
|
|
1650
|
+
name: "clone_instance",
|
|
1651
|
+
description: "Clone an existing instance in Roblox Studio and optionally reposition/rename it. Great for duplicating parts, models, or entire trees.",
|
|
1652
|
+
parameters: {
|
|
1653
|
+
type: "object",
|
|
1654
|
+
properties: {
|
|
1655
|
+
path: { type: "string", description: "Path of instance to clone" },
|
|
1656
|
+
newName: { type: "string", description: "Name for the clone (optional)" },
|
|
1657
|
+
newParent: { type: "string", description: "Parent path for the clone (optional, defaults to same parent)" },
|
|
1658
|
+
offset: {
|
|
1659
|
+
type: "object",
|
|
1660
|
+
properties: { x: { type: "number" }, y: { type: "number" }, z: { type: "number" } },
|
|
1661
|
+
description: "Position offset from original {x, y, z}"
|
|
1662
|
+
}
|
|
1663
|
+
},
|
|
1664
|
+
required: ["path"]
|
|
1665
|
+
},
|
|
1666
|
+
async execute(params, ctx) {
|
|
1667
|
+
if (!ctx.isStudioConnected()) {
|
|
1668
|
+
return { success: false, error: "Studio is not connected" };
|
|
1669
|
+
}
|
|
1670
|
+
const code = buildCloneCode(params);
|
|
1671
|
+
const res = await ctx.sendToStudio(CliMsg.RUN_CODE, { code });
|
|
1672
|
+
const payload = res.payload;
|
|
1673
|
+
if (!payload.success) {
|
|
1674
|
+
return { success: false, error: payload.error ?? "Failed to clone" };
|
|
1675
|
+
}
|
|
1676
|
+
return { success: true, data: { output: payload.output } };
|
|
1677
|
+
}
|
|
1678
|
+
};
|
|
1679
|
+
}
|
|
1680
|
+
});
|
|
1681
|
+
|
|
1682
|
+
// src/tools/studio/group-instances.ts
|
|
1683
|
+
var group_instances_exports = {};
|
|
1684
|
+
__export(group_instances_exports, {
|
|
1685
|
+
tool: () => tool20
|
|
1686
|
+
});
|
|
1687
|
+
var tool20;
|
|
1688
|
+
var init_group_instances = __esm({
|
|
1689
|
+
"src/tools/studio/group-instances.ts"() {
|
|
1690
|
+
init_protocol();
|
|
1691
|
+
tool20 = {
|
|
1692
|
+
name: "group_instances",
|
|
1693
|
+
description: 'Group multiple instances under a new Model or Folder. Useful for organizing parts into objects (e.g., grouping trunk + leaves into a "Tree" model).',
|
|
1694
|
+
parameters: {
|
|
1695
|
+
type: "object",
|
|
1696
|
+
properties: {
|
|
1697
|
+
paths: {
|
|
1698
|
+
type: "array",
|
|
1699
|
+
items: { type: "string" },
|
|
1700
|
+
description: "Array of instance paths to group together"
|
|
1701
|
+
},
|
|
1702
|
+
groupName: { type: "string", description: "Name for the new group" },
|
|
1703
|
+
groupClass: {
|
|
1704
|
+
type: "string",
|
|
1705
|
+
enum: ["Model", "Folder"],
|
|
1706
|
+
description: "Class for the group container (default: Model)"
|
|
1707
|
+
},
|
|
1708
|
+
parent: { type: "string", description: "Parent path (default: Workspace)" }
|
|
1709
|
+
},
|
|
1710
|
+
required: ["paths", "groupName"]
|
|
1711
|
+
},
|
|
1712
|
+
async execute(params, ctx) {
|
|
1713
|
+
if (!ctx.isStudioConnected()) {
|
|
1714
|
+
return { success: false, error: "Studio is not connected" };
|
|
1715
|
+
}
|
|
1716
|
+
const paths = params.paths;
|
|
1717
|
+
const groupName = params.groupName;
|
|
1718
|
+
const groupClass = params.groupClass || "Model";
|
|
1719
|
+
const parent = params.parent || "Workspace";
|
|
1720
|
+
const resolves = paths.map((p) => {
|
|
1721
|
+
const parts = p.split(".");
|
|
1722
|
+
let r = "game";
|
|
1723
|
+
for (const part of parts) r += `["${part}"]`;
|
|
1724
|
+
return r;
|
|
1725
|
+
});
|
|
1726
|
+
const parentParts = parent.split(".");
|
|
1727
|
+
let parentResolve = "game";
|
|
1728
|
+
for (const p of parentParts) parentResolve += `["${p}"]`;
|
|
1729
|
+
const code = [
|
|
1730
|
+
`local group = Instance.new("${groupClass}")`,
|
|
1731
|
+
`group.Name = "${groupName}"`,
|
|
1732
|
+
`group.Parent = ${parentResolve}`,
|
|
1733
|
+
...resolves.map((r) => `${r}.Parent = group`),
|
|
1734
|
+
groupClass === "Model" ? `if group:IsA("Model") then group:MoveTo(group:GetBoundingBox().Position) end` : "",
|
|
1735
|
+
`print("Grouped ${paths.length} instances into: " .. group:GetFullName())`
|
|
1736
|
+
].filter(Boolean).join("\n");
|
|
1737
|
+
const res = await ctx.sendToStudio(CliMsg.RUN_CODE, { code });
|
|
1738
|
+
const payload = res.payload;
|
|
1739
|
+
if (!payload.success) {
|
|
1740
|
+
return { success: false, error: payload.error ?? "Failed to group" };
|
|
1741
|
+
}
|
|
1742
|
+
return { success: true, data: { groupPath: `${parent}.${groupName}`, count: paths.length } };
|
|
1743
|
+
}
|
|
1744
|
+
};
|
|
1745
|
+
}
|
|
1746
|
+
});
|
|
1747
|
+
|
|
1748
|
+
// src/tools/studio/build-multiple.ts
|
|
1749
|
+
var build_multiple_exports = {};
|
|
1750
|
+
__export(build_multiple_exports, {
|
|
1751
|
+
tool: () => tool21
|
|
1752
|
+
});
|
|
1753
|
+
function generateBatchCode(parts, groupName, groupParent) {
|
|
1754
|
+
const lines = [];
|
|
1755
|
+
function resolveParent(path4) {
|
|
1756
|
+
const segs = path4.split(".");
|
|
1757
|
+
let r = "game";
|
|
1758
|
+
for (const s of segs) r += `["${s}"]`;
|
|
1759
|
+
return r;
|
|
1760
|
+
}
|
|
1761
|
+
if (groupName) {
|
|
1762
|
+
const gp = resolveParent(groupParent);
|
|
1763
|
+
lines.push(`local _group = Instance.new("Model")`);
|
|
1764
|
+
lines.push(`_group.Name = "${groupName}"`);
|
|
1765
|
+
lines.push(`_group.Parent = ${gp}`);
|
|
1766
|
+
}
|
|
1767
|
+
for (let i = 0; i < parts.length; i++) {
|
|
1768
|
+
const p = parts[i];
|
|
1769
|
+
const varName = `_p${i}`;
|
|
1770
|
+
const cn = p.className || (p.shape === "Wedge" ? "WedgePart" : "Part");
|
|
1771
|
+
lines.push(`local ${varName} = Instance.new("${cn}")`);
|
|
1772
|
+
lines.push(`${varName}.Name = "${p.name}"`);
|
|
1773
|
+
if (p.position) {
|
|
1774
|
+
lines.push(`${varName}.Position = Vector3.new(${p.position.x ?? 0}, ${p.position.y ?? 0}, ${p.position.z ?? 0})`);
|
|
1775
|
+
}
|
|
1776
|
+
if (p.size) {
|
|
1777
|
+
lines.push(`${varName}.Size = Vector3.new(${p.size.x ?? 4}, ${p.size.y ?? 1}, ${p.size.z ?? 2})`);
|
|
1778
|
+
}
|
|
1779
|
+
if (p.color) {
|
|
1780
|
+
if (p.color.startsWith("#")) {
|
|
1781
|
+
lines.push(`${varName}.Color = Color3.fromHex("${p.color}")`);
|
|
1782
|
+
} else {
|
|
1783
|
+
lines.push(`${varName}.BrickColor = BrickColor.new("${p.color}")`);
|
|
1784
|
+
}
|
|
1785
|
+
}
|
|
1786
|
+
if (p.material) {
|
|
1787
|
+
lines.push(`${varName}.Material = Enum.Material.${p.material}`);
|
|
1788
|
+
}
|
|
1789
|
+
if (p.transparency !== void 0) {
|
|
1790
|
+
lines.push(`${varName}.Transparency = ${p.transparency}`);
|
|
1791
|
+
}
|
|
1792
|
+
if (p.anchored !== void 0) {
|
|
1793
|
+
lines.push(`${varName}.Anchored = ${p.anchored}`);
|
|
1794
|
+
} else {
|
|
1795
|
+
lines.push(`${varName}.Anchored = true`);
|
|
1796
|
+
}
|
|
1797
|
+
if (p.shape && p.shape !== "Block" && cn === "Part") {
|
|
1798
|
+
lines.push(`${varName}.Shape = Enum.PartType.${p.shape}`);
|
|
1799
|
+
}
|
|
1800
|
+
if (groupName) {
|
|
1801
|
+
lines.push(`${varName}.Parent = _group`);
|
|
1802
|
+
} else {
|
|
1803
|
+
const parent = p.parent || "Workspace";
|
|
1804
|
+
lines.push(`${varName}.Parent = ${resolveParent(parent)}`);
|
|
1805
|
+
}
|
|
1806
|
+
}
|
|
1807
|
+
lines.push(`print("Built ${parts.length} parts${groupName ? ` in ${groupName}` : ""}")`);
|
|
1808
|
+
return lines.join("\n");
|
|
1809
|
+
}
|
|
1810
|
+
var tool21;
|
|
1811
|
+
var init_build_multiple = __esm({
|
|
1812
|
+
"src/tools/studio/build-multiple.ts"() {
|
|
1813
|
+
init_protocol();
|
|
1814
|
+
tool21 = {
|
|
1815
|
+
name: "build_multiple",
|
|
1816
|
+
description: "Create multiple parts/instances in one batch call. Send an array of specs and they all get created in a single Studio round-trip. Ideal for building trees, houses, vehicles, terrain \u2014 anything composed of multiple parts.",
|
|
1817
|
+
parameters: {
|
|
1818
|
+
type: "object",
|
|
1819
|
+
properties: {
|
|
1820
|
+
parts: {
|
|
1821
|
+
type: "array",
|
|
1822
|
+
items: {
|
|
1823
|
+
type: "object",
|
|
1824
|
+
properties: {
|
|
1825
|
+
name: { type: "string", description: "Part name" },
|
|
1826
|
+
className: { type: "string", description: "ClassName (default: Part)" },
|
|
1827
|
+
parent: { type: "string", description: "Parent path (default: Workspace)" },
|
|
1828
|
+
position: {
|
|
1829
|
+
type: "object",
|
|
1830
|
+
properties: { x: { type: "number" }, y: { type: "number" }, z: { type: "number" } }
|
|
1831
|
+
},
|
|
1832
|
+
size: {
|
|
1833
|
+
type: "object",
|
|
1834
|
+
properties: { x: { type: "number" }, y: { type: "number" }, z: { type: "number" } }
|
|
1835
|
+
},
|
|
1836
|
+
color: { type: "string", description: 'Hex "#RRGGBB" or BrickColor name' },
|
|
1837
|
+
material: { type: "string" },
|
|
1838
|
+
transparency: { type: "number" },
|
|
1839
|
+
anchored: { type: "boolean" },
|
|
1840
|
+
shape: { type: "string", enum: ["Block", "Ball", "Cylinder", "Wedge"] }
|
|
1841
|
+
},
|
|
1842
|
+
required: ["name"]
|
|
1843
|
+
},
|
|
1844
|
+
description: "Array of part specifications"
|
|
1845
|
+
},
|
|
1846
|
+
groupName: {
|
|
1847
|
+
type: "string",
|
|
1848
|
+
description: "Optional: group all parts under a Model with this name"
|
|
1849
|
+
},
|
|
1850
|
+
groupParent: {
|
|
1851
|
+
type: "string",
|
|
1852
|
+
description: "Parent for the group Model (default: Workspace)"
|
|
1853
|
+
}
|
|
1854
|
+
},
|
|
1855
|
+
required: ["parts"]
|
|
1856
|
+
},
|
|
1857
|
+
async execute(params, ctx) {
|
|
1858
|
+
if (!ctx.isStudioConnected()) {
|
|
1859
|
+
return { success: false, error: "Studio is not connected" };
|
|
1860
|
+
}
|
|
1861
|
+
const parts = params.parts;
|
|
1862
|
+
const groupName = params.groupName;
|
|
1863
|
+
const groupParent = params.groupParent || "Workspace";
|
|
1864
|
+
const code = generateBatchCode(parts, groupName, groupParent);
|
|
1865
|
+
const res = await ctx.sendToStudio(CliMsg.RUN_CODE, { code });
|
|
1866
|
+
const payload = res.payload;
|
|
1867
|
+
if (!payload.success) {
|
|
1868
|
+
return { success: false, error: payload.error ?? "Batch build failed" };
|
|
1869
|
+
}
|
|
1870
|
+
return {
|
|
1871
|
+
success: true,
|
|
1872
|
+
data: {
|
|
1873
|
+
count: parts.length,
|
|
1874
|
+
group: groupName ?? null,
|
|
1875
|
+
output: payload.output
|
|
1876
|
+
}
|
|
1877
|
+
};
|
|
1878
|
+
}
|
|
1879
|
+
};
|
|
1880
|
+
}
|
|
1881
|
+
});
|
|
1882
|
+
|
|
1883
|
+
// src/tools/studio/serialize-ui.ts
|
|
1884
|
+
var serialize_ui_exports = {};
|
|
1885
|
+
__export(serialize_ui_exports, {
|
|
1886
|
+
tool: () => tool22
|
|
1887
|
+
});
|
|
1888
|
+
var tool22;
|
|
1889
|
+
var init_serialize_ui = __esm({
|
|
1890
|
+
"src/tools/studio/serialize-ui.ts"() {
|
|
1891
|
+
init_protocol();
|
|
1892
|
+
tool22 = {
|
|
1893
|
+
name: "serialize_ui",
|
|
1894
|
+
description: "Serialize an existing UI tree from Roblox Studio into create_ui-compatible JSON. This is the reverse of create_ui \u2014 read any instance and get back a declarative JSON tree you can modify and pass back to create_ui. Perfect for cloning, templating, or refactoring UIs.\n\nThe output includes ClassName, Name, all non-default properties (with values converted to JSON-friendly formats like [r,g,b] for Color3, [xS,xO,yS,yO] for UDim2, etc.), and Children.",
|
|
1895
|
+
parameters: {
|
|
1896
|
+
type: "object",
|
|
1897
|
+
properties: {
|
|
1898
|
+
path: {
|
|
1899
|
+
type: "string",
|
|
1900
|
+
description: 'Instance path to serialize (e.g. "StarterGui.MyScreenGui")'
|
|
1901
|
+
},
|
|
1902
|
+
maxDepth: {
|
|
1903
|
+
type: "number",
|
|
1904
|
+
description: "Maximum depth to serialize (default: 50)"
|
|
1905
|
+
}
|
|
1906
|
+
},
|
|
1907
|
+
required: ["path"]
|
|
1908
|
+
},
|
|
1909
|
+
async execute(params, ctx) {
|
|
1910
|
+
if (!ctx.isStudioConnected()) {
|
|
1911
|
+
return { success: false, error: "Studio is not connected" };
|
|
1912
|
+
}
|
|
1913
|
+
const response = await ctx.sendToStudio(CliMsg.SERIALIZE_UI, {
|
|
1914
|
+
path: params.path,
|
|
1915
|
+
maxDepth: params.maxDepth ?? 50
|
|
1916
|
+
}, 1e3 * 60 * 2);
|
|
1917
|
+
const payload = response.payload;
|
|
1918
|
+
if (!payload.success) {
|
|
1919
|
+
return { success: false, error: payload.error ?? "Serialization failed" };
|
|
1920
|
+
}
|
|
1921
|
+
return { success: true, data: payload.tree };
|
|
1922
|
+
}
|
|
1923
|
+
};
|
|
1924
|
+
}
|
|
1925
|
+
});
|
|
1926
|
+
|
|
1927
|
+
// src/tools/studio/move-instance.ts
|
|
1928
|
+
var move_instance_exports = {};
|
|
1929
|
+
__export(move_instance_exports, {
|
|
1930
|
+
tool: () => tool23
|
|
1931
|
+
});
|
|
1932
|
+
var tool23;
|
|
1933
|
+
var init_move_instance = __esm({
|
|
1934
|
+
"src/tools/studio/move-instance.ts"() {
|
|
1935
|
+
init_protocol();
|
|
1936
|
+
tool23 = {
|
|
1937
|
+
name: "move_instance",
|
|
1938
|
+
description: "Move (reparent) an instance to a new parent in Roblox Studio. This preserves the instance and all its descendants \u2014 much better than delete + recreate. Sets a ChangeHistoryService waypoint so the move can be undone.",
|
|
1939
|
+
parameters: {
|
|
1940
|
+
type: "object",
|
|
1941
|
+
properties: {
|
|
1942
|
+
path: {
|
|
1943
|
+
type: "string",
|
|
1944
|
+
description: 'Full path of the instance to move (e.g. "Workspace.OldFolder.MyPart")'
|
|
1945
|
+
},
|
|
1946
|
+
newParent: {
|
|
1947
|
+
type: "string",
|
|
1948
|
+
description: 'Full path of the new parent (e.g. "Workspace.NewFolder")'
|
|
1949
|
+
}
|
|
1950
|
+
},
|
|
1951
|
+
required: ["path", "newParent"]
|
|
1952
|
+
},
|
|
1953
|
+
async execute(params, ctx) {
|
|
1954
|
+
if (!ctx.isStudioConnected()) {
|
|
1955
|
+
return { success: false, error: "Studio is not connected" };
|
|
1956
|
+
}
|
|
1957
|
+
const response = await ctx.sendToStudio(CliMsg.MOVE_INSTANCE, {
|
|
1958
|
+
path: params.path,
|
|
1959
|
+
newParent: params.newParent
|
|
1960
|
+
});
|
|
1961
|
+
const payload = response.payload;
|
|
1962
|
+
if (!payload.success) {
|
|
1963
|
+
return { success: false, error: payload.error ?? "Move failed" };
|
|
1964
|
+
}
|
|
1965
|
+
return { success: true, data: { newPath: payload.newPath } };
|
|
1966
|
+
}
|
|
1967
|
+
};
|
|
1968
|
+
}
|
|
1969
|
+
});
|
|
1970
|
+
|
|
1971
|
+
// src/tools/studio/bulk-set-properties.ts
|
|
1972
|
+
var bulk_set_properties_exports = {};
|
|
1973
|
+
__export(bulk_set_properties_exports, {
|
|
1974
|
+
tool: () => tool24
|
|
1975
|
+
});
|
|
1976
|
+
var tool24;
|
|
1977
|
+
var init_bulk_set_properties = __esm({
|
|
1978
|
+
"src/tools/studio/bulk-set-properties.ts"() {
|
|
1979
|
+
init_protocol();
|
|
1980
|
+
tool24 = {
|
|
1981
|
+
name: "bulk_set_properties",
|
|
1982
|
+
description: 'Set properties on multiple instances at once in a single call. Much faster than calling set_properties repeatedly. Supports two modes:\n1. Explicit paths: provide an array of {path, properties} objects\n2. By class filter: provide a root path, className filter, and properties to apply to all matches\n\nFilter mode also supports addChildren: insert child instances (e.g. UIStroke, UICorner) on every match. Idempotent \u2014 skips if a child of that class already exists.\n\nExample (explicit): targets=[{"path": "StarterGui.HUD.Title", "properties": {"TextColor3": "#fff"}}]\nExample (filter): root="StarterGui.HUD", className="TextLabel", properties={"Font": "RobotoBold"}\nExample (addChildren): root="StarterGui.HUD", className="TextLabel", addChildren=[{"ClassName": "UIStroke", "Color": "#000000", "Thickness": 1.5}]',
|
|
1983
|
+
parameters: {
|
|
1984
|
+
type: "object",
|
|
1985
|
+
properties: {
|
|
1986
|
+
targets: {
|
|
1987
|
+
type: "array",
|
|
1988
|
+
description: "Array of {path, properties} objects for explicit mode.",
|
|
1989
|
+
items: {
|
|
1990
|
+
type: "object",
|
|
1991
|
+
description: "A target with path and properties to set."
|
|
1992
|
+
}
|
|
1993
|
+
},
|
|
1994
|
+
root: {
|
|
1995
|
+
type: "string",
|
|
1996
|
+
description: "Root instance path to search under (filter mode)"
|
|
1997
|
+
},
|
|
1998
|
+
className: {
|
|
1999
|
+
type: "string",
|
|
2000
|
+
description: "Only apply to instances of this ClassName (filter mode)"
|
|
2001
|
+
},
|
|
2002
|
+
properties: {
|
|
2003
|
+
type: "object",
|
|
2004
|
+
description: "Properties to set on all matched instances (filter mode)"
|
|
2005
|
+
},
|
|
2006
|
+
addChildren: {
|
|
2007
|
+
type: "array",
|
|
2008
|
+
description: "Child instances to insert on every match (filter mode). Each item: {ClassName, Name?, ...props}. Idempotent \u2014 skips if child of that class already exists.",
|
|
2009
|
+
items: {
|
|
2010
|
+
type: "object",
|
|
2011
|
+
description: "Child instance spec with ClassName and properties"
|
|
2012
|
+
}
|
|
2013
|
+
}
|
|
2014
|
+
}
|
|
2015
|
+
},
|
|
2016
|
+
async execute(params, ctx) {
|
|
2017
|
+
if (!ctx.isStudioConnected()) {
|
|
2018
|
+
return { success: false, error: "Studio is not connected" };
|
|
2019
|
+
}
|
|
2020
|
+
const response = await ctx.sendToStudio(CliMsg.BULK_SET_PROPERTIES, {
|
|
2021
|
+
targets: params.targets,
|
|
2022
|
+
root: params.root,
|
|
2023
|
+
className: params.className,
|
|
2024
|
+
properties: params.properties,
|
|
2025
|
+
addChildren: params.addChildren
|
|
2026
|
+
}, 1e3 * 60);
|
|
2027
|
+
const payload = response.payload;
|
|
2028
|
+
if (!payload.success) {
|
|
2029
|
+
return { success: false, error: payload.error ?? "Bulk set failed" };
|
|
2030
|
+
}
|
|
2031
|
+
return {
|
|
2032
|
+
success: true,
|
|
2033
|
+
data: {
|
|
2034
|
+
modified: payload.modified,
|
|
2035
|
+
errors: payload.errors?.length ? payload.errors : void 0
|
|
2036
|
+
}
|
|
2037
|
+
};
|
|
2038
|
+
}
|
|
2039
|
+
};
|
|
2040
|
+
}
|
|
2041
|
+
});
|
|
2042
|
+
|
|
2043
|
+
// src/tools/studio/find-replace-scripts.ts
|
|
2044
|
+
var find_replace_scripts_exports = {};
|
|
2045
|
+
__export(find_replace_scripts_exports, {
|
|
2046
|
+
tool: () => tool25
|
|
2047
|
+
});
|
|
2048
|
+
var tool25;
|
|
2049
|
+
var init_find_replace_scripts = __esm({
|
|
2050
|
+
"src/tools/studio/find-replace-scripts.ts"() {
|
|
2051
|
+
init_protocol();
|
|
2052
|
+
tool25 = {
|
|
2053
|
+
name: "find_replace_scripts",
|
|
2054
|
+
description: 'Find and replace text across all scripts in the game (or under a root path). Supports plain text and Lua pattern matching. Returns a list of modified scripts with match counts.\n\nUse dryRun=true to preview changes without applying them.\nExample: find="workspace" replace="Workspace" root="ServerScriptService"',
|
|
2055
|
+
parameters: {
|
|
2056
|
+
type: "object",
|
|
2057
|
+
properties: {
|
|
2058
|
+
find: {
|
|
2059
|
+
type: "string",
|
|
2060
|
+
description: "Text or Lua pattern to search for"
|
|
2061
|
+
},
|
|
2062
|
+
replace: {
|
|
2063
|
+
type: "string",
|
|
2064
|
+
description: "Replacement text (supports Lua pattern captures like %1, %2)"
|
|
2065
|
+
},
|
|
2066
|
+
root: {
|
|
2067
|
+
type: "string",
|
|
2068
|
+
description: "Root path to limit search scope (optional, searches entire game if omitted)"
|
|
2069
|
+
},
|
|
2070
|
+
usePattern: {
|
|
2071
|
+
type: "boolean",
|
|
2072
|
+
description: "If true, treat find/replace as Lua patterns. Default: false (plain text)"
|
|
2073
|
+
},
|
|
2074
|
+
dryRun: {
|
|
2075
|
+
type: "boolean",
|
|
2076
|
+
description: "If true, report matches but do not modify scripts. Default: false"
|
|
2077
|
+
}
|
|
2078
|
+
},
|
|
2079
|
+
required: ["find", "replace"]
|
|
2080
|
+
},
|
|
2081
|
+
async execute(params, ctx) {
|
|
2082
|
+
if (!ctx.isStudioConnected()) {
|
|
2083
|
+
return { success: false, error: "Studio is not connected" };
|
|
2084
|
+
}
|
|
2085
|
+
const response = await ctx.sendToStudio(CliMsg.FIND_REPLACE_SCRIPTS, {
|
|
2086
|
+
find: params.find,
|
|
2087
|
+
replace: params.replace,
|
|
2088
|
+
root: params.root,
|
|
2089
|
+
usePattern: params.usePattern ?? false,
|
|
2090
|
+
dryRun: params.dryRun ?? false
|
|
2091
|
+
}, 1e3 * 60 * 5);
|
|
2092
|
+
const payload = response.payload;
|
|
2093
|
+
if (!payload.success) {
|
|
2094
|
+
return { success: false, error: payload.error ?? "Find/replace failed" };
|
|
2095
|
+
}
|
|
2096
|
+
return {
|
|
2097
|
+
success: true,
|
|
2098
|
+
data: {
|
|
2099
|
+
totalFiles: payload.totalFiles,
|
|
2100
|
+
totalMatches: payload.totalMatches,
|
|
2101
|
+
modified: payload.modified,
|
|
2102
|
+
dryRun: params.dryRun ?? false
|
|
2103
|
+
}
|
|
2104
|
+
};
|
|
2105
|
+
}
|
|
2106
|
+
};
|
|
2107
|
+
}
|
|
2108
|
+
});
|
|
2109
|
+
|
|
2110
|
+
// src/tools/docs/search-roblox-api.ts
|
|
2111
|
+
var search_roblox_api_exports = {};
|
|
2112
|
+
__export(search_roblox_api_exports, {
|
|
2113
|
+
tool: () => tool26
|
|
2114
|
+
});
|
|
2115
|
+
var tool26;
|
|
2116
|
+
var init_search_roblox_api = __esm({
|
|
2117
|
+
"src/tools/docs/search-roblox-api.ts"() {
|
|
2118
|
+
init_roblox_api();
|
|
2119
|
+
tool26 = {
|
|
2120
|
+
name: "search_roblox_api",
|
|
2121
|
+
description: "Search Roblox Creator Docs engine API reference names. Use this when you are unsure of an exact class, enum, datatype, global, or library name.",
|
|
2122
|
+
parameters: {
|
|
2123
|
+
type: "object",
|
|
2124
|
+
properties: {
|
|
2125
|
+
query: {
|
|
2126
|
+
type: "string",
|
|
2127
|
+
description: 'Name fragment to search, e.g. "Text", "Run", "UDim", "FrameStyle"'
|
|
2128
|
+
},
|
|
2129
|
+
kind: {
|
|
2130
|
+
type: "string",
|
|
2131
|
+
description: "Optional reference kind filter.",
|
|
2132
|
+
enum: ["any", "class", "datatype", "enum", "global", "library"],
|
|
2133
|
+
default: "any"
|
|
2134
|
+
},
|
|
2135
|
+
limit: {
|
|
2136
|
+
type: "number",
|
|
2137
|
+
description: "Maximum results to return (default: 10, max: 50)",
|
|
2138
|
+
default: 10
|
|
2139
|
+
}
|
|
2140
|
+
},
|
|
2141
|
+
required: ["query"]
|
|
2142
|
+
},
|
|
2143
|
+
async execute(params) {
|
|
2144
|
+
try {
|
|
2145
|
+
const results = await searchRobloxApi(String(params.query ?? ""), {
|
|
2146
|
+
kind: params.kind ?? "any",
|
|
2147
|
+
limit: params.limit ?? 10
|
|
2148
|
+
});
|
|
2149
|
+
return { success: true, data: { results } };
|
|
2150
|
+
} catch (err) {
|
|
2151
|
+
return { success: false, error: err instanceof Error ? err.message : String(err) };
|
|
2152
|
+
}
|
|
2153
|
+
}
|
|
2154
|
+
};
|
|
2155
|
+
}
|
|
2156
|
+
});
|
|
2157
|
+
|
|
2158
|
+
// src/tools/docs/get-roblox-api-reference.ts
|
|
2159
|
+
var get_roblox_api_reference_exports = {};
|
|
2160
|
+
__export(get_roblox_api_reference_exports, {
|
|
2161
|
+
tool: () => tool27
|
|
2162
|
+
});
|
|
2163
|
+
var tool27;
|
|
2164
|
+
var init_get_roblox_api_reference = __esm({
|
|
2165
|
+
"src/tools/docs/get-roblox-api-reference.ts"() {
|
|
2166
|
+
init_roblox_api();
|
|
2167
|
+
tool27 = {
|
|
2168
|
+
name: "get_roblox_api_reference",
|
|
2169
|
+
description: "Look up Roblox Creator Docs engine API reference for a class, datatype, enum, global, or library. Works without Studio connected and includes summaries, descriptions, tags, members, parameters, returns, security, and thread safety.",
|
|
2170
|
+
parameters: {
|
|
2171
|
+
type: "object",
|
|
2172
|
+
properties: {
|
|
2173
|
+
name: {
|
|
2174
|
+
type: "string",
|
|
2175
|
+
description: 'API reference name, e.g. "Frame", "RunService", "UDim2", "FrameStyle", "task"'
|
|
2176
|
+
},
|
|
2177
|
+
kind: {
|
|
2178
|
+
type: "string",
|
|
2179
|
+
description: 'Optional reference kind. Use "any" to search all reference kinds.',
|
|
2180
|
+
enum: ["any", "class", "datatype", "enum", "global", "library"],
|
|
2181
|
+
default: "any"
|
|
2182
|
+
},
|
|
2183
|
+
includeInherited: {
|
|
2184
|
+
type: "boolean",
|
|
2185
|
+
description: "For classes, include members from inherited base classes (default: true).",
|
|
2186
|
+
default: true
|
|
2187
|
+
}
|
|
2188
|
+
},
|
|
2189
|
+
required: ["name"]
|
|
2190
|
+
},
|
|
2191
|
+
async execute(params) {
|
|
2192
|
+
try {
|
|
2193
|
+
const reference = await getRobloxApiReference(String(params.name ?? ""), {
|
|
2194
|
+
kind: params.kind ?? "any",
|
|
2195
|
+
includeInherited: params.includeInherited !== false
|
|
2196
|
+
});
|
|
2197
|
+
return { success: true, data: reference };
|
|
2198
|
+
} catch (err) {
|
|
2199
|
+
return { success: false, error: err instanceof Error ? err.message : String(err) };
|
|
2200
|
+
}
|
|
2201
|
+
}
|
|
2202
|
+
};
|
|
2203
|
+
}
|
|
2204
|
+
});
|
|
2205
|
+
|
|
2206
|
+
// src/tools/memory/recall.ts
|
|
2207
|
+
var recall_exports = {};
|
|
2208
|
+
__export(recall_exports, {
|
|
2209
|
+
tool: () => tool28
|
|
2210
|
+
});
|
|
2211
|
+
var tool28;
|
|
2212
|
+
var init_recall = __esm({
|
|
2213
|
+
"src/tools/memory/recall.ts"() {
|
|
2214
|
+
init_store();
|
|
2215
|
+
tool28 = {
|
|
2216
|
+
name: "recall_memory",
|
|
2217
|
+
description: "Search your memory for previously learned facts about the current project. Use this to recall architecture decisions, patterns, known bugs, or user preferences.",
|
|
2218
|
+
parameters: {
|
|
2219
|
+
type: "object",
|
|
2220
|
+
properties: {
|
|
2221
|
+
query: {
|
|
2222
|
+
type: "string",
|
|
2223
|
+
description: 'What to search for in memory (e.g., "player data storage", "combat system architecture")'
|
|
2224
|
+
},
|
|
2225
|
+
limit: {
|
|
2226
|
+
type: "number",
|
|
2227
|
+
description: "Max number of facts to return (default: 10)",
|
|
2228
|
+
default: 10
|
|
2229
|
+
}
|
|
2230
|
+
},
|
|
2231
|
+
required: ["query"]
|
|
2232
|
+
},
|
|
2233
|
+
async execute(params) {
|
|
2234
|
+
const query = params.query;
|
|
2235
|
+
const limit = params.limit ?? 10;
|
|
2236
|
+
const projectId = "current";
|
|
2237
|
+
const facts = recallFacts(projectId, query, limit);
|
|
2238
|
+
const summaries = getProjectSummaries(projectId, 5);
|
|
2239
|
+
return {
|
|
2240
|
+
success: true,
|
|
2241
|
+
data: {
|
|
2242
|
+
facts: facts.map((f) => ({ content: f.content, category: f.category })),
|
|
2243
|
+
recentSessions: summaries.map((s) => s.summary)
|
|
2244
|
+
}
|
|
2245
|
+
};
|
|
2246
|
+
}
|
|
2247
|
+
};
|
|
2248
|
+
}
|
|
2249
|
+
});
|
|
12
2250
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
};
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
}
|
|
57
|
-
function parseMessage(raw) {
|
|
58
|
-
try {
|
|
59
|
-
return JSON.parse(raw);
|
|
60
|
-
} catch {
|
|
61
|
-
throw new Error(`Failed to parse WebSocket message: ${raw.slice(0, 100)}`);
|
|
2251
|
+
// src/tools/memory/remember.ts
|
|
2252
|
+
var remember_exports = {};
|
|
2253
|
+
__export(remember_exports, {
|
|
2254
|
+
tool: () => tool29
|
|
2255
|
+
});
|
|
2256
|
+
var tool29;
|
|
2257
|
+
var init_remember = __esm({
|
|
2258
|
+
"src/tools/memory/remember.ts"() {
|
|
2259
|
+
init_store();
|
|
2260
|
+
tool29 = {
|
|
2261
|
+
name: "remember",
|
|
2262
|
+
description: "Store a fact about the current project in persistent memory. Use this to remember architecture decisions, patterns, bugs found, or user preferences so you can recall them in future sessions.",
|
|
2263
|
+
parameters: {
|
|
2264
|
+
type: "object",
|
|
2265
|
+
properties: {
|
|
2266
|
+
fact: {
|
|
2267
|
+
type: "string",
|
|
2268
|
+
description: 'The fact to remember (e.g., "PlayerData DataStore uses key format Player_{UserId}")'
|
|
2269
|
+
},
|
|
2270
|
+
category: {
|
|
2271
|
+
type: "string",
|
|
2272
|
+
description: "Category of the fact",
|
|
2273
|
+
enum: ["architecture", "pattern", "preference", "bug", "api", "general"]
|
|
2274
|
+
}
|
|
2275
|
+
},
|
|
2276
|
+
required: ["fact", "category"]
|
|
2277
|
+
},
|
|
2278
|
+
async execute(params) {
|
|
2279
|
+
const fact = params.fact;
|
|
2280
|
+
const category = params.category ?? "general";
|
|
2281
|
+
const now = Date.now();
|
|
2282
|
+
storeFact({
|
|
2283
|
+
projectId: "current",
|
|
2284
|
+
content: fact,
|
|
2285
|
+
category,
|
|
2286
|
+
source: "agent",
|
|
2287
|
+
relevance: 1,
|
|
2288
|
+
createdAt: now,
|
|
2289
|
+
accessedAt: now
|
|
2290
|
+
});
|
|
2291
|
+
return { success: true, data: { stored: fact, category } };
|
|
2292
|
+
}
|
|
2293
|
+
};
|
|
62
2294
|
}
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
}
|
|
2295
|
+
});
|
|
2296
|
+
|
|
2297
|
+
// src/tools/cloud/upload-asset.ts
|
|
2298
|
+
var upload_asset_exports = {};
|
|
2299
|
+
__export(upload_asset_exports, {
|
|
2300
|
+
tool: () => tool30
|
|
2301
|
+
});
|
|
2302
|
+
var tool30;
|
|
2303
|
+
var init_upload_asset = __esm({
|
|
2304
|
+
"src/tools/cloud/upload-asset.ts"() {
|
|
2305
|
+
tool30 = {
|
|
2306
|
+
name: "upload_asset",
|
|
2307
|
+
description: "Upload a local file (image, model, audio) to Roblox via the Open Cloud API. Requires an Open Cloud API key configured in dominus.",
|
|
2308
|
+
parameters: {
|
|
2309
|
+
type: "object",
|
|
2310
|
+
properties: {
|
|
2311
|
+
filePath: {
|
|
2312
|
+
type: "string",
|
|
2313
|
+
description: "Local file path of the asset to upload"
|
|
2314
|
+
},
|
|
2315
|
+
assetType: {
|
|
2316
|
+
type: "string",
|
|
2317
|
+
description: "Type of asset",
|
|
2318
|
+
enum: ["Image", "Audio", "Model"]
|
|
2319
|
+
},
|
|
2320
|
+
name: {
|
|
2321
|
+
type: "string",
|
|
2322
|
+
description: "Display name for the asset on Roblox"
|
|
2323
|
+
},
|
|
2324
|
+
description: {
|
|
2325
|
+
type: "string",
|
|
2326
|
+
description: "Description of the asset"
|
|
2327
|
+
}
|
|
2328
|
+
},
|
|
2329
|
+
required: ["filePath", "assetType", "name"]
|
|
2330
|
+
},
|
|
2331
|
+
async execute(params, ctx) {
|
|
2332
|
+
const apiKey = ctx.config.get("openCloudApiKey");
|
|
2333
|
+
if (!apiKey) {
|
|
2334
|
+
return {
|
|
2335
|
+
success: false,
|
|
2336
|
+
error: "Open Cloud API key not configured. Run: dominus config set openCloudApiKey <key>"
|
|
2337
|
+
};
|
|
2338
|
+
}
|
|
2339
|
+
const filePath = params.filePath;
|
|
2340
|
+
const assetType = params.assetType;
|
|
2341
|
+
const name = params.name;
|
|
2342
|
+
const description = params.description ?? "";
|
|
2343
|
+
if (!fs3.existsSync(filePath)) {
|
|
2344
|
+
return { success: false, error: `File not found: ${filePath}` };
|
|
2345
|
+
}
|
|
2346
|
+
const fileBuffer = fs3.readFileSync(filePath);
|
|
2347
|
+
const fileName = path2.basename(filePath);
|
|
2348
|
+
const typeMapping = {
|
|
2349
|
+
Image: "Decal",
|
|
2350
|
+
Audio: "Audio",
|
|
2351
|
+
Model: "Model"
|
|
2352
|
+
};
|
|
2353
|
+
try {
|
|
2354
|
+
const formData = new FormData();
|
|
2355
|
+
const blob = new Blob([fileBuffer]);
|
|
2356
|
+
formData.append("fileContent", blob, fileName);
|
|
2357
|
+
formData.append(
|
|
2358
|
+
"request",
|
|
2359
|
+
JSON.stringify({
|
|
2360
|
+
assetType: typeMapping[assetType] ?? assetType,
|
|
2361
|
+
displayName: name,
|
|
2362
|
+
description,
|
|
2363
|
+
creationContext: { creator: { userId: "me" } }
|
|
2364
|
+
})
|
|
2365
|
+
);
|
|
2366
|
+
const response = await fetch(
|
|
2367
|
+
"https://apis.roblox.com/assets/v1/assets",
|
|
2368
|
+
{
|
|
2369
|
+
method: "POST",
|
|
2370
|
+
headers: {
|
|
2371
|
+
"x-api-key": apiKey
|
|
2372
|
+
},
|
|
2373
|
+
body: formData
|
|
2374
|
+
}
|
|
2375
|
+
);
|
|
2376
|
+
if (!response.ok) {
|
|
2377
|
+
const text = await response.text();
|
|
2378
|
+
return { success: false, error: `Upload failed (${response.status}): ${text}` };
|
|
2379
|
+
}
|
|
2380
|
+
const data = await response.json();
|
|
2381
|
+
return {
|
|
2382
|
+
success: true,
|
|
2383
|
+
data: {
|
|
2384
|
+
assetId: data.assetId ?? data.id,
|
|
2385
|
+
operationId: data.operationId ?? data.path,
|
|
2386
|
+
name,
|
|
2387
|
+
type: assetType
|
|
2388
|
+
}
|
|
2389
|
+
};
|
|
2390
|
+
} catch (err) {
|
|
2391
|
+
return {
|
|
2392
|
+
success: false,
|
|
2393
|
+
error: `Upload error: ${err instanceof Error ? err.message : String(err)}`
|
|
2394
|
+
};
|
|
2395
|
+
}
|
|
2396
|
+
}
|
|
2397
|
+
};
|
|
2398
|
+
}
|
|
2399
|
+
});
|
|
2400
|
+
|
|
2401
|
+
// src/tools/studio/undo-redo.ts
|
|
2402
|
+
var undo_redo_exports = {};
|
|
2403
|
+
__export(undo_redo_exports, {
|
|
2404
|
+
redoTool: () => redoTool,
|
|
2405
|
+
undoTool: () => undoTool
|
|
2406
|
+
});
|
|
2407
|
+
var undoTool, redoTool;
|
|
2408
|
+
var init_undo_redo = __esm({
|
|
2409
|
+
"src/tools/studio/undo-redo.ts"() {
|
|
2410
|
+
init_protocol();
|
|
2411
|
+
undoTool = {
|
|
2412
|
+
name: "undo",
|
|
2413
|
+
description: "Undo the last action in Roblox Studio using ChangeHistoryService. Works just like Ctrl+Z. Use this to roll back mistakes.",
|
|
2414
|
+
parameters: {
|
|
2415
|
+
type: "object",
|
|
2416
|
+
properties: {}
|
|
2417
|
+
},
|
|
2418
|
+
async execute(_params, ctx) {
|
|
2419
|
+
if (!ctx.isStudioConnected()) {
|
|
2420
|
+
return { success: false, error: "Studio is not connected" };
|
|
2421
|
+
}
|
|
2422
|
+
const response = await ctx.sendToStudio(CliMsg.UNDO, {});
|
|
2423
|
+
const payload = response.payload;
|
|
2424
|
+
if (!payload.success) {
|
|
2425
|
+
return { success: false, error: payload.error ?? "Undo failed" };
|
|
2426
|
+
}
|
|
2427
|
+
return { success: true, data: "Undo performed" };
|
|
2428
|
+
}
|
|
2429
|
+
};
|
|
2430
|
+
redoTool = {
|
|
2431
|
+
name: "redo",
|
|
2432
|
+
description: "Redo the last undone action in Roblox Studio using ChangeHistoryService. Works just like Ctrl+Y.",
|
|
2433
|
+
parameters: {
|
|
2434
|
+
type: "object",
|
|
2435
|
+
properties: {}
|
|
2436
|
+
},
|
|
2437
|
+
async execute(_params, ctx) {
|
|
2438
|
+
if (!ctx.isStudioConnected()) {
|
|
2439
|
+
return { success: false, error: "Studio is not connected" };
|
|
2440
|
+
}
|
|
2441
|
+
const response = await ctx.sendToStudio(CliMsg.REDO, {});
|
|
2442
|
+
const payload = response.payload;
|
|
2443
|
+
if (!payload.success) {
|
|
2444
|
+
return { success: false, error: payload.error ?? "Redo failed" };
|
|
2445
|
+
}
|
|
2446
|
+
return { success: true, data: "Redo performed" };
|
|
2447
|
+
}
|
|
2448
|
+
};
|
|
2449
|
+
}
|
|
2450
|
+
});
|
|
67
2451
|
|
|
68
2452
|
// src/transport/ws-server.ts
|
|
2453
|
+
init_protocol();
|
|
69
2454
|
var DominusServer = class extends EventEmitter {
|
|
70
2455
|
wss = null;
|
|
71
2456
|
/** All connected Studio instances, keyed by placeId (0 for unknown) */
|
|
@@ -141,8 +2526,8 @@ var DominusServer = class extends EventEmitter {
|
|
|
141
2526
|
ws.on("close", () => {
|
|
142
2527
|
if (this.controllers.has(ws)) {
|
|
143
2528
|
this.controllers.delete(ws);
|
|
144
|
-
for (const [id,
|
|
145
|
-
if (controllerWs === ws) this.relayRequests.delete(id);
|
|
2529
|
+
for (const [id, relay] of this.relayRequests) {
|
|
2530
|
+
if (relay.controllerWs === ws) this.relayRequests.delete(id);
|
|
146
2531
|
}
|
|
147
2532
|
} else {
|
|
148
2533
|
const placeId = this.wsToPlaceId.get(ws);
|
|
@@ -153,12 +2538,28 @@ var DominusServer = class extends EventEmitter {
|
|
|
153
2538
|
this.emit("studio:disconnected", { placeId, placeName: conn?.placeName });
|
|
154
2539
|
this.broadcastToControllers("studio:disconnected", { placeId, placeName: conn?.placeName });
|
|
155
2540
|
for (const [id, pending] of this.pendingRequests) {
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
2541
|
+
if (pending.targetWs === ws) {
|
|
2542
|
+
clearTimeout(pending.timer);
|
|
2543
|
+
pending.reject(new Error(`Studio disconnected (place ${placeId})`));
|
|
2544
|
+
this.pendingRequests.delete(id);
|
|
2545
|
+
}
|
|
2546
|
+
}
|
|
2547
|
+
for (const [id, relay] of this.relayRequests) {
|
|
2548
|
+
if (relay.targetWs === ws) {
|
|
2549
|
+
if (relay.controllerWs.readyState === WebSocket.OPEN) {
|
|
2550
|
+
relay.controllerWs.send(serializeMessage({
|
|
2551
|
+
id,
|
|
2552
|
+
type: StudioMsg.RESPONSE,
|
|
2553
|
+
payload: { error: `Studio disconnected (place ${placeId})` },
|
|
2554
|
+
ts: Date.now()
|
|
2555
|
+
}));
|
|
2556
|
+
}
|
|
2557
|
+
this.relayRequests.delete(id);
|
|
2558
|
+
}
|
|
159
2559
|
}
|
|
160
2560
|
if (this.activePlaceId === placeId) {
|
|
161
2561
|
this.activePlaceId = 0;
|
|
2562
|
+
this.broadcastTargetState();
|
|
162
2563
|
}
|
|
163
2564
|
}
|
|
164
2565
|
}
|
|
@@ -177,6 +2578,7 @@ var DominusServer = class extends EventEmitter {
|
|
|
177
2578
|
}
|
|
178
2579
|
const conn = {
|
|
179
2580
|
ws,
|
|
2581
|
+
connectionId: `${placeId}:${Date.now()}:${Math.random().toString(36).slice(2, 8)}`,
|
|
180
2582
|
connectedAt: Date.now(),
|
|
181
2583
|
studioVersion: payload.studioVersion,
|
|
182
2584
|
placeId,
|
|
@@ -184,8 +2586,12 @@ var DominusServer = class extends EventEmitter {
|
|
|
184
2586
|
};
|
|
185
2587
|
this.studioClients.set(placeId, conn);
|
|
186
2588
|
this.wsToPlaceId.set(ws, placeId);
|
|
187
|
-
this.
|
|
188
|
-
|
|
2589
|
+
if (this.studioClients.size === 1 && this.activePlaceId === 0) {
|
|
2590
|
+
this.activePlaceId = placeId;
|
|
2591
|
+
}
|
|
2592
|
+
this.emit("studio:connected", { ...payload, placeId, connectionId: conn.connectionId });
|
|
2593
|
+
this.broadcastToControllers("studio:connected", { ...payload, placeId, connectionId: conn.connectionId });
|
|
2594
|
+
this.broadcastTargetState();
|
|
189
2595
|
}
|
|
190
2596
|
addController(ws) {
|
|
191
2597
|
this.controllers.add(ws);
|
|
@@ -200,6 +2606,27 @@ var DominusServer = class extends EventEmitter {
|
|
|
200
2606
|
}
|
|
201
2607
|
}
|
|
202
2608
|
handleControllerMessage(controllerWs, msg) {
|
|
2609
|
+
if (msg.type === "controller:set_active_place") {
|
|
2610
|
+
const payload = msg.payload;
|
|
2611
|
+
try {
|
|
2612
|
+
this.setActivePlaceId(Number(payload.placeId ?? 0));
|
|
2613
|
+
} catch (err) {
|
|
2614
|
+
controllerWs.send(serializeMessage({
|
|
2615
|
+
id: msg.id,
|
|
2616
|
+
type: StudioMsg.RESPONSE,
|
|
2617
|
+
payload: { success: false, error: err instanceof Error ? err.message : String(err) },
|
|
2618
|
+
ts: Date.now()
|
|
2619
|
+
}));
|
|
2620
|
+
return;
|
|
2621
|
+
}
|
|
2622
|
+
controllerWs.send(serializeMessage({
|
|
2623
|
+
id: msg.id,
|
|
2624
|
+
type: StudioMsg.RESPONSE,
|
|
2625
|
+
payload: { success: true, activePlaceId: this.activePlaceId, connections: this.listConnections() },
|
|
2626
|
+
ts: Date.now()
|
|
2627
|
+
}));
|
|
2628
|
+
return;
|
|
2629
|
+
}
|
|
203
2630
|
const target = this.resolveTarget();
|
|
204
2631
|
if (!target || target.ws.readyState !== WebSocket.OPEN) {
|
|
205
2632
|
controllerWs.send(serializeMessage({
|
|
@@ -210,7 +2637,11 @@ var DominusServer = class extends EventEmitter {
|
|
|
210
2637
|
}));
|
|
211
2638
|
return;
|
|
212
2639
|
}
|
|
213
|
-
this.relayRequests.set(msg.id,
|
|
2640
|
+
this.relayRequests.set(msg.id, {
|
|
2641
|
+
controllerWs,
|
|
2642
|
+
targetWs: target.ws,
|
|
2643
|
+
targetPlaceId: target.placeId ?? 0
|
|
2644
|
+
});
|
|
214
2645
|
target.ws.send(serializeMessage(msg));
|
|
215
2646
|
}
|
|
216
2647
|
handleStudioMessage(ws, msg) {
|
|
@@ -230,14 +2661,25 @@ var DominusServer = class extends EventEmitter {
|
|
|
230
2661
|
return;
|
|
231
2662
|
}
|
|
232
2663
|
if (msg.type === StudioMsg.RESPONSE) {
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
2664
|
+
if (!this.wsToPlaceId.has(ws)) return;
|
|
2665
|
+
const relay = this.relayRequests.get(msg.id);
|
|
2666
|
+
if (relay) {
|
|
2667
|
+
if (relay.targetWs !== ws) {
|
|
2668
|
+
this.emit("server:error", new Error(`Ignoring relayed response ${msg.id} from non-target Studio`));
|
|
2669
|
+
return;
|
|
2670
|
+
}
|
|
2671
|
+
if (relay.controllerWs.readyState === WebSocket.OPEN) {
|
|
2672
|
+
relay.controllerWs.send(serializeMessage(msg));
|
|
2673
|
+
}
|
|
236
2674
|
this.relayRequests.delete(msg.id);
|
|
237
2675
|
return;
|
|
238
2676
|
}
|
|
239
2677
|
const pending = this.pendingRequests.get(msg.id);
|
|
240
2678
|
if (pending) {
|
|
2679
|
+
if (pending.targetWs !== ws) {
|
|
2680
|
+
this.emit("server:error", new Error(`Ignoring response ${msg.id} from non-target Studio`));
|
|
2681
|
+
return;
|
|
2682
|
+
}
|
|
241
2683
|
clearTimeout(pending.timer);
|
|
242
2684
|
pending.resolve(msg);
|
|
243
2685
|
this.pendingRequests.delete(msg.id);
|
|
@@ -287,7 +2729,9 @@ var DominusServer = class extends EventEmitter {
|
|
|
287
2729
|
this.pendingRequests.set(msg.id, {
|
|
288
2730
|
resolve,
|
|
289
2731
|
reject,
|
|
290
|
-
timer
|
|
2732
|
+
timer,
|
|
2733
|
+
targetWs: target.ws,
|
|
2734
|
+
targetPlaceId: target.placeId ?? 0
|
|
291
2735
|
});
|
|
292
2736
|
target.ws.send(serializeMessage(msg));
|
|
293
2737
|
});
|
|
@@ -307,16 +2751,28 @@ var DominusServer = class extends EventEmitter {
|
|
|
307
2751
|
listConnections() {
|
|
308
2752
|
return [...this.studioClients.values()].filter((c) => c.ws.readyState === WebSocket.OPEN).map((c) => ({
|
|
309
2753
|
placeId: c.placeId ?? 0,
|
|
2754
|
+
connectionId: c.connectionId,
|
|
310
2755
|
placeName: c.placeName,
|
|
311
2756
|
studioVersion: c.studioVersion,
|
|
312
|
-
connectedAt: c.connectedAt
|
|
2757
|
+
connectedAt: c.connectedAt,
|
|
2758
|
+
active: this.activePlaceId === 0 && this.studioClients.size === 1 || c.placeId === this.activePlaceId
|
|
313
2759
|
}));
|
|
314
2760
|
}
|
|
315
2761
|
getActivePlaceId() {
|
|
316
2762
|
return this.activePlaceId;
|
|
317
2763
|
}
|
|
318
2764
|
setActivePlaceId(placeId) {
|
|
2765
|
+
if (placeId !== 0 && !this.studioClients.has(placeId)) {
|
|
2766
|
+
throw new Error(`Place ${placeId} is not connected`);
|
|
2767
|
+
}
|
|
319
2768
|
this.activePlaceId = placeId;
|
|
2769
|
+
this.broadcastTargetState();
|
|
2770
|
+
}
|
|
2771
|
+
broadcastTargetState() {
|
|
2772
|
+
this.broadcastToControllers("controller:target_state", {
|
|
2773
|
+
activePlaceId: this.activePlaceId,
|
|
2774
|
+
connections: this.listConnections()
|
|
2775
|
+
});
|
|
320
2776
|
}
|
|
321
2777
|
getPort() {
|
|
322
2778
|
return this.port;
|
|
@@ -346,6 +2802,9 @@ var DominusServer = class extends EventEmitter {
|
|
|
346
2802
|
});
|
|
347
2803
|
}
|
|
348
2804
|
};
|
|
2805
|
+
|
|
2806
|
+
// src/transport/ws-client.ts
|
|
2807
|
+
init_protocol();
|
|
349
2808
|
var DominusClient = class extends EventEmitter {
|
|
350
2809
|
ws = null;
|
|
351
2810
|
port;
|
|
@@ -398,12 +2857,7 @@ var DominusClient = class extends EventEmitter {
|
|
|
398
2857
|
if (p.connections) {
|
|
399
2858
|
this.connections.clear();
|
|
400
2859
|
for (const c of p.connections) {
|
|
401
|
-
this.connections.set(c.placeId,
|
|
402
|
-
connectedAt: c.connectedAt,
|
|
403
|
-
studioVersion: c.studioVersion,
|
|
404
|
-
placeId: c.placeId,
|
|
405
|
-
placeName: c.placeName
|
|
406
|
-
});
|
|
2860
|
+
this.connections.set(c.placeId, toConnectionInfo(c));
|
|
407
2861
|
}
|
|
408
2862
|
}
|
|
409
2863
|
if (p.activePlaceId !== void 0) {
|
|
@@ -414,12 +2868,19 @@ var DominusClient = class extends EventEmitter {
|
|
|
414
2868
|
}
|
|
415
2869
|
return;
|
|
416
2870
|
}
|
|
2871
|
+
if (msg.type === "controller:target_state") {
|
|
2872
|
+
const payload = msg.payload;
|
|
2873
|
+
this.applyTargetState(payload);
|
|
2874
|
+
this.emit("controller:target_state", payload);
|
|
2875
|
+
return;
|
|
2876
|
+
}
|
|
417
2877
|
if (msg.type === "studio:connected") {
|
|
418
2878
|
this.studioConnected = true;
|
|
419
2879
|
const payload = msg.payload;
|
|
420
2880
|
const placeId = payload.placeId ?? 0;
|
|
421
2881
|
this.connections.set(placeId, {
|
|
422
2882
|
connectedAt: Date.now(),
|
|
2883
|
+
connectionId: payload.connectionId ?? `${placeId}`,
|
|
423
2884
|
studioVersion: payload.studioVersion,
|
|
424
2885
|
placeId,
|
|
425
2886
|
placeName: payload.placeName
|
|
@@ -437,6 +2898,7 @@ var DominusClient = class extends EventEmitter {
|
|
|
437
2898
|
if (this.activePlaceId === placeId) {
|
|
438
2899
|
this.activePlaceId = 0;
|
|
439
2900
|
}
|
|
2901
|
+
this.studioConnected = this.resolveTargetInfo() !== null;
|
|
440
2902
|
this.emit("studio:disconnected", payload);
|
|
441
2903
|
return;
|
|
442
2904
|
}
|
|
@@ -481,27 +2943,57 @@ var DominusClient = class extends EventEmitter {
|
|
|
481
2943
|
this.ws.send(serializeMessage(msg));
|
|
482
2944
|
}
|
|
483
2945
|
isConnected() {
|
|
484
|
-
return this.
|
|
2946
|
+
return this.resolveTargetInfo() !== null;
|
|
485
2947
|
}
|
|
486
2948
|
getConnectionInfo() {
|
|
487
|
-
|
|
488
|
-
const target = this.activePlaceId !== 0 ? this.connections.get(this.activePlaceId) : this.connections.values().next().value;
|
|
2949
|
+
const target = this.resolveTargetInfo();
|
|
489
2950
|
if (!target) return null;
|
|
490
2951
|
return { ...target, ws: this.ws };
|
|
491
2952
|
}
|
|
492
2953
|
listConnections() {
|
|
493
2954
|
return [...this.connections.values()].map((c) => ({
|
|
494
2955
|
placeId: c.placeId ?? 0,
|
|
2956
|
+
connectionId: c.connectionId,
|
|
495
2957
|
placeName: c.placeName,
|
|
496
2958
|
studioVersion: c.studioVersion,
|
|
497
|
-
connectedAt: c.connectedAt
|
|
2959
|
+
connectedAt: c.connectedAt,
|
|
2960
|
+
active: this.activePlaceId === 0 && this.connections.size === 1 || c.placeId === this.activePlaceId
|
|
498
2961
|
}));
|
|
499
2962
|
}
|
|
500
2963
|
getActivePlaceId() {
|
|
501
2964
|
return this.activePlaceId;
|
|
502
2965
|
}
|
|
503
2966
|
setActivePlaceId(placeId) {
|
|
2967
|
+
if (placeId !== 0 && !this.connections.has(placeId)) {
|
|
2968
|
+
throw new Error(`Place ${placeId} is not connected`);
|
|
2969
|
+
}
|
|
504
2970
|
this.activePlaceId = placeId;
|
|
2971
|
+
if (this.ws?.readyState === WebSocket.OPEN) {
|
|
2972
|
+
const msg = createMessage("controller:set_active_place", { placeId });
|
|
2973
|
+
this.ws.send(serializeMessage(msg));
|
|
2974
|
+
}
|
|
2975
|
+
}
|
|
2976
|
+
applyTargetState(payload) {
|
|
2977
|
+
if (payload.activePlaceId !== void 0) {
|
|
2978
|
+
this.activePlaceId = payload.activePlaceId;
|
|
2979
|
+
}
|
|
2980
|
+
if (payload.connections) {
|
|
2981
|
+
this.connections.clear();
|
|
2982
|
+
for (const c of payload.connections) {
|
|
2983
|
+
this.connections.set(c.placeId, toConnectionInfo(c));
|
|
2984
|
+
}
|
|
2985
|
+
}
|
|
2986
|
+
this.studioConnected = this.resolveTargetInfo() !== null;
|
|
2987
|
+
}
|
|
2988
|
+
resolveTargetInfo() {
|
|
2989
|
+
if (this.connections.size === 0) return null;
|
|
2990
|
+
if (this.activePlaceId !== 0) {
|
|
2991
|
+
return this.connections.get(this.activePlaceId) ?? null;
|
|
2992
|
+
}
|
|
2993
|
+
if (this.connections.size === 1) {
|
|
2994
|
+
return this.connections.values().next().value ?? null;
|
|
2995
|
+
}
|
|
2996
|
+
return null;
|
|
505
2997
|
}
|
|
506
2998
|
getPort() {
|
|
507
2999
|
return this.port;
|
|
@@ -521,6 +3013,15 @@ var DominusClient = class extends EventEmitter {
|
|
|
521
3013
|
});
|
|
522
3014
|
}
|
|
523
3015
|
};
|
|
3016
|
+
function toConnectionInfo(c) {
|
|
3017
|
+
return {
|
|
3018
|
+
connectedAt: c.connectedAt,
|
|
3019
|
+
connectionId: c.connectionId,
|
|
3020
|
+
studioVersion: c.studioVersion,
|
|
3021
|
+
placeId: c.placeId,
|
|
3022
|
+
placeName: c.placeName
|
|
3023
|
+
};
|
|
3024
|
+
}
|
|
524
3025
|
async function isPortInUse(port) {
|
|
525
3026
|
return new Promise((resolve) => {
|
|
526
3027
|
const ws = new WebSocket(`ws://localhost:${port}`);
|
|
@@ -531,200 +3032,20 @@ async function isPortInUse(port) {
|
|
|
531
3032
|
ws.on("open", () => {
|
|
532
3033
|
clearTimeout(timer);
|
|
533
3034
|
ws.close();
|
|
534
|
-
resolve(true);
|
|
535
|
-
});
|
|
536
|
-
ws.on("error", () => {
|
|
537
|
-
clearTimeout(timer);
|
|
538
|
-
resolve(false);
|
|
539
|
-
});
|
|
540
|
-
});
|
|
541
|
-
}
|
|
542
|
-
var DOMINUS_DIR = path.join(os.homedir(), ".dominus");
|
|
543
|
-
var CONFIG_PATH = path.join(DOMINUS_DIR, "config.json");
|
|
544
|
-
var DB_PATH = path.join(DOMINUS_DIR, "dominus.db");
|
|
545
|
-
path.join(DOMINUS_DIR, "rules.json");
|
|
546
|
-
var DEFAULTS = {
|
|
547
|
-
provider: "openai",
|
|
548
|
-
apiBaseUrl: "",
|
|
549
|
-
model: "",
|
|
550
|
-
port: 18088,
|
|
551
|
-
maxToolIterations: 25,
|
|
552
|
-
maxVerifyRetries: 3,
|
|
553
|
-
tokenBudget: 128e3,
|
|
554
|
-
autoIndex: true,
|
|
555
|
-
autoLearn: true,
|
|
556
|
-
userName: "Developer"
|
|
557
|
-
};
|
|
558
|
-
function ensureDir() {
|
|
559
|
-
if (!fs.existsSync(DOMINUS_DIR)) {
|
|
560
|
-
fs.mkdirSync(DOMINUS_DIR, { recursive: true });
|
|
561
|
-
}
|
|
562
|
-
}
|
|
563
|
-
function readJson(filePath, defaults) {
|
|
564
|
-
try {
|
|
565
|
-
if (fs.existsSync(filePath)) {
|
|
566
|
-
const raw = fs.readFileSync(filePath, "utf-8");
|
|
567
|
-
return { ...defaults, ...JSON.parse(raw) };
|
|
568
|
-
}
|
|
569
|
-
} catch {
|
|
570
|
-
}
|
|
571
|
-
return { ...defaults };
|
|
572
|
-
}
|
|
573
|
-
function loadConfig() {
|
|
574
|
-
ensureDir();
|
|
575
|
-
return readJson(CONFIG_PATH, DEFAULTS);
|
|
576
|
-
}
|
|
577
|
-
function getDbPath() {
|
|
578
|
-
ensureDir();
|
|
579
|
-
return DB_PATH;
|
|
580
|
-
}
|
|
581
|
-
|
|
582
|
-
// src/memory/store.ts
|
|
583
|
-
var db = null;
|
|
584
|
-
var dbPath;
|
|
585
|
-
var SCHEMA = `
|
|
586
|
-
CREATE TABLE IF NOT EXISTS facts (
|
|
587
|
-
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
588
|
-
project_id TEXT NOT NULL,
|
|
589
|
-
content TEXT NOT NULL,
|
|
590
|
-
category TEXT NOT NULL,
|
|
591
|
-
source TEXT,
|
|
592
|
-
relevance REAL DEFAULT 1.0,
|
|
593
|
-
created_at INTEGER NOT NULL,
|
|
594
|
-
accessed_at INTEGER NOT NULL
|
|
595
|
-
);
|
|
596
|
-
|
|
597
|
-
CREATE TABLE IF NOT EXISTS summaries (
|
|
598
|
-
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
599
|
-
project_id TEXT NOT NULL,
|
|
600
|
-
session_id TEXT NOT NULL,
|
|
601
|
-
summary TEXT NOT NULL,
|
|
602
|
-
created_at INTEGER NOT NULL
|
|
603
|
-
);
|
|
604
|
-
|
|
605
|
-
CREATE TABLE IF NOT EXISTS messages (
|
|
606
|
-
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
607
|
-
session_id TEXT NOT NULL,
|
|
608
|
-
role TEXT NOT NULL,
|
|
609
|
-
content TEXT NOT NULL,
|
|
610
|
-
tool_name TEXT,
|
|
611
|
-
created_at INTEGER NOT NULL
|
|
612
|
-
);
|
|
613
|
-
|
|
614
|
-
CREATE TABLE IF NOT EXISTS script_index (
|
|
615
|
-
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
616
|
-
project_id TEXT NOT NULL,
|
|
617
|
-
path TEXT NOT NULL,
|
|
618
|
-
class_name TEXT NOT NULL,
|
|
619
|
-
summary TEXT,
|
|
620
|
-
line_count INTEGER,
|
|
621
|
-
last_hash TEXT,
|
|
622
|
-
updated_at INTEGER NOT NULL
|
|
623
|
-
);
|
|
624
|
-
|
|
625
|
-
CREATE INDEX IF NOT EXISTS idx_facts_project ON facts(project_id);
|
|
626
|
-
CREATE INDEX IF NOT EXISTS idx_facts_category ON facts(project_id, category);
|
|
627
|
-
CREATE INDEX IF NOT EXISTS idx_scripts_project ON script_index(project_id);
|
|
628
|
-
CREATE INDEX IF NOT EXISTS idx_messages_session ON messages(session_id);
|
|
629
|
-
CREATE INDEX IF NOT EXISTS idx_summaries_project ON summaries(project_id);
|
|
630
|
-
`;
|
|
631
|
-
function persist() {
|
|
632
|
-
if (!db) return;
|
|
633
|
-
const data = db.export();
|
|
634
|
-
fs.writeFileSync(dbPath, Buffer.from(data));
|
|
635
|
-
}
|
|
636
|
-
async function initMemoryStore() {
|
|
637
|
-
if (db) return;
|
|
638
|
-
dbPath = getDbPath();
|
|
639
|
-
const SQL = await initSqlJs();
|
|
640
|
-
if (fs.existsSync(dbPath)) {
|
|
641
|
-
const buffer = fs.readFileSync(dbPath);
|
|
642
|
-
db = new SQL.Database(buffer);
|
|
643
|
-
} else {
|
|
644
|
-
db = new SQL.Database();
|
|
645
|
-
}
|
|
646
|
-
db.run(SCHEMA);
|
|
647
|
-
persist();
|
|
648
|
-
}
|
|
649
|
-
function getDb() {
|
|
650
|
-
if (!db) throw new Error("Memory store not initialized. Call initMemoryStore() first.");
|
|
651
|
-
return db;
|
|
652
|
-
}
|
|
653
|
-
function storeFact(fact) {
|
|
654
|
-
const d = getDb();
|
|
655
|
-
d.run(
|
|
656
|
-
`INSERT INTO facts (project_id, content, category, source, relevance, created_at, accessed_at)
|
|
657
|
-
VALUES (?, ?, ?, ?, ?, ?, ?)`,
|
|
658
|
-
[
|
|
659
|
-
fact.projectId,
|
|
660
|
-
fact.content,
|
|
661
|
-
fact.category,
|
|
662
|
-
fact.source ?? null,
|
|
663
|
-
fact.relevance,
|
|
664
|
-
fact.createdAt,
|
|
665
|
-
fact.accessedAt
|
|
666
|
-
]
|
|
667
|
-
);
|
|
668
|
-
persist();
|
|
669
|
-
const stmt = d.prepare("SELECT last_insert_rowid()");
|
|
670
|
-
stmt.step();
|
|
671
|
-
const row = stmt.get();
|
|
672
|
-
stmt.free();
|
|
673
|
-
return row?.[0] ?? 0;
|
|
674
|
-
}
|
|
675
|
-
function recallFacts(projectId, query, limit = 10) {
|
|
676
|
-
const d = getDb();
|
|
677
|
-
const keywords = query.toLowerCase().split(/\s+/).filter((w) => w.length > 2);
|
|
678
|
-
if (keywords.length === 0) {
|
|
679
|
-
const result2 = d.exec(
|
|
680
|
-
`SELECT * FROM facts WHERE project_id = ? ORDER BY relevance DESC, accessed_at DESC LIMIT ?`,
|
|
681
|
-
[projectId, limit]
|
|
682
|
-
);
|
|
683
|
-
return rowsToFacts(result2);
|
|
684
|
-
}
|
|
685
|
-
const likeClauses = keywords.map(() => `LOWER(content) LIKE ?`).join(" OR ");
|
|
686
|
-
const likeParams = keywords.map((k) => `%${k}%`);
|
|
687
|
-
const result = d.exec(
|
|
688
|
-
`SELECT * FROM facts WHERE project_id = ? AND (${likeClauses})
|
|
689
|
-
ORDER BY relevance DESC, accessed_at DESC LIMIT ?`,
|
|
690
|
-
[projectId, ...likeParams, limit]
|
|
691
|
-
);
|
|
692
|
-
const facts = rowsToFacts(result);
|
|
693
|
-
for (const fact of facts) {
|
|
694
|
-
if (fact.id) {
|
|
695
|
-
d.run(`UPDATE facts SET accessed_at = ? WHERE id = ?`, [Date.now(), fact.id]);
|
|
696
|
-
}
|
|
697
|
-
}
|
|
698
|
-
if (facts.length > 0) persist();
|
|
699
|
-
return facts;
|
|
700
|
-
}
|
|
701
|
-
function rowsToFacts(result) {
|
|
702
|
-
if (!result[0]) return [];
|
|
703
|
-
return result[0].values.map((row) => ({
|
|
704
|
-
id: row[0],
|
|
705
|
-
projectId: row[1],
|
|
706
|
-
content: row[2],
|
|
707
|
-
category: row[3],
|
|
708
|
-
source: row[4],
|
|
709
|
-
relevance: row[5],
|
|
710
|
-
createdAt: row[6],
|
|
711
|
-
accessedAt: row[7]
|
|
712
|
-
}));
|
|
713
|
-
}
|
|
714
|
-
function getScriptIndex(projectId) {
|
|
715
|
-
const d = getDb();
|
|
716
|
-
const result = d.exec(
|
|
717
|
-
`SELECT path, summary, class_name FROM script_index WHERE project_id = ? ORDER BY path`,
|
|
718
|
-
[projectId]
|
|
719
|
-
);
|
|
720
|
-
if (!result[0]) return [];
|
|
721
|
-
return result[0].values.map((row) => ({
|
|
722
|
-
path: row[0],
|
|
723
|
-
summary: row[1] || void 0,
|
|
724
|
-
className: row[2]
|
|
725
|
-
}));
|
|
3035
|
+
resolve(true);
|
|
3036
|
+
});
|
|
3037
|
+
ws.on("error", () => {
|
|
3038
|
+
clearTimeout(timer);
|
|
3039
|
+
resolve(false);
|
|
3040
|
+
});
|
|
3041
|
+
});
|
|
726
3042
|
}
|
|
727
3043
|
|
|
3044
|
+
// src/mcp/server.ts
|
|
3045
|
+
init_protocol();
|
|
3046
|
+
init_store();
|
|
3047
|
+
init_config();
|
|
3048
|
+
|
|
728
3049
|
// src/agent/internal-memory.ts
|
|
729
3050
|
var INTERNAL_MEMORY = `## Internal Memory \u2014 Known Quirks & Lessons
|
|
730
3051
|
|
|
@@ -786,6 +3107,8 @@ These are built-in lessons that ship with Dominus. Follow them exactly.
|
|
|
786
3107
|
- Converting UI to Roact/React \u2192 use \`convert_ui_to_code\`
|
|
787
3108
|
- Creating instances \u2192 use \`create_part\`, \`create_ui\`, \`insert_instance\`, \`build_multiple\`
|
|
788
3109
|
- Modifying properties \u2192 use \`set_properties\` or \`bulk_set_properties\`
|
|
3110
|
+
- Bulk font/style/scaling changes \u2192 use \`bulk_set_properties\` with filter mode (root + className + properties)
|
|
3111
|
+
- Adding UIStroke/UICorner to many instances \u2192 use \`bulk_set_properties\` with \`addChildren\` (idempotent)
|
|
789
3112
|
- Reading scripts \u2192 use \`read_script\`
|
|
790
3113
|
- Browsing the explorer \u2192 use \`get_explorer\`
|
|
791
3114
|
\`run_code\` is ONLY for procedural logic, terrain generation, raycasts, physics queries, or custom algorithms with no tool equivalent.
|
|
@@ -795,6 +3118,19 @@ These are built-in lessons that ship with Dominus. Follow them exactly.
|
|
|
795
3118
|
- \`inspect_instance\` \u2014 Returns compact properties + children tree for a single instance in ONE call. Combines get_properties + get_explorer.
|
|
796
3119
|
- \`convert_ui_to_code\` \u2014 Serializes a UI tree and generates a Roact/React component. Use when user says "turn this into a component" or "convert to React".
|
|
797
3120
|
|
|
3121
|
+
### UI Pre-Flight \u2014 ASK BEFORE BUILDING
|
|
3122
|
+
Before creating any non-trivial UI (more than a single element), **ask the user these questions** to avoid rework:
|
|
3123
|
+
- **A) Device scaling** \u2014 "Should this UI scale for mobile/tablet, or is it PC-only?"
|
|
3124
|
+
- Mobile: use Scale-based sizing, UIAspectRatioConstraint, TextScaled or AutomaticSize, avoid large pixel offsets.
|
|
3125
|
+
- PC-only: pixel offsets are fine.
|
|
3126
|
+
- **B) ScreenGui structure** \u2014 "All panels in one ScreenGui, or separate ScreenGuis per panel?"
|
|
3127
|
+
- One: simpler, easy to toggle all at once.
|
|
3128
|
+
- Separate: independent ZIndex, modular show/hide.
|
|
3129
|
+
- **C) Font & style** \u2014 "Any preferred font, color scheme, or style reference?"
|
|
3130
|
+
- Default to Roboto if user has no preference. Use FontFace with Weight=Bold for bold.
|
|
3131
|
+
- Match screenshots closely (transparency, stroke, spacing).
|
|
3132
|
+
- Skip these only if the user already specified preferences or the task is trivially small.
|
|
3133
|
+
|
|
798
3134
|
### Verification habits
|
|
799
3135
|
- After a build, call \`get_explorer\` or \`get_selection\` to confirm the tree looks right.
|
|
800
3136
|
- After editing a script, read it back.
|
|
@@ -818,6 +3154,1059 @@ These are built-in lessons that ship with Dominus. Follow them exactly.
|
|
|
818
3154
|
- Use instance Name as dictionary key in children table
|
|
819
3155
|
`;
|
|
820
3156
|
|
|
3157
|
+
// src/mcp/server.ts
|
|
3158
|
+
init_roblox_api();
|
|
3159
|
+
|
|
3160
|
+
// src/ai/provider.ts
|
|
3161
|
+
init_config();
|
|
3162
|
+
var AIProvider = class {
|
|
3163
|
+
client;
|
|
3164
|
+
model;
|
|
3165
|
+
providerName;
|
|
3166
|
+
constructor(config) {
|
|
3167
|
+
if (!config.apiKey) {
|
|
3168
|
+
throw new Error("API key not configured. Run: dominus config set apiKey <your-key>");
|
|
3169
|
+
}
|
|
3170
|
+
const { baseUrl, model } = resolveProviderSettings(config);
|
|
3171
|
+
this.providerName = PROVIDERS[config.provider]?.name ?? config.provider;
|
|
3172
|
+
this.client = new OpenAI({
|
|
3173
|
+
apiKey: config.apiKey,
|
|
3174
|
+
baseURL: baseUrl
|
|
3175
|
+
});
|
|
3176
|
+
this.model = model;
|
|
3177
|
+
}
|
|
3178
|
+
setModel(model) {
|
|
3179
|
+
this.model = model;
|
|
3180
|
+
}
|
|
3181
|
+
getModel() {
|
|
3182
|
+
return this.model;
|
|
3183
|
+
}
|
|
3184
|
+
getProviderName() {
|
|
3185
|
+
return this.providerName;
|
|
3186
|
+
}
|
|
3187
|
+
async listModels() {
|
|
3188
|
+
try {
|
|
3189
|
+
const list = await this.client.models.list();
|
|
3190
|
+
const models = [];
|
|
3191
|
+
for await (const m of list) {
|
|
3192
|
+
models.push({ id: m.id, owned_by: m.owned_by });
|
|
3193
|
+
}
|
|
3194
|
+
models.sort((a, b) => a.id.localeCompare(b.id));
|
|
3195
|
+
return models;
|
|
3196
|
+
} catch {
|
|
3197
|
+
return [];
|
|
3198
|
+
}
|
|
3199
|
+
}
|
|
3200
|
+
async chat(messages, tools) {
|
|
3201
|
+
const openAITools = tools?.map(toolToOpenAI);
|
|
3202
|
+
const response = await this.client.chat.completions.create({
|
|
3203
|
+
model: this.model,
|
|
3204
|
+
messages,
|
|
3205
|
+
tools: openAITools?.length ? openAITools : void 0,
|
|
3206
|
+
temperature: 0.3
|
|
3207
|
+
});
|
|
3208
|
+
const choice = response.choices[0];
|
|
3209
|
+
if (!choice) throw new Error("No response from AI");
|
|
3210
|
+
const toolCalls = (choice.message.tool_calls ?? []).map((tc) => ({
|
|
3211
|
+
id: tc.id,
|
|
3212
|
+
name: tc.function.name,
|
|
3213
|
+
arguments: JSON.parse(tc.function.arguments)
|
|
3214
|
+
}));
|
|
3215
|
+
return {
|
|
3216
|
+
content: choice.message.content,
|
|
3217
|
+
toolCalls,
|
|
3218
|
+
finishReason: choice.finish_reason ?? "stop",
|
|
3219
|
+
usage: response.usage ? {
|
|
3220
|
+
promptTokens: response.usage.prompt_tokens,
|
|
3221
|
+
completionTokens: response.usage.completion_tokens,
|
|
3222
|
+
totalTokens: response.usage.total_tokens
|
|
3223
|
+
} : void 0
|
|
3224
|
+
};
|
|
3225
|
+
}
|
|
3226
|
+
async *streamChat(messages, tools) {
|
|
3227
|
+
const openAITools = tools?.map(toolToOpenAI);
|
|
3228
|
+
const stream = await this.client.chat.completions.create({
|
|
3229
|
+
model: this.model,
|
|
3230
|
+
messages,
|
|
3231
|
+
tools: openAITools?.length ? openAITools : void 0,
|
|
3232
|
+
temperature: 0.3,
|
|
3233
|
+
stream: true
|
|
3234
|
+
});
|
|
3235
|
+
const toolCallBuffers = /* @__PURE__ */ new Map();
|
|
3236
|
+
for await (const chunk of stream) {
|
|
3237
|
+
const delta = chunk.choices[0]?.delta;
|
|
3238
|
+
if (!delta) continue;
|
|
3239
|
+
if (delta.content) {
|
|
3240
|
+
yield { type: "text", content: delta.content };
|
|
3241
|
+
}
|
|
3242
|
+
if (delta.tool_calls) {
|
|
3243
|
+
for (const tc of delta.tool_calls) {
|
|
3244
|
+
if (!toolCallBuffers.has(tc.index)) {
|
|
3245
|
+
toolCallBuffers.set(tc.index, {
|
|
3246
|
+
id: tc.id ?? "",
|
|
3247
|
+
name: tc.function?.name ?? "",
|
|
3248
|
+
args: ""
|
|
3249
|
+
});
|
|
3250
|
+
if (tc.function?.name) {
|
|
3251
|
+
yield {
|
|
3252
|
+
type: "tool_call_start",
|
|
3253
|
+
toolCall: { id: tc.id, name: tc.function.name }
|
|
3254
|
+
};
|
|
3255
|
+
}
|
|
3256
|
+
}
|
|
3257
|
+
const buffer = toolCallBuffers.get(tc.index);
|
|
3258
|
+
if (tc.id) buffer.id = tc.id;
|
|
3259
|
+
if (tc.function?.name) buffer.name = tc.function.name;
|
|
3260
|
+
if (tc.function?.arguments) {
|
|
3261
|
+
buffer.args += tc.function.arguments;
|
|
3262
|
+
yield {
|
|
3263
|
+
type: "tool_call_delta",
|
|
3264
|
+
content: tc.function.arguments,
|
|
3265
|
+
toolCall: { id: buffer.id, name: buffer.name }
|
|
3266
|
+
};
|
|
3267
|
+
}
|
|
3268
|
+
}
|
|
3269
|
+
}
|
|
3270
|
+
if (chunk.choices[0]?.finish_reason === "tool_calls") {
|
|
3271
|
+
for (const [, buffer] of toolCallBuffers) {
|
|
3272
|
+
let args = {};
|
|
3273
|
+
try {
|
|
3274
|
+
args = JSON.parse(buffer.args);
|
|
3275
|
+
} catch {
|
|
3276
|
+
}
|
|
3277
|
+
yield {
|
|
3278
|
+
type: "tool_call_end",
|
|
3279
|
+
toolCall: { id: buffer.id, name: buffer.name, arguments: args }
|
|
3280
|
+
};
|
|
3281
|
+
}
|
|
3282
|
+
toolCallBuffers.clear();
|
|
3283
|
+
}
|
|
3284
|
+
}
|
|
3285
|
+
yield { type: "done" };
|
|
3286
|
+
}
|
|
3287
|
+
};
|
|
3288
|
+
function toolToOpenAI(tool31) {
|
|
3289
|
+
return {
|
|
3290
|
+
type: "function",
|
|
3291
|
+
function: {
|
|
3292
|
+
name: tool31.name,
|
|
3293
|
+
description: tool31.description,
|
|
3294
|
+
parameters: tool31.parameters
|
|
3295
|
+
}
|
|
3296
|
+
};
|
|
3297
|
+
}
|
|
3298
|
+
|
|
3299
|
+
// src/agent/verifier.ts
|
|
3300
|
+
init_protocol();
|
|
3301
|
+
async function verifyAction(check, ctx) {
|
|
3302
|
+
switch (check.type) {
|
|
3303
|
+
case "script_edit":
|
|
3304
|
+
return verifyScriptEdit(check.path, check.expectedContent, ctx);
|
|
3305
|
+
case "code_run":
|
|
3306
|
+
return verifyCodeRun(ctx);
|
|
3307
|
+
case "instance_create":
|
|
3308
|
+
return verifyInstanceExists(check.path, ctx);
|
|
3309
|
+
case "property_set":
|
|
3310
|
+
return verifyPropertySet(check.path, check.expectedProperties ?? {}, ctx);
|
|
3311
|
+
default:
|
|
3312
|
+
return { success: true };
|
|
3313
|
+
}
|
|
3314
|
+
}
|
|
3315
|
+
function inferVerifyChecks(call, result) {
|
|
3316
|
+
if (!result.success) return [];
|
|
3317
|
+
switch (call.name) {
|
|
3318
|
+
case "edit_script":
|
|
3319
|
+
return [
|
|
3320
|
+
{
|
|
3321
|
+
type: "script_edit",
|
|
3322
|
+
path: typeof call.arguments.path === "string" ? call.arguments.path : void 0,
|
|
3323
|
+
expectedContent: typeof call.arguments.source === "string" ? call.arguments.source : void 0
|
|
3324
|
+
}
|
|
3325
|
+
];
|
|
3326
|
+
case "run_code":
|
|
3327
|
+
case "execute_run_test":
|
|
3328
|
+
case "execute_play_test":
|
|
3329
|
+
return [{ type: "code_run" }];
|
|
3330
|
+
case "create_ui":
|
|
3331
|
+
case "create_part":
|
|
3332
|
+
case "clone_instance":
|
|
3333
|
+
case "insert_instance":
|
|
3334
|
+
case "group_instances": {
|
|
3335
|
+
const createdPath = extractPath(result.data) ?? extractStringArg(call.arguments, "path");
|
|
3336
|
+
return createdPath ? [{ type: "instance_create", path: createdPath }] : [];
|
|
3337
|
+
}
|
|
3338
|
+
case "set_properties":
|
|
3339
|
+
return [
|
|
3340
|
+
{
|
|
3341
|
+
type: "property_set",
|
|
3342
|
+
path: extractStringArg(call.arguments, "path"),
|
|
3343
|
+
expectedProperties: asRecord(call.arguments.properties)
|
|
3344
|
+
}
|
|
3345
|
+
];
|
|
3346
|
+
case "bulk_set_properties": {
|
|
3347
|
+
const path4 = extractPath(result.data) ?? extractStringArg(call.arguments, "path");
|
|
3348
|
+
const properties = asRecord(call.arguments.properties);
|
|
3349
|
+
return path4 && properties ? [{ type: "property_set", path: path4, expectedProperties: properties }] : [];
|
|
3350
|
+
}
|
|
3351
|
+
default:
|
|
3352
|
+
return [];
|
|
3353
|
+
}
|
|
3354
|
+
}
|
|
3355
|
+
async function verifyScriptEdit(scriptPath, expectedContent, ctx) {
|
|
3356
|
+
if (!ctx.isStudioConnected()) {
|
|
3357
|
+
return { success: false, error: "Studio disconnected during verification" };
|
|
3358
|
+
}
|
|
3359
|
+
try {
|
|
3360
|
+
const response = await ctx.sendToStudio(CliMsg.GET_SCRIPT, {
|
|
3361
|
+
path: scriptPath
|
|
3362
|
+
});
|
|
3363
|
+
if (!response.payload?.source) {
|
|
3364
|
+
return { success: false, error: `Could not read back script: ${scriptPath}` };
|
|
3365
|
+
}
|
|
3366
|
+
if (expectedContent && response.payload.source !== expectedContent) {
|
|
3367
|
+
return {
|
|
3368
|
+
success: false,
|
|
3369
|
+
error: "Script content does not match expected output",
|
|
3370
|
+
data: {
|
|
3371
|
+
expected: expectedContent.slice(0, 200),
|
|
3372
|
+
actual: response.payload.source.slice(0, 200)
|
|
3373
|
+
}
|
|
3374
|
+
};
|
|
3375
|
+
}
|
|
3376
|
+
return { success: true, data: { verified: scriptPath } };
|
|
3377
|
+
} catch (err) {
|
|
3378
|
+
return {
|
|
3379
|
+
success: false,
|
|
3380
|
+
error: `Verification failed: ${err instanceof Error ? err.message : String(err)}`
|
|
3381
|
+
};
|
|
3382
|
+
}
|
|
3383
|
+
}
|
|
3384
|
+
async function verifyCodeRun(ctx) {
|
|
3385
|
+
if (!ctx.isStudioConnected()) {
|
|
3386
|
+
return { success: false, error: "Studio disconnected during verification" };
|
|
3387
|
+
}
|
|
3388
|
+
try {
|
|
3389
|
+
const response = await ctx.sendToStudio(
|
|
3390
|
+
CliMsg.GET_OUTPUT,
|
|
3391
|
+
{ limit: 5, level: "error" }
|
|
3392
|
+
);
|
|
3393
|
+
const errors = response.payload?.entries?.filter((e) => e.level === "error") ?? [];
|
|
3394
|
+
if (errors.length > 0) {
|
|
3395
|
+
return {
|
|
3396
|
+
success: false,
|
|
3397
|
+
error: "Errors detected after code execution",
|
|
3398
|
+
data: { errors: errors.map((e) => e.message) }
|
|
3399
|
+
};
|
|
3400
|
+
}
|
|
3401
|
+
return { success: true };
|
|
3402
|
+
} catch {
|
|
3403
|
+
return { success: true };
|
|
3404
|
+
}
|
|
3405
|
+
}
|
|
3406
|
+
async function verifyInstanceExists(instancePath, ctx) {
|
|
3407
|
+
if (!ctx.isStudioConnected()) {
|
|
3408
|
+
return { success: false, error: "Studio disconnected during verification" };
|
|
3409
|
+
}
|
|
3410
|
+
try {
|
|
3411
|
+
const response = await ctx.sendToStudio(CliMsg.RUN_CODE, {
|
|
3412
|
+
code: buildInstanceExistsCode(instancePath)
|
|
3413
|
+
});
|
|
3414
|
+
if (response.payload.error) {
|
|
3415
|
+
return { success: false, error: `Instance verification failed: ${response.payload.error}` };
|
|
3416
|
+
}
|
|
3417
|
+
const exists = String(response.payload.output ?? "").trim().toLowerCase();
|
|
3418
|
+
if (!exists.includes("true")) {
|
|
3419
|
+
return { success: false, error: `Instance not found after creation: ${instancePath}` };
|
|
3420
|
+
}
|
|
3421
|
+
return { success: true, data: { exists: instancePath } };
|
|
3422
|
+
} catch {
|
|
3423
|
+
return { success: true };
|
|
3424
|
+
}
|
|
3425
|
+
}
|
|
3426
|
+
async function verifyPropertySet(instancePath, expectedProperties, ctx) {
|
|
3427
|
+
if (!ctx.isStudioConnected()) {
|
|
3428
|
+
return { success: false, error: "Studio disconnected during verification" };
|
|
3429
|
+
}
|
|
3430
|
+
if (!instancePath || Object.keys(expectedProperties).length === 0) {
|
|
3431
|
+
return { success: true, data: { verified: instancePath } };
|
|
3432
|
+
}
|
|
3433
|
+
try {
|
|
3434
|
+
const response = await ctx.sendToStudio(CliMsg.GET_PROPERTIES, {
|
|
3435
|
+
path: instancePath,
|
|
3436
|
+
compact: false
|
|
3437
|
+
});
|
|
3438
|
+
const entries = response.payload?.properties ?? [];
|
|
3439
|
+
const byName = new Map(entries.map((entry) => [entry.name, entry.value]));
|
|
3440
|
+
const mismatches = [];
|
|
3441
|
+
for (const [name, expected] of Object.entries(expectedProperties)) {
|
|
3442
|
+
const actual = byName.get(name);
|
|
3443
|
+
if (actual == null) {
|
|
3444
|
+
mismatches.push(`${name}: missing`);
|
|
3445
|
+
continue;
|
|
3446
|
+
}
|
|
3447
|
+
if (!matchesPropertyValue(actual, expected)) {
|
|
3448
|
+
mismatches.push(`${name}: expected ${stringifyExpected(expected)}, got ${actual}`);
|
|
3449
|
+
}
|
|
3450
|
+
}
|
|
3451
|
+
if (mismatches.length > 0) {
|
|
3452
|
+
return {
|
|
3453
|
+
success: false,
|
|
3454
|
+
error: "Property verification failed",
|
|
3455
|
+
data: { path: instancePath, mismatches }
|
|
3456
|
+
};
|
|
3457
|
+
}
|
|
3458
|
+
return {
|
|
3459
|
+
success: true,
|
|
3460
|
+
data: { verified: instancePath, properties: Object.keys(expectedProperties) }
|
|
3461
|
+
};
|
|
3462
|
+
} catch (err) {
|
|
3463
|
+
return {
|
|
3464
|
+
success: false,
|
|
3465
|
+
error: `Property verification failed: ${err instanceof Error ? err.message : String(err)}`
|
|
3466
|
+
};
|
|
3467
|
+
}
|
|
3468
|
+
}
|
|
3469
|
+
function extractPath(data) {
|
|
3470
|
+
if (!data || typeof data !== "object") return void 0;
|
|
3471
|
+
const candidate = data.path;
|
|
3472
|
+
return typeof candidate === "string" ? candidate : void 0;
|
|
3473
|
+
}
|
|
3474
|
+
function extractStringArg(args, key) {
|
|
3475
|
+
const value = args[key];
|
|
3476
|
+
return typeof value === "string" ? value : void 0;
|
|
3477
|
+
}
|
|
3478
|
+
function asRecord(value) {
|
|
3479
|
+
return value && typeof value === "object" && !Array.isArray(value) ? value : void 0;
|
|
3480
|
+
}
|
|
3481
|
+
function buildInstanceExistsCode(instancePath) {
|
|
3482
|
+
const parts = instancePath.split(".").filter(Boolean);
|
|
3483
|
+
const lines = ["local node = game"];
|
|
3484
|
+
for (const part of parts) {
|
|
3485
|
+
lines.push(`node = node and node:FindFirstChild(${luaString(part)})`);
|
|
3486
|
+
}
|
|
3487
|
+
lines.push("return node ~= nil");
|
|
3488
|
+
return lines.join("\n");
|
|
3489
|
+
}
|
|
3490
|
+
function luaString(value) {
|
|
3491
|
+
return `"${value.replace(/\\/g, "\\\\").replace(/"/g, '\\"')}"`;
|
|
3492
|
+
}
|
|
3493
|
+
function matchesPropertyValue(actual, expected) {
|
|
3494
|
+
const normalizedActual = normalizeValue(actual);
|
|
3495
|
+
if (typeof expected === "boolean" || typeof expected === "number") {
|
|
3496
|
+
return normalizedActual === normalizeValue(String(expected));
|
|
3497
|
+
}
|
|
3498
|
+
if (typeof expected === "string") {
|
|
3499
|
+
const normalizedExpected = normalizeValue(expected);
|
|
3500
|
+
return normalizedActual === normalizedExpected || normalizedActual.includes(normalizedExpected);
|
|
3501
|
+
}
|
|
3502
|
+
if (Array.isArray(expected)) {
|
|
3503
|
+
const normalizedExpected = normalizeValue(expected.join(","));
|
|
3504
|
+
return normalizedActual.includes(normalizedExpected);
|
|
3505
|
+
}
|
|
3506
|
+
if (expected && typeof expected === "object") {
|
|
3507
|
+
const compactExpected = normalizeValue(JSON.stringify(expected));
|
|
3508
|
+
return normalizedActual.includes(compactExpected);
|
|
3509
|
+
}
|
|
3510
|
+
return true;
|
|
3511
|
+
}
|
|
3512
|
+
function normalizeValue(value) {
|
|
3513
|
+
return value.replace(/[\s{}\[\]"]/g, "").toLowerCase();
|
|
3514
|
+
}
|
|
3515
|
+
function stringifyExpected(value) {
|
|
3516
|
+
return typeof value === "string" ? value : JSON.stringify(value);
|
|
3517
|
+
}
|
|
3518
|
+
|
|
3519
|
+
// src/agent/json-response.ts
|
|
3520
|
+
function parseJsonResponse(content) {
|
|
3521
|
+
if (!content) return null;
|
|
3522
|
+
const trimmed = content.trim();
|
|
3523
|
+
const candidates = [
|
|
3524
|
+
trimmed,
|
|
3525
|
+
...extractMarkdownJsonBlocks(trimmed),
|
|
3526
|
+
...extractBalancedJsonBlocks(trimmed)
|
|
3527
|
+
];
|
|
3528
|
+
for (const candidate of candidates) {
|
|
3529
|
+
try {
|
|
3530
|
+
return JSON.parse(candidate);
|
|
3531
|
+
} catch {
|
|
3532
|
+
}
|
|
3533
|
+
}
|
|
3534
|
+
return null;
|
|
3535
|
+
}
|
|
3536
|
+
function extractMarkdownJsonBlocks(content) {
|
|
3537
|
+
const matches = content.match(/```(?:json)?\s*([\s\S]*?)```/gi) ?? [];
|
|
3538
|
+
return matches.map((block) => block.replace(/```(?:json)?/i, "").replace(/```$/, "").trim()).filter(Boolean);
|
|
3539
|
+
}
|
|
3540
|
+
function extractBalancedJsonBlocks(content) {
|
|
3541
|
+
const blocks = [];
|
|
3542
|
+
const openers = ["{", "["];
|
|
3543
|
+
for (let start = 0; start < content.length; start++) {
|
|
3544
|
+
if (!openers.includes(content[start])) continue;
|
|
3545
|
+
let depth = 0;
|
|
3546
|
+
let inString = false;
|
|
3547
|
+
let escapeNext = false;
|
|
3548
|
+
for (let end = start; end < content.length; end++) {
|
|
3549
|
+
const ch = content[end];
|
|
3550
|
+
if (escapeNext) {
|
|
3551
|
+
escapeNext = false;
|
|
3552
|
+
continue;
|
|
3553
|
+
}
|
|
3554
|
+
if (ch === "\\") {
|
|
3555
|
+
escapeNext = true;
|
|
3556
|
+
continue;
|
|
3557
|
+
}
|
|
3558
|
+
if (ch === '"') {
|
|
3559
|
+
inString = !inString;
|
|
3560
|
+
continue;
|
|
3561
|
+
}
|
|
3562
|
+
if (inString) continue;
|
|
3563
|
+
if (ch === "{" || ch === "[") depth++;
|
|
3564
|
+
if (ch === "}" || ch === "]") depth--;
|
|
3565
|
+
if (depth === 0) {
|
|
3566
|
+
blocks.push(content.slice(start, end + 1).trim());
|
|
3567
|
+
break;
|
|
3568
|
+
}
|
|
3569
|
+
}
|
|
3570
|
+
}
|
|
3571
|
+
return blocks;
|
|
3572
|
+
}
|
|
3573
|
+
|
|
3574
|
+
// src/agent/multi-agent/conflicts.ts
|
|
3575
|
+
var READ_ONLY_TOOL_NAMES = /* @__PURE__ */ new Set([
|
|
3576
|
+
"get_explorer",
|
|
3577
|
+
"get_properties",
|
|
3578
|
+
"get_descendants_properties",
|
|
3579
|
+
"get_selection",
|
|
3580
|
+
"search_scripts",
|
|
3581
|
+
"read_script",
|
|
3582
|
+
"get_output",
|
|
3583
|
+
"get_class_info",
|
|
3584
|
+
"serialize_ui",
|
|
3585
|
+
"recall_memory",
|
|
3586
|
+
"search_roblox_api",
|
|
3587
|
+
"get_roblox_api_reference"
|
|
3588
|
+
]);
|
|
3589
|
+
var COORDINATOR_WRITE_TOOL_NAMES = /* @__PURE__ */ new Set([
|
|
3590
|
+
"create_ui",
|
|
3591
|
+
"set_properties",
|
|
3592
|
+
"bulk_set_properties",
|
|
3593
|
+
"insert_instance",
|
|
3594
|
+
"move_instance"
|
|
3595
|
+
]);
|
|
3596
|
+
var FORBIDDEN_WORKER_WRITE_TOOL_NAMES = /* @__PURE__ */ new Set([
|
|
3597
|
+
"run_code",
|
|
3598
|
+
"edit_script",
|
|
3599
|
+
"delete_instance",
|
|
3600
|
+
"create_part",
|
|
3601
|
+
"build_multiple",
|
|
3602
|
+
"clone_instance",
|
|
3603
|
+
"group_instances",
|
|
3604
|
+
"find_replace_scripts",
|
|
3605
|
+
"undo",
|
|
3606
|
+
"redo"
|
|
3607
|
+
]);
|
|
3608
|
+
function normalizeScopePath(pathValue) {
|
|
3609
|
+
return pathValue.trim().replace(/\[(?:"|')([^"']+)(?:"|')\]/g, ".$1").replace(/^game\./i, "").replace(/\.+/g, ".").replace(/^\./, "").replace(/\.$/, "");
|
|
3610
|
+
}
|
|
3611
|
+
function pathsOverlap(a, b) {
|
|
3612
|
+
const left = normalizeScopePath(a).toLowerCase();
|
|
3613
|
+
const right = normalizeScopePath(b).toLowerCase();
|
|
3614
|
+
if (!left || !right) return false;
|
|
3615
|
+
return left === right || left.startsWith(`${right}.`) || right.startsWith(`${left}.`);
|
|
3616
|
+
}
|
|
3617
|
+
function isPathWithinScopes(pathValue, scopes) {
|
|
3618
|
+
const target = normalizeScopePath(pathValue).toLowerCase();
|
|
3619
|
+
if (!target) return scopes.length === 0;
|
|
3620
|
+
return scopes.some((scope) => {
|
|
3621
|
+
const normalizedScope = normalizeScopePath(scope).toLowerCase();
|
|
3622
|
+
return target === normalizedScope || target.startsWith(`${normalizedScope}.`);
|
|
3623
|
+
});
|
|
3624
|
+
}
|
|
3625
|
+
function inferTargetPath(call) {
|
|
3626
|
+
if (call.targetPath) return call.targetPath;
|
|
3627
|
+
const args = call.args ?? {};
|
|
3628
|
+
for (const key of ["path", "root", "rootPath", "parent", "parentPath"]) {
|
|
3629
|
+
const value = args[key];
|
|
3630
|
+
if (typeof value === "string" && value.trim()) return value;
|
|
3631
|
+
}
|
|
3632
|
+
const targets = args.targets;
|
|
3633
|
+
if (Array.isArray(targets)) {
|
|
3634
|
+
const first = targets.find(
|
|
3635
|
+
(target) => target && typeof target === "object" && typeof target.path === "string"
|
|
3636
|
+
);
|
|
3637
|
+
if (first?.path) return first.path;
|
|
3638
|
+
}
|
|
3639
|
+
return void 0;
|
|
3640
|
+
}
|
|
3641
|
+
function validateWorkerProposal(assignment, proposal) {
|
|
3642
|
+
const errors = [];
|
|
3643
|
+
if (proposal.workerId !== assignment.id) {
|
|
3644
|
+
errors.push(`Proposal workerId "${proposal.workerId}" does not match assignment "${assignment.id}"`);
|
|
3645
|
+
}
|
|
3646
|
+
if (!proposal.summary?.trim()) {
|
|
3647
|
+
errors.push("Proposal summary is required");
|
|
3648
|
+
}
|
|
3649
|
+
for (const call of proposal.proposedToolCalls ?? []) {
|
|
3650
|
+
if (FORBIDDEN_WORKER_WRITE_TOOL_NAMES.has(call.tool)) {
|
|
3651
|
+
errors.push(`Worker proposed forbidden write tool "${call.tool}"`);
|
|
3652
|
+
continue;
|
|
3653
|
+
}
|
|
3654
|
+
if (!COORDINATOR_WRITE_TOOL_NAMES.has(call.tool)) {
|
|
3655
|
+
errors.push(`Worker proposed unsupported coordinator write tool "${call.tool}"`);
|
|
3656
|
+
continue;
|
|
3657
|
+
}
|
|
3658
|
+
const targetPath = inferTargetPath(call);
|
|
3659
|
+
if (!targetPath) {
|
|
3660
|
+
errors.push(`Proposed ${call.tool} call is missing a target path`);
|
|
3661
|
+
continue;
|
|
3662
|
+
}
|
|
3663
|
+
if (!isPathWithinScopes(targetPath, assignment.ownedScopes)) {
|
|
3664
|
+
errors.push(`Proposed ${call.tool} target "${targetPath}" is outside owned scopes`);
|
|
3665
|
+
}
|
|
3666
|
+
}
|
|
3667
|
+
return errors;
|
|
3668
|
+
}
|
|
3669
|
+
function rejectConflictingAcceptedProposals(accepted) {
|
|
3670
|
+
const finalAccepted = [];
|
|
3671
|
+
const rejected = [];
|
|
3672
|
+
const claimed = [];
|
|
3673
|
+
for (const item of accepted) {
|
|
3674
|
+
const conflict = findConflict(item.proposal, claimed);
|
|
3675
|
+
if (conflict) {
|
|
3676
|
+
rejected.push({
|
|
3677
|
+
workerId: item.workerId,
|
|
3678
|
+
proposal: item.proposal,
|
|
3679
|
+
reason: `Conflicts with accepted worker ${conflict.workerId} on ${conflict.path}`
|
|
3680
|
+
});
|
|
3681
|
+
continue;
|
|
3682
|
+
}
|
|
3683
|
+
finalAccepted.push(item);
|
|
3684
|
+
for (const call of item.proposal.proposedToolCalls) {
|
|
3685
|
+
const targetPath = inferTargetPath(call);
|
|
3686
|
+
if (targetPath) claimed.push({ workerId: item.workerId, path: targetPath });
|
|
3687
|
+
}
|
|
3688
|
+
}
|
|
3689
|
+
return { accepted: finalAccepted, rejected };
|
|
3690
|
+
}
|
|
3691
|
+
function findConflict(proposal, claimed) {
|
|
3692
|
+
for (const call of proposal.proposedToolCalls) {
|
|
3693
|
+
const targetPath = inferTargetPath(call);
|
|
3694
|
+
if (!targetPath) continue;
|
|
3695
|
+
for (const existing of claimed) {
|
|
3696
|
+
if (pathsOverlap(targetPath, existing.path)) return existing;
|
|
3697
|
+
}
|
|
3698
|
+
}
|
|
3699
|
+
return null;
|
|
3700
|
+
}
|
|
3701
|
+
|
|
3702
|
+
// src/agent/multi-agent/worker.ts
|
|
3703
|
+
var WorkerAgent = class {
|
|
3704
|
+
constructor(ai, tools, toolCtx) {
|
|
3705
|
+
this.ai = ai;
|
|
3706
|
+
this.tools = tools;
|
|
3707
|
+
this.toolCtx = toolCtx;
|
|
3708
|
+
}
|
|
3709
|
+
async run(goal, assignment) {
|
|
3710
|
+
const messages = [
|
|
3711
|
+
{
|
|
3712
|
+
role: "system",
|
|
3713
|
+
content: buildWorkerPrompt(goal, assignment)
|
|
3714
|
+
}
|
|
3715
|
+
];
|
|
3716
|
+
const toolResults = [];
|
|
3717
|
+
for (let iteration = 0; iteration < 4; iteration++) {
|
|
3718
|
+
const response = await this.ai.chat(messages, this.tools);
|
|
3719
|
+
const assistantMessage = {
|
|
3720
|
+
role: "assistant",
|
|
3721
|
+
content: response.content ?? null,
|
|
3722
|
+
...response.toolCalls.length > 0 ? {
|
|
3723
|
+
tool_calls: response.toolCalls.map((call) => ({
|
|
3724
|
+
id: call.id,
|
|
3725
|
+
type: "function",
|
|
3726
|
+
function: { name: call.name, arguments: JSON.stringify(call.arguments) }
|
|
3727
|
+
}))
|
|
3728
|
+
} : {}
|
|
3729
|
+
};
|
|
3730
|
+
messages.push(assistantMessage);
|
|
3731
|
+
if (response.toolCalls.length === 0) {
|
|
3732
|
+
const proposal = normalizeProposal(
|
|
3733
|
+
assignment.id,
|
|
3734
|
+
parseJsonResponse(response.content)
|
|
3735
|
+
);
|
|
3736
|
+
if (!proposal) {
|
|
3737
|
+
return {
|
|
3738
|
+
assignment,
|
|
3739
|
+
error: "Worker did not return a valid WorkerProposal JSON object",
|
|
3740
|
+
toolResults
|
|
3741
|
+
};
|
|
3742
|
+
}
|
|
3743
|
+
return { assignment, proposal, toolResults };
|
|
3744
|
+
}
|
|
3745
|
+
for (const call of response.toolCalls) {
|
|
3746
|
+
const tool31 = this.tools.find((candidate) => candidate.name === call.name);
|
|
3747
|
+
const result = tool31 ? await tool31.execute(call.arguments, this.toolCtx) : { success: false, error: `Read-only worker cannot use tool: ${call.name}` };
|
|
3748
|
+
toolResults.push({ call, result });
|
|
3749
|
+
messages.push({
|
|
3750
|
+
role: "tool",
|
|
3751
|
+
content: JSON.stringify(result),
|
|
3752
|
+
tool_call_id: call.id
|
|
3753
|
+
});
|
|
3754
|
+
}
|
|
3755
|
+
}
|
|
3756
|
+
return {
|
|
3757
|
+
assignment,
|
|
3758
|
+
error: "Worker exhausted its iteration budget before returning a proposal",
|
|
3759
|
+
toolResults
|
|
3760
|
+
};
|
|
3761
|
+
}
|
|
3762
|
+
};
|
|
3763
|
+
function buildWorkerPrompt(goal, assignment) {
|
|
3764
|
+
return [
|
|
3765
|
+
"You are a Dominus worker agent. You are not the coordinator.",
|
|
3766
|
+
"You may inspect and reason, but you must not mutate Roblox Studio.",
|
|
3767
|
+
"Use only the read tools provided to gather evidence.",
|
|
3768
|
+
"Your final answer must be JSON only with this shape:",
|
|
3769
|
+
'{"workerId":"...","summary":"...","evidence":[{"source":"...","summary":"...","data":{}}],"proposedToolCalls":[{"tool":"set_properties","args":{},"targetPath":"...","rationale":"..."}],"risks":["..."],"verificationSuggestions":["..."]}',
|
|
3770
|
+
"",
|
|
3771
|
+
`Global goal: ${goal}`,
|
|
3772
|
+
`Worker id: ${assignment.id}`,
|
|
3773
|
+
`Role: ${assignment.role}`,
|
|
3774
|
+
`Objective: ${assignment.objective}`,
|
|
3775
|
+
`Owned scopes: ${assignment.ownedScopes.join(", ") || "(none)"}`,
|
|
3776
|
+
`Allowed read tools: ${assignment.allowedReadTools.join(", ")}`,
|
|
3777
|
+
"Acceptance criteria:",
|
|
3778
|
+
...assignment.acceptanceCriteria.map((criterion) => `- ${criterion}`),
|
|
3779
|
+
"",
|
|
3780
|
+
"Proposal rules:",
|
|
3781
|
+
"- Propose only coordinator-owned write tools: create_ui, set_properties, bulk_set_properties, insert_instance, move_instance.",
|
|
3782
|
+
"- Every proposed write must include targetPath and stay inside your owned scopes.",
|
|
3783
|
+
"- If you need no changes, return proposedToolCalls as an empty array and explain why."
|
|
3784
|
+
].join("\n");
|
|
3785
|
+
}
|
|
3786
|
+
function normalizeProposal(workerId, parsed) {
|
|
3787
|
+
if (!parsed || typeof parsed !== "object") return null;
|
|
3788
|
+
return {
|
|
3789
|
+
workerId: typeof parsed.workerId === "string" ? parsed.workerId : workerId,
|
|
3790
|
+
summary: typeof parsed.summary === "string" ? parsed.summary : "",
|
|
3791
|
+
evidence: Array.isArray(parsed.evidence) ? parsed.evidence.map((item) => ({
|
|
3792
|
+
source: String(item?.source ?? "worker"),
|
|
3793
|
+
summary: String(item?.summary ?? ""),
|
|
3794
|
+
data: item?.data
|
|
3795
|
+
})) : [],
|
|
3796
|
+
proposedToolCalls: Array.isArray(parsed.proposedToolCalls) ? parsed.proposedToolCalls.map((call) => ({
|
|
3797
|
+
tool: String(call?.tool ?? ""),
|
|
3798
|
+
args: call?.args && typeof call.args === "object" ? call.args : {},
|
|
3799
|
+
targetPath: typeof call?.targetPath === "string" ? call.targetPath : void 0,
|
|
3800
|
+
rationale: typeof call?.rationale === "string" ? call.rationale : void 0
|
|
3801
|
+
})) : [],
|
|
3802
|
+
risks: Array.isArray(parsed.risks) ? parsed.risks.map(String) : [],
|
|
3803
|
+
verificationSuggestions: Array.isArray(parsed.verificationSuggestions) ? parsed.verificationSuggestions.map(String) : []
|
|
3804
|
+
};
|
|
3805
|
+
}
|
|
3806
|
+
|
|
3807
|
+
// src/agent/multi-agent/coordinator.ts
|
|
3808
|
+
var MultiAgentCoordinator = class {
|
|
3809
|
+
constructor(ai, allTools) {
|
|
3810
|
+
this.ai = ai;
|
|
3811
|
+
this.allTools = allTools;
|
|
3812
|
+
}
|
|
3813
|
+
async execute(args, toolCtx, emit) {
|
|
3814
|
+
const normalized = normalizeArgs(args);
|
|
3815
|
+
const readTools = this.getReadOnlyTools();
|
|
3816
|
+
const assignments = await this.createAssignments(normalized, toolCtx, readTools);
|
|
3817
|
+
emit?.({ type: "multi_agent_start", goal: normalized.goal, workerCount: assignments.length });
|
|
3818
|
+
const workerResults = await Promise.allSettled(
|
|
3819
|
+
assignments.map(async (assignment) => {
|
|
3820
|
+
emit?.({
|
|
3821
|
+
type: "multi_agent_worker_start",
|
|
3822
|
+
workerId: assignment.id,
|
|
3823
|
+
role: assignment.role,
|
|
3824
|
+
scopes: assignment.ownedScopes
|
|
3825
|
+
});
|
|
3826
|
+
const worker = new WorkerAgent(this.ai, readTools, toolCtx);
|
|
3827
|
+
const result = await worker.run(normalized.goal, assignment);
|
|
3828
|
+
emit?.({
|
|
3829
|
+
type: "multi_agent_worker_done",
|
|
3830
|
+
workerId: assignment.id,
|
|
3831
|
+
summary: result.proposal?.summary ?? result.error ?? "Worker failed",
|
|
3832
|
+
proposedToolCallCount: result.proposal?.proposedToolCalls.length ?? 0
|
|
3833
|
+
});
|
|
3834
|
+
return result;
|
|
3835
|
+
})
|
|
3836
|
+
);
|
|
3837
|
+
const { accepted, rejected } = this.validateWorkerResults(assignments, workerResults, emit);
|
|
3838
|
+
const conflictChecked = rejectConflictingAcceptedProposals(accepted);
|
|
3839
|
+
for (const item of conflictChecked.rejected) {
|
|
3840
|
+
emit?.({ type: "multi_agent_proposal", workerId: item.workerId, accepted: false, reason: item.reason });
|
|
3841
|
+
}
|
|
3842
|
+
const finalAccepted = conflictChecked.accepted;
|
|
3843
|
+
const finalRejected = [...rejected, ...conflictChecked.rejected];
|
|
3844
|
+
const finalToolCalls = await this.synthesizeFinalToolCalls(normalized.goal, finalAccepted);
|
|
3845
|
+
const decision = {
|
|
3846
|
+
accepted: finalAccepted,
|
|
3847
|
+
rejected: finalRejected,
|
|
3848
|
+
finalToolCalls
|
|
3849
|
+
};
|
|
3850
|
+
emit?.({
|
|
3851
|
+
type: "multi_agent_decision",
|
|
3852
|
+
acceptedCount: decision.accepted.length,
|
|
3853
|
+
rejectedCount: decision.rejected.length,
|
|
3854
|
+
finalToolCallCount: decision.finalToolCalls.length
|
|
3855
|
+
});
|
|
3856
|
+
emit?.({ type: "multi_agent_apply_start", toolCallCount: decision.finalToolCalls.length });
|
|
3857
|
+
const applied = await this.applyFinalToolCalls(decision.finalToolCalls, toolCtx);
|
|
3858
|
+
const verificationFailures = await this.verifyAppliedCalls(applied, toolCtx);
|
|
3859
|
+
emit?.({
|
|
3860
|
+
type: "multi_agent_apply_done",
|
|
3861
|
+
appliedCount: applied.length,
|
|
3862
|
+
verificationFailureCount: verificationFailures.length
|
|
3863
|
+
});
|
|
3864
|
+
return {
|
|
3865
|
+
goal: normalized.goal,
|
|
3866
|
+
assignments,
|
|
3867
|
+
decision,
|
|
3868
|
+
applied,
|
|
3869
|
+
verificationFailures
|
|
3870
|
+
};
|
|
3871
|
+
}
|
|
3872
|
+
getReadOnlyTools() {
|
|
3873
|
+
return this.allTools.filter((tool31) => READ_ONLY_TOOL_NAMES.has(tool31.name));
|
|
3874
|
+
}
|
|
3875
|
+
getMetaTool() {
|
|
3876
|
+
return {
|
|
3877
|
+
name: "run_parallel_task",
|
|
3878
|
+
description: "Split a broad task into coordinated read-only worker agents. Workers inspect and propose scoped changes; the coordinator validates proposals and applies safe serial writes.",
|
|
3879
|
+
parameters: {
|
|
3880
|
+
type: "object",
|
|
3881
|
+
properties: {
|
|
3882
|
+
goal: {
|
|
3883
|
+
type: "string",
|
|
3884
|
+
description: "The task to coordinate across sub-agents."
|
|
3885
|
+
},
|
|
3886
|
+
rootPath: {
|
|
3887
|
+
type: "string",
|
|
3888
|
+
description: "Optional root instance path to partition work under, e.g. StarterGui.HUD."
|
|
3889
|
+
},
|
|
3890
|
+
maxWorkers: {
|
|
3891
|
+
type: "number",
|
|
3892
|
+
description: "Number of workers to use. Defaults to 3, max 5.",
|
|
3893
|
+
default: 3
|
|
3894
|
+
},
|
|
3895
|
+
constraints: {
|
|
3896
|
+
type: "string",
|
|
3897
|
+
description: "Extra constraints the workers and coordinator must follow."
|
|
3898
|
+
},
|
|
3899
|
+
taskType: {
|
|
3900
|
+
type: "string",
|
|
3901
|
+
description: "Optional task type hint such as ui, script, build, refactor, or general."
|
|
3902
|
+
}
|
|
3903
|
+
},
|
|
3904
|
+
required: ["goal"]
|
|
3905
|
+
},
|
|
3906
|
+
execute: async () => ({
|
|
3907
|
+
success: false,
|
|
3908
|
+
error: "run_parallel_task is a meta-tool handled by AgentLoop or MCP, not ToolRegistry."
|
|
3909
|
+
})
|
|
3910
|
+
};
|
|
3911
|
+
}
|
|
3912
|
+
shouldAutoTrigger(userMessage) {
|
|
3913
|
+
const lower = userMessage.toLowerCase();
|
|
3914
|
+
return [
|
|
3915
|
+
"parallel",
|
|
3916
|
+
"sub-agent",
|
|
3917
|
+
"subagent",
|
|
3918
|
+
"agents",
|
|
3919
|
+
"split this",
|
|
3920
|
+
"entire ui",
|
|
3921
|
+
"whole ui",
|
|
3922
|
+
"organize all",
|
|
3923
|
+
"organize entire",
|
|
3924
|
+
"large task"
|
|
3925
|
+
].some((cue) => lower.includes(cue));
|
|
3926
|
+
}
|
|
3927
|
+
async createAssignments(args, toolCtx, readTools) {
|
|
3928
|
+
const scopes = await this.discoverScopes(args, toolCtx);
|
|
3929
|
+
const fallback = buildFallbackAssignments(args, scopes, readTools);
|
|
3930
|
+
try {
|
|
3931
|
+
const response = await this.ai.chat([
|
|
3932
|
+
{
|
|
3933
|
+
role: "system",
|
|
3934
|
+
content: buildAssignmentPrompt(args, scopes, readTools)
|
|
3935
|
+
}
|
|
3936
|
+
]);
|
|
3937
|
+
const parsed = parseJsonResponse(response.content);
|
|
3938
|
+
const assignments = normalizeAssignments(parsed?.assignments, args, scopes, readTools);
|
|
3939
|
+
return assignments.length > 0 ? assignments : fallback;
|
|
3940
|
+
} catch {
|
|
3941
|
+
return fallback;
|
|
3942
|
+
}
|
|
3943
|
+
}
|
|
3944
|
+
async discoverScopes(args, toolCtx) {
|
|
3945
|
+
if (!args.rootPath || !toolCtx.isStudioConnected()) {
|
|
3946
|
+
return args.rootPath ? [args.rootPath] : ["Workspace", "StarterGui", "ReplicatedStorage"];
|
|
3947
|
+
}
|
|
3948
|
+
const serializeTool = this.allTools.find((tool31) => tool31.name === "serialize_ui");
|
|
3949
|
+
const looksUi = (args.taskType ?? "").toLowerCase().includes("ui") || args.rootPath.includes("Gui");
|
|
3950
|
+
if (serializeTool && looksUi) {
|
|
3951
|
+
const result = await serializeTool.execute({ path: args.rootPath, maxDepth: 2 }, toolCtx);
|
|
3952
|
+
const childScopes = result.success ? getSerializedChildScopes(args.rootPath, result.data) : [];
|
|
3953
|
+
if (childScopes.length > 0) return childScopes.slice(0, args.maxWorkers);
|
|
3954
|
+
}
|
|
3955
|
+
const explorerTool = this.allTools.find((tool31) => tool31.name === "get_explorer");
|
|
3956
|
+
if (explorerTool) {
|
|
3957
|
+
const result = await explorerTool.execute({ rootPath: args.rootPath, maxDepth: 2 }, toolCtx);
|
|
3958
|
+
const childScopes = result.success ? getExplorerChildScopes(args.rootPath, result.data) : [];
|
|
3959
|
+
if (childScopes.length > 0) return childScopes.slice(0, args.maxWorkers);
|
|
3960
|
+
}
|
|
3961
|
+
return [args.rootPath];
|
|
3962
|
+
}
|
|
3963
|
+
validateWorkerResults(assignments, settledResults, emit) {
|
|
3964
|
+
const accepted = [];
|
|
3965
|
+
const rejected = [];
|
|
3966
|
+
for (let i = 0; i < settledResults.length; i++) {
|
|
3967
|
+
const assignment = assignments[i];
|
|
3968
|
+
const result = settledResults[i];
|
|
3969
|
+
if (result.status === "rejected") {
|
|
3970
|
+
const reason = result.reason instanceof Error ? result.reason.message : String(result.reason);
|
|
3971
|
+
rejected.push({ workerId: assignment.id, reason });
|
|
3972
|
+
emit?.({ type: "multi_agent_proposal", workerId: assignment.id, accepted: false, reason });
|
|
3973
|
+
continue;
|
|
3974
|
+
}
|
|
3975
|
+
if (!result.value.proposal) {
|
|
3976
|
+
const reason = result.value.error ?? "Worker returned no proposal";
|
|
3977
|
+
rejected.push({ workerId: assignment.id, reason });
|
|
3978
|
+
emit?.({ type: "multi_agent_proposal", workerId: assignment.id, accepted: false, reason });
|
|
3979
|
+
continue;
|
|
3980
|
+
}
|
|
3981
|
+
const errors = validateWorkerProposal(assignment, result.value.proposal);
|
|
3982
|
+
if (errors.length > 0) {
|
|
3983
|
+
const reason = errors.join("; ");
|
|
3984
|
+
rejected.push({ workerId: assignment.id, proposal: result.value.proposal, reason });
|
|
3985
|
+
emit?.({ type: "multi_agent_proposal", workerId: assignment.id, accepted: false, reason });
|
|
3986
|
+
continue;
|
|
3987
|
+
}
|
|
3988
|
+
accepted.push({ workerId: assignment.id, proposal: result.value.proposal });
|
|
3989
|
+
emit?.({ type: "multi_agent_proposal", workerId: assignment.id, accepted: true });
|
|
3990
|
+
}
|
|
3991
|
+
return { accepted, rejected };
|
|
3992
|
+
}
|
|
3993
|
+
async synthesizeFinalToolCalls(goal, accepted) {
|
|
3994
|
+
const proposed = accepted.flatMap((item) => item.proposal.proposedToolCalls);
|
|
3995
|
+
const safeCalls = proposed.filter((call) => COORDINATOR_WRITE_TOOL_NAMES.has(call.tool)).map((call) => toToolCall(call.tool, call.args));
|
|
3996
|
+
if (safeCalls.length <= 1) return safeCalls;
|
|
3997
|
+
try {
|
|
3998
|
+
const response = await this.ai.chat([
|
|
3999
|
+
{
|
|
4000
|
+
role: "system",
|
|
4001
|
+
content: [
|
|
4002
|
+
"You are the Dominus multi-agent coordinator.",
|
|
4003
|
+
"Merge accepted worker proposals into a deterministic serial write plan.",
|
|
4004
|
+
'Return JSON only: {"toolCalls":[{"tool":"set_properties","args":{}}]}',
|
|
4005
|
+
"Only use these tools: create_ui, set_properties, bulk_set_properties, insert_instance, move_instance.",
|
|
4006
|
+
"Do not include run_code, delete_instance, edit_script, undo, or redo.",
|
|
4007
|
+
`Goal: ${goal}`,
|
|
4008
|
+
"Accepted proposals:",
|
|
4009
|
+
JSON.stringify(accepted, null, 2)
|
|
4010
|
+
].join("\n")
|
|
4011
|
+
}
|
|
4012
|
+
]);
|
|
4013
|
+
const parsed = parseJsonResponse(
|
|
4014
|
+
response.content
|
|
4015
|
+
);
|
|
4016
|
+
const merged = (parsed?.toolCalls ?? []).filter((call) => call.tool && COORDINATOR_WRITE_TOOL_NAMES.has(call.tool)).map((call) => toToolCall(call.tool, call.args ?? {}));
|
|
4017
|
+
return merged.length > 0 ? merged : safeCalls;
|
|
4018
|
+
} catch {
|
|
4019
|
+
return safeCalls;
|
|
4020
|
+
}
|
|
4021
|
+
}
|
|
4022
|
+
async applyFinalToolCalls(calls, toolCtx) {
|
|
4023
|
+
const applied = [];
|
|
4024
|
+
for (const call of calls) {
|
|
4025
|
+
const targetPath = inferTargetPath({ tool: call.name, args: call.arguments });
|
|
4026
|
+
if (!targetPath) {
|
|
4027
|
+
applied.push({ call, result: { success: false, error: `Missing target path for ${call.name}` } });
|
|
4028
|
+
continue;
|
|
4029
|
+
}
|
|
4030
|
+
const tool31 = this.allTools.find((candidate) => candidate.name === call.name);
|
|
4031
|
+
if (!tool31) {
|
|
4032
|
+
applied.push({ call, result: { success: false, error: `Unknown coordinator tool: ${call.name}` } });
|
|
4033
|
+
continue;
|
|
4034
|
+
}
|
|
4035
|
+
applied.push({ call, result: await tool31.execute(call.arguments, toolCtx) });
|
|
4036
|
+
}
|
|
4037
|
+
return applied;
|
|
4038
|
+
}
|
|
4039
|
+
async verifyAppliedCalls(applied, toolCtx) {
|
|
4040
|
+
const failures = [];
|
|
4041
|
+
for (const { call, result } of applied) {
|
|
4042
|
+
for (const check of inferVerifyChecks(call, result)) {
|
|
4043
|
+
const verified = await verifyAction(check, toolCtx);
|
|
4044
|
+
if (!verified.success) failures.push(verified);
|
|
4045
|
+
}
|
|
4046
|
+
}
|
|
4047
|
+
return failures;
|
|
4048
|
+
}
|
|
4049
|
+
};
|
|
4050
|
+
function normalizeArgs(args) {
|
|
4051
|
+
return {
|
|
4052
|
+
...args,
|
|
4053
|
+
goal: String(args.goal ?? "").trim(),
|
|
4054
|
+
maxWorkers: Math.max(1, Math.min(Number(args.maxWorkers ?? 3), 5))
|
|
4055
|
+
};
|
|
4056
|
+
}
|
|
4057
|
+
function buildAssignmentPrompt(args, scopes, readTools) {
|
|
4058
|
+
return [
|
|
4059
|
+
"You are the Dominus multi-agent assignment planner.",
|
|
4060
|
+
"Create non-overlapping worker assignments for a proposal-only parallel task.",
|
|
4061
|
+
'Return JSON only: {"assignments":[{"id":"worker-1","role":"...","objective":"...","ownedScopes":["..."],"allowedReadTools":["..."],"acceptanceCriteria":["..."]}]}',
|
|
4062
|
+
`Goal: ${args.goal}`,
|
|
4063
|
+
`Root path: ${args.rootPath ?? "(none)"}`,
|
|
4064
|
+
`Task type: ${args.taskType ?? "general"}`,
|
|
4065
|
+
`Constraints: ${args.constraints ?? "(none)"}`,
|
|
4066
|
+
`Max workers: ${args.maxWorkers}`,
|
|
4067
|
+
`Candidate scopes: ${scopes.join(", ")}`,
|
|
4068
|
+
`Read tools: ${readTools.map((tool31) => tool31.name).join(", ")}`
|
|
4069
|
+
].join("\n");
|
|
4070
|
+
}
|
|
4071
|
+
function buildFallbackAssignments(args, scopes, readTools) {
|
|
4072
|
+
const selectedScopes = scopes.slice(0, args.maxWorkers);
|
|
4073
|
+
const allowedReadTools = readTools.map((tool31) => tool31.name);
|
|
4074
|
+
const criteria = [
|
|
4075
|
+
"Gather concrete evidence with read-only tools.",
|
|
4076
|
+
"Propose only scoped coordinator-owned write calls.",
|
|
4077
|
+
"Explain risks and verification suggestions."
|
|
4078
|
+
];
|
|
4079
|
+
return selectedScopes.map((scope, index) => ({
|
|
4080
|
+
id: `worker-${index + 1}`,
|
|
4081
|
+
role: index === 0 ? "Inspector" : index === selectedScopes.length - 1 ? "Verifier" : "Specialist",
|
|
4082
|
+
objective: `Inspect ${scope} for: ${args.goal}`,
|
|
4083
|
+
ownedScopes: [scope],
|
|
4084
|
+
allowedReadTools,
|
|
4085
|
+
acceptanceCriteria: args.constraints ? [...criteria, args.constraints] : criteria
|
|
4086
|
+
}));
|
|
4087
|
+
}
|
|
4088
|
+
function normalizeAssignments(rawAssignments, args, scopes, readTools) {
|
|
4089
|
+
if (!Array.isArray(rawAssignments)) return [];
|
|
4090
|
+
const allowedReadTools = readTools.map((tool31) => tool31.name);
|
|
4091
|
+
return rawAssignments.slice(0, args.maxWorkers).map((assignment, index) => ({
|
|
4092
|
+
id: assignment.id?.trim() || `worker-${index + 1}`,
|
|
4093
|
+
role: assignment.role?.trim() || "Specialist",
|
|
4094
|
+
objective: assignment.objective?.trim() || `Inspect assigned scope for: ${args.goal}`,
|
|
4095
|
+
ownedScopes: Array.isArray(assignment.ownedScopes) && assignment.ownedScopes.length > 0 ? assignment.ownedScopes.map(String) : [scopes[index] ?? scopes[0] ?? args.rootPath ?? "Workspace"],
|
|
4096
|
+
allowedReadTools: allowedReadTools.filter(
|
|
4097
|
+
(tool31) => assignment.allowedReadTools?.length ? assignment.allowedReadTools.includes(tool31) : true
|
|
4098
|
+
),
|
|
4099
|
+
acceptanceCriteria: Array.isArray(assignment.acceptanceCriteria) && assignment.acceptanceCriteria.length > 0 ? assignment.acceptanceCriteria.map(String) : ["Gather evidence and propose scoped changes only."]
|
|
4100
|
+
})).filter((assignment) => assignment.ownedScopes.length > 0);
|
|
4101
|
+
}
|
|
4102
|
+
function getSerializedChildScopes(rootPath, data) {
|
|
4103
|
+
const node = data && typeof data === "object" ? data : null;
|
|
4104
|
+
const children = Array.isArray(node?.Children) ? node.Children : Array.isArray(node?.children) ? node.children : [];
|
|
4105
|
+
return children.map((child) => child && typeof child === "object" ? child.Name : void 0).filter((name) => typeof name === "string" && name.length > 0).map((name) => `${rootPath}.${name}`);
|
|
4106
|
+
}
|
|
4107
|
+
function getExplorerChildScopes(rootPath, data) {
|
|
4108
|
+
const payload = data && typeof data === "object" ? data : null;
|
|
4109
|
+
const tree = Array.isArray(payload?.tree) ? payload.tree : [];
|
|
4110
|
+
const root = tree[0] && typeof tree[0] === "object" ? tree[0] : null;
|
|
4111
|
+
const children = Array.isArray(root?.children) ? root.children : [];
|
|
4112
|
+
return children.map((child) => child && typeof child === "object" ? child.path : void 0).filter((pathValue) => typeof pathValue === "string" && pathValue.length > 0).map((pathValue) => pathValue || rootPath);
|
|
4113
|
+
}
|
|
4114
|
+
function toToolCall(name, args) {
|
|
4115
|
+
return {
|
|
4116
|
+
id: `multi_${nanoid()}`,
|
|
4117
|
+
name,
|
|
4118
|
+
arguments: args
|
|
4119
|
+
};
|
|
4120
|
+
}
|
|
4121
|
+
|
|
4122
|
+
// src/tools/registry.ts
|
|
4123
|
+
var ToolRegistry = class {
|
|
4124
|
+
tools = /* @__PURE__ */ new Map();
|
|
4125
|
+
readyTasks = [];
|
|
4126
|
+
register(tool31) {
|
|
4127
|
+
if (this.tools.has(tool31.name)) {
|
|
4128
|
+
throw new Error(`Tool already registered: ${tool31.name}`);
|
|
4129
|
+
}
|
|
4130
|
+
this.tools.set(tool31.name, tool31);
|
|
4131
|
+
}
|
|
4132
|
+
get(name) {
|
|
4133
|
+
return this.tools.get(name);
|
|
4134
|
+
}
|
|
4135
|
+
getAll() {
|
|
4136
|
+
return Array.from(this.tools.values());
|
|
4137
|
+
}
|
|
4138
|
+
getSchemas() {
|
|
4139
|
+
return this.getAll();
|
|
4140
|
+
}
|
|
4141
|
+
async execute(call, ctx) {
|
|
4142
|
+
const tool31 = this.tools.get(call.name);
|
|
4143
|
+
if (!tool31) {
|
|
4144
|
+
return { success: false, error: `Unknown tool: ${call.name}` };
|
|
4145
|
+
}
|
|
4146
|
+
try {
|
|
4147
|
+
return await tool31.execute(call.arguments, ctx);
|
|
4148
|
+
} catch (err) {
|
|
4149
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
4150
|
+
return { success: false, error: message };
|
|
4151
|
+
}
|
|
4152
|
+
}
|
|
4153
|
+
listNames() {
|
|
4154
|
+
return Array.from(this.tools.keys());
|
|
4155
|
+
}
|
|
4156
|
+
addReadyTask(task) {
|
|
4157
|
+
this.readyTasks.push(task);
|
|
4158
|
+
}
|
|
4159
|
+
async ready() {
|
|
4160
|
+
await Promise.all(this.readyTasks);
|
|
4161
|
+
}
|
|
4162
|
+
};
|
|
4163
|
+
function createDefaultRegistry() {
|
|
4164
|
+
const registry = new ToolRegistry();
|
|
4165
|
+
const toolModules = [
|
|
4166
|
+
Promise.resolve().then(() => (init_read_script(), read_script_exports)),
|
|
4167
|
+
Promise.resolve().then(() => (init_edit_script(), edit_script_exports)),
|
|
4168
|
+
Promise.resolve().then(() => (init_run_code(), run_code_exports)),
|
|
4169
|
+
Promise.resolve().then(() => (init_get_explorer(), get_explorer_exports)),
|
|
4170
|
+
Promise.resolve().then(() => (init_get_properties(), get_properties_exports)),
|
|
4171
|
+
Promise.resolve().then(() => (init_get_descendants_properties(), get_descendants_properties_exports)),
|
|
4172
|
+
Promise.resolve().then(() => (init_set_properties(), set_properties_exports)),
|
|
4173
|
+
Promise.resolve().then(() => (init_insert_instance(), insert_instance_exports)),
|
|
4174
|
+
Promise.resolve().then(() => (init_delete_instance(), delete_instance_exports)),
|
|
4175
|
+
Promise.resolve().then(() => (init_get_output(), get_output_exports)),
|
|
4176
|
+
Promise.resolve().then(() => (init_get_selection(), get_selection_exports)),
|
|
4177
|
+
Promise.resolve().then(() => (init_search_scripts(), search_scripts_exports)),
|
|
4178
|
+
Promise.resolve().then(() => (init_run_tests(), run_tests_exports)),
|
|
4179
|
+
Promise.resolve().then(() => (init_get_class_info(), get_class_info_exports)),
|
|
4180
|
+
Promise.resolve().then(() => (init_create_ui(), create_ui_exports)),
|
|
4181
|
+
Promise.resolve().then(() => (init_execute_run_test(), execute_run_test_exports)),
|
|
4182
|
+
Promise.resolve().then(() => (init_execute_play_test(), execute_play_test_exports)),
|
|
4183
|
+
Promise.resolve().then(() => (init_create_part(), create_part_exports)),
|
|
4184
|
+
Promise.resolve().then(() => (init_clone_instance(), clone_instance_exports)),
|
|
4185
|
+
Promise.resolve().then(() => (init_group_instances(), group_instances_exports)),
|
|
4186
|
+
Promise.resolve().then(() => (init_build_multiple(), build_multiple_exports)),
|
|
4187
|
+
Promise.resolve().then(() => (init_serialize_ui(), serialize_ui_exports)),
|
|
4188
|
+
Promise.resolve().then(() => (init_move_instance(), move_instance_exports)),
|
|
4189
|
+
Promise.resolve().then(() => (init_bulk_set_properties(), bulk_set_properties_exports)),
|
|
4190
|
+
Promise.resolve().then(() => (init_find_replace_scripts(), find_replace_scripts_exports)),
|
|
4191
|
+
Promise.resolve().then(() => (init_search_roblox_api(), search_roblox_api_exports)),
|
|
4192
|
+
Promise.resolve().then(() => (init_get_roblox_api_reference(), get_roblox_api_reference_exports)),
|
|
4193
|
+
Promise.resolve().then(() => (init_recall(), recall_exports)),
|
|
4194
|
+
Promise.resolve().then(() => (init_remember(), remember_exports)),
|
|
4195
|
+
Promise.resolve().then(() => (init_upload_asset(), upload_asset_exports))
|
|
4196
|
+
];
|
|
4197
|
+
registry.addReadyTask(Promise.all(
|
|
4198
|
+
toolModules.map(async (mod) => {
|
|
4199
|
+
const m = await mod;
|
|
4200
|
+
if (m.tool) registry.register(m.tool);
|
|
4201
|
+
})
|
|
4202
|
+
));
|
|
4203
|
+
registry.addReadyTask(Promise.resolve().then(() => (init_undo_redo(), undo_redo_exports)).then((m) => {
|
|
4204
|
+
registry.register(m.undoTool);
|
|
4205
|
+
registry.register(m.redoTool);
|
|
4206
|
+
}));
|
|
4207
|
+
return registry;
|
|
4208
|
+
}
|
|
4209
|
+
|
|
821
4210
|
// src/mcp/server.ts
|
|
822
4211
|
var bridge;
|
|
823
4212
|
async function startMcpServer() {
|
|
@@ -835,7 +4224,7 @@ async function startMcpServer() {
|
|
|
835
4224
|
}
|
|
836
4225
|
const mcp = new McpServer({
|
|
837
4226
|
name: "dominus",
|
|
838
|
-
version: "0.
|
|
4227
|
+
version: "2.0.0"
|
|
839
4228
|
}, {
|
|
840
4229
|
instructions: INTERNAL_MEMORY
|
|
841
4230
|
});
|
|
@@ -947,9 +4336,9 @@ ${" ".repeat(_depth)}}`;
|
|
|
947
4336
|
"read_script",
|
|
948
4337
|
"Read the source code of a script in Roblox Studio",
|
|
949
4338
|
{ path: z.string().describe('Full instance path, e.g. "ServerScriptService.GameManager"') },
|
|
950
|
-
async ({ path:
|
|
4339
|
+
async ({ path: path4 }) => {
|
|
951
4340
|
assertConnected();
|
|
952
|
-
const res = await bridge.sendRequest(CliMsg.GET_SCRIPT, { path:
|
|
4341
|
+
const res = await bridge.sendRequest(CliMsg.GET_SCRIPT, { path: path4 });
|
|
953
4342
|
return { content: [{ type: "text", text: JSON.stringify(res.payload, null, 2) }] };
|
|
954
4343
|
}
|
|
955
4344
|
);
|
|
@@ -960,9 +4349,9 @@ ${" ".repeat(_depth)}}`;
|
|
|
960
4349
|
path: z.string().describe("Full instance path"),
|
|
961
4350
|
source: z.string().describe("New complete source code")
|
|
962
4351
|
},
|
|
963
|
-
async ({ path:
|
|
4352
|
+
async ({ path: path4, source }) => {
|
|
964
4353
|
assertConnected();
|
|
965
|
-
const res = await bridge.sendRequest(CliMsg.SET_SCRIPT, { path:
|
|
4354
|
+
const res = await bridge.sendRequest(CliMsg.SET_SCRIPT, { path: path4, source });
|
|
966
4355
|
return { content: [{ type: "text", text: JSON.stringify(res.payload) }] };
|
|
967
4356
|
}
|
|
968
4357
|
);
|
|
@@ -999,9 +4388,9 @@ ${" ".repeat(_depth)}}`;
|
|
|
999
4388
|
path: z.string().describe('Full instance path (e.g. "Workspace.Part", "StarterGui.MyUI.Frame")'),
|
|
1000
4389
|
compact: z.boolean().optional().default(true).describe("Strip read-only, nil, deprecated, and default-valued properties (default true). Set false for full dump.")
|
|
1001
4390
|
},
|
|
1002
|
-
async ({ path:
|
|
4391
|
+
async ({ path: path4, compact }) => {
|
|
1003
4392
|
assertConnected();
|
|
1004
|
-
const res = await bridge.sendRequest(CliMsg.GET_PROPERTIES, { path:
|
|
4393
|
+
const res = await bridge.sendRequest(CliMsg.GET_PROPERTIES, { path: path4, compact });
|
|
1005
4394
|
return { content: [{ type: "text", text: JSON.stringify(res.payload, null, 2) }] };
|
|
1006
4395
|
}
|
|
1007
4396
|
);
|
|
@@ -1013,9 +4402,9 @@ ${" ".repeat(_depth)}}`;
|
|
|
1013
4402
|
compact: z.boolean().optional().default(true).describe("Strip noisy/default properties (default true). Set false for full dump."),
|
|
1014
4403
|
outputFile: z.string().optional().describe("Optional file path to write to prevent limit errors/timeouts")
|
|
1015
4404
|
},
|
|
1016
|
-
async ({ path:
|
|
4405
|
+
async ({ path: path4, compact, outputFile }) => {
|
|
1017
4406
|
assertConnected();
|
|
1018
|
-
const res = await bridge.sendRequest(CliMsg.GET_DESCENDANTS_PROPERTIES, { path:
|
|
4407
|
+
const res = await bridge.sendRequest(CliMsg.GET_DESCENDANTS_PROPERTIES, { path: path4, compact, outputFile }, 1e3 * 60 * 5);
|
|
1019
4408
|
return { content: [{ type: "text", text: JSON.stringify(res.payload, null, 2) }] };
|
|
1020
4409
|
}
|
|
1021
4410
|
);
|
|
@@ -1026,9 +4415,9 @@ ${" ".repeat(_depth)}}`;
|
|
|
1026
4415
|
path: z.string().describe("Full instance path"),
|
|
1027
4416
|
properties: z.record(z.unknown()).describe("Key-value pairs of properties to set")
|
|
1028
4417
|
},
|
|
1029
|
-
async ({ path:
|
|
4418
|
+
async ({ path: path4, properties }) => {
|
|
1030
4419
|
assertConnected();
|
|
1031
|
-
const res = await bridge.sendRequest(CliMsg.SET_PROPERTIES, { path:
|
|
4420
|
+
const res = await bridge.sendRequest(CliMsg.SET_PROPERTIES, { path: path4, properties });
|
|
1032
4421
|
return { content: [{ type: "text", text: JSON.stringify(res.payload) }] };
|
|
1033
4422
|
}
|
|
1034
4423
|
);
|
|
@@ -1056,9 +4445,9 @@ ${" ".repeat(_depth)}}`;
|
|
|
1056
4445
|
"delete_instance",
|
|
1057
4446
|
"Delete an instance from the Roblox Studio DataModel",
|
|
1058
4447
|
{ path: z.string().describe("Full instance path to delete") },
|
|
1059
|
-
async ({ path:
|
|
4448
|
+
async ({ path: path4 }) => {
|
|
1060
4449
|
assertConnected();
|
|
1061
|
-
const res = await bridge.sendRequest(CliMsg.DELETE_INSTANCE, { path:
|
|
4450
|
+
const res = await bridge.sendRequest(CliMsg.DELETE_INSTANCE, { path: path4 });
|
|
1062
4451
|
return { content: [{ type: "text", text: JSON.stringify(res.payload) }] };
|
|
1063
4452
|
}
|
|
1064
4453
|
);
|
|
@@ -1111,6 +4500,61 @@ ${" ".repeat(_depth)}}`;
|
|
|
1111
4500
|
return { content: [{ type: "text", text: JSON.stringify(res.payload, null, 2) }] };
|
|
1112
4501
|
}
|
|
1113
4502
|
);
|
|
4503
|
+
mcp.tool(
|
|
4504
|
+
"search_roblox_api",
|
|
4505
|
+
"Search Roblox Creator Docs engine API reference names. Use when you are unsure of the exact class, enum, datatype, global, or library name. Works without Studio connected.",
|
|
4506
|
+
{
|
|
4507
|
+
query: z.string().describe('Name fragment to search, e.g. "Text", "Run", "UDim", "FrameStyle"'),
|
|
4508
|
+
kind: z.enum(["any", "class", "datatype", "enum", "global", "library"]).optional().default("any"),
|
|
4509
|
+
limit: z.number().optional().default(10).describe("Maximum results to return")
|
|
4510
|
+
},
|
|
4511
|
+
async ({ query, kind, limit }) => {
|
|
4512
|
+
const results = await searchRobloxApi(query, { kind, limit });
|
|
4513
|
+
return { content: [{ type: "text", text: JSON.stringify({ results }, null, 2) }] };
|
|
4514
|
+
}
|
|
4515
|
+
);
|
|
4516
|
+
mcp.tool(
|
|
4517
|
+
"get_roblox_api_reference",
|
|
4518
|
+
"Look up Roblox Creator Docs engine API reference for a class, datatype, enum, global, or library. Includes summaries, descriptions, members, parameters, returns, security, and thread safety. Works without Studio connected.",
|
|
4519
|
+
{
|
|
4520
|
+
name: z.string().describe('API reference name, e.g. "Frame", "RunService", "UDim2", "FrameStyle", "task"'),
|
|
4521
|
+
kind: z.enum(["any", "class", "datatype", "enum", "global", "library"]).optional().default("any"),
|
|
4522
|
+
includeInherited: z.boolean().optional().default(true).describe("For classes, include members from inherited base classes")
|
|
4523
|
+
},
|
|
4524
|
+
async ({ name, kind, includeInherited }) => {
|
|
4525
|
+
const reference = await getRobloxApiReference(name, { kind, includeInherited });
|
|
4526
|
+
return { content: [{ type: "text", text: JSON.stringify(reference, null, 2) }] };
|
|
4527
|
+
}
|
|
4528
|
+
);
|
|
4529
|
+
mcp.tool(
|
|
4530
|
+
"run_parallel_task",
|
|
4531
|
+
"Split a broad task into coordinated read-only worker agents. Workers inspect and propose scoped changes; the coordinator validates proposals and applies safe serial writes.",
|
|
4532
|
+
{
|
|
4533
|
+
goal: z.string().describe("The task to coordinate across sub-agents"),
|
|
4534
|
+
rootPath: z.string().optional().describe("Optional root instance path to partition work under, e.g. StarterGui.HUD"),
|
|
4535
|
+
maxWorkers: z.number().optional().default(3).describe("Number of workers to use, max 5"),
|
|
4536
|
+
constraints: z.string().optional().describe("Extra constraints workers and coordinator must follow"),
|
|
4537
|
+
taskType: z.string().optional().describe("Optional task type hint such as ui, script, build, refactor, or general")
|
|
4538
|
+
},
|
|
4539
|
+
async ({ goal, rootPath, maxWorkers, constraints, taskType }) => {
|
|
4540
|
+
const registry = createDefaultRegistry();
|
|
4541
|
+
await registry.ready();
|
|
4542
|
+
const ai = new AIProvider(loadConfig());
|
|
4543
|
+
const coordinator = new MultiAgentCoordinator(ai, registry.getAll());
|
|
4544
|
+
const events = [];
|
|
4545
|
+
const result = await coordinator.execute(
|
|
4546
|
+
{ goal, rootPath, maxWorkers, constraints, taskType },
|
|
4547
|
+
createMcpToolContext(),
|
|
4548
|
+
(event) => events.push(event)
|
|
4549
|
+
);
|
|
4550
|
+
return {
|
|
4551
|
+
content: [{
|
|
4552
|
+
type: "text",
|
|
4553
|
+
text: JSON.stringify({ ...result, events }, null, 2)
|
|
4554
|
+
}]
|
|
4555
|
+
};
|
|
4556
|
+
}
|
|
4557
|
+
);
|
|
1114
4558
|
mcp.tool(
|
|
1115
4559
|
"create_ui",
|
|
1116
4560
|
'Create an entire UI tree in one call. Pass a declarative JSON tree \u2014 ClassName, Name, properties, Children. All types auto-coerced in Luau (UDim2, Color3, Font, enums). 10x faster than run_code. UDim2: [xScale, xOffset, yScale, yOffset]. Color3: "#hex" or "rgb(r,g,b)". Font: "GothamBold". Enums: string names.',
|
|
@@ -1326,11 +4770,11 @@ ${" ".repeat(_depth)}}`;
|
|
|
1326
4770
|
path: z.string().describe('Instance path to serialize (e.g. "StarterGui.MyScreenGui")'),
|
|
1327
4771
|
maxDepth: z.number().optional().default(50).describe("Maximum tree depth (default 50)")
|
|
1328
4772
|
},
|
|
1329
|
-
async ({ path:
|
|
4773
|
+
async ({ path: path4, maxDepth }) => {
|
|
1330
4774
|
assertConnected();
|
|
1331
4775
|
const res = await bridge.sendRequest(
|
|
1332
4776
|
CliMsg.SERIALIZE_UI,
|
|
1333
|
-
{ path:
|
|
4777
|
+
{ path: path4, maxDepth },
|
|
1334
4778
|
1e3 * 60 * 2
|
|
1335
4779
|
);
|
|
1336
4780
|
if (!res.payload.success) {
|
|
@@ -1375,16 +4819,16 @@ ${" ".repeat(_depth)}}`;
|
|
|
1375
4819
|
path: z.string().describe('Full instance path (e.g. "Workspace.MyModel")'),
|
|
1376
4820
|
childDepth: z.number().optional().default(2).describe("How deep to show the children tree (default 2)")
|
|
1377
4821
|
},
|
|
1378
|
-
async ({ path:
|
|
4822
|
+
async ({ path: path4, childDepth }) => {
|
|
1379
4823
|
assertConnected();
|
|
1380
4824
|
const [propRes, treeRes] = await Promise.all([
|
|
1381
4825
|
bridge.sendRequest(
|
|
1382
4826
|
CliMsg.GET_PROPERTIES,
|
|
1383
|
-
{ path:
|
|
4827
|
+
{ path: path4, compact: true }
|
|
1384
4828
|
),
|
|
1385
4829
|
bridge.sendRequest(
|
|
1386
4830
|
CliMsg.GET_EXPLORER,
|
|
1387
|
-
{ rootPath:
|
|
4831
|
+
{ rootPath: path4, maxDepth: childDepth ?? 2 }
|
|
1388
4832
|
)
|
|
1389
4833
|
]);
|
|
1390
4834
|
return {
|
|
@@ -1406,11 +4850,11 @@ ${" ".repeat(_depth)}}`;
|
|
|
1406
4850
|
framework: z.enum(["roact", "react"]).optional().default("react").describe('Target framework: "roact" (legacy) or "react" (react-lua, default)'),
|
|
1407
4851
|
componentName: z.string().optional().describe("Name for the generated component (defaults to instance Name)")
|
|
1408
4852
|
},
|
|
1409
|
-
async ({ path:
|
|
4853
|
+
async ({ path: path4, framework, componentName }) => {
|
|
1410
4854
|
assertConnected();
|
|
1411
4855
|
const res = await bridge.sendRequest(
|
|
1412
4856
|
CliMsg.SERIALIZE_UI,
|
|
1413
|
-
{ path:
|
|
4857
|
+
{ path: path4, maxDepth: 50 },
|
|
1414
4858
|
1e3 * 60 * 2
|
|
1415
4859
|
);
|
|
1416
4860
|
if (!res.payload.success || !res.payload.tree) {
|
|
@@ -1461,7 +4905,16 @@ ${" ".repeat(_depth)}}`;
|
|
|
1461
4905
|
}]
|
|
1462
4906
|
};
|
|
1463
4907
|
}
|
|
1464
|
-
|
|
4908
|
+
try {
|
|
4909
|
+
bridge.setActivePlaceId(placeId);
|
|
4910
|
+
} catch (err) {
|
|
4911
|
+
return {
|
|
4912
|
+
content: [{
|
|
4913
|
+
type: "text",
|
|
4914
|
+
text: err instanceof Error ? err.message : String(err)
|
|
4915
|
+
}]
|
|
4916
|
+
};
|
|
4917
|
+
}
|
|
1465
4918
|
if (placeId === 0) {
|
|
1466
4919
|
return { content: [{ type: "text", text: "Active place reset to auto-select." }] };
|
|
1467
4920
|
}
|
|
@@ -1527,11 +4980,50 @@ ${" ".repeat(_depth)}}`;
|
|
|
1527
4980
|
}
|
|
1528
4981
|
function assertConnected() {
|
|
1529
4982
|
if (!bridge.isConnected()) {
|
|
4983
|
+
const connections = bridge.listConnections();
|
|
4984
|
+
const activePlaceId = bridge.getActivePlaceId();
|
|
4985
|
+
if (connections.length > 1 && activePlaceId === 0) {
|
|
4986
|
+
const available = connections.map((c) => `${c.placeName ?? "Unknown"} (${c.placeId})`).join(", ");
|
|
4987
|
+
throw new Error(`Multiple Roblox Studio instances are connected: ${available}. Use list_connections and set_active_place before running Studio tools.`);
|
|
4988
|
+
}
|
|
4989
|
+
if (connections.length > 0 && activePlaceId !== 0) {
|
|
4990
|
+
throw new Error(`Active Roblox Studio place ${activePlaceId} is not connected. Use list_connections and set_active_place to choose a connected Studio instance.`);
|
|
4991
|
+
}
|
|
1530
4992
|
throw new Error(
|
|
1531
4993
|
"Roblox Studio is not connected. Open Studio and click the Dominus plugin Connect button."
|
|
1532
4994
|
);
|
|
1533
4995
|
}
|
|
1534
4996
|
}
|
|
4997
|
+
function createMcpToolContext() {
|
|
4998
|
+
return {
|
|
4999
|
+
sendToStudio: (type, payload, timeoutMs) => {
|
|
5000
|
+
return bridge.sendRequest(type, payload, timeoutMs);
|
|
5001
|
+
},
|
|
5002
|
+
isStudioConnected: () => bridge.isConnected(),
|
|
5003
|
+
memory: {
|
|
5004
|
+
recall: (query, projectId, limit) => {
|
|
5005
|
+
const facts = recallFacts(projectId || "current", query, limit);
|
|
5006
|
+
return facts.map((fact) => fact.content);
|
|
5007
|
+
},
|
|
5008
|
+
learn: (projectId, facts) => {
|
|
5009
|
+
const now = Date.now();
|
|
5010
|
+
for (const fact of facts) {
|
|
5011
|
+
storeFact({
|
|
5012
|
+
projectId: projectId || "current",
|
|
5013
|
+
content: fact.content,
|
|
5014
|
+
category: fact.category,
|
|
5015
|
+
source: "mcp",
|
|
5016
|
+
relevance: 1,
|
|
5017
|
+
createdAt: now,
|
|
5018
|
+
accessedAt: now
|
|
5019
|
+
});
|
|
5020
|
+
}
|
|
5021
|
+
},
|
|
5022
|
+
getScriptIndex: (projectId) => getScriptIndex(projectId || "current")
|
|
5023
|
+
},
|
|
5024
|
+
config: createConfigAccess()
|
|
5025
|
+
};
|
|
5026
|
+
}
|
|
1535
5027
|
|
|
1536
5028
|
// src/mcp/index.ts
|
|
1537
5029
|
startMcpServer().catch((err) => {
|