dominus-cli 0.2.0 → 0.5.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 CHANGED
@@ -1,7 +1,7 @@
1
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';
2
+ import fs5, { existsSync, readFileSync, unlinkSync } from 'fs';
3
+ import path4, { join } from 'path';
4
+ import os3, { tmpdir } from 'os';
5
5
  import { nanoid } from 'nanoid';
6
6
  import { WebSocket, WebSocketServer } from 'ws';
7
7
  import { EventEmitter } from 'events';
@@ -18,6 +18,7 @@ import { execSync } from 'child_process';
18
18
  import { randomBytes } from 'crypto';
19
19
  import Spinner from 'ink-spinner';
20
20
  import OpenAI from 'openai';
21
+ import https from 'https';
21
22
 
22
23
  var __defProp = Object.defineProperty;
23
24
  var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
@@ -55,14 +56,14 @@ function resolveProviderSettings(config) {
55
56
  };
56
57
  }
57
58
  function ensureDir() {
58
- if (!fs4.existsSync(DOMINUS_DIR)) {
59
- fs4.mkdirSync(DOMINUS_DIR, { recursive: true });
59
+ if (!fs5.existsSync(DOMINUS_DIR)) {
60
+ fs5.mkdirSync(DOMINUS_DIR, { recursive: true });
60
61
  }
61
62
  }
62
63
  function readJson(filePath, defaults) {
63
64
  try {
64
- if (fs4.existsSync(filePath)) {
65
- const raw = fs4.readFileSync(filePath, "utf-8");
65
+ if (fs5.existsSync(filePath)) {
66
+ const raw = fs5.readFileSync(filePath, "utf-8");
66
67
  return { ...defaults, ...JSON.parse(raw) };
67
68
  }
68
69
  } catch {
@@ -71,7 +72,7 @@ function readJson(filePath, defaults) {
71
72
  }
72
73
  function writeJson(filePath, data) {
73
74
  ensureDir();
74
- fs4.writeFileSync(filePath, JSON.stringify(data, null, 2), "utf-8");
75
+ fs5.writeFileSync(filePath, JSON.stringify(data, null, 2), "utf-8");
75
76
  }
76
77
  function loadConfig() {
77
78
  ensureDir();
@@ -123,10 +124,10 @@ function createConfigAccess() {
123
124
  var DOMINUS_DIR, CONFIG_PATH, DB_PATH, RULES_PATH, PROVIDERS, DEFAULTS, DEFAULT_RULES;
124
125
  var init_config = __esm({
125
126
  "src/config/config.ts"() {
126
- DOMINUS_DIR = 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");
127
+ DOMINUS_DIR = path4.join(os3.homedir(), ".dominus");
128
+ CONFIG_PATH = path4.join(DOMINUS_DIR, "config.json");
129
+ DB_PATH = path4.join(DOMINUS_DIR, "dominus.db");
130
+ RULES_PATH = path4.join(DOMINUS_DIR, "rules.json");
130
131
  PROVIDERS = {
131
132
  openai: {
132
133
  name: "OpenAI",
@@ -220,6 +221,7 @@ var init_protocol = __esm({
220
221
  SET_SCRIPT: "cli:set:script",
221
222
  RUN_CODE: "cli:run",
222
223
  GET_PROPERTIES: "cli:get:properties",
224
+ GET_DESCENDANTS_PROPERTIES: "cli:get:descendants:properties",
223
225
  SET_PROPERTIES: "cli:set:properties",
224
226
  INSERT_INSTANCE: "cli:insert",
225
227
  DELETE_INSTANCE: "cli:delete",
@@ -233,7 +235,13 @@ var init_protocol = __esm({
233
235
  END_TEST: "cli:test:end",
234
236
  BUILD_UI: "cli:build:ui",
235
237
  GET_REFLECTION: "cli:get:reflection",
236
- UPLOAD_ASSET: "cli:upload:asset"
238
+ UPLOAD_ASSET: "cli:upload:asset",
239
+ SERIALIZE_UI: "cli:serialize:ui",
240
+ MOVE_INSTANCE: "cli:move:instance",
241
+ UNDO: "cli:undo",
242
+ REDO: "cli:redo",
243
+ BULK_SET_PROPERTIES: "cli:bulk:set:properties",
244
+ FIND_REPLACE_SCRIPTS: "cli:find:replace:scripts"
237
245
  };
238
246
  }
239
247
  });
@@ -641,14 +649,14 @@ __export(store_exports, {
641
649
  function persist() {
642
650
  if (!db) return;
643
651
  const data = db.export();
644
- fs4.writeFileSync(dbPath, Buffer.from(data));
652
+ fs5.writeFileSync(dbPath, Buffer.from(data));
645
653
  }
646
654
  async function initMemoryStore() {
647
655
  if (db) return;
648
656
  dbPath = getDbPath();
649
657
  const SQL = await initSqlJs();
650
- if (fs4.existsSync(dbPath)) {
651
- const buffer = fs4.readFileSync(dbPath);
658
+ if (fs5.existsSync(dbPath)) {
659
+ const buffer = fs5.readFileSync(dbPath);
652
660
  db = new SQL.Database(buffer);
653
661
  } else {
654
662
  db = new SQL.Database();
@@ -939,10 +947,10 @@ var init_read_script = __esm({
939
947
  if (!ctx.isStudioConnected()) {
940
948
  return { success: false, error: "Studio is not connected" };
941
949
  }
942
- const path4 = params.path;
943
- const response = await ctx.sendToStudio(CliMsg.GET_SCRIPT, { path: path4 });
950
+ const path5 = params.path;
951
+ const response = await ctx.sendToStudio(CliMsg.GET_SCRIPT, { path: path5 });
944
952
  if (!response.payload) {
945
- return { success: false, error: `Script not found: ${path4}` };
953
+ return { success: false, error: `Script not found: ${path5}` };
946
954
  }
947
955
  return {
948
956
  success: true,
@@ -988,14 +996,14 @@ var init_edit_script = __esm({
988
996
  if (!ctx.isStudioConnected()) {
989
997
  return { success: false, error: "Studio is not connected" };
990
998
  }
991
- const path4 = params.path;
999
+ const path5 = params.path;
992
1000
  const source = params.source;
993
- const response = await ctx.sendToStudio(CliMsg.SET_SCRIPT, { path: path4, source });
1001
+ const response = await ctx.sendToStudio(CliMsg.SET_SCRIPT, { path: path5, source });
994
1002
  const payload = response.payload;
995
1003
  if (!payload.success) {
996
1004
  return { success: false, error: payload.error ?? "Failed to edit script" };
997
1005
  }
998
- return { success: true, data: { path: path4, linesWritten: source.split("\n").length } };
1006
+ return { success: true, data: { path: path5, linesWritten: source.split("\n").length } };
999
1007
  }
1000
1008
  };
1001
1009
  }
@@ -1118,16 +1126,65 @@ var init_get_properties = __esm({
1118
1126
  }
1119
1127
  });
1120
1128
 
1129
+ // src/tools/studio/get-descendants-properties.ts
1130
+ var get_descendants_properties_exports = {};
1131
+ __export(get_descendants_properties_exports, {
1132
+ tool: () => tool6
1133
+ });
1134
+ var tool6;
1135
+ var init_get_descendants_properties = __esm({
1136
+ "src/tools/studio/get-descendants-properties.ts"() {
1137
+ init_protocol();
1138
+ tool6 = {
1139
+ name: "get_descendants_properties",
1140
+ description: "Easily get all descendants of a UI instance (or any instance) and all of their properties.",
1141
+ parameters: {
1142
+ type: "object",
1143
+ properties: {
1144
+ path: {
1145
+ type: "string",
1146
+ description: 'Full instance path (e.g., "Workspace.Part", "StarterGui.ScreenGui")'
1147
+ },
1148
+ outputFile: {
1149
+ type: "string",
1150
+ description: "Optional file path to write the JSON result to, useful for very large trees where context limits apply."
1151
+ }
1152
+ },
1153
+ required: ["path"]
1154
+ },
1155
+ async execute(params, ctx) {
1156
+ if (!ctx.isStudioConnected()) {
1157
+ return { success: false, error: "Studio is not connected" };
1158
+ }
1159
+ const response = await ctx.sendToStudio(
1160
+ CliMsg.GET_DESCENDANTS_PROPERTIES,
1161
+ { path: params.path },
1162
+ 1e3 * 60 * 5
1163
+ // 5 minute timeout since getting ALL descendants can take a very long time
1164
+ );
1165
+ if (params.outputFile && typeof params.outputFile === "string") {
1166
+ const fs6 = await import('fs/promises');
1167
+ const path5 = await import('path');
1168
+ const outputPath = path5.resolve(process.cwd(), params.outputFile);
1169
+ await fs6.writeFile(outputPath, JSON.stringify(response.payload, null, 2), "utf-8");
1170
+ return { success: true, data: `Successfully saved descendants to ${outputPath}` };
1171
+ }
1172
+ return { success: true, data: response.payload };
1173
+ }
1174
+ };
1175
+ }
1176
+ });
1177
+
1121
1178
  // src/tools/studio/set-properties.ts
1122
1179
  var set_properties_exports = {};
1123
1180
  __export(set_properties_exports, {
1124
- tool: () => tool6
1181
+ tool: () => tool7
1125
1182
  });
1126
- var tool6;
1183
+ var tool7;
1127
1184
  var init_set_properties = __esm({
1128
1185
  "src/tools/studio/set-properties.ts"() {
1129
1186
  init_protocol();
1130
- tool6 = {
1187
+ tool7 = {
1131
1188
  name: "set_properties",
1132
1189
  description: "Set properties on an instance in Roblox Studio.",
1133
1190
  parameters: {
@@ -1165,13 +1222,13 @@ var init_set_properties = __esm({
1165
1222
  // src/tools/studio/insert-instance.ts
1166
1223
  var insert_instance_exports = {};
1167
1224
  __export(insert_instance_exports, {
1168
- tool: () => tool7
1225
+ tool: () => tool8
1169
1226
  });
1170
- var tool7;
1227
+ var tool8;
1171
1228
  var init_insert_instance = __esm({
1172
1229
  "src/tools/studio/insert-instance.ts"() {
1173
1230
  init_protocol();
1174
- tool7 = {
1231
+ tool8 = {
1175
1232
  name: "insert_instance",
1176
1233
  description: "Insert a new instance into the Roblox Studio DataModel.",
1177
1234
  parameters: {
@@ -1219,13 +1276,13 @@ var init_insert_instance = __esm({
1219
1276
  // src/tools/studio/delete-instance.ts
1220
1277
  var delete_instance_exports = {};
1221
1278
  __export(delete_instance_exports, {
1222
- tool: () => tool8
1279
+ tool: () => tool9
1223
1280
  });
1224
- var tool8;
1281
+ var tool9;
1225
1282
  var init_delete_instance = __esm({
1226
1283
  "src/tools/studio/delete-instance.ts"() {
1227
1284
  init_protocol();
1228
- tool8 = {
1285
+ tool9 = {
1229
1286
  name: "delete_instance",
1230
1287
  description: "Delete an instance from the Roblox Studio DataModel by path.",
1231
1288
  parameters: {
@@ -1256,13 +1313,13 @@ var init_delete_instance = __esm({
1256
1313
  // src/tools/studio/get-output.ts
1257
1314
  var get_output_exports = {};
1258
1315
  __export(get_output_exports, {
1259
- tool: () => tool9
1316
+ tool: () => tool10
1260
1317
  });
1261
- var tool9;
1318
+ var tool10;
1262
1319
  var init_get_output = __esm({
1263
1320
  "src/tools/studio/get-output.ts"() {
1264
1321
  init_protocol();
1265
- tool9 = {
1322
+ tool10 = {
1266
1323
  name: "get_output",
1267
1324
  description: "Get recent output log entries from Roblox Studio (prints, warnings, errors).",
1268
1325
  parameters: {
@@ -1297,13 +1354,13 @@ var init_get_output = __esm({
1297
1354
  // src/tools/studio/get-selection.ts
1298
1355
  var get_selection_exports = {};
1299
1356
  __export(get_selection_exports, {
1300
- tool: () => tool10
1357
+ tool: () => tool11
1301
1358
  });
1302
- var tool10;
1359
+ var tool11;
1303
1360
  var init_get_selection = __esm({
1304
1361
  "src/tools/studio/get-selection.ts"() {
1305
1362
  init_protocol();
1306
- tool10 = {
1363
+ tool11 = {
1307
1364
  name: "get_selection",
1308
1365
  description: "Get the currently selected instances in Roblox Studio.",
1309
1366
  parameters: {
@@ -1324,13 +1381,13 @@ var init_get_selection = __esm({
1324
1381
  // src/tools/studio/search-scripts.ts
1325
1382
  var search_scripts_exports = {};
1326
1383
  __export(search_scripts_exports, {
1327
- tool: () => tool11
1384
+ tool: () => tool12
1328
1385
  });
1329
- var tool11;
1386
+ var tool12;
1330
1387
  var init_search_scripts = __esm({
1331
1388
  "src/tools/studio/search-scripts.ts"() {
1332
1389
  init_protocol();
1333
- tool11 = {
1390
+ tool12 = {
1334
1391
  name: "search_scripts",
1335
1392
  description: "Search across all scripts in Roblox Studio for a text pattern. Returns matching lines with context.",
1336
1393
  parameters: {
@@ -1365,13 +1422,13 @@ var init_search_scripts = __esm({
1365
1422
  // src/tools/studio/run-tests.ts
1366
1423
  var run_tests_exports = {};
1367
1424
  __export(run_tests_exports, {
1368
- tool: () => tool12
1425
+ tool: () => tool13
1369
1426
  });
1370
- var tool12;
1427
+ var tool13;
1371
1428
  var init_run_tests = __esm({
1372
1429
  "src/tools/studio/run-tests.ts"() {
1373
1430
  init_protocol();
1374
- tool12 = {
1431
+ tool13 = {
1375
1432
  name: "run_tests",
1376
1433
  description: "Run tests in Roblox Studio using StudioTestService. Can run all tests or a specific test by name.",
1377
1434
  parameters: {
@@ -1405,13 +1462,13 @@ var init_run_tests = __esm({
1405
1462
  // src/tools/studio/get-class-info.ts
1406
1463
  var get_class_info_exports = {};
1407
1464
  __export(get_class_info_exports, {
1408
- tool: () => tool13
1465
+ tool: () => tool14
1409
1466
  });
1410
- var tool13;
1467
+ var tool14;
1411
1468
  var init_get_class_info = __esm({
1412
1469
  "src/tools/studio/get-class-info.ts"() {
1413
1470
  init_protocol();
1414
- tool13 = {
1471
+ tool14 = {
1415
1472
  name: "get_class_info",
1416
1473
  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).",
1417
1474
  parameters: {
@@ -1442,13 +1499,13 @@ var init_get_class_info = __esm({
1442
1499
  // src/tools/studio/create-ui.ts
1443
1500
  var create_ui_exports = {};
1444
1501
  __export(create_ui_exports, {
1445
- tool: () => tool14
1502
+ tool: () => tool15
1446
1503
  });
1447
- var tool14;
1504
+ var tool15;
1448
1505
  var init_create_ui = __esm({
1449
1506
  "src/tools/studio/create-ui.ts"() {
1450
1507
  init_protocol();
1451
- tool14 = {
1508
+ tool15 = {
1452
1509
  name: "create_ui",
1453
1510
  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}',
1454
1511
  parameters: {
@@ -1460,7 +1517,11 @@ var init_create_ui = __esm({
1460
1517
  },
1461
1518
  tree: {
1462
1519
  type: "object",
1463
- 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}'
1520
+ 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} (auto-disambiguated from UDim by property name)\n NumberRange: [min, max] or number\n Rect: [minX, minY, maxX, maxY] or {MinX, MinY, MaxX, MaxY}\n Font: "GothamBold", "SourceSans", etc. (Enum.Font names)\n Enums: string name like "Center", "Left", "Fit"\n FontFace: {Family, Weight, Style}\n 2-element arrays are auto-disambiguated: AnchorPoint\u2192Vector2, CornerRadius/Padding\u2192UDim, etc.'
1521
+ },
1522
+ animate: {
1523
+ type: "boolean",
1524
+ description: "If true, task.wait() will be injected between creation of nodes so that the UI writes out dynamically."
1464
1525
  }
1465
1526
  },
1466
1527
  required: ["tree"]
@@ -1471,6 +1532,7 @@ var init_create_ui = __esm({
1471
1532
  }
1472
1533
  const response = await ctx.sendToStudio(CliMsg.BUILD_UI, {
1473
1534
  parent: params.parent ?? "StarterGui",
1535
+ animate: params.animate,
1474
1536
  tree: params.tree
1475
1537
  });
1476
1538
  const payload = response.payload;
@@ -1489,13 +1551,13 @@ var init_create_ui = __esm({
1489
1551
  // src/tools/studio/execute-run-test.ts
1490
1552
  var execute_run_test_exports = {};
1491
1553
  __export(execute_run_test_exports, {
1492
- tool: () => tool15
1554
+ tool: () => tool16
1493
1555
  });
1494
- var tool15;
1556
+ var tool16;
1495
1557
  var init_execute_run_test = __esm({
1496
1558
  "src/tools/studio/execute-run-test.ts"() {
1497
1559
  init_protocol();
1498
- tool15 = {
1560
+ tool16 = {
1499
1561
  name: "execute_run_test",
1500
1562
  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.",
1501
1563
  parameters: {
@@ -1534,13 +1596,13 @@ var init_execute_run_test = __esm({
1534
1596
  // src/tools/studio/execute-play-test.ts
1535
1597
  var execute_play_test_exports = {};
1536
1598
  __export(execute_play_test_exports, {
1537
- tool: () => tool16
1599
+ tool: () => tool17
1538
1600
  });
1539
- var tool16;
1601
+ var tool17;
1540
1602
  var init_execute_play_test = __esm({
1541
1603
  "src/tools/studio/execute-play-test.ts"() {
1542
1604
  init_protocol();
1543
- tool16 = {
1605
+ tool17 = {
1544
1606
  name: "execute_play_test",
1545
1607
  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.",
1546
1608
  parameters: {
@@ -1579,13 +1641,13 @@ var init_execute_play_test = __esm({
1579
1641
  // src/tools/studio/create-part.ts
1580
1642
  var create_part_exports = {};
1581
1643
  __export(create_part_exports, {
1582
- tool: () => tool17
1644
+ tool: () => tool18
1583
1645
  });
1584
- var tool17;
1646
+ var tool18;
1585
1647
  var init_create_part = __esm({
1586
1648
  "src/tools/studio/create-part.ts"() {
1587
1649
  init_protocol();
1588
- tool17 = {
1650
+ tool18 = {
1589
1651
  name: "create_part",
1590
1652
  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.",
1591
1653
  parameters: {
@@ -1678,11 +1740,11 @@ var init_create_part = __esm({
1678
1740
  // src/tools/studio/clone-instance.ts
1679
1741
  var clone_instance_exports = {};
1680
1742
  __export(clone_instance_exports, {
1681
- tool: () => tool18
1743
+ tool: () => tool19
1682
1744
  });
1683
1745
  function buildCloneCode(params) {
1684
- const path4 = params.path;
1685
- const parts = path4.split(".");
1746
+ const path5 = params.path;
1747
+ const parts = path5.split(".");
1686
1748
  let resolve = "game";
1687
1749
  for (const p of parts) {
1688
1750
  resolve += `["${p}"]`;
@@ -1713,11 +1775,11 @@ function buildCloneCode(params) {
1713
1775
  lines.push(`print("Cloned: " .. clone:GetFullName())`);
1714
1776
  return lines.join("\n");
1715
1777
  }
1716
- var tool18;
1778
+ var tool19;
1717
1779
  var init_clone_instance = __esm({
1718
1780
  "src/tools/studio/clone-instance.ts"() {
1719
1781
  init_protocol();
1720
- tool18 = {
1782
+ tool19 = {
1721
1783
  name: "clone_instance",
1722
1784
  description: "Clone an existing instance in Roblox Studio and optionally reposition/rename it. Great for duplicating parts, models, or entire trees.",
1723
1785
  parameters: {
@@ -1753,13 +1815,13 @@ var init_clone_instance = __esm({
1753
1815
  // src/tools/studio/group-instances.ts
1754
1816
  var group_instances_exports = {};
1755
1817
  __export(group_instances_exports, {
1756
- tool: () => tool19
1818
+ tool: () => tool20
1757
1819
  });
1758
- var tool19;
1820
+ var tool20;
1759
1821
  var init_group_instances = __esm({
1760
1822
  "src/tools/studio/group-instances.ts"() {
1761
1823
  init_protocol();
1762
- tool19 = {
1824
+ tool20 = {
1763
1825
  name: "group_instances",
1764
1826
  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).',
1765
1827
  parameters: {
@@ -1819,12 +1881,12 @@ var init_group_instances = __esm({
1819
1881
  // src/tools/studio/build-multiple.ts
1820
1882
  var build_multiple_exports = {};
1821
1883
  __export(build_multiple_exports, {
1822
- tool: () => tool20
1884
+ tool: () => tool21
1823
1885
  });
1824
1886
  function generateBatchCode(parts, groupName, groupParent) {
1825
1887
  const lines = [];
1826
- function resolveParent(path4) {
1827
- const segs = path4.split(".");
1888
+ function resolveParent(path5) {
1889
+ const segs = path5.split(".");
1828
1890
  let r = "game";
1829
1891
  for (const s of segs) r += `["${s}"]`;
1830
1892
  return r;
@@ -1878,11 +1940,11 @@ function generateBatchCode(parts, groupName, groupParent) {
1878
1940
  lines.push(`print("Built ${parts.length} parts${groupName ? ` in ${groupName}` : ""}")`);
1879
1941
  return lines.join("\n");
1880
1942
  }
1881
- var tool20;
1943
+ var tool21;
1882
1944
  var init_build_multiple = __esm({
1883
1945
  "src/tools/studio/build-multiple.ts"() {
1884
1946
  init_protocol();
1885
- tool20 = {
1947
+ tool21 = {
1886
1948
  name: "build_multiple",
1887
1949
  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.",
1888
1950
  parameters: {
@@ -1951,16 +2013,234 @@ var init_build_multiple = __esm({
1951
2013
  }
1952
2014
  });
1953
2015
 
2016
+ // src/tools/studio/serialize-ui.ts
2017
+ var serialize_ui_exports = {};
2018
+ __export(serialize_ui_exports, {
2019
+ tool: () => tool22
2020
+ });
2021
+ var tool22;
2022
+ var init_serialize_ui = __esm({
2023
+ "src/tools/studio/serialize-ui.ts"() {
2024
+ init_protocol();
2025
+ tool22 = {
2026
+ name: "serialize_ui",
2027
+ description: "Serialize an existing UI tree from Roblox Studio into create_ui-compatible JSON. This is the reverse of create_ui \u2014 read any instance and get back a declarative JSON tree you can modify and pass back to create_ui. Perfect for cloning, templating, or refactoring UIs.\n\nThe output includes ClassName, Name, all non-default properties (with values converted to JSON-friendly formats like [r,g,b] for Color3, [xS,xO,yS,yO] for UDim2, etc.), and Children.",
2028
+ parameters: {
2029
+ type: "object",
2030
+ properties: {
2031
+ path: {
2032
+ type: "string",
2033
+ description: 'Instance path to serialize (e.g. "StarterGui.MyScreenGui")'
2034
+ },
2035
+ maxDepth: {
2036
+ type: "number",
2037
+ description: "Maximum depth to serialize (default: 50)"
2038
+ }
2039
+ },
2040
+ required: ["path"]
2041
+ },
2042
+ async execute(params, ctx) {
2043
+ if (!ctx.isStudioConnected()) {
2044
+ return { success: false, error: "Studio is not connected" };
2045
+ }
2046
+ const response = await ctx.sendToStudio(CliMsg.SERIALIZE_UI, {
2047
+ path: params.path,
2048
+ maxDepth: params.maxDepth ?? 50
2049
+ }, 1e3 * 60 * 2);
2050
+ const payload = response.payload;
2051
+ if (!payload.success) {
2052
+ return { success: false, error: payload.error ?? "Serialization failed" };
2053
+ }
2054
+ return { success: true, data: payload.tree };
2055
+ }
2056
+ };
2057
+ }
2058
+ });
2059
+
2060
+ // src/tools/studio/move-instance.ts
2061
+ var move_instance_exports = {};
2062
+ __export(move_instance_exports, {
2063
+ tool: () => tool23
2064
+ });
2065
+ var tool23;
2066
+ var init_move_instance = __esm({
2067
+ "src/tools/studio/move-instance.ts"() {
2068
+ init_protocol();
2069
+ tool23 = {
2070
+ name: "move_instance",
2071
+ description: "Move (reparent) an instance to a new parent in Roblox Studio. This preserves the instance and all its descendants \u2014 much better than delete + recreate. Sets a ChangeHistoryService waypoint so the move can be undone.",
2072
+ parameters: {
2073
+ type: "object",
2074
+ properties: {
2075
+ path: {
2076
+ type: "string",
2077
+ description: 'Full path of the instance to move (e.g. "Workspace.OldFolder.MyPart")'
2078
+ },
2079
+ newParent: {
2080
+ type: "string",
2081
+ description: 'Full path of the new parent (e.g. "Workspace.NewFolder")'
2082
+ }
2083
+ },
2084
+ required: ["path", "newParent"]
2085
+ },
2086
+ async execute(params, ctx) {
2087
+ if (!ctx.isStudioConnected()) {
2088
+ return { success: false, error: "Studio is not connected" };
2089
+ }
2090
+ const response = await ctx.sendToStudio(CliMsg.MOVE_INSTANCE, {
2091
+ path: params.path,
2092
+ newParent: params.newParent
2093
+ });
2094
+ const payload = response.payload;
2095
+ if (!payload.success) {
2096
+ return { success: false, error: payload.error ?? "Move failed" };
2097
+ }
2098
+ return { success: true, data: { newPath: payload.newPath } };
2099
+ }
2100
+ };
2101
+ }
2102
+ });
2103
+
2104
+ // src/tools/studio/bulk-set-properties.ts
2105
+ var bulk_set_properties_exports = {};
2106
+ __export(bulk_set_properties_exports, {
2107
+ tool: () => tool24
2108
+ });
2109
+ var tool24;
2110
+ var init_bulk_set_properties = __esm({
2111
+ "src/tools/studio/bulk-set-properties.ts"() {
2112
+ init_protocol();
2113
+ tool24 = {
2114
+ name: "bulk_set_properties",
2115
+ description: 'Set properties on multiple instances at once in a single call. Much faster than calling set_properties repeatedly. Supports two modes:\n1. Explicit paths: provide an array of {path, properties} objects\n2. By class filter: provide a root path, className filter, and properties to apply to all matches\n\nExample (explicit): [{"path": "StarterGui.HUD.Title", "properties": {"TextColor3": "#fff"}}, ...]\nExample (filter): root="StarterGui.HUD", className="TextLabel", properties={"Font": "GothamBold"}',
2116
+ parameters: {
2117
+ type: "object",
2118
+ properties: {
2119
+ targets: {
2120
+ type: "array",
2121
+ description: "Array of {path, properties} objects for explicit mode.",
2122
+ items: {
2123
+ type: "object",
2124
+ description: "A target with path and properties to set."
2125
+ }
2126
+ },
2127
+ root: {
2128
+ type: "string",
2129
+ description: "Root instance path to search under (filter mode)"
2130
+ },
2131
+ className: {
2132
+ type: "string",
2133
+ description: "Only apply to instances of this ClassName (filter mode)"
2134
+ },
2135
+ properties: {
2136
+ type: "object",
2137
+ description: "Properties to set on all matched instances (filter mode)"
2138
+ }
2139
+ }
2140
+ },
2141
+ async execute(params, ctx) {
2142
+ if (!ctx.isStudioConnected()) {
2143
+ return { success: false, error: "Studio is not connected" };
2144
+ }
2145
+ const response = await ctx.sendToStudio(CliMsg.BULK_SET_PROPERTIES, {
2146
+ targets: params.targets,
2147
+ root: params.root,
2148
+ className: params.className,
2149
+ properties: params.properties
2150
+ }, 1e3 * 60);
2151
+ const payload = response.payload;
2152
+ if (!payload.success) {
2153
+ return { success: false, error: payload.error ?? "Bulk set failed" };
2154
+ }
2155
+ return {
2156
+ success: true,
2157
+ data: {
2158
+ modified: payload.modified,
2159
+ errors: payload.errors?.length ? payload.errors : void 0
2160
+ }
2161
+ };
2162
+ }
2163
+ };
2164
+ }
2165
+ });
2166
+
2167
+ // src/tools/studio/find-replace-scripts.ts
2168
+ var find_replace_scripts_exports = {};
2169
+ __export(find_replace_scripts_exports, {
2170
+ tool: () => tool25
2171
+ });
2172
+ var tool25;
2173
+ var init_find_replace_scripts = __esm({
2174
+ "src/tools/studio/find-replace-scripts.ts"() {
2175
+ init_protocol();
2176
+ tool25 = {
2177
+ name: "find_replace_scripts",
2178
+ description: 'Find and replace text across all scripts in the game (or under a root path). Supports plain text and Lua pattern matching. Returns a list of modified scripts with match counts.\n\nUse dryRun=true to preview changes without applying them.\nExample: find="workspace" replace="Workspace" root="ServerScriptService"',
2179
+ parameters: {
2180
+ type: "object",
2181
+ properties: {
2182
+ find: {
2183
+ type: "string",
2184
+ description: "Text or Lua pattern to search for"
2185
+ },
2186
+ replace: {
2187
+ type: "string",
2188
+ description: "Replacement text (supports Lua pattern captures like %1, %2)"
2189
+ },
2190
+ root: {
2191
+ type: "string",
2192
+ description: "Root path to limit search scope (optional, searches entire game if omitted)"
2193
+ },
2194
+ usePattern: {
2195
+ type: "boolean",
2196
+ description: "If true, treat find/replace as Lua patterns. Default: false (plain text)"
2197
+ },
2198
+ dryRun: {
2199
+ type: "boolean",
2200
+ description: "If true, report matches but do not modify scripts. Default: false"
2201
+ }
2202
+ },
2203
+ required: ["find", "replace"]
2204
+ },
2205
+ async execute(params, ctx) {
2206
+ if (!ctx.isStudioConnected()) {
2207
+ return { success: false, error: "Studio is not connected" };
2208
+ }
2209
+ const response = await ctx.sendToStudio(CliMsg.FIND_REPLACE_SCRIPTS, {
2210
+ find: params.find,
2211
+ replace: params.replace,
2212
+ root: params.root,
2213
+ usePattern: params.usePattern ?? false,
2214
+ dryRun: params.dryRun ?? false
2215
+ }, 1e3 * 60 * 5);
2216
+ const payload = response.payload;
2217
+ if (!payload.success) {
2218
+ return { success: false, error: payload.error ?? "Find/replace failed" };
2219
+ }
2220
+ return {
2221
+ success: true,
2222
+ data: {
2223
+ totalFiles: payload.totalFiles,
2224
+ totalMatches: payload.totalMatches,
2225
+ modified: payload.modified,
2226
+ dryRun: params.dryRun ?? false
2227
+ }
2228
+ };
2229
+ }
2230
+ };
2231
+ }
2232
+ });
2233
+
1954
2234
  // src/tools/memory/recall.ts
1955
2235
  var recall_exports = {};
1956
2236
  __export(recall_exports, {
1957
- tool: () => tool21
2237
+ tool: () => tool26
1958
2238
  });
1959
- var tool21;
2239
+ var tool26;
1960
2240
  var init_recall = __esm({
1961
2241
  "src/tools/memory/recall.ts"() {
1962
2242
  init_store();
1963
- tool21 = {
2243
+ tool26 = {
1964
2244
  name: "recall_memory",
1965
2245
  description: "Search your memory for previously learned facts about the current project. Use this to recall architecture decisions, patterns, known bugs, or user preferences.",
1966
2246
  parameters: {
@@ -1999,13 +2279,13 @@ var init_recall = __esm({
1999
2279
  // src/tools/memory/remember.ts
2000
2280
  var remember_exports = {};
2001
2281
  __export(remember_exports, {
2002
- tool: () => tool22
2282
+ tool: () => tool27
2003
2283
  });
2004
- var tool22;
2284
+ var tool27;
2005
2285
  var init_remember = __esm({
2006
2286
  "src/tools/memory/remember.ts"() {
2007
2287
  init_store();
2008
- tool22 = {
2288
+ tool27 = {
2009
2289
  name: "remember",
2010
2290
  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.",
2011
2291
  parameters: {
@@ -2045,12 +2325,12 @@ var init_remember = __esm({
2045
2325
  // src/tools/cloud/upload-asset.ts
2046
2326
  var upload_asset_exports = {};
2047
2327
  __export(upload_asset_exports, {
2048
- tool: () => tool23
2328
+ tool: () => tool28
2049
2329
  });
2050
- var tool23;
2330
+ var tool28;
2051
2331
  var init_upload_asset = __esm({
2052
2332
  "src/tools/cloud/upload-asset.ts"() {
2053
- tool23 = {
2333
+ tool28 = {
2054
2334
  name: "upload_asset",
2055
2335
  description: "Upload a local file (image, model, audio) to Roblox via the Open Cloud API. Requires an Open Cloud API key configured in dominus.",
2056
2336
  parameters: {
@@ -2088,11 +2368,11 @@ var init_upload_asset = __esm({
2088
2368
  const assetType = params.assetType;
2089
2369
  const name = params.name;
2090
2370
  const description = params.description ?? "";
2091
- if (!fs4.existsSync(filePath)) {
2371
+ if (!fs5.existsSync(filePath)) {
2092
2372
  return { success: false, error: `File not found: ${filePath}` };
2093
2373
  }
2094
- const fileBuffer = fs4.readFileSync(filePath);
2095
- const fileName = path3.basename(filePath);
2374
+ const fileBuffer = fs5.readFileSync(filePath);
2375
+ const fileName = path4.basename(filePath);
2096
2376
  const typeMapping = {
2097
2377
  Image: "Decal",
2098
2378
  Audio: "Audio",
@@ -2146,6 +2426,57 @@ var init_upload_asset = __esm({
2146
2426
  }
2147
2427
  });
2148
2428
 
2429
+ // src/tools/studio/undo-redo.ts
2430
+ var undo_redo_exports = {};
2431
+ __export(undo_redo_exports, {
2432
+ redoTool: () => redoTool,
2433
+ undoTool: () => undoTool
2434
+ });
2435
+ var undoTool, redoTool;
2436
+ var init_undo_redo = __esm({
2437
+ "src/tools/studio/undo-redo.ts"() {
2438
+ init_protocol();
2439
+ undoTool = {
2440
+ name: "undo",
2441
+ description: "Undo the last action in Roblox Studio using ChangeHistoryService. Works just like Ctrl+Z. Use this to roll back mistakes.",
2442
+ parameters: {
2443
+ type: "object",
2444
+ properties: {}
2445
+ },
2446
+ async execute(_params, ctx) {
2447
+ if (!ctx.isStudioConnected()) {
2448
+ return { success: false, error: "Studio is not connected" };
2449
+ }
2450
+ const response = await ctx.sendToStudio(CliMsg.UNDO, {});
2451
+ const payload = response.payload;
2452
+ if (!payload.success) {
2453
+ return { success: false, error: payload.error ?? "Undo failed" };
2454
+ }
2455
+ return { success: true, data: "Undo performed" };
2456
+ }
2457
+ };
2458
+ redoTool = {
2459
+ name: "redo",
2460
+ description: "Redo the last undone action in Roblox Studio using ChangeHistoryService. Works just like Ctrl+Y.",
2461
+ parameters: {
2462
+ type: "object",
2463
+ properties: {}
2464
+ },
2465
+ async execute(_params, ctx) {
2466
+ if (!ctx.isStudioConnected()) {
2467
+ return { success: false, error: "Studio is not connected" };
2468
+ }
2469
+ const response = await ctx.sendToStudio(CliMsg.REDO, {});
2470
+ const payload = response.payload;
2471
+ if (!payload.success) {
2472
+ return { success: false, error: payload.error ?? "Redo failed" };
2473
+ }
2474
+ return { success: true, data: "Redo performed" };
2475
+ }
2476
+ };
2477
+ }
2478
+ });
2479
+
2149
2480
  // src/mcp/server.ts
2150
2481
  var server_exports = {};
2151
2482
  __export(server_exports, {
@@ -2166,15 +2497,15 @@ async function startMcpServer() {
2166
2497
  }
2167
2498
  const mcp = new McpServer({
2168
2499
  name: "dominus",
2169
- version: "0.2.0"
2500
+ version: "0.2.1"
2170
2501
  });
2171
2502
  mcp.tool(
2172
2503
  "read_script",
2173
2504
  "Read the source code of a script in Roblox Studio",
2174
2505
  { path: z.string().describe('Full instance path, e.g. "ServerScriptService.GameManager"') },
2175
- async ({ path: path4 }) => {
2506
+ async ({ path: path5 }) => {
2176
2507
  assertConnected();
2177
- const res = await bridge.sendRequest(CliMsg.GET_SCRIPT, { path: path4 });
2508
+ const res = await bridge.sendRequest(CliMsg.GET_SCRIPT, { path: path5 });
2178
2509
  return { content: [{ type: "text", text: JSON.stringify(res.payload, null, 2) }] };
2179
2510
  }
2180
2511
  );
@@ -2185,9 +2516,9 @@ async function startMcpServer() {
2185
2516
  path: z.string().describe("Full instance path"),
2186
2517
  source: z.string().describe("New complete source code")
2187
2518
  },
2188
- async ({ path: path4, source }) => {
2519
+ async ({ path: path5, source }) => {
2189
2520
  assertConnected();
2190
- const res = await bridge.sendRequest(CliMsg.SET_SCRIPT, { path: path4, source });
2521
+ const res = await bridge.sendRequest(CliMsg.SET_SCRIPT, { path: path5, source });
2191
2522
  return { content: [{ type: "text", text: JSON.stringify(res.payload) }] };
2192
2523
  }
2193
2524
  );
@@ -2221,9 +2552,22 @@ async function startMcpServer() {
2221
2552
  "get_properties",
2222
2553
  "Get all properties of an instance in Roblox Studio",
2223
2554
  { path: z.string().describe("Full instance path") },
2224
- async ({ path: path4 }) => {
2555
+ async ({ path: path5 }) => {
2225
2556
  assertConnected();
2226
- const res = await bridge.sendRequest(CliMsg.GET_PROPERTIES, { path: path4 });
2557
+ const res = await bridge.sendRequest(CliMsg.GET_PROPERTIES, { path: path5 });
2558
+ return { content: [{ type: "text", text: JSON.stringify(res.payload, null, 2) }] };
2559
+ }
2560
+ );
2561
+ mcp.tool(
2562
+ "get_descendants_properties",
2563
+ "Easily get all descendants of a UI instance (or any instance) and all of their properties.",
2564
+ {
2565
+ path: z.string().describe('Full instance path (e.g., "Workspace.Part", "StarterGui.ScreenGui")'),
2566
+ outputFile: z.string().optional().describe("Optional file path to write to prevent limit errors/timeouts")
2567
+ },
2568
+ async ({ path: path5, outputFile }) => {
2569
+ assertConnected();
2570
+ const res = await bridge.sendRequest(CliMsg.GET_DESCENDANTS_PROPERTIES, { path: path5, outputFile }, 1e3 * 60 * 5);
2227
2571
  return { content: [{ type: "text", text: JSON.stringify(res.payload, null, 2) }] };
2228
2572
  }
2229
2573
  );
@@ -2234,9 +2578,9 @@ async function startMcpServer() {
2234
2578
  path: z.string().describe("Full instance path"),
2235
2579
  properties: z.record(z.unknown()).describe("Key-value pairs of properties to set")
2236
2580
  },
2237
- async ({ path: path4, properties }) => {
2581
+ async ({ path: path5, properties }) => {
2238
2582
  assertConnected();
2239
- const res = await bridge.sendRequest(CliMsg.SET_PROPERTIES, { path: path4, properties });
2583
+ const res = await bridge.sendRequest(CliMsg.SET_PROPERTIES, { path: path5, properties });
2240
2584
  return { content: [{ type: "text", text: JSON.stringify(res.payload) }] };
2241
2585
  }
2242
2586
  );
@@ -2264,9 +2608,9 @@ async function startMcpServer() {
2264
2608
  "delete_instance",
2265
2609
  "Delete an instance from the Roblox Studio DataModel",
2266
2610
  { path: z.string().describe("Full instance path to delete") },
2267
- async ({ path: path4 }) => {
2611
+ async ({ path: path5 }) => {
2268
2612
  assertConnected();
2269
- const res = await bridge.sendRequest(CliMsg.DELETE_INSTANCE, { path: path4 });
2613
+ const res = await bridge.sendRequest(CliMsg.DELETE_INSTANCE, { path: path5 });
2270
2614
  return { content: [{ type: "text", text: JSON.stringify(res.payload) }] };
2271
2615
  }
2272
2616
  );
@@ -2326,13 +2670,14 @@ async function startMcpServer() {
2326
2670
  parent: z.string().optional().describe('Parent: "StarterGui", "PlayerGui", or path. Default: StarterGui'),
2327
2671
  tree: z.record(z.unknown()).describe(
2328
2672
  'Root UI node: {ClassName, Name, properties..., Children: [{...}, ...]}. UDim2 as [xScale, xOffset, yScale, yOffset], Color3 as "#hex", Font as "GothamBold"'
2329
- )
2673
+ ),
2674
+ animate: z.boolean().optional().describe("If true, task.wait() will be injected between creation of nodes so that the UI writes out dynamically.")
2330
2675
  },
2331
- async ({ parent, tree }) => {
2676
+ async ({ parent, tree, animate }) => {
2332
2677
  assertConnected();
2333
2678
  const res = await bridge.sendRequest(
2334
2679
  CliMsg.BUILD_UI,
2335
- { parent: parent ?? "StarterGui", tree }
2680
+ { parent: parent ?? "StarterGui", tree, animate }
2336
2681
  );
2337
2682
  return { content: [{ type: "text", text: JSON.stringify(res.payload, null, 2) }] };
2338
2683
  }
@@ -2463,8 +2808,8 @@ async function startMcpServer() {
2463
2808
  const parentParts = parent.split(".");
2464
2809
  let parentResolve = "game";
2465
2810
  for (const p of parentParts) parentResolve += `["${p}"]`;
2466
- const resolves = params.paths.map((path4) => {
2467
- const parts = path4.split(".");
2811
+ const resolves = params.paths.map((path5) => {
2812
+ const parts = path5.split(".");
2468
2813
  let r = "game";
2469
2814
  for (const part of parts) r += `["${part}"]`;
2470
2815
  return r;
@@ -2504,8 +2849,8 @@ async function startMcpServer() {
2504
2849
  const parts = params.parts;
2505
2850
  const groupName = params.groupName;
2506
2851
  const groupParent = params.groupParent ?? "Workspace";
2507
- function resolveP(path4) {
2508
- const segs = path4.split(".");
2852
+ function resolveP(path5) {
2853
+ const segs = path5.split(".");
2509
2854
  let r = "game";
2510
2855
  for (const s of segs) r += `["${s}"]`;
2511
2856
  return r;
@@ -3774,13 +4119,13 @@ var AIProvider = class {
3774
4119
  yield { type: "done" };
3775
4120
  }
3776
4121
  };
3777
- function toolToOpenAI(tool24) {
4122
+ function toolToOpenAI(tool29) {
3778
4123
  return {
3779
4124
  type: "function",
3780
4125
  function: {
3781
- name: tool24.name,
3782
- description: tool24.description,
3783
- parameters: tool24.parameters
4126
+ name: tool29.name,
4127
+ description: tool29.description,
4128
+ parameters: tool29.parameters
3784
4129
  }
3785
4130
  };
3786
4131
  }
@@ -4236,11 +4581,11 @@ var AgentLoop = class {
4236
4581
  // src/tools/registry.ts
4237
4582
  var ToolRegistry = class {
4238
4583
  tools = /* @__PURE__ */ new Map();
4239
- register(tool24) {
4240
- if (this.tools.has(tool24.name)) {
4241
- throw new Error(`Tool already registered: ${tool24.name}`);
4584
+ register(tool29) {
4585
+ if (this.tools.has(tool29.name)) {
4586
+ throw new Error(`Tool already registered: ${tool29.name}`);
4242
4587
  }
4243
- this.tools.set(tool24.name, tool24);
4588
+ this.tools.set(tool29.name, tool29);
4244
4589
  }
4245
4590
  get(name) {
4246
4591
  return this.tools.get(name);
@@ -4252,12 +4597,12 @@ var ToolRegistry = class {
4252
4597
  return this.getAll();
4253
4598
  }
4254
4599
  async execute(call, ctx) {
4255
- const tool24 = this.tools.get(call.name);
4256
- if (!tool24) {
4600
+ const tool29 = this.tools.get(call.name);
4601
+ if (!tool29) {
4257
4602
  return { success: false, error: `Unknown tool: ${call.name}` };
4258
4603
  }
4259
4604
  try {
4260
- return await tool24.execute(call.arguments, ctx);
4605
+ return await tool29.execute(call.arguments, ctx);
4261
4606
  } catch (err) {
4262
4607
  const message = err instanceof Error ? err.message : String(err);
4263
4608
  return { success: false, error: message };
@@ -4275,6 +4620,7 @@ function createDefaultRegistry() {
4275
4620
  Promise.resolve().then(() => (init_run_code(), run_code_exports)),
4276
4621
  Promise.resolve().then(() => (init_get_explorer(), get_explorer_exports)),
4277
4622
  Promise.resolve().then(() => (init_get_properties(), get_properties_exports)),
4623
+ Promise.resolve().then(() => (init_get_descendants_properties(), get_descendants_properties_exports)),
4278
4624
  Promise.resolve().then(() => (init_set_properties(), set_properties_exports)),
4279
4625
  Promise.resolve().then(() => (init_insert_instance(), insert_instance_exports)),
4280
4626
  Promise.resolve().then(() => (init_delete_instance(), delete_instance_exports)),
@@ -4290,6 +4636,10 @@ function createDefaultRegistry() {
4290
4636
  Promise.resolve().then(() => (init_clone_instance(), clone_instance_exports)),
4291
4637
  Promise.resolve().then(() => (init_group_instances(), group_instances_exports)),
4292
4638
  Promise.resolve().then(() => (init_build_multiple(), build_multiple_exports)),
4639
+ Promise.resolve().then(() => (init_serialize_ui(), serialize_ui_exports)),
4640
+ Promise.resolve().then(() => (init_move_instance(), move_instance_exports)),
4641
+ Promise.resolve().then(() => (init_bulk_set_properties(), bulk_set_properties_exports)),
4642
+ Promise.resolve().then(() => (init_find_replace_scripts(), find_replace_scripts_exports)),
4293
4643
  Promise.resolve().then(() => (init_recall(), recall_exports)),
4294
4644
  Promise.resolve().then(() => (init_remember(), remember_exports)),
4295
4645
  Promise.resolve().then(() => (init_upload_asset(), upload_asset_exports))
@@ -4300,13 +4650,108 @@ function createDefaultRegistry() {
4300
4650
  if (m.tool) registry.register(m.tool);
4301
4651
  })
4302
4652
  );
4653
+ void Promise.resolve().then(() => (init_undo_redo(), undo_redo_exports)).then((m) => {
4654
+ registry.register(m.undoTool);
4655
+ registry.register(m.redoTool);
4656
+ });
4303
4657
  return registry;
4304
4658
  }
4305
4659
 
4306
4660
  // src/index.tsx
4307
4661
  init_store();
4308
4662
  init_config();
4309
- var VERSION = "0.2.0";
4663
+ var CACHE_DIR = path4.join(os3.homedir(), ".dominus");
4664
+ var CACHE_FILE = path4.join(CACHE_DIR, "update-check.json");
4665
+ var CHECK_INTERVAL_MS = 4 * 60 * 60 * 1e3;
4666
+ var NPM_PACKAGE = "dominus-cli";
4667
+ function readCache() {
4668
+ try {
4669
+ if (fs5.existsSync(CACHE_FILE)) {
4670
+ return JSON.parse(fs5.readFileSync(CACHE_FILE, "utf-8"));
4671
+ }
4672
+ } catch {
4673
+ }
4674
+ return { lastCheck: 0, latestVersion: null };
4675
+ }
4676
+ function writeCache(cache) {
4677
+ try {
4678
+ if (!fs5.existsSync(CACHE_DIR)) fs5.mkdirSync(CACHE_DIR, { recursive: true });
4679
+ fs5.writeFileSync(CACHE_FILE, JSON.stringify(cache), "utf-8");
4680
+ } catch {
4681
+ }
4682
+ }
4683
+ function fetchLatestVersion() {
4684
+ return new Promise((resolve) => {
4685
+ const req = https.get(
4686
+ `https://registry.npmjs.org/${NPM_PACKAGE}/latest`,
4687
+ { headers: { Accept: "application/json" }, timeout: 5e3 },
4688
+ (res) => {
4689
+ if (res.statusCode !== 200) {
4690
+ resolve(null);
4691
+ res.resume();
4692
+ return;
4693
+ }
4694
+ let body = "";
4695
+ res.on("data", (c) => body += c);
4696
+ res.on("end", () => {
4697
+ try {
4698
+ resolve(JSON.parse(body).version ?? null);
4699
+ } catch {
4700
+ resolve(null);
4701
+ }
4702
+ });
4703
+ }
4704
+ );
4705
+ req.on("error", () => resolve(null));
4706
+ req.on("timeout", () => {
4707
+ req.destroy();
4708
+ resolve(null);
4709
+ });
4710
+ });
4711
+ }
4712
+ function compareVersions(current, latest) {
4713
+ const a = current.split(".").map(Number);
4714
+ const b = latest.split(".").map(Number);
4715
+ for (let i = 0; i < 3; i++) {
4716
+ if ((b[i] ?? 0) > (a[i] ?? 0)) return 1;
4717
+ if ((b[i] ?? 0) < (a[i] ?? 0)) return -1;
4718
+ }
4719
+ return 0;
4720
+ }
4721
+ async function checkForUpdate(currentVersion) {
4722
+ try {
4723
+ const cache = readCache();
4724
+ const now = Date.now();
4725
+ if (now - cache.lastCheck < CHECK_INTERVAL_MS && cache.latestVersion) {
4726
+ if (compareVersions(currentVersion, cache.latestVersion) > 0) {
4727
+ return { updateAvailable: true, currentVersion, latestVersion: cache.latestVersion };
4728
+ }
4729
+ return null;
4730
+ }
4731
+ const latest = await fetchLatestVersion();
4732
+ writeCache({ lastCheck: now, latestVersion: latest });
4733
+ if (latest && compareVersions(currentVersion, latest) > 0) {
4734
+ return { updateAvailable: true, currentVersion, latestVersion: latest };
4735
+ }
4736
+ } catch {
4737
+ }
4738
+ return null;
4739
+ }
4740
+ function formatUpdateMessage(info) {
4741
+ const line1 = `Update available: ${info.currentVersion} \u2192 ${info.latestVersion}`;
4742
+ const line2 = `Run: npm i -g ${NPM_PACKAGE}`;
4743
+ const width = Math.max(line1.length, line2.length) + 4;
4744
+ const pad = (s) => ` \u2502 ${s}${" ".repeat(width - s.length - 4)} \u2502`;
4745
+ return [
4746
+ "",
4747
+ ` \u256D${"\u2500".repeat(width)}\u256E`,
4748
+ pad(line1),
4749
+ pad(line2),
4750
+ ` \u2570${"\u2500".repeat(width)}\u256F`,
4751
+ ""
4752
+ ].join("\n");
4753
+ }
4754
+ var VERSION = "0.2.1";
4310
4755
  var program = new Command();
4311
4756
  program.name("dominus").description("AI-powered autonomous agent for Roblox Studio development").version(VERSION);
4312
4757
  program.command("start", { isDefault: true }).description("Launch the Dominus TUI agent").option("-p, --port <port>", "WebSocket server port", "18088").action(async (opts) => {
@@ -4326,6 +4771,10 @@ program.command("start", { isDefault: true }).description("Launch the Dominus TU
4326
4771
  setConfigValue("userName", firstName);
4327
4772
  }
4328
4773
  }
4774
+ checkForUpdate(VERSION).then((info) => {
4775
+ if (info) process.stderr.write(formatUpdateMessage(info));
4776
+ }).catch(() => {
4777
+ });
4329
4778
  await launchApp(port);
4330
4779
  });
4331
4780
  var configCmd = program.command("config").description("Manage Dominus configuration");
@@ -4416,24 +4865,24 @@ rulesCmd.command("list").description("List all rules").action(() => {
4416
4865
  });
4417
4866
  program.command("install-plugin").description("Build & install the Dominus plugin into Roblox Studio").action(async () => {
4418
4867
  const { execSync: execSync2 } = await import('child_process');
4419
- const packageRoot = path3.resolve(import.meta.dirname ?? __dirname, "..");
4420
- const pluginDir = path3.join(packageRoot, "plugin");
4421
- const pluginProject = path3.join(pluginDir, "default.project.json");
4422
- const studioPlugins = path3.join(
4423
- os2.homedir(),
4868
+ const packageRoot = path4.resolve(import.meta.dirname ?? __dirname, "..");
4869
+ const pluginDir = path4.join(packageRoot, "plugin");
4870
+ const pluginProject = path4.join(pluginDir, "default.project.json");
4871
+ const studioPlugins = path4.join(
4872
+ os3.homedir(),
4424
4873
  "AppData",
4425
4874
  "Local",
4426
4875
  "Roblox",
4427
4876
  "Plugins"
4428
4877
  );
4429
- const destFile = path3.join(studioPlugins, "Dominus.rbxm");
4430
- if (!fs4.existsSync(pluginProject)) {
4878
+ const destFile = path4.join(studioPlugins, "Dominus.rbxm");
4879
+ if (!fs5.existsSync(pluginProject)) {
4431
4880
  console.log(" \u2716 Plugin source not found.");
4432
4881
  console.log(` Expected at: ${pluginDir}`);
4433
4882
  return;
4434
4883
  }
4435
- if (!fs4.existsSync(studioPlugins)) {
4436
- fs4.mkdirSync(studioPlugins, { recursive: true });
4884
+ if (!fs5.existsSync(studioPlugins)) {
4885
+ fs5.mkdirSync(studioPlugins, { recursive: true });
4437
4886
  }
4438
4887
  let rojoAvailable = false;
4439
4888
  try {
@@ -4460,49 +4909,49 @@ program.command("mcp").description("Start the Dominus MCP server (for Cursor, Cl
4460
4909
  await startMcpServer2();
4461
4910
  });
4462
4911
  function getMcpTargets() {
4463
- const home = os2.homedir();
4464
- const appData = process.env.APPDATA || path3.join(home, "AppData", "Roaming");
4465
- const localAppData = process.env.LOCALAPPDATA || path3.join(home, "AppData", "Local");
4912
+ const home = os3.homedir();
4913
+ const appData = process.env.APPDATA || path4.join(home, "AppData", "Roaming");
4914
+ const localAppData = process.env.LOCALAPPDATA || path4.join(home, "AppData", "Local");
4466
4915
  const cwd = process.cwd();
4467
4916
  return [
4468
4917
  {
4469
4918
  name: "Cursor (project)",
4470
- configPath: path3.join(cwd, ".cursor", "mcp.json"),
4919
+ configPath: path4.join(cwd, ".cursor", "mcp.json"),
4471
4920
  keyPath: ["mcpServers"]
4472
4921
  },
4473
4922
  {
4474
4923
  name: "Cursor (global)",
4475
- configPath: path3.join(home, ".cursor", "mcp.json"),
4924
+ configPath: path4.join(home, ".cursor", "mcp.json"),
4476
4925
  keyPath: ["mcpServers"]
4477
4926
  },
4478
4927
  {
4479
4928
  name: "Claude Desktop",
4480
- configPath: path3.join(appData, "Claude", "claude_desktop_config.json"),
4929
+ configPath: path4.join(appData, "Claude", "claude_desktop_config.json"),
4481
4930
  keyPath: ["mcpServers"]
4482
4931
  },
4483
4932
  {
4484
4933
  name: "Claude Code",
4485
- configPath: path3.join(home, ".claude.json"),
4934
+ configPath: path4.join(home, ".claude.json"),
4486
4935
  keyPath: ["mcpServers"]
4487
4936
  },
4488
4937
  {
4489
4938
  name: "Windsurf (project)",
4490
- configPath: path3.join(cwd, ".windsurf", "mcp.json"),
4939
+ configPath: path4.join(cwd, ".windsurf", "mcp.json"),
4491
4940
  keyPath: ["mcpServers"]
4492
4941
  },
4493
4942
  {
4494
4943
  name: "Windsurf (global)",
4495
- configPath: path3.join(home, ".codeium", "windsurf", "mcp_config.json"),
4944
+ configPath: path4.join(home, ".codeium", "windsurf", "mcp_config.json"),
4496
4945
  keyPath: ["mcpServers"]
4497
4946
  },
4498
4947
  {
4499
4948
  name: "VS Code (project)",
4500
- configPath: path3.join(cwd, ".vscode", "mcp.json"),
4949
+ configPath: path4.join(cwd, ".vscode", "mcp.json"),
4501
4950
  keyPath: ["servers"]
4502
4951
  },
4503
4952
  {
4504
4953
  name: "VS Code (global)",
4505
- configPath: path3.join(localAppData, "Code", "User", "settings.json"),
4954
+ configPath: path4.join(localAppData, "Code", "User", "settings.json"),
4506
4955
  keyPath: ["mcp", "servers"]
4507
4956
  }
4508
4957
  ];
@@ -4514,9 +4963,9 @@ var dominusEntry = {
4514
4963
  };
4515
4964
  function injectMcpConfig(target) {
4516
4965
  let config = {};
4517
- if (fs4.existsSync(target.configPath)) {
4966
+ if (fs5.existsSync(target.configPath)) {
4518
4967
  try {
4519
- config = JSON.parse(fs4.readFileSync(target.configPath, "utf-8"));
4968
+ config = JSON.parse(fs5.readFileSync(target.configPath, "utf-8"));
4520
4969
  } catch {
4521
4970
  config = {};
4522
4971
  }
@@ -4532,12 +4981,12 @@ function injectMcpConfig(target) {
4532
4981
  return { status: "already" };
4533
4982
  }
4534
4983
  obj["dominus"] = { ...dominusEntry };
4535
- const dir = path3.dirname(target.configPath);
4536
- if (!fs4.existsSync(dir)) {
4537
- fs4.mkdirSync(dir, { recursive: true });
4984
+ const dir = path4.dirname(target.configPath);
4985
+ if (!fs5.existsSync(dir)) {
4986
+ fs5.mkdirSync(dir, { recursive: true });
4538
4987
  }
4539
- fs4.writeFileSync(target.configPath, JSON.stringify(config, null, 2), "utf-8");
4540
- return { status: fs4.existsSync(target.configPath) ? "installed" : "created" };
4988
+ fs5.writeFileSync(target.configPath, JSON.stringify(config, null, 2), "utf-8");
4989
+ return { status: fs5.existsSync(target.configPath) ? "installed" : "created" };
4541
4990
  }
4542
4991
  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) => {
4543
4992
  const targets = getMcpTargets();
@@ -4559,7 +5008,7 @@ program.command("mcp-install").description("Auto-install Dominus MCP into Cursor
4559
5008
  }
4560
5009
  const detected = targets.map((t) => ({
4561
5010
  target: t,
4562
- exists: fs4.existsSync(path3.dirname(t.configPath))
5011
+ exists: fs5.existsSync(path4.dirname(t.configPath))
4563
5012
  }));
4564
5013
  const toInstall = detected.filter((d) => d.exists);
4565
5014
  if (toInstall.length === 0) {
@@ -4632,23 +5081,23 @@ program.command("mcp-setup").description("Print MCP configuration for manual set
4632
5081
  console.log("");
4633
5082
  });
4634
5083
  function buildPluginXml(pluginDir, destFile) {
4635
- const srcDir = path3.join(pluginDir, "src");
4636
- if (!fs4.existsSync(srcDir)) {
5084
+ const srcDir = path4.join(pluginDir, "src");
5085
+ if (!fs5.existsSync(srcDir)) {
4637
5086
  console.log(` \u2716 Plugin source directory not found: ${srcDir}`);
4638
5087
  return;
4639
5088
  }
4640
- const files = fs4.readdirSync(srcDir).filter((f) => f.endsWith(".lua"));
5089
+ const files = fs5.readdirSync(srcDir).filter((f) => f.endsWith(".lua"));
4641
5090
  const mainFile = files.find((f) => f === "init.server.lua");
4642
5091
  const moduleFiles = files.filter((f) => f !== "init.server.lua");
4643
5092
  if (!mainFile) {
4644
5093
  console.log(" \u2716 init.server.lua not found in plugin source");
4645
5094
  return;
4646
5095
  }
4647
- const mainSource = fs4.readFileSync(path3.join(srcDir, mainFile), "utf-8");
5096
+ const mainSource = fs5.readFileSync(path4.join(srcDir, mainFile), "utf-8");
4648
5097
  let moduleItems = "";
4649
5098
  for (const mf of moduleFiles) {
4650
5099
  const modName = mf.replace(".lua", "");
4651
- const modSource = fs4.readFileSync(path3.join(srcDir, mf), "utf-8");
5100
+ const modSource = fs5.readFileSync(path4.join(srcDir, mf), "utf-8");
4652
5101
  moduleItems += `
4653
5102
  <Item class="ModuleScript" referent="${modName}">
4654
5103
  <Properties>
@@ -4666,7 +5115,7 @@ function buildPluginXml(pluginDir, destFile) {
4666
5115
  </Properties>${moduleItems}
4667
5116
  </Item>
4668
5117
  </roblox>`;
4669
- fs4.writeFileSync(destFile, rbxmx, "utf-8");
5118
+ fs5.writeFileSync(destFile, rbxmx, "utf-8");
4670
5119
  console.log(` \u2714 Plugin installed to: ${destFile}`);
4671
5120
  console.log(" Restart Roblox Studio to load the plugin.");
4672
5121
  }