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/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 { nanoid } from 'nanoid';
8
- import fs from 'fs';
9
- import initSqlJs from 'sql.js';
10
- import path from 'path';
11
- import os from 'os';
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
- var StudioMsg = {
14
- CONNECTED: "studio:connected",
15
- DISCONNECTED: "studio:disconnected",
16
- EXPLORER: "studio:explorer",
17
- OUTPUT: "studio:output",
18
- ERROR: "studio:error",
19
- SELECTION: "studio:selection",
20
- SCRIPT_CONTENT: "studio:script:content",
21
- SCRIPT_CHANGED: "studio:script:changed",
22
- RESPONSE: "studio:response",
23
- REFLECTION: "studio:reflection",
24
- TEST_RESULTS: "studio:test:results"
25
- };
26
- var CliMsg = {
27
- GET_EXPLORER: "cli:get:explorer",
28
- GET_SCRIPT: "cli:get:script",
29
- SET_SCRIPT: "cli:set:script",
30
- RUN_CODE: "cli:run",
31
- GET_PROPERTIES: "cli:get:properties",
32
- GET_DESCENDANTS_PROPERTIES: "cli:get:descendants:properties",
33
- SET_PROPERTIES: "cli:set:properties",
34
- INSERT_INSTANCE: "cli:insert",
35
- DELETE_INSTANCE: "cli:delete",
36
- GET_SELECTION: "cli:get:selection",
37
- SELECT: "cli:select",
38
- SEARCH_SCRIPTS: "cli:search:scripts",
39
- GET_OUTPUT: "cli:get:output",
40
- RUN_TESTS: "cli:run:tests",
41
- EXECUTE_RUN_TEST: "cli:test:run",
42
- EXECUTE_PLAY_TEST: "cli:test:play",
43
- END_TEST: "cli:test:end",
44
- BUILD_UI: "cli:build:ui",
45
- GET_REFLECTION: "cli:get:reflection",
46
- UPLOAD_ASSET: "cli:upload:asset",
47
- SERIALIZE_UI: "cli:serialize:ui",
48
- MOVE_INSTANCE: "cli:move:instance",
49
- UNDO: "cli:undo",
50
- REDO: "cli:redo",
51
- BULK_SET_PROPERTIES: "cli:bulk:set:properties",
52
- FIND_REPLACE_SCRIPTS: "cli:find:replace:scripts"
53
- };
54
- function createMessage(type, payload) {
55
- return { id: nanoid(), type, payload, ts: Date.now() };
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
- function serializeMessage(msg) {
65
- return JSON.stringify(msg);
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, controllerWs] of this.relayRequests) {
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
- clearTimeout(pending.timer);
157
- pending.reject(new Error(`Studio disconnected (place ${placeId})`));
158
- this.pendingRequests.delete(id);
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.emit("studio:connected", { ...payload, placeId });
188
- this.broadcastToControllers("studio:connected", { ...payload, placeId });
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, controllerWs);
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
- const controllerWs = this.relayRequests.get(msg.id);
234
- if (controllerWs && controllerWs.readyState === WebSocket.OPEN) {
235
- controllerWs.send(serializeMessage(msg));
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.studioConnected && this.connections.size > 0;
2946
+ return this.resolveTargetInfo() !== null;
485
2947
  }
486
2948
  getConnectionInfo() {
487
- if (this.connections.size === 0) return null;
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.6.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: path2 }) => {
4339
+ async ({ path: path4 }) => {
951
4340
  assertConnected();
952
- const res = await bridge.sendRequest(CliMsg.GET_SCRIPT, { path: path2 });
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: path2, source }) => {
4352
+ async ({ path: path4, source }) => {
964
4353
  assertConnected();
965
- const res = await bridge.sendRequest(CliMsg.SET_SCRIPT, { path: path2, source });
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: path2, compact }) => {
4391
+ async ({ path: path4, compact }) => {
1003
4392
  assertConnected();
1004
- const res = await bridge.sendRequest(CliMsg.GET_PROPERTIES, { path: path2, compact });
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: path2, compact, outputFile }) => {
4405
+ async ({ path: path4, compact, outputFile }) => {
1017
4406
  assertConnected();
1018
- const res = await bridge.sendRequest(CliMsg.GET_DESCENDANTS_PROPERTIES, { path: path2, compact, outputFile }, 1e3 * 60 * 5);
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: path2, properties }) => {
4418
+ async ({ path: path4, properties }) => {
1030
4419
  assertConnected();
1031
- const res = await bridge.sendRequest(CliMsg.SET_PROPERTIES, { path: path2, properties });
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: path2 }) => {
4448
+ async ({ path: path4 }) => {
1060
4449
  assertConnected();
1061
- const res = await bridge.sendRequest(CliMsg.DELETE_INSTANCE, { path: path2 });
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: path2, maxDepth }) => {
4773
+ async ({ path: path4, maxDepth }) => {
1330
4774
  assertConnected();
1331
4775
  const res = await bridge.sendRequest(
1332
4776
  CliMsg.SERIALIZE_UI,
1333
- { path: path2, maxDepth },
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: path2, childDepth }) => {
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: path2, compact: true }
4827
+ { path: path4, compact: true }
1384
4828
  ),
1385
4829
  bridge.sendRequest(
1386
4830
  CliMsg.GET_EXPLORER,
1387
- { rootPath: path2, maxDepth: childDepth ?? 2 }
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: path2, framework, componentName }) => {
4853
+ async ({ path: path4, framework, componentName }) => {
1410
4854
  assertConnected();
1411
4855
  const res = await bridge.sendRequest(
1412
4856
  CliMsg.SERIALIZE_UI,
1413
- { path: path2, maxDepth: 50 },
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
- bridge.setActivePlaceId(placeId);
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) => {