@a-company/atelier 0.28.2 → 0.36.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
@@ -12,13 +12,15 @@ function generateId() {
12
12
  }
13
13
  var DocumentStore = class {
14
14
  docs = /* @__PURE__ */ new Map();
15
+ listeners = [];
15
16
  /** Create a new document entry. Returns the assigned ID. */
16
- create(doc, id) {
17
+ create(doc, id, source = "system") {
17
18
  const docId = id ?? generateId();
18
19
  if (this.docs.has(docId)) {
19
20
  throw new Error(`Document "${docId}" already exists`);
20
21
  }
21
22
  this.docs.set(docId, doc);
23
+ this.emit(docId, doc, source);
22
24
  return docId;
23
25
  }
24
26
  /** Get a document by ID. */
@@ -26,12 +28,15 @@ var DocumentStore = class {
26
28
  return this.docs.get(id);
27
29
  }
28
30
  /** Set (overwrite) a document by ID. */
29
- set(id, doc) {
31
+ set(id, doc, source = "system") {
30
32
  this.docs.set(id, doc);
33
+ this.emit(id, doc, source);
31
34
  }
32
35
  /** Delete a document by ID. Returns true if it existed. */
33
- delete(id) {
34
- return this.docs.delete(id);
36
+ delete(id, source = "system") {
37
+ const existed = this.docs.delete(id);
38
+ if (existed) this.emit(id, null, source);
39
+ return existed;
35
40
  }
36
41
  /** Check if a document exists. */
37
42
  has(id) {
@@ -45,14 +50,43 @@ var DocumentStore = class {
45
50
  }
46
51
  return result;
47
52
  }
48
- /** Clear all documents (useful for testing). */
53
+ /** Clear all documents (useful for testing). Does NOT fire onChange. */
49
54
  clear() {
50
55
  this.docs.clear();
51
56
  }
57
+ /**
58
+ * Subscribe to mutation events. The listener is invoked after every
59
+ * create()/set()/delete() with the changed document ID, the new value
60
+ * (or `null` when deleted), and the source tag.
61
+ *
62
+ * Returns an unsubscribe function. Multiple listeners are supported and
63
+ * delivered in registration order. A listener that throws is isolated —
64
+ * the error is swallowed and remaining listeners still fire.
65
+ *
66
+ * TODO(#10): the live WS bridge will subscribe here to forward LLM-side
67
+ * mutations into the in-browser Studio (which exposes the symmetric
68
+ * `AtelierStudio.applyMutation()` API).
69
+ */
70
+ onChange(listener) {
71
+ this.listeners.push(listener);
72
+ return () => {
73
+ const idx = this.listeners.indexOf(listener);
74
+ if (idx !== -1) this.listeners.splice(idx, 1);
75
+ };
76
+ }
77
+ emit(id, doc, source) {
78
+ const snapshot = this.listeners.slice();
79
+ for (const listener of snapshot) {
80
+ try {
81
+ listener(id, doc, source);
82
+ } catch {
83
+ }
84
+ }
85
+ }
52
86
  };
53
87
 
54
88
  // ../mcp/src/tools/document.ts
55
- import { z as z15 } from "zod";
89
+ import { z as z16 } from "zod";
56
90
 
57
91
  // ../schema/dist/index.js
58
92
  import { z } from "zod";
@@ -69,6 +103,7 @@ import { z as z11 } from "zod";
69
103
  import { z as z12 } from "zod";
70
104
  import { z as z13 } from "zod";
71
105
  import { z as z14 } from "zod";
106
+ import { z as z15 } from "zod";
72
107
  import { parse as yamlParse, stringify as yamlStringify } from "yaml";
73
108
  var PixelSchema = z.number();
74
109
  var PercentageSchema = z.string().regex(/^-?\d+(\.\d+)?%$/, {
@@ -310,6 +345,18 @@ var ImageVisualSchema = z8.object({
310
345
  spritesheet: SpritesheetConfigSchema.optional(),
311
346
  frameIndex: z8.number().int().min(0).optional()
312
347
  });
348
+ var VideoVisualSchema = z8.object({
349
+ type: z8.literal("video"),
350
+ assetId: z8.string().min(1, "assetId is required"),
351
+ src: z8.string().optional(),
352
+ startFrame: z8.number().int().min(0).optional(),
353
+ sourceOffset: z8.number().min(0).optional(),
354
+ sourceEnd: z8.number().positive().optional(),
355
+ playbackRate: z8.number().positive().optional(),
356
+ volume: z8.number().min(0).max(1).optional(),
357
+ muted: z8.boolean().optional(),
358
+ objectFit: z8.enum(["contain", "cover", "fill"]).optional()
359
+ });
313
360
  var GroupVisualSchema = z8.object({
314
361
  type: z8.literal("group")
315
362
  });
@@ -323,6 +370,7 @@ var VisualSchema = z8.discriminatedUnion("type", [
323
370
  ShapeVisualSchema,
324
371
  TextVisualSchema,
325
372
  ImageVisualSchema,
373
+ VideoVisualSchema,
326
374
  GroupVisualSchema,
327
375
  RefVisualSchema
328
376
  ]);
@@ -442,7 +490,7 @@ var VariableSchema = z12.object({
442
490
  default: z12.unknown().optional(),
443
491
  description: z12.string().optional()
444
492
  });
445
- var AssetTypeSchema = z13.enum(["image", "svg", "font", "animation", "audio"]);
493
+ var AssetTypeSchema = z13.enum(["image", "svg", "font", "animation", "audio", "video"]);
446
494
  var AssetSchema = z13.object({
447
495
  type: AssetTypeSchema,
448
496
  src: z13.string().min(1, "Asset src is required"),
@@ -453,6 +501,12 @@ var AssetSchema = z13.object({
453
501
  frameCount: z13.number().int().positive().optional(),
454
502
  frameWidth: z13.number().positive(),
455
503
  frameHeight: z13.number().positive()
504
+ }).optional(),
505
+ videoMeta: z13.object({
506
+ duration: z13.number().positive("videoMeta.duration must be positive"),
507
+ fps: z13.number().positive("videoMeta.fps must be positive"),
508
+ width: z13.number().int().positive(),
509
+ height: z13.number().int().positive()
456
510
  }).optional()
457
511
  });
458
512
  var CanvasSchema = z14.object({
@@ -473,6 +527,121 @@ var AtelierDocumentSchema = z14.object({
473
527
  layers: z14.array(LayerSchema),
474
528
  states: z14.record(z14.string(), StateSchema)
475
529
  });
530
+ var SilencePolicySchema = z15.object({
531
+ noise: z15.string().optional(),
532
+ min_silence: z15.number().nonnegative().optional(),
533
+ default_padding_pre: z15.number().nonnegative().optional(),
534
+ default_padding_post: z15.number().nonnegative().optional(),
535
+ match_tolerance: z15.number().nonnegative().optional()
536
+ }).strict();
537
+ var CaptionStyleSchema = z15.object({
538
+ font_family: z15.string().optional(),
539
+ font_size: z15.number().positive().optional(),
540
+ font_weight: z15.union([z15.literal("normal"), z15.literal("bold"), z15.number()]).optional(),
541
+ text_align: z15.enum(["left", "center", "right"]).optional(),
542
+ color: z15.string().optional(),
543
+ y_ratio: z15.number().min(0).max(1).optional(),
544
+ width_ratio: z15.number().min(0).max(1).optional(),
545
+ fade_seconds: z15.number().nonnegative().optional()
546
+ }).strict();
547
+ var CaptionGroupingSchema = z15.object({
548
+ max_words: z15.number().int().positive().optional(),
549
+ pause_gap: z15.number().nonnegative().optional()
550
+ }).strict();
551
+ var OverlayAnchorSchema = z15.enum([
552
+ "top-left",
553
+ "top-right",
554
+ "bottom-left",
555
+ "bottom-right"
556
+ ]);
557
+ var OverlayTextStyleSchema = z15.object({
558
+ font_family: z15.string().optional(),
559
+ font_size: z15.number().positive().optional(),
560
+ font_weight: z15.union([z15.literal("normal"), z15.literal("bold"), z15.number()]).optional(),
561
+ color: z15.string().optional()
562
+ }).strict();
563
+ function validatePageNumberFormat(format, ctx) {
564
+ if (format.length === 0) {
565
+ ctx.addIssue({
566
+ code: z15.ZodIssueCode.custom,
567
+ message: "format must be a non-empty string"
568
+ });
569
+ return;
570
+ }
571
+ const open = (format.match(/\{/g) ?? []).length;
572
+ const close = (format.match(/\}/g) ?? []).length;
573
+ if (open !== close) {
574
+ ctx.addIssue({
575
+ code: z15.ZodIssueCode.custom,
576
+ message: `format has unbalanced braces (${open} '{' vs ${close} '}')`
577
+ });
578
+ return;
579
+ }
580
+ const groupRe = /\{([^{}]*)\}/g;
581
+ const groupRule = /^(current|total)(:0\d+d)?$/;
582
+ let m;
583
+ let sawCurrent = false;
584
+ let sawTotal = false;
585
+ while ((m = groupRe.exec(format)) !== null) {
586
+ const inner = m[1];
587
+ if (!groupRule.test(inner)) {
588
+ ctx.addIssue({
589
+ code: z15.ZodIssueCode.custom,
590
+ message: `format placeholder "{${inner}}" is not recognized \u2014 expected {current}, {total}, {current:0Nd}, or {total:0Nd}`
591
+ });
592
+ return;
593
+ }
594
+ if (inner.startsWith("current")) sawCurrent = true;
595
+ if (inner.startsWith("total")) sawTotal = true;
596
+ }
597
+ if (!sawCurrent && !sawTotal) {
598
+ ctx.addIssue({
599
+ code: z15.ZodIssueCode.custom,
600
+ message: "format must contain at least one {current} or {total} placeholder"
601
+ });
602
+ }
603
+ }
604
+ var OverlayHandleRuleSchema = z15.object({
605
+ text: z15.string().min(1),
606
+ anchor: OverlayAnchorSchema,
607
+ margin: z15.number().nonnegative().optional(),
608
+ style: OverlayTextStyleSchema.optional()
609
+ }).strict();
610
+ var OverlayPageNumberRuleSchema = z15.object({
611
+ format: z15.string().superRefine(validatePageNumberFormat),
612
+ anchor: OverlayAnchorSchema,
613
+ margin: z15.number().nonnegative().optional(),
614
+ style: OverlayTextStyleSchema.optional()
615
+ }).strict();
616
+ var OverlayRulesSchema = z15.object({
617
+ handle: OverlayHandleRuleSchema.optional(),
618
+ page_number: OverlayPageNumberRuleSchema.optional()
619
+ }).strict();
620
+ var StudioRecipeSchema = z15.object({
621
+ version: z15.string(),
622
+ name: z15.string(),
623
+ description: z15.string().optional(),
624
+ author: z15.string().optional(),
625
+ tags: z15.array(z15.string()).optional(),
626
+ silence_policy: SilencePolicySchema.optional(),
627
+ caption_style: CaptionStyleSchema.optional(),
628
+ caption_grouping: CaptionGroupingSchema.optional(),
629
+ // Phase 1.5 — first-class overlay rules
630
+ overlay_rules: OverlayRulesSchema.optional(),
631
+ // Reserved — Phase 3 (parse-opaque)
632
+ caption_highlight: z15.unknown().optional(),
633
+ transition_kit: z15.unknown().optional(),
634
+ palette: z15.unknown().optional(),
635
+ audio_policy: z15.unknown().optional(),
636
+ aspect_targets: z15.array(z15.unknown()).optional()
637
+ }).strict();
638
+ var RESERVED_RECIPE_FIELDS = [
639
+ "caption_highlight",
640
+ "transition_kit",
641
+ "palette",
642
+ "audio_policy",
643
+ "aspect_targets"
644
+ ];
476
645
  function formatErrors(error) {
477
646
  return error.issues.map((issue) => ({
478
647
  path: issue.path.join(".") || "(root)",
@@ -486,14 +655,30 @@ function validateDocument(input) {
486
655
  }
487
656
  return { success: false, errors: formatErrors(result.error) };
488
657
  }
658
+ function validateRecipe(recipe) {
659
+ const parsed = StudioRecipeSchema.safeParse(recipe);
660
+ if (!parsed.success) {
661
+ return { success: false, errors: formatErrors(parsed.error) };
662
+ }
663
+ const warnings = [];
664
+ const data = parsed.data;
665
+ for (const field of RESERVED_RECIPE_FIELDS) {
666
+ if (data[field] !== void 0) {
667
+ warnings.push(
668
+ `${field} is reserved for Phase 3 and currently has no effect.`
669
+ );
670
+ }
671
+ }
672
+ return { success: true, data, ...warnings.length > 0 && { warnings } };
673
+ }
489
674
  function parseAtelier(yamlString) {
490
675
  let parsed;
491
676
  try {
492
677
  parsed = yamlParse(yamlString);
493
- } catch (err17) {
678
+ } catch (err20) {
494
679
  return {
495
680
  success: false,
496
- errors: [{ path: "(yaml)", message: `YAML parse error: ${err17.message}` }]
681
+ errors: [{ path: "(yaml)", message: `YAML parse error: ${err20.message}` }]
497
682
  };
498
683
  }
499
684
  return validateDocument(parsed);
@@ -519,13 +704,13 @@ function register(server, store) {
519
704
  "atelier_create",
520
705
  "Create a new Atelier animation document with canvas settings",
521
706
  {
522
- name: z15.string().describe("Animation name"),
523
- width: z15.number().positive().describe("Canvas width in pixels"),
524
- height: z15.number().positive().describe("Canvas height in pixels"),
525
- fps: z15.number().positive().int().describe("Frames per second"),
526
- background: z15.string().optional().describe("Background color (hex string)"),
527
- description: z15.string().optional().describe("Animation description"),
528
- tags: z15.array(z15.string()).optional().describe("Tags for categorization")
707
+ name: z16.string().describe("Animation name"),
708
+ width: z16.number().positive().describe("Canvas width in pixels"),
709
+ height: z16.number().positive().describe("Canvas height in pixels"),
710
+ fps: z16.number().positive().int().describe("Frames per second"),
711
+ background: z16.string().optional().describe("Background color (hex string)"),
712
+ description: z16.string().optional().describe("Animation description"),
713
+ tags: z16.array(z16.string()).optional().describe("Tags for categorization")
529
714
  },
530
715
  { readOnlyHint: false, destructiveHint: false },
531
716
  async ({ name, width, height, fps, background, description, tags }) => {
@@ -550,7 +735,7 @@ function register(server, store) {
550
735
  "atelier_info",
551
736
  "Get summary information about an Atelier document",
552
737
  {
553
- id: z15.string().describe("Document ID")
738
+ id: z16.string().describe("Document ID")
554
739
  },
555
740
  { readOnlyHint: true, destructiveHint: false },
556
741
  async ({ id }) => {
@@ -578,8 +763,8 @@ function register(server, store) {
578
763
  "atelier_load",
579
764
  "Load an Atelier document from a YAML string",
580
765
  {
581
- id: z15.string().optional().describe("Custom document ID (auto-generated if omitted)"),
582
- yaml: z15.string().describe("YAML string representing an Atelier document")
766
+ id: z16.string().optional().describe("Custom document ID (auto-generated if omitted)"),
767
+ yaml: z16.string().describe("YAML string representing an Atelier document")
583
768
  },
584
769
  { readOnlyHint: false, destructiveHint: false },
585
770
  async ({ id, yaml }) => {
@@ -595,7 +780,7 @@ function register(server, store) {
595
780
  "atelier_export",
596
781
  "Export an Atelier document as a YAML string",
597
782
  {
598
- id: z15.string().describe("Document ID")
783
+ id: z16.string().describe("Document ID")
599
784
  },
600
785
  { readOnlyHint: true, destructiveHint: false },
601
786
  async ({ id }) => {
@@ -618,7 +803,7 @@ function register(server, store) {
618
803
  }
619
804
 
620
805
  // ../mcp/src/tools/layers.ts
621
- import { z as z16 } from "zod";
806
+ import { z as z17 } from "zod";
622
807
  function getDoc2(store, id) {
623
808
  const doc = store.get(id);
624
809
  if (!doc) return { error: `Document "${id}" not found` };
@@ -630,69 +815,69 @@ function ok2(data) {
630
815
  function err2(message) {
631
816
  return { content: [{ type: "text", text: JSON.stringify({ error: message }) }], isError: true };
632
817
  }
633
- var VisualInputSchema = z16.object({
634
- type: z16.enum(["shape", "text", "image", "group", "ref"]).describe("Visual type"),
818
+ var VisualInputSchema = z17.object({
819
+ type: z17.enum(["shape", "text", "image", "group", "ref"]).describe("Visual type"),
635
820
  // shape visual fields
636
- shape: z16.object({
637
- type: z16.enum(["rect", "ellipse", "path"]),
638
- cornerRadius: z16.union([z16.number(), z16.tuple([z16.number(), z16.number(), z16.number(), z16.number()])]).optional(),
639
- points: z16.array(z16.object({
640
- x: z16.number(),
641
- y: z16.number(),
642
- in: z16.object({ x: z16.number(), y: z16.number() }).optional(),
643
- out: z16.object({ x: z16.number(), y: z16.number() }).optional()
821
+ shape: z17.object({
822
+ type: z17.enum(["rect", "ellipse", "path"]),
823
+ cornerRadius: z17.union([z17.number(), z17.tuple([z17.number(), z17.number(), z17.number(), z17.number()])]).optional(),
824
+ points: z17.array(z17.object({
825
+ x: z17.number(),
826
+ y: z17.number(),
827
+ in: z17.object({ x: z17.number(), y: z17.number() }).optional(),
828
+ out: z17.object({ x: z17.number(), y: z17.number() }).optional()
644
829
  })).optional(),
645
- closed: z16.boolean().optional()
830
+ closed: z17.boolean().optional()
646
831
  }).optional(),
647
- fill: z16.record(z16.unknown()).optional(),
648
- stroke: z16.record(z16.unknown()).optional(),
832
+ fill: z17.record(z17.unknown()).optional(),
833
+ stroke: z17.record(z17.unknown()).optional(),
649
834
  // text visual fields
650
- content: z16.string().optional(),
651
- style: z16.record(z16.unknown()).optional(),
835
+ content: z17.string().optional(),
836
+ style: z17.record(z17.unknown()).optional(),
652
837
  // image visual fields
653
- assetId: z16.string().optional(),
654
- sourceRect: z16.object({
655
- x: z16.number(),
656
- y: z16.number(),
657
- width: z16.number().positive(),
658
- height: z16.number().positive()
838
+ assetId: z17.string().optional(),
839
+ sourceRect: z17.object({
840
+ x: z17.number(),
841
+ y: z17.number(),
842
+ width: z17.number().positive(),
843
+ height: z17.number().positive()
659
844
  }).optional(),
660
- spritesheet: z16.object({
661
- columns: z16.number().int().positive(),
662
- rows: z16.number().int().positive(),
663
- frameCount: z16.number().int().positive().optional(),
664
- frameWidth: z16.number().positive(),
665
- frameHeight: z16.number().positive()
845
+ spritesheet: z17.object({
846
+ columns: z17.number().int().positive(),
847
+ rows: z17.number().int().positive(),
848
+ frameCount: z17.number().int().positive().optional(),
849
+ frameWidth: z17.number().positive(),
850
+ frameHeight: z17.number().positive()
666
851
  }).optional(),
667
- frameIndex: z16.number().int().min(0).optional(),
852
+ frameIndex: z17.number().int().min(0).optional(),
668
853
  // ref visual fields
669
- src: z16.string().optional(),
670
- state: z16.string().optional(),
671
- frame: z16.number().int().min(0).optional()
854
+ src: z17.string().optional(),
855
+ state: z17.string().optional(),
856
+ frame: z17.number().int().min(0).optional()
672
857
  }).describe("Visual content definition");
673
858
  function register2(server, store) {
674
859
  server.tool(
675
860
  "atelier_add_layer",
676
861
  "Add a new layer to an Atelier document",
677
862
  {
678
- id: z16.string().describe("Document ID"),
679
- layerId: z16.string().describe("Unique layer ID"),
863
+ id: z17.string().describe("Document ID"),
864
+ layerId: z17.string().describe("Unique layer ID"),
680
865
  visual: VisualInputSchema.describe("Visual content definition"),
681
- x: z16.union([z16.number(), z16.string()]).describe("X position (pixels or percentage)"),
682
- y: z16.union([z16.number(), z16.string()]).describe("Y position (pixels or percentage)"),
683
- width: z16.union([z16.number(), z16.string()]).describe("Width (pixels or percentage)"),
684
- height: z16.union([z16.number(), z16.string()]).describe("Height (pixels or percentage)"),
685
- description: z16.string().optional().describe("Layer description"),
686
- tags: z16.array(z16.string()).optional().describe("Tags"),
687
- opacity: z16.number().min(0).max(1).optional().describe("Opacity 0-1"),
688
- rotation: z16.number().optional().describe("Rotation in degrees"),
689
- parentId: z16.string().optional().describe("Parent layer ID for transform inheritance"),
690
- anchorPoint: z16.object({ x: z16.number(), y: z16.number() }).optional().describe("Anchor point (0-1 normalized)"),
691
- scale: z16.object({ x: z16.number(), y: z16.number() }).optional().describe("Scale factors"),
692
- visible: z16.boolean().optional().describe("Whether layer is visible"),
693
- tint: z16.object({
694
- color: z16.string().describe("Tint color (hex string)"),
695
- amount: z16.number().min(0).max(1).describe("Tint amount (0 = none, 1 = full)")
866
+ x: z17.union([z17.number(), z17.string()]).describe("X position (pixels or percentage)"),
867
+ y: z17.union([z17.number(), z17.string()]).describe("Y position (pixels or percentage)"),
868
+ width: z17.union([z17.number(), z17.string()]).describe("Width (pixels or percentage)"),
869
+ height: z17.union([z17.number(), z17.string()]).describe("Height (pixels or percentage)"),
870
+ description: z17.string().optional().describe("Layer description"),
871
+ tags: z17.array(z17.string()).optional().describe("Tags"),
872
+ opacity: z17.number().min(0).max(1).optional().describe("Opacity 0-1"),
873
+ rotation: z17.number().optional().describe("Rotation in degrees"),
874
+ parentId: z17.string().optional().describe("Parent layer ID for transform inheritance"),
875
+ anchorPoint: z17.object({ x: z17.number(), y: z17.number() }).optional().describe("Anchor point (0-1 normalized)"),
876
+ scale: z17.object({ x: z17.number(), y: z17.number() }).optional().describe("Scale factors"),
877
+ visible: z17.boolean().optional().describe("Whether layer is visible"),
878
+ tint: z17.object({
879
+ color: z17.string().describe("Tint color (hex string)"),
880
+ amount: z17.number().min(0).max(1).describe("Tint amount (0 = none, 1 = full)")
696
881
  }).optional().describe("Color tint overlay")
697
882
  },
698
883
  { readOnlyHint: false, destructiveHint: false },
@@ -729,20 +914,20 @@ function register2(server, store) {
729
914
  "atelier_edit_layer",
730
915
  "Edit properties of an existing layer",
731
916
  {
732
- id: z16.string().describe("Document ID"),
733
- layerId: z16.string().describe("Layer ID to edit"),
734
- x: z16.union([z16.number(), z16.string()]).optional().describe("New X position"),
735
- y: z16.union([z16.number(), z16.string()]).optional().describe("New Y position"),
736
- width: z16.union([z16.number(), z16.string()]).optional().describe("New width"),
737
- height: z16.union([z16.number(), z16.string()]).optional().describe("New height"),
738
- description: z16.string().optional().describe("New description"),
739
- tags: z16.array(z16.string()).optional().describe("New tags"),
740
- opacity: z16.number().min(0).max(1).optional().describe("New opacity"),
741
- rotation: z16.number().optional().describe("New rotation"),
742
- parentId: z16.string().nullable().optional().describe("New parent layer ID (null to clear)"),
743
- anchorPoint: z16.object({ x: z16.number(), y: z16.number() }).optional().describe("New anchor point"),
744
- scale: z16.object({ x: z16.number(), y: z16.number() }).optional().describe("New scale"),
745
- visible: z16.boolean().optional().describe("New visibility")
917
+ id: z17.string().describe("Document ID"),
918
+ layerId: z17.string().describe("Layer ID to edit"),
919
+ x: z17.union([z17.number(), z17.string()]).optional().describe("New X position"),
920
+ y: z17.union([z17.number(), z17.string()]).optional().describe("New Y position"),
921
+ width: z17.union([z17.number(), z17.string()]).optional().describe("New width"),
922
+ height: z17.union([z17.number(), z17.string()]).optional().describe("New height"),
923
+ description: z17.string().optional().describe("New description"),
924
+ tags: z17.array(z17.string()).optional().describe("New tags"),
925
+ opacity: z17.number().min(0).max(1).optional().describe("New opacity"),
926
+ rotation: z17.number().optional().describe("New rotation"),
927
+ parentId: z17.string().nullable().optional().describe("New parent layer ID (null to clear)"),
928
+ anchorPoint: z17.object({ x: z17.number(), y: z17.number() }).optional().describe("New anchor point"),
929
+ scale: z17.object({ x: z17.number(), y: z17.number() }).optional().describe("New scale"),
930
+ visible: z17.boolean().optional().describe("New visibility")
746
931
  },
747
932
  { readOnlyHint: false, destructiveHint: false },
748
933
  async ({ id, layerId, x, y, width, height, description, tags, opacity, rotation, parentId, anchorPoint, scale, visible }) => {
@@ -777,8 +962,8 @@ function register2(server, store) {
777
962
  "atelier_remove_layer",
778
963
  "Remove a layer from an Atelier document",
779
964
  {
780
- id: z16.string().describe("Document ID"),
781
- layerId: z16.string().describe("Layer ID to remove")
965
+ id: z17.string().describe("Document ID"),
966
+ layerId: z17.string().describe("Layer ID to remove")
782
967
  },
783
968
  { readOnlyHint: false, destructiveHint: true },
784
969
  async ({ id, layerId }) => {
@@ -806,7 +991,7 @@ function register2(server, store) {
806
991
  "atelier_list_layers",
807
992
  "List all layers in an Atelier document",
808
993
  {
809
- id: z16.string().describe("Document ID")
994
+ id: z17.string().describe("Document ID")
810
995
  },
811
996
  { readOnlyHint: true, destructiveHint: false },
812
997
  async ({ id }) => {
@@ -833,9 +1018,9 @@ function register2(server, store) {
833
1018
  "atelier_reorder",
834
1019
  "Move a layer to a new position in the layer stack",
835
1020
  {
836
- id: z16.string().describe("Document ID"),
837
- layerId: z16.string().describe("Layer ID to move"),
838
- position: z16.number().int().min(0).describe("Target position index (0-based)")
1021
+ id: z17.string().describe("Document ID"),
1022
+ layerId: z17.string().describe("Layer ID to move"),
1023
+ position: z17.number().int().min(0).describe("Target position index (0-based)")
839
1024
  },
840
1025
  { readOnlyHint: false, destructiveHint: false },
841
1026
  async ({ id, layerId, position }) => {
@@ -853,7 +1038,7 @@ function register2(server, store) {
853
1038
  }
854
1039
 
855
1040
  // ../mcp/src/tools/shapes.ts
856
- import { z as z17 } from "zod";
1041
+ import { z as z18 } from "zod";
857
1042
  function getDoc3(store, id) {
858
1043
  const doc = store.get(id);
859
1044
  if (!doc) return { error: `Document "${id}" not found` };
@@ -870,21 +1055,21 @@ function register3(server, store) {
870
1055
  "atelier_set_shape",
871
1056
  "Set the shape on a shape-type layer",
872
1057
  {
873
- id: z17.string().describe("Document ID"),
874
- layerId: z17.string().describe("Layer ID (must be a shape visual)"),
875
- shape: z17.object({
876
- type: z17.enum(["rect", "ellipse", "path"]).describe("Shape type"),
877
- cornerRadius: z17.union([
878
- z17.number(),
879
- z17.tuple([z17.number(), z17.number(), z17.number(), z17.number()])
1058
+ id: z18.string().describe("Document ID"),
1059
+ layerId: z18.string().describe("Layer ID (must be a shape visual)"),
1060
+ shape: z18.object({
1061
+ type: z18.enum(["rect", "ellipse", "path"]).describe("Shape type"),
1062
+ cornerRadius: z18.union([
1063
+ z18.number(),
1064
+ z18.tuple([z18.number(), z18.number(), z18.number(), z18.number()])
880
1065
  ]).optional().describe("Corner radius (rect only)"),
881
- points: z17.array(z17.object({
882
- x: z17.number(),
883
- y: z17.number(),
884
- in: z17.object({ x: z17.number(), y: z17.number() }).optional(),
885
- out: z17.object({ x: z17.number(), y: z17.number() }).optional()
1066
+ points: z18.array(z18.object({
1067
+ x: z18.number(),
1068
+ y: z18.number(),
1069
+ in: z18.object({ x: z18.number(), y: z18.number() }).optional(),
1070
+ out: z18.object({ x: z18.number(), y: z18.number() }).optional()
886
1071
  })).optional().describe("Path points (path only)"),
887
- closed: z17.boolean().optional().describe("Whether path is closed (path only)")
1072
+ closed: z18.boolean().optional().describe("Whether path is closed (path only)")
888
1073
  }).describe("Shape definition")
889
1074
  },
890
1075
  { readOnlyHint: false, destructiveHint: false },
@@ -903,20 +1088,20 @@ function register3(server, store) {
903
1088
  "atelier_set_fill",
904
1089
  "Set the fill on a shape-type layer",
905
1090
  {
906
- id: z17.string().describe("Document ID"),
907
- layerId: z17.string().describe("Layer ID (must be a shape visual)"),
908
- fill: z17.object({
909
- type: z17.enum(["solid", "linear-gradient", "radial-gradient"]).describe("Fill type"),
910
- color: z17.unknown().optional().describe("Color for solid fill (hex string or RGBA/HSLA object)"),
911
- angle: z17.number().optional().describe("Angle in degrees (linear-gradient only)"),
912
- center: z17.object({
913
- x: z17.union([z17.number(), z17.string()]),
914
- y: z17.union([z17.number(), z17.string()])
1091
+ id: z18.string().describe("Document ID"),
1092
+ layerId: z18.string().describe("Layer ID (must be a shape visual)"),
1093
+ fill: z18.object({
1094
+ type: z18.enum(["solid", "linear-gradient", "radial-gradient"]).describe("Fill type"),
1095
+ color: z18.unknown().optional().describe("Color for solid fill (hex string or RGBA/HSLA object)"),
1096
+ angle: z18.number().optional().describe("Angle in degrees (linear-gradient only)"),
1097
+ center: z18.object({
1098
+ x: z18.union([z18.number(), z18.string()]),
1099
+ y: z18.union([z18.number(), z18.string()])
915
1100
  }).optional().describe("Center point (radial-gradient only)"),
916
- radius: z17.union([z17.number(), z17.string()]).optional().describe("Radius (radial-gradient only)"),
917
- stops: z17.array(z17.object({
918
- offset: z17.number().min(0).max(1),
919
- color: z17.unknown()
1101
+ radius: z18.union([z18.number(), z18.string()]).optional().describe("Radius (radial-gradient only)"),
1102
+ stops: z18.array(z18.object({
1103
+ offset: z18.number().min(0).max(1),
1104
+ color: z18.unknown()
920
1105
  })).optional().describe("Gradient stops")
921
1106
  }).describe("Fill definition")
922
1107
  },
@@ -936,14 +1121,14 @@ function register3(server, store) {
936
1121
  "atelier_set_stroke",
937
1122
  "Set the stroke on a shape-type layer",
938
1123
  {
939
- id: z17.string().describe("Document ID"),
940
- layerId: z17.string().describe("Layer ID (must be a shape visual)"),
941
- stroke: z17.object({
942
- color: z17.unknown().describe("Stroke color (hex string or RGBA/HSLA object)"),
943
- width: z17.number().positive().describe("Stroke width in pixels"),
944
- dash: z17.array(z17.number()).optional().describe("Dash pattern"),
945
- lineCap: z17.enum(["butt", "round", "square"]).optional().describe("Line cap style"),
946
- lineJoin: z17.enum(["miter", "round", "bevel"]).optional().describe("Line join style")
1124
+ id: z18.string().describe("Document ID"),
1125
+ layerId: z18.string().describe("Layer ID (must be a shape visual)"),
1126
+ stroke: z18.object({
1127
+ color: z18.unknown().describe("Stroke color (hex string or RGBA/HSLA object)"),
1128
+ width: z18.number().positive().describe("Stroke width in pixels"),
1129
+ dash: z18.array(z18.number()).optional().describe("Dash pattern"),
1130
+ lineCap: z18.enum(["butt", "round", "square"]).optional().describe("Line cap style"),
1131
+ lineJoin: z18.enum(["miter", "round", "bevel"]).optional().describe("Line join style")
947
1132
  }).describe("Stroke definition")
948
1133
  },
949
1134
  { readOnlyHint: false, destructiveHint: false },
@@ -961,7 +1146,7 @@ function register3(server, store) {
961
1146
  }
962
1147
 
963
1148
  // ../mcp/src/tools/states.ts
964
- import { z as z18 } from "zod";
1149
+ import { z as z19 } from "zod";
965
1150
  function getDoc4(store, id) {
966
1151
  const doc = store.get(id);
967
1152
  if (!doc) return { error: `Document "${id}" not found` };
@@ -978,11 +1163,11 @@ function register4(server, store) {
978
1163
  "atelier_add_state",
979
1164
  "Add a named animation state to a document",
980
1165
  {
981
- id: z18.string().describe("Document ID"),
982
- stateName: z18.string().describe("State name (e.g. 'intro', 'hover', 'exit')"),
983
- duration: z18.number().positive().int().describe("Duration in frames"),
984
- description: z18.string().optional().describe("State description"),
985
- tags: z18.array(z18.string()).optional().describe("Tags")
1166
+ id: z19.string().describe("Document ID"),
1167
+ stateName: z19.string().describe("State name (e.g. 'intro', 'hover', 'exit')"),
1168
+ duration: z19.number().positive().int().describe("Duration in frames"),
1169
+ description: z19.string().optional().describe("State description"),
1170
+ tags: z19.array(z19.string()).optional().describe("Tags")
986
1171
  },
987
1172
  { readOnlyHint: false, destructiveHint: false },
988
1173
  async ({ id, stateName, duration, description, tags }) => {
@@ -1005,11 +1190,11 @@ function register4(server, store) {
1005
1190
  "atelier_edit_state",
1006
1191
  "Edit metadata of an existing animation state",
1007
1192
  {
1008
- id: z18.string().describe("Document ID"),
1009
- stateName: z18.string().describe("State name to edit"),
1010
- duration: z18.number().positive().int().optional().describe("New duration in frames"),
1011
- description: z18.string().optional().describe("New description"),
1012
- tags: z18.array(z18.string()).optional().describe("New tags")
1193
+ id: z19.string().describe("Document ID"),
1194
+ stateName: z19.string().describe("State name to edit"),
1195
+ duration: z19.number().positive().int().optional().describe("New duration in frames"),
1196
+ description: z19.string().optional().describe("New description"),
1197
+ tags: z19.array(z19.string()).optional().describe("New tags")
1013
1198
  },
1014
1199
  { readOnlyHint: false, destructiveHint: false },
1015
1200
  async ({ id, stateName, duration, description, tags }) => {
@@ -1028,8 +1213,8 @@ function register4(server, store) {
1028
1213
  "atelier_remove_state",
1029
1214
  "Remove an animation state and all its deltas",
1030
1215
  {
1031
- id: z18.string().describe("Document ID"),
1032
- stateName: z18.string().describe("State name to remove")
1216
+ id: z19.string().describe("Document ID"),
1217
+ stateName: z19.string().describe("State name to remove")
1033
1218
  },
1034
1219
  { readOnlyHint: false, destructiveHint: true },
1035
1220
  async ({ id, stateName }) => {
@@ -1046,7 +1231,7 @@ function register4(server, store) {
1046
1231
  "atelier_list_states",
1047
1232
  "List all animation states in a document",
1048
1233
  {
1049
- id: z18.string().describe("Document ID")
1234
+ id: z19.string().describe("Document ID")
1050
1235
  },
1051
1236
  { readOnlyHint: true, destructiveHint: false },
1052
1237
  async ({ id }) => {
@@ -1066,7 +1251,7 @@ function register4(server, store) {
1066
1251
  }
1067
1252
 
1068
1253
  // ../mcp/src/tools/deltas.ts
1069
- import { z as z19 } from "zod";
1254
+ import { z as z20 } from "zod";
1070
1255
 
1071
1256
  // ../math/dist/index.js
1072
1257
  function linear(t) {
@@ -1622,7 +1807,18 @@ function resolveFrame(doc, stateName, frame, overrideDeltas) {
1622
1807
  }
1623
1808
  }
1624
1809
  }
1625
- return { id: layer.id, layer, computedProperties };
1810
+ const resolvedLayer = { id: layer.id, layer, computedProperties };
1811
+ if (layer.visual.type === "video") {
1812
+ const video = layer.visual;
1813
+ const fps = doc.canvas.fps;
1814
+ const startFrame = video.startFrame ?? 0;
1815
+ const sourceOffset = video.sourceOffset ?? 0;
1816
+ const playbackRate = video.playbackRate ?? 1;
1817
+ const relativeFrame = Math.max(0, frame - startFrame);
1818
+ const sourceTime = relativeFrame / fps * playbackRate + sourceOffset;
1819
+ resolvedLayer.videoSourceTime = video.sourceEnd !== void 0 ? Math.min(sourceTime, video.sourceEnd) : sourceTime;
1820
+ }
1821
+ return resolvedLayer;
1626
1822
  });
1627
1823
  return { frame, stateName, layers: resolvedLayers };
1628
1824
  }
@@ -1794,17 +1990,17 @@ function register5(server, store) {
1794
1990
  "atelier_add_delta",
1795
1991
  "Add an animation delta (keyframe transition) to a state",
1796
1992
  {
1797
- id: z19.string().describe("Document ID"),
1798
- stateName: z19.string().describe("State name"),
1799
- layer: z19.string().describe("Target layer ID"),
1993
+ id: z20.string().describe("Document ID"),
1994
+ stateName: z20.string().describe("State name"),
1995
+ layer: z20.string().describe("Target layer ID"),
1800
1996
  property: AnimatablePropertyEnum.describe("Property to animate"),
1801
- range: z19.tuple([z19.number().int().min(0), z19.number().int().min(0)]).describe("Frame range [start, end] inclusive"),
1802
- from: z19.unknown().describe("Starting value"),
1803
- to: z19.unknown().describe("Ending value"),
1997
+ range: z20.tuple([z20.number().int().min(0), z20.number().int().min(0)]).describe("Frame range [start, end] inclusive"),
1998
+ from: z20.unknown().describe("Starting value"),
1999
+ to: z20.unknown().describe("Ending value"),
1804
2000
  easing: EasingInputSchema.optional().describe("Easing function"),
1805
- description: z19.string().optional().describe("Delta description"),
1806
- tags: z19.array(z19.string()).optional().describe("Tags"),
1807
- deltaId: z19.string().optional().describe("Custom delta ID")
2001
+ description: z20.string().optional().describe("Delta description"),
2002
+ tags: z20.array(z20.string()).optional().describe("Tags"),
2003
+ deltaId: z20.string().optional().describe("Custom delta ID")
1808
2004
  },
1809
2005
  { readOnlyHint: false, destructiveHint: false },
1810
2006
  async ({ id, stateName, layer, property, range, from, to, easing, description, tags, deltaId }) => {
@@ -1842,17 +2038,17 @@ function register5(server, store) {
1842
2038
  "atelier_edit_delta",
1843
2039
  "Edit an existing delta by index within a state",
1844
2040
  {
1845
- id: z19.string().describe("Document ID"),
1846
- stateName: z19.string().describe("State name"),
1847
- deltaIndex: z19.number().int().min(0).describe("Delta index within the state"),
1848
- layer: z19.string().optional().describe("New target layer ID"),
2041
+ id: z20.string().describe("Document ID"),
2042
+ stateName: z20.string().describe("State name"),
2043
+ deltaIndex: z20.number().int().min(0).describe("Delta index within the state"),
2044
+ layer: z20.string().optional().describe("New target layer ID"),
1849
2045
  property: AnimatablePropertyEnum.optional().describe("New property to animate"),
1850
- range: z19.tuple([z19.number().int().min(0), z19.number().int().min(0)]).optional().describe("New frame range [start, end]"),
1851
- from: z19.unknown().optional().describe("New starting value"),
1852
- to: z19.unknown().optional().describe("New ending value"),
2046
+ range: z20.tuple([z20.number().int().min(0), z20.number().int().min(0)]).optional().describe("New frame range [start, end]"),
2047
+ from: z20.unknown().optional().describe("New starting value"),
2048
+ to: z20.unknown().optional().describe("New ending value"),
1853
2049
  easing: EasingInputSchema.optional().describe("New easing function"),
1854
- description: z19.string().optional().describe("New description"),
1855
- tags: z19.array(z19.string()).optional().describe("New tags")
2050
+ description: z20.string().optional().describe("New description"),
2051
+ tags: z20.array(z20.string()).optional().describe("New tags")
1856
2052
  },
1857
2053
  { readOnlyHint: false, destructiveHint: false },
1858
2054
  async ({ id, stateName, deltaIndex, layer, property, range, from, to, easing, description, tags }) => {
@@ -1891,9 +2087,9 @@ function register5(server, store) {
1891
2087
  "atelier_remove_delta",
1892
2088
  "Remove a delta by index from a state",
1893
2089
  {
1894
- id: z19.string().describe("Document ID"),
1895
- stateName: z19.string().describe("State name"),
1896
- deltaIndex: z19.number().int().min(0).describe("Delta index to remove")
2090
+ id: z20.string().describe("Document ID"),
2091
+ stateName: z20.string().describe("State name"),
2092
+ deltaIndex: z20.number().int().min(0).describe("Delta index to remove")
1897
2093
  },
1898
2094
  { readOnlyHint: false, destructiveHint: true },
1899
2095
  async ({ id, stateName, deltaIndex }) => {
@@ -1921,12 +2117,12 @@ function register5(server, store) {
1921
2117
  "atelier_apply_preset",
1922
2118
  "Apply a preset to a layer, expanding it into concrete deltas",
1923
2119
  {
1924
- id: z19.string().describe("Document ID"),
1925
- stateName: z19.string().describe("State name to add deltas to"),
1926
- presetName: z19.string().describe("Preset name defined in the document"),
1927
- layerId: z19.string().describe("Target layer ID"),
1928
- startFrame: z19.number().int().min(0).optional().describe("Start frame (default: 0)"),
1929
- duration: z19.number().positive().int().optional().describe("Duration for preset (default: state duration)")
2120
+ id: z20.string().describe("Document ID"),
2121
+ stateName: z20.string().describe("State name to add deltas to"),
2122
+ presetName: z20.string().describe("Preset name defined in the document"),
2123
+ layerId: z20.string().describe("Target layer ID"),
2124
+ startFrame: z20.number().int().min(0).optional().describe("Start frame (default: 0)"),
2125
+ duration: z20.number().positive().int().optional().describe("Duration for preset (default: state duration)")
1930
2126
  },
1931
2127
  { readOnlyHint: false, destructiveHint: false },
1932
2128
  async ({ id, stateName, presetName, layerId, startFrame, duration }) => {
@@ -1969,7 +2165,7 @@ ${errors.join("\n")}`);
1969
2165
  }
1970
2166
 
1971
2167
  // ../mcp/src/tools/presets.ts
1972
- import { z as z20 } from "zod";
2168
+ import { z as z21 } from "zod";
1973
2169
  function getDoc6(store, id) {
1974
2170
  const doc = store.get(id);
1975
2171
  if (!doc) return { error: `Document "${id}" not found` };
@@ -1983,11 +2179,11 @@ function err6(message) {
1983
2179
  }
1984
2180
  var AnimatablePropertyEnum2 = AnimatablePropertySchema;
1985
2181
  var EasingInputSchema2 = EasingSchema;
1986
- var PresetDeltaSchema2 = z20.object({
2182
+ var PresetDeltaSchema2 = z21.object({
1987
2183
  property: AnimatablePropertyEnum2.describe("Animatable property"),
1988
- offset: z20.tuple([z20.number().int().min(0), z20.number().int().min(0)]).optional().describe("Relative frame offset [start, end]"),
1989
- from: z20.unknown().describe("Starting value"),
1990
- to: z20.unknown().describe("Ending value"),
2184
+ offset: z21.tuple([z21.number().int().min(0), z21.number().int().min(0)]).optional().describe("Relative frame offset [start, end]"),
2185
+ from: z21.unknown().describe("Starting value"),
2186
+ to: z21.unknown().describe("Ending value"),
1991
2187
  easing: EasingInputSchema2.optional().describe("Easing function")
1992
2188
  });
1993
2189
  function register6(server, store) {
@@ -1995,11 +2191,11 @@ function register6(server, store) {
1995
2191
  "atelier_define_preset",
1996
2192
  "Define a reusable animation preset on a document",
1997
2193
  {
1998
- id: z20.string().describe("Document ID"),
1999
- presetName: z20.string().describe("Preset name"),
2000
- description: z20.string().optional().describe("Preset description"),
2001
- tags: z20.array(z20.string()).optional().describe("Tags"),
2002
- deltas: z20.array(PresetDeltaSchema2).min(1).describe("Array of preset delta definitions")
2194
+ id: z21.string().describe("Document ID"),
2195
+ presetName: z21.string().describe("Preset name"),
2196
+ description: z21.string().optional().describe("Preset description"),
2197
+ tags: z21.array(z21.string()).optional().describe("Tags"),
2198
+ deltas: z21.array(PresetDeltaSchema2).min(1).describe("Array of preset delta definitions")
2003
2199
  },
2004
2200
  { readOnlyHint: false, destructiveHint: false },
2005
2201
  async ({ id, presetName, description, tags, deltas }) => {
@@ -2020,7 +2216,7 @@ function register6(server, store) {
2020
2216
  "atelier_list_presets",
2021
2217
  "List all presets defined on a document",
2022
2218
  {
2023
- id: z20.string().describe("Document ID")
2219
+ id: z21.string().describe("Document ID")
2024
2220
  },
2025
2221
  { readOnlyHint: true, destructiveHint: false },
2026
2222
  async ({ id }) => {
@@ -2043,7 +2239,7 @@ function register6(server, store) {
2043
2239
  }
2044
2240
 
2045
2241
  // ../mcp/src/tools/preview.ts
2046
- import { z as z21 } from "zod";
2242
+ import { z as z22 } from "zod";
2047
2243
  function getDoc7(store, id) {
2048
2244
  const doc = store.get(id);
2049
2245
  if (!doc) return { error: `Document "${id}" not found` };
@@ -2060,7 +2256,7 @@ function register7(server, store) {
2060
2256
  "atelier_validate",
2061
2257
  "Validate an Atelier document for schema correctness and delta overlaps",
2062
2258
  {
2063
- id: z21.string().describe("Document ID")
2259
+ id: z22.string().describe("Document ID")
2064
2260
  },
2065
2261
  { readOnlyHint: true, destructiveHint: false },
2066
2262
  async ({ id }) => {
@@ -2089,9 +2285,9 @@ function register7(server, store) {
2089
2285
  "atelier_preview",
2090
2286
  "Preview the resolved state of all layers at a specific frame",
2091
2287
  {
2092
- id: z21.string().describe("Document ID"),
2093
- stateName: z21.string().describe("State name to preview"),
2094
- frame: z21.number().int().min(0).describe("Frame number to resolve")
2288
+ id: z22.string().describe("Document ID"),
2289
+ stateName: z22.string().describe("State name to preview"),
2290
+ frame: z22.number().int().min(0).describe("Frame number to resolve")
2095
2291
  },
2096
2292
  { readOnlyHint: true, destructiveHint: false },
2097
2293
  async ({ id, stateName, frame }) => {
@@ -2126,7 +2322,7 @@ function register7(server, store) {
2126
2322
  }
2127
2323
 
2128
2324
  // ../mcp/src/tools/templates.ts
2129
- import { z as z22 } from "zod";
2325
+ import { z as z23 } from "zod";
2130
2326
  function getDoc8(store, id) {
2131
2327
  const doc = store.get(id);
2132
2328
  if (!doc) return { error: `Document "${id}" not found` };
@@ -2143,8 +2339,8 @@ function register8(server, store) {
2143
2339
  "atelier_instantiate_template",
2144
2340
  "Instantiate a template document with variable bindings. Creates a new document in the store.",
2145
2341
  {
2146
- id: z22.string().describe("Template document ID"),
2147
- bindings: z22.record(z22.unknown()).describe("Variable bindings: { variableName: value }")
2342
+ id: z23.string().describe("Template document ID"),
2343
+ bindings: z23.record(z23.unknown()).describe("Variable bindings: { variableName: value }")
2148
2344
  },
2149
2345
  { readOnlyHint: false, destructiveHint: false },
2150
2346
  async ({ id, bindings }) => {
@@ -2167,7 +2363,7 @@ function register8(server, store) {
2167
2363
  "atelier_find_variables",
2168
2364
  "Scan a document for {{variableName}} patterns. Returns the list of variable references found.",
2169
2365
  {
2170
- id: z22.string().describe("Document ID")
2366
+ id: z23.string().describe("Document ID")
2171
2367
  },
2172
2368
  { readOnlyHint: true, destructiveHint: false },
2173
2369
  async ({ id }) => {
@@ -2187,7 +2383,7 @@ function register8(server, store) {
2187
2383
  }
2188
2384
 
2189
2385
  // ../mcp/src/tools/layer-effects.ts
2190
- import { z as z23 } from "zod";
2386
+ import { z as z24 } from "zod";
2191
2387
  function getDoc9(store, id) {
2192
2388
  const doc = store.get(id);
2193
2389
  if (!doc) return { error: `Document "${id}" not found` };
@@ -2204,9 +2400,9 @@ function register9(server, store) {
2204
2400
  "atelier_set_blend_mode",
2205
2401
  "Set or clear the blend mode on a layer",
2206
2402
  {
2207
- id: z23.string().describe("Document ID"),
2208
- layerId: z23.string().describe("Layer ID"),
2209
- blendMode: z23.enum([
2403
+ id: z24.string().describe("Document ID"),
2404
+ layerId: z24.string().describe("Layer ID"),
2405
+ blendMode: z24.enum([
2210
2406
  "normal",
2211
2407
  "multiply",
2212
2408
  "screen",
@@ -2244,13 +2440,13 @@ function register9(server, store) {
2244
2440
  "atelier_set_shadow",
2245
2441
  "Set or remove a drop shadow on a layer",
2246
2442
  {
2247
- id: z23.string().describe("Document ID"),
2248
- layerId: z23.string().describe("Layer ID"),
2249
- shadow: z23.object({
2250
- color: z23.unknown().describe("Shadow color (hex string or RGBA/HSLA object)"),
2251
- blur: z23.number().min(0).describe("Blur radius in pixels"),
2252
- offsetX: z23.number().optional().describe("Horizontal offset in pixels"),
2253
- offsetY: z23.number().optional().describe("Vertical offset in pixels")
2443
+ id: z24.string().describe("Document ID"),
2444
+ layerId: z24.string().describe("Layer ID"),
2445
+ shadow: z24.object({
2446
+ color: z24.unknown().describe("Shadow color (hex string or RGBA/HSLA object)"),
2447
+ blur: z24.number().min(0).describe("Blur radius in pixels"),
2448
+ offsetX: z24.number().optional().describe("Horizontal offset in pixels"),
2449
+ offsetY: z24.number().optional().describe("Vertical offset in pixels")
2254
2450
  }).nullable().describe("Shadow definition, or null to remove")
2255
2451
  },
2256
2452
  { readOnlyHint: false, destructiveHint: false },
@@ -2272,11 +2468,11 @@ function register9(server, store) {
2272
2468
  "atelier_set_tint",
2273
2469
  "Set or remove a color tint on a layer (works on any visual type)",
2274
2470
  {
2275
- id: z23.string().describe("Document ID"),
2276
- layerId: z23.string().describe("Layer ID"),
2277
- tint: z23.object({
2278
- color: z23.string().describe("Tint color (hex string)"),
2279
- amount: z23.number().min(0).max(1).describe("Tint amount (0 = none, 1 = full)")
2471
+ id: z24.string().describe("Document ID"),
2472
+ layerId: z24.string().describe("Layer ID"),
2473
+ tint: z24.object({
2474
+ color: z24.string().describe("Tint color (hex string)"),
2475
+ amount: z24.number().min(0).max(1).describe("Tint amount (0 = none, 1 = full)")
2280
2476
  }).nullable().describe("Tint definition, or null to remove")
2281
2477
  },
2282
2478
  { readOnlyHint: false, destructiveHint: false },
@@ -2298,18 +2494,18 @@ function register9(server, store) {
2298
2494
  "atelier_set_motion_path",
2299
2495
  "Set or remove a motion path on a layer for path-based position animation",
2300
2496
  {
2301
- id: z23.string().describe("Document ID"),
2302
- layerId: z23.string().describe("Layer ID"),
2303
- motionPath: z23.object({
2304
- points: z23.array(z23.object({
2305
- x: z23.number(),
2306
- y: z23.number(),
2307
- in: z23.object({ x: z23.number(), y: z23.number() }).optional(),
2308
- out: z23.object({ x: z23.number(), y: z23.number() }).optional()
2497
+ id: z24.string().describe("Document ID"),
2498
+ layerId: z24.string().describe("Layer ID"),
2499
+ motionPath: z24.object({
2500
+ points: z24.array(z24.object({
2501
+ x: z24.number(),
2502
+ y: z24.number(),
2503
+ in: z24.object({ x: z24.number(), y: z24.number() }).optional(),
2504
+ out: z24.object({ x: z24.number(), y: z24.number() }).optional()
2309
2505
  })).min(2).describe("Path points (minimum 2)"),
2310
- closed: z23.boolean().optional().describe("Whether the path is closed"),
2311
- autoRotate: z23.boolean().optional().describe("Auto-rotate layer to follow path tangent"),
2312
- autoRotateOffset: z23.number().optional().describe("Rotation offset in degrees when autoRotate is enabled")
2506
+ closed: z24.boolean().optional().describe("Whether the path is closed"),
2507
+ autoRotate: z24.boolean().optional().describe("Auto-rotate layer to follow path tangent"),
2508
+ autoRotateOffset: z24.number().optional().describe("Rotation offset in degrees when autoRotate is enabled")
2313
2509
  }).nullable().describe("Motion path definition, or null to remove")
2314
2510
  },
2315
2511
  { readOnlyHint: false, destructiveHint: false },
@@ -2331,21 +2527,21 @@ function register9(server, store) {
2331
2527
  "atelier_set_clip_path",
2332
2528
  "Set or remove a clip path on a layer to restrict rendering to within a shape",
2333
2529
  {
2334
- id: z23.string().describe("Document ID"),
2335
- layerId: z23.string().describe("Layer ID"),
2336
- clipPath: z23.object({
2337
- type: z23.enum(["rect", "ellipse", "path"]).describe("Clip shape type"),
2338
- cornerRadius: z23.union([
2339
- z23.number(),
2340
- z23.tuple([z23.number(), z23.number(), z23.number(), z23.number()])
2530
+ id: z24.string().describe("Document ID"),
2531
+ layerId: z24.string().describe("Layer ID"),
2532
+ clipPath: z24.object({
2533
+ type: z24.enum(["rect", "ellipse", "path"]).describe("Clip shape type"),
2534
+ cornerRadius: z24.union([
2535
+ z24.number(),
2536
+ z24.tuple([z24.number(), z24.number(), z24.number(), z24.number()])
2341
2537
  ]).optional().describe("Corner radius (rect only)"),
2342
- points: z23.array(z23.object({
2343
- x: z23.number(),
2344
- y: z23.number(),
2345
- in: z23.object({ x: z23.number(), y: z23.number() }).optional(),
2346
- out: z23.object({ x: z23.number(), y: z23.number() }).optional()
2538
+ points: z24.array(z24.object({
2539
+ x: z24.number(),
2540
+ y: z24.number(),
2541
+ in: z24.object({ x: z24.number(), y: z24.number() }).optional(),
2542
+ out: z24.object({ x: z24.number(), y: z24.number() }).optional()
2347
2543
  })).optional().describe("Path points (path only)"),
2348
- closed: z23.boolean().optional().describe("Whether path is closed (path only)")
2544
+ closed: z24.boolean().optional().describe("Whether path is closed (path only)")
2349
2545
  }).nullable().describe("Clip path shape, or null to remove")
2350
2546
  },
2351
2547
  { readOnlyHint: false, destructiveHint: false },
@@ -2367,45 +2563,45 @@ function register9(server, store) {
2367
2563
  "atelier_edit_visual",
2368
2564
  "Edit visual content of an existing layer (text content/style, image src/assetId, shape/fill/stroke)",
2369
2565
  {
2370
- id: z23.string().describe("Document ID"),
2371
- layerId: z23.string().describe("Layer ID"),
2566
+ id: z24.string().describe("Document ID"),
2567
+ layerId: z24.string().describe("Layer ID"),
2372
2568
  // Text fields
2373
- content: z23.string().optional().describe("New text content (text layers only)"),
2374
- style: z23.record(z23.unknown()).optional().describe("Partial style update \u2014 only provided keys are overwritten (text layers only)"),
2569
+ content: z24.string().optional().describe("New text content (text layers only)"),
2570
+ style: z24.record(z24.unknown()).optional().describe("Partial style update \u2014 only provided keys are overwritten (text layers only)"),
2375
2571
  // Image fields
2376
- src: z23.string().optional().describe("Image source URL (image layers only)"),
2377
- assetId: z23.string().optional().describe("Asset ID reference (image layers only)"),
2378
- sourceRect: z23.object({
2379
- x: z23.number(),
2380
- y: z23.number(),
2381
- width: z23.number().positive(),
2382
- height: z23.number().positive()
2572
+ src: z24.string().optional().describe("Image source URL (image layers only)"),
2573
+ assetId: z24.string().optional().describe("Asset ID reference (image layers only)"),
2574
+ sourceRect: z24.object({
2575
+ x: z24.number(),
2576
+ y: z24.number(),
2577
+ width: z24.number().positive(),
2578
+ height: z24.number().positive()
2383
2579
  }).nullable().optional().describe("Source rectangle crop (image layers only). Null to remove."),
2384
- spritesheet: z23.object({
2385
- columns: z23.number().int().positive(),
2386
- rows: z23.number().int().positive(),
2387
- frameCount: z23.number().int().positive().optional(),
2388
- frameWidth: z23.number().positive(),
2389
- frameHeight: z23.number().positive()
2580
+ spritesheet: z24.object({
2581
+ columns: z24.number().int().positive(),
2582
+ rows: z24.number().int().positive(),
2583
+ frameCount: z24.number().int().positive().optional(),
2584
+ frameWidth: z24.number().positive(),
2585
+ frameHeight: z24.number().positive()
2390
2586
  }).nullable().optional().describe("Spritesheet config (image layers only). Null to remove."),
2391
- frameIndex: z23.number().int().min(0).optional().describe("Spritesheet frame index (image layers only)"),
2587
+ frameIndex: z24.number().int().min(0).optional().describe("Spritesheet frame index (image layers only)"),
2392
2588
  // Shape fields
2393
- shape: z23.object({
2394
- type: z23.enum(["rect", "ellipse", "path"]),
2395
- cornerRadius: z23.union([
2396
- z23.number(),
2397
- z23.tuple([z23.number(), z23.number(), z23.number(), z23.number()])
2589
+ shape: z24.object({
2590
+ type: z24.enum(["rect", "ellipse", "path"]),
2591
+ cornerRadius: z24.union([
2592
+ z24.number(),
2593
+ z24.tuple([z24.number(), z24.number(), z24.number(), z24.number()])
2398
2594
  ]).optional(),
2399
- points: z23.array(z23.object({
2400
- x: z23.number(),
2401
- y: z23.number(),
2402
- in: z23.object({ x: z23.number(), y: z23.number() }).optional(),
2403
- out: z23.object({ x: z23.number(), y: z23.number() }).optional()
2595
+ points: z24.array(z24.object({
2596
+ x: z24.number(),
2597
+ y: z24.number(),
2598
+ in: z24.object({ x: z24.number(), y: z24.number() }).optional(),
2599
+ out: z24.object({ x: z24.number(), y: z24.number() }).optional()
2404
2600
  })).optional(),
2405
- closed: z23.boolean().optional()
2601
+ closed: z24.boolean().optional()
2406
2602
  }).optional().describe("New shape (shape layers only)"),
2407
- fill: z23.record(z23.unknown()).optional().describe("New fill definition (shape layers only)"),
2408
- stroke: z23.record(z23.unknown()).optional().describe("New stroke definition (shape layers only)")
2603
+ fill: z24.record(z24.unknown()).optional().describe("New fill definition (shape layers only)"),
2604
+ stroke: z24.record(z24.unknown()).optional().describe("New stroke definition (shape layers only)")
2409
2605
  },
2410
2606
  { readOnlyHint: false, destructiveHint: false },
2411
2607
  async ({ id, layerId, content, style, src, assetId, sourceRect, spritesheet, frameIndex, shape, fill, stroke }) => {
@@ -2486,7 +2682,7 @@ function register9(server, store) {
2486
2682
  }
2487
2683
 
2488
2684
  // ../mcp/src/tools/state-config.ts
2489
- import { z as z24 } from "zod";
2685
+ import { z as z25 } from "zod";
2490
2686
  function getDoc10(store, id) {
2491
2687
  const doc = store.get(id);
2492
2688
  if (!doc) return { error: `Document "${id}" not found` };
@@ -2498,27 +2694,27 @@ function ok10(data) {
2498
2694
  function err10(message) {
2499
2695
  return { content: [{ type: "text", text: JSON.stringify({ error: message }) }], isError: true };
2500
2696
  }
2501
- var EasingInputSchema3 = z24.union([
2502
- z24.enum(["ease-in", "ease-out", "ease-in-out"]),
2503
- z24.object({ type: z24.literal("linear") }),
2504
- z24.object({
2505
- type: z24.literal("cubic-bezier"),
2506
- x1: z24.number(),
2507
- y1: z24.number(),
2508
- x2: z24.number(),
2509
- y2: z24.number()
2697
+ var EasingInputSchema3 = z25.union([
2698
+ z25.enum(["ease-in", "ease-out", "ease-in-out"]),
2699
+ z25.object({ type: z25.literal("linear") }),
2700
+ z25.object({
2701
+ type: z25.literal("cubic-bezier"),
2702
+ x1: z25.number(),
2703
+ y1: z25.number(),
2704
+ x2: z25.number(),
2705
+ y2: z25.number()
2510
2706
  }),
2511
- z24.object({
2512
- type: z24.literal("spring"),
2513
- mass: z24.number().optional(),
2514
- stiffness: z24.number().optional(),
2515
- damping: z24.number().optional(),
2516
- velocity: z24.number().optional()
2707
+ z25.object({
2708
+ type: z25.literal("spring"),
2709
+ mass: z25.number().optional(),
2710
+ stiffness: z25.number().optional(),
2711
+ damping: z25.number().optional(),
2712
+ velocity: z25.number().optional()
2517
2713
  }),
2518
- z24.object({
2519
- type: z24.literal("step"),
2520
- steps: z24.number().int().positive(),
2521
- position: z24.enum(["start", "end"]).optional()
2714
+ z25.object({
2715
+ type: z25.literal("step"),
2716
+ steps: z25.number().int().positive(),
2717
+ position: z25.enum(["start", "end"]).optional()
2522
2718
  })
2523
2719
  ]).describe("Easing function");
2524
2720
  function register10(server, store) {
@@ -2526,14 +2722,14 @@ function register10(server, store) {
2526
2722
  "atelier_set_audio",
2527
2723
  "Set or remove an audio track on a state",
2528
2724
  {
2529
- id: z24.string().describe("Document ID"),
2530
- stateName: z24.string().describe("State name"),
2531
- audio: z24.object({
2532
- src: z24.string().describe("Audio source \u2014 asset reference, file path, or URL"),
2533
- offset: z24.number().min(0).optional().describe("Skip into audio in seconds (default: 0)"),
2534
- volume: z24.number().min(0).max(1).optional().describe("Playback volume 0\u20131 (default: 1)"),
2535
- loop: z24.boolean().optional().describe("Loop for state duration (default: false)"),
2536
- startFrame: z24.number().int().min(0).optional().describe("Frame to start playing (default: 0)")
2725
+ id: z25.string().describe("Document ID"),
2726
+ stateName: z25.string().describe("State name"),
2727
+ audio: z25.object({
2728
+ src: z25.string().describe("Audio source \u2014 asset reference, file path, or URL"),
2729
+ offset: z25.number().min(0).optional().describe("Skip into audio in seconds (default: 0)"),
2730
+ volume: z25.number().min(0).max(1).optional().describe("Playback volume 0\u20131 (default: 1)"),
2731
+ loop: z25.boolean().optional().describe("Loop for state duration (default: false)"),
2732
+ startFrame: z25.number().int().min(0).optional().describe("Frame to start playing (default: 0)")
2537
2733
  }).nullable().describe("Audio configuration, or null to remove")
2538
2734
  },
2539
2735
  { readOnlyHint: false, destructiveHint: false },
@@ -2555,11 +2751,11 @@ function register10(server, store) {
2555
2751
  "atelier_configure_transition",
2556
2752
  "Configure or remove a transition from one state to a target state",
2557
2753
  {
2558
- id: z24.string().describe("Document ID"),
2559
- stateName: z24.string().describe("Source state name"),
2560
- targetState: z24.string().describe("Target state name"),
2561
- transition: z24.object({
2562
- duration: z24.number().positive().int().describe("Transition duration in frames"),
2754
+ id: z25.string().describe("Document ID"),
2755
+ stateName: z25.string().describe("Source state name"),
2756
+ targetState: z25.string().describe("Target state name"),
2757
+ transition: z25.object({
2758
+ duration: z25.number().positive().int().describe("Transition duration in frames"),
2563
2759
  easing: EasingInputSchema3.optional()
2564
2760
  }).nullable().describe("Transition config, or null to remove")
2565
2761
  },
@@ -2596,9 +2792,9 @@ function register10(server, store) {
2596
2792
  "atelier_set_state_parent",
2597
2793
  "Set or clear the parent state for hierarchical delta inheritance",
2598
2794
  {
2599
- id: z24.string().describe("Document ID"),
2600
- stateName: z24.string().describe("State name"),
2601
- parent: z24.string().nullable().describe("Parent state name, or null to clear")
2795
+ id: z25.string().describe("Document ID"),
2796
+ stateName: z25.string().describe("State name"),
2797
+ parent: z25.string().nullable().describe("Parent state name, or null to clear")
2602
2798
  },
2603
2799
  { readOnlyHint: false, destructiveHint: false },
2604
2800
  async ({ id, stateName, parent }) => {
@@ -2633,7 +2829,7 @@ function register10(server, store) {
2633
2829
  }
2634
2830
 
2635
2831
  // ../mcp/src/tools/export.ts
2636
- import { z as z25 } from "zod";
2832
+ import { z as z26 } from "zod";
2637
2833
 
2638
2834
  // ../canvas/dist/index.js
2639
2835
  function colorToCSS(color) {
@@ -3818,12 +4014,12 @@ function register11(server, store) {
3818
4014
  "atelier_export_svg",
3819
4015
  "Export a frame as an SVG string",
3820
4016
  {
3821
- id: z25.string().describe("Document ID"),
3822
- stateName: z25.string().optional().describe("State name (defaults to first state)"),
3823
- frame: z25.number().int().min(0).optional().describe("Frame number (defaults to 0)"),
3824
- xmlDeclaration: z25.boolean().optional().describe("Include XML declaration (default: false)"),
3825
- viewBox: z25.boolean().optional().describe("Include viewBox attribute (default: true)"),
3826
- indent: z25.number().int().min(0).optional().describe("Indent size in spaces (default: 2)")
4017
+ id: z26.string().describe("Document ID"),
4018
+ stateName: z26.string().optional().describe("State name (defaults to first state)"),
4019
+ frame: z26.number().int().min(0).optional().describe("Frame number (defaults to 0)"),
4020
+ xmlDeclaration: z26.boolean().optional().describe("Include XML declaration (default: false)"),
4021
+ viewBox: z26.boolean().optional().describe("Include viewBox attribute (default: true)"),
4022
+ indent: z26.number().int().min(0).optional().describe("Indent size in spaces (default: 2)")
3827
4023
  },
3828
4024
  { readOnlyHint: true, destructiveHint: false },
3829
4025
  async ({ id, stateName, frame, xmlDeclaration, viewBox, indent }) => {
@@ -3854,8 +4050,8 @@ function register11(server, store) {
3854
4050
  "atelier_export_lottie",
3855
4051
  "Export a document to Lottie JSON format (lossy, best-effort)",
3856
4052
  {
3857
- id: z25.string().describe("Document ID"),
3858
- stateName: z25.string().optional().describe("State to export (defaults to first state)")
4053
+ id: z26.string().describe("Document ID"),
4054
+ stateName: z26.string().optional().describe("State to export (defaults to first state)")
3859
4055
  },
3860
4056
  { readOnlyHint: true, destructiveHint: false },
3861
4057
  async ({ id, stateName }) => {
@@ -3875,7 +4071,7 @@ function register11(server, store) {
3875
4071
  }
3876
4072
 
3877
4073
  // ../mcp/src/tools/assets.ts
3878
- import { z as z26 } from "zod";
4074
+ import { z as z27 } from "zod";
3879
4075
  function getDoc12(store, id) {
3880
4076
  const doc = store.get(id);
3881
4077
  if (!doc) return { error: `Document "${id}" not found` };
@@ -3892,17 +4088,17 @@ function register12(server, store) {
3892
4088
  "atelier_add_asset",
3893
4089
  "Register an external asset (image, SVG, font, animation, audio) on a document",
3894
4090
  {
3895
- id: z26.string().describe("Document ID"),
3896
- assetId: z26.string().describe("Unique asset identifier"),
3897
- type: z26.enum(["image", "svg", "font", "animation", "audio"]).describe("Asset type"),
3898
- src: z26.string().describe("File path or URL"),
3899
- description: z26.string().optional().describe("Human-readable description"),
3900
- spritesheet: z26.object({
3901
- columns: z26.number().int().positive().describe("Number of columns in the spritesheet grid"),
3902
- rows: z26.number().int().positive().describe("Number of rows in the spritesheet grid"),
3903
- frameCount: z26.number().int().positive().optional().describe("Total frame count (defaults to columns \xD7 rows)"),
3904
- frameWidth: z26.number().positive().describe("Width of each frame in pixels"),
3905
- frameHeight: z26.number().positive().describe("Height of each frame in pixels")
4091
+ id: z27.string().describe("Document ID"),
4092
+ assetId: z27.string().describe("Unique asset identifier"),
4093
+ type: z27.enum(["image", "svg", "font", "animation", "audio"]).describe("Asset type"),
4094
+ src: z27.string().describe("File path or URL"),
4095
+ description: z27.string().optional().describe("Human-readable description"),
4096
+ spritesheet: z27.object({
4097
+ columns: z27.number().int().positive().describe("Number of columns in the spritesheet grid"),
4098
+ rows: z27.number().int().positive().describe("Number of rows in the spritesheet grid"),
4099
+ frameCount: z27.number().int().positive().optional().describe("Total frame count (defaults to columns \xD7 rows)"),
4100
+ frameWidth: z27.number().positive().describe("Width of each frame in pixels"),
4101
+ frameHeight: z27.number().positive().describe("Height of each frame in pixels")
3906
4102
  }).optional().describe("Spritesheet metadata (image assets only)")
3907
4103
  },
3908
4104
  { readOnlyHint: false, destructiveHint: false },
@@ -3925,7 +4121,7 @@ function register12(server, store) {
3925
4121
  "atelier_list_assets",
3926
4122
  "List all registered assets with usage info (which layers/states reference them)",
3927
4123
  {
3928
- id: z26.string().describe("Document ID")
4124
+ id: z27.string().describe("Document ID")
3929
4125
  },
3930
4126
  { readOnlyHint: true, destructiveHint: false },
3931
4127
  async ({ id }) => {
@@ -3952,8 +4148,8 @@ function register12(server, store) {
3952
4148
  "atelier_remove_asset",
3953
4149
  "Remove a registered asset. Warns if layers or state audio still reference it.",
3954
4150
  {
3955
- id: z26.string().describe("Document ID"),
3956
- assetId: z26.string().describe("Asset ID to remove")
4151
+ id: z27.string().describe("Document ID"),
4152
+ assetId: z27.string().describe("Asset ID to remove")
3957
4153
  },
3958
4154
  { readOnlyHint: false, destructiveHint: true },
3959
4155
  async ({ id, assetId }) => {
@@ -3977,7 +4173,7 @@ function register12(server, store) {
3977
4173
  }
3978
4174
 
3979
4175
  // ../mcp/src/tools/variables.ts
3980
- import { z as z27 } from "zod";
4176
+ import { z as z28 } from "zod";
3981
4177
  function getDoc13(store, id) {
3982
4178
  const doc = store.get(id);
3983
4179
  if (!doc) return { error: `Document "${id}" not found` };
@@ -3994,11 +4190,11 @@ function register13(server, store) {
3994
4190
  "atelier_add_variable",
3995
4191
  "Define a template variable on a document",
3996
4192
  {
3997
- id: z27.string().describe("Document ID"),
3998
- variableName: z27.string().describe("Variable name (used in {{variableName}} patterns)"),
3999
- type: z27.enum(["string", "number", "color", "asset", "boolean"]).describe("Variable type"),
4000
- description: z27.string().optional().describe("Human-readable description"),
4001
- default: z27.unknown().optional().describe("Default value")
4193
+ id: z28.string().describe("Document ID"),
4194
+ variableName: z28.string().describe("Variable name (used in {{variableName}} patterns)"),
4195
+ type: z28.enum(["string", "number", "color", "asset", "boolean"]).describe("Variable type"),
4196
+ description: z28.string().optional().describe("Human-readable description"),
4197
+ default: z28.unknown().optional().describe("Default value")
4002
4198
  },
4003
4199
  { readOnlyHint: false, destructiveHint: false },
4004
4200
  async (args) => {
@@ -4019,7 +4215,7 @@ function register13(server, store) {
4019
4215
  "atelier_list_variables",
4020
4216
  "List all declared variables with usage info (which {{references}} exist in the document)",
4021
4217
  {
4022
- id: z27.string().describe("Document ID")
4218
+ id: z28.string().describe("Document ID")
4023
4219
  },
4024
4220
  { readOnlyHint: true, destructiveHint: false },
4025
4221
  async ({ id }) => {
@@ -4043,8 +4239,8 @@ function register13(server, store) {
4043
4239
  "atelier_remove_variable",
4044
4240
  "Remove a declared variable. Warns if {{references}} still exist in the document.",
4045
4241
  {
4046
- id: z27.string().describe("Document ID"),
4047
- variableName: z27.string().describe("Variable name to remove")
4242
+ id: z28.string().describe("Document ID"),
4243
+ variableName: z28.string().describe("Variable name to remove")
4048
4244
  },
4049
4245
  { readOnlyHint: false, destructiveHint: true },
4050
4246
  async ({ id, variableName }) => {
@@ -4067,7 +4263,7 @@ function register13(server, store) {
4067
4263
  }
4068
4264
 
4069
4265
  // ../mcp/src/tools/interactions.ts
4070
- import { z as z28 } from "zod";
4266
+ import { z as z29 } from "zod";
4071
4267
  function getDoc14(store, id) {
4072
4268
  const doc = store.get(id);
4073
4269
  if (!doc) return { error: `Document "${id}" not found` };
@@ -4084,23 +4280,23 @@ function register14(server, store) {
4084
4280
  "atelier_add_interaction",
4085
4281
  "Add a trigger-action interaction to a layer (click, hover, timer, signal \u2192 go-to-state, emit-signal, set-variable, toggle-visibility)",
4086
4282
  {
4087
- id: z28.string().describe("Document ID"),
4088
- layerId: z28.string().describe("Layer to attach the interaction to"),
4089
- interactionId: z28.string().describe("Unique interaction ID"),
4090
- trigger: z28.object({
4091
- type: z28.enum(["click", "hover", "pointerdown", "pointerup", "timer", "signal"]).describe("Trigger type"),
4092
- delay: z28.number().optional().describe("Delay in ms (timer trigger only)"),
4093
- signal: z28.string().optional().describe("Signal name (signal trigger only)")
4283
+ id: z29.string().describe("Document ID"),
4284
+ layerId: z29.string().describe("Layer to attach the interaction to"),
4285
+ interactionId: z29.string().describe("Unique interaction ID"),
4286
+ trigger: z29.object({
4287
+ type: z29.enum(["click", "hover", "pointerdown", "pointerup", "timer", "signal"]).describe("Trigger type"),
4288
+ delay: z29.number().optional().describe("Delay in ms (timer trigger only)"),
4289
+ signal: z29.string().optional().describe("Signal name (signal trigger only)")
4094
4290
  }).describe("What triggers the interaction"),
4095
- action: z28.object({
4096
- type: z28.enum(["go-to-state", "emit-signal", "set-variable", "toggle-visibility"]).describe("Action type"),
4097
- state: z28.string().optional().describe("Target state (go-to-state only)"),
4098
- signal: z28.string().optional().describe("Signal to emit (emit-signal only)"),
4099
- variable: z28.string().optional().describe("Variable name (set-variable only)"),
4100
- value: z28.unknown().optional().describe("Value to set (set-variable only)"),
4101
- targetLayer: z28.string().optional().describe("Target layer ID (toggle-visibility, defaults to self)")
4291
+ action: z29.object({
4292
+ type: z29.enum(["go-to-state", "emit-signal", "set-variable", "toggle-visibility"]).describe("Action type"),
4293
+ state: z29.string().optional().describe("Target state (go-to-state only)"),
4294
+ signal: z29.string().optional().describe("Signal to emit (emit-signal only)"),
4295
+ variable: z29.string().optional().describe("Variable name (set-variable only)"),
4296
+ value: z29.unknown().optional().describe("Value to set (set-variable only)"),
4297
+ targetLayer: z29.string().optional().describe("Target layer ID (toggle-visibility, defaults to self)")
4102
4298
  }).describe("What happens when triggered"),
4103
- description: z28.string().optional().describe("Human-readable description")
4299
+ description: z29.string().optional().describe("Human-readable description")
4104
4300
  },
4105
4301
  { readOnlyHint: false, destructiveHint: false },
4106
4302
  async ({ id, layerId, interactionId, trigger, action, description }) => {
@@ -4149,8 +4345,8 @@ function register14(server, store) {
4149
4345
  "atelier_list_interactions",
4150
4346
  "List all interactions across all layers or for a specific layer",
4151
4347
  {
4152
- id: z28.string().describe("Document ID"),
4153
- layerId: z28.string().optional().describe("Optional: filter to a specific layer")
4348
+ id: z29.string().describe("Document ID"),
4349
+ layerId: z29.string().optional().describe("Optional: filter to a specific layer")
4154
4350
  },
4155
4351
  { readOnlyHint: true, destructiveHint: false },
4156
4352
  async ({ id, layerId }) => {
@@ -4177,9 +4373,9 @@ function register14(server, store) {
4177
4373
  "atelier_remove_interaction",
4178
4374
  "Remove an interaction from a layer",
4179
4375
  {
4180
- id: z28.string().describe("Document ID"),
4181
- layerId: z28.string().describe("Layer ID"),
4182
- interactionId: z28.string().describe("Interaction ID to remove")
4376
+ id: z29.string().describe("Document ID"),
4377
+ layerId: z29.string().describe("Layer ID"),
4378
+ interactionId: z29.string().describe("Interaction ID to remove")
4183
4379
  },
4184
4380
  { readOnlyHint: false, destructiveHint: true },
4185
4381
  async ({ id, layerId, interactionId }) => {
@@ -4201,7 +4397,7 @@ function register14(server, store) {
4201
4397
  }
4202
4398
 
4203
4399
  // ../mcp/src/tools/refs.ts
4204
- import { z as z29 } from "zod";
4400
+ import { z as z30 } from "zod";
4205
4401
  function getDoc15(store, id) {
4206
4402
  const doc = store.get(id);
4207
4403
  if (!doc) return { error: `Document "${id}" not found` };
@@ -4218,9 +4414,9 @@ function register15(server, store) {
4218
4414
  "atelier_set_ref",
4219
4415
  "Set the source reference on a ref-type layer. Can point to a file path or another in-memory document ID.",
4220
4416
  {
4221
- id: z29.string().describe("Document ID"),
4222
- layerId: z29.string().describe("Layer ID (must be a ref visual)"),
4223
- src: z29.string().describe("File path, URL, or in-memory document ID")
4417
+ id: z30.string().describe("Document ID"),
4418
+ layerId: z30.string().describe("Layer ID (must be a ref visual)"),
4419
+ src: z30.string().describe("File path, URL, or in-memory document ID")
4224
4420
  },
4225
4421
  { readOnlyHint: false, destructiveHint: false },
4226
4422
  async ({ id, layerId, src }) => {
@@ -4240,7 +4436,7 @@ function register15(server, store) {
4240
4436
  "atelier_resolve_refs",
4241
4437
  "Check which ref layers can be resolved (point to valid in-memory documents)",
4242
4438
  {
4243
- id: z29.string().describe("Document ID")
4439
+ id: z30.string().describe("Document ID")
4244
4440
  },
4245
4441
  { readOnlyHint: true, destructiveHint: false },
4246
4442
  async ({ id }) => {
@@ -4267,7 +4463,7 @@ function register15(server, store) {
4267
4463
  }
4268
4464
 
4269
4465
  // ../mcp/src/tools/performance.ts
4270
- import { z as z30 } from "zod";
4466
+ import { z as z31 } from "zod";
4271
4467
  function getDoc16(store, id) {
4272
4468
  const doc = store.get(id);
4273
4469
  if (!doc) return { error: `Document "${id}" not found` };
@@ -4284,9 +4480,9 @@ function register16(server, store) {
4284
4480
  "atelier_profile",
4285
4481
  "Profile frame resolution performance \u2014 returns timing, layer count, property count, and delta count for a frame",
4286
4482
  {
4287
- id: z30.string().describe("Document ID"),
4288
- stateName: z30.string().describe("State name to profile"),
4289
- frame: z30.number().int().min(0).describe("Frame number to resolve")
4483
+ id: z31.string().describe("Document ID"),
4484
+ stateName: z31.string().describe("State name to profile"),
4485
+ frame: z31.number().int().min(0).describe("Frame number to resolve")
4290
4486
  },
4291
4487
  { readOnlyHint: true, destructiveHint: false },
4292
4488
  async ({ id, stateName, frame }) => {
@@ -4332,10 +4528,10 @@ function register16(server, store) {
4332
4528
  "atelier_diff_frames",
4333
4529
  "Compare two frames and return only the properties that changed between them",
4334
4530
  {
4335
- id: z30.string().describe("Document ID"),
4336
- stateName: z30.string().describe("State name"),
4337
- frameA: z30.number().int().min(0).describe("First frame number"),
4338
- frameB: z30.number().int().min(0).describe("Second frame number")
4531
+ id: z31.string().describe("Document ID"),
4532
+ stateName: z31.string().describe("State name"),
4533
+ frameA: z31.number().int().min(0).describe("First frame number"),
4534
+ frameB: z31.number().int().min(0).describe("Second frame number")
4339
4535
  },
4340
4536
  { readOnlyHint: true, destructiveHint: false },
4341
4537
  async ({ id, stateName, frameA, frameB }) => {
@@ -4374,9 +4570,9 @@ function register16(server, store) {
4374
4570
  "atelier_batch_preview",
4375
4571
  "Preview multiple frames at once \u2014 reduces round-trips for scrubbing or timeline inspection",
4376
4572
  {
4377
- id: z30.string().describe("Document ID"),
4378
- stateName: z30.string().describe("State name to preview"),
4379
- frames: z30.array(z30.number().int().min(0)).min(1).max(60).describe("Array of frame numbers to resolve (max 60)")
4573
+ id: z31.string().describe("Document ID"),
4574
+ stateName: z31.string().describe("State name to preview"),
4575
+ frames: z31.array(z31.number().int().min(0)).min(1).max(60).describe("Array of frame numbers to resolve (max 60)")
4380
4576
  },
4381
4577
  { readOnlyHint: true, destructiveHint: false },
4382
4578
  async ({ id, stateName, frames }) => {
@@ -4412,7 +4608,7 @@ function register16(server, store) {
4412
4608
  "atelier_complexity",
4413
4609
  "Analyze document complexity \u2014 layer count, delta count, expression usage, state hierarchy depth",
4414
4610
  {
4415
- id: z30.string().describe("Document ID")
4611
+ id: z31.string().describe("Document ID")
4416
4612
  },
4417
4613
  { readOnlyHint: true, destructiveHint: false },
4418
4614
  async ({ id }) => {
@@ -4484,9 +4680,678 @@ function register16(server, store) {
4484
4680
  );
4485
4681
  }
4486
4682
 
4683
+ // ../mcp/src/tools/overlays.ts
4684
+ import { z as z32 } from "zod";
4685
+ function getDoc17(store, id) {
4686
+ const doc = store.get(id);
4687
+ if (!doc) return { error: `Document "${id}" not found` };
4688
+ return { doc };
4689
+ }
4690
+ function ok17(data) {
4691
+ return { content: [{ type: "text", text: JSON.stringify({ success: true, ...data }) }] };
4692
+ }
4693
+ function err17(message) {
4694
+ return { content: [{ type: "text", text: JSON.stringify({ error: message }) }], isError: true };
4695
+ }
4696
+ var AnchorEnum = z32.enum(["top-left", "top-right", "bottom-left", "bottom-right"]);
4697
+ var OverlayStyleSchema = z32.object({
4698
+ font_family: z32.string().optional(),
4699
+ font_size: z32.number().positive().optional(),
4700
+ font_weight: z32.union([z32.number(), z32.enum(["normal", "bold"])]).optional(),
4701
+ color: z32.string().optional()
4702
+ }).optional();
4703
+ var DEFAULT_OVERLAY_MARGIN = 24;
4704
+ var DEFAULT_TEXT_STYLE = {
4705
+ fontFamily: "Inter",
4706
+ fontSize: 24,
4707
+ fontWeight: 600,
4708
+ color: "#F5F5F7"
4709
+ };
4710
+ function anchorToFrame(anchor, canvas, margin) {
4711
+ switch (anchor) {
4712
+ case "top-left":
4713
+ return { frame: { x: margin, y: margin }, anchorPoint: { x: 0, y: 0 } };
4714
+ case "top-right":
4715
+ return { frame: { x: canvas.width - margin, y: margin }, anchorPoint: { x: 1, y: 0 } };
4716
+ case "bottom-left":
4717
+ return { frame: { x: margin, y: canvas.height - margin }, anchorPoint: { x: 0, y: 1 } };
4718
+ case "bottom-right":
4719
+ return {
4720
+ frame: { x: canvas.width - margin, y: canvas.height - margin },
4721
+ anchorPoint: { x: 1, y: 1 }
4722
+ };
4723
+ }
4724
+ }
4725
+ function mergeStyle(style) {
4726
+ return {
4727
+ fontFamily: style?.font_family ?? DEFAULT_TEXT_STYLE.fontFamily,
4728
+ fontSize: style?.font_size ?? DEFAULT_TEXT_STYLE.fontSize,
4729
+ fontWeight: style?.font_weight ?? DEFAULT_TEXT_STYLE.fontWeight,
4730
+ color: style?.color ?? DEFAULT_TEXT_STYLE.color
4731
+ };
4732
+ }
4733
+ function renderPageNumberFormat(format, currentIndex, totalCount) {
4734
+ return format.replace(
4735
+ /\{(current|total)(?::0(\d+)d)?\}/g,
4736
+ (_, name, padWidth) => {
4737
+ const value = name === "current" ? currentIndex : totalCount;
4738
+ const str = String(value);
4739
+ if (padWidth) {
4740
+ const width = parseInt(padWidth, 10);
4741
+ return str.padStart(width, "0");
4742
+ }
4743
+ return str;
4744
+ }
4745
+ );
4746
+ }
4747
+ function register17(server, store) {
4748
+ server.tool(
4749
+ "atelier_add_handle",
4750
+ "Add an anchored creator-handle TextVisual overlay layer (tag: overlay)",
4751
+ {
4752
+ id: z32.string().describe("Document ID"),
4753
+ text: z32.string().min(1).describe('Handle text, e.g. "@username"'),
4754
+ anchor: AnchorEnum.describe("Anchored corner"),
4755
+ margin: z32.number().nonnegative().optional().describe("Px from anchored edges (default 24)"),
4756
+ style: OverlayStyleSchema.describe("Text style overrides (snake_case to match recipe)"),
4757
+ layerId: z32.string().optional().describe("Layer ID (default: overlay-handle)")
4758
+ },
4759
+ { readOnlyHint: false, destructiveHint: false },
4760
+ async ({ id, text, anchor, margin, style, layerId }) => {
4761
+ const result = getDoc17(store, id);
4762
+ if ("error" in result) return err17(result.error);
4763
+ const { doc } = result;
4764
+ const targetId = layerId ?? "overlay-handle";
4765
+ if (doc.layers.some((l) => l.id === targetId)) {
4766
+ return err17(`Layer "${targetId}" already exists in document "${id}"`);
4767
+ }
4768
+ const m = margin ?? DEFAULT_OVERLAY_MARGIN;
4769
+ const { frame, anchorPoint } = anchorToFrame(anchor, doc.canvas, m);
4770
+ const layer = {
4771
+ id: targetId,
4772
+ tags: ["overlay"],
4773
+ visual: { type: "text", content: text, style: mergeStyle(style) },
4774
+ frame,
4775
+ bounds: { width: 600, height: 80 },
4776
+ anchorPoint
4777
+ };
4778
+ doc.layers.push(layer);
4779
+ store.set(id, doc);
4780
+ return ok17({ layerId: targetId, layer });
4781
+ }
4782
+ );
4783
+ server.tool(
4784
+ "atelier_add_page_number",
4785
+ "Add an anchored page-number TextVisual overlay layer (tag: overlay). Substitutes {current} / {total} (and zero-pad forms {current:02d} / {total:02d}) into format.",
4786
+ {
4787
+ id: z32.string().describe("Document ID"),
4788
+ format: z32.string().min(1).describe('Format string, e.g. "{current}/{total}" or "{current:02d}/{total:02d}"'),
4789
+ anchor: AnchorEnum.describe("Anchored corner"),
4790
+ currentIndex: z32.number().int().nonnegative().describe("1-based current index"),
4791
+ totalCount: z32.number().int().positive().describe("Total count"),
4792
+ margin: z32.number().nonnegative().optional().describe("Px from anchored edges (default 24)"),
4793
+ style: OverlayStyleSchema.describe("Text style overrides (snake_case to match recipe)"),
4794
+ layerId: z32.string().optional().describe("Layer ID (default: overlay-page-number)")
4795
+ },
4796
+ { readOnlyHint: false, destructiveHint: false },
4797
+ async ({ id, format, anchor, currentIndex, totalCount, margin, style, layerId }) => {
4798
+ const result = getDoc17(store, id);
4799
+ if ("error" in result) return err17(result.error);
4800
+ const { doc } = result;
4801
+ const targetId = layerId ?? "overlay-page-number";
4802
+ if (doc.layers.some((l) => l.id === targetId)) {
4803
+ return err17(`Layer "${targetId}" already exists in document "${id}"`);
4804
+ }
4805
+ const content = renderPageNumberFormat(format, currentIndex, totalCount);
4806
+ const m = margin ?? DEFAULT_OVERLAY_MARGIN;
4807
+ const { frame, anchorPoint } = anchorToFrame(anchor, doc.canvas, m);
4808
+ const layer = {
4809
+ id: targetId,
4810
+ tags: ["overlay"],
4811
+ visual: { type: "text", content, style: mergeStyle(style) },
4812
+ frame,
4813
+ bounds: { width: 200, height: 80 },
4814
+ anchorPoint
4815
+ };
4816
+ doc.layers.push(layer);
4817
+ store.set(id, doc);
4818
+ return ok17({ layerId: targetId, layer, rendered: content });
4819
+ }
4820
+ );
4821
+ }
4822
+
4823
+ // ../mcp/src/tools/recipes.ts
4824
+ import { readFileSync, writeFileSync, existsSync, readdirSync, mkdirSync } from "fs";
4825
+ import { join, resolve, isAbsolute, dirname } from "path";
4826
+ import { homedir } from "os";
4827
+ import { z as z33 } from "zod";
4828
+ import { parse as parseYaml, stringify as stringifyYaml } from "yaml";
4829
+ function getDoc18(store, id) {
4830
+ const doc = store.get(id);
4831
+ if (!doc) return { error: `Document "${id}" not found` };
4832
+ return { doc };
4833
+ }
4834
+ function ok18(data) {
4835
+ return { content: [{ type: "text", text: JSON.stringify({ success: true, ...data }) }] };
4836
+ }
4837
+ function err18(message, extra) {
4838
+ return {
4839
+ content: [{ type: "text", text: JSON.stringify({ error: message, ...extra }) }],
4840
+ isError: true
4841
+ };
4842
+ }
4843
+ function resolveRecipePath(pathOrName, projectDir) {
4844
+ if (isAbsolute(pathOrName) || pathOrName.includes("/") || pathOrName.includes("\\")) {
4845
+ return resolve(pathOrName);
4846
+ }
4847
+ const candidates = [];
4848
+ const exts = [".recipe.yaml", ".recipe.json", ".yaml", ".yml", ".json"];
4849
+ const projectRecipesDir = join(resolve(projectDir), ".atelier", "recipes");
4850
+ for (const ext of exts) candidates.push(join(projectRecipesDir, `${pathOrName}${ext}`));
4851
+ const userRecipesDir = join(homedir(), ".atelier", "recipes");
4852
+ for (const ext of exts) candidates.push(join(userRecipesDir, `${pathOrName}${ext}`));
4853
+ for (const candidate of candidates) {
4854
+ if (existsSync(candidate)) return candidate;
4855
+ }
4856
+ throw new Error(
4857
+ `Recipe "${pathOrName}" not found. Looked in:
4858
+ ${candidates.map((c) => ` ${c}`).join("\n")}`
4859
+ );
4860
+ }
4861
+ function parseRecipeText(raw, path) {
4862
+ if (path.endsWith(".json")) return JSON.parse(raw);
4863
+ return parseYaml(raw);
4864
+ }
4865
+ function loadRecipe(pathOrName, projectDir) {
4866
+ const path = resolveRecipePath(pathOrName, projectDir);
4867
+ const raw = readFileSync(path, "utf-8");
4868
+ const parsed = parseRecipeText(raw, path);
4869
+ const result = validateRecipe(parsed);
4870
+ if (!result.success) {
4871
+ const msg = result.errors.map((e) => ` ${e.path}: ${e.message}`).join("\n");
4872
+ throw new Error(`Invalid recipe at ${path}:
4873
+ ${msg}`);
4874
+ }
4875
+ return { recipe: result.data, path, warnings: result.warnings ?? [] };
4876
+ }
4877
+ function recipeToYaml(recipe) {
4878
+ return stringifyYaml(recipe);
4879
+ }
4880
+ var DEFAULT_OVERLAY_MARGIN2 = 24;
4881
+ var DEFAULT_OVERLAY_TEXT_STYLE = {
4882
+ fontFamily: "Inter",
4883
+ fontSize: 24,
4884
+ fontWeight: 600,
4885
+ color: "#F5F5F7"
4886
+ };
4887
+ var HANDLE_LAYER_ID = "overlay-handle";
4888
+ var PAGE_NUMBER_LAYER_ID = "overlay-page-number";
4889
+ function applyRecipeToOverlay(doc, recipe, ctx) {
4890
+ const preserved = doc.layers.filter((l) => !(l.tags ?? []).includes("overlay"));
4891
+ const overlayLayers = [];
4892
+ const warnings = [];
4893
+ const rules = recipe.overlay_rules;
4894
+ if (!rules) {
4895
+ return { doc: { ...doc, layers: preserved }, warnings };
4896
+ }
4897
+ if (rules.handle) {
4898
+ overlayLayers.push(buildHandleLayer(rules.handle, doc.canvas));
4899
+ }
4900
+ if (rules.page_number) {
4901
+ if (ctx?.currentIndex != null && ctx?.totalCount != null) {
4902
+ overlayLayers.push(
4903
+ buildPageNumberLayer(rules.page_number, doc.canvas, ctx.currentIndex, ctx.totalCount)
4904
+ );
4905
+ } else {
4906
+ warnings.push(
4907
+ "recipe.overlay_rules.page_number is present but currentIndex/totalCount were not provided \u2014 the page_number layer was skipped."
4908
+ );
4909
+ }
4910
+ }
4911
+ return { doc: { ...doc, layers: [...preserved, ...overlayLayers] }, warnings };
4912
+ }
4913
+ function buildHandleLayer(rule, canvas) {
4914
+ const margin = rule.margin ?? DEFAULT_OVERLAY_MARGIN2;
4915
+ const { frame, anchorPoint } = anchorToFrame2(rule.anchor, canvas, margin);
4916
+ return {
4917
+ id: HANDLE_LAYER_ID,
4918
+ tags: ["overlay"],
4919
+ visual: { type: "text", content: rule.text, style: mergeOverlayStyle(rule.style) },
4920
+ frame,
4921
+ bounds: { width: 600, height: 80 },
4922
+ anchorPoint
4923
+ };
4924
+ }
4925
+ function buildPageNumberLayer(rule, canvas, currentIndex, totalCount) {
4926
+ const margin = rule.margin ?? DEFAULT_OVERLAY_MARGIN2;
4927
+ const { frame, anchorPoint } = anchorToFrame2(rule.anchor, canvas, margin);
4928
+ return {
4929
+ id: PAGE_NUMBER_LAYER_ID,
4930
+ tags: ["overlay"],
4931
+ visual: {
4932
+ type: "text",
4933
+ content: renderPageNumberFormat2(rule.format, currentIndex, totalCount),
4934
+ style: mergeOverlayStyle(rule.style)
4935
+ },
4936
+ frame,
4937
+ bounds: { width: 200, height: 80 },
4938
+ anchorPoint
4939
+ };
4940
+ }
4941
+ function anchorToFrame2(anchor, canvas, margin) {
4942
+ switch (anchor) {
4943
+ case "top-left":
4944
+ return { frame: { x: margin, y: margin }, anchorPoint: { x: 0, y: 0 } };
4945
+ case "top-right":
4946
+ return { frame: { x: canvas.width - margin, y: margin }, anchorPoint: { x: 1, y: 0 } };
4947
+ case "bottom-left":
4948
+ return { frame: { x: margin, y: canvas.height - margin }, anchorPoint: { x: 0, y: 1 } };
4949
+ case "bottom-right":
4950
+ return {
4951
+ frame: { x: canvas.width - margin, y: canvas.height - margin },
4952
+ anchorPoint: { x: 1, y: 1 }
4953
+ };
4954
+ }
4955
+ }
4956
+ function mergeOverlayStyle(style) {
4957
+ return {
4958
+ fontFamily: style?.font_family ?? DEFAULT_OVERLAY_TEXT_STYLE.fontFamily,
4959
+ fontSize: style?.font_size ?? DEFAULT_OVERLAY_TEXT_STYLE.fontSize,
4960
+ fontWeight: style?.font_weight ?? DEFAULT_OVERLAY_TEXT_STYLE.fontWeight,
4961
+ color: style?.color ?? DEFAULT_OVERLAY_TEXT_STYLE.color
4962
+ };
4963
+ }
4964
+ function renderPageNumberFormat2(format, currentIndex, totalCount) {
4965
+ return format.replace(
4966
+ /\{(current|total)(?::0(\d+)d)?\}/g,
4967
+ (_, name, padWidth) => {
4968
+ const value = name === "current" ? currentIndex : totalCount;
4969
+ const str = String(value);
4970
+ if (padWidth) return str.padStart(parseInt(padWidth, 10), "0");
4971
+ return str;
4972
+ }
4973
+ );
4974
+ }
4975
+ var RECIPE_EXTS = [".recipe.yaml", ".recipe.yml", ".recipe.json"];
4976
+ function isRecipeFile(name) {
4977
+ return RECIPE_EXTS.some((ext) => name.endsWith(ext));
4978
+ }
4979
+ function listRecipesIn(dir) {
4980
+ if (!existsSync(dir)) return [];
4981
+ let entries;
4982
+ try {
4983
+ entries = readdirSync(dir);
4984
+ } catch {
4985
+ return [];
4986
+ }
4987
+ return entries.filter(isRecipeFile).map((name) => ({ name, path: join(dir, name) })).sort((a, b) => a.name.localeCompare(b.name));
4988
+ }
4989
+ function register18(server, store) {
4990
+ server.tool(
4991
+ "atelier_recipe_list",
4992
+ "List discoverable Studio Recipes \u2014 project-local <projectDir>/.atelier/recipes/*.recipe.* plus ~/.atelier/recipes/*.recipe.*. Returns { name, path } entries with absolute paths.",
4993
+ {
4994
+ projectDir: z33.string().optional().describe("Project root to search (default: process.cwd())")
4995
+ },
4996
+ { readOnlyHint: true, destructiveHint: false },
4997
+ async ({ projectDir }) => {
4998
+ const root = resolve(projectDir ?? process.cwd());
4999
+ const projectRecipes = listRecipesIn(join(root, ".atelier", "recipes"));
5000
+ const userRecipes = listRecipesIn(join(homedir(), ".atelier", "recipes"));
5001
+ const seen = new Set(projectRecipes.map((r) => r.name));
5002
+ const recipes = [...projectRecipes, ...userRecipes.filter((r) => !seen.has(r.name))];
5003
+ return ok18({ count: recipes.length, recipes });
5004
+ }
5005
+ );
5006
+ server.tool(
5007
+ "atelier_recipe_get",
5008
+ "Load a recipe and return BOTH the parsed object and its serialized YAML text (so the agent can show/explain it), plus any validation warnings.",
5009
+ {
5010
+ path: z33.string().describe("Recipe path or bare name (resolved like the CLI)"),
5011
+ projectDir: z33.string().optional().describe("Project root for name resolution (default: process.cwd())")
5012
+ },
5013
+ { readOnlyHint: true, destructiveHint: false },
5014
+ async ({ path, projectDir }) => {
5015
+ const root = resolve(projectDir ?? process.cwd());
5016
+ try {
5017
+ const loaded = loadRecipe(path, root);
5018
+ return ok18({
5019
+ path: loaded.path,
5020
+ recipe: loaded.recipe,
5021
+ yaml: recipeToYaml(loaded.recipe),
5022
+ warnings: loaded.warnings
5023
+ });
5024
+ } catch (e) {
5025
+ return err18(e.message);
5026
+ }
5027
+ }
5028
+ );
5029
+ server.tool(
5030
+ "atelier_recipe_validate",
5031
+ "Validate a recipe from a path OR inline YAML text. Returns { success, warnings, errors }.",
5032
+ {
5033
+ path: z33.string().optional().describe("Recipe path or bare name to validate"),
5034
+ yaml: z33.string().optional().describe("Inline YAML text to validate (alternative to path)"),
5035
+ projectDir: z33.string().optional().describe("Project root for name resolution (default: process.cwd())")
5036
+ },
5037
+ { readOnlyHint: true, destructiveHint: false },
5038
+ async ({ path, yaml, projectDir }) => {
5039
+ if (!path && yaml === void 0) {
5040
+ return err18("Provide either `path` or `yaml` to validate");
5041
+ }
5042
+ const root = resolve(projectDir ?? process.cwd());
5043
+ let parsed;
5044
+ try {
5045
+ if (yaml !== void 0) {
5046
+ parsed = parseYaml(yaml);
5047
+ } else {
5048
+ const resolved = resolveRecipePath(path, root);
5049
+ parsed = parseRecipeText(readFileSync(resolved, "utf-8"), resolved);
5050
+ }
5051
+ } catch (e) {
5052
+ return ok18({ valid: false, warnings: [], errors: [{ path: "(yaml)", message: e.message }] });
5053
+ }
5054
+ const result = validateRecipe(parsed);
5055
+ if (!result.success) {
5056
+ return ok18({ valid: false, warnings: [], errors: result.errors });
5057
+ }
5058
+ return ok18({ valid: true, warnings: result.warnings ?? [], errors: [] });
5059
+ }
5060
+ );
5061
+ server.tool(
5062
+ "atelier_recipe_save",
5063
+ "Write a recipe to disk. Accepts a recipe object (serialized via YAML) OR raw YAML text. Validates BEFORE writing and REFUSES to write an invalid recipe (returns an error with the validation issues). Creates parent directories as needed.",
5064
+ {
5065
+ path: z33.string().describe("Absolute or relative file path to write the recipe to"),
5066
+ recipe: z33.record(z33.unknown()).optional().describe("Recipe object (will be serialized to YAML)"),
5067
+ yaml: z33.string().optional().describe("Raw YAML text (alternative to `recipe`)")
5068
+ },
5069
+ { readOnlyHint: false, destructiveHint: true },
5070
+ async ({ path, recipe, yaml }) => {
5071
+ if (recipe === void 0 && yaml === void 0) {
5072
+ return err18("Provide either `recipe` (object) or `yaml` (text) to save");
5073
+ }
5074
+ let parsed;
5075
+ let yamlText;
5076
+ if (yaml !== void 0) {
5077
+ yamlText = yaml;
5078
+ try {
5079
+ parsed = parseYaml(yaml);
5080
+ } catch (e) {
5081
+ return err18(`YAML parse error: ${e.message}`);
5082
+ }
5083
+ } else {
5084
+ parsed = recipe;
5085
+ yamlText = stringifyYaml(recipe);
5086
+ }
5087
+ const result = validateRecipe(parsed);
5088
+ if (!result.success) {
5089
+ return err18("Refusing to write an invalid recipe", { errors: result.errors });
5090
+ }
5091
+ const absPath = resolve(path);
5092
+ try {
5093
+ mkdirSync(dirname(absPath), { recursive: true });
5094
+ writeFileSync(absPath, yamlText, "utf-8");
5095
+ } catch (e) {
5096
+ return err18(`Failed to write recipe: ${e.message}`);
5097
+ }
5098
+ return ok18({ path: absPath, warnings: result.warnings ?? [] });
5099
+ }
5100
+ );
5101
+ server.tool(
5102
+ "atelier_recipe_apply",
5103
+ `Apply a recipe's overlay_rules to an in-store document (handle + page-number overlays). Accepts a recipe object OR a path/name. The result is stored back with source="llm". NOTE: only overlay_rules are applied here \u2014 silence-trim / transcribe / caption application remain CLI-only. page_number overlays require currentIndex + totalCount; if a recipe requests page_number without them, that layer is skipped and a warning is returned.`,
5104
+ {
5105
+ documentId: z33.string().describe("In-store document ID to apply the recipe to"),
5106
+ path: z33.string().optional().describe("Recipe path or bare name (alternative to `recipe`)"),
5107
+ recipe: z33.record(z33.unknown()).optional().describe("Recipe object (alternative to `path`)"),
5108
+ projectDir: z33.string().optional().describe("Project root for name resolution (default: process.cwd())"),
5109
+ currentIndex: z33.number().int().nonnegative().optional().describe("1-based carousel index for page_number"),
5110
+ totalCount: z33.number().int().positive().optional().describe("Total carousel size for page_number")
5111
+ },
5112
+ { readOnlyHint: false, destructiveHint: false },
5113
+ async ({ documentId, path, recipe, projectDir, currentIndex, totalCount }) => {
5114
+ const result = getDoc18(store, documentId);
5115
+ if ("error" in result) return err18(result.error);
5116
+ const { doc } = result;
5117
+ if (path === void 0 && recipe === void 0) {
5118
+ return err18("Provide either `path` or `recipe` to apply");
5119
+ }
5120
+ let resolvedRecipe;
5121
+ if (recipe !== void 0) {
5122
+ const validated = validateRecipe(recipe);
5123
+ if (!validated.success) {
5124
+ return err18("Invalid recipe", { errors: validated.errors });
5125
+ }
5126
+ resolvedRecipe = validated.data;
5127
+ } else {
5128
+ const root = resolve(projectDir ?? process.cwd());
5129
+ try {
5130
+ resolvedRecipe = loadRecipe(path, root).recipe;
5131
+ } catch (e) {
5132
+ return err18(e.message);
5133
+ }
5134
+ }
5135
+ const { doc: updated, warnings } = applyRecipeToOverlay(doc, resolvedRecipe, {
5136
+ currentIndex,
5137
+ totalCount
5138
+ });
5139
+ store.set(documentId, updated, "llm");
5140
+ return ok18({
5141
+ documentId,
5142
+ warnings,
5143
+ layerCount: updated.layers.length,
5144
+ document: updated
5145
+ });
5146
+ }
5147
+ );
5148
+ }
5149
+
5150
+ // ../mcp/src/tools/import-images.ts
5151
+ import { existsSync as existsSync2, readdirSync as readdirSync2, statSync } from "fs";
5152
+ import { resolve as resolve2, join as join2, basename, extname } from "path";
5153
+ import { z as z34 } from "zod";
5154
+ function getDoc19(store, id) {
5155
+ const doc = store.get(id);
5156
+ if (!doc) return { error: `Document "${id}" not found` };
5157
+ return { doc };
5158
+ }
5159
+ function ok19(data) {
5160
+ return { content: [{ type: "text", text: JSON.stringify({ success: true, ...data }) }] };
5161
+ }
5162
+ function err19(message) {
5163
+ return { content: [{ type: "text", text: JSON.stringify({ error: message }) }], isError: true };
5164
+ }
5165
+ var IMAGE_EXTS = /* @__PURE__ */ new Set([".png", ".jpg", ".jpeg", ".webp", ".gif", ".svg"]);
5166
+ function isImageFile(name) {
5167
+ return IMAGE_EXTS.has(extname(name).toLowerCase());
5168
+ }
5169
+ function matchesGlob(name, glob) {
5170
+ if (!glob) return true;
5171
+ const escaped = glob.replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/\*/g, ".*").replace(/\?/g, ".");
5172
+ return new RegExp(`^${escaped}$`, "i").test(name);
5173
+ }
5174
+ function deriveAssetId(filename, used) {
5175
+ const stem = basename(filename, extname(filename));
5176
+ const base = stem.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "") || "image";
5177
+ let candidate = base;
5178
+ let n = 2;
5179
+ while (used.has(candidate)) {
5180
+ candidate = `${base}-${n}`;
5181
+ n++;
5182
+ }
5183
+ used.add(candidate);
5184
+ return candidate;
5185
+ }
5186
+ function nextImageLayerId(existing, seq) {
5187
+ let candidate = `image-${seq}`;
5188
+ let n = seq;
5189
+ while (existing.has(candidate)) {
5190
+ n++;
5191
+ candidate = `image-${n}`;
5192
+ }
5193
+ existing.add(candidate);
5194
+ return candidate;
5195
+ }
5196
+ function register19(server, store) {
5197
+ server.tool(
5198
+ "atelier_import_images",
5199
+ `Import every image in a folder into a document. Registers each as an asset and, unless asLayers is false, creates one fit-to-canvas ImageVisual layer per image (centered). Extensions: .png .jpg .jpeg .webp .gif .svg. Optional glob filters by filename (e.g. "*.png"); results are sorted by name. If the folder has exactly one image AND the doc's layer[0] is the welcome "background" placeholder, that image replaces the placeholder in place. Asset src is the absolute file path. Layer bounds default to a centered min(canvas.w, canvas.h) square because natural image dimensions are not measured (no image-dimension dependency).`,
5200
+ {
5201
+ documentId: z34.string().describe("In-store document ID to import into"),
5202
+ dir: z34.string().describe("Directory containing the images"),
5203
+ glob: z34.string().optional().describe('Optional filename glob, e.g. "*.png" (default: all images)'),
5204
+ asLayers: z34.boolean().optional().describe("Create layers per image (default: true). false = register assets only.")
5205
+ },
5206
+ { readOnlyHint: false, destructiveHint: false },
5207
+ async ({ documentId, dir, glob, asLayers }) => {
5208
+ const result = getDoc19(store, documentId);
5209
+ if ("error" in result) return err19(result.error);
5210
+ const { doc } = result;
5211
+ const absDir = resolve2(dir);
5212
+ if (!existsSync2(absDir)) return err19(`Directory "${absDir}" does not exist`);
5213
+ if (!statSync(absDir).isDirectory()) return err19(`Path "${absDir}" is not a directory`);
5214
+ const files = readdirSync2(absDir).filter((name) => isImageFile(name) && matchesGlob(name, glob)).sort((a, b) => a.localeCompare(b));
5215
+ if (files.length === 0) {
5216
+ return err19(`No image files matched in "${absDir}"${glob ? ` (glob: ${glob})` : ""}`);
5217
+ }
5218
+ const makeLayers = asLayers !== false;
5219
+ const canvas = doc.canvas;
5220
+ const square = Math.min(canvas.width, canvas.height);
5221
+ const allowBackgroundSwap = files.length === 1 && doc.layers[0]?.id === "background";
5222
+ const newAssets = { ...doc.assets ?? {} };
5223
+ const newLayers = [...doc.layers];
5224
+ const usedAssetIds = new Set(Object.keys(newAssets));
5225
+ const existingLayerIds = new Set(newLayers.map((l) => l.id));
5226
+ const assetIds = [];
5227
+ const layerIds = [];
5228
+ let layerSeq = newLayers.length + 1;
5229
+ for (const file of files) {
5230
+ const absPath = join2(absDir, file);
5231
+ const assetId = deriveAssetId(file, usedAssetIds);
5232
+ newAssets[assetId] = { type: "image", src: absPath, description: file };
5233
+ assetIds.push(assetId);
5234
+ if (!makeLayers) continue;
5235
+ const layerId = nextImageLayerId(existingLayerIds, layerSeq);
5236
+ layerSeq++;
5237
+ const layer = {
5238
+ id: layerId,
5239
+ visual: { type: "image", assetId, src: absPath },
5240
+ frame: { x: canvas.width / 2, y: canvas.height / 2 },
5241
+ bounds: { width: square, height: square },
5242
+ anchorPoint: { x: 0.5, y: 0.5 },
5243
+ opacity: 1
5244
+ };
5245
+ if (allowBackgroundSwap) {
5246
+ newLayers[0] = layer;
5247
+ } else {
5248
+ newLayers.unshift(layer);
5249
+ }
5250
+ layerIds.push(layerId);
5251
+ }
5252
+ const newDoc = { ...doc, assets: newAssets, layers: newLayers };
5253
+ store.set(documentId, newDoc, "llm");
5254
+ return ok19({
5255
+ imported: files.length,
5256
+ assetIds,
5257
+ layerIds,
5258
+ asLayers: makeLayers,
5259
+ note: "Layer bounds default to a centered square (min canvas dimension) \u2014 natural image dimensions are not measured."
5260
+ });
5261
+ }
5262
+ );
5263
+ }
5264
+
5265
+ // ../mcp/src/bridge-protocol.ts
5266
+ var BRIDGE_PROTOCOL_VERSION = 1;
5267
+ function isBridgeEnvelope(value) {
5268
+ if (!value || typeof value !== "object") return false;
5269
+ const t = value.type;
5270
+ return t === "hello" || t === "doc:load" || t === "doc:loaded" || t === "doc:patch" || t === "llm:mutation" || t === "error";
5271
+ }
5272
+
5273
+ // ../mcp/src/transports/ws-transport.ts
5274
+ var WebSocketServerTransport = class {
5275
+ constructor(ws) {
5276
+ this.ws = ws;
5277
+ }
5278
+ onclose;
5279
+ onerror;
5280
+ onmessage;
5281
+ sessionId;
5282
+ started = false;
5283
+ closed = false;
5284
+ async start() {
5285
+ if (this.started) {
5286
+ throw new Error(
5287
+ "WebSocketServerTransport already started \u2014 the MCP SDK calls start() automatically; do not call it again"
5288
+ );
5289
+ }
5290
+ this.started = true;
5291
+ this.ws.on("message", (data) => {
5292
+ let text;
5293
+ try {
5294
+ if (typeof data === "string") {
5295
+ text = data;
5296
+ } else if (data instanceof Buffer) {
5297
+ text = data.toString("utf-8");
5298
+ } else if (Array.isArray(data)) {
5299
+ text = Buffer.concat(data).toString("utf-8");
5300
+ } else if (data instanceof ArrayBuffer) {
5301
+ text = Buffer.from(data).toString("utf-8");
5302
+ } else {
5303
+ text = String(data);
5304
+ }
5305
+ } catch (err20) {
5306
+ this.onerror?.(err20 instanceof Error ? err20 : new Error(String(err20)));
5307
+ return;
5308
+ }
5309
+ let parsed;
5310
+ try {
5311
+ parsed = JSON.parse(text);
5312
+ } catch (err20) {
5313
+ this.onerror?.(
5314
+ new Error(
5315
+ `WebSocketServerTransport: failed to parse JSON-RPC message: ${err20 instanceof Error ? err20.message : String(err20)}`
5316
+ )
5317
+ );
5318
+ return;
5319
+ }
5320
+ this.onmessage?.(parsed);
5321
+ });
5322
+ this.ws.on("close", () => {
5323
+ if (this.closed) return;
5324
+ this.closed = true;
5325
+ this.onclose?.();
5326
+ });
5327
+ this.ws.on("error", (err20) => {
5328
+ this.onerror?.(err20);
5329
+ });
5330
+ }
5331
+ async send(message) {
5332
+ if (this.closed) {
5333
+ throw new Error("WebSocketServerTransport: cannot send after close");
5334
+ }
5335
+ if (this.ws.readyState !== 1) {
5336
+ throw new Error(
5337
+ `WebSocketServerTransport: WebSocket is not OPEN (readyState=${this.ws.readyState})`
5338
+ );
5339
+ }
5340
+ this.ws.send(JSON.stringify(message));
5341
+ }
5342
+ async close() {
5343
+ if (this.closed) return;
5344
+ this.closed = true;
5345
+ try {
5346
+ this.ws.close();
5347
+ } catch {
5348
+ }
5349
+ this.onclose?.();
5350
+ }
5351
+ };
5352
+
4487
5353
  // ../mcp/src/index.ts
4488
- function createServer() {
4489
- const store = new DocumentStore();
5354
+ function createServer(store = new DocumentStore()) {
4490
5355
  const server = new McpServer(
4491
5356
  { name: "atelier", version: "0.1.0" },
4492
5357
  { capabilities: { tools: {} } }
@@ -4507,6 +5372,9 @@ function createServer() {
4507
5372
  register14(server, store);
4508
5373
  register15(server, store);
4509
5374
  register16(server, store);
5375
+ register17(server, store);
5376
+ register18(server, store);
5377
+ register19(server, store);
4510
5378
  return { server, store };
4511
5379
  }
4512
5380
  var isMain = typeof process !== "undefined" && process.argv[1] && (process.argv[1].endsWith("/index.js") || process.argv[1].endsWith("/index.cjs") || process.argv[1].includes("atelier-mcp"));
@@ -4519,18 +5387,24 @@ if (isMain) {
4519
5387
  });
4520
5388
  }
4521
5389
  export {
5390
+ BRIDGE_PROTOCOL_VERSION,
4522
5391
  DocumentStore,
5392
+ WebSocketServerTransport,
4523
5393
  createServer,
5394
+ isBridgeEnvelope,
4524
5395
  register12 as registerAssetTools,
4525
5396
  register5 as registerDeltaTools,
4526
5397
  register as registerDocumentTools,
4527
5398
  register11 as registerExportTools,
5399
+ register19 as registerImportImageTools,
4528
5400
  register14 as registerInteractionTools,
4529
5401
  register9 as registerLayerEffectTools,
4530
5402
  register2 as registerLayerTools,
5403
+ register17 as registerOverlayTools,
4531
5404
  register16 as registerPerformanceTools,
4532
5405
  register6 as registerPresetTools,
4533
5406
  register7 as registerPreviewTools,
5407
+ register18 as registerRecipeTools,
4534
5408
  register15 as registerRefTools,
4535
5409
  register3 as registerShapeTools,
4536
5410
  register10 as registerStateConfigTools,