dominus-cli 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js ADDED
@@ -0,0 +1,4700 @@
1
+ #!/usr/bin/env node
2
+ import fs4, { existsSync, readFileSync, unlinkSync } from 'fs';
3
+ import path3, { join } from 'path';
4
+ import os2, { tmpdir } from 'os';
5
+ import { nanoid } from 'nanoid';
6
+ import { WebSocket, WebSocketServer } from 'ws';
7
+ import { EventEmitter } from 'events';
8
+ import initSqlJs from 'sql.js';
9
+ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
10
+ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
11
+ import { z } from 'zod';
12
+ import { Box, Text, useInput, render } from 'ink';
13
+ import { Command } from 'commander';
14
+ import React6, { useState, useRef, useMemo, useCallback, useEffect } from 'react';
15
+ import { jsxs, jsx, Fragment } from 'react/jsx-runtime';
16
+ import TextInput from 'ink-text-input';
17
+ import { execSync } from 'child_process';
18
+ import { randomBytes } from 'crypto';
19
+ import Spinner from 'ink-spinner';
20
+ import OpenAI from 'openai';
21
+
22
+ var __defProp = Object.defineProperty;
23
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
24
+ var __getOwnPropNames = Object.getOwnPropertyNames;
25
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
26
+ var __esm = (fn, res) => function __init() {
27
+ return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
28
+ };
29
+ var __export = (target, all) => {
30
+ for (var name in all)
31
+ __defProp(target, name, { get: all[name], enumerable: true });
32
+ };
33
+ var __copyProps = (to, from, except, desc) => {
34
+ if (from && typeof from === "object" || typeof from === "function") {
35
+ for (let key of __getOwnPropNames(from))
36
+ if (!__hasOwnProp.call(to, key) && key !== except)
37
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
38
+ }
39
+ return to;
40
+ };
41
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
42
+ function detectProviderFromEnv() {
43
+ const priority = ["openai", "anthropic", "gemini", "xai", "openrouter"];
44
+ for (const p of priority) {
45
+ const key = process.env[PROVIDERS[p].envVar];
46
+ if (key) return { provider: p, apiKey: key };
47
+ }
48
+ return null;
49
+ }
50
+ function resolveProviderSettings(config) {
51
+ const providerInfo = PROVIDERS[config.provider];
52
+ return {
53
+ baseUrl: config.apiBaseUrl || providerInfo?.baseUrl || PROVIDERS.openrouter.baseUrl,
54
+ model: config.model || providerInfo?.defaultModel || "gpt-4o"
55
+ };
56
+ }
57
+ function ensureDir() {
58
+ if (!fs4.existsSync(DOMINUS_DIR)) {
59
+ fs4.mkdirSync(DOMINUS_DIR, { recursive: true });
60
+ }
61
+ }
62
+ function readJson(filePath, defaults) {
63
+ try {
64
+ if (fs4.existsSync(filePath)) {
65
+ const raw = fs4.readFileSync(filePath, "utf-8");
66
+ return { ...defaults, ...JSON.parse(raw) };
67
+ }
68
+ } catch {
69
+ }
70
+ return { ...defaults };
71
+ }
72
+ function writeJson(filePath, data) {
73
+ ensureDir();
74
+ fs4.writeFileSync(filePath, JSON.stringify(data, null, 2), "utf-8");
75
+ }
76
+ function loadConfig() {
77
+ ensureDir();
78
+ return readJson(CONFIG_PATH, DEFAULTS);
79
+ }
80
+ function saveConfig(config) {
81
+ writeJson(CONFIG_PATH, config);
82
+ }
83
+ function getConfigValue(key) {
84
+ const config = loadConfig();
85
+ return config[key];
86
+ }
87
+ function setConfigValue(key, value) {
88
+ const config = loadConfig();
89
+ config[key] = value;
90
+ saveConfig(config);
91
+ }
92
+ function loadRules() {
93
+ ensureDir();
94
+ return readJson(RULES_PATH, DEFAULT_RULES);
95
+ }
96
+ function saveRules(rules) {
97
+ writeJson(RULES_PATH, rules);
98
+ }
99
+ function getDominusDir() {
100
+ ensureDir();
101
+ return DOMINUS_DIR;
102
+ }
103
+ function getDbPath() {
104
+ ensureDir();
105
+ return DB_PATH;
106
+ }
107
+ function createConfigAccess() {
108
+ return {
109
+ get(key) {
110
+ const config = loadConfig();
111
+ return config[key];
112
+ },
113
+ set(key, value) {
114
+ const config = loadConfig();
115
+ config[key] = value;
116
+ saveConfig(config);
117
+ },
118
+ getAll() {
119
+ return loadConfig();
120
+ }
121
+ };
122
+ }
123
+ var DOMINUS_DIR, CONFIG_PATH, DB_PATH, RULES_PATH, PROVIDERS, DEFAULTS, DEFAULT_RULES;
124
+ var init_config = __esm({
125
+ "src/config/config.ts"() {
126
+ DOMINUS_DIR = path3.join(os2.homedir(), ".dominus");
127
+ CONFIG_PATH = path3.join(DOMINUS_DIR, "config.json");
128
+ DB_PATH = path3.join(DOMINUS_DIR, "dominus.db");
129
+ RULES_PATH = path3.join(DOMINUS_DIR, "rules.json");
130
+ PROVIDERS = {
131
+ openai: {
132
+ name: "OpenAI",
133
+ baseUrl: "https://api.openai.com/v1",
134
+ envVar: "OPENAI_API_KEY",
135
+ defaultModel: "gpt-5.3-codex"
136
+ },
137
+ anthropic: {
138
+ name: "Anthropic",
139
+ baseUrl: "https://api.anthropic.com/v1/",
140
+ envVar: "ANTHROPIC_API_KEY",
141
+ defaultModel: "claude-sonnet-4-6"
142
+ },
143
+ gemini: {
144
+ name: "Google Gemini",
145
+ baseUrl: "https://generativelanguage.googleapis.com/v1beta/openai",
146
+ envVar: "GEMINI_API_KEY",
147
+ defaultModel: "gemini-3.1-pro"
148
+ },
149
+ xai: {
150
+ name: "xAI (Grok)",
151
+ baseUrl: "https://api.x.ai/v1",
152
+ envVar: "XAI_API_KEY",
153
+ defaultModel: "grok-4.2"
154
+ },
155
+ openrouter: {
156
+ name: "OpenRouter",
157
+ baseUrl: "https://openrouter.ai/api/v1",
158
+ envVar: "OPENROUTER_API_KEY",
159
+ defaultModel: "anthropic/claude-sonnet-4-6"
160
+ },
161
+ custom: {
162
+ name: "Custom",
163
+ baseUrl: "",
164
+ envVar: "",
165
+ defaultModel: ""
166
+ }
167
+ };
168
+ DEFAULTS = {
169
+ provider: "openai",
170
+ apiBaseUrl: "",
171
+ model: "",
172
+ port: 18088,
173
+ maxToolIterations: 25,
174
+ maxVerifyRetries: 3,
175
+ tokenBudget: 128e3,
176
+ autoIndex: true,
177
+ autoLearn: true,
178
+ userName: "Developer"
179
+ };
180
+ DEFAULT_RULES = {
181
+ do: [],
182
+ dont: [],
183
+ conventions: [],
184
+ context: ""
185
+ };
186
+ }
187
+ });
188
+ function createMessage(type, payload) {
189
+ return { id: nanoid(), type, payload, ts: Date.now() };
190
+ }
191
+ function parseMessage(raw) {
192
+ try {
193
+ return JSON.parse(raw);
194
+ } catch {
195
+ throw new Error(`Failed to parse WebSocket message: ${raw.slice(0, 100)}`);
196
+ }
197
+ }
198
+ function serializeMessage(msg) {
199
+ return JSON.stringify(msg);
200
+ }
201
+ var StudioMsg, CliMsg;
202
+ var init_protocol = __esm({
203
+ "src/transport/protocol.ts"() {
204
+ StudioMsg = {
205
+ CONNECTED: "studio:connected",
206
+ DISCONNECTED: "studio:disconnected",
207
+ EXPLORER: "studio:explorer",
208
+ OUTPUT: "studio:output",
209
+ ERROR: "studio:error",
210
+ SELECTION: "studio:selection",
211
+ SCRIPT_CONTENT: "studio:script:content",
212
+ SCRIPT_CHANGED: "studio:script:changed",
213
+ RESPONSE: "studio:response",
214
+ REFLECTION: "studio:reflection",
215
+ TEST_RESULTS: "studio:test:results"
216
+ };
217
+ CliMsg = {
218
+ GET_EXPLORER: "cli:get:explorer",
219
+ GET_SCRIPT: "cli:get:script",
220
+ SET_SCRIPT: "cli:set:script",
221
+ RUN_CODE: "cli:run",
222
+ GET_PROPERTIES: "cli:get:properties",
223
+ SET_PROPERTIES: "cli:set:properties",
224
+ INSERT_INSTANCE: "cli:insert",
225
+ DELETE_INSTANCE: "cli:delete",
226
+ GET_SELECTION: "cli:get:selection",
227
+ SELECT: "cli:select",
228
+ SEARCH_SCRIPTS: "cli:search:scripts",
229
+ GET_OUTPUT: "cli:get:output",
230
+ RUN_TESTS: "cli:run:tests",
231
+ EXECUTE_RUN_TEST: "cli:test:run",
232
+ EXECUTE_PLAY_TEST: "cli:test:play",
233
+ END_TEST: "cli:test:end",
234
+ BUILD_UI: "cli:build:ui",
235
+ GET_REFLECTION: "cli:get:reflection",
236
+ UPLOAD_ASSET: "cli:upload:asset"
237
+ };
238
+ }
239
+ });
240
+ var DominusServer;
241
+ var init_ws_server = __esm({
242
+ "src/transport/ws-server.ts"() {
243
+ init_protocol();
244
+ DominusServer = class extends EventEmitter {
245
+ wss = null;
246
+ studioClient = null;
247
+ controllers = /* @__PURE__ */ new Set();
248
+ pendingRequests = /* @__PURE__ */ new Map();
249
+ relayRequests = /* @__PURE__ */ new Map();
250
+ port;
251
+ constructor(port = 18088) {
252
+ super();
253
+ this.port = port;
254
+ }
255
+ start(maxRetries = 0) {
256
+ return new Promise((resolve, reject) => {
257
+ const tryPort = (port, attempt) => {
258
+ const wss = new WebSocketServer({ port });
259
+ wss.on("listening", () => {
260
+ this.wss = wss;
261
+ this.port = port;
262
+ this.wss.on("connection", (ws) => this.handleConnection(ws));
263
+ this.emit("server:started", { port });
264
+ resolve();
265
+ });
266
+ wss.on("error", (err) => {
267
+ if (err.code === "EADDRINUSE" && attempt < maxRetries) {
268
+ wss.close();
269
+ tryPort(port + 1, attempt + 1);
270
+ } else {
271
+ this.emit("server:error", err);
272
+ reject(err);
273
+ }
274
+ });
275
+ };
276
+ tryPort(this.port, 0);
277
+ });
278
+ }
279
+ handleConnection(ws) {
280
+ let identified = false;
281
+ ws.on("message", (raw) => {
282
+ try {
283
+ const msg = parseMessage(raw.toString());
284
+ if (!identified) {
285
+ if (msg.type === StudioMsg.CONNECTED) {
286
+ identified = true;
287
+ this.setStudioClient(ws, msg);
288
+ return;
289
+ }
290
+ if (msg.type === "controller:connect") {
291
+ identified = true;
292
+ this.addController(ws);
293
+ ws.send(serializeMessage(createMessage("controller:welcome", {
294
+ studioConnected: this.isConnected(),
295
+ port: this.port
296
+ })));
297
+ return;
298
+ }
299
+ }
300
+ if (this.controllers.has(ws)) {
301
+ this.handleControllerMessage(ws, msg);
302
+ } else {
303
+ this.handleStudioMessage(msg);
304
+ }
305
+ } catch (err) {
306
+ this.emit("server:error", err);
307
+ }
308
+ });
309
+ ws.on("close", () => {
310
+ if (this.controllers.has(ws)) {
311
+ this.controllers.delete(ws);
312
+ for (const [id, controllerWs] of this.relayRequests) {
313
+ if (controllerWs === ws) this.relayRequests.delete(id);
314
+ }
315
+ } else if (this.studioClient?.ws === ws) {
316
+ this.studioClient = null;
317
+ this.emit("studio:disconnected");
318
+ this.broadcastToControllers("studio:disconnected", {});
319
+ for (const [id, pending] of this.pendingRequests) {
320
+ clearTimeout(pending.timer);
321
+ pending.reject(new Error("Studio disconnected"));
322
+ this.pendingRequests.delete(id);
323
+ }
324
+ }
325
+ });
326
+ ws.on("error", (err) => {
327
+ this.emit("server:error", err);
328
+ });
329
+ }
330
+ setStudioClient(ws, msg) {
331
+ if (this.studioClient) {
332
+ this.studioClient.ws.close();
333
+ }
334
+ const payload = msg.payload;
335
+ this.studioClient = {
336
+ ws,
337
+ connectedAt: Date.now(),
338
+ studioVersion: payload.studioVersion,
339
+ placeId: payload.placeId,
340
+ placeName: payload.placeName
341
+ };
342
+ this.emit("studio:connected", payload);
343
+ this.broadcastToControllers("studio:connected", payload);
344
+ }
345
+ addController(ws) {
346
+ this.controllers.add(ws);
347
+ this.emit("controller:connected");
348
+ }
349
+ broadcastToControllers(type, payload) {
350
+ const msg = serializeMessage(createMessage(type, payload));
351
+ for (const ws of this.controllers) {
352
+ if (ws.readyState === WebSocket.OPEN) {
353
+ ws.send(msg);
354
+ }
355
+ }
356
+ }
357
+ handleControllerMessage(controllerWs, msg) {
358
+ if (!this.studioClient || this.studioClient.ws.readyState !== WebSocket.OPEN) {
359
+ controllerWs.send(serializeMessage({
360
+ id: msg.id,
361
+ type: StudioMsg.RESPONSE,
362
+ payload: { error: "Studio not connected" },
363
+ ts: Date.now()
364
+ }));
365
+ return;
366
+ }
367
+ this.relayRequests.set(msg.id, controllerWs);
368
+ this.studioClient.ws.send(serializeMessage(msg));
369
+ }
370
+ handleStudioMessage(msg) {
371
+ if (msg.type === StudioMsg.CONNECTED) {
372
+ const payload = msg.payload;
373
+ if (this.studioClient) {
374
+ this.studioClient.studioVersion = payload.studioVersion;
375
+ this.studioClient.placeId = payload.placeId;
376
+ this.studioClient.placeName = payload.placeName;
377
+ }
378
+ this.emit("studio:connected", payload);
379
+ this.broadcastToControllers("studio:connected", payload);
380
+ return;
381
+ }
382
+ if (msg.type === StudioMsg.RESPONSE) {
383
+ const controllerWs = this.relayRequests.get(msg.id);
384
+ if (controllerWs && controllerWs.readyState === WebSocket.OPEN) {
385
+ controllerWs.send(serializeMessage(msg));
386
+ this.relayRequests.delete(msg.id);
387
+ return;
388
+ }
389
+ const pending = this.pendingRequests.get(msg.id);
390
+ if (pending) {
391
+ clearTimeout(pending.timer);
392
+ pending.resolve(msg);
393
+ this.pendingRequests.delete(msg.id);
394
+ }
395
+ return;
396
+ }
397
+ this.emit(msg.type, msg.payload);
398
+ this.emit("message", msg);
399
+ this.broadcastToControllers(msg.type, msg.payload);
400
+ }
401
+ sendRequest(type, payload, timeoutMs = 1e4) {
402
+ return new Promise((resolve, reject) => {
403
+ if (!this.studioClient || this.studioClient.ws.readyState !== WebSocket.OPEN) {
404
+ reject(new Error("No Studio connection"));
405
+ return;
406
+ }
407
+ const msg = createMessage(type, payload);
408
+ const timer = setTimeout(() => {
409
+ this.pendingRequests.delete(msg.id);
410
+ reject(new Error(`Request timed out: ${type}`));
411
+ }, timeoutMs);
412
+ this.pendingRequests.set(msg.id, {
413
+ resolve,
414
+ reject,
415
+ timer
416
+ });
417
+ this.studioClient.ws.send(serializeMessage(msg));
418
+ });
419
+ }
420
+ sendNotification(type, payload) {
421
+ if (!this.studioClient || this.studioClient.ws.readyState !== WebSocket.OPEN) {
422
+ return;
423
+ }
424
+ const msg = createMessage(type, payload);
425
+ this.studioClient.ws.send(serializeMessage(msg));
426
+ }
427
+ isConnected() {
428
+ return this.studioClient !== null && this.studioClient.ws.readyState === WebSocket.OPEN;
429
+ }
430
+ getConnectionInfo() {
431
+ return this.studioClient;
432
+ }
433
+ getPort() {
434
+ return this.port;
435
+ }
436
+ stop() {
437
+ return new Promise((resolve) => {
438
+ if (this.studioClient) {
439
+ this.studioClient.ws.close();
440
+ this.studioClient = null;
441
+ }
442
+ for (const ws of this.controllers) {
443
+ ws.close();
444
+ }
445
+ this.controllers.clear();
446
+ for (const [, pending] of this.pendingRequests) {
447
+ clearTimeout(pending.timer);
448
+ pending.reject(new Error("Server shutting down"));
449
+ }
450
+ this.pendingRequests.clear();
451
+ this.relayRequests.clear();
452
+ if (this.wss) {
453
+ this.wss.close(() => resolve());
454
+ } else {
455
+ resolve();
456
+ }
457
+ });
458
+ }
459
+ };
460
+ }
461
+ });
462
+
463
+ // src/memory/store.ts
464
+ var store_exports = {};
465
+ __export(store_exports, {
466
+ closeMemoryStore: () => closeMemoryStore,
467
+ decayRelevance: () => decayRelevance,
468
+ getAllFacts: () => getAllFacts,
469
+ getDb: () => getDb,
470
+ getProjectSummaries: () => getProjectSummaries,
471
+ getScriptEntry: () => getScriptEntry,
472
+ getScriptIndex: () => getScriptIndex,
473
+ getSessionMessages: () => getSessionMessages,
474
+ initMemoryStore: () => initMemoryStore,
475
+ recallFacts: () => recallFacts,
476
+ storeFact: () => storeFact,
477
+ storeMessage: () => storeMessage,
478
+ storeSummary: () => storeSummary,
479
+ upsertScript: () => upsertScript
480
+ });
481
+ function persist() {
482
+ if (!db) return;
483
+ const data = db.export();
484
+ fs4.writeFileSync(dbPath, Buffer.from(data));
485
+ }
486
+ async function initMemoryStore() {
487
+ if (db) return;
488
+ dbPath = getDbPath();
489
+ const SQL = await initSqlJs();
490
+ if (fs4.existsSync(dbPath)) {
491
+ const buffer = fs4.readFileSync(dbPath);
492
+ db = new SQL.Database(buffer);
493
+ } else {
494
+ db = new SQL.Database();
495
+ }
496
+ db.run(SCHEMA);
497
+ persist();
498
+ }
499
+ function getDb() {
500
+ if (!db) throw new Error("Memory store not initialized. Call initMemoryStore() first.");
501
+ return db;
502
+ }
503
+ function closeMemoryStore() {
504
+ if (db) {
505
+ persist();
506
+ db.close();
507
+ db = null;
508
+ }
509
+ }
510
+ function storeFact(fact) {
511
+ const d = getDb();
512
+ d.run(
513
+ `INSERT INTO facts (project_id, content, category, source, relevance, created_at, accessed_at)
514
+ VALUES (?, ?, ?, ?, ?, ?, ?)`,
515
+ [
516
+ fact.projectId,
517
+ fact.content,
518
+ fact.category,
519
+ fact.source ?? null,
520
+ fact.relevance,
521
+ fact.createdAt,
522
+ fact.accessedAt
523
+ ]
524
+ );
525
+ persist();
526
+ const stmt = d.prepare("SELECT last_insert_rowid()");
527
+ stmt.step();
528
+ const row = stmt.get();
529
+ stmt.free();
530
+ return row?.[0] ?? 0;
531
+ }
532
+ function recallFacts(projectId, query, limit = 10) {
533
+ const d = getDb();
534
+ const keywords = query.toLowerCase().split(/\s+/).filter((w) => w.length > 2);
535
+ if (keywords.length === 0) {
536
+ const result2 = d.exec(
537
+ `SELECT * FROM facts WHERE project_id = ? ORDER BY relevance DESC, accessed_at DESC LIMIT ?`,
538
+ [projectId, limit]
539
+ );
540
+ return rowsToFacts(result2);
541
+ }
542
+ const likeClauses = keywords.map(() => `LOWER(content) LIKE ?`).join(" OR ");
543
+ const likeParams = keywords.map((k) => `%${k}%`);
544
+ const result = d.exec(
545
+ `SELECT * FROM facts WHERE project_id = ? AND (${likeClauses})
546
+ ORDER BY relevance DESC, accessed_at DESC LIMIT ?`,
547
+ [projectId, ...likeParams, limit]
548
+ );
549
+ const facts = rowsToFacts(result);
550
+ for (const fact of facts) {
551
+ if (fact.id) {
552
+ d.run(`UPDATE facts SET accessed_at = ? WHERE id = ?`, [Date.now(), fact.id]);
553
+ }
554
+ }
555
+ if (facts.length > 0) persist();
556
+ return facts;
557
+ }
558
+ function getAllFacts(projectId) {
559
+ const d = getDb();
560
+ const result = d.exec(
561
+ `SELECT * FROM facts WHERE project_id = ? ORDER BY relevance DESC`,
562
+ [projectId]
563
+ );
564
+ return rowsToFacts(result);
565
+ }
566
+ function rowsToFacts(result) {
567
+ if (!result[0]) return [];
568
+ return result[0].values.map((row) => ({
569
+ id: row[0],
570
+ projectId: row[1],
571
+ content: row[2],
572
+ category: row[3],
573
+ source: row[4],
574
+ relevance: row[5],
575
+ createdAt: row[6],
576
+ accessedAt: row[7]
577
+ }));
578
+ }
579
+ function storeMessage(msg) {
580
+ const d = getDb();
581
+ d.run(
582
+ `INSERT INTO messages (session_id, role, content, tool_name, created_at)
583
+ VALUES (?, ?, ?, ?, ?)`,
584
+ [msg.sessionId, msg.role, msg.content, msg.toolName ?? null, msg.createdAt]
585
+ );
586
+ persist();
587
+ }
588
+ function getSessionMessages(sessionId, limit = 100) {
589
+ const d = getDb();
590
+ const result = d.exec(
591
+ `SELECT * FROM messages WHERE session_id = ? ORDER BY created_at ASC LIMIT ?`,
592
+ [sessionId, limit]
593
+ );
594
+ if (!result[0]) return [];
595
+ return result[0].values.map((row) => ({
596
+ id: row[0],
597
+ sessionId: row[1],
598
+ role: row[2],
599
+ content: row[3],
600
+ toolName: row[4] || void 0,
601
+ createdAt: row[5]
602
+ }));
603
+ }
604
+ function storeSummary(summary) {
605
+ const d = getDb();
606
+ d.run(
607
+ `INSERT INTO summaries (project_id, session_id, summary, created_at)
608
+ VALUES (?, ?, ?, ?)`,
609
+ [summary.projectId, summary.sessionId, summary.summary, summary.createdAt]
610
+ );
611
+ persist();
612
+ }
613
+ function getProjectSummaries(projectId, limit = 20) {
614
+ const d = getDb();
615
+ const result = d.exec(
616
+ `SELECT * FROM summaries WHERE project_id = ? ORDER BY created_at DESC LIMIT ?`,
617
+ [projectId, limit]
618
+ );
619
+ if (!result[0]) return [];
620
+ return result[0].values.map((row) => ({
621
+ id: row[0],
622
+ projectId: row[1],
623
+ sessionId: row[2],
624
+ summary: row[3],
625
+ createdAt: row[4]
626
+ }));
627
+ }
628
+ function upsertScript(entry) {
629
+ const d = getDb();
630
+ const existing = d.exec(
631
+ `SELECT id FROM script_index WHERE project_id = ? AND path = ?`,
632
+ [entry.projectId, entry.path]
633
+ );
634
+ if (existing[0]?.values.length) {
635
+ d.run(
636
+ `UPDATE script_index SET class_name = ?, summary = ?, line_count = ?, last_hash = ?, updated_at = ?
637
+ WHERE project_id = ? AND path = ?`,
638
+ [
639
+ entry.className,
640
+ entry.summary ?? null,
641
+ entry.lineCount ?? null,
642
+ entry.lastHash ?? null,
643
+ entry.updatedAt,
644
+ entry.projectId,
645
+ entry.path
646
+ ]
647
+ );
648
+ } else {
649
+ d.run(
650
+ `INSERT INTO script_index (project_id, path, class_name, summary, line_count, last_hash, updated_at)
651
+ VALUES (?, ?, ?, ?, ?, ?, ?)`,
652
+ [
653
+ entry.projectId,
654
+ entry.path,
655
+ entry.className,
656
+ entry.summary ?? null,
657
+ entry.lineCount ?? null,
658
+ entry.lastHash ?? null,
659
+ entry.updatedAt
660
+ ]
661
+ );
662
+ }
663
+ persist();
664
+ }
665
+ function getScriptIndex(projectId) {
666
+ const d = getDb();
667
+ const result = d.exec(
668
+ `SELECT path, summary, class_name FROM script_index WHERE project_id = ? ORDER BY path`,
669
+ [projectId]
670
+ );
671
+ if (!result[0]) return [];
672
+ return result[0].values.map((row) => ({
673
+ path: row[0],
674
+ summary: row[1] || void 0,
675
+ className: row[2]
676
+ }));
677
+ }
678
+ function getScriptEntry(projectId, scriptPath) {
679
+ const d = getDb();
680
+ const result = d.exec(
681
+ `SELECT * FROM script_index WHERE project_id = ? AND path = ?`,
682
+ [projectId, scriptPath]
683
+ );
684
+ if (!result[0]?.values.length) return null;
685
+ const row = result[0].values[0];
686
+ return {
687
+ id: row[0],
688
+ projectId: row[1],
689
+ path: row[2],
690
+ className: row[3],
691
+ summary: row[4] || void 0,
692
+ lineCount: row[5] || void 0,
693
+ lastHash: row[6] || void 0,
694
+ updatedAt: row[7]
695
+ };
696
+ }
697
+ function decayRelevance(projectId, factor = 0.95) {
698
+ const d = getDb();
699
+ d.run(`UPDATE facts SET relevance = relevance * ? WHERE project_id = ?`, [factor, projectId]);
700
+ persist();
701
+ }
702
+ var db, dbPath, SCHEMA;
703
+ var init_store = __esm({
704
+ "src/memory/store.ts"() {
705
+ init_config();
706
+ db = null;
707
+ SCHEMA = `
708
+ CREATE TABLE IF NOT EXISTS facts (
709
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
710
+ project_id TEXT NOT NULL,
711
+ content TEXT NOT NULL,
712
+ category TEXT NOT NULL,
713
+ source TEXT,
714
+ relevance REAL DEFAULT 1.0,
715
+ created_at INTEGER NOT NULL,
716
+ accessed_at INTEGER NOT NULL
717
+ );
718
+
719
+ CREATE TABLE IF NOT EXISTS summaries (
720
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
721
+ project_id TEXT NOT NULL,
722
+ session_id TEXT NOT NULL,
723
+ summary TEXT NOT NULL,
724
+ created_at INTEGER NOT NULL
725
+ );
726
+
727
+ CREATE TABLE IF NOT EXISTS messages (
728
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
729
+ session_id TEXT NOT NULL,
730
+ role TEXT NOT NULL,
731
+ content TEXT NOT NULL,
732
+ tool_name TEXT,
733
+ created_at INTEGER NOT NULL
734
+ );
735
+
736
+ CREATE TABLE IF NOT EXISTS script_index (
737
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
738
+ project_id TEXT NOT NULL,
739
+ path TEXT NOT NULL,
740
+ class_name TEXT NOT NULL,
741
+ summary TEXT,
742
+ line_count INTEGER,
743
+ last_hash TEXT,
744
+ updated_at INTEGER NOT NULL
745
+ );
746
+
747
+ CREATE INDEX IF NOT EXISTS idx_facts_project ON facts(project_id);
748
+ CREATE INDEX IF NOT EXISTS idx_facts_category ON facts(project_id, category);
749
+ CREATE INDEX IF NOT EXISTS idx_scripts_project ON script_index(project_id);
750
+ CREATE INDEX IF NOT EXISTS idx_messages_session ON messages(session_id);
751
+ CREATE INDEX IF NOT EXISTS idx_summaries_project ON summaries(project_id);
752
+ `;
753
+ }
754
+ });
755
+
756
+ // src/tools/studio/read-script.ts
757
+ var read_script_exports = {};
758
+ __export(read_script_exports, {
759
+ tool: () => tool
760
+ });
761
+ var tool;
762
+ var init_read_script = __esm({
763
+ "src/tools/studio/read-script.ts"() {
764
+ init_protocol();
765
+ tool = {
766
+ name: "read_script",
767
+ description: 'Read the source code of a script in Roblox Studio. Provide the full instance path (e.g., "ServerScriptService.GameManager").',
768
+ parameters: {
769
+ type: "object",
770
+ properties: {
771
+ path: {
772
+ type: "string",
773
+ description: 'Full instance path of the script (e.g., "ServerScriptService.GameManager")'
774
+ }
775
+ },
776
+ required: ["path"]
777
+ },
778
+ async execute(params, ctx) {
779
+ if (!ctx.isStudioConnected()) {
780
+ return { success: false, error: "Studio is not connected" };
781
+ }
782
+ const path4 = params.path;
783
+ const response = await ctx.sendToStudio(CliMsg.GET_SCRIPT, { path: path4 });
784
+ if (!response.payload) {
785
+ return { success: false, error: `Script not found: ${path4}` };
786
+ }
787
+ return {
788
+ success: true,
789
+ data: {
790
+ path: response.payload.path,
791
+ className: response.payload.className,
792
+ source: response.payload.source,
793
+ lineCount: response.payload.lineCount
794
+ }
795
+ };
796
+ }
797
+ };
798
+ }
799
+ });
800
+
801
+ // src/tools/studio/edit-script.ts
802
+ var edit_script_exports = {};
803
+ __export(edit_script_exports, {
804
+ tool: () => tool2
805
+ });
806
+ var tool2;
807
+ var init_edit_script = __esm({
808
+ "src/tools/studio/edit-script.ts"() {
809
+ init_protocol();
810
+ tool2 = {
811
+ name: "edit_script",
812
+ description: "Replace the entire source code of a script in Roblox Studio. Provide the full path and the new source code.",
813
+ parameters: {
814
+ type: "object",
815
+ properties: {
816
+ path: {
817
+ type: "string",
818
+ description: "Full instance path of the script"
819
+ },
820
+ source: {
821
+ type: "string",
822
+ description: "The new complete source code for the script"
823
+ }
824
+ },
825
+ required: ["path", "source"]
826
+ },
827
+ async execute(params, ctx) {
828
+ if (!ctx.isStudioConnected()) {
829
+ return { success: false, error: "Studio is not connected" };
830
+ }
831
+ const path4 = params.path;
832
+ const source = params.source;
833
+ const response = await ctx.sendToStudio(CliMsg.SET_SCRIPT, { path: path4, source });
834
+ const payload = response.payload;
835
+ if (!payload.success) {
836
+ return { success: false, error: payload.error ?? "Failed to edit script" };
837
+ }
838
+ return { success: true, data: { path: path4, linesWritten: source.split("\n").length } };
839
+ }
840
+ };
841
+ }
842
+ });
843
+
844
+ // src/tools/studio/run-code.ts
845
+ var run_code_exports = {};
846
+ __export(run_code_exports, {
847
+ tool: () => tool3
848
+ });
849
+ var tool3;
850
+ var init_run_code = __esm({
851
+ "src/tools/studio/run-code.ts"() {
852
+ init_protocol();
853
+ tool3 = {
854
+ name: "run_code",
855
+ description: "Execute arbitrary Luau code in Roblox Studio and return the output. The code runs in the Studio plugin context with full API access.",
856
+ parameters: {
857
+ type: "object",
858
+ properties: {
859
+ code: {
860
+ type: "string",
861
+ description: "Luau code to execute in Studio"
862
+ }
863
+ },
864
+ required: ["code"]
865
+ },
866
+ async execute(params, ctx) {
867
+ if (!ctx.isStudioConnected()) {
868
+ return { success: false, error: "Studio is not connected" };
869
+ }
870
+ const code = params.code;
871
+ const response = await ctx.sendToStudio(CliMsg.RUN_CODE, { code });
872
+ const payload = response.payload;
873
+ if (payload.error) {
874
+ return { success: false, error: payload.error, data: { output: payload.output } };
875
+ }
876
+ return {
877
+ success: true,
878
+ data: { output: payload.output, duration: payload.duration }
879
+ };
880
+ }
881
+ };
882
+ }
883
+ });
884
+
885
+ // src/tools/studio/get-explorer.ts
886
+ var get_explorer_exports = {};
887
+ __export(get_explorer_exports, {
888
+ tool: () => tool4
889
+ });
890
+ var tool4;
891
+ var init_get_explorer = __esm({
892
+ "src/tools/studio/get-explorer.ts"() {
893
+ init_protocol();
894
+ tool4 = {
895
+ name: "get_explorer",
896
+ description: "Get the DataModel explorer tree from Roblox Studio. Optionally filter by a root path and max depth.",
897
+ parameters: {
898
+ type: "object",
899
+ properties: {
900
+ rootPath: {
901
+ type: "string",
902
+ description: 'Root path to start from (e.g., "ServerScriptService"). Omit for entire tree.'
903
+ },
904
+ maxDepth: {
905
+ type: "number",
906
+ description: "Maximum depth of the tree (default: 3)",
907
+ default: 3
908
+ }
909
+ }
910
+ },
911
+ async execute(params, ctx) {
912
+ if (!ctx.isStudioConnected()) {
913
+ return { success: false, error: "Studio is not connected" };
914
+ }
915
+ const response = await ctx.sendToStudio(CliMsg.GET_EXPLORER, {
916
+ rootPath: params.rootPath ?? null,
917
+ maxDepth: params.maxDepth ?? 3
918
+ });
919
+ return { success: true, data: response.payload };
920
+ }
921
+ };
922
+ }
923
+ });
924
+
925
+ // src/tools/studio/get-properties.ts
926
+ var get_properties_exports = {};
927
+ __export(get_properties_exports, {
928
+ tool: () => tool5
929
+ });
930
+ var tool5;
931
+ var init_get_properties = __esm({
932
+ "src/tools/studio/get-properties.ts"() {
933
+ init_protocol();
934
+ tool5 = {
935
+ name: "get_properties",
936
+ description: "Get all properties of an instance in Roblox Studio by its path.",
937
+ parameters: {
938
+ type: "object",
939
+ properties: {
940
+ path: {
941
+ type: "string",
942
+ description: 'Full instance path (e.g., "Workspace.Part")'
943
+ }
944
+ },
945
+ required: ["path"]
946
+ },
947
+ async execute(params, ctx) {
948
+ if (!ctx.isStudioConnected()) {
949
+ return { success: false, error: "Studio is not connected" };
950
+ }
951
+ const response = await ctx.sendToStudio(
952
+ CliMsg.GET_PROPERTIES,
953
+ { path: params.path }
954
+ );
955
+ return { success: true, data: response.payload };
956
+ }
957
+ };
958
+ }
959
+ });
960
+
961
+ // src/tools/studio/set-properties.ts
962
+ var set_properties_exports = {};
963
+ __export(set_properties_exports, {
964
+ tool: () => tool6
965
+ });
966
+ var tool6;
967
+ var init_set_properties = __esm({
968
+ "src/tools/studio/set-properties.ts"() {
969
+ init_protocol();
970
+ tool6 = {
971
+ name: "set_properties",
972
+ description: "Set properties on an instance in Roblox Studio.",
973
+ parameters: {
974
+ type: "object",
975
+ properties: {
976
+ path: {
977
+ type: "string",
978
+ description: 'Full instance path (e.g., "Workspace.Part")'
979
+ },
980
+ properties: {
981
+ type: "object",
982
+ description: 'Key-value pairs of properties to set (e.g., {"Name": "MyPart", "BrickColor": "Bright red"})'
983
+ }
984
+ },
985
+ required: ["path", "properties"]
986
+ },
987
+ async execute(params, ctx) {
988
+ if (!ctx.isStudioConnected()) {
989
+ return { success: false, error: "Studio is not connected" };
990
+ }
991
+ const response = await ctx.sendToStudio(CliMsg.SET_PROPERTIES, {
992
+ path: params.path,
993
+ properties: params.properties
994
+ });
995
+ const payload = response.payload;
996
+ if (!payload.success) {
997
+ return { success: false, error: payload.error ?? "Failed to set properties" };
998
+ }
999
+ return { success: true, data: { path: params.path, updated: Object.keys(params.properties) } };
1000
+ }
1001
+ };
1002
+ }
1003
+ });
1004
+
1005
+ // src/tools/studio/insert-instance.ts
1006
+ var insert_instance_exports = {};
1007
+ __export(insert_instance_exports, {
1008
+ tool: () => tool7
1009
+ });
1010
+ var tool7;
1011
+ var init_insert_instance = __esm({
1012
+ "src/tools/studio/insert-instance.ts"() {
1013
+ init_protocol();
1014
+ tool7 = {
1015
+ name: "insert_instance",
1016
+ description: "Insert a new instance into the Roblox Studio DataModel.",
1017
+ parameters: {
1018
+ type: "object",
1019
+ properties: {
1020
+ className: {
1021
+ type: "string",
1022
+ description: 'Class name of the instance to create (e.g., "Part", "Script", "ModuleScript")'
1023
+ },
1024
+ parentPath: {
1025
+ type: "string",
1026
+ description: 'Full path of the parent instance (e.g., "Workspace", "ServerScriptService")'
1027
+ },
1028
+ name: {
1029
+ type: "string",
1030
+ description: "Name for the new instance"
1031
+ },
1032
+ properties: {
1033
+ type: "object",
1034
+ description: "Optional properties to set on the new instance"
1035
+ }
1036
+ },
1037
+ required: ["className", "parentPath", "name"]
1038
+ },
1039
+ async execute(params, ctx) {
1040
+ if (!ctx.isStudioConnected()) {
1041
+ return { success: false, error: "Studio is not connected" };
1042
+ }
1043
+ const response = await ctx.sendToStudio(CliMsg.INSERT_INSTANCE, {
1044
+ className: params.className,
1045
+ parentPath: params.parentPath,
1046
+ name: params.name,
1047
+ properties: params.properties ?? {}
1048
+ });
1049
+ const payload = response.payload;
1050
+ if (!payload.success) {
1051
+ return { success: false, error: payload.error ?? "Failed to insert instance" };
1052
+ }
1053
+ return { success: true, data: { path: payload.path } };
1054
+ }
1055
+ };
1056
+ }
1057
+ });
1058
+
1059
+ // src/tools/studio/delete-instance.ts
1060
+ var delete_instance_exports = {};
1061
+ __export(delete_instance_exports, {
1062
+ tool: () => tool8
1063
+ });
1064
+ var tool8;
1065
+ var init_delete_instance = __esm({
1066
+ "src/tools/studio/delete-instance.ts"() {
1067
+ init_protocol();
1068
+ tool8 = {
1069
+ name: "delete_instance",
1070
+ description: "Delete an instance from the Roblox Studio DataModel by path.",
1071
+ parameters: {
1072
+ type: "object",
1073
+ properties: {
1074
+ path: {
1075
+ type: "string",
1076
+ description: 'Full instance path to delete (e.g., "Workspace.OldPart")'
1077
+ }
1078
+ },
1079
+ required: ["path"]
1080
+ },
1081
+ async execute(params, ctx) {
1082
+ if (!ctx.isStudioConnected()) {
1083
+ return { success: false, error: "Studio is not connected" };
1084
+ }
1085
+ const response = await ctx.sendToStudio(CliMsg.DELETE_INSTANCE, { path: params.path });
1086
+ const payload = response.payload;
1087
+ if (!payload.success) {
1088
+ return { success: false, error: payload.error ?? "Failed to delete instance" };
1089
+ }
1090
+ return { success: true, data: { deleted: params.path } };
1091
+ }
1092
+ };
1093
+ }
1094
+ });
1095
+
1096
+ // src/tools/studio/get-output.ts
1097
+ var get_output_exports = {};
1098
+ __export(get_output_exports, {
1099
+ tool: () => tool9
1100
+ });
1101
+ var tool9;
1102
+ var init_get_output = __esm({
1103
+ "src/tools/studio/get-output.ts"() {
1104
+ init_protocol();
1105
+ tool9 = {
1106
+ name: "get_output",
1107
+ description: "Get recent output log entries from Roblox Studio (prints, warnings, errors).",
1108
+ parameters: {
1109
+ type: "object",
1110
+ properties: {
1111
+ limit: {
1112
+ type: "number",
1113
+ description: "Maximum number of log entries to return (default: 50)",
1114
+ default: 50
1115
+ },
1116
+ level: {
1117
+ type: "string",
1118
+ description: "Filter by log level",
1119
+ enum: ["info", "warn", "error"]
1120
+ }
1121
+ }
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.GET_OUTPUT, {
1128
+ limit: params.limit ?? 50,
1129
+ level: params.level ?? null
1130
+ });
1131
+ return { success: true, data: response.payload };
1132
+ }
1133
+ };
1134
+ }
1135
+ });
1136
+
1137
+ // src/tools/studio/get-selection.ts
1138
+ var get_selection_exports = {};
1139
+ __export(get_selection_exports, {
1140
+ tool: () => tool10
1141
+ });
1142
+ var tool10;
1143
+ var init_get_selection = __esm({
1144
+ "src/tools/studio/get-selection.ts"() {
1145
+ init_protocol();
1146
+ tool10 = {
1147
+ name: "get_selection",
1148
+ description: "Get the currently selected instances in Roblox Studio.",
1149
+ parameters: {
1150
+ type: "object",
1151
+ properties: {}
1152
+ },
1153
+ async execute(_params, ctx) {
1154
+ if (!ctx.isStudioConnected()) {
1155
+ return { success: false, error: "Studio is not connected" };
1156
+ }
1157
+ const response = await ctx.sendToStudio(CliMsg.GET_SELECTION, {});
1158
+ return { success: true, data: response.payload };
1159
+ }
1160
+ };
1161
+ }
1162
+ });
1163
+
1164
+ // src/tools/studio/search-scripts.ts
1165
+ var search_scripts_exports = {};
1166
+ __export(search_scripts_exports, {
1167
+ tool: () => tool11
1168
+ });
1169
+ var tool11;
1170
+ var init_search_scripts = __esm({
1171
+ "src/tools/studio/search-scripts.ts"() {
1172
+ init_protocol();
1173
+ tool11 = {
1174
+ name: "search_scripts",
1175
+ description: "Search across all scripts in Roblox Studio for a text pattern. Returns matching lines with context.",
1176
+ parameters: {
1177
+ type: "object",
1178
+ properties: {
1179
+ query: {
1180
+ type: "string",
1181
+ description: "Text or pattern to search for in script sources"
1182
+ },
1183
+ maxResults: {
1184
+ type: "number",
1185
+ description: "Maximum number of script results (default: 20)",
1186
+ default: 20
1187
+ }
1188
+ },
1189
+ required: ["query"]
1190
+ },
1191
+ async execute(params, ctx) {
1192
+ if (!ctx.isStudioConnected()) {
1193
+ return { success: false, error: "Studio is not connected" };
1194
+ }
1195
+ const response = await ctx.sendToStudio(CliMsg.SEARCH_SCRIPTS, {
1196
+ query: params.query,
1197
+ maxResults: params.maxResults ?? 20
1198
+ });
1199
+ return { success: true, data: response.payload };
1200
+ }
1201
+ };
1202
+ }
1203
+ });
1204
+
1205
+ // src/tools/studio/run-tests.ts
1206
+ var run_tests_exports = {};
1207
+ __export(run_tests_exports, {
1208
+ tool: () => tool12
1209
+ });
1210
+ var tool12;
1211
+ var init_run_tests = __esm({
1212
+ "src/tools/studio/run-tests.ts"() {
1213
+ init_protocol();
1214
+ tool12 = {
1215
+ name: "run_tests",
1216
+ description: "Run tests in Roblox Studio using StudioTestService. Can run all tests or a specific test by name.",
1217
+ parameters: {
1218
+ type: "object",
1219
+ properties: {
1220
+ testName: {
1221
+ type: "string",
1222
+ description: "Specific test name to run. Omit to run all tests."
1223
+ }
1224
+ }
1225
+ },
1226
+ async execute(params, ctx) {
1227
+ if (!ctx.isStudioConnected()) {
1228
+ return { success: false, error: "Studio is not connected" };
1229
+ }
1230
+ const response = await ctx.sendToStudio(CliMsg.RUN_TESTS, {
1231
+ testName: params.testName ?? null
1232
+ });
1233
+ const results = response.payload.results ?? [];
1234
+ const passed = results.filter((r) => r.passed).length;
1235
+ const failed = results.filter((r) => !r.passed).length;
1236
+ return {
1237
+ success: failed === 0,
1238
+ data: { total: results.length, passed, failed, results }
1239
+ };
1240
+ }
1241
+ };
1242
+ }
1243
+ });
1244
+
1245
+ // src/tools/studio/get-class-info.ts
1246
+ var get_class_info_exports = {};
1247
+ __export(get_class_info_exports, {
1248
+ tool: () => tool13
1249
+ });
1250
+ var tool13;
1251
+ var init_get_class_info = __esm({
1252
+ "src/tools/studio/get-class-info.ts"() {
1253
+ init_protocol();
1254
+ tool13 = {
1255
+ name: "get_class_info",
1256
+ 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).",
1257
+ parameters: {
1258
+ type: "object",
1259
+ properties: {
1260
+ className: {
1261
+ type: "string",
1262
+ description: 'Roblox class name, e.g. "Frame", "TextLabel", "Part", "MeshPart", "Camera"'
1263
+ }
1264
+ },
1265
+ required: ["className"]
1266
+ },
1267
+ async execute(params, ctx) {
1268
+ if (!ctx.isStudioConnected()) {
1269
+ return { success: false, error: "Studio is not connected" };
1270
+ }
1271
+ const response = await ctx.sendToStudio(CliMsg.GET_REFLECTION, { className: params.className });
1272
+ const payload = response.payload;
1273
+ if (payload.error) {
1274
+ return { success: false, error: payload.error };
1275
+ }
1276
+ return { success: true, data: payload };
1277
+ }
1278
+ };
1279
+ }
1280
+ });
1281
+
1282
+ // src/tools/studio/create-ui.ts
1283
+ var create_ui_exports = {};
1284
+ __export(create_ui_exports, {
1285
+ tool: () => tool14
1286
+ });
1287
+ var tool14;
1288
+ var init_create_ui = __esm({
1289
+ "src/tools/studio/create-ui.ts"() {
1290
+ init_protocol();
1291
+ tool14 = {
1292
+ name: "create_ui",
1293
+ 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}',
1294
+ parameters: {
1295
+ type: "object",
1296
+ properties: {
1297
+ parent: {
1298
+ type: "string",
1299
+ description: 'Parent path: "StarterGui", "PlayerGui", or any instance path. Default: StarterGui'
1300
+ },
1301
+ tree: {
1302
+ type: "object",
1303
+ description: 'The root UI node. Each node has: ClassName (string), Name (string), any properties as key-value pairs, and Children (array of child nodes). 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]\n Font: "GothamBold", "SourceSans", etc. (Enum.Font names)\n Enums: string name like "Center", "Left", "Fit"\n FontFace: {Family, Weight, Style}'
1304
+ }
1305
+ },
1306
+ required: ["tree"]
1307
+ },
1308
+ async execute(params, ctx) {
1309
+ if (!ctx.isStudioConnected()) {
1310
+ return { success: false, error: "Studio is not connected" };
1311
+ }
1312
+ const response = await ctx.sendToStudio(CliMsg.BUILD_UI, {
1313
+ parent: params.parent ?? "StarterGui",
1314
+ tree: params.tree
1315
+ });
1316
+ const payload = response.payload;
1317
+ if (!payload.success) {
1318
+ return { success: false, error: payload.error ?? "UI build failed" };
1319
+ }
1320
+ return {
1321
+ success: true,
1322
+ data: { path: payload.path, instanceCount: payload.instanceCount }
1323
+ };
1324
+ }
1325
+ };
1326
+ }
1327
+ });
1328
+
1329
+ // src/tools/studio/execute-run-test.ts
1330
+ var execute_run_test_exports = {};
1331
+ __export(execute_run_test_exports, {
1332
+ tool: () => tool15
1333
+ });
1334
+ var tool15;
1335
+ var init_execute_run_test = __esm({
1336
+ "src/tools/studio/execute-run-test.ts"() {
1337
+ init_protocol();
1338
+ tool15 = {
1339
+ name: "execute_run_test",
1340
+ 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.",
1341
+ parameters: {
1342
+ type: "object",
1343
+ properties: {
1344
+ args: {
1345
+ 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."
1346
+ }
1347
+ }
1348
+ },
1349
+ async execute(params, ctx) {
1350
+ if (!ctx.isStudioConnected()) {
1351
+ return { success: false, error: "Studio is not connected" };
1352
+ }
1353
+ const response = await ctx.sendToStudio(
1354
+ CliMsg.EXECUTE_RUN_TEST,
1355
+ { args: params.args ?? null },
1356
+ 12e4
1357
+ );
1358
+ const payload = response.payload;
1359
+ if (!payload.success) {
1360
+ return { success: false, error: payload.error ?? "Run mode test failed" };
1361
+ }
1362
+ return {
1363
+ success: true,
1364
+ data: {
1365
+ result: payload.result,
1366
+ duration: payload.duration
1367
+ }
1368
+ };
1369
+ }
1370
+ };
1371
+ }
1372
+ });
1373
+
1374
+ // src/tools/studio/execute-play-test.ts
1375
+ var execute_play_test_exports = {};
1376
+ __export(execute_play_test_exports, {
1377
+ tool: () => tool16
1378
+ });
1379
+ var tool16;
1380
+ var init_execute_play_test = __esm({
1381
+ "src/tools/studio/execute-play-test.ts"() {
1382
+ init_protocol();
1383
+ tool16 = {
1384
+ name: "execute_play_test",
1385
+ 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.",
1386
+ parameters: {
1387
+ type: "object",
1388
+ properties: {
1389
+ args: {
1390
+ 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."
1391
+ }
1392
+ }
1393
+ },
1394
+ async execute(params, ctx) {
1395
+ if (!ctx.isStudioConnected()) {
1396
+ return { success: false, error: "Studio is not connected" };
1397
+ }
1398
+ const response = await ctx.sendToStudio(
1399
+ CliMsg.EXECUTE_PLAY_TEST,
1400
+ { args: params.args ?? null },
1401
+ 12e4
1402
+ );
1403
+ const payload = response.payload;
1404
+ if (!payload.success) {
1405
+ return { success: false, error: payload.error ?? "Play mode test failed" };
1406
+ }
1407
+ return {
1408
+ success: true,
1409
+ data: {
1410
+ result: payload.result,
1411
+ duration: payload.duration
1412
+ }
1413
+ };
1414
+ }
1415
+ };
1416
+ }
1417
+ });
1418
+
1419
+ // src/tools/studio/create-part.ts
1420
+ var create_part_exports = {};
1421
+ __export(create_part_exports, {
1422
+ tool: () => tool17
1423
+ });
1424
+ var tool17;
1425
+ var init_create_part = __esm({
1426
+ "src/tools/studio/create-part.ts"() {
1427
+ init_protocol();
1428
+ tool17 = {
1429
+ name: "create_part",
1430
+ 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.",
1431
+ parameters: {
1432
+ type: "object",
1433
+ properties: {
1434
+ name: { type: "string", description: "Name for the part" },
1435
+ parent: {
1436
+ type: "string",
1437
+ description: 'Parent path (default: "Workspace")'
1438
+ },
1439
+ shape: {
1440
+ type: "string",
1441
+ enum: ["Block", "Ball", "Cylinder", "Wedge", "CornerWedge"],
1442
+ description: "Part shape (default: Block)"
1443
+ },
1444
+ position: {
1445
+ type: "object",
1446
+ properties: { x: { type: "number" }, y: { type: "number" }, z: { type: "number" } },
1447
+ description: "World position {x, y, z}"
1448
+ },
1449
+ size: {
1450
+ type: "object",
1451
+ properties: { x: { type: "number" }, y: { type: "number" }, z: { type: "number" } },
1452
+ description: "Size in studs {x, y, z} (default: {4, 1, 2})"
1453
+ },
1454
+ color: {
1455
+ type: "string",
1456
+ description: 'Color as hex "#FF0000", BrickColor name "Bright red", or RGB "rgb(255,0,0)"'
1457
+ },
1458
+ material: {
1459
+ type: "string",
1460
+ description: "Material name: Plastic, Wood, Grass, Ice, Metal, Concrete, Sand, Neon, Glass, etc."
1461
+ },
1462
+ transparency: { type: "number", description: "0 (opaque) to 1 (invisible)" },
1463
+ anchored: { type: "boolean", description: "Whether the part is anchored (default: true)" },
1464
+ canCollide: { type: "boolean", description: "Whether the part has collision" }
1465
+ },
1466
+ required: ["name"]
1467
+ },
1468
+ async execute(params, ctx) {
1469
+ if (!ctx.isStudioConnected()) {
1470
+ return { success: false, error: "Studio is not connected" };
1471
+ }
1472
+ const parent = params.parent || "Workspace";
1473
+ const name = params.name;
1474
+ const pos = params.position;
1475
+ const size = params.size;
1476
+ const properties = {};
1477
+ if (pos) properties.Position = { X: pos.x ?? 0, Y: pos.y ?? 0, Z: pos.z ?? 0 };
1478
+ if (size) properties.Size = { X: size.x ?? 4, Y: size.y ?? 1, Z: size.z ?? 2 };
1479
+ if (params.color) {
1480
+ const c = params.color;
1481
+ if (c.startsWith("#")) {
1482
+ properties.Color = c;
1483
+ } else if (c.startsWith("rgb")) {
1484
+ const m = c.match(/(\d+)/g);
1485
+ if (m && m.length >= 3) {
1486
+ properties.Color = { red: parseInt(m[0]), green: parseInt(m[1]), blue: parseInt(m[2]) };
1487
+ }
1488
+ } else {
1489
+ properties.BrickColor = c;
1490
+ }
1491
+ }
1492
+ if (params.material) properties.Material = params.material;
1493
+ if (params.transparency !== void 0) properties.Transparency = params.transparency;
1494
+ if (params.anchored !== void 0) properties.Anchored = params.anchored;
1495
+ else properties.Anchored = true;
1496
+ if (params.canCollide !== void 0) properties.CanCollide = params.canCollide;
1497
+ if (params.shape && params.shape !== "Block") properties.Shape = params.shape;
1498
+ const className = params.shape === "Wedge" ? "WedgePart" : params.shape === "CornerWedge" ? "CornerWedgePart" : "Part";
1499
+ const res = await ctx.sendToStudio(CliMsg.INSERT_INSTANCE, {
1500
+ className,
1501
+ parentPath: parent,
1502
+ name,
1503
+ properties
1504
+ });
1505
+ const payload = res.payload;
1506
+ if (!payload.success) {
1507
+ return { success: false, error: payload.error ?? "Failed to create part" };
1508
+ }
1509
+ return {
1510
+ success: true,
1511
+ data: { path: payload.path, properties: Object.keys(properties) }
1512
+ };
1513
+ }
1514
+ };
1515
+ }
1516
+ });
1517
+
1518
+ // src/tools/studio/clone-instance.ts
1519
+ var clone_instance_exports = {};
1520
+ __export(clone_instance_exports, {
1521
+ tool: () => tool18
1522
+ });
1523
+ function buildCloneCode(params) {
1524
+ const path4 = params.path;
1525
+ const parts = path4.split(".");
1526
+ let resolve = "game";
1527
+ for (const p of parts) {
1528
+ resolve += `["${p}"]`;
1529
+ }
1530
+ const lines = [
1531
+ `local orig = ${resolve}`,
1532
+ `local clone = orig:Clone()`
1533
+ ];
1534
+ if (params.newName) {
1535
+ lines.push(`clone.Name = "${params.newName}"`);
1536
+ }
1537
+ if (params.newParent) {
1538
+ const parentParts = params.newParent.split(".");
1539
+ let parentResolve = "game";
1540
+ for (const p of parentParts) {
1541
+ parentResolve += `["${p}"]`;
1542
+ }
1543
+ lines.push(`clone.Parent = ${parentResolve}`);
1544
+ } else {
1545
+ lines.push(`clone.Parent = orig.Parent`);
1546
+ }
1547
+ const offset = params.offset;
1548
+ if (offset) {
1549
+ lines.push(`if clone:IsA("BasePart") then`);
1550
+ lines.push(` clone.Position = clone.Position + Vector3.new(${offset.x ?? 0}, ${offset.y ?? 0}, ${offset.z ?? 0})`);
1551
+ lines.push(`end`);
1552
+ }
1553
+ lines.push(`print("Cloned: " .. clone:GetFullName())`);
1554
+ return lines.join("\n");
1555
+ }
1556
+ var tool18;
1557
+ var init_clone_instance = __esm({
1558
+ "src/tools/studio/clone-instance.ts"() {
1559
+ init_protocol();
1560
+ tool18 = {
1561
+ name: "clone_instance",
1562
+ description: "Clone an existing instance in Roblox Studio and optionally reposition/rename it. Great for duplicating parts, models, or entire trees.",
1563
+ parameters: {
1564
+ type: "object",
1565
+ properties: {
1566
+ path: { type: "string", description: "Path of instance to clone" },
1567
+ newName: { type: "string", description: "Name for the clone (optional)" },
1568
+ newParent: { type: "string", description: "Parent path for the clone (optional, defaults to same parent)" },
1569
+ offset: {
1570
+ type: "object",
1571
+ properties: { x: { type: "number" }, y: { type: "number" }, z: { type: "number" } },
1572
+ description: "Position offset from original {x, y, z}"
1573
+ }
1574
+ },
1575
+ required: ["path"]
1576
+ },
1577
+ async execute(params, ctx) {
1578
+ if (!ctx.isStudioConnected()) {
1579
+ return { success: false, error: "Studio is not connected" };
1580
+ }
1581
+ const code = buildCloneCode(params);
1582
+ const res = await ctx.sendToStudio(CliMsg.RUN_CODE, { code });
1583
+ const payload = res.payload;
1584
+ if (!payload.success) {
1585
+ return { success: false, error: payload.error ?? "Failed to clone" };
1586
+ }
1587
+ return { success: true, data: { output: payload.output } };
1588
+ }
1589
+ };
1590
+ }
1591
+ });
1592
+
1593
+ // src/tools/studio/group-instances.ts
1594
+ var group_instances_exports = {};
1595
+ __export(group_instances_exports, {
1596
+ tool: () => tool19
1597
+ });
1598
+ var tool19;
1599
+ var init_group_instances = __esm({
1600
+ "src/tools/studio/group-instances.ts"() {
1601
+ init_protocol();
1602
+ tool19 = {
1603
+ name: "group_instances",
1604
+ 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).',
1605
+ parameters: {
1606
+ type: "object",
1607
+ properties: {
1608
+ paths: {
1609
+ type: "array",
1610
+ items: { type: "string" },
1611
+ description: "Array of instance paths to group together"
1612
+ },
1613
+ groupName: { type: "string", description: "Name for the new group" },
1614
+ groupClass: {
1615
+ type: "string",
1616
+ enum: ["Model", "Folder"],
1617
+ description: "Class for the group container (default: Model)"
1618
+ },
1619
+ parent: { type: "string", description: "Parent path (default: Workspace)" }
1620
+ },
1621
+ required: ["paths", "groupName"]
1622
+ },
1623
+ async execute(params, ctx) {
1624
+ if (!ctx.isStudioConnected()) {
1625
+ return { success: false, error: "Studio is not connected" };
1626
+ }
1627
+ const paths = params.paths;
1628
+ const groupName = params.groupName;
1629
+ const groupClass = params.groupClass || "Model";
1630
+ const parent = params.parent || "Workspace";
1631
+ const resolves = paths.map((p) => {
1632
+ const parts = p.split(".");
1633
+ let r = "game";
1634
+ for (const part of parts) r += `["${part}"]`;
1635
+ return r;
1636
+ });
1637
+ const parentParts = parent.split(".");
1638
+ let parentResolve = "game";
1639
+ for (const p of parentParts) parentResolve += `["${p}"]`;
1640
+ const code = [
1641
+ `local group = Instance.new("${groupClass}")`,
1642
+ `group.Name = "${groupName}"`,
1643
+ `group.Parent = ${parentResolve}`,
1644
+ ...resolves.map((r) => `${r}.Parent = group`),
1645
+ groupClass === "Model" ? `if group:IsA("Model") then group:MoveTo(group:GetBoundingBox().Position) end` : "",
1646
+ `print("Grouped ${paths.length} instances into: " .. group:GetFullName())`
1647
+ ].filter(Boolean).join("\n");
1648
+ const res = await ctx.sendToStudio(CliMsg.RUN_CODE, { code });
1649
+ const payload = res.payload;
1650
+ if (!payload.success) {
1651
+ return { success: false, error: payload.error ?? "Failed to group" };
1652
+ }
1653
+ return { success: true, data: { groupPath: `${parent}.${groupName}`, count: paths.length } };
1654
+ }
1655
+ };
1656
+ }
1657
+ });
1658
+
1659
+ // src/tools/studio/build-multiple.ts
1660
+ var build_multiple_exports = {};
1661
+ __export(build_multiple_exports, {
1662
+ tool: () => tool20
1663
+ });
1664
+ function generateBatchCode(parts, groupName, groupParent) {
1665
+ const lines = [];
1666
+ function resolveParent(path4) {
1667
+ const segs = path4.split(".");
1668
+ let r = "game";
1669
+ for (const s of segs) r += `["${s}"]`;
1670
+ return r;
1671
+ }
1672
+ if (groupName) {
1673
+ const gp = resolveParent(groupParent);
1674
+ lines.push(`local _group = Instance.new("Model")`);
1675
+ lines.push(`_group.Name = "${groupName}"`);
1676
+ lines.push(`_group.Parent = ${gp}`);
1677
+ }
1678
+ for (let i = 0; i < parts.length; i++) {
1679
+ const p = parts[i];
1680
+ const varName = `_p${i}`;
1681
+ const cn = p.className || (p.shape === "Wedge" ? "WedgePart" : "Part");
1682
+ lines.push(`local ${varName} = Instance.new("${cn}")`);
1683
+ lines.push(`${varName}.Name = "${p.name}"`);
1684
+ if (p.position) {
1685
+ lines.push(`${varName}.Position = Vector3.new(${p.position.x ?? 0}, ${p.position.y ?? 0}, ${p.position.z ?? 0})`);
1686
+ }
1687
+ if (p.size) {
1688
+ lines.push(`${varName}.Size = Vector3.new(${p.size.x ?? 4}, ${p.size.y ?? 1}, ${p.size.z ?? 2})`);
1689
+ }
1690
+ if (p.color) {
1691
+ if (p.color.startsWith("#")) {
1692
+ lines.push(`${varName}.Color = Color3.fromHex("${p.color}")`);
1693
+ } else {
1694
+ lines.push(`${varName}.BrickColor = BrickColor.new("${p.color}")`);
1695
+ }
1696
+ }
1697
+ if (p.material) {
1698
+ lines.push(`${varName}.Material = Enum.Material.${p.material}`);
1699
+ }
1700
+ if (p.transparency !== void 0) {
1701
+ lines.push(`${varName}.Transparency = ${p.transparency}`);
1702
+ }
1703
+ if (p.anchored !== void 0) {
1704
+ lines.push(`${varName}.Anchored = ${p.anchored}`);
1705
+ } else {
1706
+ lines.push(`${varName}.Anchored = true`);
1707
+ }
1708
+ if (p.shape && p.shape !== "Block" && cn === "Part") {
1709
+ lines.push(`${varName}.Shape = Enum.PartType.${p.shape}`);
1710
+ }
1711
+ if (groupName) {
1712
+ lines.push(`${varName}.Parent = _group`);
1713
+ } else {
1714
+ const parent = p.parent || "Workspace";
1715
+ lines.push(`${varName}.Parent = ${resolveParent(parent)}`);
1716
+ }
1717
+ }
1718
+ lines.push(`print("Built ${parts.length} parts${groupName ? ` in ${groupName}` : ""}")`);
1719
+ return lines.join("\n");
1720
+ }
1721
+ var tool20;
1722
+ var init_build_multiple = __esm({
1723
+ "src/tools/studio/build-multiple.ts"() {
1724
+ init_protocol();
1725
+ tool20 = {
1726
+ name: "build_multiple",
1727
+ 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.",
1728
+ parameters: {
1729
+ type: "object",
1730
+ properties: {
1731
+ parts: {
1732
+ type: "array",
1733
+ items: {
1734
+ type: "object",
1735
+ properties: {
1736
+ name: { type: "string", description: "Part name" },
1737
+ className: { type: "string", description: "ClassName (default: Part)" },
1738
+ parent: { type: "string", description: "Parent path (default: Workspace)" },
1739
+ position: {
1740
+ type: "object",
1741
+ properties: { x: { type: "number" }, y: { type: "number" }, z: { type: "number" } }
1742
+ },
1743
+ size: {
1744
+ type: "object",
1745
+ properties: { x: { type: "number" }, y: { type: "number" }, z: { type: "number" } }
1746
+ },
1747
+ color: { type: "string", description: 'Hex "#RRGGBB" or BrickColor name' },
1748
+ material: { type: "string" },
1749
+ transparency: { type: "number" },
1750
+ anchored: { type: "boolean" },
1751
+ shape: { type: "string", enum: ["Block", "Ball", "Cylinder", "Wedge"] }
1752
+ },
1753
+ required: ["name"]
1754
+ },
1755
+ description: "Array of part specifications"
1756
+ },
1757
+ groupName: {
1758
+ type: "string",
1759
+ description: "Optional: group all parts under a Model with this name"
1760
+ },
1761
+ groupParent: {
1762
+ type: "string",
1763
+ description: "Parent for the group Model (default: Workspace)"
1764
+ }
1765
+ },
1766
+ required: ["parts"]
1767
+ },
1768
+ async execute(params, ctx) {
1769
+ if (!ctx.isStudioConnected()) {
1770
+ return { success: false, error: "Studio is not connected" };
1771
+ }
1772
+ const parts = params.parts;
1773
+ const groupName = params.groupName;
1774
+ const groupParent = params.groupParent || "Workspace";
1775
+ const code = generateBatchCode(parts, groupName, groupParent);
1776
+ const res = await ctx.sendToStudio(CliMsg.RUN_CODE, { code });
1777
+ const payload = res.payload;
1778
+ if (!payload.success) {
1779
+ return { success: false, error: payload.error ?? "Batch build failed" };
1780
+ }
1781
+ return {
1782
+ success: true,
1783
+ data: {
1784
+ count: parts.length,
1785
+ group: groupName ?? null,
1786
+ output: payload.output
1787
+ }
1788
+ };
1789
+ }
1790
+ };
1791
+ }
1792
+ });
1793
+
1794
+ // src/tools/memory/recall.ts
1795
+ var recall_exports = {};
1796
+ __export(recall_exports, {
1797
+ tool: () => tool21
1798
+ });
1799
+ var tool21;
1800
+ var init_recall = __esm({
1801
+ "src/tools/memory/recall.ts"() {
1802
+ init_store();
1803
+ tool21 = {
1804
+ name: "recall_memory",
1805
+ description: "Search your memory for previously learned facts about the current project. Use this to recall architecture decisions, patterns, known bugs, or user preferences.",
1806
+ parameters: {
1807
+ type: "object",
1808
+ properties: {
1809
+ query: {
1810
+ type: "string",
1811
+ description: 'What to search for in memory (e.g., "player data storage", "combat system architecture")'
1812
+ },
1813
+ limit: {
1814
+ type: "number",
1815
+ description: "Max number of facts to return (default: 10)",
1816
+ default: 10
1817
+ }
1818
+ },
1819
+ required: ["query"]
1820
+ },
1821
+ async execute(params) {
1822
+ const query = params.query;
1823
+ const limit = params.limit ?? 10;
1824
+ const projectId = "current";
1825
+ const facts = recallFacts(projectId, query, limit);
1826
+ const summaries = getProjectSummaries(projectId, 5);
1827
+ return {
1828
+ success: true,
1829
+ data: {
1830
+ facts: facts.map((f) => ({ content: f.content, category: f.category })),
1831
+ recentSessions: summaries.map((s) => s.summary)
1832
+ }
1833
+ };
1834
+ }
1835
+ };
1836
+ }
1837
+ });
1838
+
1839
+ // src/tools/memory/remember.ts
1840
+ var remember_exports = {};
1841
+ __export(remember_exports, {
1842
+ tool: () => tool22
1843
+ });
1844
+ var tool22;
1845
+ var init_remember = __esm({
1846
+ "src/tools/memory/remember.ts"() {
1847
+ init_store();
1848
+ tool22 = {
1849
+ name: "remember",
1850
+ 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.",
1851
+ parameters: {
1852
+ type: "object",
1853
+ properties: {
1854
+ fact: {
1855
+ type: "string",
1856
+ description: 'The fact to remember (e.g., "PlayerData DataStore uses key format Player_{UserId}")'
1857
+ },
1858
+ category: {
1859
+ type: "string",
1860
+ description: "Category of the fact",
1861
+ enum: ["architecture", "pattern", "preference", "bug", "api", "general"]
1862
+ }
1863
+ },
1864
+ required: ["fact", "category"]
1865
+ },
1866
+ async execute(params) {
1867
+ const fact = params.fact;
1868
+ const category = params.category ?? "general";
1869
+ const now = Date.now();
1870
+ storeFact({
1871
+ projectId: "current",
1872
+ content: fact,
1873
+ category,
1874
+ source: "agent",
1875
+ relevance: 1,
1876
+ createdAt: now,
1877
+ accessedAt: now
1878
+ });
1879
+ return { success: true, data: { stored: fact, category } };
1880
+ }
1881
+ };
1882
+ }
1883
+ });
1884
+
1885
+ // src/tools/cloud/upload-asset.ts
1886
+ var upload_asset_exports = {};
1887
+ __export(upload_asset_exports, {
1888
+ tool: () => tool23
1889
+ });
1890
+ var tool23;
1891
+ var init_upload_asset = __esm({
1892
+ "src/tools/cloud/upload-asset.ts"() {
1893
+ tool23 = {
1894
+ name: "upload_asset",
1895
+ description: "Upload a local file (image, model, audio) to Roblox via the Open Cloud API. Requires an Open Cloud API key configured in dominus.",
1896
+ parameters: {
1897
+ type: "object",
1898
+ properties: {
1899
+ filePath: {
1900
+ type: "string",
1901
+ description: "Local file path of the asset to upload"
1902
+ },
1903
+ assetType: {
1904
+ type: "string",
1905
+ description: "Type of asset",
1906
+ enum: ["Image", "Audio", "Model"]
1907
+ },
1908
+ name: {
1909
+ type: "string",
1910
+ description: "Display name for the asset on Roblox"
1911
+ },
1912
+ description: {
1913
+ type: "string",
1914
+ description: "Description of the asset"
1915
+ }
1916
+ },
1917
+ required: ["filePath", "assetType", "name"]
1918
+ },
1919
+ async execute(params, ctx) {
1920
+ const apiKey = ctx.config.get("openCloudApiKey");
1921
+ if (!apiKey) {
1922
+ return {
1923
+ success: false,
1924
+ error: "Open Cloud API key not configured. Run: dominus config set openCloudApiKey <key>"
1925
+ };
1926
+ }
1927
+ const filePath = params.filePath;
1928
+ const assetType = params.assetType;
1929
+ const name = params.name;
1930
+ const description = params.description ?? "";
1931
+ if (!fs4.existsSync(filePath)) {
1932
+ return { success: false, error: `File not found: ${filePath}` };
1933
+ }
1934
+ const fileBuffer = fs4.readFileSync(filePath);
1935
+ const fileName = path3.basename(filePath);
1936
+ const typeMapping = {
1937
+ Image: "Decal",
1938
+ Audio: "Audio",
1939
+ Model: "Model"
1940
+ };
1941
+ try {
1942
+ const formData = new FormData();
1943
+ const blob = new Blob([fileBuffer]);
1944
+ formData.append("fileContent", blob, fileName);
1945
+ formData.append(
1946
+ "request",
1947
+ JSON.stringify({
1948
+ assetType: typeMapping[assetType] ?? assetType,
1949
+ displayName: name,
1950
+ description,
1951
+ creationContext: { creator: { userId: "me" } }
1952
+ })
1953
+ );
1954
+ const response = await fetch(
1955
+ "https://apis.roblox.com/assets/v1/assets",
1956
+ {
1957
+ method: "POST",
1958
+ headers: {
1959
+ "x-api-key": apiKey
1960
+ },
1961
+ body: formData
1962
+ }
1963
+ );
1964
+ if (!response.ok) {
1965
+ const text = await response.text();
1966
+ return { success: false, error: `Upload failed (${response.status}): ${text}` };
1967
+ }
1968
+ const data = await response.json();
1969
+ return {
1970
+ success: true,
1971
+ data: {
1972
+ assetId: data.assetId ?? data.id,
1973
+ operationId: data.operationId ?? data.path,
1974
+ name,
1975
+ type: assetType
1976
+ }
1977
+ };
1978
+ } catch (err) {
1979
+ return {
1980
+ success: false,
1981
+ error: `Upload error: ${err instanceof Error ? err.message : String(err)}`
1982
+ };
1983
+ }
1984
+ }
1985
+ };
1986
+ }
1987
+ });
1988
+
1989
+ // src/mcp/server.ts
1990
+ var server_exports = {};
1991
+ __export(server_exports, {
1992
+ startMcpServer: () => startMcpServer
1993
+ });
1994
+ async function startMcpServer() {
1995
+ const config = loadConfig();
1996
+ await initMemoryStore();
1997
+ wsServer = new DominusServer(config.port);
1998
+ await wsServer.start();
1999
+ const mcp = new McpServer({
2000
+ name: "dominus",
2001
+ version: "0.1.0"
2002
+ });
2003
+ mcp.tool(
2004
+ "read_script",
2005
+ "Read the source code of a script in Roblox Studio",
2006
+ { path: z.string().describe('Full instance path, e.g. "ServerScriptService.GameManager"') },
2007
+ async ({ path: path4 }) => {
2008
+ assertConnected();
2009
+ const res = await wsServer.sendRequest(CliMsg.GET_SCRIPT, { path: path4 });
2010
+ return { content: [{ type: "text", text: JSON.stringify(res.payload, null, 2) }] };
2011
+ }
2012
+ );
2013
+ mcp.tool(
2014
+ "edit_script",
2015
+ "Replace the entire source code of a script in Roblox Studio",
2016
+ {
2017
+ path: z.string().describe("Full instance path"),
2018
+ source: z.string().describe("New complete source code")
2019
+ },
2020
+ async ({ path: path4, source }) => {
2021
+ assertConnected();
2022
+ const res = await wsServer.sendRequest(CliMsg.SET_SCRIPT, { path: path4, source });
2023
+ return { content: [{ type: "text", text: JSON.stringify(res.payload) }] };
2024
+ }
2025
+ );
2026
+ mcp.tool(
2027
+ "run_code",
2028
+ "Execute Luau code in Roblox Studio and return output",
2029
+ { code: z.string().describe("Luau code to execute") },
2030
+ async ({ code }) => {
2031
+ assertConnected();
2032
+ const res = await wsServer.sendRequest(CliMsg.RUN_CODE, { code });
2033
+ return { content: [{ type: "text", text: JSON.stringify(res.payload, null, 2) }] };
2034
+ }
2035
+ );
2036
+ mcp.tool(
2037
+ "get_explorer",
2038
+ "Get the DataModel explorer tree from Roblox Studio",
2039
+ {
2040
+ rootPath: z.string().optional().describe("Root path to start from. Omit for entire tree."),
2041
+ maxDepth: z.number().optional().default(3).describe("Maximum tree depth")
2042
+ },
2043
+ async ({ rootPath, maxDepth }) => {
2044
+ assertConnected();
2045
+ const res = await wsServer.sendRequest(CliMsg.GET_EXPLORER, {
2046
+ rootPath: rootPath ?? null,
2047
+ maxDepth
2048
+ });
2049
+ return { content: [{ type: "text", text: JSON.stringify(res.payload, null, 2) }] };
2050
+ }
2051
+ );
2052
+ mcp.tool(
2053
+ "get_properties",
2054
+ "Get all properties of an instance in Roblox Studio",
2055
+ { path: z.string().describe("Full instance path") },
2056
+ async ({ path: path4 }) => {
2057
+ assertConnected();
2058
+ const res = await wsServer.sendRequest(CliMsg.GET_PROPERTIES, { path: path4 });
2059
+ return { content: [{ type: "text", text: JSON.stringify(res.payload, null, 2) }] };
2060
+ }
2061
+ );
2062
+ mcp.tool(
2063
+ "set_properties",
2064
+ "Set properties on an instance in Roblox Studio",
2065
+ {
2066
+ path: z.string().describe("Full instance path"),
2067
+ properties: z.record(z.unknown()).describe("Key-value pairs of properties to set")
2068
+ },
2069
+ async ({ path: path4, properties }) => {
2070
+ assertConnected();
2071
+ const res = await wsServer.sendRequest(CliMsg.SET_PROPERTIES, { path: path4, properties });
2072
+ return { content: [{ type: "text", text: JSON.stringify(res.payload) }] };
2073
+ }
2074
+ );
2075
+ mcp.tool(
2076
+ "insert_instance",
2077
+ "Create a new instance in the Roblox Studio DataModel",
2078
+ {
2079
+ className: z.string().describe('Class name (e.g. "Part", "Script")'),
2080
+ parentPath: z.string().describe("Parent instance path"),
2081
+ name: z.string().describe("Name for the new instance"),
2082
+ properties: z.record(z.unknown()).optional().describe("Optional properties to set")
2083
+ },
2084
+ async ({ className, parentPath, name, properties }) => {
2085
+ assertConnected();
2086
+ const res = await wsServer.sendRequest(CliMsg.INSERT_INSTANCE, {
2087
+ className,
2088
+ parentPath,
2089
+ name,
2090
+ properties: properties ?? {}
2091
+ });
2092
+ return { content: [{ type: "text", text: JSON.stringify(res.payload) }] };
2093
+ }
2094
+ );
2095
+ mcp.tool(
2096
+ "delete_instance",
2097
+ "Delete an instance from the Roblox Studio DataModel",
2098
+ { path: z.string().describe("Full instance path to delete") },
2099
+ async ({ path: path4 }) => {
2100
+ assertConnected();
2101
+ const res = await wsServer.sendRequest(CliMsg.DELETE_INSTANCE, { path: path4 });
2102
+ return { content: [{ type: "text", text: JSON.stringify(res.payload) }] };
2103
+ }
2104
+ );
2105
+ mcp.tool(
2106
+ "get_selection",
2107
+ "Get the currently selected instances in Roblox Studio",
2108
+ {},
2109
+ async () => {
2110
+ assertConnected();
2111
+ const res = await wsServer.sendRequest(CliMsg.GET_SELECTION, {});
2112
+ return { content: [{ type: "text", text: JSON.stringify(res.payload, null, 2) }] };
2113
+ }
2114
+ );
2115
+ mcp.tool(
2116
+ "search_scripts",
2117
+ "Search all scripts in Roblox Studio for a text pattern",
2118
+ {
2119
+ query: z.string().describe("Text to search for"),
2120
+ maxResults: z.number().optional().default(20).describe("Max results")
2121
+ },
2122
+ async ({ query, maxResults }) => {
2123
+ assertConnected();
2124
+ const res = await wsServer.sendRequest(CliMsg.SEARCH_SCRIPTS, { query, maxResults });
2125
+ return { content: [{ type: "text", text: JSON.stringify(res.payload, null, 2) }] };
2126
+ }
2127
+ );
2128
+ mcp.tool(
2129
+ "get_output",
2130
+ "Get recent output log from Roblox Studio",
2131
+ {
2132
+ limit: z.number().optional().default(50).describe("Max entries"),
2133
+ level: z.enum(["info", "warn", "error"]).optional().describe("Filter by level")
2134
+ },
2135
+ async ({ limit, level }) => {
2136
+ assertConnected();
2137
+ const res = await wsServer.sendRequest(CliMsg.GET_OUTPUT, {
2138
+ limit,
2139
+ level: level ?? null
2140
+ });
2141
+ return { content: [{ type: "text", text: JSON.stringify(res.payload, null, 2) }] };
2142
+ }
2143
+ );
2144
+ mcp.tool(
2145
+ "get_class_info",
2146
+ "Look up the schema of any Roblox class using ReflectionService. Returns all properties (with types), methods (with parameters), and events. ALWAYS use this before creating or modifying GUI elements to know correct property names and types.",
2147
+ { className: z.string().describe('Roblox class name, e.g. "Frame", "TextLabel", "Part", "MeshPart"') },
2148
+ async ({ className }) => {
2149
+ assertConnected();
2150
+ const res = await wsServer.sendRequest(CliMsg.GET_REFLECTION, { className });
2151
+ return { content: [{ type: "text", text: JSON.stringify(res.payload, null, 2) }] };
2152
+ }
2153
+ );
2154
+ mcp.tool(
2155
+ "create_ui",
2156
+ '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.',
2157
+ {
2158
+ parent: z.string().optional().describe('Parent: "StarterGui", "PlayerGui", or path. Default: StarterGui'),
2159
+ tree: z.record(z.unknown()).describe(
2160
+ 'Root UI node: {ClassName, Name, properties..., Children: [{...}, ...]}. UDim2 as [xScale, xOffset, yScale, yOffset], Color3 as "#hex", Font as "GothamBold"'
2161
+ )
2162
+ },
2163
+ async ({ parent, tree }) => {
2164
+ assertConnected();
2165
+ const res = await wsServer.sendRequest(
2166
+ CliMsg.BUILD_UI,
2167
+ { parent: parent ?? "StarterGui", tree }
2168
+ );
2169
+ return { content: [{ type: "text", text: JSON.stringify(res.payload, null, 2) }] };
2170
+ }
2171
+ );
2172
+ mcp.tool(
2173
+ "run_tests",
2174
+ "Run tests by requiring ModuleScripts matching test naming conventions (*.test, *.spec, *Test, *Spec). Lightweight \u2014 does not enter Run/Play mode.",
2175
+ { testName: z.string().optional().describe("Specific test name, or omit to run all") },
2176
+ async ({ testName }) => {
2177
+ assertConnected();
2178
+ const res = await wsServer.sendRequest(CliMsg.RUN_TESTS, {
2179
+ testName: testName ?? null
2180
+ });
2181
+ return { content: [{ type: "text", text: JSON.stringify(res.payload, null, 2) }] };
2182
+ }
2183
+ );
2184
+ mcp.tool(
2185
+ "execute_run_test",
2186
+ "Start a Run mode test session using StudioTestService:ExecuteRunModeAsync(). This actually enters Run mode in Studio. Pass args that server scripts can retrieve via GetTestArgs(). Yields until EndTest() is called or the session stops. Returns the value from EndTest(). Use for headless automated tests \u2014 no player, just server scripts.",
2187
+ { args: z.unknown().optional().describe("Arguments passed to the test session. Server scripts get this via StudioTestService:GetTestArgs()") },
2188
+ async ({ args }) => {
2189
+ assertConnected();
2190
+ const res = await wsServer.sendRequest(CliMsg.EXECUTE_RUN_TEST, { args: args ?? null }, 12e4);
2191
+ return { content: [{ type: "text", text: JSON.stringify(res.payload, null, 2) }] };
2192
+ }
2193
+ );
2194
+ mcp.tool(
2195
+ "execute_play_test",
2196
+ "Start a solo Play mode test session using StudioTestService:ExecutePlayModeAsync(). This enters Play mode with a player character. Pass args that server scripts can retrieve via GetTestArgs(). Yields until EndTest() is called or the session stops. Returns the value from EndTest(). Use for tests needing a player character and full client-server simulation.",
2197
+ { args: z.unknown().optional().describe("Arguments passed to the test session. Server scripts get this via StudioTestService:GetTestArgs()") },
2198
+ async ({ args }) => {
2199
+ assertConnected();
2200
+ const res = await wsServer.sendRequest(CliMsg.EXECUTE_PLAY_TEST, { args: args ?? null }, 12e4);
2201
+ return { content: [{ type: "text", text: JSON.stringify(res.payload, null, 2) }] };
2202
+ }
2203
+ );
2204
+ mcp.tool(
2205
+ "create_part",
2206
+ "Create a Part with position, size, color, material \u2014 all in one call. Faster than insert_instance + set_properties.",
2207
+ {
2208
+ name: z.string().describe("Name for the part"),
2209
+ parent: z.string().optional().describe("Parent path (default: Workspace)"),
2210
+ shape: z.enum(["Block", "Ball", "Cylinder", "Wedge", "CornerWedge"]).optional().describe("Part shape"),
2211
+ position: z.object({ x: z.number(), y: z.number(), z: z.number() }).optional().describe("World position"),
2212
+ size: z.object({ x: z.number(), y: z.number(), z: z.number() }).optional().describe("Size in studs"),
2213
+ color: z.string().optional().describe('Hex "#00FF00", BrickColor name, or "rgb(0,255,0)"'),
2214
+ material: z.string().optional().describe("Plastic, Wood, Grass, Neon, Metal, etc."),
2215
+ transparency: z.number().optional().describe("0=opaque, 1=invisible"),
2216
+ anchored: z.boolean().optional().describe("Anchored (default: true)"),
2217
+ canCollide: z.boolean().optional().describe("Has collision")
2218
+ },
2219
+ async (params) => {
2220
+ assertConnected();
2221
+ const properties = {};
2222
+ if (params.position) properties.Position = { X: params.position.x, Y: params.position.y, Z: params.position.z };
2223
+ if (params.size) properties.Size = { X: params.size.x, Y: params.size.y, Z: params.size.z };
2224
+ if (params.color) {
2225
+ const c = params.color;
2226
+ if (c.startsWith("#")) properties.Color = c;
2227
+ else if (c.startsWith("rgb")) {
2228
+ const m = c.match(/(\d+)/g);
2229
+ if (m && m.length >= 3) properties.Color = { red: parseInt(m[0]), green: parseInt(m[1]), blue: parseInt(m[2]) };
2230
+ } else properties.BrickColor = c;
2231
+ }
2232
+ if (params.material) properties.Material = params.material;
2233
+ if (params.transparency !== void 0) properties.Transparency = params.transparency;
2234
+ if (params.anchored !== void 0) properties.Anchored = params.anchored;
2235
+ else properties.Anchored = true;
2236
+ if (params.canCollide !== void 0) properties.CanCollide = params.canCollide;
2237
+ if (params.shape && params.shape !== "Block") properties.Shape = params.shape;
2238
+ const className = params.shape === "Wedge" ? "WedgePart" : params.shape === "CornerWedge" ? "CornerWedgePart" : "Part";
2239
+ const res = await wsServer.sendRequest(CliMsg.INSERT_INSTANCE, {
2240
+ className,
2241
+ parentPath: params.parent ?? "Workspace",
2242
+ name: params.name,
2243
+ properties
2244
+ });
2245
+ return { content: [{ type: "text", text: JSON.stringify(res.payload, null, 2) }] };
2246
+ }
2247
+ );
2248
+ mcp.tool(
2249
+ "clone_instance",
2250
+ "Clone an existing instance, optionally rename it, move it, or offset its position",
2251
+ {
2252
+ path: z.string().describe("Path of instance to clone"),
2253
+ newName: z.string().optional().describe("Name for the clone"),
2254
+ newParent: z.string().optional().describe("Parent path for the clone"),
2255
+ offset: z.object({ x: z.number(), y: z.number(), z: z.number() }).optional().describe("Position offset from original")
2256
+ },
2257
+ async (params) => {
2258
+ assertConnected();
2259
+ const parts = params.path.split(".");
2260
+ let resolve = "game";
2261
+ for (const p of parts) resolve += `["${p}"]`;
2262
+ const lines = [`local orig = ${resolve}`, `local clone = orig:Clone()`];
2263
+ if (params.newName) lines.push(`clone.Name = "${params.newName}"`);
2264
+ if (params.newParent) {
2265
+ const pp = params.newParent.split(".");
2266
+ let pr = "game";
2267
+ for (const p of pp) pr += `["${p}"]`;
2268
+ lines.push(`clone.Parent = ${pr}`);
2269
+ } else {
2270
+ lines.push(`clone.Parent = orig.Parent`);
2271
+ }
2272
+ if (params.offset) {
2273
+ lines.push(`if clone:IsA("BasePart") then`);
2274
+ lines.push(` clone.Position = clone.Position + Vector3.new(${params.offset.x}, ${params.offset.y}, ${params.offset.z})`);
2275
+ lines.push(`end`);
2276
+ }
2277
+ lines.push(`print("Cloned: " .. clone:GetFullName())`);
2278
+ const res = await wsServer.sendRequest(CliMsg.RUN_CODE, { code: lines.join("\n") });
2279
+ return { content: [{ type: "text", text: JSON.stringify(res.payload, null, 2) }] };
2280
+ }
2281
+ );
2282
+ mcp.tool(
2283
+ "group_instances",
2284
+ "Group multiple instances into a Model or Folder",
2285
+ {
2286
+ paths: z.array(z.string()).describe("Array of instance paths to group"),
2287
+ groupName: z.string().describe("Name for the group"),
2288
+ groupClass: z.enum(["Model", "Folder"]).optional().describe("Container class (default: Model)"),
2289
+ parent: z.string().optional().describe("Parent path (default: Workspace)")
2290
+ },
2291
+ async (params) => {
2292
+ assertConnected();
2293
+ const gc = params.groupClass ?? "Model";
2294
+ const parent = params.parent ?? "Workspace";
2295
+ const parentParts = parent.split(".");
2296
+ let parentResolve = "game";
2297
+ for (const p of parentParts) parentResolve += `["${p}"]`;
2298
+ const resolves = params.paths.map((path4) => {
2299
+ const parts = path4.split(".");
2300
+ let r = "game";
2301
+ for (const part of parts) r += `["${part}"]`;
2302
+ return r;
2303
+ });
2304
+ const code = [
2305
+ `local group = Instance.new("${gc}")`,
2306
+ `group.Name = "${params.groupName}"`,
2307
+ `group.Parent = ${parentResolve}`,
2308
+ ...resolves.map((r) => `${r}.Parent = group`),
2309
+ `print("Grouped ${params.paths.length} instances into: " .. group:GetFullName())`
2310
+ ].join("\n");
2311
+ const res = await wsServer.sendRequest(CliMsg.RUN_CODE, { code });
2312
+ return { content: [{ type: "text", text: JSON.stringify(res.payload, null, 2) }] };
2313
+ }
2314
+ );
2315
+ mcp.tool(
2316
+ "build_multiple",
2317
+ "Create many parts/instances in one batch. Send an array of specs \u2014 all created in a single round-trip. Ideal for building trees, houses, vehicles.",
2318
+ {
2319
+ parts: z.array(z.object({
2320
+ name: z.string(),
2321
+ className: z.string().optional(),
2322
+ parent: z.string().optional(),
2323
+ position: z.object({ x: z.number(), y: z.number(), z: z.number() }).optional(),
2324
+ size: z.object({ x: z.number(), y: z.number(), z: z.number() }).optional(),
2325
+ color: z.string().optional().describe('Hex "#RRGGBB" or BrickColor name'),
2326
+ material: z.string().optional(),
2327
+ transparency: z.number().optional(),
2328
+ anchored: z.boolean().optional(),
2329
+ shape: z.enum(["Block", "Ball", "Cylinder", "Wedge"]).optional()
2330
+ })).describe("Array of part specifications"),
2331
+ groupName: z.string().optional().describe("Group all parts into a Model with this name"),
2332
+ groupParent: z.string().optional().describe("Parent for the group (default: Workspace)")
2333
+ },
2334
+ async (params) => {
2335
+ assertConnected();
2336
+ const parts = params.parts;
2337
+ const groupName = params.groupName;
2338
+ const groupParent = params.groupParent ?? "Workspace";
2339
+ function resolveP(path4) {
2340
+ const segs = path4.split(".");
2341
+ let r = "game";
2342
+ for (const s of segs) r += `["${s}"]`;
2343
+ return r;
2344
+ }
2345
+ const lines = [];
2346
+ if (groupName) {
2347
+ lines.push(`local _group = Instance.new("Model")`);
2348
+ lines.push(`_group.Name = "${groupName}"`);
2349
+ lines.push(`_group.Parent = ${resolveP(groupParent)}`);
2350
+ }
2351
+ for (let i = 0; i < parts.length; i++) {
2352
+ const p = parts[i];
2353
+ const v = `_p${i}`;
2354
+ const cn = p.className ?? (p.shape === "Wedge" ? "WedgePart" : "Part");
2355
+ lines.push(`local ${v} = Instance.new("${cn}")`);
2356
+ lines.push(`${v}.Name = "${p.name}"`);
2357
+ if (p.position) lines.push(`${v}.Position = Vector3.new(${p.position.x}, ${p.position.y}, ${p.position.z})`);
2358
+ if (p.size) lines.push(`${v}.Size = Vector3.new(${p.size.x}, ${p.size.y}, ${p.size.z})`);
2359
+ if (p.color) {
2360
+ if (p.color.startsWith("#")) lines.push(`${v}.Color = Color3.fromHex("${p.color}")`);
2361
+ else lines.push(`${v}.BrickColor = BrickColor.new("${p.color}")`);
2362
+ }
2363
+ if (p.material) lines.push(`${v}.Material = Enum.Material.${p.material}`);
2364
+ if (p.transparency !== void 0) lines.push(`${v}.Transparency = ${p.transparency}`);
2365
+ lines.push(`${v}.Anchored = ${p.anchored ?? true}`);
2366
+ if (p.shape && p.shape !== "Block" && cn === "Part") lines.push(`${v}.Shape = Enum.PartType.${p.shape}`);
2367
+ lines.push(`${v}.Parent = ${groupName ? "_group" : resolveP(p.parent ?? "Workspace")}`);
2368
+ }
2369
+ lines.push(`print("Built ${parts.length} parts${groupName ? ` in ${groupName}` : ""}")`);
2370
+ const res = await wsServer.sendRequest(CliMsg.RUN_CODE, { code: lines.join("\n") });
2371
+ return { content: [{ type: "text", text: JSON.stringify(res.payload, null, 2) }] };
2372
+ }
2373
+ );
2374
+ mcp.tool(
2375
+ "recall_memory",
2376
+ "Search persistent memory for facts about the current Roblox project",
2377
+ { query: z.string().describe("What to search for") },
2378
+ async ({ query }) => {
2379
+ const facts = recallFacts("current", query, 15);
2380
+ const scripts = getScriptIndex("current");
2381
+ return {
2382
+ content: [{
2383
+ type: "text",
2384
+ text: JSON.stringify({ facts: facts.map((f) => f.content), scriptIndex: scripts }, null, 2)
2385
+ }]
2386
+ };
2387
+ }
2388
+ );
2389
+ mcp.tool(
2390
+ "remember",
2391
+ "Store a fact about the project in persistent memory for future sessions",
2392
+ {
2393
+ fact: z.string().describe("The fact to remember"),
2394
+ category: z.enum(["architecture", "pattern", "preference", "bug", "api", "general"]).describe("Category")
2395
+ },
2396
+ async ({ fact, category }) => {
2397
+ const now = Date.now();
2398
+ storeFact({ projectId: "current", content: fact, category, source: "mcp", relevance: 1, createdAt: now, accessedAt: now });
2399
+ return { content: [{ type: "text", text: `Remembered: ${fact}` }] };
2400
+ }
2401
+ );
2402
+ mcp.resource(
2403
+ "studio://status",
2404
+ "studio://status",
2405
+ async () => {
2406
+ const info = wsServer.getConnectionInfo();
2407
+ return {
2408
+ contents: [{
2409
+ uri: "studio://status",
2410
+ text: JSON.stringify({
2411
+ connected: wsServer.isConnected(),
2412
+ placeName: info?.placeName,
2413
+ placeId: info?.placeId,
2414
+ studioVersion: info?.studioVersion,
2415
+ port: wsServer.getPort()
2416
+ }, null, 2),
2417
+ mimeType: "application/json"
2418
+ }]
2419
+ };
2420
+ }
2421
+ );
2422
+ const transport = new StdioServerTransport();
2423
+ await mcp.connect(transport);
2424
+ }
2425
+ function assertConnected() {
2426
+ if (!wsServer.isConnected()) {
2427
+ throw new Error(
2428
+ "Roblox Studio is not connected. Open Studio and click the Dominus plugin Connect button."
2429
+ );
2430
+ }
2431
+ }
2432
+ var wsServer;
2433
+ var init_server = __esm({
2434
+ "src/mcp/server.ts"() {
2435
+ init_ws_server();
2436
+ init_protocol();
2437
+ init_store();
2438
+ init_config();
2439
+ }
2440
+ });
2441
+ var CROWN_ART = ` \u25C6 \u25C6 \u25C6
2442
+ \u2554\u2588\u2557 \u2554\u2588\u2557 \u2554\u2588\u2557
2443
+ \u2551\u2588\u2551 \u2551\u2588\u2551 \u2551\u2588\u2551
2444
+ \u255A\u2588\u255A\u2550\u2550\u2550\u2550\u2550\u255D\u2588\u255A\u2550\u2550\u2550\u2550\u2550\u255D\u2588\u255D
2445
+ \u255A\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u255D`;
2446
+ function getGreeting(name) {
2447
+ const hour = (/* @__PURE__ */ new Date()).getHours();
2448
+ if (hour >= 5 && hour < 12) return `Good morning ${name}!`;
2449
+ if (hour >= 12 && hour < 17) return `Good afternoon ${name}!`;
2450
+ if (hour >= 17 && hour < 21) return `Good evening ${name}!`;
2451
+ return `Burning the midnight oil, ${name}!`;
2452
+ }
2453
+ var Welcome = React6.memo(({
2454
+ userName,
2455
+ version,
2456
+ model,
2457
+ provider,
2458
+ port,
2459
+ hasApiKey,
2460
+ connected,
2461
+ cwd
2462
+ }) => {
2463
+ const greeting = getGreeting(userName);
2464
+ return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", width: "100%", children: [
2465
+ /* @__PURE__ */ jsxs(
2466
+ Box,
2467
+ {
2468
+ borderStyle: "round",
2469
+ borderColor: "magenta",
2470
+ paddingX: 1,
2471
+ justifyContent: "space-between",
2472
+ children: [
2473
+ /* @__PURE__ */ jsxs(Text, { bold: true, color: "magenta", children: [
2474
+ "Dominus v",
2475
+ version
2476
+ ] }),
2477
+ /* @__PURE__ */ jsx(Text, { dimColor: true, children: "AI-Powered Roblox Studio Agent" })
2478
+ ]
2479
+ }
2480
+ ),
2481
+ /* @__PURE__ */ jsxs(Box, { children: [
2482
+ /* @__PURE__ */ jsxs(
2483
+ Box,
2484
+ {
2485
+ flexDirection: "column",
2486
+ borderStyle: "round",
2487
+ borderColor: "gray",
2488
+ paddingX: 2,
2489
+ paddingY: 1,
2490
+ width: "50%",
2491
+ children: [
2492
+ /* @__PURE__ */ jsx(Text, { bold: true, color: "cyan", children: greeting }),
2493
+ /* @__PURE__ */ jsx(Text, { children: " " }),
2494
+ CROWN_ART.split("\n").map((line, i) => /* @__PURE__ */ jsx(Text, { color: "yellow", children: line }, i)),
2495
+ /* @__PURE__ */ jsx(Text, { children: " " }),
2496
+ /* @__PURE__ */ jsxs(Text, { dimColor: true, children: [
2497
+ provider,
2498
+ " \xB7 ",
2499
+ model || "no model set"
2500
+ ] }),
2501
+ /* @__PURE__ */ jsx(Text, { dimColor: true, children: cwd })
2502
+ ]
2503
+ }
2504
+ ),
2505
+ /* @__PURE__ */ jsx(
2506
+ Box,
2507
+ {
2508
+ flexDirection: "column",
2509
+ borderStyle: "round",
2510
+ borderColor: "gray",
2511
+ paddingX: 2,
2512
+ paddingY: 1,
2513
+ width: "50%",
2514
+ children: hasApiKey ? /* @__PURE__ */ jsxs(Fragment, { children: [
2515
+ /* @__PURE__ */ jsx(Text, { bold: true, color: "yellow", children: "Tips for getting started" }),
2516
+ /* @__PURE__ */ jsx(Text, { children: "Just type a message to chat with the AI agent" }),
2517
+ /* @__PURE__ */ jsxs(Text, { children: [
2518
+ "Use ",
2519
+ /* @__PURE__ */ jsx(Text, { color: "cyan", children: "/help" }),
2520
+ " to see all commands"
2521
+ ] }),
2522
+ /* @__PURE__ */ jsxs(Text, { children: [
2523
+ "Use ",
2524
+ /* @__PURE__ */ jsxs(Text, { color: "cyan", children: [
2525
+ "/run ",
2526
+ "<code>"
2527
+ ] }),
2528
+ " to execute Luau in Studio"
2529
+ ] }),
2530
+ /* @__PURE__ */ jsx(Text, { children: " " }),
2531
+ /* @__PURE__ */ jsx(Text, { bold: true, color: "yellow", children: "Studio connection" }),
2532
+ connected ? /* @__PURE__ */ jsx(Text, { color: "green", children: "\u25CF Connected to Roblox Studio" }) : /* @__PURE__ */ jsxs(Fragment, { children: [
2533
+ /* @__PURE__ */ jsx(Text, { color: "red", children: "\u25CB Waiting for Studio plugin..." }),
2534
+ /* @__PURE__ */ jsxs(Text, { dimColor: true, children: [
2535
+ "WebSocket server on port ",
2536
+ port
2537
+ ] }),
2538
+ /* @__PURE__ */ jsxs(Text, { dimColor: true, children: [
2539
+ "Run ",
2540
+ /* @__PURE__ */ jsx(Text, { color: "cyan", children: "dominus install-plugin" }),
2541
+ " if needed"
2542
+ ] })
2543
+ ] })
2544
+ ] }) : /* @__PURE__ */ jsxs(Fragment, { children: [
2545
+ /* @__PURE__ */ jsx(Text, { bold: true, color: "yellow", children: "Setup \u2014 pick any provider" }),
2546
+ /* @__PURE__ */ jsx(Text, { children: " " }),
2547
+ /* @__PURE__ */ jsxs(Text, { children: [
2548
+ /* @__PURE__ */ jsx(Text, { color: "green", bold: true, children: "OpenAI " }),
2549
+ /* @__PURE__ */ jsx(Text, { dimColor: true, children: "set " }),
2550
+ /* @__PURE__ */ jsx(Text, { color: "cyan", children: "OPENAI_API_KEY" })
2551
+ ] }),
2552
+ /* @__PURE__ */ jsxs(Text, { children: [
2553
+ /* @__PURE__ */ jsx(Text, { color: "green", bold: true, children: "Anthropic " }),
2554
+ /* @__PURE__ */ jsx(Text, { dimColor: true, children: "set " }),
2555
+ /* @__PURE__ */ jsx(Text, { color: "cyan", children: "ANTHROPIC_API_KEY" })
2556
+ ] }),
2557
+ /* @__PURE__ */ jsxs(Text, { children: [
2558
+ /* @__PURE__ */ jsx(Text, { color: "green", bold: true, children: "Gemini " }),
2559
+ /* @__PURE__ */ jsx(Text, { dimColor: true, children: "set " }),
2560
+ /* @__PURE__ */ jsx(Text, { color: "cyan", children: "GEMINI_API_KEY" })
2561
+ ] }),
2562
+ /* @__PURE__ */ jsxs(Text, { children: [
2563
+ /* @__PURE__ */ jsx(Text, { color: "green", bold: true, children: "Grok (xAI) " }),
2564
+ /* @__PURE__ */ jsx(Text, { dimColor: true, children: "set " }),
2565
+ /* @__PURE__ */ jsx(Text, { color: "cyan", children: "XAI_API_KEY" })
2566
+ ] }),
2567
+ /* @__PURE__ */ jsxs(Text, { children: [
2568
+ /* @__PURE__ */ jsx(Text, { color: "green", bold: true, children: "OpenRouter " }),
2569
+ /* @__PURE__ */ jsx(Text, { dimColor: true, children: "set " }),
2570
+ /* @__PURE__ */ jsx(Text, { color: "cyan", children: "OPENROUTER_API_KEY" })
2571
+ ] }),
2572
+ /* @__PURE__ */ jsx(Text, { children: " " }),
2573
+ /* @__PURE__ */ jsx(Text, { dimColor: true, children: "Or configure manually:" }),
2574
+ /* @__PURE__ */ jsx(Text, { children: /* @__PURE__ */ jsx(Text, { color: "cyan", children: "dominus config set provider openai" }) }),
2575
+ /* @__PURE__ */ jsx(Text, { children: /* @__PURE__ */ jsx(Text, { color: "cyan", children: "dominus config set apiKey sk-..." }) })
2576
+ ] })
2577
+ }
2578
+ )
2579
+ ] })
2580
+ ] });
2581
+ });
2582
+
2583
+ // src/ui/components/Setup.tsx
2584
+ init_config();
2585
+ var PROVIDER_OPTIONS = [
2586
+ { key: "openai", label: "OpenAI", hint: "GPT-5.3, GPT-5.2, o3-mini" },
2587
+ { key: "anthropic", label: "Anthropic", hint: "Claude Opus 4-6, Sonnet 4-6" },
2588
+ { key: "gemini", label: "Google Gemini", hint: "Gemini 3.1 Pro, 2.0 Flash" },
2589
+ { key: "xai", label: "xAI (Grok)", hint: "Grok 4.2, Grok 3" },
2590
+ { key: "openrouter", label: "OpenRouter", hint: "300+ models, one key" }
2591
+ ];
2592
+ var Setup = ({ onComplete }) => {
2593
+ const [step, setStep] = useState("provider");
2594
+ const [selectedIdx, setSelectedIdx] = useState(0);
2595
+ const [selectedProvider, setSelectedProvider] = useState("openai");
2596
+ const [apiKey, setApiKey] = useState("");
2597
+ useInput((input, key) => {
2598
+ if (step !== "provider") return;
2599
+ if (key.upArrow) {
2600
+ setSelectedIdx((prev) => Math.max(0, prev - 1));
2601
+ } else if (key.downArrow) {
2602
+ setSelectedIdx((prev) => Math.min(PROVIDER_OPTIONS.length - 1, prev + 1));
2603
+ } else if (key.return) {
2604
+ setSelectedProvider(PROVIDER_OPTIONS[selectedIdx].key);
2605
+ setStep("key");
2606
+ }
2607
+ });
2608
+ const handleKeySubmit = (value) => {
2609
+ const trimmed = value.trim();
2610
+ if (trimmed) {
2611
+ onComplete(selectedProvider, trimmed);
2612
+ }
2613
+ };
2614
+ const providerInfo = PROVIDERS[selectedProvider];
2615
+ return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", paddingX: 2, paddingY: 1, children: [
2616
+ /* @__PURE__ */ jsx(Text, { bold: true, color: "magenta", children: "\u25C6 Dominus \u2014 First Time Setup" }),
2617
+ /* @__PURE__ */ jsx(Text, { children: " " }),
2618
+ step === "provider" && /* @__PURE__ */ jsxs(Fragment, { children: [
2619
+ /* @__PURE__ */ jsx(Text, { bold: true, children: "Select your AI provider:" }),
2620
+ /* @__PURE__ */ jsx(Text, { children: " " }),
2621
+ PROVIDER_OPTIONS.map((opt, i) => /* @__PURE__ */ jsxs(Box, { children: [
2622
+ /* @__PURE__ */ jsxs(Text, { color: i === selectedIdx ? "cyan" : void 0, bold: i === selectedIdx, children: [
2623
+ i === selectedIdx ? " \u276F " : " ",
2624
+ opt.label
2625
+ ] }),
2626
+ /* @__PURE__ */ jsxs(Text, { dimColor: true, children: [
2627
+ " ",
2628
+ opt.hint
2629
+ ] })
2630
+ ] }, opt.key)),
2631
+ /* @__PURE__ */ jsx(Text, { children: " " }),
2632
+ /* @__PURE__ */ jsx(Text, { dimColor: true, children: "\u2191/\u2193 to select, Enter to confirm" })
2633
+ ] }),
2634
+ step === "key" && /* @__PURE__ */ jsxs(Fragment, { children: [
2635
+ /* @__PURE__ */ jsxs(Text, { children: [
2636
+ "Provider: ",
2637
+ /* @__PURE__ */ jsx(Text, { color: "cyan", bold: true, children: PROVIDERS[selectedProvider].name })
2638
+ ] }),
2639
+ /* @__PURE__ */ jsxs(Text, { children: [
2640
+ "Default model: ",
2641
+ /* @__PURE__ */ jsx(Text, { color: "green", children: providerInfo.defaultModel })
2642
+ ] }),
2643
+ /* @__PURE__ */ jsx(Text, { children: " " }),
2644
+ /* @__PURE__ */ jsx(Text, { bold: true, children: "Enter your API key:" }),
2645
+ /* @__PURE__ */ jsx(Text, { dimColor: true, children: "Get one from your provider's dashboard" }),
2646
+ /* @__PURE__ */ jsx(Text, { children: " " }),
2647
+ /* @__PURE__ */ jsxs(Box, { children: [
2648
+ /* @__PURE__ */ jsx(Text, { color: "cyan", children: "\u276F " }),
2649
+ /* @__PURE__ */ jsx(
2650
+ TextInput,
2651
+ {
2652
+ value: apiKey,
2653
+ onChange: setApiKey,
2654
+ onSubmit: handleKeySubmit,
2655
+ mask: "*",
2656
+ placeholder: "sk-..."
2657
+ }
2658
+ )
2659
+ ] })
2660
+ ] })
2661
+ ] });
2662
+ };
2663
+ var StatusBar = React6.memo(({
2664
+ connected,
2665
+ placeName,
2666
+ model,
2667
+ sessionTokens
2668
+ }) => {
2669
+ const statusColor = connected ? "green" : "red";
2670
+ const statusText = connected ? `\u25CF Connected${placeName ? ` \u2014 ${placeName}` : ""}` : "\u25CB Disconnected";
2671
+ const shortModel = model.split("/").pop() ?? model;
2672
+ return /* @__PURE__ */ jsxs(
2673
+ Box,
2674
+ {
2675
+ borderStyle: "single",
2676
+ borderColor: "gray",
2677
+ paddingX: 1,
2678
+ justifyContent: "space-between",
2679
+ width: "100%",
2680
+ children: [
2681
+ /* @__PURE__ */ jsxs(Box, { gap: 2, children: [
2682
+ /* @__PURE__ */ jsx(Text, { bold: true, color: "magenta", children: "DOMINUS" }),
2683
+ /* @__PURE__ */ jsx(Text, { color: statusColor, children: statusText })
2684
+ ] }),
2685
+ /* @__PURE__ */ jsxs(Box, { gap: 2, children: [
2686
+ sessionTokens !== void 0 && /* @__PURE__ */ jsxs(Text, { dimColor: true, children: [
2687
+ "~",
2688
+ Math.round(sessionTokens / 1e3),
2689
+ "k tokens"
2690
+ ] }),
2691
+ /* @__PURE__ */ jsx(Text, { color: "cyan", children: shortModel }),
2692
+ /* @__PURE__ */ jsx(Text, { dimColor: true, children: "Ctrl+C to quit" })
2693
+ ] })
2694
+ ]
2695
+ }
2696
+ );
2697
+ });
2698
+ function formatTime(ts) {
2699
+ const d = new Date(ts);
2700
+ return d.toLocaleTimeString("en-US", { hour12: false });
2701
+ }
2702
+ var LEVEL_COLORS = {
2703
+ info: "white",
2704
+ warn: "yellow",
2705
+ error: "red"
2706
+ };
2707
+ var LEVEL_ICONS = {
2708
+ info: "\u2502",
2709
+ warn: "\u26A0",
2710
+ error: "\u2716"
2711
+ };
2712
+ var OutputPane = React6.memo(({ entries, maxLines = 10 }) => {
2713
+ const visible = entries.slice(-maxLines);
2714
+ return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", borderStyle: "single", borderColor: "gray", paddingX: 1, children: [
2715
+ /* @__PURE__ */ jsx(Text, { bold: true, dimColor: true, children: "Output" }),
2716
+ visible.length === 0 ? /* @__PURE__ */ jsx(Text, { dimColor: true, italic: true, children: "Waiting for Studio output..." }) : visible.map((entry, i) => /* @__PURE__ */ jsxs(Text, { color: LEVEL_COLORS[entry.level] ?? "white", children: [
2717
+ /* @__PURE__ */ jsxs(Text, { dimColor: true, children: [
2718
+ "[",
2719
+ formatTime(entry.timestamp),
2720
+ "]"
2721
+ ] }),
2722
+ " ",
2723
+ /* @__PURE__ */ jsx(Text, { children: LEVEL_ICONS[entry.level] ?? "\u2502" }),
2724
+ " ",
2725
+ entry.message
2726
+ ] }, i))
2727
+ ] });
2728
+ });
2729
+ function formatTime2(ts) {
2730
+ const d = new Date(ts);
2731
+ return d.toLocaleTimeString("en-US", { hour12: false });
2732
+ }
2733
+ var ChatPane = React6.memo(({ entries, streaming, maxLines = 20 }) => {
2734
+ const visible = entries.slice(-maxLines);
2735
+ return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", flexGrow: 1, paddingX: 1, children: [
2736
+ visible.map((entry, i) => /* @__PURE__ */ jsxs(Box, { flexDirection: "column", marginBottom: entry.role === "user" ? 1 : 0, children: [
2737
+ entry.role === "user" && /* @__PURE__ */ jsxs(Box, { children: [
2738
+ /* @__PURE__ */ jsxs(Text, { color: "cyan", bold: true, children: [
2739
+ "You",
2740
+ " "
2741
+ ] }),
2742
+ /* @__PURE__ */ jsxs(Text, { dimColor: true, children: [
2743
+ "[",
2744
+ formatTime2(entry.timestamp),
2745
+ "]"
2746
+ ] })
2747
+ ] }),
2748
+ entry.role === "assistant" && /* @__PURE__ */ jsxs(Box, { children: [
2749
+ /* @__PURE__ */ jsxs(Text, { color: "magenta", bold: true, children: [
2750
+ "Dominus",
2751
+ " "
2752
+ ] }),
2753
+ /* @__PURE__ */ jsxs(Text, { dimColor: true, children: [
2754
+ "[",
2755
+ formatTime2(entry.timestamp),
2756
+ "]"
2757
+ ] })
2758
+ ] }),
2759
+ entry.role === "tool" && /* @__PURE__ */ jsxs(Text, { color: "yellow", children: [
2760
+ "\u26A1 ",
2761
+ entry.toolName ?? "tool",
2762
+ ": ",
2763
+ entry.content
2764
+ ] }),
2765
+ entry.role === "system" && /* @__PURE__ */ jsx(Text, { color: "yellow", bold: true, children: "Dominus " }),
2766
+ entry.role === "error" && /* @__PURE__ */ jsxs(Text, { color: "red", children: [
2767
+ "\u2716 Error: ",
2768
+ entry.content
2769
+ ] }),
2770
+ (entry.role === "user" || entry.role === "assistant" || entry.role === "system") && /* @__PURE__ */ jsx(Text, { wrap: "wrap", children: entry.content })
2771
+ ] }, i)),
2772
+ streaming && /* @__PURE__ */ jsxs(Box, { flexDirection: "column", children: [
2773
+ /* @__PURE__ */ jsxs(Box, { children: [
2774
+ /* @__PURE__ */ jsxs(Text, { color: "magenta", bold: true, children: [
2775
+ "Dominus",
2776
+ " "
2777
+ ] }),
2778
+ /* @__PURE__ */ jsx(Text, { dimColor: true, children: "(typing...)" })
2779
+ ] }),
2780
+ /* @__PURE__ */ jsx(Text, { wrap: "wrap", children: streaming })
2781
+ ] })
2782
+ ] });
2783
+ });
2784
+ function getClipboardImage() {
2785
+ if (process.platform !== "win32") {
2786
+ return getClipboardImageUnix();
2787
+ }
2788
+ const tmpPath = join(tmpdir(), `dominus-clip-${randomBytes(4).toString("hex")}.png`);
2789
+ try {
2790
+ execSync(
2791
+ `powershell -NoProfile -Command "Add-Type -AssemblyName System.Windows.Forms; $img = [System.Windows.Forms.Clipboard]::GetImage(); if ($img -eq $null) { exit 1 }; $img.Save('${tmpPath.replace(/'/g, "''")}', [System.Drawing.Imaging.ImageFormat]::Png); $img.Dispose()"`,
2792
+ { stdio: "pipe", timeout: 5e3 }
2793
+ );
2794
+ if (!existsSync(tmpPath)) return null;
2795
+ const buffer = readFileSync(tmpPath);
2796
+ unlinkSync(tmpPath);
2797
+ if (buffer.length < 100) return null;
2798
+ return {
2799
+ base64: buffer.toString("base64"),
2800
+ mimeType: "image/png"
2801
+ };
2802
+ } catch {
2803
+ try {
2804
+ if (existsSync(tmpPath)) unlinkSync(tmpPath);
2805
+ } catch {
2806
+ }
2807
+ return null;
2808
+ }
2809
+ }
2810
+ function getClipboardImageUnix() {
2811
+ const tmpPath = join(tmpdir(), `dominus-clip-${randomBytes(4).toString("hex")}.png`);
2812
+ try {
2813
+ if (process.platform === "darwin") {
2814
+ execSync(`osascript -e 'set png to (the clipboard as \xABclass PNGf\xBB)' -e 'set f to open for access POSIX file "${tmpPath}" with write permission' -e 'write png to f' -e 'close access f'`, { stdio: "pipe", timeout: 5e3 });
2815
+ } else {
2816
+ execSync(`xclip -selection clipboard -t image/png -o > "${tmpPath}"`, { stdio: "pipe", timeout: 5e3 });
2817
+ }
2818
+ if (!existsSync(tmpPath)) return null;
2819
+ const buffer = readFileSync(tmpPath);
2820
+ unlinkSync(tmpPath);
2821
+ if (buffer.length < 100) return null;
2822
+ return {
2823
+ base64: buffer.toString("base64"),
2824
+ mimeType: "image/png"
2825
+ };
2826
+ } catch {
2827
+ try {
2828
+ if (existsSync(tmpPath)) unlinkSync(tmpPath);
2829
+ } catch {
2830
+ }
2831
+ return null;
2832
+ }
2833
+ }
2834
+ var COMMANDS = [
2835
+ { command: "/help", description: "Show help and available commands" },
2836
+ { command: "/model", description: "Switch the AI model" },
2837
+ { command: "/models", description: "List all available models from your provider" },
2838
+ { command: "/status", description: "Show connection and config status" },
2839
+ { command: "/clear", description: "Clear chat history" },
2840
+ { command: "/paste", description: "Paste image from clipboard (or Alt+V)" }
2841
+ ];
2842
+ var SuggestionBox = React6.memo(
2843
+ ({ suggestions, selectedIdx }) => /* @__PURE__ */ jsxs(Box, { flexDirection: "column", paddingX: 1, borderStyle: "single", borderColor: "gray", children: [
2844
+ suggestions.map((s, i) => /* @__PURE__ */ jsxs(Box, { children: [
2845
+ /* @__PURE__ */ jsx(Text, { color: i === selectedIdx ? "cyan" : void 0, bold: i === selectedIdx, children: i === selectedIdx ? "\u276F " : " " }),
2846
+ /* @__PURE__ */ jsx(Text, { color: i === selectedIdx ? "cyan" : "white", bold: i === selectedIdx, children: s.command.padEnd(16) }),
2847
+ /* @__PURE__ */ jsx(Text, { dimColor: true, children: s.description })
2848
+ ] }, s.command)),
2849
+ /* @__PURE__ */ jsx(Text, { dimColor: true, children: " \u2191\u2193 navigate \xB7 Tab complete \xB7 Enter select" })
2850
+ ] })
2851
+ );
2852
+ var InputBar = React6.memo(({
2853
+ onSubmit,
2854
+ onImagePaste,
2855
+ isProcessing,
2856
+ placeholder = "Ask Dominus anything, or /help for commands...",
2857
+ imageAttached,
2858
+ onClearImage
2859
+ }) => {
2860
+ const [value, setValue] = useState("");
2861
+ const [selectedIdx, setSelectedIdx] = useState(0);
2862
+ const [pasteStatus, setPasteStatus] = useState("none");
2863
+ const valueRef = useRef(value);
2864
+ valueRef.current = value;
2865
+ const suggestions = useMemo(() => {
2866
+ if (!value.startsWith("/") || value.includes(" ")) return [];
2867
+ const query = value.toLowerCase();
2868
+ return COMMANDS.filter((c) => c.command.startsWith(query));
2869
+ }, [value]);
2870
+ const showSuggestions = suggestions.length > 0 && value.length > 0;
2871
+ const tryPasteImage = useCallback(() => {
2872
+ if (!onImagePaste) return false;
2873
+ setPasteStatus("checking");
2874
+ try {
2875
+ const img = getClipboardImage();
2876
+ if (img) {
2877
+ onImagePaste(img);
2878
+ setPasteStatus("none");
2879
+ return true;
2880
+ }
2881
+ } catch {
2882
+ }
2883
+ setPasteStatus("no_image");
2884
+ setTimeout(() => setPasteStatus("none"), 2e3);
2885
+ return false;
2886
+ }, [onImagePaste]);
2887
+ useEffect(() => {
2888
+ const handler = (data) => {
2889
+ if (isProcessing) return;
2890
+ const str = data.toString();
2891
+ if (str === "\x1Bv" || str === "\x1BV") {
2892
+ tryPasteImage();
2893
+ return;
2894
+ }
2895
+ if (str === "") {
2896
+ tryPasteImage();
2897
+ return;
2898
+ }
2899
+ };
2900
+ process.stdin.on("data", handler);
2901
+ return () => {
2902
+ process.stdin.off("data", handler);
2903
+ };
2904
+ }, [isProcessing, tryPasteImage]);
2905
+ useInput((_input, key) => {
2906
+ if (isProcessing) return;
2907
+ if (!showSuggestions) return;
2908
+ if (key.upArrow) {
2909
+ setSelectedIdx((prev) => Math.max(0, prev - 1));
2910
+ } else if (key.downArrow) {
2911
+ setSelectedIdx((prev) => Math.min(suggestions.length - 1, prev + 1));
2912
+ } else if (key.tab) {
2913
+ const selected = suggestions[selectedIdx];
2914
+ if (selected) {
2915
+ setValue(selected.command + " ");
2916
+ setSelectedIdx(0);
2917
+ }
2918
+ }
2919
+ });
2920
+ const handleChange = useCallback((v) => {
2921
+ setValue(v);
2922
+ setSelectedIdx(0);
2923
+ }, []);
2924
+ const handleSubmit = useCallback((text) => {
2925
+ if (isProcessing) return;
2926
+ if (text.trim().toLowerCase() === "/paste") {
2927
+ tryPasteImage();
2928
+ setValue("");
2929
+ return;
2930
+ }
2931
+ const currentValue = valueRef.current;
2932
+ const currentSuggestions = COMMANDS.filter(
2933
+ (c) => currentValue.startsWith("/") && !currentValue.includes(" ") && c.command.startsWith(currentValue.toLowerCase())
2934
+ );
2935
+ const hasSuggestions = currentSuggestions.length > 0 && currentValue.length > 0;
2936
+ if (hasSuggestions) {
2937
+ const cmd = currentSuggestions[0]?.command;
2938
+ if (cmd && currentValue !== cmd && !currentValue.includes(" ")) {
2939
+ const noArgCmds = ["/clear", "/help", "/status", "/models", "/paste"];
2940
+ setValue(cmd + (noArgCmds.includes(cmd) ? "" : " "));
2941
+ setSelectedIdx(0);
2942
+ return;
2943
+ }
2944
+ }
2945
+ if (text.trim()) {
2946
+ onSubmit(text.trim());
2947
+ setValue("");
2948
+ setSelectedIdx(0);
2949
+ }
2950
+ }, [isProcessing, onSubmit, tryPasteImage]);
2951
+ return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", children: [
2952
+ showSuggestions && /* @__PURE__ */ jsx(SuggestionBox, { suggestions, selectedIdx }),
2953
+ imageAttached && /* @__PURE__ */ jsxs(Box, { paddingX: 1, gap: 1, children: [
2954
+ /* @__PURE__ */ jsx(Text, { color: "green", bold: true, children: "[image 1]" }),
2955
+ /* @__PURE__ */ jsx(Text, { dimColor: true, children: "pasted \u2014 send a message to use it" })
2956
+ ] }),
2957
+ pasteStatus === "checking" && /* @__PURE__ */ jsx(Box, { paddingX: 1, children: /* @__PURE__ */ jsx(Text, { color: "yellow", children: "Checking clipboard..." }) }),
2958
+ pasteStatus === "no_image" && /* @__PURE__ */ jsx(Box, { paddingX: 1, children: /* @__PURE__ */ jsx(Text, { color: "red", children: "No image found in clipboard. Copy an image first (screenshot, snipping tool, etc.)" }) }),
2959
+ /* @__PURE__ */ jsxs(Box, { borderStyle: "single", borderColor: isProcessing ? "yellow" : imageAttached ? "green" : "cyan", paddingX: 1, children: [
2960
+ /* @__PURE__ */ jsx(Text, { color: isProcessing ? "yellow" : "cyan", bold: true, children: isProcessing ? "\u27F3 " : imageAttached ? "\u{1F4CE} " : "\u276F " }),
2961
+ isProcessing ? /* @__PURE__ */ jsx(Text, { dimColor: true, children: "Processing..." }) : /* @__PURE__ */ jsx(
2962
+ TextInput,
2963
+ {
2964
+ value,
2965
+ onChange: handleChange,
2966
+ onSubmit: handleSubmit,
2967
+ placeholder: imageAttached ? "Describe what you want (image attached)..." : placeholder
2968
+ }
2969
+ )
2970
+ ] })
2971
+ ] });
2972
+ });
2973
+ var CLASS_ICONS = {
2974
+ Workspace: "\u{1F30D}",
2975
+ ServerScriptService: "\u2699\uFE0F",
2976
+ ReplicatedStorage: "\u{1F4E6}",
2977
+ ReplicatedFirst: "\u{1F680}",
2978
+ StarterGui: "\u{1F5A5}\uFE0F",
2979
+ StarterPack: "\u{1F392}",
2980
+ StarterPlayer: "\u{1F9D1}",
2981
+ Players: "\u{1F465}",
2982
+ Lighting: "\u{1F4A1}",
2983
+ SoundService: "\u{1F50A}",
2984
+ Script: "\u{1F4DC}",
2985
+ LocalScript: "\u{1F4F1}",
2986
+ ModuleScript: "\u{1F4D8}",
2987
+ Part: "\u{1F9F1}",
2988
+ Model: "\u{1F4D0}",
2989
+ Folder: "\u{1F4C1}",
2990
+ RemoteEvent: "\u{1F4E1}",
2991
+ RemoteFunction: "\u{1F4E1}",
2992
+ BindableEvent: "\u{1F517}"
2993
+ };
2994
+ function getIcon(className) {
2995
+ return CLASS_ICONS[className] ?? "\u25C7";
2996
+ }
2997
+ var TreeNode = ({ node, depth, maxDepth, selectedPath }) => {
2998
+ const indent = " ".repeat(depth);
2999
+ const isSelected = node.path === selectedPath;
3000
+ const hasChildren = node.children && node.children.length > 0;
3001
+ const icon = getIcon(node.className);
3002
+ return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", children: [
3003
+ /* @__PURE__ */ jsxs(Text, { children: [
3004
+ /* @__PURE__ */ jsx(Text, { dimColor: true, children: indent }),
3005
+ /* @__PURE__ */ jsx(Text, { children: hasChildren ? "\u25BE " : " " }),
3006
+ /* @__PURE__ */ jsxs(Text, { children: [
3007
+ icon,
3008
+ " "
3009
+ ] }),
3010
+ /* @__PURE__ */ jsx(Text, { bold: isSelected, color: isSelected ? "cyan" : void 0, children: node.name }),
3011
+ /* @__PURE__ */ jsxs(Text, { dimColor: true, children: [
3012
+ " (",
3013
+ node.className,
3014
+ ")"
3015
+ ] })
3016
+ ] }),
3017
+ hasChildren && depth < maxDepth && node.children.map((child, i) => /* @__PURE__ */ jsx(
3018
+ TreeNode,
3019
+ {
3020
+ node: child,
3021
+ depth: depth + 1,
3022
+ maxDepth,
3023
+ selectedPath
3024
+ },
3025
+ `${child.path}-${i}`
3026
+ ))
3027
+ ] });
3028
+ };
3029
+ var ExplorerPane = React6.memo(({
3030
+ tree,
3031
+ selectedPath,
3032
+ maxDepth = 4
3033
+ }) => {
3034
+ return /* @__PURE__ */ jsxs(
3035
+ Box,
3036
+ {
3037
+ flexDirection: "column",
3038
+ borderStyle: "single",
3039
+ borderColor: "gray",
3040
+ paddingX: 1,
3041
+ width: 36,
3042
+ minWidth: 30,
3043
+ children: [
3044
+ /* @__PURE__ */ jsx(Text, { bold: true, dimColor: true, children: "Explorer" }),
3045
+ tree.length === 0 ? /* @__PURE__ */ jsx(Text, { dimColor: true, italic: true, children: "Connect Studio plugin..." }) : tree.map((node, i) => /* @__PURE__ */ jsx(
3046
+ TreeNode,
3047
+ {
3048
+ node,
3049
+ depth: 0,
3050
+ maxDepth,
3051
+ selectedPath
3052
+ },
3053
+ `${node.path}-${i}`
3054
+ ))
3055
+ ]
3056
+ }
3057
+ );
3058
+ });
3059
+ function summarizeArgs(args) {
3060
+ const parts = [];
3061
+ for (const [key, value] of Object.entries(args)) {
3062
+ const str = typeof value === "string" ? value : JSON.stringify(value);
3063
+ parts.push(`${key}=${str.length > 40 ? str.slice(0, 40) + "..." : str}`);
3064
+ }
3065
+ return parts.join(", ");
3066
+ }
3067
+ var ToolCallView = React6.memo(({ calls }) => {
3068
+ if (calls.length === 0) return null;
3069
+ return /* @__PURE__ */ jsx(Box, { flexDirection: "column", paddingX: 1, children: calls.map((call, i) => /* @__PURE__ */ jsxs(Box, { gap: 1, children: [
3070
+ call.status === "running" && /* @__PURE__ */ jsx(Text, { color: "yellow", children: /* @__PURE__ */ jsx(Spinner, { type: "dots" }) }),
3071
+ call.status === "done" && /* @__PURE__ */ jsx(Text, { color: "green", children: "\u2714" }),
3072
+ call.status === "error" && /* @__PURE__ */ jsx(Text, { color: "red", children: "\u2716" }),
3073
+ /* @__PURE__ */ jsx(Text, { color: "yellow", bold: true, children: call.name }),
3074
+ /* @__PURE__ */ jsxs(Text, { dimColor: true, children: [
3075
+ "(",
3076
+ summarizeArgs(call.args),
3077
+ ")"
3078
+ ] }),
3079
+ call.result && !call.result.success && /* @__PURE__ */ jsxs(Text, { color: "red", children: [
3080
+ " \u2014 ",
3081
+ call.result.error
3082
+ ] })
3083
+ ] }, i)) });
3084
+ });
3085
+ var Layout = React6.memo(({
3086
+ connected,
3087
+ placeName,
3088
+ model,
3089
+ chatEntries,
3090
+ outputEntries,
3091
+ explorerTree,
3092
+ toolCalls,
3093
+ streaming,
3094
+ isProcessing,
3095
+ onSubmit,
3096
+ onImagePaste,
3097
+ imageAttached,
3098
+ onClearImage
3099
+ }) => {
3100
+ return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", width: "100%", children: [
3101
+ /* @__PURE__ */ jsx(StatusBar, { connected, placeName, model }),
3102
+ /* @__PURE__ */ jsxs(Box, { flexGrow: 1, children: [
3103
+ /* @__PURE__ */ jsx(ExplorerPane, { tree: explorerTree }),
3104
+ /* @__PURE__ */ jsxs(Box, { flexDirection: "column", flexGrow: 1, children: [
3105
+ /* @__PURE__ */ jsx(ChatPane, { entries: chatEntries, streaming }),
3106
+ toolCalls.length > 0 && /* @__PURE__ */ jsx(ToolCallView, { calls: toolCalls })
3107
+ ] })
3108
+ ] }),
3109
+ /* @__PURE__ */ jsx(OutputPane, { entries: outputEntries, maxLines: 6 }),
3110
+ /* @__PURE__ */ jsx(
3111
+ InputBar,
3112
+ {
3113
+ onSubmit,
3114
+ onImagePaste,
3115
+ isProcessing,
3116
+ imageAttached,
3117
+ onClearImage
3118
+ }
3119
+ )
3120
+ ] });
3121
+ });
3122
+
3123
+ // src/ui/hooks/useStudio.ts
3124
+ init_protocol();
3125
+ function useStudio(server) {
3126
+ const connInfo = server.getConnectionInfo();
3127
+ const [state, setState] = useState({
3128
+ connected: server.isConnected(),
3129
+ placeName: connInfo?.placeName,
3130
+ placeId: connInfo?.placeId,
3131
+ explorerTree: [],
3132
+ outputLog: []
3133
+ });
3134
+ const stateRef = useRef(state);
3135
+ stateRef.current = state;
3136
+ useEffect(() => {
3137
+ if (server.isConnected() && !stateRef.current.connected) {
3138
+ const info = server.getConnectionInfo();
3139
+ setState((prev) => ({
3140
+ ...prev,
3141
+ connected: true,
3142
+ placeName: info?.placeName,
3143
+ placeId: info?.placeId
3144
+ }));
3145
+ }
3146
+ const onConnected = (payload) => {
3147
+ setState((prev) => ({
3148
+ ...prev,
3149
+ connected: true,
3150
+ placeName: payload.placeName ?? prev.placeName,
3151
+ placeId: payload.placeId ?? prev.placeId
3152
+ }));
3153
+ };
3154
+ const onDisconnected = () => {
3155
+ setState((prev) => ({
3156
+ ...prev,
3157
+ connected: false,
3158
+ placeName: void 0,
3159
+ placeId: void 0
3160
+ }));
3161
+ };
3162
+ const onExplorer = (payload) => {
3163
+ setState((prev) => ({ ...prev, explorerTree: payload.tree ?? [] }));
3164
+ };
3165
+ const onOutput = (payload) => {
3166
+ setState((prev) => ({
3167
+ ...prev,
3168
+ outputLog: [
3169
+ ...prev.outputLog.slice(-200),
3170
+ {
3171
+ message: payload.message,
3172
+ level: payload.level,
3173
+ timestamp: payload.timestamp ?? Date.now(),
3174
+ source: payload.source
3175
+ }
3176
+ ]
3177
+ }));
3178
+ };
3179
+ const onError = (payload) => {
3180
+ setState((prev) => ({
3181
+ ...prev,
3182
+ outputLog: [
3183
+ ...prev.outputLog.slice(-200),
3184
+ {
3185
+ message: payload.message,
3186
+ level: "error",
3187
+ timestamp: payload.timestamp ?? Date.now(),
3188
+ source: payload.source
3189
+ }
3190
+ ]
3191
+ }));
3192
+ };
3193
+ server.on("studio:connected", onConnected);
3194
+ server.on("studio:disconnected", onDisconnected);
3195
+ server.on(StudioMsg.EXPLORER, onExplorer);
3196
+ server.on(StudioMsg.OUTPUT, onOutput);
3197
+ server.on(StudioMsg.ERROR, onError);
3198
+ return () => {
3199
+ server.off("studio:connected", onConnected);
3200
+ server.off("studio:disconnected", onDisconnected);
3201
+ server.off(StudioMsg.EXPLORER, onExplorer);
3202
+ server.off(StudioMsg.OUTPUT, onOutput);
3203
+ server.off(StudioMsg.ERROR, onError);
3204
+ };
3205
+ }, [server]);
3206
+ const clearOutput = useCallback(() => {
3207
+ setState((prev) => ({ ...prev, outputLog: [] }));
3208
+ }, []);
3209
+ return { ...state, clearOutput };
3210
+ }
3211
+
3212
+ // src/app.tsx
3213
+ init_config();
3214
+ var App = ({ server, agent: initialAgent, version }) => {
3215
+ const [showWelcome, setShowWelcome] = useState(true);
3216
+ const [chatEntries, setChatEntries] = useState([]);
3217
+ const [toolCalls, setToolCalls] = useState([]);
3218
+ const [streaming, setStreaming] = useState("");
3219
+ const [isProcessing, setIsProcessing] = useState(false);
3220
+ const [agent, setAgent] = useState(initialAgent);
3221
+ const studio = useStudio(server);
3222
+ const config = loadConfig();
3223
+ const hasApiKey = !!config.apiKey;
3224
+ const [needsSetup, setNeedsSetup] = useState(!hasApiKey);
3225
+ const [pendingImage, setPendingImage] = useState(null);
3226
+ const pendingImageRef = useRef(null);
3227
+ const handleImagePaste = useCallback((image) => {
3228
+ setPendingImage(image);
3229
+ pendingImageRef.current = image;
3230
+ }, []);
3231
+ const handleClearImage = useCallback(() => {
3232
+ setPendingImage(null);
3233
+ pendingImageRef.current = null;
3234
+ }, []);
3235
+ const handleSetupComplete = useCallback((provider, apiKey) => {
3236
+ setConfigValue("provider", provider);
3237
+ setConfigValue("apiKey", apiKey);
3238
+ const providerInfo = PROVIDERS[provider];
3239
+ setConfigValue("apiBaseUrl", providerInfo.baseUrl);
3240
+ setConfigValue("model", providerInfo.defaultModel);
3241
+ setNeedsSetup(false);
3242
+ }, []);
3243
+ const addSystemMessage = useCallback((content) => {
3244
+ setChatEntries((prev) => [
3245
+ ...prev,
3246
+ { role: "system", content, timestamp: Date.now() }
3247
+ ]);
3248
+ }, []);
3249
+ const handleCommand = useCallback(
3250
+ (text) => {
3251
+ const input = text.startsWith("/") ? text.slice(1) : text;
3252
+ const [cmd, ...args] = input.split(" ");
3253
+ const arg = args.join(" ");
3254
+ const lowerCmd = cmd.toLowerCase();
3255
+ const currentModel = agent?.getModel() ?? resolveProviderSettings(config).model;
3256
+ const providerName = PROVIDERS[config.provider]?.name ?? config.provider;
3257
+ const keyStatus = config.apiKey ? `${config.apiKey.slice(0, 6)}...${config.apiKey.slice(-4)}` : "NOT SET";
3258
+ switch (lowerCmd) {
3259
+ case "help":
3260
+ case "h":
3261
+ case "?":
3262
+ addSystemMessage([
3263
+ "\u2500\u2500 Your Config \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500",
3264
+ ` Provider: ${providerName}`,
3265
+ ` Model: ${currentModel}`,
3266
+ ` API Key: ${keyStatus}`,
3267
+ ` Studio: ${studio.connected ? "\u25CF Connected" + (studio.placeName ? ` (${studio.placeName})` : "") : "\u25CB Disconnected"}`,
3268
+ "",
3269
+ "\u2500\u2500 Commands \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500",
3270
+ " /help Show this help",
3271
+ " /model <name> Switch AI model",
3272
+ " /models List available models",
3273
+ " /status Connection status",
3274
+ " /clear Clear chat history",
3275
+ "",
3276
+ "\u2500\u2500 Setup (run in your terminal) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500",
3277
+ " dominus config set provider <name>",
3278
+ " Providers: openai, anthropic, gemini, xai, openrouter",
3279
+ " dominus config set apiKey <key>",
3280
+ " dominus config list",
3281
+ " dominus install-plugin",
3282
+ " dominus mcp-install",
3283
+ "",
3284
+ "Or just type a message to chat with Dominus!"
3285
+ ].join("\n"));
3286
+ return true;
3287
+ case "clear":
3288
+ agent?.clearHistory();
3289
+ setChatEntries([]);
3290
+ setToolCalls([]);
3291
+ return true;
3292
+ case "model":
3293
+ if (arg && agent) {
3294
+ agent.setModel(arg);
3295
+ addSystemMessage(`Model switched to: ${arg}`);
3296
+ } else {
3297
+ addSystemMessage("Usage: /model <model-name>");
3298
+ }
3299
+ return true;
3300
+ case "models": {
3301
+ if (!agent) {
3302
+ addSystemMessage("No API key configured \u2014 cannot fetch models.");
3303
+ return true;
3304
+ }
3305
+ addSystemMessage(`Fetching models from ${providerName}...`);
3306
+ agent.listModels().then((models) => {
3307
+ if (models.length === 0) {
3308
+ addSystemMessage("Could not fetch models. Check your API key.");
3309
+ return;
3310
+ }
3311
+ const lines = models.slice(0, 40).map((m) => ` ${m.id}`);
3312
+ addSystemMessage([
3313
+ `\u2500\u2500 ${providerName} Models (${models.length} total) \u2500\u2500`,
3314
+ ...lines,
3315
+ models.length > 40 ? ` ... and ${models.length - 40} more` : "",
3316
+ "",
3317
+ ` Current: ${currentModel}`,
3318
+ " Switch with: /model <name>"
3319
+ ].join("\n"));
3320
+ });
3321
+ return true;
3322
+ }
3323
+ case "status":
3324
+ addSystemMessage([
3325
+ "\u2500\u2500 Status \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500",
3326
+ ` Provider: ${providerName}`,
3327
+ ` Model: ${currentModel}`,
3328
+ ` API Key: ${keyStatus}`,
3329
+ ` Port: ${server.getPort()}`,
3330
+ ` Studio: ${studio.connected ? "\u25CF Connected" + (studio.placeName ? ` (${studio.placeName})` : "") : "\u25CB Disconnected"}`
3331
+ ].join("\n"));
3332
+ return true;
3333
+ default:
3334
+ return false;
3335
+ }
3336
+ },
3337
+ [agent, studio, server, config, addSystemMessage]
3338
+ );
3339
+ const handleSubmit = useCallback(
3340
+ async (text) => {
3341
+ if (isProcessing) return;
3342
+ if (text.trim().toLowerCase() === "/paste" || text.trim().toLowerCase() === "paste") return;
3343
+ const isCommand = text.startsWith("/") || ["help", "h", "?", "clear", "model", "models", "status"].includes(text.split(" ")[0].toLowerCase());
3344
+ if (isCommand) {
3345
+ setShowWelcome(false);
3346
+ handleCommand(text);
3347
+ return;
3348
+ }
3349
+ setShowWelcome(false);
3350
+ if (!agent) {
3351
+ setChatEntries((prev) => [
3352
+ ...prev,
3353
+ { role: "user", content: text, timestamp: Date.now() },
3354
+ {
3355
+ role: "error",
3356
+ content: "No API key configured. Set OPENAI_API_KEY, ANTHROPIC_API_KEY, GEMINI_API_KEY, or XAI_API_KEY \u2014 or run: dominus config set apiKey <key>",
3357
+ timestamp: Date.now()
3358
+ }
3359
+ ]);
3360
+ return;
3361
+ }
3362
+ const image = pendingImageRef.current;
3363
+ setPendingImage(null);
3364
+ pendingImageRef.current = null;
3365
+ setIsProcessing(true);
3366
+ setToolCalls([]);
3367
+ const displayText = image ? `${text}
3368
+ [image 1]` : text;
3369
+ setChatEntries((prev) => [
3370
+ ...prev,
3371
+ { role: "user", content: displayText, timestamp: Date.now() }
3372
+ ]);
3373
+ let fullText = "";
3374
+ try {
3375
+ for await (const event of agent.run(text, image ? { base64: image.base64, mimeType: image.mimeType } : void 0)) {
3376
+ switch (event.type) {
3377
+ case "text":
3378
+ fullText += event.content;
3379
+ setStreaming(fullText);
3380
+ break;
3381
+ case "tool_call":
3382
+ setToolCalls((prev) => [
3383
+ ...prev,
3384
+ { name: event.tool, args: event.args, status: "running" }
3385
+ ]);
3386
+ break;
3387
+ case "tool_result":
3388
+ setToolCalls(
3389
+ (prev) => prev.map(
3390
+ (tc) => tc.name === event.tool && tc.status === "running" ? { ...tc, status: event.result.success ? "done" : "error", result: event.result } : tc
3391
+ )
3392
+ );
3393
+ break;
3394
+ case "error":
3395
+ setChatEntries((prev) => [
3396
+ ...prev,
3397
+ { role: "error", content: event.message, timestamp: Date.now() }
3398
+ ]);
3399
+ break;
3400
+ }
3401
+ }
3402
+ if (fullText) {
3403
+ setChatEntries((prev) => [
3404
+ ...prev,
3405
+ { role: "assistant", content: fullText, timestamp: Date.now() }
3406
+ ]);
3407
+ }
3408
+ } catch (err) {
3409
+ setChatEntries((prev) => [
3410
+ ...prev,
3411
+ {
3412
+ role: "error",
3413
+ content: err instanceof Error ? err.message : String(err),
3414
+ timestamp: Date.now()
3415
+ }
3416
+ ]);
3417
+ } finally {
3418
+ setIsProcessing(false);
3419
+ setStreaming("");
3420
+ setToolCalls([]);
3421
+ }
3422
+ },
3423
+ [agent, isProcessing, handleCommand]
3424
+ );
3425
+ if (needsSetup) {
3426
+ return /* @__PURE__ */ jsx(Setup, { onComplete: handleSetupComplete });
3427
+ }
3428
+ if (showWelcome) {
3429
+ return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", width: "100%", children: [
3430
+ /* @__PURE__ */ jsx(
3431
+ Welcome,
3432
+ {
3433
+ userName: config.userName,
3434
+ version,
3435
+ model: resolveProviderSettings(config).model,
3436
+ provider: PROVIDERS[config.provider]?.name ?? config.provider,
3437
+ port: server.getPort(),
3438
+ hasApiKey,
3439
+ connected: studio.connected,
3440
+ cwd: process.cwd()
3441
+ }
3442
+ ),
3443
+ /* @__PURE__ */ jsx(
3444
+ InputBar,
3445
+ {
3446
+ onSubmit: handleSubmit,
3447
+ onImagePaste: handleImagePaste,
3448
+ isProcessing: false,
3449
+ imageAttached: !!pendingImage,
3450
+ onClearImage: handleClearImage,
3451
+ placeholder: hasApiKey ? "Type a message to begin, or Ctrl+V to paste an image..." : "Set up your API key to get started, or type /help..."
3452
+ }
3453
+ )
3454
+ ] });
3455
+ }
3456
+ return /* @__PURE__ */ jsx(
3457
+ Layout,
3458
+ {
3459
+ connected: studio.connected,
3460
+ placeName: studio.placeName,
3461
+ model: agent?.getModel() ?? resolveProviderSettings(config).model,
3462
+ chatEntries,
3463
+ outputEntries: studio.outputLog,
3464
+ explorerTree: studio.explorerTree,
3465
+ toolCalls,
3466
+ streaming: streaming || void 0,
3467
+ isProcessing,
3468
+ onSubmit: handleSubmit,
3469
+ onImagePaste: handleImagePaste,
3470
+ imageAttached: !!pendingImage,
3471
+ onClearImage: handleClearImage
3472
+ }
3473
+ );
3474
+ };
3475
+
3476
+ // src/index.tsx
3477
+ init_ws_server();
3478
+
3479
+ // src/transport/ws-client.ts
3480
+ init_protocol();
3481
+ var DominusClient = class extends EventEmitter {
3482
+ ws = null;
3483
+ port;
3484
+ studioConnected = false;
3485
+ connectionInfo = null;
3486
+ pendingRequests = /* @__PURE__ */ new Map();
3487
+ constructor(port = 18088) {
3488
+ super();
3489
+ this.port = port;
3490
+ }
3491
+ connect() {
3492
+ return new Promise((resolve, reject) => {
3493
+ const url = `ws://localhost:${this.port}`;
3494
+ const ws = new WebSocket(url);
3495
+ ws.on("open", () => {
3496
+ this.ws = ws;
3497
+ ws.send(serializeMessage(createMessage("controller:connect", {})));
3498
+ this.emit("server:started", { port: this.port, relay: true });
3499
+ resolve();
3500
+ });
3501
+ ws.on("message", (raw) => {
3502
+ try {
3503
+ const msg = parseMessage(raw.toString());
3504
+ this.handleMessage(msg);
3505
+ } catch (err) {
3506
+ this.emit("server:error", err);
3507
+ }
3508
+ });
3509
+ ws.on("close", () => {
3510
+ this.ws = null;
3511
+ this.studioConnected = false;
3512
+ this.emit("studio:disconnected");
3513
+ for (const [id, pending] of this.pendingRequests) {
3514
+ clearTimeout(pending.timer);
3515
+ pending.reject(new Error("Relay connection lost"));
3516
+ this.pendingRequests.delete(id);
3517
+ }
3518
+ });
3519
+ ws.on("error", (err) => {
3520
+ if (!this.ws) reject(err);
3521
+ this.emit("server:error", err);
3522
+ });
3523
+ });
3524
+ }
3525
+ handleMessage(msg) {
3526
+ if (msg.type === "controller:welcome") {
3527
+ const p = msg.payload;
3528
+ this.studioConnected = p.studioConnected;
3529
+ if (this.studioConnected) {
3530
+ this.emit("studio:connected", {});
3531
+ }
3532
+ return;
3533
+ }
3534
+ if (msg.type === "studio:connected") {
3535
+ this.studioConnected = true;
3536
+ const payload = msg.payload;
3537
+ this.connectionInfo = {
3538
+ connectedAt: Date.now(),
3539
+ studioVersion: payload.studioVersion,
3540
+ placeId: payload.placeId,
3541
+ placeName: payload.placeName
3542
+ };
3543
+ this.emit("studio:connected", payload);
3544
+ return;
3545
+ }
3546
+ if (msg.type === "studio:disconnected") {
3547
+ this.studioConnected = false;
3548
+ this.connectionInfo = null;
3549
+ this.emit("studio:disconnected");
3550
+ return;
3551
+ }
3552
+ if (msg.type === StudioMsg.RESPONSE) {
3553
+ const pending = this.pendingRequests.get(msg.id);
3554
+ if (pending) {
3555
+ clearTimeout(pending.timer);
3556
+ pending.resolve(msg);
3557
+ this.pendingRequests.delete(msg.id);
3558
+ }
3559
+ return;
3560
+ }
3561
+ this.emit(msg.type, msg.payload);
3562
+ this.emit("message", msg);
3563
+ }
3564
+ sendRequest(type, payload, timeoutMs = 1e4) {
3565
+ return new Promise((resolve, reject) => {
3566
+ if (!this.ws || this.ws.readyState !== WebSocket.OPEN) {
3567
+ reject(new Error("Not connected to Dominus server"));
3568
+ return;
3569
+ }
3570
+ if (!this.studioConnected) {
3571
+ reject(new Error("Studio not connected"));
3572
+ return;
3573
+ }
3574
+ const msg = createMessage(type, payload);
3575
+ const timer = setTimeout(() => {
3576
+ this.pendingRequests.delete(msg.id);
3577
+ reject(new Error(`Request timed out: ${type}`));
3578
+ }, timeoutMs);
3579
+ this.pendingRequests.set(msg.id, {
3580
+ resolve,
3581
+ reject,
3582
+ timer
3583
+ });
3584
+ this.ws.send(serializeMessage(msg));
3585
+ });
3586
+ }
3587
+ sendNotification(type, payload) {
3588
+ if (!this.ws || this.ws.readyState !== WebSocket.OPEN) return;
3589
+ const msg = createMessage(type, payload);
3590
+ this.ws.send(serializeMessage(msg));
3591
+ }
3592
+ isConnected() {
3593
+ return this.studioConnected;
3594
+ }
3595
+ getConnectionInfo() {
3596
+ if (!this.connectionInfo) return null;
3597
+ return { ...this.connectionInfo, ws: this.ws };
3598
+ }
3599
+ getPort() {
3600
+ return this.port;
3601
+ }
3602
+ stop() {
3603
+ return new Promise((resolve) => {
3604
+ for (const [, pending] of this.pendingRequests) {
3605
+ clearTimeout(pending.timer);
3606
+ pending.reject(new Error("Client shutting down"));
3607
+ }
3608
+ this.pendingRequests.clear();
3609
+ if (this.ws) {
3610
+ this.ws.close();
3611
+ this.ws = null;
3612
+ }
3613
+ resolve();
3614
+ });
3615
+ }
3616
+ };
3617
+ async function isPortInUse(port) {
3618
+ return new Promise((resolve) => {
3619
+ const ws = new WebSocket(`ws://localhost:${port}`);
3620
+ const timer = setTimeout(() => {
3621
+ ws.close();
3622
+ resolve(false);
3623
+ }, 1e3);
3624
+ ws.on("open", () => {
3625
+ clearTimeout(timer);
3626
+ ws.close();
3627
+ resolve(true);
3628
+ });
3629
+ ws.on("error", () => {
3630
+ clearTimeout(timer);
3631
+ resolve(false);
3632
+ });
3633
+ });
3634
+ }
3635
+
3636
+ // src/ai/provider.ts
3637
+ init_config();
3638
+ var AIProvider = class {
3639
+ client;
3640
+ model;
3641
+ providerName;
3642
+ constructor(config) {
3643
+ if (!config.apiKey) {
3644
+ throw new Error("API key not configured. Run: dominus config set apiKey <your-key>");
3645
+ }
3646
+ const { baseUrl, model } = resolveProviderSettings(config);
3647
+ this.providerName = PROVIDERS[config.provider]?.name ?? config.provider;
3648
+ this.client = new OpenAI({
3649
+ apiKey: config.apiKey,
3650
+ baseURL: baseUrl
3651
+ });
3652
+ this.model = model;
3653
+ }
3654
+ setModel(model) {
3655
+ this.model = model;
3656
+ }
3657
+ getModel() {
3658
+ return this.model;
3659
+ }
3660
+ getProviderName() {
3661
+ return this.providerName;
3662
+ }
3663
+ async listModels() {
3664
+ try {
3665
+ const list = await this.client.models.list();
3666
+ const models = [];
3667
+ for await (const m of list) {
3668
+ models.push({ id: m.id, owned_by: m.owned_by });
3669
+ }
3670
+ models.sort((a, b) => a.id.localeCompare(b.id));
3671
+ return models;
3672
+ } catch {
3673
+ return [];
3674
+ }
3675
+ }
3676
+ async chat(messages, tools) {
3677
+ const openAITools = tools?.map(toolToOpenAI);
3678
+ const response = await this.client.chat.completions.create({
3679
+ model: this.model,
3680
+ messages,
3681
+ tools: openAITools?.length ? openAITools : void 0,
3682
+ temperature: 0.3
3683
+ });
3684
+ const choice = response.choices[0];
3685
+ if (!choice) throw new Error("No response from AI");
3686
+ const toolCalls = (choice.message.tool_calls ?? []).map((tc) => ({
3687
+ id: tc.id,
3688
+ name: tc.function.name,
3689
+ arguments: JSON.parse(tc.function.arguments)
3690
+ }));
3691
+ return {
3692
+ content: choice.message.content,
3693
+ toolCalls,
3694
+ finishReason: choice.finish_reason ?? "stop",
3695
+ usage: response.usage ? {
3696
+ promptTokens: response.usage.prompt_tokens,
3697
+ completionTokens: response.usage.completion_tokens,
3698
+ totalTokens: response.usage.total_tokens
3699
+ } : void 0
3700
+ };
3701
+ }
3702
+ async *streamChat(messages, tools) {
3703
+ const openAITools = tools?.map(toolToOpenAI);
3704
+ const stream = await this.client.chat.completions.create({
3705
+ model: this.model,
3706
+ messages,
3707
+ tools: openAITools?.length ? openAITools : void 0,
3708
+ temperature: 0.3,
3709
+ stream: true
3710
+ });
3711
+ const toolCallBuffers = /* @__PURE__ */ new Map();
3712
+ for await (const chunk of stream) {
3713
+ const delta = chunk.choices[0]?.delta;
3714
+ if (!delta) continue;
3715
+ if (delta.content) {
3716
+ yield { type: "text", content: delta.content };
3717
+ }
3718
+ if (delta.tool_calls) {
3719
+ for (const tc of delta.tool_calls) {
3720
+ if (!toolCallBuffers.has(tc.index)) {
3721
+ toolCallBuffers.set(tc.index, {
3722
+ id: tc.id ?? "",
3723
+ name: tc.function?.name ?? "",
3724
+ args: ""
3725
+ });
3726
+ if (tc.function?.name) {
3727
+ yield {
3728
+ type: "tool_call_start",
3729
+ toolCall: { id: tc.id, name: tc.function.name }
3730
+ };
3731
+ }
3732
+ }
3733
+ const buffer = toolCallBuffers.get(tc.index);
3734
+ if (tc.id) buffer.id = tc.id;
3735
+ if (tc.function?.name) buffer.name = tc.function.name;
3736
+ if (tc.function?.arguments) {
3737
+ buffer.args += tc.function.arguments;
3738
+ yield {
3739
+ type: "tool_call_delta",
3740
+ content: tc.function.arguments,
3741
+ toolCall: { id: buffer.id, name: buffer.name }
3742
+ };
3743
+ }
3744
+ }
3745
+ }
3746
+ if (chunk.choices[0]?.finish_reason === "tool_calls") {
3747
+ for (const [, buffer] of toolCallBuffers) {
3748
+ let args = {};
3749
+ try {
3750
+ args = JSON.parse(buffer.args);
3751
+ } catch {
3752
+ }
3753
+ yield {
3754
+ type: "tool_call_end",
3755
+ toolCall: { id: buffer.id, name: buffer.name, arguments: args }
3756
+ };
3757
+ }
3758
+ toolCallBuffers.clear();
3759
+ }
3760
+ }
3761
+ yield { type: "done" };
3762
+ }
3763
+ };
3764
+ function toolToOpenAI(tool24) {
3765
+ return {
3766
+ type: "function",
3767
+ function: {
3768
+ name: tool24.name,
3769
+ description: tool24.description,
3770
+ parameters: tool24.parameters
3771
+ }
3772
+ };
3773
+ }
3774
+
3775
+ // src/agent/system-prompt.ts
3776
+ function buildSystemPrompt(opts) {
3777
+ const parts = [];
3778
+ parts.push(`You are Dominus, an expert AI agent for Roblox Studio development.
3779
+ You assist ${opts.userName} with building, debugging, and refactoring Roblox experiences.
3780
+
3781
+ You are deeply knowledgeable about:
3782
+ - Luau (Roblox's programming language): syntax, idioms, metatables, type annotations, coroutines
3783
+ - Roblox Engine API: all services (Players, Workspace, DataStoreService, ReplicatedStorage, etc.)
3784
+ - Roblox architecture patterns: client-server model, remotes, replication, DataStores
3785
+ - Common frameworks: Knit, ProfileService, Rodux, Roact, Fusion
3786
+ - Studio workflows: plugins, packages, team collaboration, testing
3787
+ - Performance: memory management, heartbeat optimization, streaming, LOD
3788
+
3789
+ You have direct access to Roblox Studio through tools. You can read/edit scripts, run code, browse the explorer tree, manage instances, and run tests -- all in real-time via WebSocket.
3790
+
3791
+ When the user asks you to do something, you should:
3792
+ 1. Use your tools to gather context (read scripts, check explorer, recall memory)
3793
+ 2. Plan your approach for complex tasks
3794
+ 3. Execute changes using your tools (edit scripts, run code, insert instances)
3795
+ 4. Verify your changes worked (read back, check output for errors)
3796
+ 5. Remember important facts for future sessions`);
3797
+ if (opts.connected) {
3798
+ parts.push(`
3799
+ ## Current Session
3800
+ - Studio: CONNECTED${opts.placeName ? ` (${opts.placeName})` : ""}${opts.placeId ? ` [PlaceID: ${opts.placeId}]` : ""}
3801
+ - You can use all Studio tools.`);
3802
+ } else {
3803
+ parts.push(`
3804
+ ## Current Session
3805
+ - Studio: NOT CONNECTED
3806
+ - You cannot use Studio tools until the plugin connects.
3807
+ - You can still use memory tools and answer Roblox questions.`);
3808
+ }
3809
+ if (opts.scriptIndex?.length) {
3810
+ parts.push("\n## Project Scripts");
3811
+ const lines = opts.scriptIndex.map(
3812
+ (s) => `- ${s.path} (${s.className})${s.summary ? `: ${s.summary}` : ""}`
3813
+ );
3814
+ if (lines.length > 40) {
3815
+ parts.push(lines.slice(0, 40).join("\n"));
3816
+ parts.push(`... and ${lines.length - 40} more scripts`);
3817
+ } else {
3818
+ parts.push(lines.join("\n"));
3819
+ }
3820
+ }
3821
+ if (opts.facts?.length) {
3822
+ parts.push("\n## Remembered Context");
3823
+ for (const fact of opts.facts.slice(0, 20)) {
3824
+ parts.push(`- [${fact.category}] ${fact.content}`);
3825
+ }
3826
+ }
3827
+ if (opts.rules) {
3828
+ if (opts.rules.do.length > 0) {
3829
+ parts.push("\n## Rules: DO");
3830
+ for (const rule of opts.rules.do) {
3831
+ parts.push(`- ${rule}`);
3832
+ }
3833
+ }
3834
+ if (opts.rules.dont.length > 0) {
3835
+ parts.push("\n## Rules: DO NOT");
3836
+ for (const rule of opts.rules.dont) {
3837
+ parts.push(`- ${rule}`);
3838
+ }
3839
+ }
3840
+ if (opts.rules.conventions.length > 0) {
3841
+ parts.push("\n## Coding Conventions");
3842
+ for (const conv of opts.rules.conventions) {
3843
+ parts.push(`- ${conv}`);
3844
+ }
3845
+ }
3846
+ if (opts.rules.context) {
3847
+ parts.push(`
3848
+ ## Project Context
3849
+ ${opts.rules.context}`);
3850
+ }
3851
+ }
3852
+ parts.push(`
3853
+ ## Tool Usage Guidelines
3854
+ - Always verify your work: after editing a script, read it back to confirm.
3855
+ - After running code, check the output for errors.
3856
+ - Use remember to store important facts you discover about the project.
3857
+ - Use recall_memory before complex tasks to check if you already know relevant context.
3858
+ - When searching scripts, be specific with your query terms.
3859
+ - For multi-step changes, explain your plan before executing.
3860
+
3861
+ ## Building & Construction
3862
+ When the user asks you to build or create objects (parts, trees, houses, terrain, etc.):
3863
+
3864
+ **PREFER these builder tools over raw run_code:**
3865
+ - \`create_part\` \u2014 create a single part with position, size, color, material, shape in one call
3866
+ - \`build_multiple\` \u2014 create many parts in a single batch round-trip (FASTEST for complex builds)
3867
+ - \`clone_instance\` \u2014 duplicate an existing object, optionally offset/rename it
3868
+ - \`group_instances\` \u2014 organize parts into a Model or Folder
3869
+ - \`set_properties\` \u2014 adjust properties on existing instances (supports Vector3, Color3, enums, hex colors)
3870
+
3871
+ **Why use builder tools instead of run_code?**
3872
+ - Tool calls are structured \u2014 the AI sends typed parameters, no string interpolation bugs
3873
+ - Properties are auto-coerced (hex \u2192 Color3, tables \u2192 Vector3, strings \u2192 Enum values)
3874
+ - \`build_multiple\` sends one batch of Luau to Studio, creating many parts in a single round-trip
3875
+ - Easier for the agent to plan, verify, and iterate
3876
+
3877
+ **Building strategy:**
3878
+ 1. For a single part: use \`create_part\`
3879
+ 2. For multi-part objects (trees, buildings, vehicles): use \`build_multiple\` with \`groupName\`
3880
+ 3. For repeating/duplicating: use \`clone_instance\` with offset
3881
+ 4. For organizing: use \`group_instances\`
3882
+ 5. Only fall back to \`run_code\` for complex logic (loops, raycasts, terrain generation algorithms)
3883
+
3884
+ ## GUI / UI Elements \u2014 CRITICAL
3885
+ **ALWAYS use \`create_ui\` for building UI.** It creates the entire UI tree in ONE call \u2014 no run_code, no get_class_info needed.
3886
+ Do NOT use run_code for UI creation. Do NOT call get_class_info before create_ui \u2014 it already handles all types natively.
3887
+
3888
+ \`create_ui\` takes a declarative JSON tree and builds everything in a single Studio round-trip:
3889
+ \`\`\`json
3890
+ {
3891
+ "parent": "StarterGui",
3892
+ "tree": {
3893
+ "ClassName": "ScreenGui", "Name": "MyUI", "ResetOnSpawn": false,
3894
+ "Children": [
3895
+ { "ClassName": "Frame", "Size": [1, 0, 1, 0], "BackgroundColor3": "#F0E6FF",
3896
+ "Children": [
3897
+ { "ClassName": "UICorner", "CornerRadius": [0, 12] },
3898
+ { "ClassName": "TextLabel", "Text": "Hello!", "Size": [0.5, 0, 0.1, 0],
3899
+ "Position": [0.25, 0, 0.45, 0], "BackgroundTransparency": 1,
3900
+ "TextColor3": "#6C5CE7", "TextScaled": true, "Font": "GothamBold" }
3901
+ ]
3902
+ }
3903
+ ]
3904
+ }
3905
+ }
3906
+ \`\`\`
3907
+
3908
+ GUI classes use DIFFERENT property types than 3D parts:
3909
+
3910
+ **Key type differences:**
3911
+ - GUI Size/Position = \`UDim2\` (NOT Vector3). Format: \`{XScale: 0.5, XOffset: 0, YScale: 0.5, YOffset: 0}\` or \`[xScale, xOffset, yScale, yOffset]\`
3912
+ - GUI AnchorPoint = \`Vector2\` = \`{x: 0.5, y: 0.5}\`
3913
+ - BackgroundColor3 = \`Color3\` = \`"#FF0000"\` or \`{R: 1, G: 0, B: 0}\` or \`"rgb(255,0,0)"\`
3914
+ - Font = \`Font\` = \`{Family: "rbxasset://fonts/families/SourceSansPro.json", Weight: "Bold"}\`
3915
+ - BorderSizePixel = \`number\`
3916
+ - TextSize = \`number\`
3917
+ - Enum properties (e.g., TextXAlignment) = string name like \`"Center"\`, \`"Left"\`
3918
+
3919
+ **Property coercion handles automatically:**
3920
+ - UDim2: tables, arrays, "0.5, 0, 0.3, 0" strings
3921
+ - UDim: {Scale, Offset} or number
3922
+ - Vector3, Vector2, Color3, BrickColor, CFrame, Rect, NumberRange
3923
+ - NumberSequence, ColorSequence (for gradients)
3924
+ - All enum properties from string names
3925
+ - Color3 from hex "#RRGGBB", "rgb(r,g,b)", or BrickColor name strings
3926
+
3927
+ ## ReflectionService
3928
+ The \`get_class_info\` tool queries Roblox's ReflectionService to return the complete API surface for any class:
3929
+ - All properties with their exact ValueType
3930
+ - All methods with parameter names and types
3931
+ - All events with parameter types
3932
+ - Superclass, creatability, serialization info
3933
+
3934
+ Use it whenever you're unsure about property names or types. It's fast and cached.`);
3935
+ return parts.join("\n");
3936
+ }
3937
+
3938
+ // src/agent/context-builder.ts
3939
+ init_store();
3940
+ init_config();
3941
+
3942
+ // src/ai/models.ts
3943
+ var POPULAR_MODELS = [
3944
+ // --- OpenAI ---
3945
+ { id: "gpt-5.3-codex", name: "GPT-5.3 Codex", contextWindow: 256e3, supportsTools: true, provider: "openai" },
3946
+ { id: "gpt-5.2", name: "GPT-5.2", contextWindow: 256e3, supportsTools: true, provider: "openai" },
3947
+ { id: "gpt-4o", name: "GPT-4o", contextWindow: 128e3, supportsTools: true, provider: "openai" },
3948
+ { id: "o3-mini", name: "o3-mini", contextWindow: 2e5, supportsTools: true, provider: "openai" },
3949
+ // --- Anthropic ---
3950
+ { id: "claude-opus-4-6", name: "Claude Opus 4.6", contextWindow: 2e5, supportsTools: true, provider: "anthropic" },
3951
+ { id: "claude-sonnet-4-6", name: "Claude Sonnet 4.6", contextWindow: 2e5, supportsTools: true, provider: "anthropic" },
3952
+ // --- Google Gemini ---
3953
+ { id: "gemini-3.1-pro", name: "Gemini 3.1 Pro", contextWindow: 2097152, supportsTools: true, provider: "gemini" },
3954
+ { id: "gemini-2.0-flash", name: "Gemini 2.0 Flash", contextWindow: 1048576, supportsTools: true, provider: "gemini" },
3955
+ // --- xAI / Grok ---
3956
+ { id: "grok-4.2", name: "Grok 4.2", contextWindow: 256e3, supportsTools: true, provider: "xai" },
3957
+ { id: "grok-3", name: "Grok 3", contextWindow: 131072, supportsTools: true, provider: "xai" },
3958
+ // --- OpenRouter (routes to any model) ---
3959
+ { id: "anthropic/claude-opus-4-6", name: "Claude Opus 4.6", contextWindow: 2e5, supportsTools: true, provider: "openrouter" },
3960
+ { id: "anthropic/claude-sonnet-4-6", name: "Claude Sonnet 4.6", contextWindow: 2e5, supportsTools: true, provider: "openrouter" },
3961
+ { id: "openai/gpt-5.3-codex", name: "GPT-5.3 Codex", contextWindow: 256e3, supportsTools: true, provider: "openrouter" },
3962
+ { id: "openai/gpt-5.2", name: "GPT-5.2", contextWindow: 256e3, supportsTools: true, provider: "openrouter" },
3963
+ { id: "google/gemini-3.1-pro", name: "Gemini 3.1 Pro", contextWindow: 2097152, supportsTools: true, provider: "openrouter" },
3964
+ { id: "x-ai/grok-4.2", name: "Grok 4.2", contextWindow: 256e3, supportsTools: true, provider: "openrouter" },
3965
+ { id: "deepseek/deepseek-v3.2", name: "DeepSeek V3.2", contextWindow: 1e6, supportsTools: true, provider: "openrouter" }
3966
+ ];
3967
+ function getModelInfo(modelId) {
3968
+ return POPULAR_MODELS.find((m) => m.id === modelId);
3969
+ }
3970
+ function getContextWindow(modelId) {
3971
+ return getModelInfo(modelId)?.contextWindow ?? 128e3;
3972
+ }
3973
+
3974
+ // src/agent/context-builder.ts
3975
+ var CHARS_PER_TOKEN = 4;
3976
+ function estimateTokens(text) {
3977
+ return Math.ceil(text.length / CHARS_PER_TOKEN);
3978
+ }
3979
+ function buildContext(opts) {
3980
+ const config = loadConfig();
3981
+ const rules = loadRules();
3982
+ const contextWindow = getContextWindow(config.model);
3983
+ const systemBudget = Math.floor(contextWindow * 0.15);
3984
+ const memoryBudget = Math.floor(contextWindow * 0.15);
3985
+ const conversationBudget = Math.floor(contextWindow * 0.45);
3986
+ const connInfo = opts.server.getConnectionInfo();
3987
+ const facts = recallFacts(opts.projectId, opts.userMessage, 15);
3988
+ const scriptIndex = getScriptIndex(opts.projectId);
3989
+ const summaries = getProjectSummaries(opts.projectId, 5);
3990
+ let systemPrompt = buildSystemPrompt({
3991
+ userName: config.userName,
3992
+ connected: opts.server.isConnected(),
3993
+ placeName: connInfo?.placeName,
3994
+ placeId: connInfo?.placeId,
3995
+ scriptIndex,
3996
+ facts,
3997
+ rules
3998
+ });
3999
+ if (estimateTokens(systemPrompt) > systemBudget) {
4000
+ systemPrompt = systemPrompt.slice(0, systemBudget * CHARS_PER_TOKEN);
4001
+ }
4002
+ const messages = [
4003
+ { role: "system", content: systemPrompt }
4004
+ ];
4005
+ if (summaries.length > 0) {
4006
+ const summaryText = summaries.map((s) => s.summary).join("\n");
4007
+ if (estimateTokens(summaryText) <= memoryBudget) {
4008
+ messages.push({
4009
+ role: "system",
4010
+ content: `Previous session summaries:
4011
+ ${summaryText}`
4012
+ });
4013
+ }
4014
+ }
4015
+ let historyTokens = 0;
4016
+ const historyMessages = [];
4017
+ const currentMsg = {
4018
+ role: "user",
4019
+ content: opts.userMessage
4020
+ };
4021
+ for (let i = opts.conversationHistory.length - 1; i >= 0; i--) {
4022
+ const msg = opts.conversationHistory[i];
4023
+ const content = typeof msg.content === "string" ? msg.content : JSON.stringify(msg.content);
4024
+ const tokens = estimateTokens(content);
4025
+ if (historyTokens + tokens > conversationBudget) break;
4026
+ historyTokens += tokens;
4027
+ historyMessages.unshift(msg);
4028
+ }
4029
+ messages.push(...historyMessages, currentMsg);
4030
+ const totalTokens = estimateTokens(
4031
+ messages.map((m) => typeof m.content === "string" ? m.content : "").join("")
4032
+ );
4033
+ return { messages, tokenEstimate: totalTokens };
4034
+ }
4035
+
4036
+ // src/agent/loop.ts
4037
+ init_config();
4038
+ init_config();
4039
+ init_store();
4040
+ var AgentLoop = class {
4041
+ ai;
4042
+ server;
4043
+ tools;
4044
+ conversationHistory = [];
4045
+ projectId = "current";
4046
+ sessionId;
4047
+ maxIterations;
4048
+ maxRetries;
4049
+ constructor(server, tools) {
4050
+ const config = loadConfig();
4051
+ this.ai = new AIProvider(config);
4052
+ this.server = server;
4053
+ this.tools = tools;
4054
+ this.sessionId = nanoid();
4055
+ this.maxIterations = config.maxToolIterations;
4056
+ this.maxRetries = config.maxVerifyRetries;
4057
+ }
4058
+ getSessionId() {
4059
+ return this.sessionId;
4060
+ }
4061
+ setModel(model) {
4062
+ this.ai.setModel(model);
4063
+ }
4064
+ getModel() {
4065
+ return this.ai.getModel();
4066
+ }
4067
+ async listModels() {
4068
+ return this.ai.listModels();
4069
+ }
4070
+ setProjectId(projectId) {
4071
+ this.projectId = projectId;
4072
+ }
4073
+ async *run(userMessage, imageAttachment) {
4074
+ storeMessage({
4075
+ sessionId: this.sessionId,
4076
+ role: "user",
4077
+ content: userMessage + (imageAttachment ? " [image attached]" : ""),
4078
+ createdAt: Date.now()
4079
+ });
4080
+ const { messages } = buildContext({
4081
+ userMessage,
4082
+ conversationHistory: this.conversationHistory,
4083
+ server: this.server,
4084
+ projectId: this.projectId,
4085
+ sessionId: this.sessionId
4086
+ });
4087
+ if (imageAttachment) {
4088
+ const lastMsg = messages[messages.length - 1];
4089
+ if (lastMsg && lastMsg.role === "user") {
4090
+ lastMsg.content = [
4091
+ { type: "text", text: typeof lastMsg.content === "string" ? lastMsg.content : userMessage },
4092
+ {
4093
+ type: "image_url",
4094
+ image_url: {
4095
+ url: `data:${imageAttachment.mimeType};base64,${imageAttachment.base64}`,
4096
+ detail: "high"
4097
+ }
4098
+ }
4099
+ ];
4100
+ }
4101
+ }
4102
+ const toolCtx = this.createToolContext();
4103
+ const allTools = this.tools.getAll();
4104
+ let currentMessages = [...messages];
4105
+ let iteration = 0;
4106
+ while (iteration < this.maxIterations) {
4107
+ iteration++;
4108
+ try {
4109
+ const response = await this.ai.chat(currentMessages, allTools);
4110
+ if (response.content) {
4111
+ yield { type: "text", content: response.content };
4112
+ storeMessage({
4113
+ sessionId: this.sessionId,
4114
+ role: "assistant",
4115
+ content: response.content,
4116
+ createdAt: Date.now()
4117
+ });
4118
+ this.conversationHistory.push({ role: "assistant", content: response.content });
4119
+ }
4120
+ if (response.toolCalls.length === 0) {
4121
+ break;
4122
+ }
4123
+ const toolResults = [];
4124
+ for (const call of response.toolCalls) {
4125
+ yield { type: "tool_call", tool: call.name, args: call.arguments };
4126
+ const result = await this.tools.execute(call, toolCtx);
4127
+ toolResults.push({ call, result });
4128
+ yield { type: "tool_result", tool: call.name, result };
4129
+ storeMessage({
4130
+ sessionId: this.sessionId,
4131
+ role: "tool_call",
4132
+ content: JSON.stringify({ name: call.name, args: call.arguments }),
4133
+ toolName: call.name,
4134
+ createdAt: Date.now()
4135
+ });
4136
+ storeMessage({
4137
+ sessionId: this.sessionId,
4138
+ role: "tool_result",
4139
+ content: JSON.stringify(result),
4140
+ toolName: call.name,
4141
+ createdAt: Date.now()
4142
+ });
4143
+ }
4144
+ const assistantMsg = {
4145
+ role: "assistant",
4146
+ content: response.content ?? null,
4147
+ tool_calls: response.toolCalls.map((tc) => ({
4148
+ id: tc.id,
4149
+ type: "function",
4150
+ function: { name: tc.name, arguments: JSON.stringify(tc.arguments) }
4151
+ }))
4152
+ };
4153
+ currentMessages.push(assistantMsg);
4154
+ for (const { call, result } of toolResults) {
4155
+ currentMessages.push({
4156
+ role: "tool",
4157
+ content: JSON.stringify(result),
4158
+ tool_call_id: call.id
4159
+ });
4160
+ }
4161
+ this.conversationHistory.push(assistantMsg);
4162
+ for (const { call, result } of toolResults) {
4163
+ this.conversationHistory.push({
4164
+ role: "tool",
4165
+ content: JSON.stringify(result),
4166
+ tool_call_id: call.id
4167
+ });
4168
+ }
4169
+ } catch (err) {
4170
+ const message = err instanceof Error ? err.message : String(err);
4171
+ yield { type: "error", message };
4172
+ break;
4173
+ }
4174
+ }
4175
+ yield { type: "done" };
4176
+ }
4177
+ async *streamRun(userMessage, imageAttachment) {
4178
+ for await (const event of this.run(userMessage, imageAttachment)) {
4179
+ yield event;
4180
+ }
4181
+ }
4182
+ createToolContext() {
4183
+ return {
4184
+ sendToStudio: (type, payload, timeoutMs) => {
4185
+ return this.server.sendRequest(type, payload, timeoutMs);
4186
+ },
4187
+ isStudioConnected: () => this.server.isConnected(),
4188
+ memory: {
4189
+ recall: (query, projectId, limit) => {
4190
+ const { recallFacts: recallFacts2 } = (init_store(), __toCommonJS(store_exports));
4191
+ const facts = recallFacts2(projectId || this.projectId, query, limit);
4192
+ return facts.map((f) => f.content);
4193
+ },
4194
+ learn: (projectId, facts) => {
4195
+ const { storeFact: storeFact2 } = (init_store(), __toCommonJS(store_exports));
4196
+ const now = Date.now();
4197
+ for (const f of facts) {
4198
+ storeFact2({
4199
+ projectId: projectId || this.projectId,
4200
+ content: f.content,
4201
+ category: f.category,
4202
+ source: "agent",
4203
+ relevance: 1,
4204
+ createdAt: now,
4205
+ accessedAt: now
4206
+ });
4207
+ }
4208
+ },
4209
+ getScriptIndex: (projectId) => {
4210
+ const { getScriptIndex: getScriptIndex2 } = (init_store(), __toCommonJS(store_exports));
4211
+ return getScriptIndex2(projectId || this.projectId);
4212
+ }
4213
+ },
4214
+ config: createConfigAccess()
4215
+ };
4216
+ }
4217
+ clearHistory() {
4218
+ this.conversationHistory = [];
4219
+ this.sessionId = nanoid();
4220
+ }
4221
+ };
4222
+
4223
+ // src/tools/registry.ts
4224
+ var ToolRegistry = class {
4225
+ tools = /* @__PURE__ */ new Map();
4226
+ register(tool24) {
4227
+ if (this.tools.has(tool24.name)) {
4228
+ throw new Error(`Tool already registered: ${tool24.name}`);
4229
+ }
4230
+ this.tools.set(tool24.name, tool24);
4231
+ }
4232
+ get(name) {
4233
+ return this.tools.get(name);
4234
+ }
4235
+ getAll() {
4236
+ return Array.from(this.tools.values());
4237
+ }
4238
+ getSchemas() {
4239
+ return this.getAll();
4240
+ }
4241
+ async execute(call, ctx) {
4242
+ const tool24 = this.tools.get(call.name);
4243
+ if (!tool24) {
4244
+ return { success: false, error: `Unknown tool: ${call.name}` };
4245
+ }
4246
+ try {
4247
+ return await tool24.execute(call.arguments, ctx);
4248
+ } catch (err) {
4249
+ const message = err instanceof Error ? err.message : String(err);
4250
+ return { success: false, error: message };
4251
+ }
4252
+ }
4253
+ listNames() {
4254
+ return Array.from(this.tools.keys());
4255
+ }
4256
+ };
4257
+ function createDefaultRegistry() {
4258
+ const registry = new ToolRegistry();
4259
+ const toolModules = [
4260
+ Promise.resolve().then(() => (init_read_script(), read_script_exports)),
4261
+ Promise.resolve().then(() => (init_edit_script(), edit_script_exports)),
4262
+ Promise.resolve().then(() => (init_run_code(), run_code_exports)),
4263
+ Promise.resolve().then(() => (init_get_explorer(), get_explorer_exports)),
4264
+ Promise.resolve().then(() => (init_get_properties(), get_properties_exports)),
4265
+ Promise.resolve().then(() => (init_set_properties(), set_properties_exports)),
4266
+ Promise.resolve().then(() => (init_insert_instance(), insert_instance_exports)),
4267
+ Promise.resolve().then(() => (init_delete_instance(), delete_instance_exports)),
4268
+ Promise.resolve().then(() => (init_get_output(), get_output_exports)),
4269
+ Promise.resolve().then(() => (init_get_selection(), get_selection_exports)),
4270
+ Promise.resolve().then(() => (init_search_scripts(), search_scripts_exports)),
4271
+ Promise.resolve().then(() => (init_run_tests(), run_tests_exports)),
4272
+ Promise.resolve().then(() => (init_get_class_info(), get_class_info_exports)),
4273
+ Promise.resolve().then(() => (init_create_ui(), create_ui_exports)),
4274
+ Promise.resolve().then(() => (init_execute_run_test(), execute_run_test_exports)),
4275
+ Promise.resolve().then(() => (init_execute_play_test(), execute_play_test_exports)),
4276
+ Promise.resolve().then(() => (init_create_part(), create_part_exports)),
4277
+ Promise.resolve().then(() => (init_clone_instance(), clone_instance_exports)),
4278
+ Promise.resolve().then(() => (init_group_instances(), group_instances_exports)),
4279
+ Promise.resolve().then(() => (init_build_multiple(), build_multiple_exports)),
4280
+ Promise.resolve().then(() => (init_recall(), recall_exports)),
4281
+ Promise.resolve().then(() => (init_remember(), remember_exports)),
4282
+ Promise.resolve().then(() => (init_upload_asset(), upload_asset_exports))
4283
+ ];
4284
+ void Promise.all(
4285
+ toolModules.map(async (mod) => {
4286
+ const m = await mod;
4287
+ if (m.tool) registry.register(m.tool);
4288
+ })
4289
+ );
4290
+ return registry;
4291
+ }
4292
+
4293
+ // src/index.tsx
4294
+ init_store();
4295
+ init_config();
4296
+ var VERSION = "0.1.0";
4297
+ var program = new Command();
4298
+ program.name("dominus").description("AI-powered autonomous agent for Roblox Studio development").version(VERSION);
4299
+ program.command("start", { isDefault: true }).description("Launch the Dominus TUI agent").option("-p, --port <port>", "WebSocket server port", "18088").action(async (opts) => {
4300
+ const config = loadConfig();
4301
+ const port = parseInt(opts.port) || config.port;
4302
+ if (!config.apiKey) {
4303
+ const detected = detectProviderFromEnv();
4304
+ if (detected) {
4305
+ setConfigValue("provider", detected.provider);
4306
+ setConfigValue("apiKey", detected.apiKey);
4307
+ }
4308
+ }
4309
+ if (config.userName === "Developer") {
4310
+ const envUser = process.env.USER || process.env.USERNAME || "Developer";
4311
+ const firstName = envUser.split(" ")[0];
4312
+ if (firstName && firstName !== "Developer") {
4313
+ setConfigValue("userName", firstName);
4314
+ }
4315
+ }
4316
+ await launchApp(port);
4317
+ });
4318
+ var configCmd = program.command("config").description("Manage Dominus configuration");
4319
+ configCmd.command("set <key> <value>").description("Set a config value").action((key, value) => {
4320
+ const numericKeys = ["port", "maxToolIterations", "maxVerifyRetries", "tokenBudget"];
4321
+ const booleanKeys = ["autoIndex", "autoLearn"];
4322
+ const validProviders = ["openai", "anthropic", "gemini", "xai", "openrouter", "custom"];
4323
+ let parsed = value;
4324
+ if (numericKeys.includes(key)) {
4325
+ parsed = parseInt(value);
4326
+ } else if (booleanKeys.includes(key)) {
4327
+ parsed = value === "true";
4328
+ } else if (key === "provider") {
4329
+ if (!validProviders.includes(value)) {
4330
+ console.log(` \u2716 Invalid provider: ${value}`);
4331
+ console.log(` Valid providers: ${validProviders.join(", ")}`);
4332
+ return;
4333
+ }
4334
+ const providerInfo = PROVIDERS[value];
4335
+ setConfigValue("apiBaseUrl", providerInfo.baseUrl);
4336
+ setConfigValue("model", providerInfo.defaultModel);
4337
+ }
4338
+ setConfigValue(key, parsed);
4339
+ console.log(` \u2714 Set ${key} = ${value}`);
4340
+ if (key === "provider") {
4341
+ const p = PROVIDERS[value];
4342
+ console.log(` \u2192 Base URL: ${p.baseUrl}`);
4343
+ console.log(` \u2192 Default model: ${p.defaultModel}`);
4344
+ }
4345
+ console.log(` Config stored at: ${getDominusDir()}/config.json`);
4346
+ });
4347
+ configCmd.command("get <key>").description("Get a config value").action((key) => {
4348
+ const value = getConfigValue(key);
4349
+ if (key.toLowerCase().includes("key") && typeof value === "string") {
4350
+ console.log(` ${key} = ${value.slice(0, 8)}...${value.slice(-4)}`);
4351
+ } else {
4352
+ console.log(` ${key} = ${JSON.stringify(value)}`);
4353
+ }
4354
+ });
4355
+ configCmd.command("list").description("List all config values").action(() => {
4356
+ const config = loadConfig();
4357
+ console.log("");
4358
+ console.log(" Dominus Configuration:");
4359
+ console.log(" \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500");
4360
+ for (const [key, value] of Object.entries(config)) {
4361
+ const display = key.toLowerCase().includes("key") && typeof value === "string" && value.length > 12 ? `${value.slice(0, 8)}...${value.slice(-4)}` : JSON.stringify(value);
4362
+ console.log(` ${key}: ${display}`);
4363
+ }
4364
+ console.log("");
4365
+ console.log(` Config file: ${getDominusDir()}/config.json`);
4366
+ console.log("");
4367
+ });
4368
+ var rulesCmd = program.command("rules").description("Manage agent rules (do/don't)");
4369
+ rulesCmd.command("add-do <rule>").description("Add a rule for things Dominus should do").action((rule) => {
4370
+ const rules = loadRules();
4371
+ rules.do.push(rule);
4372
+ saveRules(rules);
4373
+ console.log(` \u2714 Added DO rule: ${rule}`);
4374
+ });
4375
+ rulesCmd.command("add-dont <rule>").description("Add a rule for things Dominus should NOT do").action((rule) => {
4376
+ const rules = loadRules();
4377
+ rules.dont.push(rule);
4378
+ saveRules(rules);
4379
+ console.log(` \u2714 Added DON'T rule: ${rule}`);
4380
+ });
4381
+ rulesCmd.command("list").description("List all rules").action(() => {
4382
+ const rules = loadRules();
4383
+ console.log("");
4384
+ console.log(" Dominus Rules:");
4385
+ if (rules.do.length) {
4386
+ console.log(" DO:");
4387
+ rules.do.forEach((r, i) => console.log(` ${i + 1}. ${r}`));
4388
+ }
4389
+ if (rules.dont.length) {
4390
+ console.log(" DON'T:");
4391
+ rules.dont.forEach((r, i) => console.log(` ${i + 1}. ${r}`));
4392
+ }
4393
+ if (rules.conventions.length) {
4394
+ console.log(" CONVENTIONS:");
4395
+ rules.conventions.forEach((r, i) => console.log(` ${i + 1}. ${r}`));
4396
+ }
4397
+ if (!rules.do.length && !rules.dont.length && !rules.conventions.length) {
4398
+ console.log(" No rules configured yet.");
4399
+ console.log(' Use: dominus rules add-do "Always use strict typing"');
4400
+ console.log(' Use: dominus rules add-dont "Never use deprecated APIs"');
4401
+ }
4402
+ console.log("");
4403
+ });
4404
+ program.command("install-plugin").description("Build & install the Dominus plugin into Roblox Studio").action(async () => {
4405
+ const { execSync: execSync2 } = await import('child_process');
4406
+ const packageRoot = path3.resolve(import.meta.dirname ?? __dirname, "..");
4407
+ const pluginDir = path3.join(packageRoot, "plugin");
4408
+ const pluginProject = path3.join(pluginDir, "default.project.json");
4409
+ const studioPlugins = path3.join(
4410
+ os2.homedir(),
4411
+ "AppData",
4412
+ "Local",
4413
+ "Roblox",
4414
+ "Plugins"
4415
+ );
4416
+ const destFile = path3.join(studioPlugins, "Dominus.rbxm");
4417
+ if (!fs4.existsSync(pluginProject)) {
4418
+ console.log(" \u2716 Plugin source not found.");
4419
+ console.log(` Expected at: ${pluginDir}`);
4420
+ return;
4421
+ }
4422
+ if (!fs4.existsSync(studioPlugins)) {
4423
+ fs4.mkdirSync(studioPlugins, { recursive: true });
4424
+ }
4425
+ let rojoAvailable = false;
4426
+ try {
4427
+ execSync2("rojo --version", { stdio: "pipe" });
4428
+ rojoAvailable = true;
4429
+ } catch {
4430
+ }
4431
+ if (rojoAvailable) {
4432
+ try {
4433
+ console.log(" Building plugin with Rojo...");
4434
+ execSync2(`rojo build "${pluginDir}" -o "${destFile}"`, { stdio: "pipe" });
4435
+ console.log(` \u2714 Plugin built and installed to: ${destFile}`);
4436
+ console.log(" Restart Roblox Studio to load the plugin.");
4437
+ } catch (err) {
4438
+ console.log(` \u2716 Rojo build failed: ${err instanceof Error ? err.message : err}`);
4439
+ }
4440
+ } else {
4441
+ console.log(" Rojo not found \u2014 building plugin manually...");
4442
+ buildPluginXml(pluginDir, destFile.replace(".rbxm", ".rbxmx"));
4443
+ }
4444
+ });
4445
+ program.command("mcp").description("Start the Dominus MCP server (for Cursor, Claude Desktop, etc.)").option("-p, --port <port>", "WebSocket server port for Studio connection", "18088").action(async () => {
4446
+ const { startMcpServer: startMcpServer2 } = await Promise.resolve().then(() => (init_server(), server_exports));
4447
+ await startMcpServer2();
4448
+ });
4449
+ function getMcpTargets() {
4450
+ const home = os2.homedir();
4451
+ const appData = process.env.APPDATA || path3.join(home, "AppData", "Roaming");
4452
+ const localAppData = process.env.LOCALAPPDATA || path3.join(home, "AppData", "Local");
4453
+ const cwd = process.cwd();
4454
+ return [
4455
+ {
4456
+ name: "Cursor (project)",
4457
+ configPath: path3.join(cwd, ".cursor", "mcp.json"),
4458
+ keyPath: ["mcpServers"]
4459
+ },
4460
+ {
4461
+ name: "Cursor (global)",
4462
+ configPath: path3.join(home, ".cursor", "mcp.json"),
4463
+ keyPath: ["mcpServers"]
4464
+ },
4465
+ {
4466
+ name: "Claude Desktop",
4467
+ configPath: path3.join(appData, "Claude", "claude_desktop_config.json"),
4468
+ keyPath: ["mcpServers"]
4469
+ },
4470
+ {
4471
+ name: "Claude Code",
4472
+ configPath: path3.join(home, ".claude.json"),
4473
+ keyPath: ["mcpServers"]
4474
+ },
4475
+ {
4476
+ name: "Windsurf (project)",
4477
+ configPath: path3.join(cwd, ".windsurf", "mcp.json"),
4478
+ keyPath: ["mcpServers"]
4479
+ },
4480
+ {
4481
+ name: "Windsurf (global)",
4482
+ configPath: path3.join(home, ".codeium", "windsurf", "mcp_config.json"),
4483
+ keyPath: ["mcpServers"]
4484
+ },
4485
+ {
4486
+ name: "VS Code (project)",
4487
+ configPath: path3.join(cwd, ".vscode", "mcp.json"),
4488
+ keyPath: ["servers"]
4489
+ },
4490
+ {
4491
+ name: "VS Code (global)",
4492
+ configPath: path3.join(localAppData, "Code", "User", "settings.json"),
4493
+ keyPath: ["mcp", "servers"]
4494
+ }
4495
+ ];
4496
+ }
4497
+ var dominusEntry = {
4498
+ command: "dominus",
4499
+ args: ["mcp"],
4500
+ type: "stdio"
4501
+ };
4502
+ function injectMcpConfig(target) {
4503
+ let config = {};
4504
+ if (fs4.existsSync(target.configPath)) {
4505
+ try {
4506
+ config = JSON.parse(fs4.readFileSync(target.configPath, "utf-8"));
4507
+ } catch {
4508
+ config = {};
4509
+ }
4510
+ }
4511
+ let obj = config;
4512
+ for (const key of target.keyPath) {
4513
+ if (!obj[key] || typeof obj[key] !== "object") {
4514
+ obj[key] = {};
4515
+ }
4516
+ obj = obj[key];
4517
+ }
4518
+ if (obj["dominus"]) {
4519
+ return { status: "already" };
4520
+ }
4521
+ obj["dominus"] = { ...dominusEntry };
4522
+ const dir = path3.dirname(target.configPath);
4523
+ if (!fs4.existsSync(dir)) {
4524
+ fs4.mkdirSync(dir, { recursive: true });
4525
+ }
4526
+ fs4.writeFileSync(target.configPath, JSON.stringify(config, null, 2), "utf-8");
4527
+ return { status: fs4.existsSync(target.configPath) ? "installed" : "created" };
4528
+ }
4529
+ program.command("mcp-install").description("Auto-install Dominus MCP into Cursor, VS Code, Claude Desktop, Windsurf, etc.").option("-a, --all", "Install to all detected editors").action(async (opts) => {
4530
+ const targets = getMcpTargets();
4531
+ console.log("");
4532
+ console.log(" Dominus MCP Auto-Installer");
4533
+ console.log(" \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550");
4534
+ console.log("");
4535
+ if (opts.all) {
4536
+ for (const target of targets) {
4537
+ const result = injectMcpConfig(target);
4538
+ const icon = result.status === "already" ? "\u25CF" : "\u2714";
4539
+ const msg = result.status === "already" ? "already configured" : "installed";
4540
+ console.log(` ${icon} ${target.name}: ${msg}`);
4541
+ }
4542
+ console.log("");
4543
+ console.log(" Restart your editors to load the Dominus MCP server.");
4544
+ console.log("");
4545
+ return;
4546
+ }
4547
+ const detected = targets.map((t) => ({
4548
+ target: t,
4549
+ exists: fs4.existsSync(path3.dirname(t.configPath))
4550
+ }));
4551
+ const toInstall = detected.filter((d) => d.exists);
4552
+ if (toInstall.length === 0) {
4553
+ console.log(" No supported editors detected.");
4554
+ console.log(" Run with --all to install configs anyway,");
4555
+ console.log(" or manually add to your editor's MCP config:");
4556
+ console.log("");
4557
+ console.log(" {");
4558
+ console.log(' "dominus": {');
4559
+ console.log(' "command": "dominus",');
4560
+ console.log(' "args": ["mcp"]');
4561
+ console.log(" }");
4562
+ console.log(" }");
4563
+ console.log("");
4564
+ return;
4565
+ }
4566
+ console.log(" Detected editors:");
4567
+ console.log("");
4568
+ for (const { target } of toInstall) {
4569
+ const result = injectMcpConfig(target);
4570
+ const icon = result.status === "already" ? "\u25CF" : "\u2714";
4571
+ const msg = result.status === "already" ? "already configured" : "installed";
4572
+ console.log(` ${icon} ${target.name}: ${msg}`);
4573
+ console.log(` ${target.configPath}`);
4574
+ }
4575
+ const skipped = detected.filter((d) => !d.exists);
4576
+ if (skipped.length > 0) {
4577
+ console.log("");
4578
+ console.log(" Skipped (not detected):");
4579
+ for (const { target } of skipped) {
4580
+ console.log(` \u25CB ${target.name}`);
4581
+ }
4582
+ }
4583
+ console.log("");
4584
+ console.log(" Provides 14 Roblox Studio tools to your AI editor.");
4585
+ console.log(" Restart your editors to load the Dominus MCP server.");
4586
+ console.log("");
4587
+ });
4588
+ program.command("mcp-setup").description("Print MCP configuration for manual setup").action(() => {
4589
+ console.log("");
4590
+ console.log(" Dominus MCP \u2014 Manual Setup");
4591
+ console.log(" \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550");
4592
+ console.log("");
4593
+ console.log(" Tip: Run `dominus mcp-install` to auto-install instead!");
4594
+ console.log("");
4595
+ console.log(" Add this to your editor's MCP config:");
4596
+ console.log("");
4597
+ console.log(" {");
4598
+ console.log(' "dominus": {');
4599
+ console.log(' "command": "dominus",');
4600
+ console.log(' "args": ["mcp"]');
4601
+ console.log(" }");
4602
+ console.log(" }");
4603
+ console.log("");
4604
+ console.log(" Config file locations:");
4605
+ console.log("");
4606
+ console.log(" Cursor (project) .cursor/mcp.json");
4607
+ console.log(" Cursor (global) ~/.cursor/mcp.json");
4608
+ console.log(" Claude Desktop %APPDATA%/Claude/claude_desktop_config.json");
4609
+ console.log(" Claude Code ~/.claude.json");
4610
+ console.log(" Windsurf (project) .windsurf/mcp.json");
4611
+ console.log(" Windsurf (global) ~/.codeium/windsurf/mcp_config.json");
4612
+ console.log(" VS Code (project) .vscode/mcp.json");
4613
+ console.log(" VS Code (global) %LOCALAPPDATA%/Code/User/settings.json");
4614
+ console.log("");
4615
+ console.log(" 14 tools: read_script, edit_script, run_code, get_explorer,");
4616
+ console.log(" get_properties, set_properties, insert_instance, delete_instance,");
4617
+ console.log(" get_selection, search_scripts, get_output, run_tests,");
4618
+ console.log(" recall_memory, remember");
4619
+ console.log("");
4620
+ });
4621
+ function buildPluginXml(pluginDir, destFile) {
4622
+ const srcDir = path3.join(pluginDir, "src");
4623
+ if (!fs4.existsSync(srcDir)) {
4624
+ console.log(` \u2716 Plugin source directory not found: ${srcDir}`);
4625
+ return;
4626
+ }
4627
+ const files = fs4.readdirSync(srcDir).filter((f) => f.endsWith(".lua"));
4628
+ const mainFile = files.find((f) => f === "init.server.lua");
4629
+ const moduleFiles = files.filter((f) => f !== "init.server.lua");
4630
+ if (!mainFile) {
4631
+ console.log(" \u2716 init.server.lua not found in plugin source");
4632
+ return;
4633
+ }
4634
+ const mainSource = fs4.readFileSync(path3.join(srcDir, mainFile), "utf-8");
4635
+ let moduleItems = "";
4636
+ for (const mf of moduleFiles) {
4637
+ const modName = mf.replace(".lua", "");
4638
+ const modSource = fs4.readFileSync(path3.join(srcDir, mf), "utf-8");
4639
+ moduleItems += `
4640
+ <Item class="ModuleScript" referent="${modName}">
4641
+ <Properties>
4642
+ <string name="Name">${modName}</string>
4643
+ <ProtectedString name="Source"><![CDATA[${modSource}]]></ProtectedString>
4644
+ </Properties>
4645
+ </Item>`;
4646
+ }
4647
+ const rbxmx = `<roblox version="4">
4648
+ <Item class="Script" referent="Dominus">
4649
+ <Properties>
4650
+ <string name="Name">Dominus</string>
4651
+ <ProtectedString name="Source"><![CDATA[${mainSource}]]></ProtectedString>
4652
+ <token name="RunContext">1</token>
4653
+ </Properties>${moduleItems}
4654
+ </Item>
4655
+ </roblox>`;
4656
+ fs4.writeFileSync(destFile, rbxmx, "utf-8");
4657
+ console.log(` \u2714 Plugin installed to: ${destFile}`);
4658
+ console.log(" Restart Roblox Studio to load the plugin.");
4659
+ }
4660
+ async function launchApp(port) {
4661
+ await initMemoryStore();
4662
+ let server;
4663
+ const portTaken = await isPortInUse(port);
4664
+ if (portTaken) {
4665
+ const client = new DominusClient(port);
4666
+ await client.connect();
4667
+ server = client;
4668
+ } else {
4669
+ const srv = new DominusServer(port);
4670
+ await srv.start();
4671
+ server = srv;
4672
+ }
4673
+ const config = loadConfig();
4674
+ let agent = null;
4675
+ if (config.apiKey) {
4676
+ const tools = createDefaultRegistry();
4677
+ await new Promise((r) => setTimeout(r, 100));
4678
+ agent = new AgentLoop(server, tools);
4679
+ }
4680
+ const { waitUntilExit } = render(
4681
+ /* @__PURE__ */ jsx(App, { server, agent, version: VERSION })
4682
+ );
4683
+ const cleanup = async () => {
4684
+ closeMemoryStore();
4685
+ await server.stop();
4686
+ };
4687
+ process.on("SIGINT", async () => {
4688
+ await cleanup();
4689
+ process.exit(0);
4690
+ });
4691
+ process.on("SIGTERM", async () => {
4692
+ await cleanup();
4693
+ process.exit(0);
4694
+ });
4695
+ await waitUntilExit();
4696
+ await cleanup();
4697
+ }
4698
+ program.parse();
4699
+ //# sourceMappingURL=index.js.map
4700
+ //# sourceMappingURL=index.js.map