dominus-cli 0.5.4 → 0.5.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/mcp.js CHANGED
@@ -41,7 +41,13 @@ var CliMsg = {
41
41
  EXECUTE_PLAY_TEST: "cli:test:play",
42
42
  BUILD_UI: "cli:build:ui",
43
43
  GET_REFLECTION: "cli:get:reflection",
44
- SERIALIZE_UI: "cli:serialize:ui"};
44
+ SERIALIZE_UI: "cli:serialize:ui",
45
+ MOVE_INSTANCE: "cli:move:instance",
46
+ UNDO: "cli:undo",
47
+ REDO: "cli:redo",
48
+ BULK_SET_PROPERTIES: "cli:bulk:set:properties",
49
+ FIND_REPLACE_SCRIPTS: "cli:find:replace:scripts"
50
+ };
45
51
  function createMessage(type, payload) {
46
52
  return { id: nanoid(), type, payload, ts: Date.now() };
47
53
  }
@@ -703,10 +709,19 @@ async function startMcpServer() {
703
709
  }
704
710
  const mcp = new McpServer({
705
711
  name: "dominus",
706
- version: "0.2.1"
712
+ version: "0.5.5"
707
713
  }, {
708
714
  instructions: INTERNAL_MEMORY
709
715
  });
716
+ function luaStr(s) {
717
+ return `"${s.replace(/\\/g, "\\\\").replace(/"/g, '\\"').replace(/\n/g, "\\n").replace(/\r/g, "\\r")}"`;
718
+ }
719
+ function resolvePath(dotPath) {
720
+ const parts = dotPath.split(".");
721
+ let r = "game";
722
+ for (const p of parts) r += `[${luaStr(p)}]`;
723
+ return r;
724
+ }
710
725
  mcp.tool(
711
726
  "read_script",
712
727
  "Read the source code of a script in Roblox Studio",
@@ -758,11 +773,14 @@ async function startMcpServer() {
758
773
  );
759
774
  mcp.tool(
760
775
  "get_properties",
761
- "Get all properties of an instance in Roblox Studio",
762
- { path: z.string().describe("Full instance path") },
763
- async ({ path: path2 }) => {
776
+ "Get all properties of a single instance in Roblox Studio. Returns property name, value, type, and category. For inspecting UI trees, prefer serialize_ui (structured tree) or get_descendants_properties (flat list with compact filtering).",
777
+ {
778
+ path: z.string().describe('Full instance path (e.g. "Workspace.Part", "StarterGui.MyUI.Frame")'),
779
+ compact: z.boolean().optional().default(true).describe("Strip read-only, nil, deprecated, and default-valued properties (default true). Set false for full dump.")
780
+ },
781
+ async ({ path: path2, compact }) => {
764
782
  assertConnected();
765
- const res = await bridge.sendRequest(CliMsg.GET_PROPERTIES, { path: path2 });
783
+ const res = await bridge.sendRequest(CliMsg.GET_PROPERTIES, { path: path2, compact });
766
784
  return { content: [{ type: "text", text: JSON.stringify(res.payload, null, 2) }] };
767
785
  }
768
786
  );
@@ -978,16 +996,10 @@ async function startMcpServer() {
978
996
  },
979
997
  async (params) => {
980
998
  assertConnected();
981
- const parts = params.path.split(".");
982
- let resolve = "game";
983
- for (const p of parts) resolve += `["${p}"]`;
984
- const lines = [`local orig = ${resolve}`, `local clone = orig:Clone()`];
985
- if (params.newName) lines.push(`clone.Name = "${params.newName}"`);
999
+ const lines = [`local orig = ${resolvePath(params.path)}`, `local clone = orig:Clone()`];
1000
+ if (params.newName) lines.push(`clone.Name = ${luaStr(params.newName)}`);
986
1001
  if (params.newParent) {
987
- const pp = params.newParent.split(".");
988
- let pr = "game";
989
- for (const p of pp) pr += `["${p}"]`;
990
- lines.push(`clone.Parent = ${pr}`);
1002
+ lines.push(`clone.Parent = ${resolvePath(params.newParent)}`);
991
1003
  } else {
992
1004
  lines.push(`clone.Parent = orig.Parent`);
993
1005
  }
@@ -1003,7 +1015,7 @@ async function startMcpServer() {
1003
1015
  );
1004
1016
  mcp.tool(
1005
1017
  "group_instances",
1006
- "Group multiple instances into a Model or Folder",
1018
+ "Group multiple instances into a Model or Folder. All listed instances become children of the new group.",
1007
1019
  {
1008
1020
  paths: z.array(z.string()).describe("Array of instance paths to group"),
1009
1021
  groupName: z.string().describe("Name for the group"),
@@ -1014,20 +1026,11 @@ async function startMcpServer() {
1014
1026
  assertConnected();
1015
1027
  const gc = params.groupClass ?? "Model";
1016
1028
  const parent = params.parent ?? "Workspace";
1017
- const parentParts = parent.split(".");
1018
- let parentResolve = "game";
1019
- for (const p of parentParts) parentResolve += `["${p}"]`;
1020
- const resolves = params.paths.map((path2) => {
1021
- const parts = path2.split(".");
1022
- let r = "game";
1023
- for (const part of parts) r += `["${part}"]`;
1024
- return r;
1025
- });
1026
1029
  const code = [
1027
- `local group = Instance.new("${gc}")`,
1028
- `group.Name = "${params.groupName}"`,
1029
- `group.Parent = ${parentResolve}`,
1030
- ...resolves.map((r) => `${r}.Parent = group`),
1030
+ `local group = Instance.new(${luaStr(gc)})`,
1031
+ `group.Name = ${luaStr(params.groupName)}`,
1032
+ `group.Parent = ${resolvePath(parent)}`,
1033
+ ...params.paths.map((p) => `${resolvePath(p)}.Parent = group`),
1031
1034
  `print("Grouped ${params.paths.length} instances into: " .. group:GetFullName())`
1032
1035
  ].join("\n");
1033
1036
  const res = await bridge.sendRequest(CliMsg.RUN_CODE, { code });
@@ -1036,7 +1039,7 @@ async function startMcpServer() {
1036
1039
  );
1037
1040
  mcp.tool(
1038
1041
  "build_multiple",
1039
- "Create many parts/instances in one batch. Send an array of specs \u2014 all created in a single round-trip. Ideal for building trees, houses, vehicles.",
1042
+ "Create many parts/instances in one batch. Send an array of specs \u2014 all created in a single round-trip. Ideal for building trees, houses, vehicles. Supports hex colors, rgb() colors, and BrickColor names.",
1040
1043
  {
1041
1044
  parts: z.array(z.object({
1042
1045
  name: z.string(),
@@ -1044,10 +1047,11 @@ async function startMcpServer() {
1044
1047
  parent: z.string().optional(),
1045
1048
  position: z.object({ x: z.number(), y: z.number(), z: z.number() }).optional(),
1046
1049
  size: z.object({ x: z.number(), y: z.number(), z: z.number() }).optional(),
1047
- color: z.string().optional().describe('Hex "#RRGGBB" or BrickColor name'),
1048
- material: z.string().optional(),
1050
+ color: z.string().optional().describe('Hex "#RRGGBB", "rgb(r,g,b)", or BrickColor name'),
1051
+ material: z.string().optional().describe("Enum.Material name: Plastic, Wood, Neon, Glass, etc."),
1049
1052
  transparency: z.number().optional(),
1050
1053
  anchored: z.boolean().optional(),
1054
+ canCollide: z.boolean().optional(),
1051
1055
  shape: z.enum(["Block", "Ball", "Cylinder", "Wedge"]).optional()
1052
1056
  })).describe("Array of part specifications"),
1053
1057
  groupName: z.string().optional().describe("Group all parts into a Model with this name"),
@@ -1058,35 +1062,36 @@ async function startMcpServer() {
1058
1062
  const parts = params.parts;
1059
1063
  const groupName = params.groupName;
1060
1064
  const groupParent = params.groupParent ?? "Workspace";
1061
- function resolveP(path2) {
1062
- const segs = path2.split(".");
1063
- let r = "game";
1064
- for (const s of segs) r += `["${s}"]`;
1065
- return r;
1066
- }
1067
1065
  const lines = [];
1068
1066
  if (groupName) {
1069
1067
  lines.push(`local _group = Instance.new("Model")`);
1070
- lines.push(`_group.Name = "${groupName}"`);
1071
- lines.push(`_group.Parent = ${resolveP(groupParent)}`);
1068
+ lines.push(`_group.Name = ${luaStr(groupName)}`);
1069
+ lines.push(`_group.Parent = ${resolvePath(groupParent)}`);
1072
1070
  }
1073
1071
  for (let i = 0; i < parts.length; i++) {
1074
1072
  const p = parts[i];
1075
1073
  const v = `_p${i}`;
1076
1074
  const cn = p.className ?? (p.shape === "Wedge" ? "WedgePart" : "Part");
1077
- lines.push(`local ${v} = Instance.new("${cn}")`);
1078
- lines.push(`${v}.Name = "${p.name}"`);
1075
+ lines.push(`local ${v} = Instance.new(${luaStr(cn)})`);
1076
+ lines.push(`${v}.Name = ${luaStr(p.name)}`);
1079
1077
  if (p.position) lines.push(`${v}.Position = Vector3.new(${p.position.x}, ${p.position.y}, ${p.position.z})`);
1080
1078
  if (p.size) lines.push(`${v}.Size = Vector3.new(${p.size.x}, ${p.size.y}, ${p.size.z})`);
1081
1079
  if (p.color) {
1082
- if (p.color.startsWith("#")) lines.push(`${v}.Color = Color3.fromHex("${p.color}")`);
1083
- else lines.push(`${v}.BrickColor = BrickColor.new("${p.color}")`);
1080
+ if (p.color.startsWith("#")) {
1081
+ lines.push(`${v}.Color = Color3.fromHex(${luaStr(p.color)})`);
1082
+ } else if (p.color.startsWith("rgb(")) {
1083
+ const m = p.color.match(/rgb\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*\)/);
1084
+ if (m) lines.push(`${v}.Color = Color3.fromRGB(${m[1]}, ${m[2]}, ${m[3]})`);
1085
+ } else {
1086
+ lines.push(`${v}.BrickColor = BrickColor.new(${luaStr(p.color)})`);
1087
+ }
1084
1088
  }
1085
1089
  if (p.material) lines.push(`${v}.Material = Enum.Material.${p.material}`);
1086
1090
  if (p.transparency !== void 0) lines.push(`${v}.Transparency = ${p.transparency}`);
1087
1091
  lines.push(`${v}.Anchored = ${p.anchored ?? true}`);
1092
+ if (p.canCollide !== void 0) lines.push(`${v}.CanCollide = ${p.canCollide}`);
1088
1093
  if (p.shape && p.shape !== "Block" && cn === "Part") lines.push(`${v}.Shape = Enum.PartType.${p.shape}`);
1089
- lines.push(`${v}.Parent = ${groupName ? "_group" : resolveP(p.parent ?? "Workspace")}`);
1094
+ lines.push(`${v}.Parent = ${groupName ? "_group" : resolvePath(p.parent ?? "Workspace")}`);
1090
1095
  }
1091
1096
  lines.push(`print("Built ${parts.length} parts${groupName ? ` in ${groupName}` : ""}")`);
1092
1097
  const res = await bridge.sendRequest(CliMsg.RUN_CODE, { code: lines.join("\n") });
@@ -1113,6 +1118,184 @@ async function startMcpServer() {
1113
1118
  return { content: [{ type: "text", text: JSON.stringify(res.payload.tree, null, 2) }] };
1114
1119
  }
1115
1120
  );
1121
+ mcp.tool(
1122
+ "move_instance",
1123
+ "Move (reparent) an instance to a new parent in Roblox Studio. Preserves the instance and all descendants \u2014 much better than delete + recreate. Sets a ChangeHistoryService waypoint so the move can be undone.",
1124
+ {
1125
+ path: z.string().describe('Full path of the instance to move (e.g. "Workspace.OldFolder.MyPart")'),
1126
+ newParent: z.string().describe('Full path of the new parent (e.g. "Workspace.NewFolder")')
1127
+ },
1128
+ async ({ path: path2, newParent }) => {
1129
+ assertConnected();
1130
+ const res = await bridge.sendRequest(
1131
+ CliMsg.MOVE_INSTANCE,
1132
+ { path: path2, newParent }
1133
+ );
1134
+ return { content: [{ type: "text", text: JSON.stringify(res.payload, null, 2) }] };
1135
+ }
1136
+ );
1137
+ mcp.tool(
1138
+ "bulk_set_properties",
1139
+ "Set properties on multiple instances at once in a single call. Much faster than calling set_properties repeatedly. Two modes:\n1. Explicit: provide targets array of {path, properties} objects\n2. Filter: provide root + className + properties to apply to all matches",
1140
+ {
1141
+ targets: z.array(z.object({
1142
+ path: z.string(),
1143
+ properties: z.record(z.unknown())
1144
+ })).optional().describe("Array of {path, properties} for explicit mode"),
1145
+ root: z.string().optional().describe("Root path to search under (filter mode)"),
1146
+ className: z.string().optional().describe("Only apply to instances of this ClassName (filter mode)"),
1147
+ properties: z.record(z.unknown()).optional().describe("Properties to set on all matches (filter mode)")
1148
+ },
1149
+ async (params) => {
1150
+ assertConnected();
1151
+ const res = await bridge.sendRequest(
1152
+ CliMsg.BULK_SET_PROPERTIES,
1153
+ {
1154
+ targets: params.targets,
1155
+ root: params.root,
1156
+ className: params.className,
1157
+ properties: params.properties
1158
+ },
1159
+ 1e3 * 60
1160
+ );
1161
+ return { content: [{ type: "text", text: JSON.stringify(res.payload, null, 2) }] };
1162
+ }
1163
+ );
1164
+ mcp.tool(
1165
+ "find_replace_scripts",
1166
+ "Find and replace text across all scripts in the game (or under a root path). Supports plain text and Lua patterns. Use dryRun=true to preview changes without applying.",
1167
+ {
1168
+ find: z.string().describe("Text or Lua pattern to search for"),
1169
+ replace: z.string().describe("Replacement text (supports Lua pattern captures like %1, %2)"),
1170
+ root: z.string().optional().describe("Root path to limit search scope (optional)"),
1171
+ usePattern: z.boolean().optional().default(false).describe("Treat find/replace as Lua patterns"),
1172
+ dryRun: z.boolean().optional().default(false).describe("Preview matches without modifying scripts")
1173
+ },
1174
+ async (params) => {
1175
+ assertConnected();
1176
+ const res = await bridge.sendRequest(
1177
+ CliMsg.FIND_REPLACE_SCRIPTS,
1178
+ {
1179
+ find: params.find,
1180
+ replace: params.replace,
1181
+ root: params.root,
1182
+ usePattern: params.usePattern,
1183
+ dryRun: params.dryRun
1184
+ },
1185
+ 1e3 * 60 * 5
1186
+ );
1187
+ return { content: [{ type: "text", text: JSON.stringify(res.payload, null, 2) }] };
1188
+ }
1189
+ );
1190
+ mcp.tool(
1191
+ "undo",
1192
+ "Undo the last action in Roblox Studio (ChangeHistoryService). Works like Ctrl+Z. Use to roll back mistakes.",
1193
+ {},
1194
+ async () => {
1195
+ assertConnected();
1196
+ const res = await bridge.sendRequest(CliMsg.UNDO, {});
1197
+ return { content: [{ type: "text", text: JSON.stringify(res.payload) }] };
1198
+ }
1199
+ );
1200
+ mcp.tool(
1201
+ "redo",
1202
+ "Redo the last undone action in Roblox Studio (ChangeHistoryService). Works like Ctrl+Y.",
1203
+ {},
1204
+ async () => {
1205
+ assertConnected();
1206
+ const res = await bridge.sendRequest(CliMsg.REDO, {});
1207
+ return { content: [{ type: "text", text: JSON.stringify(res.payload) }] };
1208
+ }
1209
+ );
1210
+ mcp.tool(
1211
+ "move_instance",
1212
+ "Move (reparent) an instance to a new parent in Roblox Studio. Preserves the instance and all descendants \u2014 much better than delete + recreate. Sets a ChangeHistoryService waypoint so the move can be undone.",
1213
+ {
1214
+ path: z.string().describe('Full path of the instance to move (e.g. "Workspace.OldFolder.MyPart")'),
1215
+ newParent: z.string().describe('Full path of the new parent (e.g. "Workspace.NewFolder")')
1216
+ },
1217
+ async ({ path: path2, newParent }) => {
1218
+ assertConnected();
1219
+ const res = await bridge.sendRequest(
1220
+ CliMsg.MOVE_INSTANCE,
1221
+ { path: path2, newParent }
1222
+ );
1223
+ return { content: [{ type: "text", text: JSON.stringify(res.payload, null, 2) }] };
1224
+ }
1225
+ );
1226
+ mcp.tool(
1227
+ "bulk_set_properties",
1228
+ "Set properties on multiple instances at once in a single call. Much faster than calling set_properties repeatedly. Two modes:\n1. Explicit: provide targets array of {path, properties} objects\n2. Filter: provide root + className + properties to apply to all matches",
1229
+ {
1230
+ targets: z.array(z.object({
1231
+ path: z.string(),
1232
+ properties: z.record(z.unknown())
1233
+ })).optional().describe("Array of {path, properties} for explicit mode"),
1234
+ root: z.string().optional().describe("Root path to search under (filter mode)"),
1235
+ className: z.string().optional().describe("Only apply to instances of this ClassName (filter mode)"),
1236
+ properties: z.record(z.unknown()).optional().describe("Properties to set on all matches (filter mode)")
1237
+ },
1238
+ async (params) => {
1239
+ assertConnected();
1240
+ const res = await bridge.sendRequest(
1241
+ CliMsg.BULK_SET_PROPERTIES,
1242
+ {
1243
+ targets: params.targets,
1244
+ root: params.root,
1245
+ className: params.className,
1246
+ properties: params.properties
1247
+ },
1248
+ 1e3 * 60
1249
+ );
1250
+ return { content: [{ type: "text", text: JSON.stringify(res.payload, null, 2) }] };
1251
+ }
1252
+ );
1253
+ mcp.tool(
1254
+ "find_replace_scripts",
1255
+ "Find and replace text across all scripts in the game (or under a root path). Supports plain text and Lua patterns. Use dryRun=true to preview changes without applying.",
1256
+ {
1257
+ find: z.string().describe("Text or Lua pattern to search for"),
1258
+ replace: z.string().describe("Replacement text (supports Lua pattern captures like %1, %2)"),
1259
+ root: z.string().optional().describe("Root path to limit search scope (optional)"),
1260
+ usePattern: z.boolean().optional().default(false).describe("Treat find/replace as Lua patterns"),
1261
+ dryRun: z.boolean().optional().default(false).describe("Preview matches without modifying scripts")
1262
+ },
1263
+ async (params) => {
1264
+ assertConnected();
1265
+ const res = await bridge.sendRequest(
1266
+ CliMsg.FIND_REPLACE_SCRIPTS,
1267
+ {
1268
+ find: params.find,
1269
+ replace: params.replace,
1270
+ root: params.root,
1271
+ usePattern: params.usePattern,
1272
+ dryRun: params.dryRun
1273
+ },
1274
+ 1e3 * 60 * 5
1275
+ );
1276
+ return { content: [{ type: "text", text: JSON.stringify(res.payload, null, 2) }] };
1277
+ }
1278
+ );
1279
+ mcp.tool(
1280
+ "undo",
1281
+ "Undo the last action in Roblox Studio (ChangeHistoryService). Works like Ctrl+Z. Use to roll back mistakes.",
1282
+ {},
1283
+ async () => {
1284
+ assertConnected();
1285
+ const res = await bridge.sendRequest(CliMsg.UNDO, {});
1286
+ return { content: [{ type: "text", text: JSON.stringify(res.payload) }] };
1287
+ }
1288
+ );
1289
+ mcp.tool(
1290
+ "redo",
1291
+ "Redo the last undone action in Roblox Studio (ChangeHistoryService). Works like Ctrl+Y.",
1292
+ {},
1293
+ async () => {
1294
+ assertConnected();
1295
+ const res = await bridge.sendRequest(CliMsg.REDO, {});
1296
+ return { content: [{ type: "text", text: JSON.stringify(res.payload) }] };
1297
+ }
1298
+ );
1116
1299
  mcp.tool(
1117
1300
  "recall_memory",
1118
1301
  "Search persistent memory for facts about the current Roblox project",