dominus-cli 0.2.1 → 0.5.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -19,6 +19,7 @@ import { randomBytes } from 'crypto';
19
19
  import Spinner from 'ink-spinner';
20
20
  import OpenAI from 'openai';
21
21
  import https from 'https';
22
+ import { fileURLToPath } from 'url';
22
23
 
23
24
  var __defProp = Object.defineProperty;
24
25
  var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
@@ -221,6 +222,7 @@ var init_protocol = __esm({
221
222
  SET_SCRIPT: "cli:set:script",
222
223
  RUN_CODE: "cli:run",
223
224
  GET_PROPERTIES: "cli:get:properties",
225
+ GET_DESCENDANTS_PROPERTIES: "cli:get:descendants:properties",
224
226
  SET_PROPERTIES: "cli:set:properties",
225
227
  INSERT_INSTANCE: "cli:insert",
226
228
  DELETE_INSTANCE: "cli:delete",
@@ -234,7 +236,13 @@ var init_protocol = __esm({
234
236
  END_TEST: "cli:test:end",
235
237
  BUILD_UI: "cli:build:ui",
236
238
  GET_REFLECTION: "cli:get:reflection",
237
- UPLOAD_ASSET: "cli:upload:asset"
239
+ UPLOAD_ASSET: "cli:upload:asset",
240
+ SERIALIZE_UI: "cli:serialize:ui",
241
+ MOVE_INSTANCE: "cli:move:instance",
242
+ UNDO: "cli:undo",
243
+ REDO: "cli:redo",
244
+ BULK_SET_PROPERTIES: "cli:bulk:set:properties",
245
+ FIND_REPLACE_SCRIPTS: "cli:find:replace:scripts"
238
246
  };
239
247
  }
240
248
  });
@@ -1119,16 +1127,65 @@ var init_get_properties = __esm({
1119
1127
  }
1120
1128
  });
1121
1129
 
1130
+ // src/tools/studio/get-descendants-properties.ts
1131
+ var get_descendants_properties_exports = {};
1132
+ __export(get_descendants_properties_exports, {
1133
+ tool: () => tool6
1134
+ });
1135
+ var tool6;
1136
+ var init_get_descendants_properties = __esm({
1137
+ "src/tools/studio/get-descendants-properties.ts"() {
1138
+ init_protocol();
1139
+ tool6 = {
1140
+ name: "get_descendants_properties",
1141
+ description: "Easily get all descendants of a UI instance (or any instance) and all of their properties.",
1142
+ parameters: {
1143
+ type: "object",
1144
+ properties: {
1145
+ path: {
1146
+ type: "string",
1147
+ description: 'Full instance path (e.g., "Workspace.Part", "StarterGui.ScreenGui")'
1148
+ },
1149
+ outputFile: {
1150
+ type: "string",
1151
+ description: "Optional file path to write the JSON result to, useful for very large trees where context limits apply."
1152
+ }
1153
+ },
1154
+ required: ["path"]
1155
+ },
1156
+ async execute(params, ctx) {
1157
+ if (!ctx.isStudioConnected()) {
1158
+ return { success: false, error: "Studio is not connected" };
1159
+ }
1160
+ const response = await ctx.sendToStudio(
1161
+ CliMsg.GET_DESCENDANTS_PROPERTIES,
1162
+ { path: params.path },
1163
+ 1e3 * 60 * 5
1164
+ // 5 minute timeout since getting ALL descendants can take a very long time
1165
+ );
1166
+ if (params.outputFile && typeof params.outputFile === "string") {
1167
+ const fs6 = await import('fs/promises');
1168
+ const path5 = await import('path');
1169
+ const outputPath = path5.resolve(process.cwd(), params.outputFile);
1170
+ await fs6.writeFile(outputPath, JSON.stringify(response.payload, null, 2), "utf-8");
1171
+ return { success: true, data: `Successfully saved descendants to ${outputPath}` };
1172
+ }
1173
+ return { success: true, data: response.payload };
1174
+ }
1175
+ };
1176
+ }
1177
+ });
1178
+
1122
1179
  // src/tools/studio/set-properties.ts
1123
1180
  var set_properties_exports = {};
1124
1181
  __export(set_properties_exports, {
1125
- tool: () => tool6
1182
+ tool: () => tool7
1126
1183
  });
1127
- var tool6;
1184
+ var tool7;
1128
1185
  var init_set_properties = __esm({
1129
1186
  "src/tools/studio/set-properties.ts"() {
1130
1187
  init_protocol();
1131
- tool6 = {
1188
+ tool7 = {
1132
1189
  name: "set_properties",
1133
1190
  description: "Set properties on an instance in Roblox Studio.",
1134
1191
  parameters: {
@@ -1166,13 +1223,13 @@ var init_set_properties = __esm({
1166
1223
  // src/tools/studio/insert-instance.ts
1167
1224
  var insert_instance_exports = {};
1168
1225
  __export(insert_instance_exports, {
1169
- tool: () => tool7
1226
+ tool: () => tool8
1170
1227
  });
1171
- var tool7;
1228
+ var tool8;
1172
1229
  var init_insert_instance = __esm({
1173
1230
  "src/tools/studio/insert-instance.ts"() {
1174
1231
  init_protocol();
1175
- tool7 = {
1232
+ tool8 = {
1176
1233
  name: "insert_instance",
1177
1234
  description: "Insert a new instance into the Roblox Studio DataModel.",
1178
1235
  parameters: {
@@ -1220,13 +1277,13 @@ var init_insert_instance = __esm({
1220
1277
  // src/tools/studio/delete-instance.ts
1221
1278
  var delete_instance_exports = {};
1222
1279
  __export(delete_instance_exports, {
1223
- tool: () => tool8
1280
+ tool: () => tool9
1224
1281
  });
1225
- var tool8;
1282
+ var tool9;
1226
1283
  var init_delete_instance = __esm({
1227
1284
  "src/tools/studio/delete-instance.ts"() {
1228
1285
  init_protocol();
1229
- tool8 = {
1286
+ tool9 = {
1230
1287
  name: "delete_instance",
1231
1288
  description: "Delete an instance from the Roblox Studio DataModel by path.",
1232
1289
  parameters: {
@@ -1257,13 +1314,13 @@ var init_delete_instance = __esm({
1257
1314
  // src/tools/studio/get-output.ts
1258
1315
  var get_output_exports = {};
1259
1316
  __export(get_output_exports, {
1260
- tool: () => tool9
1317
+ tool: () => tool10
1261
1318
  });
1262
- var tool9;
1319
+ var tool10;
1263
1320
  var init_get_output = __esm({
1264
1321
  "src/tools/studio/get-output.ts"() {
1265
1322
  init_protocol();
1266
- tool9 = {
1323
+ tool10 = {
1267
1324
  name: "get_output",
1268
1325
  description: "Get recent output log entries from Roblox Studio (prints, warnings, errors).",
1269
1326
  parameters: {
@@ -1298,13 +1355,13 @@ var init_get_output = __esm({
1298
1355
  // src/tools/studio/get-selection.ts
1299
1356
  var get_selection_exports = {};
1300
1357
  __export(get_selection_exports, {
1301
- tool: () => tool10
1358
+ tool: () => tool11
1302
1359
  });
1303
- var tool10;
1360
+ var tool11;
1304
1361
  var init_get_selection = __esm({
1305
1362
  "src/tools/studio/get-selection.ts"() {
1306
1363
  init_protocol();
1307
- tool10 = {
1364
+ tool11 = {
1308
1365
  name: "get_selection",
1309
1366
  description: "Get the currently selected instances in Roblox Studio.",
1310
1367
  parameters: {
@@ -1325,13 +1382,13 @@ var init_get_selection = __esm({
1325
1382
  // src/tools/studio/search-scripts.ts
1326
1383
  var search_scripts_exports = {};
1327
1384
  __export(search_scripts_exports, {
1328
- tool: () => tool11
1385
+ tool: () => tool12
1329
1386
  });
1330
- var tool11;
1387
+ var tool12;
1331
1388
  var init_search_scripts = __esm({
1332
1389
  "src/tools/studio/search-scripts.ts"() {
1333
1390
  init_protocol();
1334
- tool11 = {
1391
+ tool12 = {
1335
1392
  name: "search_scripts",
1336
1393
  description: "Search across all scripts in Roblox Studio for a text pattern. Returns matching lines with context.",
1337
1394
  parameters: {
@@ -1366,13 +1423,13 @@ var init_search_scripts = __esm({
1366
1423
  // src/tools/studio/run-tests.ts
1367
1424
  var run_tests_exports = {};
1368
1425
  __export(run_tests_exports, {
1369
- tool: () => tool12
1426
+ tool: () => tool13
1370
1427
  });
1371
- var tool12;
1428
+ var tool13;
1372
1429
  var init_run_tests = __esm({
1373
1430
  "src/tools/studio/run-tests.ts"() {
1374
1431
  init_protocol();
1375
- tool12 = {
1432
+ tool13 = {
1376
1433
  name: "run_tests",
1377
1434
  description: "Run tests in Roblox Studio using StudioTestService. Can run all tests or a specific test by name.",
1378
1435
  parameters: {
@@ -1406,13 +1463,13 @@ var init_run_tests = __esm({
1406
1463
  // src/tools/studio/get-class-info.ts
1407
1464
  var get_class_info_exports = {};
1408
1465
  __export(get_class_info_exports, {
1409
- tool: () => tool13
1466
+ tool: () => tool14
1410
1467
  });
1411
- var tool13;
1468
+ var tool14;
1412
1469
  var init_get_class_info = __esm({
1413
1470
  "src/tools/studio/get-class-info.ts"() {
1414
1471
  init_protocol();
1415
- tool13 = {
1472
+ tool14 = {
1416
1473
  name: "get_class_info",
1417
1474
  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).",
1418
1475
  parameters: {
@@ -1443,13 +1500,13 @@ var init_get_class_info = __esm({
1443
1500
  // src/tools/studio/create-ui.ts
1444
1501
  var create_ui_exports = {};
1445
1502
  __export(create_ui_exports, {
1446
- tool: () => tool14
1503
+ tool: () => tool15
1447
1504
  });
1448
- var tool14;
1505
+ var tool15;
1449
1506
  var init_create_ui = __esm({
1450
1507
  "src/tools/studio/create-ui.ts"() {
1451
1508
  init_protocol();
1452
- tool14 = {
1509
+ tool15 = {
1453
1510
  name: "create_ui",
1454
1511
  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}',
1455
1512
  parameters: {
@@ -1461,7 +1518,11 @@ var init_create_ui = __esm({
1461
1518
  },
1462
1519
  tree: {
1463
1520
  type: "object",
1464
- 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}'
1521
+ 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.'
1522
+ },
1523
+ animate: {
1524
+ type: "boolean",
1525
+ description: "If true, task.wait() will be injected between creation of nodes so that the UI writes out dynamically."
1465
1526
  }
1466
1527
  },
1467
1528
  required: ["tree"]
@@ -1472,6 +1533,7 @@ var init_create_ui = __esm({
1472
1533
  }
1473
1534
  const response = await ctx.sendToStudio(CliMsg.BUILD_UI, {
1474
1535
  parent: params.parent ?? "StarterGui",
1536
+ animate: params.animate,
1475
1537
  tree: params.tree
1476
1538
  });
1477
1539
  const payload = response.payload;
@@ -1490,13 +1552,13 @@ var init_create_ui = __esm({
1490
1552
  // src/tools/studio/execute-run-test.ts
1491
1553
  var execute_run_test_exports = {};
1492
1554
  __export(execute_run_test_exports, {
1493
- tool: () => tool15
1555
+ tool: () => tool16
1494
1556
  });
1495
- var tool15;
1557
+ var tool16;
1496
1558
  var init_execute_run_test = __esm({
1497
1559
  "src/tools/studio/execute-run-test.ts"() {
1498
1560
  init_protocol();
1499
- tool15 = {
1561
+ tool16 = {
1500
1562
  name: "execute_run_test",
1501
1563
  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.",
1502
1564
  parameters: {
@@ -1535,13 +1597,13 @@ var init_execute_run_test = __esm({
1535
1597
  // src/tools/studio/execute-play-test.ts
1536
1598
  var execute_play_test_exports = {};
1537
1599
  __export(execute_play_test_exports, {
1538
- tool: () => tool16
1600
+ tool: () => tool17
1539
1601
  });
1540
- var tool16;
1602
+ var tool17;
1541
1603
  var init_execute_play_test = __esm({
1542
1604
  "src/tools/studio/execute-play-test.ts"() {
1543
1605
  init_protocol();
1544
- tool16 = {
1606
+ tool17 = {
1545
1607
  name: "execute_play_test",
1546
1608
  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.",
1547
1609
  parameters: {
@@ -1580,13 +1642,13 @@ var init_execute_play_test = __esm({
1580
1642
  // src/tools/studio/create-part.ts
1581
1643
  var create_part_exports = {};
1582
1644
  __export(create_part_exports, {
1583
- tool: () => tool17
1645
+ tool: () => tool18
1584
1646
  });
1585
- var tool17;
1647
+ var tool18;
1586
1648
  var init_create_part = __esm({
1587
1649
  "src/tools/studio/create-part.ts"() {
1588
1650
  init_protocol();
1589
- tool17 = {
1651
+ tool18 = {
1590
1652
  name: "create_part",
1591
1653
  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.",
1592
1654
  parameters: {
@@ -1679,7 +1741,7 @@ var init_create_part = __esm({
1679
1741
  // src/tools/studio/clone-instance.ts
1680
1742
  var clone_instance_exports = {};
1681
1743
  __export(clone_instance_exports, {
1682
- tool: () => tool18
1744
+ tool: () => tool19
1683
1745
  });
1684
1746
  function buildCloneCode(params) {
1685
1747
  const path5 = params.path;
@@ -1714,11 +1776,11 @@ function buildCloneCode(params) {
1714
1776
  lines.push(`print("Cloned: " .. clone:GetFullName())`);
1715
1777
  return lines.join("\n");
1716
1778
  }
1717
- var tool18;
1779
+ var tool19;
1718
1780
  var init_clone_instance = __esm({
1719
1781
  "src/tools/studio/clone-instance.ts"() {
1720
1782
  init_protocol();
1721
- tool18 = {
1783
+ tool19 = {
1722
1784
  name: "clone_instance",
1723
1785
  description: "Clone an existing instance in Roblox Studio and optionally reposition/rename it. Great for duplicating parts, models, or entire trees.",
1724
1786
  parameters: {
@@ -1754,13 +1816,13 @@ var init_clone_instance = __esm({
1754
1816
  // src/tools/studio/group-instances.ts
1755
1817
  var group_instances_exports = {};
1756
1818
  __export(group_instances_exports, {
1757
- tool: () => tool19
1819
+ tool: () => tool20
1758
1820
  });
1759
- var tool19;
1821
+ var tool20;
1760
1822
  var init_group_instances = __esm({
1761
1823
  "src/tools/studio/group-instances.ts"() {
1762
1824
  init_protocol();
1763
- tool19 = {
1825
+ tool20 = {
1764
1826
  name: "group_instances",
1765
1827
  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).',
1766
1828
  parameters: {
@@ -1820,7 +1882,7 @@ var init_group_instances = __esm({
1820
1882
  // src/tools/studio/build-multiple.ts
1821
1883
  var build_multiple_exports = {};
1822
1884
  __export(build_multiple_exports, {
1823
- tool: () => tool20
1885
+ tool: () => tool21
1824
1886
  });
1825
1887
  function generateBatchCode(parts, groupName, groupParent) {
1826
1888
  const lines = [];
@@ -1879,11 +1941,11 @@ function generateBatchCode(parts, groupName, groupParent) {
1879
1941
  lines.push(`print("Built ${parts.length} parts${groupName ? ` in ${groupName}` : ""}")`);
1880
1942
  return lines.join("\n");
1881
1943
  }
1882
- var tool20;
1944
+ var tool21;
1883
1945
  var init_build_multiple = __esm({
1884
1946
  "src/tools/studio/build-multiple.ts"() {
1885
1947
  init_protocol();
1886
- tool20 = {
1948
+ tool21 = {
1887
1949
  name: "build_multiple",
1888
1950
  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.",
1889
1951
  parameters: {
@@ -1952,16 +2014,234 @@ var init_build_multiple = __esm({
1952
2014
  }
1953
2015
  });
1954
2016
 
2017
+ // src/tools/studio/serialize-ui.ts
2018
+ var serialize_ui_exports = {};
2019
+ __export(serialize_ui_exports, {
2020
+ tool: () => tool22
2021
+ });
2022
+ var tool22;
2023
+ var init_serialize_ui = __esm({
2024
+ "src/tools/studio/serialize-ui.ts"() {
2025
+ init_protocol();
2026
+ tool22 = {
2027
+ name: "serialize_ui",
2028
+ 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.",
2029
+ parameters: {
2030
+ type: "object",
2031
+ properties: {
2032
+ path: {
2033
+ type: "string",
2034
+ description: 'Instance path to serialize (e.g. "StarterGui.MyScreenGui")'
2035
+ },
2036
+ maxDepth: {
2037
+ type: "number",
2038
+ description: "Maximum depth to serialize (default: 50)"
2039
+ }
2040
+ },
2041
+ required: ["path"]
2042
+ },
2043
+ async execute(params, ctx) {
2044
+ if (!ctx.isStudioConnected()) {
2045
+ return { success: false, error: "Studio is not connected" };
2046
+ }
2047
+ const response = await ctx.sendToStudio(CliMsg.SERIALIZE_UI, {
2048
+ path: params.path,
2049
+ maxDepth: params.maxDepth ?? 50
2050
+ }, 1e3 * 60 * 2);
2051
+ const payload = response.payload;
2052
+ if (!payload.success) {
2053
+ return { success: false, error: payload.error ?? "Serialization failed" };
2054
+ }
2055
+ return { success: true, data: payload.tree };
2056
+ }
2057
+ };
2058
+ }
2059
+ });
2060
+
2061
+ // src/tools/studio/move-instance.ts
2062
+ var move_instance_exports = {};
2063
+ __export(move_instance_exports, {
2064
+ tool: () => tool23
2065
+ });
2066
+ var tool23;
2067
+ var init_move_instance = __esm({
2068
+ "src/tools/studio/move-instance.ts"() {
2069
+ init_protocol();
2070
+ tool23 = {
2071
+ name: "move_instance",
2072
+ 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.",
2073
+ parameters: {
2074
+ type: "object",
2075
+ properties: {
2076
+ path: {
2077
+ type: "string",
2078
+ description: 'Full path of the instance to move (e.g. "Workspace.OldFolder.MyPart")'
2079
+ },
2080
+ newParent: {
2081
+ type: "string",
2082
+ description: 'Full path of the new parent (e.g. "Workspace.NewFolder")'
2083
+ }
2084
+ },
2085
+ required: ["path", "newParent"]
2086
+ },
2087
+ async execute(params, ctx) {
2088
+ if (!ctx.isStudioConnected()) {
2089
+ return { success: false, error: "Studio is not connected" };
2090
+ }
2091
+ const response = await ctx.sendToStudio(CliMsg.MOVE_INSTANCE, {
2092
+ path: params.path,
2093
+ newParent: params.newParent
2094
+ });
2095
+ const payload = response.payload;
2096
+ if (!payload.success) {
2097
+ return { success: false, error: payload.error ?? "Move failed" };
2098
+ }
2099
+ return { success: true, data: { newPath: payload.newPath } };
2100
+ }
2101
+ };
2102
+ }
2103
+ });
2104
+
2105
+ // src/tools/studio/bulk-set-properties.ts
2106
+ var bulk_set_properties_exports = {};
2107
+ __export(bulk_set_properties_exports, {
2108
+ tool: () => tool24
2109
+ });
2110
+ var tool24;
2111
+ var init_bulk_set_properties = __esm({
2112
+ "src/tools/studio/bulk-set-properties.ts"() {
2113
+ init_protocol();
2114
+ tool24 = {
2115
+ name: "bulk_set_properties",
2116
+ 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"}',
2117
+ parameters: {
2118
+ type: "object",
2119
+ properties: {
2120
+ targets: {
2121
+ type: "array",
2122
+ description: "Array of {path, properties} objects for explicit mode.",
2123
+ items: {
2124
+ type: "object",
2125
+ description: "A target with path and properties to set."
2126
+ }
2127
+ },
2128
+ root: {
2129
+ type: "string",
2130
+ description: "Root instance path to search under (filter mode)"
2131
+ },
2132
+ className: {
2133
+ type: "string",
2134
+ description: "Only apply to instances of this ClassName (filter mode)"
2135
+ },
2136
+ properties: {
2137
+ type: "object",
2138
+ description: "Properties to set on all matched instances (filter mode)"
2139
+ }
2140
+ }
2141
+ },
2142
+ async execute(params, ctx) {
2143
+ if (!ctx.isStudioConnected()) {
2144
+ return { success: false, error: "Studio is not connected" };
2145
+ }
2146
+ const response = await ctx.sendToStudio(CliMsg.BULK_SET_PROPERTIES, {
2147
+ targets: params.targets,
2148
+ root: params.root,
2149
+ className: params.className,
2150
+ properties: params.properties
2151
+ }, 1e3 * 60);
2152
+ const payload = response.payload;
2153
+ if (!payload.success) {
2154
+ return { success: false, error: payload.error ?? "Bulk set failed" };
2155
+ }
2156
+ return {
2157
+ success: true,
2158
+ data: {
2159
+ modified: payload.modified,
2160
+ errors: payload.errors?.length ? payload.errors : void 0
2161
+ }
2162
+ };
2163
+ }
2164
+ };
2165
+ }
2166
+ });
2167
+
2168
+ // src/tools/studio/find-replace-scripts.ts
2169
+ var find_replace_scripts_exports = {};
2170
+ __export(find_replace_scripts_exports, {
2171
+ tool: () => tool25
2172
+ });
2173
+ var tool25;
2174
+ var init_find_replace_scripts = __esm({
2175
+ "src/tools/studio/find-replace-scripts.ts"() {
2176
+ init_protocol();
2177
+ tool25 = {
2178
+ name: "find_replace_scripts",
2179
+ 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"',
2180
+ parameters: {
2181
+ type: "object",
2182
+ properties: {
2183
+ find: {
2184
+ type: "string",
2185
+ description: "Text or Lua pattern to search for"
2186
+ },
2187
+ replace: {
2188
+ type: "string",
2189
+ description: "Replacement text (supports Lua pattern captures like %1, %2)"
2190
+ },
2191
+ root: {
2192
+ type: "string",
2193
+ description: "Root path to limit search scope (optional, searches entire game if omitted)"
2194
+ },
2195
+ usePattern: {
2196
+ type: "boolean",
2197
+ description: "If true, treat find/replace as Lua patterns. Default: false (plain text)"
2198
+ },
2199
+ dryRun: {
2200
+ type: "boolean",
2201
+ description: "If true, report matches but do not modify scripts. Default: false"
2202
+ }
2203
+ },
2204
+ required: ["find", "replace"]
2205
+ },
2206
+ async execute(params, ctx) {
2207
+ if (!ctx.isStudioConnected()) {
2208
+ return { success: false, error: "Studio is not connected" };
2209
+ }
2210
+ const response = await ctx.sendToStudio(CliMsg.FIND_REPLACE_SCRIPTS, {
2211
+ find: params.find,
2212
+ replace: params.replace,
2213
+ root: params.root,
2214
+ usePattern: params.usePattern ?? false,
2215
+ dryRun: params.dryRun ?? false
2216
+ }, 1e3 * 60 * 5);
2217
+ const payload = response.payload;
2218
+ if (!payload.success) {
2219
+ return { success: false, error: payload.error ?? "Find/replace failed" };
2220
+ }
2221
+ return {
2222
+ success: true,
2223
+ data: {
2224
+ totalFiles: payload.totalFiles,
2225
+ totalMatches: payload.totalMatches,
2226
+ modified: payload.modified,
2227
+ dryRun: params.dryRun ?? false
2228
+ }
2229
+ };
2230
+ }
2231
+ };
2232
+ }
2233
+ });
2234
+
1955
2235
  // src/tools/memory/recall.ts
1956
2236
  var recall_exports = {};
1957
2237
  __export(recall_exports, {
1958
- tool: () => tool21
2238
+ tool: () => tool26
1959
2239
  });
1960
- var tool21;
2240
+ var tool26;
1961
2241
  var init_recall = __esm({
1962
2242
  "src/tools/memory/recall.ts"() {
1963
2243
  init_store();
1964
- tool21 = {
2244
+ tool26 = {
1965
2245
  name: "recall_memory",
1966
2246
  description: "Search your memory for previously learned facts about the current project. Use this to recall architecture decisions, patterns, known bugs, or user preferences.",
1967
2247
  parameters: {
@@ -2000,13 +2280,13 @@ var init_recall = __esm({
2000
2280
  // src/tools/memory/remember.ts
2001
2281
  var remember_exports = {};
2002
2282
  __export(remember_exports, {
2003
- tool: () => tool22
2283
+ tool: () => tool27
2004
2284
  });
2005
- var tool22;
2285
+ var tool27;
2006
2286
  var init_remember = __esm({
2007
2287
  "src/tools/memory/remember.ts"() {
2008
2288
  init_store();
2009
- tool22 = {
2289
+ tool27 = {
2010
2290
  name: "remember",
2011
2291
  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.",
2012
2292
  parameters: {
@@ -2046,12 +2326,12 @@ var init_remember = __esm({
2046
2326
  // src/tools/cloud/upload-asset.ts
2047
2327
  var upload_asset_exports = {};
2048
2328
  __export(upload_asset_exports, {
2049
- tool: () => tool23
2329
+ tool: () => tool28
2050
2330
  });
2051
- var tool23;
2331
+ var tool28;
2052
2332
  var init_upload_asset = __esm({
2053
2333
  "src/tools/cloud/upload-asset.ts"() {
2054
- tool23 = {
2334
+ tool28 = {
2055
2335
  name: "upload_asset",
2056
2336
  description: "Upload a local file (image, model, audio) to Roblox via the Open Cloud API. Requires an Open Cloud API key configured in dominus.",
2057
2337
  parameters: {
@@ -2147,6 +2427,57 @@ var init_upload_asset = __esm({
2147
2427
  }
2148
2428
  });
2149
2429
 
2430
+ // src/tools/studio/undo-redo.ts
2431
+ var undo_redo_exports = {};
2432
+ __export(undo_redo_exports, {
2433
+ redoTool: () => redoTool,
2434
+ undoTool: () => undoTool
2435
+ });
2436
+ var undoTool, redoTool;
2437
+ var init_undo_redo = __esm({
2438
+ "src/tools/studio/undo-redo.ts"() {
2439
+ init_protocol();
2440
+ undoTool = {
2441
+ name: "undo",
2442
+ description: "Undo the last action in Roblox Studio using ChangeHistoryService. Works just like Ctrl+Z. Use this to roll back mistakes.",
2443
+ parameters: {
2444
+ type: "object",
2445
+ properties: {}
2446
+ },
2447
+ async execute(_params, ctx) {
2448
+ if (!ctx.isStudioConnected()) {
2449
+ return { success: false, error: "Studio is not connected" };
2450
+ }
2451
+ const response = await ctx.sendToStudio(CliMsg.UNDO, {});
2452
+ const payload = response.payload;
2453
+ if (!payload.success) {
2454
+ return { success: false, error: payload.error ?? "Undo failed" };
2455
+ }
2456
+ return { success: true, data: "Undo performed" };
2457
+ }
2458
+ };
2459
+ redoTool = {
2460
+ name: "redo",
2461
+ description: "Redo the last undone action in Roblox Studio using ChangeHistoryService. Works just like Ctrl+Y.",
2462
+ parameters: {
2463
+ type: "object",
2464
+ properties: {}
2465
+ },
2466
+ async execute(_params, ctx) {
2467
+ if (!ctx.isStudioConnected()) {
2468
+ return { success: false, error: "Studio is not connected" };
2469
+ }
2470
+ const response = await ctx.sendToStudio(CliMsg.REDO, {});
2471
+ const payload = response.payload;
2472
+ if (!payload.success) {
2473
+ return { success: false, error: payload.error ?? "Redo failed" };
2474
+ }
2475
+ return { success: true, data: "Redo performed" };
2476
+ }
2477
+ };
2478
+ }
2479
+ });
2480
+
2150
2481
  // src/mcp/server.ts
2151
2482
  var server_exports = {};
2152
2483
  __export(server_exports, {
@@ -2228,6 +2559,19 @@ async function startMcpServer() {
2228
2559
  return { content: [{ type: "text", text: JSON.stringify(res.payload, null, 2) }] };
2229
2560
  }
2230
2561
  );
2562
+ mcp.tool(
2563
+ "get_descendants_properties",
2564
+ "Easily get all descendants of a UI instance (or any instance) and all of their properties.",
2565
+ {
2566
+ path: z.string().describe('Full instance path (e.g., "Workspace.Part", "StarterGui.ScreenGui")'),
2567
+ outputFile: z.string().optional().describe("Optional file path to write to prevent limit errors/timeouts")
2568
+ },
2569
+ async ({ path: path5, outputFile }) => {
2570
+ assertConnected();
2571
+ const res = await bridge.sendRequest(CliMsg.GET_DESCENDANTS_PROPERTIES, { path: path5, outputFile }, 1e3 * 60 * 5);
2572
+ return { content: [{ type: "text", text: JSON.stringify(res.payload, null, 2) }] };
2573
+ }
2574
+ );
2231
2575
  mcp.tool(
2232
2576
  "set_properties",
2233
2577
  "Set properties on an instance in Roblox Studio",
@@ -2327,13 +2671,14 @@ async function startMcpServer() {
2327
2671
  parent: z.string().optional().describe('Parent: "StarterGui", "PlayerGui", or path. Default: StarterGui'),
2328
2672
  tree: z.record(z.unknown()).describe(
2329
2673
  'Root UI node: {ClassName, Name, properties..., Children: [{...}, ...]}. UDim2 as [xScale, xOffset, yScale, yOffset], Color3 as "#hex", Font as "GothamBold"'
2330
- )
2674
+ ),
2675
+ animate: z.boolean().optional().describe("If true, task.wait() will be injected between creation of nodes so that the UI writes out dynamically.")
2331
2676
  },
2332
- async ({ parent, tree }) => {
2677
+ async ({ parent, tree, animate }) => {
2333
2678
  assertConnected();
2334
2679
  const res = await bridge.sendRequest(
2335
2680
  CliMsg.BUILD_UI,
2336
- { parent: parent ?? "StarterGui", tree }
2681
+ { parent: parent ?? "StarterGui", tree, animate }
2337
2682
  );
2338
2683
  return { content: [{ type: "text", text: JSON.stringify(res.payload, null, 2) }] };
2339
2684
  }
@@ -3775,13 +4120,13 @@ var AIProvider = class {
3775
4120
  yield { type: "done" };
3776
4121
  }
3777
4122
  };
3778
- function toolToOpenAI(tool24) {
4123
+ function toolToOpenAI(tool29) {
3779
4124
  return {
3780
4125
  type: "function",
3781
4126
  function: {
3782
- name: tool24.name,
3783
- description: tool24.description,
3784
- parameters: tool24.parameters
4127
+ name: tool29.name,
4128
+ description: tool29.description,
4129
+ parameters: tool29.parameters
3785
4130
  }
3786
4131
  };
3787
4132
  }
@@ -4237,11 +4582,11 @@ var AgentLoop = class {
4237
4582
  // src/tools/registry.ts
4238
4583
  var ToolRegistry = class {
4239
4584
  tools = /* @__PURE__ */ new Map();
4240
- register(tool24) {
4241
- if (this.tools.has(tool24.name)) {
4242
- throw new Error(`Tool already registered: ${tool24.name}`);
4585
+ register(tool29) {
4586
+ if (this.tools.has(tool29.name)) {
4587
+ throw new Error(`Tool already registered: ${tool29.name}`);
4243
4588
  }
4244
- this.tools.set(tool24.name, tool24);
4589
+ this.tools.set(tool29.name, tool29);
4245
4590
  }
4246
4591
  get(name) {
4247
4592
  return this.tools.get(name);
@@ -4253,12 +4598,12 @@ var ToolRegistry = class {
4253
4598
  return this.getAll();
4254
4599
  }
4255
4600
  async execute(call, ctx) {
4256
- const tool24 = this.tools.get(call.name);
4257
- if (!tool24) {
4601
+ const tool29 = this.tools.get(call.name);
4602
+ if (!tool29) {
4258
4603
  return { success: false, error: `Unknown tool: ${call.name}` };
4259
4604
  }
4260
4605
  try {
4261
- return await tool24.execute(call.arguments, ctx);
4606
+ return await tool29.execute(call.arguments, ctx);
4262
4607
  } catch (err) {
4263
4608
  const message = err instanceof Error ? err.message : String(err);
4264
4609
  return { success: false, error: message };
@@ -4276,6 +4621,7 @@ function createDefaultRegistry() {
4276
4621
  Promise.resolve().then(() => (init_run_code(), run_code_exports)),
4277
4622
  Promise.resolve().then(() => (init_get_explorer(), get_explorer_exports)),
4278
4623
  Promise.resolve().then(() => (init_get_properties(), get_properties_exports)),
4624
+ Promise.resolve().then(() => (init_get_descendants_properties(), get_descendants_properties_exports)),
4279
4625
  Promise.resolve().then(() => (init_set_properties(), set_properties_exports)),
4280
4626
  Promise.resolve().then(() => (init_insert_instance(), insert_instance_exports)),
4281
4627
  Promise.resolve().then(() => (init_delete_instance(), delete_instance_exports)),
@@ -4291,6 +4637,10 @@ function createDefaultRegistry() {
4291
4637
  Promise.resolve().then(() => (init_clone_instance(), clone_instance_exports)),
4292
4638
  Promise.resolve().then(() => (init_group_instances(), group_instances_exports)),
4293
4639
  Promise.resolve().then(() => (init_build_multiple(), build_multiple_exports)),
4640
+ Promise.resolve().then(() => (init_serialize_ui(), serialize_ui_exports)),
4641
+ Promise.resolve().then(() => (init_move_instance(), move_instance_exports)),
4642
+ Promise.resolve().then(() => (init_bulk_set_properties(), bulk_set_properties_exports)),
4643
+ Promise.resolve().then(() => (init_find_replace_scripts(), find_replace_scripts_exports)),
4294
4644
  Promise.resolve().then(() => (init_recall(), recall_exports)),
4295
4645
  Promise.resolve().then(() => (init_remember(), remember_exports)),
4296
4646
  Promise.resolve().then(() => (init_upload_asset(), upload_asset_exports))
@@ -4301,6 +4651,10 @@ function createDefaultRegistry() {
4301
4651
  if (m.tool) registry.register(m.tool);
4302
4652
  })
4303
4653
  );
4654
+ void Promise.resolve().then(() => (init_undo_redo(), undo_redo_exports)).then((m) => {
4655
+ registry.register(m.undoTool);
4656
+ registry.register(m.redoTool);
4657
+ });
4304
4658
  return registry;
4305
4659
  }
4306
4660
 
@@ -4398,7 +4752,62 @@ function formatUpdateMessage(info) {
4398
4752
  ""
4399
4753
  ].join("\n");
4400
4754
  }
4401
- var VERSION = "0.2.1";
4755
+ async function autoUpdate(currentVersion) {
4756
+ const info = await checkForUpdate(currentVersion);
4757
+ if (!info?.updateAvailable) {
4758
+ return { success: true, from: currentVersion, to: currentVersion };
4759
+ }
4760
+ const managers = [
4761
+ { name: "pnpm", check: "pnpm list -g dominus-cli", cmd: "pnpm add -g dominus-cli@latest" },
4762
+ { name: "npm", check: "npm list -g dominus-cli", cmd: "npm install -g dominus-cli@latest" },
4763
+ { name: "yarn", check: "yarn global list --pattern dominus-cli", cmd: "yarn global add dominus-cli@latest" }
4764
+ ];
4765
+ let installCmd = null;
4766
+ for (const mgr of managers) {
4767
+ try {
4768
+ const out = execSync(mgr.check, { stdio: "pipe", timeout: 1e4 }).toString();
4769
+ if (out.includes("dominus-cli")) {
4770
+ installCmd = mgr.cmd;
4771
+ break;
4772
+ }
4773
+ } catch {
4774
+ }
4775
+ }
4776
+ if (!installCmd) {
4777
+ try {
4778
+ const out = execSync("pnpm list -g dominus-cli", { stdio: "pipe", timeout: 1e4 }).toString();
4779
+ if (out.includes("link:")) {
4780
+ const pkgPath = path4.resolve(__dirname$1, "..");
4781
+ if (fs5.existsSync(path4.join(pkgPath, ".git"))) {
4782
+ try {
4783
+ execSync("git pull", { cwd: pkgPath, stdio: "pipe", timeout: 3e4 });
4784
+ execSync("pnpm install", { cwd: pkgPath, stdio: "pipe", timeout: 6e4 });
4785
+ execSync("pnpm build", { cwd: pkgPath, stdio: "pipe", timeout: 6e4 });
4786
+ execSync("pnpm build:plugin", { cwd: pkgPath, stdio: "pipe", timeout: 3e4 });
4787
+ return { success: true, from: currentVersion, to: info.latestVersion };
4788
+ } catch (err) {
4789
+ return { success: false, from: currentVersion, to: info.latestVersion, error: `Git pull/rebuild failed: ${err}` };
4790
+ }
4791
+ }
4792
+ return { success: false, from: currentVersion, to: info.latestVersion, error: 'Linked install without git \u2014 run "pnpm build" manually in the repo' };
4793
+ }
4794
+ } catch {
4795
+ }
4796
+ }
4797
+ if (!installCmd) {
4798
+ installCmd = `npm install -g ${NPM_PACKAGE}@latest`;
4799
+ }
4800
+ try {
4801
+ execSync(installCmd, { stdio: "pipe", timeout: 12e4 });
4802
+ writeCache({ lastCheck: 0, latestVersion: null });
4803
+ return { success: true, from: currentVersion, to: info.latestVersion };
4804
+ } catch (err) {
4805
+ return { success: false, from: currentVersion, to: info.latestVersion, error: `Update command failed: ${err}` };
4806
+ }
4807
+ }
4808
+ var __dirname$1 = path4.dirname(new URL(import.meta.url).pathname.replace(/^\/([A-Z]:)/, "$1"));
4809
+ var __dirname2 = path4.dirname(fileURLToPath(import.meta.url));
4810
+ var VERSION = JSON.parse(fs5.readFileSync(path4.join(__dirname2, "..", "package.json"), "utf-8")).version;
4402
4811
  var program = new Command();
4403
4812
  program.name("dominus").description("AI-powered autonomous agent for Roblox Studio development").version(VERSION);
4404
4813
  program.command("start", { isDefault: true }).description("Launch the Dominus TUI agent").option("-p, --port <port>", "WebSocket server port", "18088").action(async (opts) => {
@@ -4510,9 +4919,24 @@ rulesCmd.command("list").description("List all rules").action(() => {
4510
4919
  }
4511
4920
  console.log("");
4512
4921
  });
4922
+ program.command("update").description("Auto-update Dominus to the latest version").action(async () => {
4923
+ console.log(` Current version: ${VERSION}`);
4924
+ console.log(" Checking for updates...");
4925
+ const result = await autoUpdate(VERSION);
4926
+ if (result.from === result.to && result.success) {
4927
+ console.log(" Already on the latest version.");
4928
+ return;
4929
+ }
4930
+ if (result.success) {
4931
+ console.log(` \u2714 Updated: ${result.from} \u2192 ${result.to}`);
4932
+ console.log(" Restart dominus to use the new version.");
4933
+ } else {
4934
+ console.error(` \u2718 Update failed: ${result.error}`);
4935
+ }
4936
+ });
4513
4937
  program.command("install-plugin").description("Build & install the Dominus plugin into Roblox Studio").action(async () => {
4514
- const { execSync: execSync2 } = await import('child_process');
4515
- const packageRoot = path4.resolve(import.meta.dirname ?? __dirname, "..");
4938
+ const { execSync: execSync3 } = await import('child_process');
4939
+ const packageRoot = path4.resolve(import.meta.dirname ?? __dirname2, "..");
4516
4940
  const pluginDir = path4.join(packageRoot, "plugin");
4517
4941
  const pluginProject = path4.join(pluginDir, "default.project.json");
4518
4942
  const studioPlugins = path4.join(
@@ -4533,14 +4957,14 @@ program.command("install-plugin").description("Build & install the Dominus plugi
4533
4957
  }
4534
4958
  let rojoAvailable = false;
4535
4959
  try {
4536
- execSync2("rojo --version", { stdio: "pipe" });
4960
+ execSync3("rojo --version", { stdio: "pipe" });
4537
4961
  rojoAvailable = true;
4538
4962
  } catch {
4539
4963
  }
4540
4964
  if (rojoAvailable) {
4541
4965
  try {
4542
4966
  console.log(" Building plugin with Rojo...");
4543
- execSync2(`rojo build "${pluginDir}" -o "${destFile}"`, { stdio: "pipe" });
4967
+ execSync3(`rojo build "${pluginDir}" -o "${destFile}"`, { stdio: "pipe" });
4544
4968
  console.log(` \u2714 Plugin built and installed to: ${destFile}`);
4545
4969
  console.log(" Restart Roblox Studio to load the plugin.");
4546
4970
  } catch (err) {