dominus-cli 0.5.3 → 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/index.js CHANGED
@@ -1180,13 +1180,17 @@ var init_get_properties = __esm({
1180
1180
  init_protocol();
1181
1181
  tool5 = {
1182
1182
  name: "get_properties",
1183
- description: "Get all properties of an instance in Roblox Studio by its path.",
1183
+ description: "Get all properties of a single instance in Roblox Studio. By default uses compact mode which strips read-only, nil, deprecated, and default-valued properties. For inspecting UI trees, prefer serialize_ui or get_descendants_properties.",
1184
1184
  parameters: {
1185
1185
  type: "object",
1186
1186
  properties: {
1187
1187
  path: {
1188
1188
  type: "string",
1189
1189
  description: 'Full instance path (e.g., "Workspace.Part")'
1190
+ },
1191
+ compact: {
1192
+ type: "boolean",
1193
+ description: "Strip read-only, nil, deprecated, and default-valued properties (default true). Set false for full dump."
1190
1194
  }
1191
1195
  },
1192
1196
  required: ["path"]
@@ -1195,9 +1199,10 @@ var init_get_properties = __esm({
1195
1199
  if (!ctx.isStudioConnected()) {
1196
1200
  return { success: false, error: "Studio is not connected" };
1197
1201
  }
1202
+ const compact = params.compact !== false;
1198
1203
  const response = await ctx.sendToStudio(
1199
1204
  CliMsg.GET_PROPERTIES,
1200
- { path: params.path }
1205
+ { path: params.path, compact }
1201
1206
  );
1202
1207
  return { success: true, data: response.payload };
1203
1208
  }
@@ -2581,10 +2586,19 @@ async function startMcpServer() {
2581
2586
  }
2582
2587
  const mcp = new McpServer({
2583
2588
  name: "dominus",
2584
- version: "0.2.1"
2589
+ version: "0.5.5"
2585
2590
  }, {
2586
2591
  instructions: INTERNAL_MEMORY
2587
2592
  });
2593
+ function luaStr(s) {
2594
+ return `"${s.replace(/\\/g, "\\\\").replace(/"/g, '\\"').replace(/\n/g, "\\n").replace(/\r/g, "\\r")}"`;
2595
+ }
2596
+ function resolvePath(dotPath) {
2597
+ const parts = dotPath.split(".");
2598
+ let r = "game";
2599
+ for (const p of parts) r += `[${luaStr(p)}]`;
2600
+ return r;
2601
+ }
2588
2602
  mcp.tool(
2589
2603
  "read_script",
2590
2604
  "Read the source code of a script in Roblox Studio",
@@ -2636,11 +2650,14 @@ async function startMcpServer() {
2636
2650
  );
2637
2651
  mcp.tool(
2638
2652
  "get_properties",
2639
- "Get all properties of an instance in Roblox Studio",
2640
- { path: z.string().describe("Full instance path") },
2641
- async ({ path: path5 }) => {
2653
+ "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).",
2654
+ {
2655
+ path: z.string().describe('Full instance path (e.g. "Workspace.Part", "StarterGui.MyUI.Frame")'),
2656
+ compact: z.boolean().optional().default(true).describe("Strip read-only, nil, deprecated, and default-valued properties (default true). Set false for full dump.")
2657
+ },
2658
+ async ({ path: path5, compact }) => {
2642
2659
  assertConnected();
2643
- const res = await bridge.sendRequest(CliMsg.GET_PROPERTIES, { path: path5 });
2660
+ const res = await bridge.sendRequest(CliMsg.GET_PROPERTIES, { path: path5, compact });
2644
2661
  return { content: [{ type: "text", text: JSON.stringify(res.payload, null, 2) }] };
2645
2662
  }
2646
2663
  );
@@ -2856,16 +2873,10 @@ async function startMcpServer() {
2856
2873
  },
2857
2874
  async (params) => {
2858
2875
  assertConnected();
2859
- const parts = params.path.split(".");
2860
- let resolve = "game";
2861
- for (const p of parts) resolve += `["${p}"]`;
2862
- const lines = [`local orig = ${resolve}`, `local clone = orig:Clone()`];
2863
- if (params.newName) lines.push(`clone.Name = "${params.newName}"`);
2876
+ const lines = [`local orig = ${resolvePath(params.path)}`, `local clone = orig:Clone()`];
2877
+ if (params.newName) lines.push(`clone.Name = ${luaStr(params.newName)}`);
2864
2878
  if (params.newParent) {
2865
- const pp = params.newParent.split(".");
2866
- let pr = "game";
2867
- for (const p of pp) pr += `["${p}"]`;
2868
- lines.push(`clone.Parent = ${pr}`);
2879
+ lines.push(`clone.Parent = ${resolvePath(params.newParent)}`);
2869
2880
  } else {
2870
2881
  lines.push(`clone.Parent = orig.Parent`);
2871
2882
  }
@@ -2881,7 +2892,7 @@ async function startMcpServer() {
2881
2892
  );
2882
2893
  mcp.tool(
2883
2894
  "group_instances",
2884
- "Group multiple instances into a Model or Folder",
2895
+ "Group multiple instances into a Model or Folder. All listed instances become children of the new group.",
2885
2896
  {
2886
2897
  paths: z.array(z.string()).describe("Array of instance paths to group"),
2887
2898
  groupName: z.string().describe("Name for the group"),
@@ -2892,20 +2903,11 @@ async function startMcpServer() {
2892
2903
  assertConnected();
2893
2904
  const gc = params.groupClass ?? "Model";
2894
2905
  const parent = params.parent ?? "Workspace";
2895
- const parentParts = parent.split(".");
2896
- let parentResolve = "game";
2897
- for (const p of parentParts) parentResolve += `["${p}"]`;
2898
- const resolves = params.paths.map((path5) => {
2899
- const parts = path5.split(".");
2900
- let r = "game";
2901
- for (const part of parts) r += `["${part}"]`;
2902
- return r;
2903
- });
2904
2906
  const code = [
2905
- `local group = Instance.new("${gc}")`,
2906
- `group.Name = "${params.groupName}"`,
2907
- `group.Parent = ${parentResolve}`,
2908
- ...resolves.map((r) => `${r}.Parent = group`),
2907
+ `local group = Instance.new(${luaStr(gc)})`,
2908
+ `group.Name = ${luaStr(params.groupName)}`,
2909
+ `group.Parent = ${resolvePath(parent)}`,
2910
+ ...params.paths.map((p) => `${resolvePath(p)}.Parent = group`),
2909
2911
  `print("Grouped ${params.paths.length} instances into: " .. group:GetFullName())`
2910
2912
  ].join("\n");
2911
2913
  const res = await bridge.sendRequest(CliMsg.RUN_CODE, { code });
@@ -2914,7 +2916,7 @@ async function startMcpServer() {
2914
2916
  );
2915
2917
  mcp.tool(
2916
2918
  "build_multiple",
2917
- "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.",
2919
+ "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.",
2918
2920
  {
2919
2921
  parts: z.array(z.object({
2920
2922
  name: z.string(),
@@ -2922,10 +2924,11 @@ async function startMcpServer() {
2922
2924
  parent: z.string().optional(),
2923
2925
  position: z.object({ x: z.number(), y: z.number(), z: z.number() }).optional(),
2924
2926
  size: z.object({ x: z.number(), y: z.number(), z: z.number() }).optional(),
2925
- color: z.string().optional().describe('Hex "#RRGGBB" or BrickColor name'),
2926
- material: z.string().optional(),
2927
+ color: z.string().optional().describe('Hex "#RRGGBB", "rgb(r,g,b)", or BrickColor name'),
2928
+ material: z.string().optional().describe("Enum.Material name: Plastic, Wood, Neon, Glass, etc."),
2927
2929
  transparency: z.number().optional(),
2928
2930
  anchored: z.boolean().optional(),
2931
+ canCollide: z.boolean().optional(),
2929
2932
  shape: z.enum(["Block", "Ball", "Cylinder", "Wedge"]).optional()
2930
2933
  })).describe("Array of part specifications"),
2931
2934
  groupName: z.string().optional().describe("Group all parts into a Model with this name"),
@@ -2936,35 +2939,36 @@ async function startMcpServer() {
2936
2939
  const parts = params.parts;
2937
2940
  const groupName = params.groupName;
2938
2941
  const groupParent = params.groupParent ?? "Workspace";
2939
- function resolveP(path5) {
2940
- const segs = path5.split(".");
2941
- let r = "game";
2942
- for (const s of segs) r += `["${s}"]`;
2943
- return r;
2944
- }
2945
2942
  const lines = [];
2946
2943
  if (groupName) {
2947
2944
  lines.push(`local _group = Instance.new("Model")`);
2948
- lines.push(`_group.Name = "${groupName}"`);
2949
- lines.push(`_group.Parent = ${resolveP(groupParent)}`);
2945
+ lines.push(`_group.Name = ${luaStr(groupName)}`);
2946
+ lines.push(`_group.Parent = ${resolvePath(groupParent)}`);
2950
2947
  }
2951
2948
  for (let i = 0; i < parts.length; i++) {
2952
2949
  const p = parts[i];
2953
2950
  const v = `_p${i}`;
2954
2951
  const cn = p.className ?? (p.shape === "Wedge" ? "WedgePart" : "Part");
2955
- lines.push(`local ${v} = Instance.new("${cn}")`);
2956
- lines.push(`${v}.Name = "${p.name}"`);
2952
+ lines.push(`local ${v} = Instance.new(${luaStr(cn)})`);
2953
+ lines.push(`${v}.Name = ${luaStr(p.name)}`);
2957
2954
  if (p.position) lines.push(`${v}.Position = Vector3.new(${p.position.x}, ${p.position.y}, ${p.position.z})`);
2958
2955
  if (p.size) lines.push(`${v}.Size = Vector3.new(${p.size.x}, ${p.size.y}, ${p.size.z})`);
2959
2956
  if (p.color) {
2960
- if (p.color.startsWith("#")) lines.push(`${v}.Color = Color3.fromHex("${p.color}")`);
2961
- else lines.push(`${v}.BrickColor = BrickColor.new("${p.color}")`);
2957
+ if (p.color.startsWith("#")) {
2958
+ lines.push(`${v}.Color = Color3.fromHex(${luaStr(p.color)})`);
2959
+ } else if (p.color.startsWith("rgb(")) {
2960
+ const m = p.color.match(/rgb\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*\)/);
2961
+ if (m) lines.push(`${v}.Color = Color3.fromRGB(${m[1]}, ${m[2]}, ${m[3]})`);
2962
+ } else {
2963
+ lines.push(`${v}.BrickColor = BrickColor.new(${luaStr(p.color)})`);
2964
+ }
2962
2965
  }
2963
2966
  if (p.material) lines.push(`${v}.Material = Enum.Material.${p.material}`);
2964
2967
  if (p.transparency !== void 0) lines.push(`${v}.Transparency = ${p.transparency}`);
2965
2968
  lines.push(`${v}.Anchored = ${p.anchored ?? true}`);
2969
+ if (p.canCollide !== void 0) lines.push(`${v}.CanCollide = ${p.canCollide}`);
2966
2970
  if (p.shape && p.shape !== "Block" && cn === "Part") lines.push(`${v}.Shape = Enum.PartType.${p.shape}`);
2967
- lines.push(`${v}.Parent = ${groupName ? "_group" : resolveP(p.parent ?? "Workspace")}`);
2971
+ lines.push(`${v}.Parent = ${groupName ? "_group" : resolvePath(p.parent ?? "Workspace")}`);
2968
2972
  }
2969
2973
  lines.push(`print("Built ${parts.length} parts${groupName ? ` in ${groupName}` : ""}")`);
2970
2974
  const res = await bridge.sendRequest(CliMsg.RUN_CODE, { code: lines.join("\n") });
@@ -2991,6 +2995,184 @@ async function startMcpServer() {
2991
2995
  return { content: [{ type: "text", text: JSON.stringify(res.payload.tree, null, 2) }] };
2992
2996
  }
2993
2997
  );
2998
+ mcp.tool(
2999
+ "move_instance",
3000
+ "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.",
3001
+ {
3002
+ path: z.string().describe('Full path of the instance to move (e.g. "Workspace.OldFolder.MyPart")'),
3003
+ newParent: z.string().describe('Full path of the new parent (e.g. "Workspace.NewFolder")')
3004
+ },
3005
+ async ({ path: path5, newParent }) => {
3006
+ assertConnected();
3007
+ const res = await bridge.sendRequest(
3008
+ CliMsg.MOVE_INSTANCE,
3009
+ { path: path5, newParent }
3010
+ );
3011
+ return { content: [{ type: "text", text: JSON.stringify(res.payload, null, 2) }] };
3012
+ }
3013
+ );
3014
+ mcp.tool(
3015
+ "bulk_set_properties",
3016
+ "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",
3017
+ {
3018
+ targets: z.array(z.object({
3019
+ path: z.string(),
3020
+ properties: z.record(z.unknown())
3021
+ })).optional().describe("Array of {path, properties} for explicit mode"),
3022
+ root: z.string().optional().describe("Root path to search under (filter mode)"),
3023
+ className: z.string().optional().describe("Only apply to instances of this ClassName (filter mode)"),
3024
+ properties: z.record(z.unknown()).optional().describe("Properties to set on all matches (filter mode)")
3025
+ },
3026
+ async (params) => {
3027
+ assertConnected();
3028
+ const res = await bridge.sendRequest(
3029
+ CliMsg.BULK_SET_PROPERTIES,
3030
+ {
3031
+ targets: params.targets,
3032
+ root: params.root,
3033
+ className: params.className,
3034
+ properties: params.properties
3035
+ },
3036
+ 1e3 * 60
3037
+ );
3038
+ return { content: [{ type: "text", text: JSON.stringify(res.payload, null, 2) }] };
3039
+ }
3040
+ );
3041
+ mcp.tool(
3042
+ "find_replace_scripts",
3043
+ "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.",
3044
+ {
3045
+ find: z.string().describe("Text or Lua pattern to search for"),
3046
+ replace: z.string().describe("Replacement text (supports Lua pattern captures like %1, %2)"),
3047
+ root: z.string().optional().describe("Root path to limit search scope (optional)"),
3048
+ usePattern: z.boolean().optional().default(false).describe("Treat find/replace as Lua patterns"),
3049
+ dryRun: z.boolean().optional().default(false).describe("Preview matches without modifying scripts")
3050
+ },
3051
+ async (params) => {
3052
+ assertConnected();
3053
+ const res = await bridge.sendRequest(
3054
+ CliMsg.FIND_REPLACE_SCRIPTS,
3055
+ {
3056
+ find: params.find,
3057
+ replace: params.replace,
3058
+ root: params.root,
3059
+ usePattern: params.usePattern,
3060
+ dryRun: params.dryRun
3061
+ },
3062
+ 1e3 * 60 * 5
3063
+ );
3064
+ return { content: [{ type: "text", text: JSON.stringify(res.payload, null, 2) }] };
3065
+ }
3066
+ );
3067
+ mcp.tool(
3068
+ "undo",
3069
+ "Undo the last action in Roblox Studio (ChangeHistoryService). Works like Ctrl+Z. Use to roll back mistakes.",
3070
+ {},
3071
+ async () => {
3072
+ assertConnected();
3073
+ const res = await bridge.sendRequest(CliMsg.UNDO, {});
3074
+ return { content: [{ type: "text", text: JSON.stringify(res.payload) }] };
3075
+ }
3076
+ );
3077
+ mcp.tool(
3078
+ "redo",
3079
+ "Redo the last undone action in Roblox Studio (ChangeHistoryService). Works like Ctrl+Y.",
3080
+ {},
3081
+ async () => {
3082
+ assertConnected();
3083
+ const res = await bridge.sendRequest(CliMsg.REDO, {});
3084
+ return { content: [{ type: "text", text: JSON.stringify(res.payload) }] };
3085
+ }
3086
+ );
3087
+ mcp.tool(
3088
+ "move_instance",
3089
+ "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.",
3090
+ {
3091
+ path: z.string().describe('Full path of the instance to move (e.g. "Workspace.OldFolder.MyPart")'),
3092
+ newParent: z.string().describe('Full path of the new parent (e.g. "Workspace.NewFolder")')
3093
+ },
3094
+ async ({ path: path5, newParent }) => {
3095
+ assertConnected();
3096
+ const res = await bridge.sendRequest(
3097
+ CliMsg.MOVE_INSTANCE,
3098
+ { path: path5, newParent }
3099
+ );
3100
+ return { content: [{ type: "text", text: JSON.stringify(res.payload, null, 2) }] };
3101
+ }
3102
+ );
3103
+ mcp.tool(
3104
+ "bulk_set_properties",
3105
+ "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",
3106
+ {
3107
+ targets: z.array(z.object({
3108
+ path: z.string(),
3109
+ properties: z.record(z.unknown())
3110
+ })).optional().describe("Array of {path, properties} for explicit mode"),
3111
+ root: z.string().optional().describe("Root path to search under (filter mode)"),
3112
+ className: z.string().optional().describe("Only apply to instances of this ClassName (filter mode)"),
3113
+ properties: z.record(z.unknown()).optional().describe("Properties to set on all matches (filter mode)")
3114
+ },
3115
+ async (params) => {
3116
+ assertConnected();
3117
+ const res = await bridge.sendRequest(
3118
+ CliMsg.BULK_SET_PROPERTIES,
3119
+ {
3120
+ targets: params.targets,
3121
+ root: params.root,
3122
+ className: params.className,
3123
+ properties: params.properties
3124
+ },
3125
+ 1e3 * 60
3126
+ );
3127
+ return { content: [{ type: "text", text: JSON.stringify(res.payload, null, 2) }] };
3128
+ }
3129
+ );
3130
+ mcp.tool(
3131
+ "find_replace_scripts",
3132
+ "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.",
3133
+ {
3134
+ find: z.string().describe("Text or Lua pattern to search for"),
3135
+ replace: z.string().describe("Replacement text (supports Lua pattern captures like %1, %2)"),
3136
+ root: z.string().optional().describe("Root path to limit search scope (optional)"),
3137
+ usePattern: z.boolean().optional().default(false).describe("Treat find/replace as Lua patterns"),
3138
+ dryRun: z.boolean().optional().default(false).describe("Preview matches without modifying scripts")
3139
+ },
3140
+ async (params) => {
3141
+ assertConnected();
3142
+ const res = await bridge.sendRequest(
3143
+ CliMsg.FIND_REPLACE_SCRIPTS,
3144
+ {
3145
+ find: params.find,
3146
+ replace: params.replace,
3147
+ root: params.root,
3148
+ usePattern: params.usePattern,
3149
+ dryRun: params.dryRun
3150
+ },
3151
+ 1e3 * 60 * 5
3152
+ );
3153
+ return { content: [{ type: "text", text: JSON.stringify(res.payload, null, 2) }] };
3154
+ }
3155
+ );
3156
+ mcp.tool(
3157
+ "undo",
3158
+ "Undo the last action in Roblox Studio (ChangeHistoryService). Works like Ctrl+Z. Use to roll back mistakes.",
3159
+ {},
3160
+ async () => {
3161
+ assertConnected();
3162
+ const res = await bridge.sendRequest(CliMsg.UNDO, {});
3163
+ return { content: [{ type: "text", text: JSON.stringify(res.payload) }] };
3164
+ }
3165
+ );
3166
+ mcp.tool(
3167
+ "redo",
3168
+ "Redo the last undone action in Roblox Studio (ChangeHistoryService). Works like Ctrl+Y.",
3169
+ {},
3170
+ async () => {
3171
+ assertConnected();
3172
+ const res = await bridge.sendRequest(CliMsg.REDO, {});
3173
+ return { content: [{ type: "text", text: JSON.stringify(res.payload) }] };
3174
+ }
3175
+ );
2994
3176
  mcp.tool(
2995
3177
  "recall_memory",
2996
3178
  "Search persistent memory for facts about the current Roblox project",