@a-company/atelier 0.36.0 → 0.38.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.
Files changed (99) hide show
  1. package/dist/{chunk-JPZ4F4PW.js → chunk-3ARBOSWY.js} +64 -5
  2. package/dist/chunk-3ARBOSWY.js.map +1 -0
  3. package/dist/cli.js +11469 -413
  4. package/dist/cli.js.map +1 -1
  5. package/dist/{dist-M67UZGFQ.js → dist-3YQK6PI6.js} +2 -2
  6. package/dist/index.cjs +3193 -227
  7. package/dist/index.cjs.map +1 -1
  8. package/dist/index.d.cts +701 -8
  9. package/dist/index.d.ts +701 -8
  10. package/dist/index.js +7237 -72
  11. package/dist/index.js.map +1 -1
  12. package/dist/mcp.js +2898 -507
  13. package/dist/mcp.js.map +1 -1
  14. package/package.json +14 -9
  15. package/src/web/inline-app.ts +55 -4
  16. package/src/web/timeline-state-types.ts +28 -0
  17. package/src/web/timeline-view.test.ts +99 -0
  18. package/src/web/timeline-view.ts +339 -0
  19. package/src/web/workspace-app.ts +3146 -0
  20. package/templates/workspace/.claude/agents/atelier-iris.md +75 -0
  21. package/templates/workspace/.claude/agents/atelier-lux.md +67 -0
  22. package/templates/workspace/.claude/agents/atelier-quill.md +61 -0
  23. package/templates/workspace/.gitignore +30 -0
  24. package/templates/workspace/.paradigm/personas/_shared/cascade-merge.md +172 -0
  25. package/templates/workspace/CLAUDE.md +93 -0
  26. package/templates/workspace/README.md +75 -0
  27. package/templates/workspace/SETUP.md +127 -0
  28. package/templates/workspace/_brand/.atelier-brand.yaml +34 -0
  29. package/templates/workspace/_brand/DESIGN.md +56 -0
  30. package/templates/workspace/_brand/SCRIPT.md +41 -0
  31. package/templates/workspace/_brand/STORYBOARD.md +33 -0
  32. package/templates/workspace/_packs/README.md +54 -0
  33. package/templates/workspace/projects/README.md +49 -0
  34. package/templates/workspace/workspace.atelier +22 -0
  35. package/university/content/notes/N-atel-001-first-render.md +114 -0
  36. package/university/content/notes/N-atel-001-install-and-launch.md +84 -0
  37. package/university/content/notes/N-atel-001-what-is-atelier.md +51 -0
  38. package/university/content/notes/N-atel-101-easings.md +97 -0
  39. package/university/content/notes/N-atel-101-layers.md +106 -0
  40. package/university/content/notes/N-atel-101-states-and-deltas.md +94 -0
  41. package/university/content/notes/N-atel-101-the-atelier-format.md +72 -0
  42. package/university/content/notes/N-atel-201-authoring-tools.md +141 -0
  43. package/university/content/notes/N-atel-201-mcp-overview.md +86 -0
  44. package/university/content/notes/N-atel-201-patterns.md +108 -0
  45. package/university/content/notes/N-atel-201-visual-and-effects.md +125 -0
  46. package/university/content/notes/N-atel-301-composition-and-overlays.md +141 -0
  47. package/university/content/notes/N-atel-301-effects.md +136 -0
  48. package/university/content/notes/N-atel-301-images-and-video.md +126 -0
  49. package/university/content/notes/N-atel-301-shapes-and-text.md +118 -0
  50. package/university/content/notes/N-atel-401-hierarchical-states.md +71 -0
  51. package/university/content/notes/N-atel-401-motion-deep-dive.md +106 -0
  52. package/university/content/notes/N-atel-401-presets-and-templates.md +98 -0
  53. package/university/content/notes/N-atel-401-transitions.md +94 -0
  54. package/university/content/notes/N-atel-501-detected-vs-user-edited.md +76 -0
  55. package/university/content/notes/N-atel-501-layer-tag-isolation.md +62 -0
  56. package/university/content/notes/N-atel-501-silence-trim.md +98 -0
  57. package/university/content/notes/N-atel-501-transcribe-and-captions.md +98 -0
  58. package/university/content/notes/N-atel-601-carousel.md +71 -0
  59. package/university/content/notes/N-atel-601-overlay-rules.md +96 -0
  60. package/university/content/notes/N-atel-601-recipe-tools-and-apply.md +84 -0
  61. package/university/content/notes/N-atel-601-studio-recipe.md +103 -0
  62. package/university/content/notes/N-atel-701-choosing-output.md +68 -0
  63. package/university/content/notes/N-atel-701-png-and-frames.md +84 -0
  64. package/university/content/notes/N-atel-701-vector.md +85 -0
  65. package/university/content/notes/N-atel-701-video.md +88 -0
  66. package/university/content/notes/N-atel-801-editing-surface.md +69 -0
  67. package/university/content/notes/N-atel-801-live-bridge.md +84 -0
  68. package/university/content/notes/N-atel-801-studio-app.md +72 -0
  69. package/university/content/notes/N-atel-801-symbiotic-loop.md +56 -0
  70. package/university/content/paths/LP-atel-001.yaml +21 -0
  71. package/university/content/paths/LP-atel-101.yaml +22 -0
  72. package/university/content/paths/LP-atel-201.yaml +23 -0
  73. package/university/content/paths/LP-atel-301.yaml +22 -0
  74. package/university/content/paths/LP-atel-401.yaml +22 -0
  75. package/university/content/paths/LP-atel-501.yaml +22 -0
  76. package/university/content/paths/LP-atel-601.yaml +22 -0
  77. package/university/content/paths/LP-atel-701.yaml +22 -0
  78. package/university/content/paths/LP-atel-801.yaml +22 -0
  79. package/university/content/quizzes/Q-atel-001-orientation.yaml +66 -0
  80. package/university/content/quizzes/Q-atel-101-document-model.yaml +66 -0
  81. package/university/content/quizzes/Q-atel-201-mcp-authoring.yaml +66 -0
  82. package/university/content/quizzes/Q-atel-301-visual-system.yaml +66 -0
  83. package/university/content/quizzes/Q-atel-401-state-machines.yaml +66 -0
  84. package/university/content/quizzes/Q-atel-501-video-pipeline.yaml +66 -0
  85. package/university/content/quizzes/Q-atel-601-recipes.yaml +66 -0
  86. package/university/content/quizzes/Q-atel-701-export.yaml +66 -0
  87. package/university/content/quizzes/Q-atel-801-studio-loop.yaml +66 -0
  88. package/university/index.yaml +720 -0
  89. package/university/pack.yaml +21 -0
  90. package/dist/chunk-5QQESXI6.js +0 -4432
  91. package/dist/chunk-5QQESXI6.js.map +0 -1
  92. package/dist/chunk-JPZ4F4PW.js.map +0 -1
  93. package/dist/cli.cjs +0 -6313
  94. package/dist/cli.cjs.map +0 -1
  95. package/dist/cli.d.cts +0 -1
  96. package/dist/cli.d.ts +0 -1
  97. package/dist/mcp.cjs +0 -5462
  98. package/dist/mcp.cjs.map +0 -1
  99. /package/dist/{dist-M67UZGFQ.js.map → dist-3YQK6PI6.js.map} +0 -0
package/dist/mcp.js CHANGED
@@ -86,7 +86,7 @@ var DocumentStore = class {
86
86
  };
87
87
 
88
88
  // ../mcp/src/tools/document.ts
89
- import { z as z16 } from "zod";
89
+ import { z as z26 } from "zod";
90
90
 
91
91
  // ../schema/dist/index.js
92
92
  import { z } from "zod";
@@ -102,9 +102,20 @@ import { z as z10 } from "zod";
102
102
  import { z as z11 } from "zod";
103
103
  import { z as z12 } from "zod";
104
104
  import { z as z13 } from "zod";
105
+ import { z as z17 } from "zod";
105
106
  import { z as z14 } from "zod";
106
107
  import { z as z15 } from "zod";
108
+ import { z as z16 } from "zod";
109
+ import { z as z18 } from "zod";
107
110
  import { parse as yamlParse, stringify as yamlStringify } from "yaml";
111
+ import { z as z19 } from "zod";
112
+ import { z as z20 } from "zod";
113
+ import { z as z21 } from "zod";
114
+ import { z as z22 } from "zod";
115
+ import { z as z23 } from "zod";
116
+ import { parse as yamlParse2, stringify as yamlStringify2 } from "yaml";
117
+ import { z as z24 } from "zod";
118
+ import { z as z25 } from "zod";
108
119
  var PixelSchema = z.number();
109
120
  var PercentageSchema = z.string().regex(/^-?\d+(\.\d+)?%$/, {
110
121
  message: 'Percentage must be a number followed by %, e.g. "50%"'
@@ -357,6 +368,19 @@ var VideoVisualSchema = z8.object({
357
368
  muted: z8.boolean().optional(),
358
369
  objectFit: z8.enum(["contain", "cover", "fill"]).optional()
359
370
  });
371
+ var AudioVisualSchema = z8.object({
372
+ type: z8.literal("audio"),
373
+ assetId: z8.string().min(1, "assetId is required"),
374
+ src: z8.string().optional(),
375
+ startFrame: z8.number().int().min(0).optional(),
376
+ sourceOffset: z8.number().min(0).optional(),
377
+ sourceEnd: z8.number().positive().optional(),
378
+ playbackRate: z8.number().positive().optional(),
379
+ volume: z8.number().min(0).max(1).optional(),
380
+ muted: z8.boolean().optional(),
381
+ fadeIn: z8.number().min(0).optional(),
382
+ fadeOut: z8.number().min(0).optional()
383
+ });
360
384
  var GroupVisualSchema = z8.object({
361
385
  type: z8.literal("group")
362
386
  });
@@ -371,6 +395,7 @@ var VisualSchema = z8.discriminatedUnion("type", [
371
395
  TextVisualSchema,
372
396
  ImageVisualSchema,
373
397
  VideoVisualSchema,
398
+ AudioVisualSchema,
374
399
  GroupVisualSchema,
375
400
  RefVisualSchema
376
401
  ]);
@@ -452,13 +477,6 @@ var DeltaSchema = z9.object({
452
477
  description: z9.string().optional(),
453
478
  tags: z9.array(z9.string()).optional()
454
479
  });
455
- var AudioSchema = z10.object({
456
- src: z10.string().min(1, "Audio src is required"),
457
- offset: z10.number().min(0, "Audio offset must be non-negative").optional(),
458
- volume: z10.number().min(0).max(1, "Audio volume must be 0\u20131").optional(),
459
- loop: z10.boolean().optional(),
460
- startFrame: z10.number().int().min(0, "Audio startFrame must be a non-negative integer").optional()
461
- });
462
480
  var StateTransitionConfigSchema = z10.object({
463
481
  duration: z10.number().int().positive("Transition duration must be a positive integer (frames)"),
464
482
  easing: EasingSchema.optional()
@@ -469,7 +487,6 @@ var StateSchema = z10.object({
469
487
  parent: z10.string().optional(),
470
488
  duration: z10.number().int().positive("State duration must be a positive integer (frames)"),
471
489
  deltas: z10.array(DeltaSchema),
472
- audio: AudioSchema.optional(),
473
490
  transitions: z10.record(z10.string(), StateTransitionConfigSchema).optional()
474
491
  });
475
492
  var PresetDeltaSchema = z11.object({
@@ -509,61 +526,119 @@ var AssetSchema = z13.object({
509
526
  height: z13.number().int().positive()
510
527
  }).optional()
511
528
  });
512
- var CanvasSchema = z14.object({
513
- width: z14.number().int().positive("Canvas width must be a positive integer"),
514
- height: z14.number().int().positive("Canvas height must be a positive integer"),
515
- fps: z14.number().int().positive("FPS must be a positive integer"),
516
- background: z14.string().optional()
529
+ var TimelineClipSchema = z14.object({
530
+ layerId: z14.string().min(1, "TimelineClip.layerId is required"),
531
+ startFrame: z14.number().int().min(0, "startFrame must be a non-negative integer"),
532
+ endFrame: z14.number().int().min(0, "endFrame must be a non-negative integer"),
533
+ source: z14.string().min(1, "TimelineClip.source is required")
534
+ });
535
+ var TimelineTrackSchema = z14.object({
536
+ id: z14.string().min(1, "TimelineTrack.id is required"),
537
+ kind: z14.enum(["video", "audio"]),
538
+ clips: z14.array(TimelineClipSchema)
539
+ });
540
+ var TimelineSchema = z14.object({
541
+ fps: z14.number().int().positive("Timeline.fps must be a positive integer"),
542
+ totalFrames: z14.number().int().min(0, "Timeline.totalFrames must be a non-negative integer"),
543
+ tracks: z14.array(TimelineTrackSchema)
544
+ });
545
+ var SlideTransitionSchema = z15.object({
546
+ kind: z15.enum(["cut", "crossfade", "fade"]),
547
+ durationFrames: z15.number().int().min(0, "transition durationFrames must be a non-negative integer").optional(),
548
+ color: z15.string().optional()
549
+ });
550
+ var SlideRefSchema = z15.object({
551
+ src: z15.string().min(1, "SlideRef.src is required"),
552
+ duration: z15.number().int().min(0, "SlideRef.duration must be a non-negative integer (frames)").optional(),
553
+ transition: SlideTransitionSchema.optional(),
554
+ label: z15.string().optional()
555
+ });
556
+ var DesignRelationSchema = z16.enum(["derived-from", "sibling", "references"]);
557
+ var AtelierDesignRelationSchema = z16.object({
558
+ source: z16.string().min(1, "AtelierDesignRelation.source is required"),
559
+ relation: DesignRelationSchema,
560
+ note: z16.string().optional()
561
+ });
562
+ var DesignTypographySchema = z16.object({
563
+ headingFamily: z16.string().optional(),
564
+ bodyFamily: z16.string().optional(),
565
+ monoFamily: z16.string().optional(),
566
+ sizes: z16.record(z16.string(), z16.number().positive()).optional(),
567
+ weights: z16.record(z16.string(), z16.string()).optional()
568
+ });
569
+ var DesignMotionSchema = z16.object({
570
+ defaultEasing: z16.string().optional(),
571
+ defaultDuration: z16.number().int().positive().optional()
572
+ });
573
+ var AtelierDesignSchema = z16.object({
574
+ palette: z16.record(z16.string(), z16.string()).optional(),
575
+ typography: DesignTypographySchema.optional(),
576
+ motion: DesignMotionSchema.optional(),
577
+ relatedTo: z16.array(AtelierDesignRelationSchema).optional()
578
+ });
579
+ var CanvasSchema = z17.object({
580
+ width: z17.number().int().positive("Canvas width must be a positive integer"),
581
+ height: z17.number().int().positive("Canvas height must be a positive integer"),
582
+ fps: z17.number().int().positive("FPS must be a positive integer"),
583
+ background: z17.string().optional()
517
584
  });
518
- var AtelierDocumentSchema = z14.object({
519
- version: z14.string().min(1, "Version is required"),
520
- name: z14.string().min(1, "Animation name is required"),
521
- description: z14.string().optional(),
522
- tags: z14.array(z14.string()).optional(),
585
+ var ProjectKindSchema = z17.enum(["video", "image", "carousel"]);
586
+ var AtelierDocumentSchema = z17.object({
587
+ version: z17.string().min(1, "Version is required"),
588
+ name: z17.string().min(1, "Animation name is required"),
589
+ kind: ProjectKindSchema.optional(),
590
+ description: z17.string().optional(),
591
+ tags: z17.array(z17.string()).optional(),
523
592
  canvas: CanvasSchema,
524
- variables: z14.record(z14.string(), VariableSchema).optional(),
525
- assets: z14.record(z14.string(), AssetSchema).optional(),
526
- presets: z14.record(z14.string(), PresetSchema).optional(),
527
- layers: z14.array(LayerSchema),
528
- states: z14.record(z14.string(), StateSchema)
593
+ /** Per-file design / brand block (self-contained — no auto-merge). */
594
+ design: AtelierDesignSchema.optional(),
595
+ variables: z17.record(z17.string(), VariableSchema).optional(),
596
+ assets: z17.record(z17.string(), AssetSchema).optional(),
597
+ presets: z17.record(z17.string(), PresetSchema).optional(),
598
+ layers: z17.array(LayerSchema),
599
+ states: z17.record(z17.string(), StateSchema),
600
+ /** Derived timeline summary for video-kind projects. Optional. */
601
+ timeline: TimelineSchema.optional(),
602
+ /** Carousel slides — only meaningful when kind === "carousel". */
603
+ slides: z17.array(SlideRefSchema).optional()
529
604
  });
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()
605
+ var SilencePolicySchema = z18.object({
606
+ noise: z18.string().optional(),
607
+ min_silence: z18.number().nonnegative().optional(),
608
+ default_padding_pre: z18.number().nonnegative().optional(),
609
+ default_padding_post: z18.number().nonnegative().optional(),
610
+ match_tolerance: z18.number().nonnegative().optional()
536
611
  }).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()
612
+ var CaptionStyleSchema = z18.object({
613
+ font_family: z18.string().optional(),
614
+ font_size: z18.number().positive().optional(),
615
+ font_weight: z18.union([z18.literal("normal"), z18.literal("bold"), z18.number()]).optional(),
616
+ text_align: z18.enum(["left", "center", "right"]).optional(),
617
+ color: z18.string().optional(),
618
+ y_ratio: z18.number().min(0).max(1).optional(),
619
+ width_ratio: z18.number().min(0).max(1).optional(),
620
+ fade_seconds: z18.number().nonnegative().optional()
546
621
  }).strict();
547
- var CaptionGroupingSchema = z15.object({
548
- max_words: z15.number().int().positive().optional(),
549
- pause_gap: z15.number().nonnegative().optional()
622
+ var CaptionGroupingSchema = z18.object({
623
+ max_words: z18.number().int().positive().optional(),
624
+ pause_gap: z18.number().nonnegative().optional()
550
625
  }).strict();
551
- var OverlayAnchorSchema = z15.enum([
626
+ var OverlayAnchorSchema = z18.enum([
552
627
  "top-left",
553
628
  "top-right",
554
629
  "bottom-left",
555
630
  "bottom-right"
556
631
  ]);
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()
632
+ var OverlayTextStyleSchema = z18.object({
633
+ font_family: z18.string().optional(),
634
+ font_size: z18.number().positive().optional(),
635
+ font_weight: z18.union([z18.literal("normal"), z18.literal("bold"), z18.number()]).optional(),
636
+ color: z18.string().optional()
562
637
  }).strict();
563
638
  function validatePageNumberFormat(format, ctx) {
564
639
  if (format.length === 0) {
565
640
  ctx.addIssue({
566
- code: z15.ZodIssueCode.custom,
641
+ code: z18.ZodIssueCode.custom,
567
642
  message: "format must be a non-empty string"
568
643
  });
569
644
  return;
@@ -572,7 +647,7 @@ function validatePageNumberFormat(format, ctx) {
572
647
  const close = (format.match(/\}/g) ?? []).length;
573
648
  if (open !== close) {
574
649
  ctx.addIssue({
575
- code: z15.ZodIssueCode.custom,
650
+ code: z18.ZodIssueCode.custom,
576
651
  message: `format has unbalanced braces (${open} '{' vs ${close} '}')`
577
652
  });
578
653
  return;
@@ -586,7 +661,7 @@ function validatePageNumberFormat(format, ctx) {
586
661
  const inner = m[1];
587
662
  if (!groupRule.test(inner)) {
588
663
  ctx.addIssue({
589
- code: z15.ZodIssueCode.custom,
664
+ code: z18.ZodIssueCode.custom,
590
665
  message: `format placeholder "{${inner}}" is not recognized \u2014 expected {current}, {total}, {current:0Nd}, or {total:0Nd}`
591
666
  });
592
667
  return;
@@ -596,44 +671,44 @@ function validatePageNumberFormat(format, ctx) {
596
671
  }
597
672
  if (!sawCurrent && !sawTotal) {
598
673
  ctx.addIssue({
599
- code: z15.ZodIssueCode.custom,
674
+ code: z18.ZodIssueCode.custom,
600
675
  message: "format must contain at least one {current} or {total} placeholder"
601
676
  });
602
677
  }
603
678
  }
604
- var OverlayHandleRuleSchema = z15.object({
605
- text: z15.string().min(1),
679
+ var OverlayHandleRuleSchema = z18.object({
680
+ text: z18.string().min(1),
606
681
  anchor: OverlayAnchorSchema,
607
- margin: z15.number().nonnegative().optional(),
682
+ margin: z18.number().nonnegative().optional(),
608
683
  style: OverlayTextStyleSchema.optional()
609
684
  }).strict();
610
- var OverlayPageNumberRuleSchema = z15.object({
611
- format: z15.string().superRefine(validatePageNumberFormat),
685
+ var OverlayPageNumberRuleSchema = z18.object({
686
+ format: z18.string().superRefine(validatePageNumberFormat),
612
687
  anchor: OverlayAnchorSchema,
613
- margin: z15.number().nonnegative().optional(),
688
+ margin: z18.number().nonnegative().optional(),
614
689
  style: OverlayTextStyleSchema.optional()
615
690
  }).strict();
616
- var OverlayRulesSchema = z15.object({
691
+ var OverlayRulesSchema = z18.object({
617
692
  handle: OverlayHandleRuleSchema.optional(),
618
693
  page_number: OverlayPageNumberRuleSchema.optional()
619
694
  }).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(),
695
+ var StudioRecipeSchema = z18.object({
696
+ version: z18.string(),
697
+ name: z18.string(),
698
+ description: z18.string().optional(),
699
+ author: z18.string().optional(),
700
+ tags: z18.array(z18.string()).optional(),
626
701
  silence_policy: SilencePolicySchema.optional(),
627
702
  caption_style: CaptionStyleSchema.optional(),
628
703
  caption_grouping: CaptionGroupingSchema.optional(),
629
704
  // Phase 1.5 — first-class overlay rules
630
705
  overlay_rules: OverlayRulesSchema.optional(),
631
706
  // 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()
707
+ caption_highlight: z18.unknown().optional(),
708
+ transition_kit: z18.unknown().optional(),
709
+ palette: z18.unknown().optional(),
710
+ audio_policy: z18.unknown().optional(),
711
+ aspect_targets: z18.array(z18.unknown()).optional()
637
712
  }).strict();
638
713
  var RESERVED_RECIPE_FIELDS = [
639
714
  "caption_highlight",
@@ -675,10 +750,10 @@ function parseAtelier(yamlString) {
675
750
  let parsed;
676
751
  try {
677
752
  parsed = yamlParse(yamlString);
678
- } catch (err20) {
753
+ } catch (err24) {
679
754
  return {
680
755
  success: false,
681
- errors: [{ path: "(yaml)", message: `YAML parse error: ${err20.message}` }]
756
+ errors: [{ path: "(yaml)", message: `YAML parse error: ${err24.message}` }]
682
757
  };
683
758
  }
684
759
  return validateDocument(parsed);
@@ -686,6 +761,290 @@ function parseAtelier(yamlString) {
686
761
  function serializeAtelier(doc) {
687
762
  return yamlStringify(doc, { indent: 2 });
688
763
  }
764
+ var SlotKindSchema = z19.enum([
765
+ "image",
766
+ "video",
767
+ "audio",
768
+ "text",
769
+ "font",
770
+ "voice",
771
+ "logo"
772
+ ]);
773
+ var SLOT_KINDS = SlotKindSchema.options;
774
+ function parseSlotRef(ref) {
775
+ if (typeof ref !== "string" || ref.length === 0) return null;
776
+ const segments = ref.split(".");
777
+ if (segments.length < 2) return null;
778
+ if (segments.some((s) => s.length === 0)) return null;
779
+ const [rawKind, category, ...rest] = segments;
780
+ const kindParsed = SlotKindSchema.safeParse(rawKind);
781
+ if (!kindParsed.success) return null;
782
+ const specifier = rest.join(".");
783
+ return { kind: kindParsed.data, category, specifier };
784
+ }
785
+ var SlotRefSchema = z19.string().superRefine((value, ctx) => {
786
+ const parsed = parseSlotRef(value);
787
+ if (parsed !== null) return;
788
+ const segments = value.split(".");
789
+ if (segments.length < 2) {
790
+ ctx.addIssue({
791
+ code: z19.ZodIssueCode.custom,
792
+ message: `slot reference "${value}" must have at least two segments (<kind>.<open-tail>)`
793
+ });
794
+ return;
795
+ }
796
+ if (segments.some((s) => s.length === 0)) {
797
+ ctx.addIssue({
798
+ code: z19.ZodIssueCode.custom,
799
+ message: `slot reference "${value}" has an empty segment`
800
+ });
801
+ return;
802
+ }
803
+ const kindOk = SlotKindSchema.safeParse(segments[0]).success;
804
+ if (!kindOk) {
805
+ ctx.addIssue({
806
+ code: z19.ZodIssueCode.custom,
807
+ message: `slot reference "${value}" has unknown kind "${segments[0]}" \u2014 expected one of ${SLOT_KINDS.join(", ")}`
808
+ });
809
+ return;
810
+ }
811
+ ctx.addIssue({
812
+ code: z19.ZodIssueCode.custom,
813
+ message: `slot reference "${value}" is malformed`
814
+ });
815
+ });
816
+ var TechniqueLibrarySchema = z20.object({
817
+ version: z20.number().int().positive(),
818
+ techniques: z20.array(z20.string().min(1))
819
+ }).strict();
820
+ var DesignAudienceSchema = z21.object({
821
+ primary: z21.string().min(1),
822
+ secondary: z21.string().optional(),
823
+ platform: z21.array(z21.string().min(1)).min(1)
824
+ }).strict();
825
+ var DesignVoiceSchema = z21.object({
826
+ descriptors: z21.array(z21.string().min(1)).min(3).max(5),
827
+ references: z21.array(z21.string().min(1)).optional()
828
+ }).strict();
829
+ var PaletteRoleSchema = z21.enum([
830
+ "background",
831
+ "surface",
832
+ "primary",
833
+ "accent",
834
+ "text-on-dark",
835
+ "text-on-light"
836
+ ]);
837
+ var PaletteEntrySchema = z21.object({
838
+ token: z21.string().min(1),
839
+ hex: z21.string().regex(/^#([0-9A-Fa-f]{3}|[0-9A-Fa-f]{6}|[0-9A-Fa-f]{8})$/, {
840
+ message: "hex must be a #RGB / #RRGGBB / #RRGGBBAA string"
841
+ }),
842
+ role: PaletteRoleSchema
843
+ }).strict();
844
+ var TypographyUsageSchema = z21.enum(["display", "body", "caption", "mono"]);
845
+ var TypographyEntrySchema = z21.object({
846
+ token: z21.string().min(1),
847
+ family: z21.string().min(1),
848
+ weights: z21.array(z21.number().int().min(1).max(1e3)).min(1),
849
+ usage: TypographyUsageSchema
850
+ }).strict();
851
+ var MotionRegisterSchema = z21.object({
852
+ tempo: z21.enum(["snappy", "steady", "calm", "languid"]),
853
+ easing_bias: z21.enum(["linear", "ease-out", "spring"]),
854
+ camera_bias: z21.enum(["static", "drift", "snap"]).optional()
855
+ }).strict();
856
+ var VisualRegisterSchema = z21.object({
857
+ palette: z21.array(PaletteEntrySchema).min(1),
858
+ typography: z21.array(TypographyEntrySchema).min(1),
859
+ motion_register: MotionRegisterSchema
860
+ }).strict();
861
+ var AspectRatioSchema = z21.enum(["9:16", "1:1", "16:9", "4:5"]);
862
+ var DesignConstraintsSchema = z21.object({
863
+ max_duration_seconds: z21.number().positive().optional(),
864
+ aspect_ratios: z21.array(AspectRatioSchema).min(1),
865
+ do_not: z21.array(z21.string().min(1))
866
+ }).strict();
867
+ var BrandLogoSchema = z21.object({
868
+ token: z21.string().min(1),
869
+ role: z21.enum(["primary", "mark", "wordmark"])
870
+ }).strict();
871
+ var BrandHandleSchema = z21.object({
872
+ platform: z21.string().min(1),
873
+ value: z21.string().min(1)
874
+ }).strict();
875
+ var BrandReferencesSchema = z21.object({
876
+ logos: z21.array(BrandLogoSchema).min(1),
877
+ handles: z21.array(BrandHandleSchema).optional(),
878
+ page_number_convention: z21.string().optional()
879
+ }).strict();
880
+ var DesignVarianceSchema = z21.object({
881
+ field: z21.string().min(1),
882
+ value: z21.unknown(),
883
+ reason: z21.string().min(1)
884
+ }).strict();
885
+ var DesignArtifactSchema = z21.object({
886
+ audience: DesignAudienceSchema,
887
+ voice: DesignVoiceSchema,
888
+ visual_register: VisualRegisterSchema,
889
+ constraints: DesignConstraintsSchema,
890
+ brand_references: BrandReferencesSchema,
891
+ variances: z21.array(DesignVarianceSchema).optional(),
892
+ /** Markdown body verbatim — attached by the parser, not part of frontmatter. */
893
+ body: z21.string()
894
+ }).strict();
895
+ var BEAT_ID_RE = /^[a-z][a-z0-9-]*(\.[a-z0-9][a-z0-9-]*)*$/;
896
+ var ScriptBeatIdSchema = z22.string().regex(BEAT_ID_RE, {
897
+ message: "beat id must be kebab-case (optionally dot-namespaced, e.g. `proof-1` or `b-roll.intro`)"
898
+ });
899
+ var NarratedBeatSchema = z22.object({
900
+ id: ScriptBeatIdSchema,
901
+ copy: z22.string().min(1),
902
+ intent: z22.string().min(1),
903
+ est_duration_s: z22.number().nonnegative(),
904
+ bind_to_transcript: z22.boolean().default(true)
905
+ }).strict();
906
+ var NonNarratedBeatSchema = z22.object({
907
+ id: ScriptBeatIdSchema,
908
+ copy: z22.string().min(1),
909
+ intent: z22.string().min(1),
910
+ est_duration_s: z22.number().nonnegative()
911
+ }).strict();
912
+ var NarratedBlocksSchema = z22.object({
913
+ hook: z22.array(NarratedBeatSchema).min(1),
914
+ story: z22.array(NarratedBeatSchema),
915
+ proof: z22.array(NarratedBeatSchema),
916
+ cta: z22.array(NarratedBeatSchema).min(1)
917
+ }).strict();
918
+ var NarratedScriptSchema = z22.object({
919
+ mode: z22.literal("narrated"),
920
+ target_duration_s: z22.number().positive(),
921
+ language: z22.string().min(1),
922
+ /** Optional slot reference for TTS voice (e.g. `voice.host.primary`). */
923
+ tts_voice: SlotRefSchema.optional(),
924
+ blocks: NarratedBlocksSchema,
925
+ /** Markdown body verbatim — attached by the parser when the body has stray prose. */
926
+ body: z22.string()
927
+ }).strict();
928
+ var CarouselScriptSchema = z22.object({
929
+ mode: z22.literal("carousel"),
930
+ target_duration_s: z22.number().positive(),
931
+ language: z22.string().min(1),
932
+ tts_voice: SlotRefSchema.optional(),
933
+ beats: z22.array(NonNarratedBeatSchema).min(1),
934
+ body: z22.string()
935
+ }).strict();
936
+ var TextOnlyScriptSchema = z22.object({
937
+ mode: z22.literal("text-only"),
938
+ target_duration_s: z22.number().positive(),
939
+ language: z22.string().min(1),
940
+ tts_voice: SlotRefSchema.optional(),
941
+ beats: z22.array(NonNarratedBeatSchema).min(1),
942
+ body: z22.string()
943
+ }).strict();
944
+ var ScriptArtifactSchema = z22.discriminatedUnion("mode", [
945
+ NarratedScriptSchema,
946
+ CarouselScriptSchema,
947
+ TextOnlyScriptSchema
948
+ ]);
949
+ var BeatWindowSchema = z23.object({
950
+ start_s: z23.number().nonnegative(),
951
+ end_s: z23.number().nonnegative()
952
+ }).strict().refine((w) => w.end_s > w.start_s, {
953
+ message: "window.end_s must be > window.start_s"
954
+ });
955
+ var CameraSchema = z23.object({
956
+ kind: z23.enum(["static", "drift", "push", "pull", "whip"]),
957
+ subject: z23.string().min(1).optional()
958
+ }).strict();
959
+ var MoodSchema = z23.string().min(1);
960
+ var TransitionKindSchema = z23.enum(["cut", "dip-to-color", "whip", "crossfade"]);
961
+ var TransitionOutSchema = z23.object({
962
+ kind: TransitionKindSchema,
963
+ duration_s: z23.number().nonnegative().optional(),
964
+ color: z23.string().min(1).optional()
965
+ }).strict();
966
+ var SfxEntrySchema = z23.object({
967
+ slot: SlotRefSchema,
968
+ gain_db: z23.number()
969
+ }).strict();
970
+ var StoryboardBeatSchema = z23.object({
971
+ id: ScriptBeatIdSchema,
972
+ window: BeatWindowSchema,
973
+ mood: z23.array(MoodSchema).min(1),
974
+ camera: CameraSchema,
975
+ slots: z23.array(SlotRefSchema),
976
+ techniques: z23.array(z23.string().min(1)),
977
+ transition_out: TransitionOutSchema,
978
+ sfx: z23.array(SfxEntrySchema).optional()
979
+ }).strict();
980
+ var StoryboardAspectRatioSchema = z23.enum(["9:16", "1:1", "16:9", "4:5"]);
981
+ var StoryboardArtifactSchema = z23.object({
982
+ technique_library_version: z23.number().int().positive(),
983
+ aspect_ratio: StoryboardAspectRatioSchema,
984
+ beats: z23.array(StoryboardBeatSchema).min(1)
985
+ }).strict();
986
+ var WorkspaceManifestSchema = z24.object({
987
+ /** Manifest schema version. Pinned to '1.0' for the initial release. */
988
+ version: z24.literal("1.0"),
989
+ /** Display name; defaults to the workspace dir basename at create time. */
990
+ name: z24.string().min(1),
991
+ /** ISO 8601 timestamp set by the writer. */
992
+ created: z24.string().min(1),
993
+ /**
994
+ * Inherited by new projects created via `atelier init` inside this
995
+ * workspace. The creator pre-declares the autonomy posture once at the
996
+ * workspace level; per-project overrides still allowed via per-project
997
+ * `.atelier/learning-mode.yaml` (writes always happen at workspace level
998
+ * unless explicitly overridden).
999
+ */
1000
+ default_mode: z24.enum(["ambient", "explicit"]),
1001
+ /**
1002
+ * Relative paths (single-segment subdir names) of known Projects inside
1003
+ * the workspace. Advisory only — on-disk `project.atelier` presence is
1004
+ * authoritative. Refresh via `listProjects(workspaceDir)`.
1005
+ */
1006
+ projects: z24.array(z24.string().min(1)).optional(),
1007
+ /** Free-text creator description. Optional. */
1008
+ description: z24.string().optional()
1009
+ }).strict();
1010
+ var NonEmptyStringListSchema = z25.array(z25.string().min(1));
1011
+ var MediaNotesConstraintsSchema = z25.object({
1012
+ /**
1013
+ * Hard exclusions. Lux + Iris MUST refuse to bind this media to a slot if
1014
+ * any do_not rule applies (the rule is a free-text string the agent reads
1015
+ * with judgement — e.g. "never use in a CTA beat", "no client logos").
1016
+ */
1017
+ do_not: NonEmptyStringListSchema.optional(),
1018
+ /**
1019
+ * Soft preferences — weight up candidacy when these conditions hold. Same
1020
+ * free-text shape as do_not but the polarity is positive ("good for hooks",
1021
+ * "prefer for warm tone").
1022
+ */
1023
+ prefer_for: NonEmptyStringListSchema.optional()
1024
+ }).strict();
1025
+ var MediaNotesFrontmatterSchema = z25.object({
1026
+ /** Basename of the media file this notes file sits next to. */
1027
+ file: z25.string().min(1),
1028
+ /** ISO 8601 timestamp captured at ingest. */
1029
+ added: z25.string().min(1),
1030
+ /** User identifier (env $USER at ingest, "creator" fallback). */
1031
+ added_by: z25.string().min(1),
1032
+ /** Free-form labels. Empty by default; agents and creator both append. */
1033
+ tags: NonEmptyStringListSchema.default([]),
1034
+ /** Optional do_not / prefer_for object. Absent means "no rules yet." */
1035
+ constraints: MediaNotesConstraintsSchema.optional()
1036
+ }).strict();
1037
+ var MediaNotesSchema = z25.object({
1038
+ frontmatter: MediaNotesFrontmatterSchema,
1039
+ body: z25.string()
1040
+ }).strict();
1041
+ var MediaNotesAuthorSchema = z25.enum(["lux", "quill", "iris", "creator"]);
1042
+ var MEDIA_NOTES_HEADINGS = {
1043
+ lux: "## Lux notes (cinematographer)",
1044
+ quill: "## Quill notes (showrunner)",
1045
+ iris: "## Iris notes (composer)",
1046
+ creator: "## Creator notes"
1047
+ };
689
1048
 
690
1049
  // ../mcp/src/tools/document.ts
691
1050
  function getDoc(store, id) {
@@ -704,13 +1063,13 @@ function register(server, store) {
704
1063
  "atelier_create",
705
1064
  "Create a new Atelier animation document with canvas settings",
706
1065
  {
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")
1066
+ name: z26.string().describe("Animation name"),
1067
+ width: z26.number().positive().describe("Canvas width in pixels"),
1068
+ height: z26.number().positive().describe("Canvas height in pixels"),
1069
+ fps: z26.number().positive().int().describe("Frames per second"),
1070
+ background: z26.string().optional().describe("Background color (hex string)"),
1071
+ description: z26.string().optional().describe("Animation description"),
1072
+ tags: z26.array(z26.string()).optional().describe("Tags for categorization")
714
1073
  },
715
1074
  { readOnlyHint: false, destructiveHint: false },
716
1075
  async ({ name, width, height, fps, background, description, tags }) => {
@@ -735,7 +1094,7 @@ function register(server, store) {
735
1094
  "atelier_info",
736
1095
  "Get summary information about an Atelier document",
737
1096
  {
738
- id: z16.string().describe("Document ID")
1097
+ id: z26.string().describe("Document ID")
739
1098
  },
740
1099
  { readOnlyHint: true, destructiveHint: false },
741
1100
  async ({ id }) => {
@@ -763,8 +1122,8 @@ function register(server, store) {
763
1122
  "atelier_load",
764
1123
  "Load an Atelier document from a YAML string",
765
1124
  {
766
- id: z16.string().optional().describe("Custom document ID (auto-generated if omitted)"),
767
- yaml: z16.string().describe("YAML string representing an Atelier document")
1125
+ id: z26.string().optional().describe("Custom document ID (auto-generated if omitted)"),
1126
+ yaml: z26.string().describe("YAML string representing an Atelier document")
768
1127
  },
769
1128
  { readOnlyHint: false, destructiveHint: false },
770
1129
  async ({ id, yaml }) => {
@@ -780,7 +1139,7 @@ function register(server, store) {
780
1139
  "atelier_export",
781
1140
  "Export an Atelier document as a YAML string",
782
1141
  {
783
- id: z16.string().describe("Document ID")
1142
+ id: z26.string().describe("Document ID")
784
1143
  },
785
1144
  { readOnlyHint: true, destructiveHint: false },
786
1145
  async ({ id }) => {
@@ -803,7 +1162,7 @@ function register(server, store) {
803
1162
  }
804
1163
 
805
1164
  // ../mcp/src/tools/layers.ts
806
- import { z as z17 } from "zod";
1165
+ import { z as z27 } from "zod";
807
1166
  function getDoc2(store, id) {
808
1167
  const doc = store.get(id);
809
1168
  if (!doc) return { error: `Document "${id}" not found` };
@@ -815,69 +1174,79 @@ function ok2(data) {
815
1174
  function err2(message) {
816
1175
  return { content: [{ type: "text", text: JSON.stringify({ error: message }) }], isError: true };
817
1176
  }
818
- var VisualInputSchema = z17.object({
819
- type: z17.enum(["shape", "text", "image", "group", "ref"]).describe("Visual type"),
1177
+ var VisualInputSchema = z27.object({
1178
+ type: z27.enum(["shape", "text", "image", "video", "audio", "group", "ref"]).describe("Visual type"),
820
1179
  // shape visual fields
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()
1180
+ shape: z27.object({
1181
+ type: z27.enum(["rect", "ellipse", "path"]),
1182
+ cornerRadius: z27.union([z27.number(), z27.tuple([z27.number(), z27.number(), z27.number(), z27.number()])]).optional(),
1183
+ points: z27.array(z27.object({
1184
+ x: z27.number(),
1185
+ y: z27.number(),
1186
+ in: z27.object({ x: z27.number(), y: z27.number() }).optional(),
1187
+ out: z27.object({ x: z27.number(), y: z27.number() }).optional()
829
1188
  })).optional(),
830
- closed: z17.boolean().optional()
1189
+ closed: z27.boolean().optional()
831
1190
  }).optional(),
832
- fill: z17.record(z17.unknown()).optional(),
833
- stroke: z17.record(z17.unknown()).optional(),
1191
+ fill: z27.record(z27.unknown()).optional(),
1192
+ stroke: z27.record(z27.unknown()).optional(),
834
1193
  // text visual fields
835
- content: z17.string().optional(),
836
- style: z17.record(z17.unknown()).optional(),
1194
+ content: z27.string().optional(),
1195
+ style: z27.record(z27.unknown()).optional(),
837
1196
  // image visual fields
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()
1197
+ assetId: z27.string().optional(),
1198
+ sourceRect: z27.object({
1199
+ x: z27.number(),
1200
+ y: z27.number(),
1201
+ width: z27.number().positive(),
1202
+ height: z27.number().positive()
844
1203
  }).optional(),
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()
1204
+ spritesheet: z27.object({
1205
+ columns: z27.number().int().positive(),
1206
+ rows: z27.number().int().positive(),
1207
+ frameCount: z27.number().int().positive().optional(),
1208
+ frameWidth: z27.number().positive(),
1209
+ frameHeight: z27.number().positive()
851
1210
  }).optional(),
852
- frameIndex: z17.number().int().min(0).optional(),
1211
+ frameIndex: z27.number().int().min(0).optional(),
853
1212
  // ref visual fields
854
- src: z17.string().optional(),
855
- state: z17.string().optional(),
856
- frame: z17.number().int().min(0).optional()
1213
+ src: z27.string().optional(),
1214
+ state: z27.string().optional(),
1215
+ frame: z27.number().int().min(0).optional(),
1216
+ // video / audio visual fields (v1.0+)
1217
+ startFrame: z27.number().int().min(0).optional().describe("Video/audio: composition frame when playback begins"),
1218
+ sourceOffset: z27.number().min(0).optional().describe("Video/audio: seconds into source to begin playback"),
1219
+ sourceEnd: z27.number().positive().optional().describe("Video/audio: seconds of source to stop at"),
1220
+ playbackRate: z27.number().positive().optional().describe("Video/audio: playback speed multiplier"),
1221
+ volume: z27.number().min(0).max(1).optional().describe("Video/audio: volume 0\u20131"),
1222
+ muted: z27.boolean().optional().describe("Video/audio: mute"),
1223
+ objectFit: z27.enum(["contain", "cover", "fill"]).optional().describe("Video: fit mode"),
1224
+ fadeIn: z27.number().min(0).optional().describe("Audio: fade-in duration in seconds"),
1225
+ fadeOut: z27.number().min(0).optional().describe("Audio: fade-out duration in seconds")
857
1226
  }).describe("Visual content definition");
858
1227
  function register2(server, store) {
859
1228
  server.tool(
860
1229
  "atelier_add_layer",
861
1230
  "Add a new layer to an Atelier document",
862
1231
  {
863
- id: z17.string().describe("Document ID"),
864
- layerId: z17.string().describe("Unique layer ID"),
1232
+ id: z27.string().describe("Document ID"),
1233
+ layerId: z27.string().describe("Unique layer ID"),
865
1234
  visual: VisualInputSchema.describe("Visual content definition"),
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)")
1235
+ x: z27.union([z27.number(), z27.string()]).describe("X position (pixels or percentage)"),
1236
+ y: z27.union([z27.number(), z27.string()]).describe("Y position (pixels or percentage)"),
1237
+ width: z27.union([z27.number(), z27.string()]).describe("Width (pixels or percentage)"),
1238
+ height: z27.union([z27.number(), z27.string()]).describe("Height (pixels or percentage)"),
1239
+ description: z27.string().optional().describe("Layer description"),
1240
+ tags: z27.array(z27.string()).optional().describe("Tags"),
1241
+ opacity: z27.number().min(0).max(1).optional().describe("Opacity 0-1"),
1242
+ rotation: z27.number().optional().describe("Rotation in degrees"),
1243
+ parentId: z27.string().optional().describe("Parent layer ID for transform inheritance"),
1244
+ anchorPoint: z27.object({ x: z27.number(), y: z27.number() }).optional().describe("Anchor point (0-1 normalized)"),
1245
+ scale: z27.object({ x: z27.number(), y: z27.number() }).optional().describe("Scale factors"),
1246
+ visible: z27.boolean().optional().describe("Whether layer is visible"),
1247
+ tint: z27.object({
1248
+ color: z27.string().describe("Tint color (hex string)"),
1249
+ amount: z27.number().min(0).max(1).describe("Tint amount (0 = none, 1 = full)")
881
1250
  }).optional().describe("Color tint overlay")
882
1251
  },
883
1252
  { readOnlyHint: false, destructiveHint: false },
@@ -914,20 +1283,20 @@ function register2(server, store) {
914
1283
  "atelier_edit_layer",
915
1284
  "Edit properties of an existing layer",
916
1285
  {
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")
1286
+ id: z27.string().describe("Document ID"),
1287
+ layerId: z27.string().describe("Layer ID to edit"),
1288
+ x: z27.union([z27.number(), z27.string()]).optional().describe("New X position"),
1289
+ y: z27.union([z27.number(), z27.string()]).optional().describe("New Y position"),
1290
+ width: z27.union([z27.number(), z27.string()]).optional().describe("New width"),
1291
+ height: z27.union([z27.number(), z27.string()]).optional().describe("New height"),
1292
+ description: z27.string().optional().describe("New description"),
1293
+ tags: z27.array(z27.string()).optional().describe("New tags"),
1294
+ opacity: z27.number().min(0).max(1).optional().describe("New opacity"),
1295
+ rotation: z27.number().optional().describe("New rotation"),
1296
+ parentId: z27.string().nullable().optional().describe("New parent layer ID (null to clear)"),
1297
+ anchorPoint: z27.object({ x: z27.number(), y: z27.number() }).optional().describe("New anchor point"),
1298
+ scale: z27.object({ x: z27.number(), y: z27.number() }).optional().describe("New scale"),
1299
+ visible: z27.boolean().optional().describe("New visibility")
931
1300
  },
932
1301
  { readOnlyHint: false, destructiveHint: false },
933
1302
  async ({ id, layerId, x, y, width, height, description, tags, opacity, rotation, parentId, anchorPoint, scale, visible }) => {
@@ -962,8 +1331,8 @@ function register2(server, store) {
962
1331
  "atelier_remove_layer",
963
1332
  "Remove a layer from an Atelier document",
964
1333
  {
965
- id: z17.string().describe("Document ID"),
966
- layerId: z17.string().describe("Layer ID to remove")
1334
+ id: z27.string().describe("Document ID"),
1335
+ layerId: z27.string().describe("Layer ID to remove")
967
1336
  },
968
1337
  { readOnlyHint: false, destructiveHint: true },
969
1338
  async ({ id, layerId }) => {
@@ -991,7 +1360,7 @@ function register2(server, store) {
991
1360
  "atelier_list_layers",
992
1361
  "List all layers in an Atelier document",
993
1362
  {
994
- id: z17.string().describe("Document ID")
1363
+ id: z27.string().describe("Document ID")
995
1364
  },
996
1365
  { readOnlyHint: true, destructiveHint: false },
997
1366
  async ({ id }) => {
@@ -1018,9 +1387,9 @@ function register2(server, store) {
1018
1387
  "atelier_reorder",
1019
1388
  "Move a layer to a new position in the layer stack",
1020
1389
  {
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)")
1390
+ id: z27.string().describe("Document ID"),
1391
+ layerId: z27.string().describe("Layer ID to move"),
1392
+ position: z27.number().int().min(0).describe("Target position index (0-based)")
1024
1393
  },
1025
1394
  { readOnlyHint: false, destructiveHint: false },
1026
1395
  async ({ id, layerId, position }) => {
@@ -1038,7 +1407,7 @@ function register2(server, store) {
1038
1407
  }
1039
1408
 
1040
1409
  // ../mcp/src/tools/shapes.ts
1041
- import { z as z18 } from "zod";
1410
+ import { z as z28 } from "zod";
1042
1411
  function getDoc3(store, id) {
1043
1412
  const doc = store.get(id);
1044
1413
  if (!doc) return { error: `Document "${id}" not found` };
@@ -1055,21 +1424,21 @@ function register3(server, store) {
1055
1424
  "atelier_set_shape",
1056
1425
  "Set the shape on a shape-type layer",
1057
1426
  {
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()])
1427
+ id: z28.string().describe("Document ID"),
1428
+ layerId: z28.string().describe("Layer ID (must be a shape visual)"),
1429
+ shape: z28.object({
1430
+ type: z28.enum(["rect", "ellipse", "path"]).describe("Shape type"),
1431
+ cornerRadius: z28.union([
1432
+ z28.number(),
1433
+ z28.tuple([z28.number(), z28.number(), z28.number(), z28.number()])
1065
1434
  ]).optional().describe("Corner radius (rect only)"),
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()
1435
+ points: z28.array(z28.object({
1436
+ x: z28.number(),
1437
+ y: z28.number(),
1438
+ in: z28.object({ x: z28.number(), y: z28.number() }).optional(),
1439
+ out: z28.object({ x: z28.number(), y: z28.number() }).optional()
1071
1440
  })).optional().describe("Path points (path only)"),
1072
- closed: z18.boolean().optional().describe("Whether path is closed (path only)")
1441
+ closed: z28.boolean().optional().describe("Whether path is closed (path only)")
1073
1442
  }).describe("Shape definition")
1074
1443
  },
1075
1444
  { readOnlyHint: false, destructiveHint: false },
@@ -1088,20 +1457,20 @@ function register3(server, store) {
1088
1457
  "atelier_set_fill",
1089
1458
  "Set the fill on a shape-type layer",
1090
1459
  {
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()])
1460
+ id: z28.string().describe("Document ID"),
1461
+ layerId: z28.string().describe("Layer ID (must be a shape visual)"),
1462
+ fill: z28.object({
1463
+ type: z28.enum(["solid", "linear-gradient", "radial-gradient"]).describe("Fill type"),
1464
+ color: z28.unknown().optional().describe("Color for solid fill (hex string or RGBA/HSLA object)"),
1465
+ angle: z28.number().optional().describe("Angle in degrees (linear-gradient only)"),
1466
+ center: z28.object({
1467
+ x: z28.union([z28.number(), z28.string()]),
1468
+ y: z28.union([z28.number(), z28.string()])
1100
1469
  }).optional().describe("Center point (radial-gradient only)"),
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()
1470
+ radius: z28.union([z28.number(), z28.string()]).optional().describe("Radius (radial-gradient only)"),
1471
+ stops: z28.array(z28.object({
1472
+ offset: z28.number().min(0).max(1),
1473
+ color: z28.unknown()
1105
1474
  })).optional().describe("Gradient stops")
1106
1475
  }).describe("Fill definition")
1107
1476
  },
@@ -1121,14 +1490,14 @@ function register3(server, store) {
1121
1490
  "atelier_set_stroke",
1122
1491
  "Set the stroke on a shape-type layer",
1123
1492
  {
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")
1493
+ id: z28.string().describe("Document ID"),
1494
+ layerId: z28.string().describe("Layer ID (must be a shape visual)"),
1495
+ stroke: z28.object({
1496
+ color: z28.unknown().describe("Stroke color (hex string or RGBA/HSLA object)"),
1497
+ width: z28.number().positive().describe("Stroke width in pixels"),
1498
+ dash: z28.array(z28.number()).optional().describe("Dash pattern"),
1499
+ lineCap: z28.enum(["butt", "round", "square"]).optional().describe("Line cap style"),
1500
+ lineJoin: z28.enum(["miter", "round", "bevel"]).optional().describe("Line join style")
1132
1501
  }).describe("Stroke definition")
1133
1502
  },
1134
1503
  { readOnlyHint: false, destructiveHint: false },
@@ -1146,7 +1515,7 @@ function register3(server, store) {
1146
1515
  }
1147
1516
 
1148
1517
  // ../mcp/src/tools/states.ts
1149
- import { z as z19 } from "zod";
1518
+ import { z as z29 } from "zod";
1150
1519
  function getDoc4(store, id) {
1151
1520
  const doc = store.get(id);
1152
1521
  if (!doc) return { error: `Document "${id}" not found` };
@@ -1163,11 +1532,11 @@ function register4(server, store) {
1163
1532
  "atelier_add_state",
1164
1533
  "Add a named animation state to a document",
1165
1534
  {
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")
1535
+ id: z29.string().describe("Document ID"),
1536
+ stateName: z29.string().describe("State name (e.g. 'intro', 'hover', 'exit')"),
1537
+ duration: z29.number().positive().int().describe("Duration in frames"),
1538
+ description: z29.string().optional().describe("State description"),
1539
+ tags: z29.array(z29.string()).optional().describe("Tags")
1171
1540
  },
1172
1541
  { readOnlyHint: false, destructiveHint: false },
1173
1542
  async ({ id, stateName, duration, description, tags }) => {
@@ -1190,11 +1559,11 @@ function register4(server, store) {
1190
1559
  "atelier_edit_state",
1191
1560
  "Edit metadata of an existing animation state",
1192
1561
  {
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")
1562
+ id: z29.string().describe("Document ID"),
1563
+ stateName: z29.string().describe("State name to edit"),
1564
+ duration: z29.number().positive().int().optional().describe("New duration in frames"),
1565
+ description: z29.string().optional().describe("New description"),
1566
+ tags: z29.array(z29.string()).optional().describe("New tags")
1198
1567
  },
1199
1568
  { readOnlyHint: false, destructiveHint: false },
1200
1569
  async ({ id, stateName, duration, description, tags }) => {
@@ -1213,8 +1582,8 @@ function register4(server, store) {
1213
1582
  "atelier_remove_state",
1214
1583
  "Remove an animation state and all its deltas",
1215
1584
  {
1216
- id: z19.string().describe("Document ID"),
1217
- stateName: z19.string().describe("State name to remove")
1585
+ id: z29.string().describe("Document ID"),
1586
+ stateName: z29.string().describe("State name to remove")
1218
1587
  },
1219
1588
  { readOnlyHint: false, destructiveHint: true },
1220
1589
  async ({ id, stateName }) => {
@@ -1231,7 +1600,7 @@ function register4(server, store) {
1231
1600
  "atelier_list_states",
1232
1601
  "List all animation states in a document",
1233
1602
  {
1234
- id: z19.string().describe("Document ID")
1603
+ id: z29.string().describe("Document ID")
1235
1604
  },
1236
1605
  { readOnlyHint: true, destructiveHint: false },
1237
1606
  async ({ id }) => {
@@ -1251,7 +1620,7 @@ function register4(server, store) {
1251
1620
  }
1252
1621
 
1253
1622
  // ../mcp/src/tools/deltas.ts
1254
- import { z as z20 } from "zod";
1623
+ import { z as z30 } from "zod";
1255
1624
 
1256
1625
  // ../math/dist/index.js
1257
1626
  function linear(t) {
@@ -1796,12 +2165,15 @@ function resolveFrame(doc, stateName, frame, overrideDeltas) {
1796
2165
  throw new Error(`State "${stateName}" not found in document "${doc.name}"`);
1797
2166
  }
1798
2167
  const deltasByLayerProperty = groupDeltas(overrideDeltas ?? state.deltas);
2168
+ const ancestorClipStartFrame = computeAncestorClipStartFrames(doc);
1799
2169
  const resolvedLayers = doc.layers.map((layer) => {
1800
2170
  const computedProperties = {};
1801
2171
  const layerDeltas = deltasByLayerProperty.get(layer.id);
1802
2172
  if (layerDeltas) {
2173
+ const offset = ancestorClipStartFrame.get(layer.id) ?? 0;
2174
+ const localFrame = frame - offset;
1803
2175
  for (const [property, deltas] of layerDeltas) {
1804
- const value = resolvePropertyAtFrame(deltas, frame);
2176
+ const value = resolvePropertyAtFrame(deltas, localFrame);
1805
2177
  if (value !== void 0) {
1806
2178
  computedProperties[property] = value;
1807
2179
  }
@@ -1822,6 +2194,26 @@ function resolveFrame(doc, stateName, frame, overrideDeltas) {
1822
2194
  });
1823
2195
  return { frame, stateName, layers: resolvedLayers };
1824
2196
  }
2197
+ function computeAncestorClipStartFrames(doc) {
2198
+ const byId = /* @__PURE__ */ new Map();
2199
+ for (const l of doc.layers) byId.set(l.id, l);
2200
+ const result = /* @__PURE__ */ new Map();
2201
+ for (const layer of doc.layers) {
2202
+ let cursor = byId.get(layer.parentId ?? "");
2203
+ const visited = /* @__PURE__ */ new Set();
2204
+ while (cursor) {
2205
+ if (visited.has(cursor.id)) break;
2206
+ visited.add(cursor.id);
2207
+ if (cursor.visual.type === "video") {
2208
+ const v = cursor.visual;
2209
+ result.set(layer.id, v.startFrame ?? 0);
2210
+ break;
2211
+ }
2212
+ cursor = byId.get(cursor.parentId ?? "");
2213
+ }
2214
+ }
2215
+ return result;
2216
+ }
1825
2217
  function groupDeltas(deltas) {
1826
2218
  const map = /* @__PURE__ */ new Map();
1827
2219
  for (const delta of deltas) {
@@ -1990,17 +2382,17 @@ function register5(server, store) {
1990
2382
  "atelier_add_delta",
1991
2383
  "Add an animation delta (keyframe transition) to a state",
1992
2384
  {
1993
- id: z20.string().describe("Document ID"),
1994
- stateName: z20.string().describe("State name"),
1995
- layer: z20.string().describe("Target layer ID"),
2385
+ id: z30.string().describe("Document ID"),
2386
+ stateName: z30.string().describe("State name"),
2387
+ layer: z30.string().describe("Target layer ID"),
1996
2388
  property: AnimatablePropertyEnum.describe("Property to animate"),
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"),
2389
+ range: z30.tuple([z30.number().int().min(0), z30.number().int().min(0)]).describe("Frame range [start, end] inclusive"),
2390
+ from: z30.unknown().describe("Starting value"),
2391
+ to: z30.unknown().describe("Ending value"),
2000
2392
  easing: EasingInputSchema.optional().describe("Easing function"),
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")
2393
+ description: z30.string().optional().describe("Delta description"),
2394
+ tags: z30.array(z30.string()).optional().describe("Tags"),
2395
+ deltaId: z30.string().optional().describe("Custom delta ID")
2004
2396
  },
2005
2397
  { readOnlyHint: false, destructiveHint: false },
2006
2398
  async ({ id, stateName, layer, property, range, from, to, easing, description, tags, deltaId }) => {
@@ -2038,17 +2430,17 @@ function register5(server, store) {
2038
2430
  "atelier_edit_delta",
2039
2431
  "Edit an existing delta by index within a state",
2040
2432
  {
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"),
2433
+ id: z30.string().describe("Document ID"),
2434
+ stateName: z30.string().describe("State name"),
2435
+ deltaIndex: z30.number().int().min(0).describe("Delta index within the state"),
2436
+ layer: z30.string().optional().describe("New target layer ID"),
2045
2437
  property: AnimatablePropertyEnum.optional().describe("New property to animate"),
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"),
2438
+ range: z30.tuple([z30.number().int().min(0), z30.number().int().min(0)]).optional().describe("New frame range [start, end]"),
2439
+ from: z30.unknown().optional().describe("New starting value"),
2440
+ to: z30.unknown().optional().describe("New ending value"),
2049
2441
  easing: EasingInputSchema.optional().describe("New easing function"),
2050
- description: z20.string().optional().describe("New description"),
2051
- tags: z20.array(z20.string()).optional().describe("New tags")
2442
+ description: z30.string().optional().describe("New description"),
2443
+ tags: z30.array(z30.string()).optional().describe("New tags")
2052
2444
  },
2053
2445
  { readOnlyHint: false, destructiveHint: false },
2054
2446
  async ({ id, stateName, deltaIndex, layer, property, range, from, to, easing, description, tags }) => {
@@ -2087,9 +2479,9 @@ function register5(server, store) {
2087
2479
  "atelier_remove_delta",
2088
2480
  "Remove a delta by index from a state",
2089
2481
  {
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")
2482
+ id: z30.string().describe("Document ID"),
2483
+ stateName: z30.string().describe("State name"),
2484
+ deltaIndex: z30.number().int().min(0).describe("Delta index to remove")
2093
2485
  },
2094
2486
  { readOnlyHint: false, destructiveHint: true },
2095
2487
  async ({ id, stateName, deltaIndex }) => {
@@ -2117,12 +2509,12 @@ function register5(server, store) {
2117
2509
  "atelier_apply_preset",
2118
2510
  "Apply a preset to a layer, expanding it into concrete deltas",
2119
2511
  {
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)")
2512
+ id: z30.string().describe("Document ID"),
2513
+ stateName: z30.string().describe("State name to add deltas to"),
2514
+ presetName: z30.string().describe("Preset name defined in the document"),
2515
+ layerId: z30.string().describe("Target layer ID"),
2516
+ startFrame: z30.number().int().min(0).optional().describe("Start frame (default: 0)"),
2517
+ duration: z30.number().positive().int().optional().describe("Duration for preset (default: state duration)")
2126
2518
  },
2127
2519
  { readOnlyHint: false, destructiveHint: false },
2128
2520
  async ({ id, stateName, presetName, layerId, startFrame, duration }) => {
@@ -2165,7 +2557,7 @@ ${errors.join("\n")}`);
2165
2557
  }
2166
2558
 
2167
2559
  // ../mcp/src/tools/presets.ts
2168
- import { z as z21 } from "zod";
2560
+ import { z as z31 } from "zod";
2169
2561
  function getDoc6(store, id) {
2170
2562
  const doc = store.get(id);
2171
2563
  if (!doc) return { error: `Document "${id}" not found` };
@@ -2179,11 +2571,11 @@ function err6(message) {
2179
2571
  }
2180
2572
  var AnimatablePropertyEnum2 = AnimatablePropertySchema;
2181
2573
  var EasingInputSchema2 = EasingSchema;
2182
- var PresetDeltaSchema2 = z21.object({
2574
+ var PresetDeltaSchema2 = z31.object({
2183
2575
  property: AnimatablePropertyEnum2.describe("Animatable property"),
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"),
2576
+ offset: z31.tuple([z31.number().int().min(0), z31.number().int().min(0)]).optional().describe("Relative frame offset [start, end]"),
2577
+ from: z31.unknown().describe("Starting value"),
2578
+ to: z31.unknown().describe("Ending value"),
2187
2579
  easing: EasingInputSchema2.optional().describe("Easing function")
2188
2580
  });
2189
2581
  function register6(server, store) {
@@ -2191,11 +2583,11 @@ function register6(server, store) {
2191
2583
  "atelier_define_preset",
2192
2584
  "Define a reusable animation preset on a document",
2193
2585
  {
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")
2586
+ id: z31.string().describe("Document ID"),
2587
+ presetName: z31.string().describe("Preset name"),
2588
+ description: z31.string().optional().describe("Preset description"),
2589
+ tags: z31.array(z31.string()).optional().describe("Tags"),
2590
+ deltas: z31.array(PresetDeltaSchema2).min(1).describe("Array of preset delta definitions")
2199
2591
  },
2200
2592
  { readOnlyHint: false, destructiveHint: false },
2201
2593
  async ({ id, presetName, description, tags, deltas }) => {
@@ -2216,7 +2608,7 @@ function register6(server, store) {
2216
2608
  "atelier_list_presets",
2217
2609
  "List all presets defined on a document",
2218
2610
  {
2219
- id: z21.string().describe("Document ID")
2611
+ id: z31.string().describe("Document ID")
2220
2612
  },
2221
2613
  { readOnlyHint: true, destructiveHint: false },
2222
2614
  async ({ id }) => {
@@ -2239,7 +2631,7 @@ function register6(server, store) {
2239
2631
  }
2240
2632
 
2241
2633
  // ../mcp/src/tools/preview.ts
2242
- import { z as z22 } from "zod";
2634
+ import { z as z32 } from "zod";
2243
2635
  function getDoc7(store, id) {
2244
2636
  const doc = store.get(id);
2245
2637
  if (!doc) return { error: `Document "${id}" not found` };
@@ -2256,7 +2648,7 @@ function register7(server, store) {
2256
2648
  "atelier_validate",
2257
2649
  "Validate an Atelier document for schema correctness and delta overlaps",
2258
2650
  {
2259
- id: z22.string().describe("Document ID")
2651
+ id: z32.string().describe("Document ID")
2260
2652
  },
2261
2653
  { readOnlyHint: true, destructiveHint: false },
2262
2654
  async ({ id }) => {
@@ -2285,9 +2677,9 @@ function register7(server, store) {
2285
2677
  "atelier_preview",
2286
2678
  "Preview the resolved state of all layers at a specific frame",
2287
2679
  {
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")
2680
+ id: z32.string().describe("Document ID"),
2681
+ stateName: z32.string().describe("State name to preview"),
2682
+ frame: z32.number().int().min(0).describe("Frame number to resolve")
2291
2683
  },
2292
2684
  { readOnlyHint: true, destructiveHint: false },
2293
2685
  async ({ id, stateName, frame }) => {
@@ -2322,7 +2714,7 @@ function register7(server, store) {
2322
2714
  }
2323
2715
 
2324
2716
  // ../mcp/src/tools/templates.ts
2325
- import { z as z23 } from "zod";
2717
+ import { z as z33 } from "zod";
2326
2718
  function getDoc8(store, id) {
2327
2719
  const doc = store.get(id);
2328
2720
  if (!doc) return { error: `Document "${id}" not found` };
@@ -2339,8 +2731,8 @@ function register8(server, store) {
2339
2731
  "atelier_instantiate_template",
2340
2732
  "Instantiate a template document with variable bindings. Creates a new document in the store.",
2341
2733
  {
2342
- id: z23.string().describe("Template document ID"),
2343
- bindings: z23.record(z23.unknown()).describe("Variable bindings: { variableName: value }")
2734
+ id: z33.string().describe("Template document ID"),
2735
+ bindings: z33.record(z33.unknown()).describe("Variable bindings: { variableName: value }")
2344
2736
  },
2345
2737
  { readOnlyHint: false, destructiveHint: false },
2346
2738
  async ({ id, bindings }) => {
@@ -2363,7 +2755,7 @@ function register8(server, store) {
2363
2755
  "atelier_find_variables",
2364
2756
  "Scan a document for {{variableName}} patterns. Returns the list of variable references found.",
2365
2757
  {
2366
- id: z23.string().describe("Document ID")
2758
+ id: z33.string().describe("Document ID")
2367
2759
  },
2368
2760
  { readOnlyHint: true, destructiveHint: false },
2369
2761
  async ({ id }) => {
@@ -2383,7 +2775,7 @@ function register8(server, store) {
2383
2775
  }
2384
2776
 
2385
2777
  // ../mcp/src/tools/layer-effects.ts
2386
- import { z as z24 } from "zod";
2778
+ import { z as z34 } from "zod";
2387
2779
  function getDoc9(store, id) {
2388
2780
  const doc = store.get(id);
2389
2781
  if (!doc) return { error: `Document "${id}" not found` };
@@ -2400,9 +2792,9 @@ function register9(server, store) {
2400
2792
  "atelier_set_blend_mode",
2401
2793
  "Set or clear the blend mode on a layer",
2402
2794
  {
2403
- id: z24.string().describe("Document ID"),
2404
- layerId: z24.string().describe("Layer ID"),
2405
- blendMode: z24.enum([
2795
+ id: z34.string().describe("Document ID"),
2796
+ layerId: z34.string().describe("Layer ID"),
2797
+ blendMode: z34.enum([
2406
2798
  "normal",
2407
2799
  "multiply",
2408
2800
  "screen",
@@ -2440,13 +2832,13 @@ function register9(server, store) {
2440
2832
  "atelier_set_shadow",
2441
2833
  "Set or remove a drop shadow on a layer",
2442
2834
  {
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")
2835
+ id: z34.string().describe("Document ID"),
2836
+ layerId: z34.string().describe("Layer ID"),
2837
+ shadow: z34.object({
2838
+ color: z34.unknown().describe("Shadow color (hex string or RGBA/HSLA object)"),
2839
+ blur: z34.number().min(0).describe("Blur radius in pixels"),
2840
+ offsetX: z34.number().optional().describe("Horizontal offset in pixels"),
2841
+ offsetY: z34.number().optional().describe("Vertical offset in pixels")
2450
2842
  }).nullable().describe("Shadow definition, or null to remove")
2451
2843
  },
2452
2844
  { readOnlyHint: false, destructiveHint: false },
@@ -2468,11 +2860,11 @@ function register9(server, store) {
2468
2860
  "atelier_set_tint",
2469
2861
  "Set or remove a color tint on a layer (works on any visual type)",
2470
2862
  {
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)")
2863
+ id: z34.string().describe("Document ID"),
2864
+ layerId: z34.string().describe("Layer ID"),
2865
+ tint: z34.object({
2866
+ color: z34.string().describe("Tint color (hex string)"),
2867
+ amount: z34.number().min(0).max(1).describe("Tint amount (0 = none, 1 = full)")
2476
2868
  }).nullable().describe("Tint definition, or null to remove")
2477
2869
  },
2478
2870
  { readOnlyHint: false, destructiveHint: false },
@@ -2494,18 +2886,18 @@ function register9(server, store) {
2494
2886
  "atelier_set_motion_path",
2495
2887
  "Set or remove a motion path on a layer for path-based position animation",
2496
2888
  {
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()
2889
+ id: z34.string().describe("Document ID"),
2890
+ layerId: z34.string().describe("Layer ID"),
2891
+ motionPath: z34.object({
2892
+ points: z34.array(z34.object({
2893
+ x: z34.number(),
2894
+ y: z34.number(),
2895
+ in: z34.object({ x: z34.number(), y: z34.number() }).optional(),
2896
+ out: z34.object({ x: z34.number(), y: z34.number() }).optional()
2505
2897
  })).min(2).describe("Path points (minimum 2)"),
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")
2898
+ closed: z34.boolean().optional().describe("Whether the path is closed"),
2899
+ autoRotate: z34.boolean().optional().describe("Auto-rotate layer to follow path tangent"),
2900
+ autoRotateOffset: z34.number().optional().describe("Rotation offset in degrees when autoRotate is enabled")
2509
2901
  }).nullable().describe("Motion path definition, or null to remove")
2510
2902
  },
2511
2903
  { readOnlyHint: false, destructiveHint: false },
@@ -2527,21 +2919,21 @@ function register9(server, store) {
2527
2919
  "atelier_set_clip_path",
2528
2920
  "Set or remove a clip path on a layer to restrict rendering to within a shape",
2529
2921
  {
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()])
2922
+ id: z34.string().describe("Document ID"),
2923
+ layerId: z34.string().describe("Layer ID"),
2924
+ clipPath: z34.object({
2925
+ type: z34.enum(["rect", "ellipse", "path"]).describe("Clip shape type"),
2926
+ cornerRadius: z34.union([
2927
+ z34.number(),
2928
+ z34.tuple([z34.number(), z34.number(), z34.number(), z34.number()])
2537
2929
  ]).optional().describe("Corner radius (rect only)"),
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()
2930
+ points: z34.array(z34.object({
2931
+ x: z34.number(),
2932
+ y: z34.number(),
2933
+ in: z34.object({ x: z34.number(), y: z34.number() }).optional(),
2934
+ out: z34.object({ x: z34.number(), y: z34.number() }).optional()
2543
2935
  })).optional().describe("Path points (path only)"),
2544
- closed: z24.boolean().optional().describe("Whether path is closed (path only)")
2936
+ closed: z34.boolean().optional().describe("Whether path is closed (path only)")
2545
2937
  }).nullable().describe("Clip path shape, or null to remove")
2546
2938
  },
2547
2939
  { readOnlyHint: false, destructiveHint: false },
@@ -2563,45 +2955,45 @@ function register9(server, store) {
2563
2955
  "atelier_edit_visual",
2564
2956
  "Edit visual content of an existing layer (text content/style, image src/assetId, shape/fill/stroke)",
2565
2957
  {
2566
- id: z24.string().describe("Document ID"),
2567
- layerId: z24.string().describe("Layer ID"),
2958
+ id: z34.string().describe("Document ID"),
2959
+ layerId: z34.string().describe("Layer ID"),
2568
2960
  // Text fields
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)"),
2961
+ content: z34.string().optional().describe("New text content (text layers only)"),
2962
+ style: z34.record(z34.unknown()).optional().describe("Partial style update \u2014 only provided keys are overwritten (text layers only)"),
2571
2963
  // Image fields
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()
2964
+ src: z34.string().optional().describe("Image source URL (image layers only)"),
2965
+ assetId: z34.string().optional().describe("Asset ID reference (image layers only)"),
2966
+ sourceRect: z34.object({
2967
+ x: z34.number(),
2968
+ y: z34.number(),
2969
+ width: z34.number().positive(),
2970
+ height: z34.number().positive()
2579
2971
  }).nullable().optional().describe("Source rectangle crop (image layers only). Null to remove."),
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()
2972
+ spritesheet: z34.object({
2973
+ columns: z34.number().int().positive(),
2974
+ rows: z34.number().int().positive(),
2975
+ frameCount: z34.number().int().positive().optional(),
2976
+ frameWidth: z34.number().positive(),
2977
+ frameHeight: z34.number().positive()
2586
2978
  }).nullable().optional().describe("Spritesheet config (image layers only). Null to remove."),
2587
- frameIndex: z24.number().int().min(0).optional().describe("Spritesheet frame index (image layers only)"),
2979
+ frameIndex: z34.number().int().min(0).optional().describe("Spritesheet frame index (image layers only)"),
2588
2980
  // Shape fields
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()])
2981
+ shape: z34.object({
2982
+ type: z34.enum(["rect", "ellipse", "path"]),
2983
+ cornerRadius: z34.union([
2984
+ z34.number(),
2985
+ z34.tuple([z34.number(), z34.number(), z34.number(), z34.number()])
2594
2986
  ]).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()
2987
+ points: z34.array(z34.object({
2988
+ x: z34.number(),
2989
+ y: z34.number(),
2990
+ in: z34.object({ x: z34.number(), y: z34.number() }).optional(),
2991
+ out: z34.object({ x: z34.number(), y: z34.number() }).optional()
2600
2992
  })).optional(),
2601
- closed: z24.boolean().optional()
2993
+ closed: z34.boolean().optional()
2602
2994
  }).optional().describe("New shape (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)")
2995
+ fill: z34.record(z34.unknown()).optional().describe("New fill definition (shape layers only)"),
2996
+ stroke: z34.record(z34.unknown()).optional().describe("New stroke definition (shape layers only)")
2605
2997
  },
2606
2998
  { readOnlyHint: false, destructiveHint: false },
2607
2999
  async ({ id, layerId, content, style, src, assetId, sourceRect, spritesheet, frameIndex, shape, fill, stroke }) => {
@@ -2682,7 +3074,7 @@ function register9(server, store) {
2682
3074
  }
2683
3075
 
2684
3076
  // ../mcp/src/tools/state-config.ts
2685
- import { z as z25 } from "zod";
3077
+ import { z as z35 } from "zod";
2686
3078
  function getDoc10(store, id) {
2687
3079
  const doc = store.get(id);
2688
3080
  if (!doc) return { error: `Document "${id}" not found` };
@@ -2694,68 +3086,39 @@ function ok10(data) {
2694
3086
  function err10(message) {
2695
3087
  return { content: [{ type: "text", text: JSON.stringify({ error: message }) }], isError: true };
2696
3088
  }
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()
3089
+ var EasingInputSchema3 = z35.union([
3090
+ z35.enum(["ease-in", "ease-out", "ease-in-out"]),
3091
+ z35.object({ type: z35.literal("linear") }),
3092
+ z35.object({
3093
+ type: z35.literal("cubic-bezier"),
3094
+ x1: z35.number(),
3095
+ y1: z35.number(),
3096
+ x2: z35.number(),
3097
+ y2: z35.number()
2706
3098
  }),
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()
3099
+ z35.object({
3100
+ type: z35.literal("spring"),
3101
+ mass: z35.number().optional(),
3102
+ stiffness: z35.number().optional(),
3103
+ damping: z35.number().optional(),
3104
+ velocity: z35.number().optional()
2713
3105
  }),
2714
- z25.object({
2715
- type: z25.literal("step"),
2716
- steps: z25.number().int().positive(),
2717
- position: z25.enum(["start", "end"]).optional()
3106
+ z35.object({
3107
+ type: z35.literal("step"),
3108
+ steps: z35.number().int().positive(),
3109
+ position: z35.enum(["start", "end"]).optional()
2718
3110
  })
2719
3111
  ]).describe("Easing function");
2720
3112
  function register10(server, store) {
2721
- server.tool(
2722
- "atelier_set_audio",
2723
- "Set or remove an audio track on a state",
2724
- {
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)")
2733
- }).nullable().describe("Audio configuration, or null to remove")
2734
- },
2735
- { readOnlyHint: false, destructiveHint: false },
2736
- async ({ id, stateName, audio }) => {
2737
- const result = getDoc10(store, id);
2738
- if ("error" in result) return err10(result.error);
2739
- const { doc } = result;
2740
- const state = doc.states[stateName];
2741
- if (!state) return err10(`State "${stateName}" not found`);
2742
- if (audio === null) {
2743
- delete state.audio;
2744
- return ok10({ stateName, audio: null, removed: true });
2745
- }
2746
- state.audio = audio;
2747
- return ok10({ stateName, audioSrc: audio.src });
2748
- }
2749
- );
2750
3113
  server.tool(
2751
3114
  "atelier_configure_transition",
2752
3115
  "Configure or remove a transition from one state to a target state",
2753
3116
  {
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"),
3117
+ id: z35.string().describe("Document ID"),
3118
+ stateName: z35.string().describe("Source state name"),
3119
+ targetState: z35.string().describe("Target state name"),
3120
+ transition: z35.object({
3121
+ duration: z35.number().positive().int().describe("Transition duration in frames"),
2759
3122
  easing: EasingInputSchema3.optional()
2760
3123
  }).nullable().describe("Transition config, or null to remove")
2761
3124
  },
@@ -2792,9 +3155,9 @@ function register10(server, store) {
2792
3155
  "atelier_set_state_parent",
2793
3156
  "Set or clear the parent state for hierarchical delta inheritance",
2794
3157
  {
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")
3158
+ id: z35.string().describe("Document ID"),
3159
+ stateName: z35.string().describe("State name"),
3160
+ parent: z35.string().nullable().describe("Parent state name, or null to clear")
2798
3161
  },
2799
3162
  { readOnlyHint: false, destructiveHint: false },
2800
3163
  async ({ id, stateName, parent }) => {
@@ -2829,7 +3192,7 @@ function register10(server, store) {
2829
3192
  }
2830
3193
 
2831
3194
  // ../mcp/src/tools/export.ts
2832
- import { z as z26 } from "zod";
3195
+ import { z as z36 } from "zod";
2833
3196
 
2834
3197
  // ../canvas/dist/index.js
2835
3198
  function colorToCSS(color) {
@@ -3924,8 +4287,11 @@ function mapBlendMode(mode) {
3924
4287
  }
3925
4288
  function collectUnsupportedWarnings(doc, state) {
3926
4289
  const warnings = [];
3927
- if (state.audio) {
3928
- warnings.push("Audio tracks are not supported in Lottie format and will be dropped");
4290
+ const audioLayers = doc.layers.filter((l) => l.visual.type === "audio");
4291
+ if (audioLayers.length > 0) {
4292
+ warnings.push(
4293
+ `${audioLayers.length} audio layer(s) are not supported in Lottie format and will be dropped`
4294
+ );
3929
4295
  }
3930
4296
  for (const delta of state.deltas) {
3931
4297
  if (isExpression22(delta.from) || isExpression22(delta.to)) {
@@ -4014,12 +4380,12 @@ function register11(server, store) {
4014
4380
  "atelier_export_svg",
4015
4381
  "Export a frame as an SVG string",
4016
4382
  {
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)")
4383
+ id: z36.string().describe("Document ID"),
4384
+ stateName: z36.string().optional().describe("State name (defaults to first state)"),
4385
+ frame: z36.number().int().min(0).optional().describe("Frame number (defaults to 0)"),
4386
+ xmlDeclaration: z36.boolean().optional().describe("Include XML declaration (default: false)"),
4387
+ viewBox: z36.boolean().optional().describe("Include viewBox attribute (default: true)"),
4388
+ indent: z36.number().int().min(0).optional().describe("Indent size in spaces (default: 2)")
4023
4389
  },
4024
4390
  { readOnlyHint: true, destructiveHint: false },
4025
4391
  async ({ id, stateName, frame, xmlDeclaration, viewBox, indent }) => {
@@ -4050,8 +4416,8 @@ function register11(server, store) {
4050
4416
  "atelier_export_lottie",
4051
4417
  "Export a document to Lottie JSON format (lossy, best-effort)",
4052
4418
  {
4053
- id: z26.string().describe("Document ID"),
4054
- stateName: z26.string().optional().describe("State to export (defaults to first state)")
4419
+ id: z36.string().describe("Document ID"),
4420
+ stateName: z36.string().optional().describe("State to export (defaults to first state)")
4055
4421
  },
4056
4422
  { readOnlyHint: true, destructiveHint: false },
4057
4423
  async ({ id, stateName }) => {
@@ -4071,7 +4437,7 @@ function register11(server, store) {
4071
4437
  }
4072
4438
 
4073
4439
  // ../mcp/src/tools/assets.ts
4074
- import { z as z27 } from "zod";
4440
+ import { z as z37 } from "zod";
4075
4441
  function getDoc12(store, id) {
4076
4442
  const doc = store.get(id);
4077
4443
  if (!doc) return { error: `Document "${id}" not found` };
@@ -4088,17 +4454,17 @@ function register12(server, store) {
4088
4454
  "atelier_add_asset",
4089
4455
  "Register an external asset (image, SVG, font, animation, audio) on a document",
4090
4456
  {
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")
4457
+ id: z37.string().describe("Document ID"),
4458
+ assetId: z37.string().describe("Unique asset identifier"),
4459
+ type: z37.enum(["image", "svg", "font", "animation", "audio"]).describe("Asset type"),
4460
+ src: z37.string().describe("File path or URL"),
4461
+ description: z37.string().optional().describe("Human-readable description"),
4462
+ spritesheet: z37.object({
4463
+ columns: z37.number().int().positive().describe("Number of columns in the spritesheet grid"),
4464
+ rows: z37.number().int().positive().describe("Number of rows in the spritesheet grid"),
4465
+ frameCount: z37.number().int().positive().optional().describe("Total frame count (defaults to columns \xD7 rows)"),
4466
+ frameWidth: z37.number().positive().describe("Width of each frame in pixels"),
4467
+ frameHeight: z37.number().positive().describe("Height of each frame in pixels")
4102
4468
  }).optional().describe("Spritesheet metadata (image assets only)")
4103
4469
  },
4104
4470
  { readOnlyHint: false, destructiveHint: false },
@@ -4121,7 +4487,7 @@ function register12(server, store) {
4121
4487
  "atelier_list_assets",
4122
4488
  "List all registered assets with usage info (which layers/states reference them)",
4123
4489
  {
4124
- id: z27.string().describe("Document ID")
4490
+ id: z37.string().describe("Document ID")
4125
4491
  },
4126
4492
  { readOnlyHint: true, destructiveHint: false },
4127
4493
  async ({ id }) => {
@@ -4130,15 +4496,16 @@ function register12(server, store) {
4130
4496
  const { doc } = result;
4131
4497
  const assets = doc.assets ?? {};
4132
4498
  const entries = Object.entries(assets).map(([assetId, asset]) => {
4133
- const referencingLayers = doc.layers.filter((l) => l.visual.type === "image" && l.visual.assetId === assetId).map((l) => l.id);
4134
- const referencingStates = Object.entries(doc.states).filter(([, state]) => state.audio?.src === assetId).map(([name]) => name);
4499
+ const referencingLayers = doc.layers.filter((l) => {
4500
+ const v = l.visual;
4501
+ return (v.type === "image" || v.type === "video" || v.type === "audio") && "assetId" in v && v.assetId === assetId;
4502
+ }).map((l) => l.id);
4135
4503
  return {
4136
4504
  assetId,
4137
4505
  type: asset.type,
4138
4506
  src: asset.src,
4139
4507
  description: asset.description,
4140
- usedByLayers: referencingLayers,
4141
- usedByStates: referencingStates
4508
+ usedByLayers: referencingLayers
4142
4509
  };
4143
4510
  });
4144
4511
  return ok12({ count: entries.length, assets: entries });
@@ -4146,10 +4513,10 @@ function register12(server, store) {
4146
4513
  );
4147
4514
  server.tool(
4148
4515
  "atelier_remove_asset",
4149
- "Remove a registered asset. Warns if layers or state audio still reference it.",
4516
+ "Remove a registered asset. Warns if layers still reference it.",
4150
4517
  {
4151
- id: z27.string().describe("Document ID"),
4152
- assetId: z27.string().describe("Asset ID to remove")
4518
+ id: z37.string().describe("Document ID"),
4519
+ assetId: z37.string().describe("Asset ID to remove")
4153
4520
  },
4154
4521
  { readOnlyHint: false, destructiveHint: true },
4155
4522
  async ({ id, assetId }) => {
@@ -4158,14 +4525,13 @@ function register12(server, store) {
4158
4525
  const { doc } = result;
4159
4526
  if (!doc.assets?.[assetId]) return err12(`Asset "${assetId}" not found`);
4160
4527
  const warnings = [];
4161
- const referencingLayers = doc.layers.filter((l) => l.visual.type === "image" && l.visual.assetId === assetId).map((l) => l.id);
4528
+ const referencingLayers = doc.layers.filter((l) => {
4529
+ const v = l.visual;
4530
+ return (v.type === "image" || v.type === "video" || v.type === "audio") && "assetId" in v && v.assetId === assetId;
4531
+ }).map((l) => l.id);
4162
4532
  if (referencingLayers.length > 0) {
4163
4533
  warnings.push(`Asset is referenced by layer(s): ${referencingLayers.join(", ")}`);
4164
4534
  }
4165
- const referencingStates = Object.entries(doc.states).filter(([, state]) => state.audio?.src === assetId).map(([name]) => name);
4166
- if (referencingStates.length > 0) {
4167
- warnings.push(`Asset is referenced by state audio: ${referencingStates.join(", ")}`);
4168
- }
4169
4535
  delete doc.assets[assetId];
4170
4536
  return ok12({ assetId, warnings });
4171
4537
  }
@@ -4173,7 +4539,7 @@ function register12(server, store) {
4173
4539
  }
4174
4540
 
4175
4541
  // ../mcp/src/tools/variables.ts
4176
- import { z as z28 } from "zod";
4542
+ import { z as z38 } from "zod";
4177
4543
  function getDoc13(store, id) {
4178
4544
  const doc = store.get(id);
4179
4545
  if (!doc) return { error: `Document "${id}" not found` };
@@ -4190,11 +4556,11 @@ function register13(server, store) {
4190
4556
  "atelier_add_variable",
4191
4557
  "Define a template variable on a document",
4192
4558
  {
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")
4559
+ id: z38.string().describe("Document ID"),
4560
+ variableName: z38.string().describe("Variable name (used in {{variableName}} patterns)"),
4561
+ type: z38.enum(["string", "number", "color", "asset", "boolean"]).describe("Variable type"),
4562
+ description: z38.string().optional().describe("Human-readable description"),
4563
+ default: z38.unknown().optional().describe("Default value")
4198
4564
  },
4199
4565
  { readOnlyHint: false, destructiveHint: false },
4200
4566
  async (args) => {
@@ -4215,7 +4581,7 @@ function register13(server, store) {
4215
4581
  "atelier_list_variables",
4216
4582
  "List all declared variables with usage info (which {{references}} exist in the document)",
4217
4583
  {
4218
- id: z28.string().describe("Document ID")
4584
+ id: z38.string().describe("Document ID")
4219
4585
  },
4220
4586
  { readOnlyHint: true, destructiveHint: false },
4221
4587
  async ({ id }) => {
@@ -4239,8 +4605,8 @@ function register13(server, store) {
4239
4605
  "atelier_remove_variable",
4240
4606
  "Remove a declared variable. Warns if {{references}} still exist in the document.",
4241
4607
  {
4242
- id: z28.string().describe("Document ID"),
4243
- variableName: z28.string().describe("Variable name to remove")
4608
+ id: z38.string().describe("Document ID"),
4609
+ variableName: z38.string().describe("Variable name to remove")
4244
4610
  },
4245
4611
  { readOnlyHint: false, destructiveHint: true },
4246
4612
  async ({ id, variableName }) => {
@@ -4263,7 +4629,7 @@ function register13(server, store) {
4263
4629
  }
4264
4630
 
4265
4631
  // ../mcp/src/tools/interactions.ts
4266
- import { z as z29 } from "zod";
4632
+ import { z as z39 } from "zod";
4267
4633
  function getDoc14(store, id) {
4268
4634
  const doc = store.get(id);
4269
4635
  if (!doc) return { error: `Document "${id}" not found` };
@@ -4280,23 +4646,23 @@ function register14(server, store) {
4280
4646
  "atelier_add_interaction",
4281
4647
  "Add a trigger-action interaction to a layer (click, hover, timer, signal \u2192 go-to-state, emit-signal, set-variable, toggle-visibility)",
4282
4648
  {
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)")
4649
+ id: z39.string().describe("Document ID"),
4650
+ layerId: z39.string().describe("Layer to attach the interaction to"),
4651
+ interactionId: z39.string().describe("Unique interaction ID"),
4652
+ trigger: z39.object({
4653
+ type: z39.enum(["click", "hover", "pointerdown", "pointerup", "timer", "signal"]).describe("Trigger type"),
4654
+ delay: z39.number().optional().describe("Delay in ms (timer trigger only)"),
4655
+ signal: z39.string().optional().describe("Signal name (signal trigger only)")
4290
4656
  }).describe("What triggers the interaction"),
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)")
4657
+ action: z39.object({
4658
+ type: z39.enum(["go-to-state", "emit-signal", "set-variable", "toggle-visibility"]).describe("Action type"),
4659
+ state: z39.string().optional().describe("Target state (go-to-state only)"),
4660
+ signal: z39.string().optional().describe("Signal to emit (emit-signal only)"),
4661
+ variable: z39.string().optional().describe("Variable name (set-variable only)"),
4662
+ value: z39.unknown().optional().describe("Value to set (set-variable only)"),
4663
+ targetLayer: z39.string().optional().describe("Target layer ID (toggle-visibility, defaults to self)")
4298
4664
  }).describe("What happens when triggered"),
4299
- description: z29.string().optional().describe("Human-readable description")
4665
+ description: z39.string().optional().describe("Human-readable description")
4300
4666
  },
4301
4667
  { readOnlyHint: false, destructiveHint: false },
4302
4668
  async ({ id, layerId, interactionId, trigger, action, description }) => {
@@ -4345,8 +4711,8 @@ function register14(server, store) {
4345
4711
  "atelier_list_interactions",
4346
4712
  "List all interactions across all layers or for a specific layer",
4347
4713
  {
4348
- id: z29.string().describe("Document ID"),
4349
- layerId: z29.string().optional().describe("Optional: filter to a specific layer")
4714
+ id: z39.string().describe("Document ID"),
4715
+ layerId: z39.string().optional().describe("Optional: filter to a specific layer")
4350
4716
  },
4351
4717
  { readOnlyHint: true, destructiveHint: false },
4352
4718
  async ({ id, layerId }) => {
@@ -4373,9 +4739,9 @@ function register14(server, store) {
4373
4739
  "atelier_remove_interaction",
4374
4740
  "Remove an interaction from a layer",
4375
4741
  {
4376
- id: z29.string().describe("Document ID"),
4377
- layerId: z29.string().describe("Layer ID"),
4378
- interactionId: z29.string().describe("Interaction ID to remove")
4742
+ id: z39.string().describe("Document ID"),
4743
+ layerId: z39.string().describe("Layer ID"),
4744
+ interactionId: z39.string().describe("Interaction ID to remove")
4379
4745
  },
4380
4746
  { readOnlyHint: false, destructiveHint: true },
4381
4747
  async ({ id, layerId, interactionId }) => {
@@ -4397,7 +4763,7 @@ function register14(server, store) {
4397
4763
  }
4398
4764
 
4399
4765
  // ../mcp/src/tools/refs.ts
4400
- import { z as z30 } from "zod";
4766
+ import { z as z40 } from "zod";
4401
4767
  function getDoc15(store, id) {
4402
4768
  const doc = store.get(id);
4403
4769
  if (!doc) return { error: `Document "${id}" not found` };
@@ -4414,9 +4780,9 @@ function register15(server, store) {
4414
4780
  "atelier_set_ref",
4415
4781
  "Set the source reference on a ref-type layer. Can point to a file path or another in-memory document ID.",
4416
4782
  {
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")
4783
+ id: z40.string().describe("Document ID"),
4784
+ layerId: z40.string().describe("Layer ID (must be a ref visual)"),
4785
+ src: z40.string().describe("File path, URL, or in-memory document ID")
4420
4786
  },
4421
4787
  { readOnlyHint: false, destructiveHint: false },
4422
4788
  async ({ id, layerId, src }) => {
@@ -4436,7 +4802,7 @@ function register15(server, store) {
4436
4802
  "atelier_resolve_refs",
4437
4803
  "Check which ref layers can be resolved (point to valid in-memory documents)",
4438
4804
  {
4439
- id: z30.string().describe("Document ID")
4805
+ id: z40.string().describe("Document ID")
4440
4806
  },
4441
4807
  { readOnlyHint: true, destructiveHint: false },
4442
4808
  async ({ id }) => {
@@ -4463,7 +4829,7 @@ function register15(server, store) {
4463
4829
  }
4464
4830
 
4465
4831
  // ../mcp/src/tools/performance.ts
4466
- import { z as z31 } from "zod";
4832
+ import { z as z41 } from "zod";
4467
4833
  function getDoc16(store, id) {
4468
4834
  const doc = store.get(id);
4469
4835
  if (!doc) return { error: `Document "${id}" not found` };
@@ -4480,9 +4846,9 @@ function register16(server, store) {
4480
4846
  "atelier_profile",
4481
4847
  "Profile frame resolution performance \u2014 returns timing, layer count, property count, and delta count for a frame",
4482
4848
  {
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")
4849
+ id: z41.string().describe("Document ID"),
4850
+ stateName: z41.string().describe("State name to profile"),
4851
+ frame: z41.number().int().min(0).describe("Frame number to resolve")
4486
4852
  },
4487
4853
  { readOnlyHint: true, destructiveHint: false },
4488
4854
  async ({ id, stateName, frame }) => {
@@ -4528,10 +4894,10 @@ function register16(server, store) {
4528
4894
  "atelier_diff_frames",
4529
4895
  "Compare two frames and return only the properties that changed between them",
4530
4896
  {
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")
4897
+ id: z41.string().describe("Document ID"),
4898
+ stateName: z41.string().describe("State name"),
4899
+ frameA: z41.number().int().min(0).describe("First frame number"),
4900
+ frameB: z41.number().int().min(0).describe("Second frame number")
4535
4901
  },
4536
4902
  { readOnlyHint: true, destructiveHint: false },
4537
4903
  async ({ id, stateName, frameA, frameB }) => {
@@ -4570,9 +4936,9 @@ function register16(server, store) {
4570
4936
  "atelier_batch_preview",
4571
4937
  "Preview multiple frames at once \u2014 reduces round-trips for scrubbing or timeline inspection",
4572
4938
  {
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)")
4939
+ id: z41.string().describe("Document ID"),
4940
+ stateName: z41.string().describe("State name to preview"),
4941
+ frames: z41.array(z41.number().int().min(0)).min(1).max(60).describe("Array of frame numbers to resolve (max 60)")
4576
4942
  },
4577
4943
  { readOnlyHint: true, destructiveHint: false },
4578
4944
  async ({ id, stateName, frames }) => {
@@ -4608,7 +4974,7 @@ function register16(server, store) {
4608
4974
  "atelier_complexity",
4609
4975
  "Analyze document complexity \u2014 layer count, delta count, expression usage, state hierarchy depth",
4610
4976
  {
4611
- id: z31.string().describe("Document ID")
4977
+ id: z41.string().describe("Document ID")
4612
4978
  },
4613
4979
  { readOnlyHint: true, destructiveHint: false },
4614
4980
  async ({ id }) => {
@@ -4637,7 +5003,9 @@ function register16(server, store) {
4637
5003
  duration: state.duration,
4638
5004
  deltaCount,
4639
5005
  expressionCount,
4640
- hasAudio: !!state.audio,
5006
+ // hasAudio removed in v1.0 — audio is no longer per-state, it's an
5007
+ // AudioVisual layer. Audio-layer counts appear in the top-level
5008
+ // layer stats below.
4641
5009
  hasParent: !!state.parent,
4642
5010
  transitionCount: state.transitions ? Object.keys(state.transitions).length : 0
4643
5011
  });
@@ -4681,7 +5049,7 @@ function register16(server, store) {
4681
5049
  }
4682
5050
 
4683
5051
  // ../mcp/src/tools/overlays.ts
4684
- import { z as z32 } from "zod";
5052
+ import { z as z42 } from "zod";
4685
5053
  function getDoc17(store, id) {
4686
5054
  const doc = store.get(id);
4687
5055
  if (!doc) return { error: `Document "${id}" not found` };
@@ -4693,12 +5061,12 @@ function ok17(data) {
4693
5061
  function err17(message) {
4694
5062
  return { content: [{ type: "text", text: JSON.stringify({ error: message }) }], isError: true };
4695
5063
  }
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()
5064
+ var AnchorEnum = z42.enum(["top-left", "top-right", "bottom-left", "bottom-right"]);
5065
+ var OverlayStyleSchema = z42.object({
5066
+ font_family: z42.string().optional(),
5067
+ font_size: z42.number().positive().optional(),
5068
+ font_weight: z42.union([z42.number(), z42.enum(["normal", "bold"])]).optional(),
5069
+ color: z42.string().optional()
4702
5070
  }).optional();
4703
5071
  var DEFAULT_OVERLAY_MARGIN = 24;
4704
5072
  var DEFAULT_TEXT_STYLE = {
@@ -4749,12 +5117,12 @@ function register17(server, store) {
4749
5117
  "atelier_add_handle",
4750
5118
  "Add an anchored creator-handle TextVisual overlay layer (tag: overlay)",
4751
5119
  {
4752
- id: z32.string().describe("Document ID"),
4753
- text: z32.string().min(1).describe('Handle text, e.g. "@username"'),
5120
+ id: z42.string().describe("Document ID"),
5121
+ text: z42.string().min(1).describe('Handle text, e.g. "@username"'),
4754
5122
  anchor: AnchorEnum.describe("Anchored corner"),
4755
- margin: z32.number().nonnegative().optional().describe("Px from anchored edges (default 24)"),
5123
+ margin: z42.number().nonnegative().optional().describe("Px from anchored edges (default 24)"),
4756
5124
  style: OverlayStyleSchema.describe("Text style overrides (snake_case to match recipe)"),
4757
- layerId: z32.string().optional().describe("Layer ID (default: overlay-handle)")
5125
+ layerId: z42.string().optional().describe("Layer ID (default: overlay-handle)")
4758
5126
  },
4759
5127
  { readOnlyHint: false, destructiveHint: false },
4760
5128
  async ({ id, text, anchor, margin, style, layerId }) => {
@@ -4784,14 +5152,14 @@ function register17(server, store) {
4784
5152
  "atelier_add_page_number",
4785
5153
  "Add an anchored page-number TextVisual overlay layer (tag: overlay). Substitutes {current} / {total} (and zero-pad forms {current:02d} / {total:02d}) into format.",
4786
5154
  {
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}"'),
5155
+ id: z42.string().describe("Document ID"),
5156
+ format: z42.string().min(1).describe('Format string, e.g. "{current}/{total}" or "{current:02d}/{total:02d}"'),
4789
5157
  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)"),
5158
+ currentIndex: z42.number().int().nonnegative().describe("1-based current index"),
5159
+ totalCount: z42.number().int().positive().describe("Total count"),
5160
+ margin: z42.number().nonnegative().optional().describe("Px from anchored edges (default 24)"),
4793
5161
  style: OverlayStyleSchema.describe("Text style overrides (snake_case to match recipe)"),
4794
- layerId: z32.string().optional().describe("Layer ID (default: overlay-page-number)")
5162
+ layerId: z42.string().optional().describe("Layer ID (default: overlay-page-number)")
4795
5163
  },
4796
5164
  { readOnlyHint: false, destructiveHint: false },
4797
5165
  async ({ id, format, anchor, currentIndex, totalCount, margin, style, layerId }) => {
@@ -4824,7 +5192,7 @@ function register17(server, store) {
4824
5192
  import { readFileSync, writeFileSync, existsSync, readdirSync, mkdirSync } from "fs";
4825
5193
  import { join, resolve, isAbsolute, dirname } from "path";
4826
5194
  import { homedir } from "os";
4827
- import { z as z33 } from "zod";
5195
+ import { z as z43 } from "zod";
4828
5196
  import { parse as parseYaml, stringify as stringifyYaml } from "yaml";
4829
5197
  function getDoc18(store, id) {
4830
5198
  const doc = store.get(id);
@@ -4991,7 +5359,7 @@ function register18(server, store) {
4991
5359
  "atelier_recipe_list",
4992
5360
  "List discoverable Studio Recipes \u2014 project-local <projectDir>/.atelier/recipes/*.recipe.* plus ~/.atelier/recipes/*.recipe.*. Returns { name, path } entries with absolute paths.",
4993
5361
  {
4994
- projectDir: z33.string().optional().describe("Project root to search (default: process.cwd())")
5362
+ projectDir: z43.string().optional().describe("Project root to search (default: process.cwd())")
4995
5363
  },
4996
5364
  { readOnlyHint: true, destructiveHint: false },
4997
5365
  async ({ projectDir }) => {
@@ -5007,8 +5375,8 @@ function register18(server, store) {
5007
5375
  "atelier_recipe_get",
5008
5376
  "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
5377
  {
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())")
5378
+ path: z43.string().describe("Recipe path or bare name (resolved like the CLI)"),
5379
+ projectDir: z43.string().optional().describe("Project root for name resolution (default: process.cwd())")
5012
5380
  },
5013
5381
  { readOnlyHint: true, destructiveHint: false },
5014
5382
  async ({ path, projectDir }) => {
@@ -5030,9 +5398,9 @@ function register18(server, store) {
5030
5398
  "atelier_recipe_validate",
5031
5399
  "Validate a recipe from a path OR inline YAML text. Returns { success, warnings, errors }.",
5032
5400
  {
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())")
5401
+ path: z43.string().optional().describe("Recipe path or bare name to validate"),
5402
+ yaml: z43.string().optional().describe("Inline YAML text to validate (alternative to path)"),
5403
+ projectDir: z43.string().optional().describe("Project root for name resolution (default: process.cwd())")
5036
5404
  },
5037
5405
  { readOnlyHint: true, destructiveHint: false },
5038
5406
  async ({ path, yaml, projectDir }) => {
@@ -5062,9 +5430,9 @@ function register18(server, store) {
5062
5430
  "atelier_recipe_save",
5063
5431
  "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
5432
  {
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`)")
5433
+ path: z43.string().describe("Absolute or relative file path to write the recipe to"),
5434
+ recipe: z43.record(z43.unknown()).optional().describe("Recipe object (will be serialized to YAML)"),
5435
+ yaml: z43.string().optional().describe("Raw YAML text (alternative to `recipe`)")
5068
5436
  },
5069
5437
  { readOnlyHint: false, destructiveHint: true },
5070
5438
  async ({ path, recipe, yaml }) => {
@@ -5102,12 +5470,12 @@ function register18(server, store) {
5102
5470
  "atelier_recipe_apply",
5103
5471
  `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
5472
  {
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")
5473
+ documentId: z43.string().describe("In-store document ID to apply the recipe to"),
5474
+ path: z43.string().optional().describe("Recipe path or bare name (alternative to `recipe`)"),
5475
+ recipe: z43.record(z43.unknown()).optional().describe("Recipe object (alternative to `path`)"),
5476
+ projectDir: z43.string().optional().describe("Project root for name resolution (default: process.cwd())"),
5477
+ currentIndex: z43.number().int().nonnegative().optional().describe("1-based carousel index for page_number"),
5478
+ totalCount: z43.number().int().positive().optional().describe("Total carousel size for page_number")
5111
5479
  },
5112
5480
  { readOnlyHint: false, destructiveHint: false },
5113
5481
  async ({ documentId, path, recipe, projectDir, currentIndex, totalCount }) => {
@@ -5150,7 +5518,7 @@ function register18(server, store) {
5150
5518
  // ../mcp/src/tools/import-images.ts
5151
5519
  import { existsSync as existsSync2, readdirSync as readdirSync2, statSync } from "fs";
5152
5520
  import { resolve as resolve2, join as join2, basename, extname } from "path";
5153
- import { z as z34 } from "zod";
5521
+ import { z as z44 } from "zod";
5154
5522
  function getDoc19(store, id) {
5155
5523
  const doc = store.get(id);
5156
5524
  if (!doc) return { error: `Document "${id}" not found` };
@@ -5198,10 +5566,10 @@ function register19(server, store) {
5198
5566
  "atelier_import_images",
5199
5567
  `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
5568
  {
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.")
5569
+ documentId: z44.string().describe("In-store document ID to import into"),
5570
+ dir: z44.string().describe("Directory containing the images"),
5571
+ glob: z44.string().optional().describe('Optional filename glob, e.g. "*.png" (default: all images)'),
5572
+ asLayers: z44.boolean().optional().describe("Create layers per image (default: true). false = register assets only.")
5205
5573
  },
5206
5574
  { readOnlyHint: false, destructiveHint: false },
5207
5575
  async ({ documentId, dir, glob, asLayers }) => {
@@ -5262,12 +5630,2023 @@ function register19(server, store) {
5262
5630
  );
5263
5631
  }
5264
5632
 
5633
+ // ../mcp/src/tools/mind-map.ts
5634
+ import { existsSync as existsSync3, statSync as statSync2, readdirSync as readdirSync3 } from "fs";
5635
+ import { resolve as resolve3, join as join3, basename as basename2, extname as extname2 } from "path";
5636
+ import { z as z45 } from "zod";
5637
+ function ok20(data) {
5638
+ return { content: [{ type: "text", text: JSON.stringify({ success: true, ...data }) }] };
5639
+ }
5640
+ function err20(message) {
5641
+ return { content: [{ type: "text", text: JSON.stringify({ error: message }) }], isError: true };
5642
+ }
5643
+ var KIND_EXTENSIONS = {
5644
+ image: /* @__PURE__ */ new Set([".png", ".jpg", ".jpeg", ".webp", ".gif", ".svg", ".avif"]),
5645
+ video: /* @__PURE__ */ new Set([".mp4", ".mov", ".webm", ".mkv", ".avi"]),
5646
+ audio: /* @__PURE__ */ new Set([".mp3", ".wav", ".m4a", ".ogg", ".flac", ".aac"]),
5647
+ font: /* @__PURE__ */ new Set([".ttf", ".otf", ".woff", ".woff2"]),
5648
+ text: /* @__PURE__ */ new Set(),
5649
+ // empty by design — handled with a per-call note
5650
+ voice: /* @__PURE__ */ new Set([".mp3", ".wav", ".m4a", ".ogg", ".flac", ".aac"]),
5651
+ logo: /* @__PURE__ */ new Set([".png", ".jpg", ".jpeg", ".webp", ".gif", ".svg", ".avif"])
5652
+ };
5653
+ var TEXT_NOTE = "text slots resolve at composition time, not from disk";
5654
+ var MAX_DEPTH = 4;
5655
+ function chooseSearchRoot(projectDir) {
5656
+ const mindMap = join3(projectDir, ".atelier", "mind-map");
5657
+ if (existsSync3(mindMap) && statSync2(mindMap).isDirectory()) return mindMap;
5658
+ const assets = join3(projectDir, "assets");
5659
+ if (existsSync3(assets) && statSync2(assets).isDirectory()) return assets;
5660
+ return projectDir;
5661
+ }
5662
+ function walk(root, searchedDirs) {
5663
+ const out = [];
5664
+ function recurse(dir, depth, relSegments) {
5665
+ searchedDirs.push(dir);
5666
+ let entries;
5667
+ try {
5668
+ entries = readdirSync3(dir);
5669
+ } catch {
5670
+ return;
5671
+ }
5672
+ for (const entry of entries) {
5673
+ const abs = join3(dir, entry);
5674
+ let stat;
5675
+ try {
5676
+ stat = statSync2(abs);
5677
+ } catch {
5678
+ continue;
5679
+ }
5680
+ if (stat.isDirectory()) {
5681
+ if (depth >= MAX_DEPTH) continue;
5682
+ recurse(abs, depth + 1, [...relSegments, entry]);
5683
+ } else if (stat.isFile()) {
5684
+ out.push({
5685
+ path: abs,
5686
+ name: entry,
5687
+ mtimeMs: stat.mtimeMs,
5688
+ relDirSegments: relSegments
5689
+ });
5690
+ }
5691
+ }
5692
+ }
5693
+ recurse(root, 0, []);
5694
+ return out;
5695
+ }
5696
+ function matchedHints(candidate, hints) {
5697
+ const haystacks = [candidate.name.toLowerCase(), ...candidate.relDirSegments.map((s) => s.toLowerCase())];
5698
+ return hints.filter((h) => {
5699
+ const needle = h.toLowerCase();
5700
+ return haystacks.some((hs) => hs.includes(needle));
5701
+ });
5702
+ }
5703
+ var DEFAULT_LIMIT = 20;
5704
+ var MAX_LIMIT = 100;
5705
+ function register20(server, _store) {
5706
+ server.tool(
5707
+ "atelier_mind_map_query",
5708
+ "Resolve a typed asset slot against the creator's local asset library. Walks <projectDir>/.atelier/mind-map/ (preferred), else <projectDir>/assets/, else <projectDir> itself. Recursive with a depth cap of 4 levels. Filters by the slot `kind` extension table (image, video, audio, font, voice, logo); `text` returns no matches by design (text resolves at composition time). Optional `hints` (case-insensitive substrings) narrow by filename or ancestor directory name within the search root. Results sorted by mtime (most-recent first). Pure read \u2014 does not touch the document store. Stub for Phase 4 mind-map.",
5709
+ {
5710
+ projectDir: z45.string().describe("Absolute or cwd-relative path to the project root"),
5711
+ kind: SlotKindSchema.describe(
5712
+ "Slot kind from the closed SlotKind enum (image|video|audio|text|font|voice|logo)"
5713
+ ),
5714
+ hints: z45.array(z45.string().min(1)).min(1).optional().describe(
5715
+ "Optional case-insensitive substrings; a candidate must match at least one against its filename or an ancestor directory name within the search root."
5716
+ ),
5717
+ limit: z45.number().int().positive().max(MAX_LIMIT).optional().describe(`Max matches to return (default ${DEFAULT_LIMIT}, max ${MAX_LIMIT})`)
5718
+ },
5719
+ { readOnlyHint: true, destructiveHint: false },
5720
+ async ({ projectDir, kind, hints, limit }) => {
5721
+ const absProject = resolve3(projectDir);
5722
+ if (!existsSync3(absProject)) {
5723
+ return err20(`projectDir "${absProject}" does not exist`);
5724
+ }
5725
+ if (!statSync2(absProject).isDirectory()) {
5726
+ return err20(`projectDir "${absProject}" is not a directory`);
5727
+ }
5728
+ const searchRoot = chooseSearchRoot(absProject);
5729
+ const searchedDirs = [];
5730
+ const candidates = walk(searchRoot, searchedDirs);
5731
+ if (kind === "text") {
5732
+ return ok20({
5733
+ matches: [],
5734
+ searched_dirs: searchedDirs,
5735
+ total_candidates: candidates.length,
5736
+ note: TEXT_NOTE
5737
+ });
5738
+ }
5739
+ const allowedExts = KIND_EXTENSIONS[kind];
5740
+ const kindMatched = candidates.filter((c) => allowedExts.has(extname2(c.name).toLowerCase()));
5741
+ const hintFiltered = hints ? kindMatched.map((c) => ({ c, matched: matchedHints(c, hints) })).filter(({ matched }) => matched.length > 0) : kindMatched.map((c) => ({ c, matched: [] }));
5742
+ hintFiltered.sort((a, b) => b.c.mtimeMs - a.c.mtimeMs);
5743
+ const effectiveLimit = limit ?? DEFAULT_LIMIT;
5744
+ const limited = hintFiltered.slice(0, effectiveLimit);
5745
+ const matches = limited.map(({ c, matched }) => ({
5746
+ path: c.path,
5747
+ name: basename2(c.path),
5748
+ kind,
5749
+ matched_hints: matched
5750
+ }));
5751
+ return ok20({
5752
+ matches,
5753
+ searched_dirs: searchedDirs,
5754
+ total_candidates: candidates.length
5755
+ });
5756
+ }
5757
+ );
5758
+ }
5759
+
5760
+ // ../mcp/src/tools/effective-transcript.ts
5761
+ import { existsSync as existsSync4, readFileSync as readFileSync2, statSync as statSync3, readdirSync as readdirSync4 } from "fs";
5762
+ import { createHash } from "crypto";
5763
+ import { join as join4, resolve as resolve4 } from "path";
5764
+ import { z as z46 } from "zod";
5765
+ function ok21(data) {
5766
+ return { content: [{ type: "text", text: JSON.stringify({ success: true, ...data }) }] };
5767
+ }
5768
+ function err21(message) {
5769
+ return {
5770
+ content: [{ type: "text", text: JSON.stringify({ error: message }) }],
5771
+ isError: true
5772
+ };
5773
+ }
5774
+ function applyCuts(transcript, cuts, options) {
5775
+ const sortedCuts = [...cuts].sort((a, b) => a.rawStart - b.rawStart);
5776
+ const keepRanges = sortedCuts.map((c) => ({
5777
+ start: Math.max(0, c.rawStart - c.paddingPre),
5778
+ end: c.rawEnd + c.paddingPost
5779
+ }));
5780
+ function mapTime(t) {
5781
+ let cumulativeOffset = 0;
5782
+ for (const r of keepRanges) {
5783
+ if (t < r.start) return null;
5784
+ if (t <= r.end) return t - r.start + cumulativeOffset;
5785
+ cumulativeOffset += r.end - r.start;
5786
+ }
5787
+ return null;
5788
+ }
5789
+ const newSegments = [];
5790
+ for (const seg of transcript.segments) {
5791
+ const newWords = [];
5792
+ for (const w of seg.words) {
5793
+ const ns = mapTime(w.start);
5794
+ const ne = mapTime(w.end);
5795
+ if (ns === null || ne === null) continue;
5796
+ newWords.push({ ...w, start: ns, end: ne });
5797
+ }
5798
+ if (newWords.length === 0) continue;
5799
+ newSegments.push({
5800
+ text: seg.text,
5801
+ start: newWords[0].start,
5802
+ end: newWords[newWords.length - 1].end,
5803
+ words: newWords
5804
+ });
5805
+ }
5806
+ const totalCutDurationS = keepRanges.reduce((sum, r) => sum + (r.end - r.start), 0);
5807
+ const derived_from = {
5808
+ transcriptName: options.transcriptName,
5809
+ cutsHash: options.cutsHash,
5810
+ derivedAt: options.derivedAt,
5811
+ cutsApplied: sortedCuts.length,
5812
+ totalCutDurationS
5813
+ };
5814
+ return {
5815
+ version: transcript.version,
5816
+ ...transcript.language !== void 0 && { language: transcript.language },
5817
+ segments: newSegments,
5818
+ derived_from
5819
+ };
5820
+ }
5821
+ function hashCuts(cuts) {
5822
+ const canonical = cuts.map((c) => [c.rawStart, c.rawEnd, c.paddingPre, c.paddingPost]);
5823
+ return createHash("sha256").update(JSON.stringify(canonical)).digest("hex").slice(0, 16);
5824
+ }
5825
+ function discoverTranscripts(projectDir) {
5826
+ const out = [];
5827
+ const canonical = join4(projectDir, "transcript.json");
5828
+ if (existsSync4(canonical) && statSync3(canonical).isFile()) {
5829
+ out.push({ absPath: canonical, name: "transcript.json" });
5830
+ }
5831
+ const transcriptsDir = join4(projectDir, "transcripts");
5832
+ if (existsSync4(transcriptsDir) && statSync3(transcriptsDir).isDirectory()) {
5833
+ try {
5834
+ const entries = readdirSync4(transcriptsDir).filter((f) => f.endsWith(".json")).sort();
5835
+ for (const entry of entries) {
5836
+ out.push({
5837
+ absPath: join4(transcriptsDir, entry),
5838
+ name: `transcripts/${entry}`
5839
+ });
5840
+ }
5841
+ } catch {
5842
+ }
5843
+ }
5844
+ return out;
5845
+ }
5846
+ function register21(server, _store) {
5847
+ server.tool(
5848
+ "atelier_get_effective_transcript",
5849
+ "Read a project's raw transcript.json + cuts.json and return the post-cut (effective) transcript \u2014 words inside cut gaps dropped, surviving timestamps shifted to the trimmed timeline. Pure read; does not touch the document store. When cuts.json is absent or has zero cuts, returns the raw transcript verbatim (no `derived_from` stamp). Optional `transcriptName` disambiguates when the project carries multiple transcripts (transcripts/*.json); without it, transcript.json wins, and an error is returned if multiple sit in transcripts/.",
5850
+ {
5851
+ projectDir: z46.string().describe("Absolute or cwd-relative project root"),
5852
+ transcriptName: z46.string().optional().describe(
5853
+ "Optional transcript filename (e.g. 'transcript.json' or 'transcripts/take-2.json'). Disambiguates multi-transcript projects."
5854
+ )
5855
+ },
5856
+ { readOnlyHint: true, destructiveHint: false },
5857
+ async ({ projectDir, transcriptName }) => {
5858
+ const absProject = resolve4(projectDir);
5859
+ if (!existsSync4(absProject)) {
5860
+ return err21(`projectDir "${absProject}" does not exist`);
5861
+ }
5862
+ if (!statSync3(absProject).isDirectory()) {
5863
+ return err21(`projectDir "${absProject}" is not a directory`);
5864
+ }
5865
+ let chosen;
5866
+ if (transcriptName !== void 0) {
5867
+ const absPath = transcriptName.startsWith("/") ? transcriptName : join4(absProject, transcriptName);
5868
+ if (!existsSync4(absPath) || !statSync3(absPath).isFile()) {
5869
+ return err21(`transcript "${transcriptName}" not found in ${absProject}`);
5870
+ }
5871
+ chosen = { absPath, name: transcriptName };
5872
+ } else {
5873
+ const candidates = discoverTranscripts(absProject);
5874
+ if (candidates.length === 0) {
5875
+ return err21(`no transcript found in ${absProject} (looked for transcript.json and transcripts/*.json)`);
5876
+ }
5877
+ if (candidates.length > 1) {
5878
+ return err21(
5879
+ `multiple transcripts present in ${absProject}; pass transcriptName explicitly. Candidates: ${candidates.map((c) => c.name).join(", ")}`
5880
+ );
5881
+ }
5882
+ chosen = candidates[0];
5883
+ }
5884
+ let rawTranscript;
5885
+ try {
5886
+ rawTranscript = JSON.parse(readFileSync2(chosen.absPath, "utf-8"));
5887
+ } catch (e) {
5888
+ return err21(`failed to parse transcript at ${chosen.absPath}: ${e.message}`);
5889
+ }
5890
+ const cutsPath = join4(absProject, "cuts.json");
5891
+ if (!existsSync4(cutsPath)) {
5892
+ return ok21({
5893
+ effective: rawTranscript,
5894
+ source: {
5895
+ transcriptName: chosen.name,
5896
+ cutsApplied: 0,
5897
+ totalCutDurationS: 0
5898
+ }
5899
+ });
5900
+ }
5901
+ let rawCuts;
5902
+ try {
5903
+ rawCuts = JSON.parse(readFileSync2(cutsPath, "utf-8"));
5904
+ } catch (e) {
5905
+ return err21(`failed to parse cuts at ${cutsPath}: ${e.message}`);
5906
+ }
5907
+ const cuts = rawCuts.cuts ?? [];
5908
+ if (cuts.length === 0) {
5909
+ return ok21({
5910
+ effective: rawTranscript,
5911
+ source: {
5912
+ transcriptName: chosen.name,
5913
+ cutsApplied: 0,
5914
+ totalCutDurationS: 0
5915
+ }
5916
+ });
5917
+ }
5918
+ const effective = applyCuts(rawTranscript, cuts, {
5919
+ transcriptName: chosen.name,
5920
+ cutsHash: hashCuts(cuts),
5921
+ derivedAt: (/* @__PURE__ */ new Date()).toISOString()
5922
+ });
5923
+ return ok21({
5924
+ effective,
5925
+ source: {
5926
+ transcriptName: chosen.name,
5927
+ cutsApplied: effective.derived_from?.cutsApplied ?? cuts.length,
5928
+ totalCutDurationS: effective.derived_from?.totalCutDurationS ?? 0
5929
+ }
5930
+ });
5931
+ }
5932
+ );
5933
+ }
5934
+
5935
+ // ../mcp/src/tools/media-notes.ts
5936
+ import { existsSync as existsSync5, readFileSync as readFileSync3, statSync as statSync4, writeFileSync as writeFileSync2 } from "fs";
5937
+ import { join as join5, resolve as resolve5 } from "path";
5938
+ import { z as z47 } from "zod";
5939
+ import { parse as parseYaml2, stringify as stringifyYaml2 } from "yaml";
5940
+ function ok22(data) {
5941
+ return { content: [{ type: "text", text: JSON.stringify({ success: true, ...data }) }] };
5942
+ }
5943
+ function err22(message) {
5944
+ return {
5945
+ content: [{ type: "text", text: JSON.stringify({ error: message }) }],
5946
+ isError: true
5947
+ };
5948
+ }
5949
+ var MEDIA_NOTES_SUFFIX = ".notes.md";
5950
+ function resolveMediaPaths(projectDir, filename) {
5951
+ const absProject = resolve5(projectDir);
5952
+ if (!existsSync5(absProject) || !statSync4(absProject).isDirectory()) {
5953
+ return { error: `projectDir "${absProject}" is not a directory` };
5954
+ }
5955
+ if (filename.includes("/") || filename.includes("\\") || filename === "..") {
5956
+ return { error: `filename must be a bare basename (got "${filename}")` };
5957
+ }
5958
+ const mediaPath = join5(absProject, "media", filename);
5959
+ const notesPath = `${mediaPath}${MEDIA_NOTES_SUFFIX}`;
5960
+ return { project: absProject, mediaPath, notesPath };
5961
+ }
5962
+ function splitFrontmatter(raw) {
5963
+ const trimmed = raw.replace(/^/, "");
5964
+ if (!trimmed.startsWith("---")) return null;
5965
+ const lines = trimmed.split(/\r?\n/);
5966
+ if (lines[0].trim() !== "---") return null;
5967
+ let closeIdx = -1;
5968
+ for (let i = 1; i < lines.length; i++) {
5969
+ if (lines[i].trim() === "---") {
5970
+ closeIdx = i;
5971
+ break;
5972
+ }
5973
+ }
5974
+ if (closeIdx === -1) return null;
5975
+ return {
5976
+ frontmatter: lines.slice(1, closeIdx).join("\n"),
5977
+ body: lines.slice(closeIdx + 1).join("\n").replace(/^\n+/, "")
5978
+ };
5979
+ }
5980
+ function readNotesFile(notesPath) {
5981
+ if (!existsSync5(notesPath)) return null;
5982
+ const raw = readFileSync3(notesPath, "utf-8");
5983
+ const split = splitFrontmatter(raw);
5984
+ if (split === null) return null;
5985
+ let parsed;
5986
+ try {
5987
+ parsed = parseYaml2(split.frontmatter);
5988
+ } catch {
5989
+ return null;
5990
+ }
5991
+ const validated = MediaNotesFrontmatterSchema.safeParse(parsed);
5992
+ if (!validated.success) return null;
5993
+ return { frontmatter: validated.data, body: split.body };
5994
+ }
5995
+ function writeNotesFile(notesPath, notes) {
5996
+ const yamlText = stringifyYaml2(notes.frontmatter);
5997
+ const body = notes.body.endsWith("\n") ? notes.body : `${notes.body}
5998
+ `;
5999
+ writeFileSync2(notesPath, `---
6000
+ ${yamlText}---
6001
+
6002
+ ${body}`, "utf-8");
6003
+ }
6004
+ function upsertSection(body, heading, newBlock) {
6005
+ const lines = body.split(/\r?\n/);
6006
+ let startIdx = -1;
6007
+ for (let i = 0; i < lines.length; i++) {
6008
+ if (lines[i].trim() === heading) {
6009
+ startIdx = i;
6010
+ break;
6011
+ }
6012
+ }
6013
+ if (startIdx === -1) {
6014
+ const existing = body.trimEnd();
6015
+ return existing.length > 0 ? `${existing}
6016
+
6017
+ ${newBlock.trimEnd()}
6018
+ ` : `${newBlock.trimEnd()}
6019
+ `;
6020
+ }
6021
+ let endIdx = lines.length;
6022
+ for (let i = startIdx + 1; i < lines.length; i++) {
6023
+ if (lines[i].startsWith("## ")) {
6024
+ endIdx = i;
6025
+ break;
6026
+ }
6027
+ }
6028
+ const before = lines.slice(0, startIdx);
6029
+ const after = lines.slice(endIdx);
6030
+ const replacement = newBlock.split(/\r?\n/);
6031
+ while (replacement.length > 0 && replacement[replacement.length - 1] === "") {
6032
+ replacement.pop();
6033
+ }
6034
+ const joined = [...before, ...replacement];
6035
+ if (after.length > 0 && after[0].startsWith("## ")) {
6036
+ joined.push("");
6037
+ }
6038
+ joined.push(...after);
6039
+ return joined.join("\n").replace(/\n+$/, "\n");
6040
+ }
6041
+ function register22(server, _store) {
6042
+ server.tool(
6043
+ "atelier_get_media_notes",
6044
+ "Read the sidecar notes file for a media entry \u2014 <projectDir>/media/<filename>.notes.md. Returns the parsed frontmatter (file, added, added_by, tags, optional constraints) plus the Markdown body. Returns an error envelope when the notes file is absent or malformed; pure read, does not touch the document store.",
6045
+ {
6046
+ projectDir: z47.string().describe("Absolute or cwd-relative project root"),
6047
+ filename: z47.string().describe("Bare basename of the media file (e.g. 'b-roll-laptop.mp4')")
6048
+ },
6049
+ { readOnlyHint: true, destructiveHint: false },
6050
+ async ({ projectDir, filename }) => {
6051
+ const paths = resolveMediaPaths(projectDir, filename);
6052
+ if ("error" in paths) return err22(paths.error);
6053
+ if (!existsSync5(paths.notesPath)) {
6054
+ return err22(`notes file not found: ${paths.notesPath}`);
6055
+ }
6056
+ const notes = readNotesFile(paths.notesPath);
6057
+ if (notes === null) {
6058
+ return err22(`notes file at ${paths.notesPath} is malformed (frontmatter parse / schema validation failed)`);
6059
+ }
6060
+ return ok22({
6061
+ path: paths.notesPath,
6062
+ frontmatter: notes.frontmatter,
6063
+ body: notes.body
6064
+ });
6065
+ }
6066
+ );
6067
+ server.tool(
6068
+ "atelier_write_media_note",
6069
+ "Append or replace an author's section in a media file's sidecar notes. Section boundaries are detected by the per-author sentinel headings (## Lux notes / Quill notes / Iris notes / Creator notes); a second call with the same author replaces the prior section rather than duplicating. The notes file MUST exist already (scaffold via `atelier media ingest` or `atelier media note`). Mutates disk; the calling agent is tagged via the `author` argument.",
6070
+ {
6071
+ projectDir: z47.string().describe("Absolute or cwd-relative project root"),
6072
+ filename: z47.string().describe("Bare basename of the media file"),
6073
+ author: MediaNotesAuthorSchema.describe("Section owner: lux | quill | iris | creator"),
6074
+ section: z47.string().describe("Markdown body for the section (heading is added automatically)")
6075
+ },
6076
+ { readOnlyHint: false, destructiveHint: false },
6077
+ async ({ projectDir, filename, author, section }) => {
6078
+ const paths = resolveMediaPaths(projectDir, filename);
6079
+ if ("error" in paths) return err22(paths.error);
6080
+ if (!existsSync5(paths.notesPath)) {
6081
+ return err22(
6082
+ `notes file not found: ${paths.notesPath} \u2014 scaffold via \`atelier media ingest\` or \`atelier media note\` first`
6083
+ );
6084
+ }
6085
+ const existing = readNotesFile(paths.notesPath);
6086
+ if (existing === null) {
6087
+ return err22(
6088
+ `notes file at ${paths.notesPath} is malformed \u2014 refusing to write into a file with unparseable frontmatter`
6089
+ );
6090
+ }
6091
+ const typedAuthor = author;
6092
+ const heading = MEDIA_NOTES_HEADINGS[typedAuthor];
6093
+ const trimmed = section.trim();
6094
+ const newBlock = `${heading}
6095
+ ${trimmed}
6096
+ `;
6097
+ const hasExistingSection = existing.body.split(/\r?\n/).some((l) => l.trim() === heading);
6098
+ const updatedBody = upsertSection(existing.body, heading, newBlock);
6099
+ const updated = { frontmatter: existing.frontmatter, body: updatedBody };
6100
+ writeNotesFile(paths.notesPath, updated);
6101
+ return ok22({
6102
+ path: paths.notesPath,
6103
+ action: hasExistingSection ? "replaced" : "appended",
6104
+ author: typedAuthor
6105
+ });
6106
+ }
6107
+ );
6108
+ }
6109
+
6110
+ // ../mcp/src/tools/clips.ts
6111
+ import { existsSync as existsSync6, readFileSync as readFileSync4, writeFileSync as writeFileSync3 } from "fs";
6112
+ import { basename as basename4, extname as extname4, isAbsolute as isAbsolute2, join as join6, resolve as resolve6 } from "path";
6113
+ import { z as z48 } from "zod";
6114
+
6115
+ // ../mcp/src/lib/timeline-ops.ts
6116
+ import { basename as basename3, extname as extname3 } from "path";
6117
+ import { spawn } from "child_process";
6118
+ var VIDEO_TRACK_ID = "track-video-1";
6119
+ var AUDIO_TRACK_ID = "track-audio-1";
6120
+ function slugifyBasename(name) {
6121
+ const base = basename3(name);
6122
+ const stem = stripExt(base);
6123
+ const lower = stem.toLowerCase();
6124
+ const replaced = lower.replace(/[^a-z0-9]+/g, "-");
6125
+ return replaced.replace(/^-+|-+$/g, "");
6126
+ }
6127
+ function stripExt(name) {
6128
+ const ext = extname3(name);
6129
+ return ext ? name.slice(0, -ext.length) : name;
6130
+ }
6131
+ function endOfTrackFrame(layers, trackId, fps) {
6132
+ let maxEnd = 0;
6133
+ for (const layer of layers) {
6134
+ if (layer.parentId !== trackId) continue;
6135
+ const v = layer.visual;
6136
+ if (v.type !== "video" && v.type !== "audio") continue;
6137
+ const tv = v;
6138
+ const start = tv.startFrame ?? 0;
6139
+ const offset = tv.sourceOffset ?? 0;
6140
+ const end = tv.sourceEnd;
6141
+ if (end === void 0) continue;
6142
+ const durFrames = Math.max(0, Math.round((end - offset) * fps));
6143
+ const layerEnd = start + durFrames;
6144
+ if (layerEnd > maxEnd) maxEnd = layerEnd;
6145
+ }
6146
+ return maxEnd;
6147
+ }
6148
+ function ensureTrackLayer(doc, trackId, kind) {
6149
+ const exists = doc.layers.some((l) => l.id === trackId);
6150
+ if (exists) return { doc, created: false };
6151
+ const trackLayer = {
6152
+ id: trackId,
6153
+ tags: ["track", kind],
6154
+ description: kind === "video" ? "Video track 1" : "Audio track 1",
6155
+ visual: { type: "group" },
6156
+ frame: { x: 0, y: 0 },
6157
+ bounds: { width: doc.canvas.width, height: doc.canvas.height }
6158
+ };
6159
+ return {
6160
+ doc: { ...doc, layers: [trackLayer, ...doc.layers] },
6161
+ created: true
6162
+ };
6163
+ }
6164
+ function ensureClipLayer(doc, args) {
6165
+ const existing = doc.layers.find((l) => l.id === args.clipLayerId);
6166
+ if (existing) {
6167
+ return { doc, addedLayer: false, clipLayerId: args.clipLayerId };
6168
+ }
6169
+ const ensured = ensureTrackLayer(doc, args.trackId, args.kind);
6170
+ const working = ensured.doc;
6171
+ const startFrame = args.startFrame !== void 0 ? args.startFrame : endOfTrackFrame(working.layers, args.trackId, working.canvas.fps);
6172
+ const sourceOffset = args.sourceOffset ?? 0;
6173
+ const sourceEnd = args.sourceEnd ?? args.clipDurationSeconds;
6174
+ const newAsset = {
6175
+ type: args.kind === "video" ? "video" : "audio",
6176
+ src: args.sourceName,
6177
+ description: `Source: ${args.sourceName}`
6178
+ };
6179
+ const assets = {
6180
+ ...working.assets ?? {},
6181
+ [args.clipLayerId]: newAsset
6182
+ };
6183
+ let newClip;
6184
+ if (args.kind === "video") {
6185
+ const visual = {
6186
+ type: "video",
6187
+ assetId: args.clipLayerId,
6188
+ src: args.sourceName,
6189
+ startFrame,
6190
+ sourceOffset,
6191
+ sourceEnd,
6192
+ playbackRate: 1,
6193
+ objectFit: "contain"
6194
+ };
6195
+ newClip = {
6196
+ id: args.clipLayerId,
6197
+ parentId: args.trackId,
6198
+ visual,
6199
+ frame: { x: 0, y: 0 },
6200
+ bounds: { width: working.canvas.width, height: working.canvas.height }
6201
+ };
6202
+ } else {
6203
+ const visual = {
6204
+ type: "audio",
6205
+ assetId: args.clipLayerId,
6206
+ src: args.sourceName,
6207
+ startFrame,
6208
+ sourceOffset,
6209
+ sourceEnd
6210
+ };
6211
+ newClip = {
6212
+ id: args.clipLayerId,
6213
+ parentId: args.trackId,
6214
+ visual,
6215
+ frame: { x: 0, y: 0 },
6216
+ bounds: { width: 0, height: 0 }
6217
+ };
6218
+ }
6219
+ return {
6220
+ doc: {
6221
+ ...working,
6222
+ assets,
6223
+ layers: [...working.layers, newClip]
6224
+ },
6225
+ addedLayer: true,
6226
+ clipLayerId: args.clipLayerId
6227
+ };
6228
+ }
6229
+ function recomputeTimeline(doc) {
6230
+ const fps = doc.canvas.fps;
6231
+ const trackGroups = doc.layers.filter(
6232
+ (l) => l.visual.type === "group" && (l.tags ?? []).includes("track")
6233
+ );
6234
+ if (trackGroups.length === 0) {
6235
+ if (doc.timeline === void 0) return doc;
6236
+ const { timeline: _omit, ...rest } = doc;
6237
+ void _omit;
6238
+ return rest;
6239
+ }
6240
+ const tracks = [];
6241
+ let totalFrames = 0;
6242
+ for (const track of trackGroups) {
6243
+ const tags = track.tags ?? [];
6244
+ const kind = tags.includes("audio") ? "audio" : "video";
6245
+ const children = doc.layers.filter(
6246
+ (l) => l.parentId === track.id && l.visual.type === kind
6247
+ );
6248
+ const clips = children.map((layer) => {
6249
+ const v = layer.visual;
6250
+ const start = v.startFrame ?? 0;
6251
+ const offset = v.sourceOffset ?? 0;
6252
+ const end = v.sourceEnd ?? offset;
6253
+ const durFrames = Math.max(0, Math.round((end - offset) * fps));
6254
+ return {
6255
+ layerId: layer.id,
6256
+ startFrame: start,
6257
+ endFrame: start + durFrames,
6258
+ source: v.src ?? ""
6259
+ };
6260
+ }).sort((a, b) => a.startFrame - b.startFrame);
6261
+ const trackEnd = clips.reduce((m, c) => Math.max(m, c.endFrame), 0);
6262
+ if (trackEnd > totalFrames) totalFrames = trackEnd;
6263
+ tracks.push({ id: track.id, kind, clips });
6264
+ }
6265
+ const timeline = { fps, totalFrames, tracks };
6266
+ return { ...doc, timeline };
6267
+ }
6268
+ var DEFAULT_STYLE = {
6269
+ fontFamily: "Inter",
6270
+ fontSize: 84,
6271
+ fontWeight: "bold",
6272
+ textAlign: "center",
6273
+ color: "#FFFFFF",
6274
+ yRatio: 0.85,
6275
+ widthRatio: 0.9,
6276
+ fadeSeconds: 0.05
6277
+ };
6278
+ var DEFAULT_PHRASE_MAX_WORDS = 5;
6279
+ var DEFAULT_PHRASE_PAUSE_GAP_SECONDS = 0.4;
6280
+ var END_PUNCT = /[.!?,;:]\s*$/;
6281
+ function flattenWords(transcript) {
6282
+ return transcript.segments.flatMap((s) => s.words);
6283
+ }
6284
+ function groupIntoPhrases(transcript, options = {}) {
6285
+ const maxWords = options.maxWords ?? DEFAULT_PHRASE_MAX_WORDS;
6286
+ const pauseGap = options.pauseGap ?? DEFAULT_PHRASE_PAUSE_GAP_SECONDS;
6287
+ const phrases = [];
6288
+ const visibleWords = flattenWords(transcript).filter((w) => !w.hidden);
6289
+ let current = [];
6290
+ for (let i = 0; i < visibleWords.length; i++) {
6291
+ const w = visibleWords[i];
6292
+ current.push(w);
6293
+ const next = visibleWords[i + 1];
6294
+ const gap = next ? next.start - w.end : 0;
6295
+ const endsOnPunct = END_PUNCT.test(w.text);
6296
+ const atMax = current.length >= maxWords;
6297
+ if (!next || atMax || endsOnPunct || gap > pauseGap) {
6298
+ phrases.push({
6299
+ start: current[0].start,
6300
+ end: current[current.length - 1].end,
6301
+ text: current.map((c) => c.text).join(" "),
6302
+ words: current
6303
+ });
6304
+ current = [];
6305
+ }
6306
+ }
6307
+ return phrases;
6308
+ }
6309
+ function buildCaptionLayers(transcript, canvas, options = {}) {
6310
+ const style = { ...DEFAULT_STYLE, ...options.style };
6311
+ const phrases = groupIntoPhrases(transcript, {
6312
+ maxWords: options.maxWords,
6313
+ pauseGap: options.pauseGap
6314
+ });
6315
+ const layers = [];
6316
+ const deltas = [];
6317
+ const fps = canvas.fps;
6318
+ phrases.forEach((phrase, idx) => {
6319
+ const layerId = `caption-${idx}`;
6320
+ layers.push(buildPhraseLayer(layerId, phrase, canvas, style));
6321
+ deltas.push(...buildPhraseDeltas(layerId, phrase, style.fadeSeconds, fps));
6322
+ });
6323
+ return { layers, deltas };
6324
+ }
6325
+ function buildPhraseLayer(id, phrase, canvas, style) {
6326
+ return {
6327
+ id,
6328
+ tags: ["caption"],
6329
+ visual: {
6330
+ type: "text",
6331
+ content: phrase.text,
6332
+ style: {
6333
+ fontFamily: style.fontFamily,
6334
+ fontSize: style.fontSize,
6335
+ fontWeight: style.fontWeight,
6336
+ textAlign: style.textAlign,
6337
+ color: style.color
6338
+ }
6339
+ },
6340
+ frame: {
6341
+ x: canvas.width / 2,
6342
+ y: canvas.height * style.yRatio
6343
+ },
6344
+ bounds: {
6345
+ width: canvas.width * style.widthRatio,
6346
+ height: Math.max(120, style.fontSize * 1.6)
6347
+ },
6348
+ anchorPoint: { x: 0.5, y: 0.5 },
6349
+ opacity: 0
6350
+ };
6351
+ }
6352
+ function buildPhraseDeltas(layerId, phrase, fadeSeconds, fps) {
6353
+ const fadeFrames = Math.max(1, Math.round(fadeSeconds * fps));
6354
+ const startFrame = Math.floor(phrase.start * fps);
6355
+ const endFrame = Math.ceil(phrase.end * fps);
6356
+ const fadeOutStart = Math.max(startFrame + 1, endFrame - fadeFrames);
6357
+ return [
6358
+ {
6359
+ layer: layerId,
6360
+ property: "opacity",
6361
+ range: [Math.max(0, startFrame - fadeFrames), startFrame],
6362
+ from: 0,
6363
+ to: 1,
6364
+ easing: "ease-out"
6365
+ },
6366
+ {
6367
+ layer: layerId,
6368
+ property: "opacity",
6369
+ range: [fadeOutStart, endFrame],
6370
+ from: 1,
6371
+ to: 0,
6372
+ easing: "ease-in"
6373
+ }
6374
+ ];
6375
+ }
6376
+ function stripCaptionsForClip(doc, clipLayerId) {
6377
+ const droppedIds = /* @__PURE__ */ new Set();
6378
+ const keptLayers = doc.layers.filter((layer) => {
6379
+ if (!(layer.tags ?? []).includes("caption")) return true;
6380
+ if (layer.parentId === clipLayerId) {
6381
+ droppedIds.add(layer.id);
6382
+ return false;
6383
+ }
6384
+ return true;
6385
+ });
6386
+ if (droppedIds.size === 0) return doc;
6387
+ const states = {};
6388
+ for (const [name, st] of Object.entries(doc.states)) {
6389
+ states[name] = {
6390
+ ...st,
6391
+ deltas: st.deltas.filter((d) => !droppedIds.has(d.layer))
6392
+ };
6393
+ }
6394
+ return { ...doc, layers: keptLayers, states };
6395
+ }
6396
+ function rebuildClipCaptions(doc, clipLayerId, transcript, canvas, captionOptions) {
6397
+ const clipSlug = clipLayerId.startsWith("clip-") ? clipLayerId.slice("clip-".length) : clipLayerId;
6398
+ let working = stripCaptionsForClip(doc, clipLayerId);
6399
+ const built = buildCaptionLayers(transcript, canvas, captionOptions ?? {});
6400
+ const idMap = /* @__PURE__ */ new Map();
6401
+ const renamedLayers = built.layers.map((layer, idx) => {
6402
+ const newId = `cap-${clipSlug}-${idx}`;
6403
+ idMap.set(layer.id, newId);
6404
+ return { ...layer, id: newId, parentId: clipLayerId };
6405
+ });
6406
+ const renamedDeltas = built.deltas.map((d) => ({
6407
+ ...d,
6408
+ layer: idMap.get(d.layer) ?? d.layer
6409
+ }));
6410
+ working = { ...working, layers: [...working.layers, ...renamedLayers] };
6411
+ const stateNames = Object.keys(working.states);
6412
+ const targetStateName = stateNames.includes("default") ? "default" : stateNames[0] ?? "default";
6413
+ const states = { ...working.states };
6414
+ const existing = states[targetStateName];
6415
+ if (existing) {
6416
+ states[targetStateName] = {
6417
+ ...existing,
6418
+ deltas: [...existing.deltas, ...renamedDeltas]
6419
+ };
6420
+ } else {
6421
+ states[targetStateName] = { duration: 0, deltas: renamedDeltas };
6422
+ }
6423
+ return { ...working, states };
6424
+ }
6425
+ async function probeDuration(sourcePath) {
6426
+ const stdout = await runCapture("ffprobe", [
6427
+ "-v",
6428
+ "error",
6429
+ "-show_entries",
6430
+ "format=duration",
6431
+ "-of",
6432
+ "csv=p=0",
6433
+ sourcePath
6434
+ ]);
6435
+ const n = parseFloat(stdout.trim());
6436
+ if (!Number.isFinite(n) || n <= 0) {
6437
+ throw new Error(`ffprobe returned invalid duration for ${sourcePath}: "${stdout}"`);
6438
+ }
6439
+ return n;
6440
+ }
6441
+ function runCapture(cmd, args) {
6442
+ return new Promise((resolvePromise, reject) => {
6443
+ const child = spawn(cmd, args);
6444
+ let stdout = "";
6445
+ let stderr = "";
6446
+ child.stdout.on("data", (d) => {
6447
+ stdout += d.toString();
6448
+ });
6449
+ child.stderr.on("data", (d) => {
6450
+ stderr += d.toString();
6451
+ });
6452
+ child.on("error", reject);
6453
+ child.on("close", (code) => {
6454
+ if (code === 0) resolvePromise(stdout);
6455
+ else reject(new Error(`${cmd} exited ${code}: ${stderr}`));
6456
+ });
6457
+ });
6458
+ }
6459
+
6460
+ // ../mcp/src/tools/clips.ts
6461
+ function ok23(data) {
6462
+ return { content: [{ type: "text", text: JSON.stringify({ success: true, ...data }) }] };
6463
+ }
6464
+ function err23(message) {
6465
+ return {
6466
+ content: [{ type: "text", text: JSON.stringify({ error: message }) }],
6467
+ isError: true
6468
+ };
6469
+ }
6470
+ function loadDocFromDisk(projectDir) {
6471
+ const absProject = resolve6(projectDir);
6472
+ const compositionPath = join6(absProject, "project.atelier");
6473
+ if (!existsSync6(compositionPath)) {
6474
+ return { error: `project.atelier not found at ${compositionPath}` };
6475
+ }
6476
+ let raw;
6477
+ try {
6478
+ raw = readFileSync4(compositionPath, "utf-8");
6479
+ } catch (e) {
6480
+ return { error: `failed to read ${compositionPath}: ${e.message}` };
6481
+ }
6482
+ let doc;
6483
+ try {
6484
+ doc = JSON.parse(raw);
6485
+ } catch (e) {
6486
+ return { error: `failed to parse ${compositionPath}: ${e.message}` };
6487
+ }
6488
+ return { doc, compositionPath };
6489
+ }
6490
+ function persistDoc(store, documentId, doc, compositionPath) {
6491
+ writeFileSync3(compositionPath, JSON.stringify(doc, null, 2), "utf-8");
6492
+ if (store.has(documentId)) {
6493
+ store.set(documentId, doc, "llm");
6494
+ } else {
6495
+ store.create(doc, documentId, "llm");
6496
+ }
6497
+ }
6498
+ function canonicalSourceName(source, projectDir) {
6499
+ const absProject = resolve6(projectDir);
6500
+ const abs = isAbsolute2(source) ? source : resolve6(absProject, source);
6501
+ if (abs.startsWith(`${absProject}/`)) {
6502
+ return { srcRef: abs.slice(absProject.length + 1), absPath: abs };
6503
+ }
6504
+ return { srcRef: basename4(abs), absPath: abs };
6505
+ }
6506
+ function clipEndFrame(visual, fps) {
6507
+ const start = visual.startFrame ?? 0;
6508
+ const offset = visual.sourceOffset ?? 0;
6509
+ const end = visual.sourceEnd ?? offset;
6510
+ const durFrames = Math.max(0, Math.round((end - offset) * fps));
6511
+ return start + durFrames;
6512
+ }
6513
+ var injectedTranscribeFn;
6514
+ var injectedProbeDurationFn;
6515
+ function __setTranscribeClipFn(fn) {
6516
+ injectedTranscribeFn = fn;
6517
+ }
6518
+ function __setProbeDurationFn(fn) {
6519
+ injectedProbeDurationFn = fn;
6520
+ }
6521
+ function register23(server, store) {
6522
+ server.tool(
6523
+ "atelier_add_clip",
6524
+ "Register a media source as a clip layer on a video or audio track. Idempotent: re-running with the same source preserves the existing clip's placement and applies only explicitly-passed startFrame/sourceOffset/sourceEnd overrides.",
6525
+ {
6526
+ documentId: z48.string().describe("In-memory document ID to reload after disk write"),
6527
+ projectDir: z48.string().describe("Absolute path to the project root (contains project.atelier)"),
6528
+ source: z48.string().describe("Absolute media path OR project-relative path (e.g. media/clip.mp4)"),
6529
+ kind: z48.enum(["video", "audio"]).optional().describe("Clip kind (default: video)"),
6530
+ trackId: z48.string().optional().describe("Track layer id (default: track-video-1 / track-audio-1)"),
6531
+ startFrame: z48.number().int().nonnegative().optional().describe("Composition-frame start (default: end-of-track)"),
6532
+ sourceOffset: z48.number().nonnegative().optional().describe("Seconds into source to begin from"),
6533
+ sourceEnd: z48.number().positive().optional().describe("Seconds into source to stop at"),
6534
+ layerIdHint: z48.string().optional().describe("Explicit layer id override (default: clip-<slug> or aud-<slug>)")
6535
+ },
6536
+ { readOnlyHint: false, destructiveHint: false },
6537
+ async ({ documentId, projectDir, source, kind, trackId, startFrame, sourceOffset, sourceEnd, layerIdHint }) => {
6538
+ const loaded = loadDocFromDisk(projectDir);
6539
+ if ("error" in loaded) return err23(loaded.error);
6540
+ let { doc } = loaded;
6541
+ const { compositionPath } = loaded;
6542
+ const clipKind = kind ?? "video";
6543
+ const resolvedTrackId = trackId ?? (clipKind === "video" ? VIDEO_TRACK_ID : AUDIO_TRACK_ID);
6544
+ const { srcRef, absPath } = canonicalSourceName(source, projectDir);
6545
+ const slug = slugifyBasename(srcRef);
6546
+ if (!slug) return err23(`cannot derive a slug from source "${source}"`);
6547
+ const layerId = layerIdHint ?? (clipKind === "video" ? `clip-${slug}` : `aud-${slug}`);
6548
+ const probeFn = injectedProbeDurationFn ?? probeDuration;
6549
+ let clipDurationSeconds = 0;
6550
+ try {
6551
+ clipDurationSeconds = await probeFn(absPath);
6552
+ } catch {
6553
+ clipDurationSeconds = 0;
6554
+ }
6555
+ const fps = doc.canvas.fps;
6556
+ const clipDurationFrames = Math.max(1, Math.round(clipDurationSeconds * fps));
6557
+ const existing = doc.layers.find((l) => l.id === layerId);
6558
+ let addedLayer = false;
6559
+ if (existing) {
6560
+ if (existing.visual.type !== clipKind) {
6561
+ return err23(`layer "${layerId}" exists but is type "${existing.visual.type}", not "${clipKind}"`);
6562
+ }
6563
+ const v = existing.visual;
6564
+ const patched = { ...v };
6565
+ if (startFrame !== void 0) patched.startFrame = startFrame;
6566
+ if (sourceOffset !== void 0) patched.sourceOffset = sourceOffset;
6567
+ if (sourceEnd !== void 0) patched.sourceEnd = sourceEnd;
6568
+ doc = {
6569
+ ...doc,
6570
+ layers: doc.layers.map((l) => l.id === layerId ? { ...l, visual: patched } : l)
6571
+ };
6572
+ } else {
6573
+ const ensured = ensureClipLayer(doc, {
6574
+ clipLayerId: layerId,
6575
+ trackId: resolvedTrackId,
6576
+ sourceName: srcRef,
6577
+ clipDurationSeconds,
6578
+ clipDurationFrames,
6579
+ kind: clipKind,
6580
+ ...startFrame !== void 0 ? { startFrame } : {},
6581
+ ...sourceOffset !== void 0 ? { sourceOffset } : {},
6582
+ ...sourceEnd !== void 0 ? { sourceEnd } : {}
6583
+ });
6584
+ doc = ensured.doc;
6585
+ addedLayer = ensured.addedLayer;
6586
+ }
6587
+ doc = recomputeTimeline(doc);
6588
+ const final = doc.layers.find((l) => l.id === layerId);
6589
+ if (!final) return err23(`internal: layer "${layerId}" missing after ensureClipLayer`);
6590
+ const visual = final.visual;
6591
+ const resolvedStart = visual.startFrame ?? 0;
6592
+ const resolvedEnd = clipEndFrame(visual, fps);
6593
+ persistDoc(store, documentId, doc, compositionPath);
6594
+ return ok23({
6595
+ layerId,
6596
+ trackId: resolvedTrackId,
6597
+ startFrame: resolvedStart,
6598
+ endFrame: resolvedEnd,
6599
+ addedLayer
6600
+ });
6601
+ }
6602
+ );
6603
+ server.tool(
6604
+ "atelier_move_clip",
6605
+ "Shift a clip's composition-frame start. Caption layers parented to the clip stay aligned automatically (renderer uses clip-local time). Recomputes the timeline.",
6606
+ {
6607
+ documentId: z48.string().describe("In-memory document ID to reload after disk write"),
6608
+ projectDir: z48.string().describe("Absolute path to the project root"),
6609
+ layerId: z48.string().describe("Layer id of the clip to move"),
6610
+ newStartFrame: z48.number().int().nonnegative().describe("New composition-frame start")
6611
+ },
6612
+ { readOnlyHint: false, destructiveHint: false },
6613
+ async ({ documentId, projectDir, layerId, newStartFrame }) => {
6614
+ const loaded = loadDocFromDisk(projectDir);
6615
+ if ("error" in loaded) return err23(loaded.error);
6616
+ let { doc } = loaded;
6617
+ const { compositionPath } = loaded;
6618
+ const layer = doc.layers.find((l) => l.id === layerId);
6619
+ if (!layer) return err23(`clip layer "${layerId}" not found`);
6620
+ if (layer.visual.type !== "video" && layer.visual.type !== "audio") {
6621
+ return err23(`layer "${layerId}" is type "${layer.visual.type}", not a clip`);
6622
+ }
6623
+ const oldVisual = layer.visual;
6624
+ const oldStartFrame = oldVisual.startFrame ?? 0;
6625
+ const newVisual = { ...oldVisual, startFrame: newStartFrame };
6626
+ doc = {
6627
+ ...doc,
6628
+ layers: doc.layers.map((l) => l.id === layerId ? { ...l, visual: newVisual } : l)
6629
+ };
6630
+ doc = recomputeTimeline(doc);
6631
+ const endFrame = clipEndFrame(newVisual, doc.canvas.fps);
6632
+ persistDoc(store, documentId, doc, compositionPath);
6633
+ return ok23({ layerId, oldStartFrame, newStartFrame, endFrame });
6634
+ }
6635
+ );
6636
+ server.tool(
6637
+ "atelier_trim_clip",
6638
+ "Adjust a clip's sourceOffset / sourceEnd (in seconds). Captions outside the new effective range are not auto-pruned \u2014 the renderer skips out-of-range deltas.",
6639
+ {
6640
+ documentId: z48.string().describe("In-memory document ID to reload after disk write"),
6641
+ projectDir: z48.string().describe("Absolute path to the project root"),
6642
+ layerId: z48.string().describe("Layer id of the clip to trim"),
6643
+ sourceOffset: z48.number().nonnegative().optional().describe("New seconds-into-source start"),
6644
+ sourceEnd: z48.number().positive().optional().describe("New seconds-into-source end")
6645
+ },
6646
+ { readOnlyHint: false, destructiveHint: false },
6647
+ async ({ documentId, projectDir, layerId, sourceOffset, sourceEnd }) => {
6648
+ if (sourceOffset === void 0 && sourceEnd === void 0) {
6649
+ return err23("at least one of sourceOffset / sourceEnd must be provided");
6650
+ }
6651
+ const loaded = loadDocFromDisk(projectDir);
6652
+ if ("error" in loaded) return err23(loaded.error);
6653
+ let { doc } = loaded;
6654
+ const { compositionPath } = loaded;
6655
+ const layer = doc.layers.find((l) => l.id === layerId);
6656
+ if (!layer) return err23(`clip layer "${layerId}" not found`);
6657
+ if (layer.visual.type !== "video" && layer.visual.type !== "audio") {
6658
+ return err23(`layer "${layerId}" is type "${layer.visual.type}", not a clip`);
6659
+ }
6660
+ const oldVisual = layer.visual;
6661
+ const newVisual = { ...oldVisual };
6662
+ if (sourceOffset !== void 0) newVisual.sourceOffset = sourceOffset;
6663
+ if (sourceEnd !== void 0) newVisual.sourceEnd = sourceEnd;
6664
+ doc = {
6665
+ ...doc,
6666
+ layers: doc.layers.map((l) => l.id === layerId ? { ...l, visual: newVisual } : l)
6667
+ };
6668
+ doc = recomputeTimeline(doc);
6669
+ persistDoc(store, documentId, doc, compositionPath);
6670
+ return ok23({
6671
+ layerId,
6672
+ sourceOffset: newVisual.sourceOffset ?? 0,
6673
+ sourceEnd: newVisual.sourceEnd,
6674
+ startFrame: newVisual.startFrame ?? 0,
6675
+ endFrame: clipEndFrame(newVisual, doc.canvas.fps)
6676
+ });
6677
+ }
6678
+ );
6679
+ server.tool(
6680
+ "atelier_remove_clip",
6681
+ "Remove a clip layer plus all transitively-parented descendants (e.g. its caption layers) and any deltas referencing them. Refuses to remove a track group layer.",
6682
+ {
6683
+ documentId: z48.string().describe("In-memory document ID to reload after disk write"),
6684
+ projectDir: z48.string().describe("Absolute path to the project root"),
6685
+ layerId: z48.string().describe("Layer id of the clip to remove")
6686
+ },
6687
+ { readOnlyHint: false, destructiveHint: true },
6688
+ async ({ documentId, projectDir, layerId }) => {
6689
+ const loaded = loadDocFromDisk(projectDir);
6690
+ if ("error" in loaded) return err23(loaded.error);
6691
+ let { doc } = loaded;
6692
+ const { compositionPath } = loaded;
6693
+ const layer = doc.layers.find((l) => l.id === layerId);
6694
+ if (!layer) return err23(`layer "${layerId}" not found`);
6695
+ if ((layer.tags ?? []).includes("track")) {
6696
+ return err23("Cannot remove track group; use atelier_remove_track (not implemented in v1)");
6697
+ }
6698
+ const toRemove = /* @__PURE__ */ new Set([layerId]);
6699
+ let grew = true;
6700
+ while (grew) {
6701
+ grew = false;
6702
+ for (const l of doc.layers) {
6703
+ if (toRemove.has(l.id)) continue;
6704
+ if (l.parentId && toRemove.has(l.parentId)) {
6705
+ toRemove.add(l.id);
6706
+ grew = true;
6707
+ }
6708
+ }
6709
+ }
6710
+ const remainingLayers = doc.layers.filter((l) => !toRemove.has(l.id));
6711
+ const newStates = {};
6712
+ for (const [name, st] of Object.entries(doc.states)) {
6713
+ newStates[name] = {
6714
+ ...st,
6715
+ deltas: st.deltas.filter((d) => !toRemove.has(d.layer))
6716
+ };
6717
+ }
6718
+ const assets = { ...doc.assets ?? {} };
6719
+ for (const id of toRemove) delete assets[id];
6720
+ if (Object.keys(assets).length === 0) {
6721
+ const { assets: _omit, ...rest } = doc;
6722
+ void _omit;
6723
+ doc = { ...rest, layers: remainingLayers, states: newStates };
6724
+ } else {
6725
+ doc = { ...doc, layers: remainingLayers, states: newStates, assets };
6726
+ }
6727
+ doc = recomputeTimeline(doc);
6728
+ persistDoc(store, documentId, doc, compositionPath);
6729
+ return ok23({
6730
+ layerId,
6731
+ removedLayerIds: Array.from(toRemove),
6732
+ removedCount: toRemove.size
6733
+ });
6734
+ }
6735
+ );
6736
+ server.tool(
6737
+ "atelier_transcribe_clip",
6738
+ "Run transcription on a clip's source media and (re)build parented caption layers. Audio-layer captions are not supported in v1. Requires a transcribeFn injected via __setTranscribeClipFn (tests) or a pre-existing transcript at <projectDir>/transcripts/<stem>.json.",
6739
+ {
6740
+ documentId: z48.string().describe("In-memory document ID to reload after disk write"),
6741
+ projectDir: z48.string().describe("Absolute path to the project root"),
6742
+ layerId: z48.string().describe("Video clip layer id"),
6743
+ model: z48.string().optional().describe("Whisper model selection forwarded to the injected transcribeFn")
6744
+ },
6745
+ { readOnlyHint: false, destructiveHint: false },
6746
+ async ({ documentId, projectDir, layerId, model }) => {
6747
+ const loaded = loadDocFromDisk(projectDir);
6748
+ if ("error" in loaded) return err23(loaded.error);
6749
+ let { doc } = loaded;
6750
+ const { compositionPath } = loaded;
6751
+ const layer = doc.layers.find((l) => l.id === layerId);
6752
+ if (!layer) return err23(`clip layer "${layerId}" not found`);
6753
+ if (layer.visual.type === "audio") {
6754
+ return err23("audio layer captions are not supported in v1");
6755
+ }
6756
+ if (layer.visual.type !== "video") {
6757
+ return err23(`layer "${layerId}" is type "${layer.visual.type}", not a video clip`);
6758
+ }
6759
+ const v = layer.visual;
6760
+ const srcRef = v.src;
6761
+ if (!srcRef) return err23(`video layer "${layerId}" has no src`);
6762
+ const mediaBasename = basename4(srcRef);
6763
+ let transcript;
6764
+ let transcriptPath;
6765
+ if (injectedTranscribeFn) {
6766
+ const result = await injectedTranscribeFn({
6767
+ projectDir,
6768
+ mediaBasename,
6769
+ ...model !== void 0 ? { model } : {}
6770
+ });
6771
+ transcript = result.transcript;
6772
+ transcriptPath = result.transcriptPath;
6773
+ } else {
6774
+ const stem = stripExt2(mediaBasename);
6775
+ const candidate = join6(resolve6(projectDir), "transcripts", `${stem}.json`);
6776
+ if (!existsSync6(candidate)) {
6777
+ return err23(
6778
+ `no transcribeFn injected and no transcript mirror at ${candidate}. Install a host-provided transcribeFn via __setTranscribeClipFn or pre-write the transcript.`
6779
+ );
6780
+ }
6781
+ try {
6782
+ transcript = JSON.parse(readFileSync4(candidate, "utf-8"));
6783
+ transcriptPath = candidate;
6784
+ } catch (e) {
6785
+ return err23(`failed to read transcript at ${candidate}: ${e.message}`);
6786
+ }
6787
+ }
6788
+ if (!transcript) return err23("transcribeFn returned no transcript");
6789
+ doc = rebuildClipCaptions(doc, layerId, transcript, doc.canvas);
6790
+ doc = recomputeTimeline(doc);
6791
+ const captionCount = doc.layers.filter(
6792
+ (l) => l.parentId === layerId && (l.tags ?? []).includes("caption")
6793
+ ).length;
6794
+ persistDoc(store, documentId, doc, compositionPath);
6795
+ return ok23({
6796
+ layerId,
6797
+ transcriptPath: transcriptPath ?? null,
6798
+ captionCount
6799
+ });
6800
+ }
6801
+ );
6802
+ server.tool(
6803
+ "atelier_move_slide",
6804
+ "Reorder a carousel project's slides[] array. Refuses on non-carousel projects.",
6805
+ {
6806
+ documentId: z48.string().describe("In-memory document ID to reload after disk write"),
6807
+ projectDir: z48.string().describe("Absolute path to the project root"),
6808
+ fromIndex: z48.number().int().nonnegative().describe("Current index in slides[]"),
6809
+ toIndex: z48.number().int().nonnegative().describe("Target index in slides[]")
6810
+ },
6811
+ { readOnlyHint: false, destructiveHint: false },
6812
+ async ({ documentId, projectDir, fromIndex, toIndex }) => {
6813
+ const loaded = loadDocFromDisk(projectDir);
6814
+ if ("error" in loaded) return err23(loaded.error);
6815
+ let { doc } = loaded;
6816
+ const { compositionPath } = loaded;
6817
+ if (doc.kind !== "carousel") {
6818
+ return err23(`project kind is "${doc.kind ?? "video"}", not "carousel"`);
6819
+ }
6820
+ const slides = [...doc.slides ?? []];
6821
+ if (fromIndex >= slides.length) {
6822
+ return err23(`fromIndex ${fromIndex} out of bounds (slides.length=${slides.length})`);
6823
+ }
6824
+ if (toIndex >= slides.length) {
6825
+ return err23(`toIndex ${toIndex} out of bounds (slides.length=${slides.length})`);
6826
+ }
6827
+ const [moved] = slides.splice(fromIndex, 1);
6828
+ slides.splice(toIndex, 0, moved);
6829
+ doc = { ...doc, slides };
6830
+ persistDoc(store, documentId, doc, compositionPath);
6831
+ return ok23({ slides });
6832
+ }
6833
+ );
6834
+ }
6835
+ function stripExt2(name) {
6836
+ const ext = extname4(name);
6837
+ return ext ? name.slice(0, -ext.length) : name;
6838
+ }
6839
+
6840
+ // ../mcp/src/tools/rename-media.ts
6841
+ import { z as z49 } from "zod";
6842
+
6843
+ // ../mcp/src/lib/rename-media.ts
6844
+ import {
6845
+ existsSync as existsSync7,
6846
+ readFileSync as readFileSync5,
6847
+ renameSync,
6848
+ writeFileSync as writeFileSync4
6849
+ } from "fs";
6850
+ import { extname as extname5, join as join7, resolve as resolve7 } from "path";
6851
+ var renameLocks = /* @__PURE__ */ new Map();
6852
+ function readAtelierDoc(absPath) {
6853
+ const raw = readFileSync5(absPath, "utf-8");
6854
+ const parsed = parseAtelier(raw);
6855
+ if (parsed.success) return parsed.data;
6856
+ return JSON.parse(raw);
6857
+ }
6858
+ function stripExt3(name) {
6859
+ const ext = extname5(name);
6860
+ return ext ? name.slice(0, -ext.length) : name;
6861
+ }
6862
+ function srcReferencesBasename(src, oldBasename) {
6863
+ if (typeof src !== "string" || src.length === 0) return false;
6864
+ if (src === oldBasename) return true;
6865
+ if (src === `media/${oldBasename}`) return true;
6866
+ if (src.endsWith(`/${oldBasename}`)) return true;
6867
+ return false;
6868
+ }
6869
+ function rewriteSrc(src, oldBasename, newBasename) {
6870
+ if (src === oldBasename) return newBasename;
6871
+ if (src === `media/${oldBasename}`) return `media/${newBasename}`;
6872
+ if (src.endsWith(`/${oldBasename}`)) {
6873
+ return src.slice(0, -oldBasename.length) + newBasename;
6874
+ }
6875
+ return src;
6876
+ }
6877
+ function applyMediaRenameToDoc(doc, oldBasename, newBasename) {
6878
+ const assetKeysUpdated = [];
6879
+ const layersUpdated = [];
6880
+ let newAssets = void 0;
6881
+ if (doc.assets && typeof doc.assets === "object") {
6882
+ const out2 = {};
6883
+ for (const [key, asset] of Object.entries(doc.assets)) {
6884
+ if (asset && typeof asset === "object" && srcReferencesBasename(asset.src, oldBasename)) {
6885
+ out2[key] = { ...asset, src: rewriteSrc(asset.src, oldBasename, newBasename) };
6886
+ assetKeysUpdated.push(key);
6887
+ } else {
6888
+ out2[key] = asset;
6889
+ }
6890
+ }
6891
+ newAssets = out2;
6892
+ }
6893
+ const newLayers = doc.layers.map((layer) => {
6894
+ const v = layer.visual;
6895
+ if (!v || v.type !== "video" && v.type !== "audio" && v.type !== "image") {
6896
+ return layer;
6897
+ }
6898
+ const visualWithSrc = v;
6899
+ if (!srcReferencesBasename(visualWithSrc.src, oldBasename)) return layer;
6900
+ layersUpdated.push(layer.id);
6901
+ return {
6902
+ ...layer,
6903
+ visual: {
6904
+ ...v,
6905
+ src: rewriteSrc(visualWithSrc.src, oldBasename, newBasename)
6906
+ }
6907
+ };
6908
+ });
6909
+ const out = { ...doc, layers: newLayers };
6910
+ if (newAssets !== void 0) out.assets = newAssets;
6911
+ return { doc: out, assetKeysUpdated, layersUpdated };
6912
+ }
6913
+ function scanArtifactsForOldBasename(projectDir, oldBasename) {
6914
+ const warnings = [];
6915
+ const artifactFiles = ["DESIGN.md", "SCRIPT.md", "STORYBOARD.md"];
6916
+ for (const name of artifactFiles) {
6917
+ const absPath = join7(projectDir, name);
6918
+ if (!existsSync7(absPath)) continue;
6919
+ try {
6920
+ const raw = readFileSync5(absPath, "utf-8");
6921
+ if (raw.includes(oldBasename)) {
6922
+ warnings.push(`${name} references old path '${oldBasename}'`);
6923
+ }
6924
+ } catch {
6925
+ }
6926
+ }
6927
+ return warnings;
6928
+ }
6929
+ async function renameMedia(opts) {
6930
+ const projectDir = resolve7(opts.projectDir);
6931
+ const oldBasename = opts.oldBasename;
6932
+ const newBasename = opts.newBasename;
6933
+ if (oldBasename === newBasename) {
6934
+ return {
6935
+ docUpdated: false,
6936
+ transcriptRenamed: false,
6937
+ assetKeysUpdated: [],
6938
+ layersUpdated: [],
6939
+ timelineRegenerated: false,
6940
+ warnings: []
6941
+ };
6942
+ }
6943
+ const lockKey = projectDir;
6944
+ const prev = renameLocks.get(lockKey) ?? Promise.resolve();
6945
+ const run = prev.catch(() => {
6946
+ }).then(() => renameMediaImpl(projectDir, oldBasename, newBasename, opts.dryRun));
6947
+ renameLocks.set(lockKey, run);
6948
+ try {
6949
+ return await run;
6950
+ } finally {
6951
+ if (renameLocks.get(lockKey) === run) renameLocks.delete(lockKey);
6952
+ }
6953
+ }
6954
+ async function renameMediaImpl(projectDir, oldBasename, newBasename, dryRun) {
6955
+ const mediaDir = join7(projectDir, "media");
6956
+ if (!existsSync7(mediaDir)) {
6957
+ throw new Error(`renameMedia: project has no media/ dir at ${mediaDir}`);
6958
+ }
6959
+ const mediaOldAbs = join7(mediaDir, oldBasename);
6960
+ const mediaNewAbs = join7(mediaDir, newBasename);
6961
+ if (!existsSync7(mediaOldAbs)) {
6962
+ throw new Error(`renameMedia: media file not found: ${mediaOldAbs}`);
6963
+ }
6964
+ if (existsSync7(mediaNewAbs)) {
6965
+ throw new Error(`renameMedia: destination file already exists: ${mediaNewAbs}`);
6966
+ }
6967
+ const transcriptsDir = join7(projectDir, "transcripts");
6968
+ const transcriptOldAbs = join7(transcriptsDir, `${stripExt3(oldBasename)}.json`);
6969
+ const transcriptNewAbs = join7(transcriptsDir, `${stripExt3(newBasename)}.json`);
6970
+ const transcriptExists = existsSync7(transcriptOldAbs);
6971
+ const docPath = join7(projectDir, "project.atelier");
6972
+ if (!existsSync7(docPath)) {
6973
+ throw new Error(`renameMedia: missing project.atelier at ${docPath}`);
6974
+ }
6975
+ const originalDoc = readAtelierDoc(docPath);
6976
+ const walked = applyMediaRenameToDoc(originalDoc, oldBasename, newBasename);
6977
+ const hadTimeline = originalDoc.timeline !== void 0;
6978
+ const newDoc = recomputeTimeline(walked.doc);
6979
+ const hasTimelineAfter = newDoc.timeline !== void 0;
6980
+ const timelineRegenerated = hadTimeline || hasTimelineAfter;
6981
+ const warnings = scanArtifactsForOldBasename(projectDir, oldBasename);
6982
+ if (dryRun === true) {
6983
+ return {
6984
+ docUpdated: false,
6985
+ transcriptRenamed: false,
6986
+ assetKeysUpdated: walked.assetKeysUpdated,
6987
+ layersUpdated: walked.layersUpdated,
6988
+ timelineRegenerated: false,
6989
+ warnings
6990
+ };
6991
+ }
6992
+ let mediaRenamed = false;
6993
+ try {
6994
+ renameSync(mediaOldAbs, mediaNewAbs);
6995
+ mediaRenamed = true;
6996
+ } catch (err24) {
6997
+ throw new Error(
6998
+ `renameMedia: failed to rename media file: ${err24 instanceof Error ? err24.message : String(err24)}`
6999
+ );
7000
+ }
7001
+ let transcriptRenamed = false;
7002
+ if (transcriptExists) {
7003
+ try {
7004
+ renameSync(transcriptOldAbs, transcriptNewAbs);
7005
+ transcriptRenamed = true;
7006
+ } catch (err24) {
7007
+ try {
7008
+ renameSync(mediaNewAbs, mediaOldAbs);
7009
+ } catch (rbErr) {
7010
+ console.error(
7011
+ `renameMedia: CRITICAL \u2014 rollback of media rename failed: ${rbErr instanceof Error ? rbErr.message : String(rbErr)}`
7012
+ );
7013
+ }
7014
+ throw new Error(
7015
+ `renameMedia: failed to rename transcript: ${err24 instanceof Error ? err24.message : String(err24)}`
7016
+ );
7017
+ }
7018
+ }
7019
+ try {
7020
+ writeFileSync4(docPath, JSON.stringify(newDoc, null, 2), "utf-8");
7021
+ } catch (err24) {
7022
+ if (transcriptRenamed) {
7023
+ try {
7024
+ renameSync(transcriptNewAbs, transcriptOldAbs);
7025
+ } catch (rbErr) {
7026
+ console.error(
7027
+ `renameMedia: CRITICAL \u2014 rollback of transcript rename failed: ${rbErr instanceof Error ? rbErr.message : String(rbErr)}`
7028
+ );
7029
+ }
7030
+ }
7031
+ if (mediaRenamed) {
7032
+ try {
7033
+ renameSync(mediaNewAbs, mediaOldAbs);
7034
+ } catch (rbErr) {
7035
+ console.error(
7036
+ `renameMedia: CRITICAL \u2014 rollback of media rename failed: ${rbErr instanceof Error ? rbErr.message : String(rbErr)}`
7037
+ );
7038
+ }
7039
+ }
7040
+ throw new Error(
7041
+ `renameMedia: failed to write project.atelier: ${err24 instanceof Error ? err24.message : String(err24)}`
7042
+ );
7043
+ }
7044
+ return {
7045
+ docUpdated: true,
7046
+ transcriptRenamed,
7047
+ assetKeysUpdated: walked.assetKeysUpdated,
7048
+ layersUpdated: walked.layersUpdated,
7049
+ timelineRegenerated,
7050
+ warnings
7051
+ };
7052
+ }
7053
+ function isValidMediaBasenameForRename(name) {
7054
+ if (typeof name !== "string") return false;
7055
+ if (name.length === 0) return false;
7056
+ if (name !== name.trim()) return false;
7057
+ if (name.includes("/") || name.includes("\\")) return false;
7058
+ if (name === "." || name === "..") return false;
7059
+ if (name.startsWith(".")) return false;
7060
+ if (/[\x00-\x1f]/.test(name)) return false;
7061
+ return true;
7062
+ }
7063
+
7064
+ // ../mcp/src/tools/rename-media.ts
7065
+ import { existsSync as existsSync8, readFileSync as readFileSync6 } from "fs";
7066
+ import { join as join8, resolve as resolve8, sep } from "path";
7067
+ function register24(server, store) {
7068
+ server.tool(
7069
+ "atelier_rename_media",
7070
+ "Rename a media file in a project, cascading the rename through transcripts and the project document. Atomic with rollback on partial failure.",
7071
+ {
7072
+ documentId: z49.string().describe("Document ID"),
7073
+ projectDir: z49.string().describe("Absolute project directory path"),
7074
+ oldBasename: z49.string().describe("Current basename inside media/, e.g. 'IMG_7346.MOV'"),
7075
+ newBasename: z49.string().describe("New basename inside media/"),
7076
+ dryRun: z49.boolean().optional().describe("If true, compute the result without writing"),
7077
+ workspaceDir: z49.string().optional().describe("Workspace root for security scoping (defaults to process.cwd())")
7078
+ },
7079
+ { readOnlyHint: false, destructiveHint: true },
7080
+ async ({ documentId, projectDir, oldBasename, newBasename, dryRun, workspaceDir }) => {
7081
+ try {
7082
+ if (!isValidMediaBasenameForRename(oldBasename)) {
7083
+ return {
7084
+ content: [{ type: "text", text: JSON.stringify({ error: "invalid oldBasename" }) }],
7085
+ isError: true
7086
+ };
7087
+ }
7088
+ if (!isValidMediaBasenameForRename(newBasename)) {
7089
+ return {
7090
+ content: [{ type: "text", text: JSON.stringify({ error: "invalid newBasename" }) }],
7091
+ isError: true
7092
+ };
7093
+ }
7094
+ const workspaceRoot = resolve8(workspaceDir ?? process.cwd());
7095
+ const resolvedProject = resolve8(projectDir);
7096
+ if (resolvedProject !== workspaceRoot && !resolvedProject.startsWith(workspaceRoot + sep)) {
7097
+ return {
7098
+ content: [{ type: "text", text: JSON.stringify({ error: `projectDir must be inside workspace ${workspaceRoot}` }) }],
7099
+ isError: true
7100
+ };
7101
+ }
7102
+ if (!existsSync8(join8(resolvedProject, "project.atelier"))) {
7103
+ return {
7104
+ content: [{ type: "text", text: JSON.stringify({ error: "projectDir does not contain project.atelier" }) }],
7105
+ isError: true
7106
+ };
7107
+ }
7108
+ const result = await renameMedia({
7109
+ projectDir: resolvedProject,
7110
+ oldBasename,
7111
+ newBasename,
7112
+ ...dryRun !== void 0 ? { dryRun } : {}
7113
+ });
7114
+ if (!dryRun) {
7115
+ const docPath = join8(resolvedProject, "project.atelier");
7116
+ const raw = readFileSync6(docPath, "utf-8");
7117
+ const parsed = parseAtelier(raw);
7118
+ if (parsed.success) {
7119
+ store.set(documentId, parsed.data, "system");
7120
+ }
7121
+ }
7122
+ return {
7123
+ content: [{ type: "text", text: JSON.stringify({ success: true, result }) }]
7124
+ };
7125
+ } catch (e) {
7126
+ return {
7127
+ content: [{ type: "text", text: JSON.stringify({ error: e.message }) }],
7128
+ isError: true
7129
+ };
7130
+ }
7131
+ }
7132
+ );
7133
+ }
7134
+
7135
+ // ../mcp/src/tools/docs.ts
7136
+ import { z as z50 } from "zod";
7137
+ import { existsSync as existsSync10, readFileSync as readFileSync8 } from "fs";
7138
+ import { join as join10, resolve as resolve10, sep as sep2 } from "path";
7139
+
7140
+ // ../mcp/src/lib/doc-management.ts
7141
+ import {
7142
+ existsSync as existsSync9,
7143
+ mkdirSync as mkdirSync2,
7144
+ readdirSync as readdirSync5,
7145
+ readFileSync as readFileSync7,
7146
+ statSync as statSync5,
7147
+ unlinkSync,
7148
+ writeFileSync as writeFileSync5
7149
+ } from "fs";
7150
+ import { basename as basename5, dirname as dirname2, extname as extname6, isAbsolute as isAbsolute3, join as join9, relative, resolve as resolve9 } from "path";
7151
+
7152
+ // ../mcp/src/lib/doc-management-types.ts
7153
+ function isValidDocName(name) {
7154
+ if (typeof name !== "string") return false;
7155
+ const trimmed = name.trim();
7156
+ if (trimmed.length === 0) return false;
7157
+ if (trimmed.length > 200) return false;
7158
+ if (trimmed.startsWith(".")) return false;
7159
+ if (trimmed.includes("/") || trimmed.includes("\\") || trimmed.includes("..")) return false;
7160
+ if (/[\x00-\x1f]/.test(trimmed)) return false;
7161
+ return true;
7162
+ }
7163
+
7164
+ // ../mcp/src/lib/doc-management.ts
7165
+ function slugifyBasename2(name) {
7166
+ const noExt = name.replace(/\.[^.]+$/, "");
7167
+ const slugged = noExt.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
7168
+ return slugged;
7169
+ }
7170
+ function readAtelierDoc2(absPath) {
7171
+ const raw = readFileSync7(absPath, "utf-8");
7172
+ const parsed = parseAtelier(raw);
7173
+ if (parsed.success) return parsed.data;
7174
+ return JSON.parse(raw);
7175
+ }
7176
+ function scaffoldDoc(slug, kind) {
7177
+ const base = {
7178
+ version: "1.0",
7179
+ name: slug,
7180
+ kind,
7181
+ canvas: { width: 1080, height: 1920, fps: 30 },
7182
+ layers: [],
7183
+ states: { default: { duration: 1, deltas: [] } }
7184
+ };
7185
+ if (kind === "carousel") {
7186
+ base.slides = [];
7187
+ }
7188
+ return base;
7189
+ }
7190
+ function createDoc(opts) {
7191
+ if (!isValidDocName(opts.name)) {
7192
+ throw new Error("invalid doc name");
7193
+ }
7194
+ const projectDir = resolve9(opts.projectDir);
7195
+ const slug = slugifyBasename2(opts.name);
7196
+ if (slug.length === 0) {
7197
+ throw new Error("invalid doc name");
7198
+ }
7199
+ const subdir = opts.asCarouselSlide ? "slides" : "";
7200
+ const baseDir = subdir ? join9(projectDir, subdir) : projectDir;
7201
+ let finalStem = slug;
7202
+ let attempt = 0;
7203
+ while (existsSync9(join9(baseDir, `${finalStem}.atelier`))) {
7204
+ attempt += 1;
7205
+ if (attempt > 999) {
7206
+ throw new Error(`createDoc: could not find a non-colliding stem for "${slug}"`);
7207
+ }
7208
+ finalStem = `${slug}-${attempt}`;
7209
+ }
7210
+ const newDocAbsPath = join9(baseDir, `${finalStem}.atelier`);
7211
+ const newDocPath = subdir ? `${subdir}/${finalStem}.atelier` : `${finalStem}.atelier`;
7212
+ const newDoc = scaffoldDoc(finalStem, opts.kind);
7213
+ mkdirSync2(dirname2(newDocAbsPath), { recursive: true });
7214
+ writeFileSync5(newDocAbsPath, JSON.stringify(newDoc, null, 2), "utf-8");
7215
+ if (opts.asCarouselSlide) {
7216
+ if (!opts.carouselDocPath) {
7217
+ throw new Error("createDoc: asCarouselSlide requires carouselDocPath");
7218
+ }
7219
+ const carouselAbs = resolve9(opts.carouselDocPath);
7220
+ if (!existsSync9(carouselAbs)) {
7221
+ throw new Error(`createDoc: carouselDocPath does not exist: ${carouselAbs}`);
7222
+ }
7223
+ const carouselDoc = readAtelierDoc2(carouselAbs);
7224
+ if (carouselDoc.kind !== "carousel") {
7225
+ throw new Error(`createDoc: carouselDocPath is not a carousel doc (kind=${carouselDoc.kind ?? "video"})`);
7226
+ }
7227
+ const slideSrc = relative(projectDir, newDocAbsPath).split("\\").join("/");
7228
+ const slides = carouselDoc.slides ?? [];
7229
+ if (slides.some((s) => s.src === slideSrc)) {
7230
+ try {
7231
+ unlinkSync(newDocAbsPath);
7232
+ } catch {
7233
+ }
7234
+ throw new Error(`createDoc: slide ${slideSrc} already exists in carousel`);
7235
+ }
7236
+ const insertAt = typeof opts.insertAt === "number" && opts.insertAt >= 0 && opts.insertAt <= slides.length ? opts.insertAt : slides.length;
7237
+ const newSlide = { src: slideSrc };
7238
+ const newSlides = [...slides.slice(0, insertAt), newSlide, ...slides.slice(insertAt)];
7239
+ const updatedCarousel = { ...carouselDoc, slides: newSlides };
7240
+ writeFileSync5(carouselAbs, JSON.stringify(updatedCarousel, null, 2), "utf-8");
7241
+ return { newDocPath, newDocAbsPath, appendedSlideIndex: insertAt };
7242
+ }
7243
+ return { newDocPath, newDocAbsPath };
7244
+ }
7245
+ function duplicateDoc(opts) {
7246
+ const projectDir = resolve9(opts.projectDir);
7247
+ const sourceAbs = join9(projectDir, opts.sourceDocPath);
7248
+ if (!existsSync9(sourceAbs)) {
7249
+ throw new Error(`duplicateDoc: source doc not found: ${opts.sourceDocPath}`);
7250
+ }
7251
+ const sourceDoc = readAtelierDoc2(sourceAbs);
7252
+ const sourceDir = dirname2(sourceAbs);
7253
+ const sourceBase = basename5(sourceAbs);
7254
+ const sourceExt = extname6(sourceBase);
7255
+ const sourceStem = sourceExt ? sourceBase.slice(0, -sourceExt.length) : sourceBase;
7256
+ let finalStem;
7257
+ for (let attempt = 1; attempt <= 999; attempt++) {
7258
+ const candidate = attempt === 1 ? `${sourceStem}-copy` : `${sourceStem}-copy-${attempt}`;
7259
+ if (!existsSync9(join9(sourceDir, `${candidate}.atelier`))) {
7260
+ finalStem = candidate;
7261
+ break;
7262
+ }
7263
+ }
7264
+ if (!finalStem) {
7265
+ throw new Error(`duplicateDoc: could not find a non-colliding stem for "${sourceStem}-copy"`);
7266
+ }
7267
+ const newDocAbsPath = join9(sourceDir, `${finalStem}.atelier`);
7268
+ const sourceRel = opts.sourceDocPath;
7269
+ const lastSlash = sourceRel.lastIndexOf("/");
7270
+ const parentRel = lastSlash >= 0 ? sourceRel.slice(0, lastSlash + 1) : "";
7271
+ const newDocPath = `${parentRel}${finalStem}.atelier`;
7272
+ const dupDoc = { ...sourceDoc };
7273
+ writeFileSync5(newDocAbsPath, JSON.stringify(dupDoc, null, 2), "utf-8");
7274
+ const report = {
7275
+ sourceDocPath: opts.sourceDocPath,
7276
+ newDocPath
7277
+ };
7278
+ if (opts.appendSlideRef) {
7279
+ const sourceDirRel = dirname2(opts.sourceDocPath);
7280
+ if (sourceDirRel !== "slides") {
7281
+ try {
7282
+ unlinkSync(newDocAbsPath);
7283
+ } catch {
7284
+ }
7285
+ throw new Error("appendSlideRef requires a slide source");
7286
+ }
7287
+ const manifestAbs = join9(projectDir, "project.atelier");
7288
+ if (!existsSync9(manifestAbs)) {
7289
+ throw new Error("duplicateDoc: appendSlideRef requested but project.atelier is missing");
7290
+ }
7291
+ const manifest = readAtelierDoc2(manifestAbs);
7292
+ if (manifest.kind !== "carousel") {
7293
+ throw new Error(`duplicateDoc: appendSlideRef requires a carousel manifest (kind=${manifest.kind ?? "video"})`);
7294
+ }
7295
+ const slideSrc = relative(projectDir, newDocAbsPath).split("\\").join("/");
7296
+ const slides = manifest.slides ?? [];
7297
+ if (slides.some((s) => s.src === slideSrc)) {
7298
+ throw new Error(`duplicateDoc: slide ${slideSrc} already exists in carousel`);
7299
+ }
7300
+ const sourceSlideSrc = relative(projectDir, sourceAbs).split("\\").join("/");
7301
+ const sourceIndex = slides.findIndex((s) => s.src === sourceSlideSrc);
7302
+ const insertAt = sourceIndex >= 0 ? sourceIndex + 1 : slides.length;
7303
+ const newSlide = { src: slideSrc };
7304
+ const newSlides = [...slides.slice(0, insertAt), newSlide, ...slides.slice(insertAt)];
7305
+ const updatedManifest = { ...manifest, slides: newSlides };
7306
+ writeFileSync5(manifestAbs, JSON.stringify(updatedManifest, null, 2), "utf-8");
7307
+ report.appendedSlideIndex = insertAt;
7308
+ }
7309
+ return report;
7310
+ }
7311
+ function deleteDoc(opts) {
7312
+ const workspaceDir = resolve9(opts.workspaceDir);
7313
+ const projectDir = join9(workspaceDir, opts.projectName);
7314
+ const docAbs = join9(projectDir, opts.docPath);
7315
+ if (!existsSync9(docAbs)) {
7316
+ throw new Error(`deleteDoc: doc not found: ${opts.docPath}`);
7317
+ }
7318
+ const report = {
7319
+ docPath: opts.docPath,
7320
+ slideRefsRemoved: [],
7321
+ refLayersDangling: []
7322
+ };
7323
+ let projectNames = [];
7324
+ try {
7325
+ projectNames = readdirSync5(workspaceDir).filter((entry) => {
7326
+ if (entry.startsWith(".")) return false;
7327
+ try {
7328
+ const projDir = join9(workspaceDir, entry);
7329
+ if (!statSync5(projDir).isDirectory()) return false;
7330
+ return existsSync9(join9(projDir, "project.atelier"));
7331
+ } catch {
7332
+ return false;
7333
+ }
7334
+ });
7335
+ } catch {
7336
+ projectNames = [];
7337
+ }
7338
+ for (const projName of projectNames) {
7339
+ const projDir = join9(workspaceDir, projName);
7340
+ const projectAtelierFiles = listAtelierFilesUnderProject(projDir);
7341
+ for (const atelierAbs of projectAtelierFiles) {
7342
+ let doc;
7343
+ try {
7344
+ doc = readAtelierDoc2(atelierAbs);
7345
+ } catch {
7346
+ continue;
7347
+ }
7348
+ let mutated = false;
7349
+ const docRel = relative(projDir, atelierAbs).split("\\").join("/");
7350
+ if (doc.kind === "carousel" && Array.isArray(doc.slides)) {
7351
+ const keptSlides = [];
7352
+ doc.slides.forEach((slide, idx) => {
7353
+ const slideAbs = resolveRefSrc(slide.src, atelierAbs, projDir, workspaceDir);
7354
+ if (slideAbs === docAbs) {
7355
+ report.slideRefsRemoved.push({ project: projName, slideIndex: idx });
7356
+ mutated = true;
7357
+ return;
7358
+ }
7359
+ keptSlides.push(slide);
7360
+ });
7361
+ if (mutated) {
7362
+ doc = { ...doc, slides: keptSlides };
7363
+ }
7364
+ }
7365
+ if (Array.isArray(doc.layers)) {
7366
+ for (const layer of doc.layers) {
7367
+ const v = layer.visual;
7368
+ if (!v || v.type !== "ref") continue;
7369
+ const refV = v;
7370
+ if (typeof refV.src !== "string" || !refV.src) continue;
7371
+ const refAbs = resolveRefSrc(refV.src, atelierAbs, projDir, workspaceDir);
7372
+ if (refAbs === docAbs) {
7373
+ report.refLayersDangling.push({
7374
+ project: projName,
7375
+ docPath: docRel,
7376
+ layerId: layer.id
7377
+ });
7378
+ }
7379
+ }
7380
+ }
7381
+ if (mutated) {
7382
+ writeFileSync5(atelierAbs, JSON.stringify(doc, null, 2), "utf-8");
7383
+ }
7384
+ }
7385
+ }
7386
+ unlinkSync(docAbs);
7387
+ return report;
7388
+ }
7389
+ function listAtelierFilesUnderProject(projectDir) {
7390
+ const out = [];
7391
+ try {
7392
+ for (const entry of readdirSync5(projectDir)) {
7393
+ if (entry === ".atelier") continue;
7394
+ if (!entry.endsWith(".atelier")) continue;
7395
+ const abs = join9(projectDir, entry);
7396
+ try {
7397
+ if (statSync5(abs).isFile()) out.push(abs);
7398
+ } catch {
7399
+ }
7400
+ }
7401
+ } catch {
7402
+ }
7403
+ const slidesDir = join9(projectDir, "slides");
7404
+ if (existsSync9(slidesDir)) {
7405
+ try {
7406
+ for (const entry of readdirSync5(slidesDir)) {
7407
+ if (!entry.endsWith(".atelier")) continue;
7408
+ const abs = join9(slidesDir, entry);
7409
+ try {
7410
+ if (statSync5(abs).isFile()) out.push(abs);
7411
+ } catch {
7412
+ }
7413
+ }
7414
+ } catch {
7415
+ }
7416
+ }
7417
+ return out;
7418
+ }
7419
+ function resolveRefSrc(src, currentDocAbs, ownerProjectDir, workspaceRoot) {
7420
+ if (isAbsolute3(src)) {
7421
+ return existsSync9(src) ? resolve9(src) : void 0;
7422
+ }
7423
+ const projectRelative = resolve9(ownerProjectDir, src);
7424
+ if (existsSync9(projectRelative)) return projectRelative;
7425
+ const docRelative = resolve9(dirname2(currentDocAbs), src);
7426
+ if (existsSync9(docRelative)) return docRelative;
7427
+ const workspaceRelative = resolve9(workspaceRoot, src);
7428
+ if (existsSync9(workspaceRelative)) return workspaceRelative;
7429
+ return void 0;
7430
+ }
7431
+ function listDocs(projectDir) {
7432
+ const projAbs = resolve9(projectDir);
7433
+ const collected = [];
7434
+ try {
7435
+ for (const entry of readdirSync5(projAbs)) {
7436
+ if (entry === ".atelier") continue;
7437
+ if (!entry.endsWith(".atelier")) continue;
7438
+ const abs = join9(projAbs, entry);
7439
+ try {
7440
+ if (!statSync5(abs).isFile()) continue;
7441
+ } catch {
7442
+ continue;
7443
+ }
7444
+ collected.push({ rel: entry, abs });
7445
+ }
7446
+ } catch {
7447
+ }
7448
+ const slidesDir = join9(projAbs, "slides");
7449
+ if (existsSync9(slidesDir)) {
7450
+ try {
7451
+ for (const entry of readdirSync5(slidesDir)) {
7452
+ if (!entry.endsWith(".atelier")) continue;
7453
+ const abs = join9(slidesDir, entry);
7454
+ try {
7455
+ if (!statSync5(abs).isFile()) continue;
7456
+ } catch {
7457
+ continue;
7458
+ }
7459
+ collected.push({ rel: `slides/${entry}`, abs });
7460
+ }
7461
+ } catch {
7462
+ }
7463
+ }
7464
+ collected.sort((a, b) => a.rel < b.rel ? -1 : a.rel > b.rel ? 1 : 0);
7465
+ const out = [];
7466
+ for (const { rel, abs } of collected) {
7467
+ let kind;
7468
+ try {
7469
+ kind = readAtelierDoc2(abs).kind;
7470
+ } catch {
7471
+ }
7472
+ const entry = { path: rel };
7473
+ if (kind !== void 0) entry.kind = kind;
7474
+ out.push(entry);
7475
+ }
7476
+ return out;
7477
+ }
7478
+
7479
+ // ../mcp/src/tools/docs.ts
7480
+ function errorEnvelope(message) {
7481
+ return {
7482
+ content: [{ type: "text", text: JSON.stringify({ error: message }) }],
7483
+ isError: true
7484
+ };
7485
+ }
7486
+ function successEnvelope(payload) {
7487
+ return {
7488
+ content: [{ type: "text", text: JSON.stringify({ success: true, ...payload }) }]
7489
+ };
7490
+ }
7491
+ function resolveProjectInWorkspace(projectDir, workspaceDir) {
7492
+ const workspaceAbs = resolve10(workspaceDir ?? process.cwd());
7493
+ const projectAbs = resolve10(projectDir);
7494
+ if (projectAbs !== workspaceAbs && !projectAbs.startsWith(workspaceAbs + sep2)) {
7495
+ return { ok: false, error: `projectDir must be inside workspace ${workspaceAbs}` };
7496
+ }
7497
+ return { ok: true, workspaceAbs, projectAbs };
7498
+ }
7499
+ function readDocFromDisk(absPath) {
7500
+ try {
7501
+ const raw = readFileSync8(absPath, "utf-8");
7502
+ const parsed = parseAtelier(raw);
7503
+ if (parsed.success) return parsed.data;
7504
+ return JSON.parse(raw);
7505
+ } catch {
7506
+ return void 0;
7507
+ }
7508
+ }
7509
+ function register25(server, store) {
7510
+ server.tool(
7511
+ "atelier_create_doc",
7512
+ "Create a new .atelier doc inside a workspace project. Optionally append it to a carousel's slides[].",
7513
+ {
7514
+ documentId: z50.string().describe("Document ID under which to reload the affected doc"),
7515
+ projectDir: z50.string().describe("Absolute project directory (must contain project.atelier)"),
7516
+ workspaceDir: z50.string().optional().describe("Workspace root for security scoping (defaults to process.cwd())"),
7517
+ name: z50.string().describe("Human-typed doc name; slugged internally to a filesystem-safe stem"),
7518
+ kind: z50.enum(["video", "image", "carousel"]).describe("Doc kind"),
7519
+ asCarouselSlide: z50.boolean().optional().describe("If true, write under slides/ and append a SlideRef to carouselDocPath"),
7520
+ carouselDocPath: z50.string().optional().describe("Absolute path to the carousel doc whose slides[] to mutate (required when asCarouselSlide:true)"),
7521
+ insertAt: z50.number().int().optional().describe("Index in carousel slides[] (default = append)")
7522
+ },
7523
+ { readOnlyHint: false, destructiveHint: false },
7524
+ async ({ documentId, projectDir, workspaceDir, name, kind, asCarouselSlide, carouselDocPath, insertAt }) => {
7525
+ try {
7526
+ if (!isValidDocName(name)) return errorEnvelope("invalid doc name");
7527
+ const scope = resolveProjectInWorkspace(projectDir, workspaceDir);
7528
+ if (!scope.ok) return errorEnvelope(scope.error);
7529
+ if (!existsSync10(join10(scope.projectAbs, "project.atelier"))) {
7530
+ return errorEnvelope("projectDir does not contain project.atelier");
7531
+ }
7532
+ if (asCarouselSlide && carouselDocPath) {
7533
+ const carouselAbs = resolve10(carouselDocPath);
7534
+ if (carouselAbs !== scope.workspaceAbs && !carouselAbs.startsWith(scope.workspaceAbs + sep2)) {
7535
+ return errorEnvelope(`carouselDocPath must be inside workspace ${scope.workspaceAbs}`);
7536
+ }
7537
+ }
7538
+ const result = createDoc({
7539
+ projectDir: scope.projectAbs,
7540
+ name,
7541
+ kind,
7542
+ ...asCarouselSlide !== void 0 ? { asCarouselSlide } : {},
7543
+ ...carouselDocPath !== void 0 ? { carouselDocPath } : {},
7544
+ ...insertAt !== void 0 ? { insertAt } : {}
7545
+ });
7546
+ const reloadTarget = asCarouselSlide && carouselDocPath ? resolve10(carouselDocPath) : result.newDocAbsPath;
7547
+ const doc = readDocFromDisk(reloadTarget);
7548
+ if (doc) store.set(documentId, doc, "llm");
7549
+ const payload = { docPath: result.newDocPath };
7550
+ if (result.appendedSlideIndex !== void 0) {
7551
+ payload.appendedSlideIndex = result.appendedSlideIndex;
7552
+ }
7553
+ return successEnvelope(payload);
7554
+ } catch (e) {
7555
+ return errorEnvelope(e.message);
7556
+ }
7557
+ }
7558
+ );
7559
+ server.tool(
7560
+ "atelier_duplicate_doc",
7561
+ "Duplicate an existing .atelier doc inside a project. Optionally appends a SlideRef to the project's carousel manifest (requires the source to live directly under slides/).",
7562
+ {
7563
+ documentId: z50.string().describe("Document ID under which to reload the affected doc"),
7564
+ projectDir: z50.string().describe("Absolute project directory (must contain project.atelier)"),
7565
+ workspaceDir: z50.string().optional().describe("Workspace root for security scoping (defaults to process.cwd())"),
7566
+ sourceDocPath: z50.string().describe("Project-relative path of the doc to duplicate (e.g. 'slides/slide-1.atelier')"),
7567
+ appendSlideRef: z50.boolean().optional().describe("If true, append a SlideRef pointing at the duplicate to the project manifest")
7568
+ },
7569
+ { readOnlyHint: false, destructiveHint: false },
7570
+ async ({ documentId, projectDir, workspaceDir, sourceDocPath, appendSlideRef }) => {
7571
+ try {
7572
+ const scope = resolveProjectInWorkspace(projectDir, workspaceDir);
7573
+ if (!scope.ok) return errorEnvelope(scope.error);
7574
+ if (!existsSync10(join10(scope.projectAbs, "project.atelier"))) {
7575
+ return errorEnvelope("projectDir does not contain project.atelier");
7576
+ }
7577
+ const report = duplicateDoc({
7578
+ projectDir: scope.projectAbs,
7579
+ sourceDocPath,
7580
+ ...appendSlideRef !== void 0 ? { appendSlideRef } : {}
7581
+ });
7582
+ const reloadTarget = appendSlideRef ? join10(scope.projectAbs, "project.atelier") : join10(scope.projectAbs, report.newDocPath);
7583
+ const doc = readDocFromDisk(reloadTarget);
7584
+ if (doc) store.set(documentId, doc, "llm");
7585
+ return successEnvelope(report);
7586
+ } catch (e) {
7587
+ return errorEnvelope(e.message);
7588
+ }
7589
+ }
7590
+ );
7591
+ server.tool(
7592
+ "atelier_delete_doc",
7593
+ "Delete any .atelier doc (including project.atelier), scrubbing matching SlideRefs across the workspace and reporting dangling RefVisual layers.",
7594
+ {
7595
+ documentId: z50.string().describe("Document ID \u2014 if equal to the deleted doc's path, the studio handles fallback"),
7596
+ workspaceDir: z50.string().optional().describe("Workspace root for security scoping (defaults to process.cwd())"),
7597
+ projectName: z50.string().describe("Project subdir name inside workspaceDir"),
7598
+ docPath: z50.string().describe("Project-relative path of the doc to delete (e.g. 'slides/slide-2.atelier')")
7599
+ },
7600
+ { readOnlyHint: false, destructiveHint: true },
7601
+ async ({ documentId: _documentId, workspaceDir, projectName, docPath }) => {
7602
+ try {
7603
+ const workspaceAbs = resolve10(workspaceDir ?? process.cwd());
7604
+ const projectAbs = resolve10(join10(workspaceAbs, projectName));
7605
+ if (projectAbs !== workspaceAbs && !projectAbs.startsWith(workspaceAbs + sep2)) {
7606
+ return errorEnvelope(`projectName must resolve inside workspace ${workspaceAbs}`);
7607
+ }
7608
+ if (!existsSync10(join10(projectAbs, "project.atelier"))) {
7609
+ return errorEnvelope("project does not contain project.atelier");
7610
+ }
7611
+ const report = deleteDoc({
7612
+ workspaceDir: workspaceAbs,
7613
+ projectName,
7614
+ docPath
7615
+ });
7616
+ void _documentId;
7617
+ return successEnvelope(report);
7618
+ } catch (e) {
7619
+ return errorEnvelope(e.message);
7620
+ }
7621
+ }
7622
+ );
7623
+ server.tool(
7624
+ "atelier_list_docs",
7625
+ "Enumerate every .atelier doc inside a project (flat alphabetical by project-relative path). Returns each as { path, kind? }.",
7626
+ {
7627
+ projectDir: z50.string().describe("Absolute project directory"),
7628
+ workspaceDir: z50.string().optional().describe("Workspace root for security scoping (defaults to process.cwd())")
7629
+ },
7630
+ { readOnlyHint: true, destructiveHint: false },
7631
+ async ({ projectDir, workspaceDir }) => {
7632
+ try {
7633
+ const scope = resolveProjectInWorkspace(projectDir, workspaceDir);
7634
+ if (!scope.ok) return errorEnvelope(scope.error);
7635
+ const docs = listDocs(scope.projectAbs);
7636
+ return successEnvelope({ docs });
7637
+ } catch (e) {
7638
+ return errorEnvelope(e.message);
7639
+ }
7640
+ }
7641
+ );
7642
+ }
7643
+
5265
7644
  // ../mcp/src/bridge-protocol.ts
5266
7645
  var BRIDGE_PROTOCOL_VERSION = 1;
5267
7646
  function isBridgeEnvelope(value) {
5268
7647
  if (!value || typeof value !== "object") return false;
5269
7648
  const t = value.type;
5270
- return t === "hello" || t === "doc:load" || t === "doc:loaded" || t === "doc:patch" || t === "llm:mutation" || t === "error";
7649
+ return t === "hello" || t === "doc:load" || t === "doc:loaded" || t === "doc:patch" || t === "llm:mutation" || t === "error" || t === "media:list:changed" || t === "transcript:added" || t === "media:ingest:progress" || t === "file:deleted" || t === "media:deleted" || t === "project:deleted" || t === "doc:list:changed";
5271
7650
  }
5272
7651
 
5273
7652
  // ../mcp/src/transports/ws-transport.ts
@@ -5302,17 +7681,17 @@ var WebSocketServerTransport = class {
5302
7681
  } else {
5303
7682
  text = String(data);
5304
7683
  }
5305
- } catch (err20) {
5306
- this.onerror?.(err20 instanceof Error ? err20 : new Error(String(err20)));
7684
+ } catch (err24) {
7685
+ this.onerror?.(err24 instanceof Error ? err24 : new Error(String(err24)));
5307
7686
  return;
5308
7687
  }
5309
7688
  let parsed;
5310
7689
  try {
5311
7690
  parsed = JSON.parse(text);
5312
- } catch (err20) {
7691
+ } catch (err24) {
5313
7692
  this.onerror?.(
5314
7693
  new Error(
5315
- `WebSocketServerTransport: failed to parse JSON-RPC message: ${err20 instanceof Error ? err20.message : String(err20)}`
7694
+ `WebSocketServerTransport: failed to parse JSON-RPC message: ${err24 instanceof Error ? err24.message : String(err24)}`
5316
7695
  )
5317
7696
  );
5318
7697
  return;
@@ -5324,8 +7703,8 @@ var WebSocketServerTransport = class {
5324
7703
  this.closed = true;
5325
7704
  this.onclose?.();
5326
7705
  });
5327
- this.ws.on("error", (err20) => {
5328
- this.onerror?.(err20);
7706
+ this.ws.on("error", (err24) => {
7707
+ this.onerror?.(err24);
5329
7708
  });
5330
7709
  }
5331
7710
  async send(message) {
@@ -5375,6 +7754,12 @@ function createServer(store = new DocumentStore()) {
5375
7754
  register17(server, store);
5376
7755
  register18(server, store);
5377
7756
  register19(server, store);
7757
+ register20(server, store);
7758
+ register21(server, store);
7759
+ register22(server, store);
7760
+ register23(server, store);
7761
+ register24(server, store);
7762
+ register25(server, store);
5378
7763
  return { server, store };
5379
7764
  }
5380
7765
  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"));
@@ -5390,22 +7775,28 @@ export {
5390
7775
  BRIDGE_PROTOCOL_VERSION,
5391
7776
  DocumentStore,
5392
7777
  WebSocketServerTransport,
7778
+ __setProbeDurationFn,
7779
+ __setTranscribeClipFn,
5393
7780
  createServer,
5394
7781
  isBridgeEnvelope,
5395
7782
  register12 as registerAssetTools,
7783
+ register23 as registerClipTools,
5396
7784
  register5 as registerDeltaTools,
7785
+ register25 as registerDocTools,
5397
7786
  register as registerDocumentTools,
5398
7787
  register11 as registerExportTools,
5399
7788
  register19 as registerImportImageTools,
5400
7789
  register14 as registerInteractionTools,
5401
7790
  register9 as registerLayerEffectTools,
5402
7791
  register2 as registerLayerTools,
7792
+ register20 as registerMindMapTools,
5403
7793
  register17 as registerOverlayTools,
5404
7794
  register16 as registerPerformanceTools,
5405
7795
  register6 as registerPresetTools,
5406
7796
  register7 as registerPreviewTools,
5407
7797
  register18 as registerRecipeTools,
5408
7798
  register15 as registerRefTools,
7799
+ register24 as registerRenameMediaTool,
5409
7800
  register3 as registerShapeTools,
5410
7801
  register10 as registerStateConfigTools,
5411
7802
  register4 as registerStateTools,