dominus-cli 2.2.1 → 2.3.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/mcp.js CHANGED
@@ -109,6 +109,14 @@ var CliMsg = {
109
109
  V2_READ_SCRIPT: "studio:v2:read_script",
110
110
  V2_UPDATE_SCRIPT: "studio:v2:update_script",
111
111
  V2_APPLY: "studio:v2:apply",
112
+ V2_CREATE_PARTS: "studio:v2:create_parts",
113
+ V2_CLONE_INSTANCES: "studio:v2:clone_instances",
114
+ V2_GROUP_INSTANCES: "studio:v2:group_instances",
115
+ V2_UNGROUP_INSTANCES: "studio:v2:ungroup_instances",
116
+ V2_TRANSFORM_INSTANCES: "studio:v2:transform_instances",
117
+ V2_CREATE_WELDS: "studio:v2:create_welds",
118
+ V2_QUERY_PARTS: "studio:v2:query_parts",
119
+ V2_SET_SELECTION: "studio:v2:set_selection",
112
120
  V2_DELETE: "studio:v2:delete",
113
121
  V2_BUILD_UI: "studio:v2:build_ui",
114
122
  V2_SNAPSHOT_UI: "studio:v2:snapshot_ui",
@@ -561,11 +569,18 @@ Required workflow for Studio changes:
561
569
 
562
570
  For broad tasks with independent Studio branches, prefer run_parallel_task when the MCP client supports Sampling. Dominus partitions immediate child scopes, runs proposal-only workers concurrently, rejects unknown refs and overlapping writes, applies only the coordinator-approved atomic plan, and reads the result back. Use the normal workflow when Sampling is unavailable or the task is a simple edit.
563
571
 
572
+ Building workflow:
573
+ - Use studio_create_parts for multi-part structures instead of many generic create operations.
574
+ - Use studio_clone_instances when an existing instance or model is the intended template. Clone rather than manually recreating descendants and properties.
575
+ - Use studio_transform_instances for Models and BaseParts so model layouts move through PVInstance pivots.
576
+ - Use studio_group_instances and studio_ungroup_instances to organize hierarchy, studio_create_welds for rigid assemblies, and studio_query_parts to inspect crowded 3D regions before editing.
577
+ - Use studio_set_selection when surfacing completed objects to the Studio user.
578
+
564
579
  Safety rules:
565
580
  - Never invent instance IDs or Roblox API members.
566
581
  - Do not retry a failed write unchanged. Inspect the error and current state first.
567
582
  - Deletion is a separate destructive tool and requires explicit user intent.
568
- - studio_apply is transactional. Keep related edits together, but stay within its documented limits.
583
+ - studio_apply is transactional and supports setProperties, create, clone, and move operations. Keep related edits together, but stay within its documented limits.
569
584
  - UI fidelity work must snapshot and inspect the result after building; verify fonts, UDim2 values, colors, strokes, and hierarchy.
570
585
  - Dominus intentionally has no arbitrary Luau execution tool.`;
571
586
 
@@ -632,6 +647,8 @@ function failure(error, data) {
632
647
  data
633
648
  });
634
649
  }
650
+
651
+ // src/mcp/tools/shared.ts
635
652
  var instanceRefSchema = z.object({
636
653
  instanceId: z.string().uuid().optional(),
637
654
  pathSegments: z.array(z.string().min(1).max(256)).min(1).max(100).optional()
@@ -664,7 +681,245 @@ function ref(value) {
664
681
  return value;
665
682
  }
666
683
 
667
- // src/mcp/tools/connections.ts
684
+ // src/mcp/tools/building.ts
685
+ var finiteNumber = z.number().finite();
686
+ var vector3Schema = z.object({ x: finiteNumber, y: finiteNumber, z: finiteNumber }).strict();
687
+ var propertiesSchema = z.record(z.unknown()).refine(
688
+ (properties) => Object.keys(properties).length <= 100 && !Object.keys(properties).some((name) => ["Parent", "ClassName", "Source"].includes(name)),
689
+ "At most 100 properties are allowed; Parent, ClassName, and Source require dedicated operations"
690
+ );
691
+ var partSchema = z.object({
692
+ name: z.string().min(1).max(100),
693
+ className: z.enum([
694
+ "Part",
695
+ "WedgePart",
696
+ "CornerWedgePart",
697
+ "TrussPart",
698
+ "MeshPart",
699
+ "SpawnLocation",
700
+ "Seat",
701
+ "VehicleSeat"
702
+ ]).optional().default("Part"),
703
+ shape: z.enum(["Block", "Ball", "Cylinder", "Wedge", "CornerWedge"]).optional(),
704
+ position: vector3Schema.optional(),
705
+ orientation: vector3Schema.optional(),
706
+ size: vector3Schema.optional(),
707
+ color: z.string().min(1).max(100).optional(),
708
+ material: z.string().min(1).max(100).optional(),
709
+ transparency: z.number().min(0).max(1).optional(),
710
+ anchored: z.boolean().optional().default(true),
711
+ canCollide: z.boolean().optional(),
712
+ properties: propertiesSchema.optional()
713
+ });
714
+ var cloneRequestSchema = z.object({
715
+ target: instanceRefSchema,
716
+ parent: instanceRefSchema.optional(),
717
+ name: z.string().min(1).max(100).optional(),
718
+ count: z.number().int().min(1).max(50).optional().default(1),
719
+ offset: vector3Schema.optional(),
720
+ stepOffset: vector3Schema.optional(),
721
+ properties: propertiesSchema.optional()
722
+ });
723
+ var cframeSchema = z.array(finiteNumber).refine((value) => value.length === 3 || value.length === 12, "CFrame requires 3 or 12 numbers");
724
+ var transformOperationSchema = z.object({
725
+ target: instanceRefSchema,
726
+ mode: z.enum(["absolute", "relative"]).optional().default("absolute"),
727
+ cframe: cframeSchema.optional(),
728
+ position: vector3Schema.optional(),
729
+ orientation: vector3Schema.optional(),
730
+ offset: vector3Schema.optional(),
731
+ rotationOffset: vector3Schema.optional(),
732
+ space: z.enum(["world", "local"]).optional().default("world")
733
+ }).refine(
734
+ (value) => value.mode === "relative" ? Boolean(value.offset || value.rotationOffset) : Boolean(value.cframe || value.position || value.orientation),
735
+ "Absolute transforms require cframe, position, or orientation; relative transforms require offset or rotationOffset"
736
+ );
737
+ function registerBuildingTools(server, bridge) {
738
+ server.registerTool(
739
+ "studio_create_parts",
740
+ {
741
+ title: "Build Roblox Parts",
742
+ description: "Create up to 200 typed BaseParts in one undoable batch, optionally grouped under a new Model or Folder. Supports positions, orientations, sizes, colors, materials, shapes, and additional writable properties.",
743
+ inputSchema: {
744
+ parent: instanceRefSchema.optional(),
745
+ groupName: z.string().min(1).max(100).optional(),
746
+ groupClass: z.enum(["Model", "Folder"]).optional().default("Model"),
747
+ parts: z.array(partSchema).min(1).max(200)
748
+ },
749
+ outputSchema: toolOutputSchema,
750
+ annotations: {
751
+ readOnlyHint: false,
752
+ destructiveHint: false,
753
+ idempotentHint: false,
754
+ openWorldHint: false
755
+ }
756
+ },
757
+ ({ parent, groupName, groupClass, parts }) => callStudio(bridge, CliMsg.V2_CREATE_PARTS, { parent, groupName, groupClass, parts }, 12e4)
758
+ );
759
+ server.registerTool(
760
+ "studio_clone_instances",
761
+ {
762
+ title: "Clone Roblox Instances",
763
+ description: "Deep-clone instances and descendants with Instance:Clone(), optionally rename, reparent, override root properties, and create repeated spatial copies using offsets. The entire request is undoable and rolls back on failure.",
764
+ inputSchema: { requests: z.array(cloneRequestSchema).min(1).max(100) },
765
+ outputSchema: toolOutputSchema,
766
+ annotations: {
767
+ readOnlyHint: false,
768
+ destructiveHint: false,
769
+ idempotentHint: false,
770
+ openWorldHint: false
771
+ }
772
+ },
773
+ ({ requests }) => callStudio(bridge, CliMsg.V2_CLONE_INSTANCES, { requests }, 12e4)
774
+ );
775
+ server.registerTool(
776
+ "studio_group_instances",
777
+ {
778
+ title: "Group Roblox Instances",
779
+ description: "Create a Model or Folder and move up to 100 independent instances into it in one undoable operation.",
780
+ inputSchema: {
781
+ targets: z.array(instanceRefSchema).min(1).max(100),
782
+ name: z.string().min(1).max(100),
783
+ className: z.enum(["Model", "Folder"]).optional().default("Model"),
784
+ parent: instanceRefSchema.optional()
785
+ },
786
+ outputSchema: toolOutputSchema,
787
+ annotations: {
788
+ readOnlyHint: false,
789
+ destructiveHint: false,
790
+ idempotentHint: false,
791
+ openWorldHint: false
792
+ }
793
+ },
794
+ ({ targets, name, className, parent }) => callStudio(bridge, CliMsg.V2_GROUP_INSTANCES, { targets, name, className, parent }, 6e4)
795
+ );
796
+ server.registerTool(
797
+ "studio_ungroup_instances",
798
+ {
799
+ title: "Ungroup Roblox Instances",
800
+ description: "Move every child out of up to 20 Models or Folders, then remove the empty containers in one undoable operation.",
801
+ inputSchema: {
802
+ targets: z.array(instanceRefSchema).min(1).max(20),
803
+ parent: instanceRefSchema.optional(),
804
+ confirm: z.literal(true)
805
+ },
806
+ outputSchema: toolOutputSchema,
807
+ annotations: {
808
+ readOnlyHint: false,
809
+ destructiveHint: true,
810
+ idempotentHint: false,
811
+ openWorldHint: false
812
+ }
813
+ },
814
+ ({ targets, parent, confirm }) => callStudio(bridge, CliMsg.V2_UNGROUP_INSTANCES, { targets, parent, confirm }, 6e4)
815
+ );
816
+ server.registerTool(
817
+ "studio_transform_instances",
818
+ {
819
+ title: "Transform Roblox Models and Parts",
820
+ description: "Move or rotate up to 100 Models and BaseParts through PVInstance:PivotTo. Supports exact absolute CFrames and world/local relative offsets without breaking model layouts.",
821
+ inputSchema: { operations: z.array(transformOperationSchema).min(1).max(100) },
822
+ outputSchema: toolOutputSchema,
823
+ annotations: {
824
+ readOnlyHint: false,
825
+ destructiveHint: false,
826
+ idempotentHint: false,
827
+ openWorldHint: false
828
+ }
829
+ },
830
+ ({ operations }) => callStudio(bridge, CliMsg.V2_TRANSFORM_INSTANCES, { operations }, 6e4)
831
+ );
832
+ server.registerTool(
833
+ "studio_create_welds",
834
+ {
835
+ title: "Create Roblox Welds",
836
+ description: "Create up to 200 WeldConstraints between explicit BasePart refs in one undoable operation.",
837
+ inputSchema: {
838
+ welds: z.array(
839
+ z.object({
840
+ part0: instanceRefSchema,
841
+ part1: instanceRefSchema,
842
+ name: z.string().min(1).max(100).optional(),
843
+ parent: instanceRefSchema.optional()
844
+ })
845
+ ).min(1).max(200)
846
+ },
847
+ outputSchema: toolOutputSchema,
848
+ annotations: {
849
+ readOnlyHint: false,
850
+ destructiveHint: false,
851
+ idempotentHint: false,
852
+ openWorldHint: false
853
+ }
854
+ },
855
+ ({ welds }) => callStudio(bridge, CliMsg.V2_CREATE_WELDS, { welds }, 6e4)
856
+ );
857
+ server.registerTool(
858
+ "studio_query_parts",
859
+ {
860
+ title: "Query Parts in 3D Space",
861
+ description: "Find BaseParts intersecting an oriented box or radius using WorldRoot spatial queries. Returns stable refs, positions, and sizes for inspection or later mutations.",
862
+ inputSchema: {
863
+ root: instanceRefSchema.optional(),
864
+ mode: z.enum(["box", "radius"]).optional().default("box"),
865
+ position: vector3Schema,
866
+ orientation: vector3Schema.optional(),
867
+ size: vector3Schema.optional(),
868
+ radius: z.number().finite().positive().optional(),
869
+ maxParts: z.number().int().min(1).max(500).optional().default(200),
870
+ filter: z.array(instanceRefSchema).max(100).optional(),
871
+ filterType: z.enum(["Include", "Exclude"]).optional().default("Exclude"),
872
+ respectCanCollide: z.boolean().optional().default(false)
873
+ },
874
+ outputSchema: toolOutputSchema,
875
+ annotations: {
876
+ readOnlyHint: true,
877
+ destructiveHint: false,
878
+ idempotentHint: true,
879
+ openWorldHint: false
880
+ }
881
+ },
882
+ ({
883
+ root,
884
+ mode,
885
+ position,
886
+ orientation,
887
+ size,
888
+ radius,
889
+ maxParts,
890
+ filter,
891
+ filterType,
892
+ respectCanCollide
893
+ }) => callStudio(bridge, CliMsg.V2_QUERY_PARTS, {
894
+ root,
895
+ mode,
896
+ position,
897
+ orientation,
898
+ size,
899
+ radius,
900
+ maxParts,
901
+ filter,
902
+ filterType,
903
+ respectCanCollide
904
+ })
905
+ );
906
+ server.registerTool(
907
+ "studio_set_selection",
908
+ {
909
+ title: "Set Roblox Studio Selection",
910
+ description: "Replace the current Studio selection with up to 200 stable instance refs. Pass an empty array to clear selection.",
911
+ inputSchema: { targets: z.array(instanceRefSchema).max(200) },
912
+ outputSchema: toolOutputSchema,
913
+ annotations: {
914
+ readOnlyHint: false,
915
+ destructiveHint: false,
916
+ idempotentHint: true,
917
+ openWorldHint: false
918
+ }
919
+ },
920
+ ({ targets }) => callStudio(bridge, CliMsg.V2_SET_SELECTION, { targets })
921
+ );
922
+ }
668
923
  function registerConnectionTools(server, bridge) {
669
924
  server.registerTool(
670
925
  "dominus_status",
@@ -1192,7 +1447,7 @@ function registerDocsTools(server) {
1192
1447
  }
1193
1448
  );
1194
1449
  }
1195
- var propertiesSchema = z.record(z.unknown()).refine(
1450
+ var propertiesSchema2 = z.record(z.unknown()).refine(
1196
1451
  (properties) => Object.keys(properties).length <= 100 && !Object.keys(properties).some((name) => ["Parent", "ClassName", "Source"].includes(name)),
1197
1452
  "At most 100 properties are allowed; Parent, ClassName, and Source require dedicated operations"
1198
1453
  );
@@ -1200,19 +1455,27 @@ var mutationOperationSchema = z.discriminatedUnion("op", [
1200
1455
  z.object({
1201
1456
  op: z.literal("setProperties"),
1202
1457
  target: instanceRefSchema,
1203
- properties: propertiesSchema
1458
+ properties: propertiesSchema2
1204
1459
  }),
1205
1460
  z.object({
1206
1461
  op: z.literal("create"),
1207
1462
  parent: instanceRefSchema,
1208
1463
  className: z.string().min(1).max(100),
1209
1464
  name: z.string().min(1).max(256),
1210
- properties: propertiesSchema.optional()
1465
+ properties: propertiesSchema2.optional()
1211
1466
  }),
1212
1467
  z.object({
1213
1468
  op: z.literal("move"),
1214
1469
  target: instanceRefSchema,
1215
1470
  parent: instanceRefSchema
1471
+ }),
1472
+ z.object({
1473
+ op: z.literal("clone"),
1474
+ target: instanceRefSchema,
1475
+ parent: instanceRefSchema,
1476
+ name: z.string().min(1).max(100),
1477
+ offset: z.object({ x: z.number().finite(), y: z.number().finite(), z: z.number().finite() }).optional(),
1478
+ properties: propertiesSchema2.optional()
1216
1479
  })
1217
1480
  ]);
1218
1481
  var workerProposalSchema = z.object({
@@ -1441,7 +1704,7 @@ async function runWorker(server, bridge, goal, assignment, constraints, taskType
1441
1704
  systemPrompt: [
1442
1705
  "You are a read-only Dominus worker. Inspect only the Studio evidence in the request.",
1443
1706
  "You cannot write to Studio. Return one strict JSON WorkerProposal and no Markdown.",
1444
- "Only propose setProperties, create, or move operations using exact instance refs copied from evidence.",
1707
+ "Only propose setProperties, create, clone, or move operations using exact instance refs copied from evidence.",
1445
1708
  "Never invent instanceId values. Never propose delete, script edits, or arbitrary execution."
1446
1709
  ].join(" "),
1447
1710
  messages: [
@@ -1472,6 +1735,13 @@ async function runWorker(server, bridge, goal, assignment, constraints, taskType
1472
1735
  op: "move",
1473
1736
  target: { instanceId: "uuid", pathSegments: ["..."] },
1474
1737
  parent: { instanceId: "uuid", pathSegments: ["..."] }
1738
+ },
1739
+ {
1740
+ op: "clone",
1741
+ target: { instanceId: "uuid", pathSegments: ["..."] },
1742
+ parent: { instanceId: "uuid", pathSegments: ["..."] },
1743
+ name: "CloneName",
1744
+ offset: { x: 0, y: 0, z: 0 }
1475
1745
  }
1476
1746
  ],
1477
1747
  risks: ["string"],
@@ -1556,7 +1826,7 @@ function validateWorkerResults(results) {
1556
1826
  function validateOperations(assignment, operations) {
1557
1827
  const errors = [];
1558
1828
  for (const [index, operation] of operations.entries()) {
1559
- const refs = operation.op === "create" ? [operation.parent] : operation.op === "move" ? [operation.target, operation.parent] : [operation.target];
1829
+ const refs = operation.op === "create" ? [operation.parent] : operation.op === "move" || operation.op === "clone" ? [operation.target, operation.parent] : [operation.target];
1560
1830
  for (const ref2 of refs) {
1561
1831
  if (!isKnownParallelRef(ref2, assignment.knownRefKeys)) {
1562
1832
  errors.push(`Operation ${index} references an instance absent from worker evidence`);
@@ -1702,12 +1972,14 @@ async function verifyPlan(bridge, rootRef, originalRoot, operations) {
1702
1972
  );
1703
1973
  const knownPaths = collectInstancePaths(originalRoot);
1704
1974
  const expectedCreates = operations.flatMap((operation) => {
1705
- if (operation.op !== "create") return [];
1975
+ if (operation.op !== "create" && operation.op !== "clone") return [];
1706
1976
  const parentPath = resolveRefPath(operation.parent, knownPaths);
1707
1977
  return parentPath ? [pathKey([...parentPath, operation.name])] : [];
1708
1978
  });
1709
1979
  const missingCreates = expectedCreates.filter((path5) => !paths.has(path5));
1710
- const inspectOperations = operations.filter((operation) => operation.op !== "create");
1980
+ const inspectOperations = operations.filter(
1981
+ (operation) => operation.op !== "create" && operation.op !== "clone"
1982
+ );
1711
1983
  const inspectFailures = [];
1712
1984
  const propertyMismatches = [];
1713
1985
  const moveMismatches = [];
@@ -1777,6 +2049,7 @@ function findRepairOperations(operations, verification, originalRoot) {
1777
2049
  const parentPath = resolveRefPath(operation.parent, knownPaths);
1778
2050
  return Boolean(parentPath && missingCreates.has(pathKey([...parentPath, operation.name])));
1779
2051
  }
2052
+ if (operation.op === "clone") return false;
1780
2053
  if (operation.op === "setProperties") return mismatchedTargets.has(refLabel(operation.target));
1781
2054
  return mismatchedMoves.has(refLabel(operation.target));
1782
2055
  });
@@ -1846,7 +2119,7 @@ function isParallelRefWithinScopes(ref2, scopes) {
1846
2119
  return scopes.some((scope) => Boolean(ref2.instanceId && scope.instanceId === ref2.instanceId));
1847
2120
  }
1848
2121
  function operationTarget(operation) {
1849
- if (operation.op === "create") {
2122
+ if (operation.op === "create" || operation.op === "clone") {
1850
2123
  const parentPath = operation.parent.pathSegments;
1851
2124
  return {
1852
2125
  key: `${refKey(operation.parent)}/create:${operation.name}`,
@@ -1951,37 +2224,46 @@ function emit(server, type, data) {
1951
2224
  function toErrorMessage(error) {
1952
2225
  return error instanceof Error ? error.message : String(error);
1953
2226
  }
1954
- var propertiesSchema2 = z.record(z.unknown()).refine(
2227
+ var propertiesSchema3 = z.record(z.unknown()).refine(
1955
2228
  (properties) => Object.keys(properties).length <= 100 && !Object.keys(properties).some((name) => ["Parent", "ClassName", "Source"].includes(name)),
1956
2229
  "At most 100 properties are allowed; Parent, ClassName, and Source require dedicated operations"
1957
2230
  );
1958
2231
  var setPropertiesOperation = z.object({
1959
2232
  op: z.literal("setProperties"),
1960
2233
  target: instanceRefSchema,
1961
- properties: propertiesSchema2
2234
+ properties: propertiesSchema3
1962
2235
  });
1963
2236
  var createOperation = z.object({
1964
2237
  op: z.literal("create"),
1965
2238
  parent: instanceRefSchema,
1966
2239
  className: z.string().min(1).max(100),
1967
2240
  name: z.string().min(1).max(256),
1968
- properties: propertiesSchema2.optional()
2241
+ properties: propertiesSchema3.optional()
1969
2242
  });
1970
2243
  var moveOperation = z.object({
1971
2244
  op: z.literal("move"),
1972
2245
  target: instanceRefSchema,
1973
2246
  parent: instanceRefSchema
1974
2247
  });
2248
+ var cloneOperation = z.object({
2249
+ op: z.literal("clone"),
2250
+ target: instanceRefSchema,
2251
+ parent: instanceRefSchema.optional(),
2252
+ name: z.string().min(1).max(100).optional(),
2253
+ offset: z.object({ x: z.number().finite(), y: z.number().finite(), z: z.number().finite() }).optional(),
2254
+ properties: propertiesSchema3.optional()
2255
+ });
1975
2256
  var mutationOperation = z.discriminatedUnion("op", [
1976
2257
  setPropertiesOperation,
1977
2258
  createOperation,
2259
+ cloneOperation,
1978
2260
  moveOperation
1979
2261
  ]);
1980
2262
  var uiNodeSchema = z.lazy(
1981
2263
  () => z.object({
1982
2264
  ClassName: z.string().min(1).max(100),
1983
2265
  Name: z.string().min(1).max(256).optional(),
1984
- properties: propertiesSchema2.optional(),
2266
+ properties: propertiesSchema3.optional(),
1985
2267
  Children: z.array(uiNodeSchema).max(1e3).optional()
1986
2268
  })
1987
2269
  );
@@ -2085,7 +2367,7 @@ function registerStudioTools(server, bridge) {
2085
2367
  "studio_apply",
2086
2368
  {
2087
2369
  title: "Apply Atomic Studio Mutations",
2088
- description: "Atomically set properties, create instances, and move instances. Any failed operation cancels and undoes the entire batch.",
2370
+ description: "Atomically set properties, create instances, deep-clone instances, and move instances. Any failed operation cancels and undoes the entire batch.",
2089
2371
  inputSchema: { operations: z.array(mutationOperation).min(1).max(100) },
2090
2372
  outputSchema: toolOutputSchema,
2091
2373
  annotations: {
@@ -2226,7 +2508,7 @@ function registerStudioTools(server, bridge) {
2226
2508
  }
2227
2509
 
2228
2510
  // src/mcp/server.ts
2229
- var VERSION = "2.2.1";
2511
+ var VERSION = "2.3.0";
2230
2512
  function createDominusMcpServer(bridge) {
2231
2513
  const server = new McpServer(
2232
2514
  {
@@ -2239,6 +2521,7 @@ function createDominusMcpServer(bridge) {
2239
2521
  );
2240
2522
  registerConnectionTools(server, bridge);
2241
2523
  registerStudioTools(server, bridge);
2524
+ registerBuildingTools(server, bridge);
2242
2525
  registerParallelTaskTool(server, bridge);
2243
2526
  registerDocsTools(server);
2244
2527
  registerDominusResources(server, bridge);