motion-cart 0.1.0 → 0.1.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (194) hide show
  1. package/README.md +33 -2
  2. package/dist/index.js +553 -257
  3. package/package.json +2 -2
  4. package/previews/advanced-interactions__add-to-cart.jpg +0 -0
  5. package/previews/advanced-interactions__add-to-cart.webm +0 -0
  6. package/previews/advanced-interactions__context-menu.jpg +0 -0
  7. package/previews/advanced-interactions__context-menu.webm +0 -0
  8. package/previews/advanced-interactions__cursor-image-hover.jpg +0 -0
  9. package/previews/advanced-interactions__cursor-image-hover.webm +0 -0
  10. package/previews/advanced-interactions__dots-morph-button.jpg +0 -0
  11. package/previews/advanced-interactions__dots-morph-button.webm +0 -0
  12. package/previews/advanced-interactions__ios-exposure-slider.jpg +0 -0
  13. package/previews/advanced-interactions__ios-exposure-slider.webm +0 -0
  14. package/previews/advanced-interactions__ios-pointer.jpg +0 -0
  15. package/previews/advanced-interactions__ios-pointer.webm +0 -0
  16. package/previews/advanced-interactions__magnetic-filings.jpg +0 -0
  17. package/previews/advanced-interactions__magnetic-filings.webm +0 -0
  18. package/previews/advanced-interactions__parallax-hover-card.jpg +0 -0
  19. package/previews/advanced-interactions__parallax-hover-card.webm +0 -0
  20. package/previews/advanced-interactions__pointer-collision.jpg +0 -0
  21. package/previews/advanced-interactions__pointer-collision.webm +0 -0
  22. package/previews/advanced-interactions__scroll-velocity-3d-planes.jpg +0 -0
  23. package/previews/advanced-interactions__scroll-velocity-3d-planes.webm +0 -0
  24. package/previews/advanced-interactions__smooth-tabs.jpg +0 -0
  25. package/previews/advanced-interactions__smooth-tabs.webm +0 -0
  26. package/previews/advanced-interactions__swipe-actions.jpg +0 -0
  27. package/previews/advanced-interactions__swipe-actions.webm +0 -0
  28. package/previews/cursor-effects__click-ripples.jpg +0 -0
  29. package/previews/cursor-effects__click-ripples.webm +0 -0
  30. package/previews/cursor-effects__cursor-distortion.jpg +0 -0
  31. package/previews/cursor-effects__cursor-distortion.webm +0 -0
  32. package/previews/cursor-effects__cursor-follower.jpg +0 -0
  33. package/previews/cursor-effects__cursor-follower.webm +0 -0
  34. package/previews/cursor-effects__cursor-spotlight.jpg +0 -0
  35. package/previews/cursor-effects__cursor-spotlight.webm +0 -0
  36. package/previews/cursor-effects__magnetic-elements.jpg +0 -0
  37. package/previews/cursor-effects__magnetic-elements.webm +0 -0
  38. package/previews/cursor-effects__particle-trail.jpg +0 -0
  39. package/previews/cursor-effects__particle-trail.webm +0 -0
  40. package/previews/data-numbers__activity-feed.jpg +0 -0
  41. package/previews/data-numbers__activity-feed.webm +0 -0
  42. package/previews/data-numbers__animated-counter.jpg +0 -0
  43. package/previews/data-numbers__animated-counter.webm +0 -0
  44. package/previews/data-numbers__counting-stats-row.jpg +0 -0
  45. package/previews/data-numbers__counting-stats-row.webm +0 -0
  46. package/previews/data-numbers__mini-bar-chart.jpg +0 -0
  47. package/previews/data-numbers__mini-bar-chart.webm +0 -0
  48. package/previews/data-numbers__revenue-card.jpg +0 -0
  49. package/previews/data-numbers__revenue-card.webm +0 -0
  50. package/previews/data-numbers__stats-grid.jpg +0 -0
  51. package/previews/data-numbers__stats-grid.webm +0 -0
  52. package/previews/gestures-interactions__hover-cards.jpg +0 -0
  53. package/previews/gestures-interactions__hover-cards.webm +0 -0
  54. package/previews/gestures-interactions__magnetic-button.jpg +0 -0
  55. package/previews/gestures-interactions__magnetic-button.webm +0 -0
  56. package/previews/gestures-interactions__swipe-to-delete.jpg +0 -0
  57. package/previews/gestures-interactions__swipe-to-delete.webm +0 -0
  58. package/previews/layout-navigation__animated-sidebar.jpg +0 -0
  59. package/previews/layout-navigation__animated-sidebar.webm +0 -0
  60. package/previews/layout-navigation__animated-tabs.jpg +0 -0
  61. package/previews/layout-navigation__animated-tabs.webm +0 -0
  62. package/previews/layout-navigation__expandable-cards.jpg +0 -0
  63. package/previews/layout-navigation__expandable-cards.webm +0 -0
  64. package/previews/layout-navigation__page-transitions.jpg +0 -0
  65. package/previews/layout-navigation__page-transitions.webm +0 -0
  66. package/previews/layout-navigation__shared-layout-modal.jpg +0 -0
  67. package/previews/layout-navigation__shared-layout-modal.webm +0 -0
  68. package/previews/lists-tables__animated-table.jpg +0 -0
  69. package/previews/lists-tables__animated-table.webm +0 -0
  70. package/previews/lists-tables__drag-to-reorder.jpg +0 -0
  71. package/previews/lists-tables__drag-to-reorder.webm +0 -0
  72. package/previews/lists-tables__staggered-list.jpg +0 -0
  73. package/previews/lists-tables__staggered-list.webm +0 -0
  74. package/previews/loading-states__circular-progress.jpg +0 -0
  75. package/previews/loading-states__circular-progress.webm +0 -0
  76. package/previews/loading-states__progress-bar.jpg +0 -0
  77. package/previews/loading-states__progress-bar.webm +0 -0
  78. package/previews/loading-states__pulsing-dots.jpg +0 -0
  79. package/previews/loading-states__pulsing-dots.webm +0 -0
  80. package/previews/loading-states__skeleton-shimmer.jpg +0 -0
  81. package/previews/loading-states__skeleton-shimmer.webm +0 -0
  82. package/previews/micro-interactions__animated-accordion.jpg +0 -0
  83. package/previews/micro-interactions__animated-accordion.webm +0 -0
  84. package/previews/micro-interactions__animated-badges.jpg +0 -0
  85. package/previews/micro-interactions__animated-badges.webm +0 -0
  86. package/previews/micro-interactions__animated-checkboxes.jpg +0 -0
  87. package/previews/micro-interactions__animated-checkboxes.webm +0 -0
  88. package/previews/micro-interactions__animated-search.jpg +0 -0
  89. package/previews/micro-interactions__animated-search.webm +0 -0
  90. package/previews/micro-interactions__command-palette.jpg +0 -0
  91. package/previews/micro-interactions__command-palette.webm +0 -0
  92. package/previews/micro-interactions__confetti-burst.jpg +0 -0
  93. package/previews/micro-interactions__confetti-burst.webm +0 -0
  94. package/previews/micro-interactions__floating-action-button.jpg +0 -0
  95. package/previews/micro-interactions__floating-action-button.webm +0 -0
  96. package/previews/micro-interactions__otp-input.jpg +0 -0
  97. package/previews/micro-interactions__otp-input.webm +0 -0
  98. package/previews/micro-interactions__segmented-control.jpg +0 -0
  99. package/previews/micro-interactions__segmented-control.webm +0 -0
  100. package/previews/micro-interactions__toggle-switch.jpg +0 -0
  101. package/previews/micro-interactions__toggle-switch.webm +0 -0
  102. package/previews/motion-premium__animate-number.jpg +0 -0
  103. package/previews/motion-premium__animate-number.webm +0 -0
  104. package/previews/motion-premium__animate-text.jpg +0 -0
  105. package/previews/motion-premium__animate-text.webm +0 -0
  106. package/previews/motion-premium__carousel.jpg +0 -0
  107. package/previews/motion-premium__carousel.webm +0 -0
  108. package/previews/motion-premium__coverflow-carousel.jpg +0 -0
  109. package/previews/motion-premium__coverflow-carousel.webm +0 -0
  110. package/previews/motion-premium__curtain-transitions.jpg +0 -0
  111. package/previews/motion-premium__curtain-transitions.webm +0 -0
  112. package/previews/motion-premium__magnetic-cursor-reticule.jpg +0 -0
  113. package/previews/motion-premium__magnetic-cursor-reticule.webm +0 -0
  114. package/previews/motion-premium__magnetic-pull.jpg +0 -0
  115. package/previews/motion-premium__magnetic-pull.webm +0 -0
  116. package/previews/motion-premium__pointer-compass.jpg +0 -0
  117. package/previews/motion-premium__pointer-compass.webm +0 -0
  118. package/previews/motion-premium__price-toggle.jpg +0 -0
  119. package/previews/motion-premium__price-toggle.webm +0 -0
  120. package/previews/motion-premium__scramble-headline.jpg +0 -0
  121. package/previews/motion-premium__scramble-headline.webm +0 -0
  122. package/previews/motion-premium__scramble-text.jpg +0 -0
  123. package/previews/motion-premium__scramble-text.webm +0 -0
  124. package/previews/motion-premium__square-cursor-reticule.jpg +0 -0
  125. package/previews/motion-premium__square-cursor-reticule.webm +0 -0
  126. package/previews/motion-premium__ticker-marquee.jpg +0 -0
  127. package/previews/motion-premium__ticker-marquee.webm +0 -0
  128. package/previews/motion-premium__typewriter.jpg +0 -0
  129. package/previews/motion-premium__typewriter.webm +0 -0
  130. package/previews/notifications__notification-bell.jpg +0 -0
  131. package/previews/notifications__notification-bell.webm +0 -0
  132. package/previews/notifications__notifications-panel.jpg +0 -0
  133. package/previews/notifications__notifications-panel.webm +0 -0
  134. package/previews/notifications__toast-stack.jpg +0 -0
  135. package/previews/notifications__toast-stack.webm +0 -0
  136. package/previews/preview-manifest.json +1 -0
  137. package/previews/scroll-animations__scroll-reveal.jpg +0 -0
  138. package/previews/scroll-animations__scroll-reveal.webm +0 -0
  139. package/previews/special-unique__3d-card-stack.jpg +0 -0
  140. package/previews/special-unique__3d-card-stack.webm +0 -0
  141. package/previews/special-unique__3d-coin-flip.jpg +0 -0
  142. package/previews/special-unique__3d-coin-flip.webm +0 -0
  143. package/previews/special-unique__3d-tilt-card.jpg +0 -0
  144. package/previews/special-unique__3d-tilt-card.webm +0 -0
  145. package/previews/special-unique__aurora-background.jpg +0 -0
  146. package/previews/special-unique__aurora-background.webm +0 -0
  147. package/previews/special-unique__elastic-grid.jpg +0 -0
  148. package/previews/special-unique__elastic-grid.webm +0 -0
  149. package/previews/special-unique__elastic-slider.jpg +0 -0
  150. package/previews/special-unique__elastic-slider.webm +0 -0
  151. package/previews/special-unique__flip-clock.jpg +0 -0
  152. package/previews/special-unique__flip-clock.webm +0 -0
  153. package/previews/special-unique__glitch-text.jpg +0 -0
  154. package/previews/special-unique__glitch-text.webm +0 -0
  155. package/previews/special-unique__glowing-orbs.jpg +0 -0
  156. package/previews/special-unique__glowing-orbs.webm +0 -0
  157. package/previews/special-unique__gravity-balls.jpg +0 -0
  158. package/previews/special-unique__gravity-balls.webm +0 -0
  159. package/previews/special-unique__heartbeat-monitor.jpg +0 -0
  160. package/previews/special-unique__heartbeat-monitor.webm +0 -0
  161. package/previews/special-unique__liquid-button.jpg +0 -0
  162. package/previews/special-unique__liquid-button.webm +0 -0
  163. package/previews/special-unique__magnetic-dock.jpg +0 -0
  164. package/previews/special-unique__magnetic-dock.webm +0 -0
  165. package/previews/special-unique__matrix-rain.jpg +0 -0
  166. package/previews/special-unique__matrix-rain.webm +0 -0
  167. package/previews/special-unique__morphing-blob.jpg +0 -0
  168. package/previews/special-unique__morphing-blob.webm +0 -0
  169. package/previews/special-unique__morphing-shapes.jpg +0 -0
  170. package/previews/special-unique__morphing-shapes.webm +0 -0
  171. package/previews/special-unique__particle-background.jpg +0 -0
  172. package/previews/special-unique__particle-background.webm +0 -0
  173. package/previews/special-unique__perspective-grid.jpg +0 -0
  174. package/previews/special-unique__perspective-grid.webm +0 -0
  175. package/previews/special-unique__radial-menu.jpg +0 -0
  176. package/previews/special-unique__radial-menu.webm +0 -0
  177. package/previews/special-unique__ripple-grid.jpg +0 -0
  178. package/previews/special-unique__ripple-grid.webm +0 -0
  179. package/previews/special-unique__signature-draw.jpg +0 -0
  180. package/previews/special-unique__signature-draw.webm +0 -0
  181. package/previews/special-unique__split-flap-display.jpg +0 -0
  182. package/previews/special-unique__split-flap-display.webm +0 -0
  183. package/previews/special-unique__spotlight-card.jpg +0 -0
  184. package/previews/special-unique__spotlight-card.webm +0 -0
  185. package/previews/special-unique__springy-text.jpg +0 -0
  186. package/previews/special-unique__springy-text.webm +0 -0
  187. package/previews/special-unique__tech-stack-orbit.jpg +0 -0
  188. package/previews/special-unique__tech-stack-orbit.webm +0 -0
  189. package/previews/special-unique__voice-waveform.jpg +0 -0
  190. package/previews/special-unique__voice-waveform.webm +0 -0
  191. package/previews/text-animations__gradient-text.jpg +0 -0
  192. package/previews/text-animations__gradient-text.webm +0 -0
  193. package/previews/text-animations__typewriter-effect.jpg +0 -0
  194. package/previews/text-animations__typewriter-effect.webm +0 -0
package/dist/index.js CHANGED
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  // src/index.ts
4
- import path3 from "path";
4
+ import path6 from "path";
5
5
 
6
6
  // ../core/dist/chunk-YCZ2J2MA.js
7
7
  var MENU_GROUPS = [
@@ -649,13 +649,216 @@ function overlayScript() {
649
649
  }
650
650
 
651
651
  // ../core/dist/index.js
652
- import { promises as fs } from "fs";
653
- import path from "path";
652
+ import { randomUUID } from "crypto";
653
+ import { promises as fs2 } from "fs";
654
+ import path3 from "path";
654
655
  import path2 from "path";
655
656
  import { realpathSync } from "fs";
656
657
  import { query } from "@anthropic-ai/claude-agent-sdk";
658
+ import { promises as fs } from "fs";
659
+ import path from "path";
657
660
  import { execFile } from "child_process";
658
661
  import { promisify } from "util";
662
+ function panelHtml(opts) {
663
+ const base = opts.base ?? "";
664
+ return `<!doctype html>
665
+ <html><head><meta charset="utf-8"><title>motion-cart</title>
666
+ <style>
667
+ :root{color-scheme:dark}
668
+ *{box-sizing:border-box}
669
+ body{margin:0;height:100vh;display:flex;flex-direction:column;background:#0a0b0e;color:#e7e7ea;font:13px system-ui}
670
+ header{display:flex;align-items:center;gap:12px;padding:10px 14px;border-bottom:1px solid #ffffff1a}
671
+ header b{font-size:14px}header small{color:#ffffff80}
672
+ .body{flex:1;display:flex;min-height:0}
673
+ iframe{flex:1;border:0;background:#fff}
674
+ aside{width:360px;border-left:1px solid #ffffff1a;display:flex;flex-direction:column;min-height:0}
675
+ input{width:100%;background:#ffffff0d;border:1px solid #ffffff26;border-radius:8px;color:#fff;padding:9px 11px;outline:none;font-size:13px}
676
+ input:focus{border-color:#22d3ee80}
677
+ .hint{color:#ffffff73;font-size:12px;margin:8px 2px 0}
678
+ .cat{flex:1;overflow:auto;padding:8px 10px}
679
+ .grp{font-size:12px;text-transform:uppercase;letter-spacing:.05em;color:#ffffff8c;margin:12px 2px 5px;font-weight:600}
680
+ .row{display:flex;justify-content:space-between;align-items:center;padding:8px 10px;border-radius:6px;cursor:pointer;color:#ffffffcc;font-size:13px}
681
+ .row:hover{background:#ffffff14;color:#fff}
682
+ .row.in{background:#22d3ee30;color:#a5f3fc;box-shadow:inset 0 0 0 1px #22d3ee66}
683
+ footer{border-top:1px solid #ffffff1a;padding:11px}
684
+ .item{border:1px solid #ffffff1a;border-radius:6px;padding:7px 9px;margin-bottom:6px;background:#ffffff0a}
685
+ .item.pick{border-color:#22d3eecc;background:#22d3ee26}
686
+ .item .top{display:flex;justify-content:space-between;align-items:center;gap:6px}
687
+ .btn{border:1px solid #ffffff33;border-radius:5px;background:#ffffff0d;color:#fff;padding:4px 9px;font-size:12px;cursor:pointer}
688
+ .btn.on{background:#22d3ee;color:#06202a;border-color:#22d3ee}
689
+ .chip{display:inline-flex;gap:5px;align-items:center;background:#00000080;border-radius:4px;padding:2px 7px;font-size:11px;color:#ffffffb3;margin:4px 4px 0 0}
690
+ .empty{border:1px dashed #ffffff26;border-radius:6px;padding:12px;text-align:center;color:#ffffff73;font-size:12px;margin-bottom:8px}
691
+ .row2{display:flex;gap:8px}
692
+ .apply{flex:1;padding:11px;border:0;border-radius:8px;font-weight:600;color:#fff;cursor:pointer;background:linear-gradient(90deg,#06b6d4,#6366f1);font-size:13px}
693
+ .apply:disabled{opacity:.35;cursor:not-allowed}
694
+ .stop{padding:11px 14px;border:1px solid #ffffff33;border-radius:8px;background:#ffffff0d;color:#fff;cursor:pointer}
695
+ .note{text-align:center;color:#ffffff66;font-size:11px;margin-top:7px}
696
+ #savebar{display:flex;gap:6px;margin-top:8px}
697
+ .save{flex:1;padding:8px;border:1px solid #ffffff26;border-radius:7px;background:#ffffff0d;color:#fff;cursor:pointer;font-size:12px}
698
+ .save:hover{background:#ffffff1a}
699
+ .save.danger{border-color:#f8717155;color:#fca5a5}
700
+ .save:disabled{opacity:.4;cursor:not-allowed}
701
+ pre{margin:7px 0 0;max-height:160px;overflow:auto;background:#000000a6;border:1px solid #ffffff26;border-radius:6px;padding:9px;font:12px ui-monospace;color:#ffffffd9;white-space:pre-wrap}
702
+ .x{color:#ffffff73;cursor:pointer}.x:hover{color:#fff}
703
+ #cart{max-height:34vh;overflow-y:auto;margin-bottom:8px}
704
+ .cat,#cart,pre{scrollbar-width:thin;scrollbar-color:#ffffff33 transparent}
705
+ .cat::-webkit-scrollbar,#cart::-webkit-scrollbar,pre::-webkit-scrollbar{width:8px;height:8px}
706
+ .cat::-webkit-scrollbar-thumb,#cart::-webkit-scrollbar-thumb,pre::-webkit-scrollbar-thumb{background:#ffffff26;border-radius:8px;border:2px solid transparent;background-clip:content-box}
707
+ .cat::-webkit-scrollbar-thumb:hover,#cart::-webkit-scrollbar-thumb:hover,pre::-webkit-scrollbar-thumb:hover{background:#ffffff45;background-clip:content-box}
708
+ .cat::-webkit-scrollbar-track,#cart::-webkit-scrollbar-track,pre::-webkit-scrollbar-track{background:transparent}
709
+ #hovercard{position:fixed;z-index:9999;pointer-events:none;width:300px;border:1px solid #ffffff2e;border-radius:10px;overflow:hidden;background:#0c0d11;box-shadow:0 12px 44px #000a;display:none}
710
+ #hovercard .cap{font:10px ui-monospace;text-transform:uppercase;letter-spacing:.06em;color:#ffffff59;padding:5px 9px;border-bottom:1px solid #ffffff14}
711
+ #hovercard video{display:block;width:100%;height:198px;object-fit:contain;background:#0c0d11}
712
+ </style></head>
713
+ <body>
714
+ <header><b>motion-cart</b><small>${opts.framework} &middot; ${opts.target}</small></header>
715
+ <div class="body">
716
+ <iframe id="site" src="${opts.iframeSrc}"></iframe>
717
+ <aside>
718
+ <div style="padding:11px;border-bottom:1px solid #ffffff1a">
719
+ <input id="q" placeholder="Search animations\u2026">
720
+ <div class="hint">Click an animation to add it &middot; then optionally pick the elements to target.</div>
721
+ </div>
722
+ <div class="cat" id="cat"></div>
723
+ <footer><div id="cart"></div>
724
+ <div class="row2">
725
+ <button class="apply" id="apply" disabled>Apply with AI</button>
726
+ <button class="stop" id="stop" style="display:none">Stop</button>
727
+ </div>
728
+ <div class="note">Runs a real Claude agent on your project &middot; ~30s &middot; uses AI credits<span id="sess"></span></div>
729
+ <div id="savebar" style="display:none">
730
+ <button class="save" id="undo">Undo last</button>
731
+ <button class="save" id="merge">Merge</button>
732
+ <button class="save" id="push" style="display:none">Push</button>
733
+ <button class="save danger" id="discard">Discard</button>
734
+ </div>
735
+ <pre id="log" style="display:none"></pre>
736
+ </footer>
737
+ </aside>
738
+ </div>
739
+ <div id="hovercard"><div class="cap">Live preview</div><video id="hovervid" muted loop playsinline preload="none"></video></div>
740
+ <script>
741
+ const MC="motion-cart", BASE=${JSON.stringify(base)}, IFRAME=${JSON.stringify(opts.iframeSrc)}, TOKEN=${JSON.stringify(opts.token)};
742
+ const FRAME_ORIGIN=new URL(IFRAME,location.href).origin;
743
+ let MENU=[],GROUPS=[],cart=[],pickingFor=null,running=false,q="",sessionCost=0,abort=null;
744
+ const site=document.getElementById('site');
745
+ function send(kind){ try{ site.contentWindow.postMessage({source:MC,kind},FRAME_ORIGIN);}catch(e){} }
746
+ window.addEventListener('message',e=>{
747
+ if(e.origin!==FRAME_ORIGIN) return;
748
+ const d=e.data; if(!d||d.source!==MC) return;
749
+ if(d.kind==='picker:ready' && pickingFor) send('picker:enable');
750
+ if(d.kind==='picker:selected' && pickingFor){
751
+ const it=cart.find(c=>c.id===pickingFor); if(!it) return;
752
+ if(!it.targets.some(t=>t.animId===d.target.animId)) it.targets.push(d.target);
753
+ renderCart();
754
+ }
755
+ });
756
+ const H={'x-mc-token':TOKEN};
757
+ fetch(BASE+'/api/catalogue',{headers:H}).then(r=>r.json()).then(d=>{MENU=d.items;GROUPS=d.groups;renderCat();});
758
+ let PREVIEWS={}, hoverTimer=null, mx=0, my=0;
759
+ fetch(BASE+'/api/previews',{headers:H}).then(r=>r.json()).then(d=>PREVIEWS=d||{});
760
+ const hc=document.getElementById('hovercard'), hv=document.getElementById('hovervid');
761
+ function placeHover(){ if(hc.style.display==='none')return; const w=300,h=224; let l=mx-w-18; if(l<8)l=mx+18; let t=my-h/2; if(t<8)t=8; if(t+h>innerHeight)t=innerHeight-h-8; hc.style.transform='translate('+l+'px,'+t+'px)'; }
762
+ window.addEventListener('pointermove',e=>{mx=e.clientX;my=e.clientY;placeHover();});
763
+ function showPreview(id){ const p=PREVIEWS[id]; if(!p){hidePreview();return;} if(hoverTimer){clearTimeout(hoverTimer);hoverTimer=null;} hv.poster=BASE+'/previews/'+p.poster; hv.src=BASE+'/previews/'+p.clip; hc.style.display='block'; placeHover(); hv.play&&hv.play().catch(()=>{}); }
764
+ function hidePreview(){ if(hoverTimer)clearTimeout(hoverTimer); hoverTimer=setTimeout(()=>{hc.style.display='none'; hv.pause&&hv.pause(); hv.removeAttribute('src');},120); }
765
+ function inCart(id){return cart.some(c=>c.id===id)}
766
+ function toggle(id){ cart=inCart(id)?cart.filter(c=>c.id!==id):cart.concat([{id,targets:[]}]); if(pickingFor===id)setPicking(null); renderCat();renderCart(); }
767
+ function setPicking(id){ pickingFor=id; send(id?'picker:enable':'picker:disable'); renderCart(); }
768
+ function pickFor(id){ if(!inCart(id))cart.push({id,targets:[]}); setPicking(pickingFor===id?null:id); }
769
+ function title(id){const m=MENU.find(x=>x.id===id);return m?m.title:id}
770
+ function renderCat(){
771
+ const cat=document.getElementById('cat');cat.innerHTML='';
772
+ const matched=GROUPS.map(g=>({g,items:MENU.filter(m=>m.group===g && (!q || (m.title+' '+g).toLowerCase().includes(q)))})).filter(x=>x.items.length);
773
+ if(!matched.length){ cat.innerHTML='<p class="empty">No animations match "'+q+'".</p>'; return; }
774
+ matched.forEach(({g,items})=>{
775
+ const h=document.createElement('div');h.className='grp';h.textContent=g;cat.appendChild(h);
776
+ items.forEach(m=>{const r=document.createElement('div');r.className='row'+(inCart(m.id)?' in':'');
777
+ r.innerHTML='<span>'+m.title+'</span><span>'+(inCart(m.id)?'\\u2713':'+')+'</span>';
778
+ r.onclick=()=>toggle(m.id);
779
+ r.onmouseenter=()=>showPreview(m.id); r.onmouseleave=()=>hidePreview();
780
+ cat.appendChild(r);});
781
+ });
782
+ }
783
+ function renderCart(){
784
+ const c=document.getElementById('cart');c.innerHTML='';
785
+ if(!cart.length){ c.innerHTML='<div class="empty">Your cart is empty. Add animations from the catalogue above.</div>'; }
786
+ cart.forEach(it=>{const d=document.createElement('div');d.className='item'+(pickingFor===it.id?' pick':'');
787
+ const picking=pickingFor===it.id;
788
+ d.innerHTML='<div class="top"><span>'+title(it.id)+'</span><span>'+
789
+ '<button class="btn" data-retry="'+it.id+'">Retry</button> '+
790
+ '<button class="btn '+(picking?'on':'')+'" data-pick="'+it.id+'">'+(picking?'Done picking':('Target'+(it.targets.length?' ('+it.targets.length+')':'')))+'</button> '+
791
+ '<span class="x" data-rm="'+it.id+'">\\u2715</span></span></div>'+
792
+ (it.targets.length?'<div>'+it.targets.map(t=>'<span class="chip">'+t.tag+(t.text?'\\u00b7'+t.text.slice(0,14):'')+'</span>').join('')+'</div>':'');
793
+ c.appendChild(d);});
794
+ c.querySelectorAll('[data-pick]').forEach(b=>b.onclick=()=>pickFor(b.getAttribute('data-pick')));
795
+ c.querySelectorAll('[data-rm]').forEach(b=>b.onclick=()=>toggle(b.getAttribute('data-rm')));
796
+ c.querySelectorAll('[data-retry]').forEach(b=>b.onclick=()=>retryItem(b.getAttribute('data-retry')));
797
+ document.getElementById('apply').disabled=!cart.length||running;
798
+ }
799
+ document.getElementById('q').oninput=e=>{q=e.target.value.trim().toLowerCase();renderCat();};
800
+ document.getElementById('stop').onclick=()=>{ if(abort) abort.abort(); };
801
+ async function runStream(endpoint, body, intro){
802
+ if(running) return; running=true; setPicking(null);
803
+ const log=document.getElementById('log');log.style.display='block';log.textContent=intro+'\\n';
804
+ document.getElementById('apply').disabled=true; document.getElementById('stop').style.display='inline-block';
805
+ const started=Date.now(); abort=new AbortController();
806
+ try{
807
+ const res=await fetch(BASE+endpoint,{method:'POST',headers:{'content-type':'application/json','x-mc-token':TOKEN},body:JSON.stringify(body),signal:abort.signal});
808
+ if(!res.ok||!res.body){ log.textContent+='Error: server returned '+res.status+'\\n'; return; }
809
+ const rd=res.body.getReader();const dec=new TextDecoder();let buf='';
810
+ for(;;){const{value,done}=await rd.read();
811
+ buf+=done?dec.decode():dec.decode(value,{stream:true});
812
+ const fr=buf.split('\\n\\n');buf=done?'':fr.pop();
813
+ for(const f of fr){const l=f.replace(/^data: /,'').trim();if(!l||l[0]===':')continue;let ev;try{ev=JSON.parse(l)}catch{continue}
814
+ if(ev.type==='status')log.textContent+=ev.message+'\\n';
815
+ else if(ev.type==='tool')log.textContent+=(ev.tool==='Read'?'read ':(/Edit|Write/.test(ev.tool)?'edit ':ev.tool.toLowerCase()+' '))+(ev.detail||'')+'\\n';
816
+ else if(ev.type==='assistant')log.textContent+='AI: '+ev.text.slice(0,200)+'\\n';
817
+ else if(ev.type==='done'){log.textContent+='Done'+(ev.turns?' \\u00b7 '+ev.turns+' turns':'')+(ev.cost?' \\u00b7 $'+ev.cost.toFixed(3):'')+'\\n'; if(ev.cost){sessionCost+=ev.cost; document.getElementById('sess').textContent=' \\u00b7 session: $'+sessionCost.toFixed(3);}}
818
+ else if(ev.type==='error')log.textContent+='Error: '+ev.message+'\\n';
819
+ log.scrollTop=log.scrollHeight;}
820
+ if(done)break;
821
+ }
822
+ }catch(err){ log.textContent+=(err&&err.name==='AbortError'?'Stopped':'Error: '+err)+'\\n'; }
823
+ finally{
824
+ running=false;abort=null;
825
+ document.getElementById('apply').disabled=false; document.getElementById('stop').style.display='none';
826
+ const secs=Math.round((Date.now()-started)/1000); log.textContent+='('+secs+'s)\\n';
827
+ setTimeout(()=>{site.src=site.src; refreshStatus();},400);
828
+ }
829
+ }
830
+ document.getElementById('apply').onclick=()=>{ if(cart.length) runStream('/api/apply',{items:cart},'Sending cart to the agent\u2026'); };
831
+ function retryItem(id){
832
+ const it=cart.find(c=>c.id===id); if(!it) return;
833
+ const note=window.prompt('Retry "'+title(id)+'" \u2014 how should it be different? (optional, e.g. "subtler", "slower", "different effect")');
834
+ if(note===null) return;
835
+ runStream('/api/retry',{items:[it],note},'Retrying "'+title(id)+'"'+(note?' \u2014 '+note:'')+'\u2026');
836
+ }
837
+ async function refreshStatus(){
838
+ try{
839
+ const s=await (await fetch(BASE+'/api/status',{headers:H})).json();
840
+ document.getElementById('savebar').style.display=(s.active && s.checkpoints>0)?'flex':'none';
841
+ document.getElementById('merge').textContent='Merge \u2192 '+s.base;
842
+ document.getElementById('push').style.display=s.remote?'block':'none';
843
+ document.getElementById('undo').disabled=s.checkpoints<1;
844
+ }catch(e){}
845
+ }
846
+ async function gitAction(endpoint, confirmMsg){
847
+ if(confirmMsg && !window.confirm(confirmMsg)) return;
848
+ const log=document.getElementById('log');log.style.display='block';
849
+ try{ const r=await fetch(BASE+endpoint,{method:'POST',headers:H}); const d=await r.json();
850
+ log.textContent+=(d.ok?'\\u2713 '+endpoint.replace('/api/','')+(d.merged?' '+d.merged:'')+(d.branch?' '+d.branch:''):'Error: '+(d.error||r.status))+'\\n';
851
+ }catch(e){ log.textContent+='Error: '+e+'\\n'; }
852
+ setTimeout(()=>{site.src=site.src; refreshStatus();},400);
853
+ }
854
+ document.getElementById('undo').onclick=()=>gitAction('/api/undo');
855
+ document.getElementById('merge').onclick=()=>gitAction('/api/merge','Merge this session into your base branch?');
856
+ document.getElementById('push').onclick=()=>gitAction('/api/push');
857
+ document.getElementById('discard').onclick=()=>gitAction('/api/discard','Discard the whole session (delete the branch)?');
858
+ refreshStatus();
859
+ </script>
860
+ </body></html>`;
861
+ }
659
862
  async function detectFramework(targetDir) {
660
863
  let pkg = {};
661
864
  try {
@@ -681,7 +884,7 @@ function frameworkGuidance(fw) {
681
884
  return "Vanilla JS/DOM. Use the `motion` package's `animate()` / `scroll()` / `inView()` functions directly on elements; no JSX. Adapt React reference techniques to imperative DOM calls.";
682
885
  }
683
886
  }
684
- function composeInstruction(framework, items) {
887
+ function composeInstruction(framework, items, note) {
685
888
  const lines = items.flatMap((ci) => {
686
889
  const it = MENU_BY_ID[ci.id];
687
890
  if (!it) return [];
@@ -706,6 +909,7 @@ function composeInstruction(framework, items) {
706
909
  `- Keep it performant (animate transform/opacity/filter; avoid layout thrash; respect prefers-reduced-motion).`,
707
910
  `- If the Motion library isn't installed yet, note it clearly in your summary (do not run package installs).`,
708
911
  `- The project must keep compiling.`,
912
+ ...note ? [``, `Adjustment requested for this pass: ${note}`] : [],
709
913
  ``,
710
914
  `Explain briefly what you changed.`
711
915
  ].join("\n");
@@ -735,8 +939,15 @@ function inside(root, abs) {
735
939
  }
736
940
  return abs === realRoot || abs.startsWith(realRoot + path2.sep);
737
941
  }
942
+ function authHint(message) {
943
+ if (/401|authenticat|bearer|api key|credential|unauthor/i.test(message)) {
944
+ return `${message}
945
+ \u2192 Auth: pass --api-key/--base-url (e.g. iO Bonzai), log into Claude Code, or set ANTHROPIC_API_KEY. Note: an invalid env key overrides your Claude login.`;
946
+ }
947
+ return message;
948
+ }
738
949
  async function* runAnimationAgent(opts) {
739
- const { root, framework, items, motionToken } = opts;
950
+ const { root, framework, items, motionToken, baseUrl, apiKey, model, note } = opts;
740
951
  const guard = async (tool, input) => {
741
952
  if (BLOCKED_TOOLS.has(tool)) return { behavior: "deny", message: `${tool} is disabled.` };
742
953
  if (WRITE_TOOLS.has(tool)) {
@@ -764,12 +975,18 @@ async function* runAnimationAgent(opts) {
764
975
  env: { TOKEN: motionToken }
765
976
  }
766
977
  } : void 0;
978
+ const agentEnv = baseUrl || apiKey ? {
979
+ ...process.env,
980
+ ...baseUrl ? { ANTHROPIC_BASE_URL: baseUrl } : {},
981
+ ...apiKey ? { ANTHROPIC_AUTH_TOKEN: apiKey, ANTHROPIC_API_KEY: apiKey } : {}
982
+ } : void 0;
983
+ const providerNote = baseUrl ? ` via ${new URL(baseUrl).host}` : "";
767
984
  yield {
768
985
  type: "status",
769
- message: `Agent starting on a ${framework} project with ${items.length} animation(s)\u2026 ${mcpServers ? "(Motion AI Kit MCP enabled)" : "(bundled Motion guidance)"}`
986
+ message: `Agent starting on a ${framework} project with ${items.length} animation(s)${providerNote}\u2026 ${mcpServers ? "(Motion AI Kit MCP enabled)" : "(bundled Motion guidance)"}`
770
987
  };
771
988
  const response = query({
772
- prompt: composeInstruction(framework, items),
989
+ prompt: composeInstruction(framework, items, note),
773
990
  options: {
774
991
  cwd: root,
775
992
  permissionMode: "default",
@@ -777,6 +994,8 @@ async function* runAnimationAgent(opts) {
777
994
  allowedTools: ["Read", "Write", "Edit", "Glob", "Grep"],
778
995
  disallowedTools: [...BLOCKED_TOOLS],
779
996
  ...mcpServers ? { mcpServers } : {},
997
+ ...agentEnv ? { env: agentEnv } : {},
998
+ ...model ? { model } : {},
780
999
  settingSources: [],
781
1000
  maxTurns: 80
782
1001
  }
@@ -799,12 +1018,12 @@ async function* runAnimationAgent(opts) {
799
1018
  if (message.subtype === "success") {
800
1019
  yield { type: "done", summary: message.result, cost: message.total_cost_usd, turns: message.num_turns, ms: message.duration_ms };
801
1020
  } else {
802
- yield { type: "error", message: `Agent ended: ${message.subtype}` };
1021
+ yield { type: "error", message: authHint(`Agent ended: ${message.subtype}`) };
803
1022
  }
804
1023
  }
805
1024
  }
806
1025
  } catch (err) {
807
- yield { type: "error", message: err instanceof Error ? err.message : String(err) };
1026
+ yield { type: "error", message: authHint(err instanceof Error ? err.message : String(err)) };
808
1027
  }
809
1028
  }
810
1029
  var run = promisify(execFile);
@@ -849,6 +1068,247 @@ async function diffStat(dir) {
849
1068
  return "";
850
1069
  }
851
1070
  }
1071
+ async function headSha(dir) {
1072
+ return git(dir, ["rev-parse", "--short", "HEAD"]);
1073
+ }
1074
+ async function getRemote(dir) {
1075
+ try {
1076
+ const out = await git(dir, ["remote"]);
1077
+ const first = out.split("\n").map((s) => s.trim()).filter(Boolean)[0];
1078
+ return first ?? null;
1079
+ } catch {
1080
+ return null;
1081
+ }
1082
+ }
1083
+ async function pushBranch(dir, branch, remote = "origin") {
1084
+ await git(dir, ["push", "-u", remote, branch]);
1085
+ }
1086
+ async function resetHard(dir, ref) {
1087
+ await git(dir, ["reset", "--hard", ref]);
1088
+ }
1089
+ async function checkoutBranch(dir, branch) {
1090
+ await git(dir, ["checkout", branch]);
1091
+ }
1092
+ async function deleteBranch(dir, branch) {
1093
+ await git(dir, ["branch", "-D", branch]);
1094
+ }
1095
+ async function mergeToBase(dir, base, session) {
1096
+ await git(dir, ["checkout", base]);
1097
+ await git(dir, ["merge", "--no-ff", "--no-edit", session]);
1098
+ }
1099
+ var MAX_BODY = 256 * 1024;
1100
+ function readBody(req) {
1101
+ return new Promise((resolve, reject) => {
1102
+ let b = "";
1103
+ let size = 0;
1104
+ req.on("data", (c) => {
1105
+ size += c.length;
1106
+ if (size > MAX_BODY) {
1107
+ reject(new Error("body too large"));
1108
+ req.destroy();
1109
+ return;
1110
+ }
1111
+ b += c;
1112
+ });
1113
+ req.on("end", () => resolve(b));
1114
+ req.on("error", reject);
1115
+ });
1116
+ }
1117
+ function createNodeDevApi(cfg) {
1118
+ const base = cfg.base ?? "";
1119
+ const token = randomUUID();
1120
+ let sessionBranch = null;
1121
+ let baseBranch = "HEAD";
1122
+ let baseSha = "";
1123
+ const checkpoints = [];
1124
+ async function ensureSession() {
1125
+ if (sessionBranch) return;
1126
+ if (!await isGitRepo(cfg.targetDir)) throw new Error("target is not a git repository");
1127
+ if (!await isClean(cfg.targetDir))
1128
+ throw new Error("target git tree has uncommitted changes \u2014 commit or stash first");
1129
+ baseBranch = await currentBranch(cfg.targetDir);
1130
+ sessionBranch = `motion-cart/session-${Date.now()}`;
1131
+ await checkoutNewBranch(cfg.targetDir, sessionBranch);
1132
+ baseSha = await headSha(cfg.targetDir);
1133
+ checkpoints.length = 0;
1134
+ }
1135
+ const json = (res, code, body) => {
1136
+ res.writeHead(code, { "content-type": "application/json" });
1137
+ res.end(JSON.stringify(body));
1138
+ };
1139
+ const authed = (req) => req.headers["x-mc-token"] === token;
1140
+ async function runAgentSSE(res, items, note) {
1141
+ res.writeHead(200, {
1142
+ "content-type": "text/event-stream; charset=utf-8",
1143
+ "cache-control": "no-cache, no-transform",
1144
+ connection: "keep-alive"
1145
+ });
1146
+ const send = (e) => res.write(`data: ${JSON.stringify(e)}
1147
+
1148
+ `);
1149
+ const heartbeat = setInterval(() => res.write(`: ping
1150
+
1151
+ `), 15e3);
1152
+ try {
1153
+ if (!items.length) throw new Error("cart is empty");
1154
+ const fresh = !sessionBranch;
1155
+ await ensureSession();
1156
+ if (fresh)
1157
+ send({
1158
+ type: "status",
1159
+ message: `Editing on branch ${sessionBranch} (branched from ${baseBranch}; your ${baseBranch} branch is untouched)`
1160
+ });
1161
+ for await (const ev of runAnimationAgent({
1162
+ root: cfg.targetDir,
1163
+ framework: cfg.framework,
1164
+ items,
1165
+ note,
1166
+ motionToken: cfg.token,
1167
+ baseUrl: cfg.baseUrl,
1168
+ apiKey: cfg.apiKey,
1169
+ model: cfg.model
1170
+ }))
1171
+ send(ev);
1172
+ const stat = await diffStat(cfg.targetDir);
1173
+ const committed = await commitAll(
1174
+ cfg.targetDir,
1175
+ note ? `motion-cart: retry \u2014 ${note.slice(0, 60)}` : `motion-cart: applied ${items.length} animation(s)`
1176
+ );
1177
+ if (committed) {
1178
+ checkpoints.push(await headSha(cfg.targetDir));
1179
+ if (stat) send({ type: "status", message: `Committed (checkpoint ${checkpoints.length}):
1180
+ ${stat}` });
1181
+ }
1182
+ } catch (err) {
1183
+ send({ type: "error", message: err instanceof Error ? err.message : String(err) });
1184
+ } finally {
1185
+ clearInterval(heartbeat);
1186
+ send({ type: "end" });
1187
+ res.end();
1188
+ }
1189
+ }
1190
+ async function servedPreview(res, file0) {
1191
+ if (!cfg.previewsDir) return false;
1192
+ const file = path3.basename(decodeURIComponent(file0));
1193
+ if (file === "preview-manifest.json") {
1194
+ try {
1195
+ res.writeHead(200, { "content-type": "application/json" });
1196
+ res.end(await fs2.readFile(path3.join(cfg.previewsDir, file)));
1197
+ } catch {
1198
+ json(res, 200, {});
1199
+ }
1200
+ return true;
1201
+ }
1202
+ if (!/^[\w.-]+\.(webm|jpg)$/.test(file)) {
1203
+ res.writeHead(404).end();
1204
+ return true;
1205
+ }
1206
+ try {
1207
+ const buf = await fs2.readFile(path3.join(cfg.previewsDir, file));
1208
+ res.writeHead(200, {
1209
+ "content-type": file.endsWith(".webm") ? "video/webm" : "image/jpeg",
1210
+ "cache-control": "public, max-age=3600"
1211
+ });
1212
+ res.end(buf);
1213
+ } catch {
1214
+ res.writeHead(404).end();
1215
+ }
1216
+ return true;
1217
+ }
1218
+ async function handle(req, res) {
1219
+ const url = new URL(req.url || "/", "http://localhost");
1220
+ let p = url.pathname;
1221
+ if (base && p.startsWith(base)) p = p.slice(base.length) || "/";
1222
+ else if (base) return false;
1223
+ if (req.method === "GET" && p === "/") {
1224
+ res.writeHead(200, { "content-type": "text/html; charset=utf-8" });
1225
+ res.end(panelHtml({ base, iframeSrc: cfg.iframeSrc, target: cfg.targetDir, framework: cfg.framework, token }));
1226
+ return true;
1227
+ }
1228
+ if (p.startsWith("/previews/")) return servedPreview(res, p.slice("/previews/".length));
1229
+ if (!p.startsWith("/api/")) return false;
1230
+ if (!authed(req)) {
1231
+ json(res, 403, { error: "forbidden" });
1232
+ return true;
1233
+ }
1234
+ if (req.method === "GET" && p === "/api/catalogue") {
1235
+ json(res, 200, { groups: MENU_GROUPS, items: MENU });
1236
+ return true;
1237
+ }
1238
+ if (req.method === "GET" && p === "/api/previews") return servedPreview(res, "preview-manifest.json");
1239
+ if (req.method === "GET" && p === "/api/status") {
1240
+ const remote = sessionBranch ? await getRemote(cfg.targetDir) : null;
1241
+ json(res, 200, { base: baseBranch, session: sessionBranch, checkpoints: checkpoints.length, remote, active: !!sessionBranch });
1242
+ return true;
1243
+ }
1244
+ if (req.method === "POST" && (p === "/api/apply" || p === "/api/retry")) {
1245
+ let items = [];
1246
+ let note;
1247
+ try {
1248
+ const body = JSON.parse(await readBody(req));
1249
+ items = body.items ?? [];
1250
+ if (p === "/api/retry" && typeof body.note === "string" && body.note.trim()) note = body.note.trim();
1251
+ } catch {
1252
+ }
1253
+ await runAgentSSE(res, items, note);
1254
+ return true;
1255
+ }
1256
+ if (req.method === "POST" && p === "/api/undo") {
1257
+ if (!sessionBranch || checkpoints.length === 0) return json(res, 400, { error: "nothing to undo" }), true;
1258
+ const target = checkpoints.length >= 2 ? checkpoints[checkpoints.length - 2] : baseSha;
1259
+ try {
1260
+ await resetHard(cfg.targetDir, target);
1261
+ checkpoints.pop();
1262
+ json(res, 200, { ok: true, checkpoints: checkpoints.length });
1263
+ } catch (err) {
1264
+ json(res, 500, { error: err instanceof Error ? err.message : String(err) });
1265
+ }
1266
+ return true;
1267
+ }
1268
+ if (req.method === "POST" && p === "/api/push") {
1269
+ if (!sessionBranch) return json(res, 400, { error: "no active session" }), true;
1270
+ const remote = await getRemote(cfg.targetDir);
1271
+ if (!remote) return json(res, 400, { error: "no git remote configured" }), true;
1272
+ try {
1273
+ await pushBranch(cfg.targetDir, sessionBranch, remote);
1274
+ json(res, 200, { ok: true, remote, branch: sessionBranch });
1275
+ } catch (err) {
1276
+ json(res, 500, { error: err instanceof Error ? err.message : String(err) });
1277
+ }
1278
+ return true;
1279
+ }
1280
+ if (req.method === "POST" && p === "/api/merge") {
1281
+ if (!sessionBranch) return json(res, 400, { error: "no active session" }), true;
1282
+ const merged = sessionBranch;
1283
+ try {
1284
+ await mergeToBase(cfg.targetDir, baseBranch, sessionBranch);
1285
+ sessionBranch = null;
1286
+ checkpoints.length = 0;
1287
+ json(res, 200, { ok: true, merged, base: baseBranch });
1288
+ } catch (err) {
1289
+ json(res, 500, { error: err instanceof Error ? err.message : String(err) });
1290
+ }
1291
+ return true;
1292
+ }
1293
+ if (req.method === "POST" && p === "/api/discard") {
1294
+ if (!sessionBranch) return json(res, 400, { error: "no active session" }), true;
1295
+ const branch = sessionBranch;
1296
+ try {
1297
+ await checkoutBranch(cfg.targetDir, baseBranch);
1298
+ await deleteBranch(cfg.targetDir, branch);
1299
+ sessionBranch = null;
1300
+ checkpoints.length = 0;
1301
+ json(res, 200, { ok: true, discarded: branch });
1302
+ } catch (err) {
1303
+ json(res, 500, { error: err instanceof Error ? err.message : String(err) });
1304
+ }
1305
+ return true;
1306
+ }
1307
+ json(res, 404, { error: "not found" });
1308
+ return true;
1309
+ }
1310
+ return { token, handle };
1311
+ }
852
1312
 
853
1313
  // src/proxy.ts
854
1314
  import http from "http";
@@ -950,261 +1410,71 @@ function startProxy(devUrl, port2, panelOrigin) {
950
1410
 
951
1411
  // src/server.ts
952
1412
  import http2 from "http";
953
- import { randomUUID } from "crypto";
954
-
955
- // src/panel.ts
956
- function panelHtml(opts) {
957
- return `<!doctype html>
958
- <html><head><meta charset="utf-8"><title>motion-cart</title>
959
- <style>
960
- :root{color-scheme:dark}
961
- *{box-sizing:border-box}
962
- body{margin:0;height:100vh;display:flex;flex-direction:column;background:#0a0b0e;color:#e7e7ea;font:13px system-ui}
963
- header{display:flex;align-items:center;gap:12px;padding:10px 14px;border-bottom:1px solid #ffffff1a}
964
- header b{font-size:14px}header small{color:#ffffff80}
965
- .body{flex:1;display:flex;min-height:0}
966
- iframe{flex:1;border:0;background:#fff}
967
- aside{width:360px;border-left:1px solid #ffffff1a;display:flex;flex-direction:column;min-height:0}
968
- input{width:100%;background:#ffffff0d;border:1px solid #ffffff26;border-radius:8px;color:#fff;padding:9px 11px;outline:none;font-size:13px}
969
- input:focus{border-color:#22d3ee80}
970
- .hint{color:#ffffff73;font-size:12px;margin:8px 2px 0}
971
- .cat{flex:1;overflow:auto;padding:8px 10px}
972
- .grp{font-size:12px;text-transform:uppercase;letter-spacing:.05em;color:#ffffff8c;margin:12px 2px 5px;font-weight:600}
973
- .row{display:flex;justify-content:space-between;align-items:center;padding:8px 10px;border-radius:6px;cursor:pointer;color:#ffffffcc;font-size:13px}
974
- .row:hover{background:#ffffff14;color:#fff}
975
- .row.in{background:#22d3ee30;color:#a5f3fc;box-shadow:inset 0 0 0 1px #22d3ee66}
976
- footer{border-top:1px solid #ffffff1a;padding:11px}
977
- .item{border:1px solid #ffffff1a;border-radius:6px;padding:7px 9px;margin-bottom:6px;background:#ffffff0a}
978
- .item.pick{border-color:#22d3eecc;background:#22d3ee26}
979
- .item .top{display:flex;justify-content:space-between;align-items:center;gap:6px}
980
- .btn{border:1px solid #ffffff33;border-radius:5px;background:#ffffff0d;color:#fff;padding:4px 9px;font-size:12px;cursor:pointer}
981
- .btn.on{background:#22d3ee;color:#06202a;border-color:#22d3ee}
982
- .chip{display:inline-flex;gap:5px;align-items:center;background:#00000080;border-radius:4px;padding:2px 7px;font-size:11px;color:#ffffffb3;margin:4px 4px 0 0}
983
- .empty{border:1px dashed #ffffff26;border-radius:6px;padding:12px;text-align:center;color:#ffffff73;font-size:12px;margin-bottom:8px}
984
- .row2{display:flex;gap:8px}
985
- .apply{flex:1;padding:11px;border:0;border-radius:8px;font-weight:600;color:#fff;cursor:pointer;background:linear-gradient(90deg,#06b6d4,#6366f1);font-size:13px}
986
- .apply:disabled{opacity:.35;cursor:not-allowed}
987
- .stop{padding:11px 14px;border:1px solid #ffffff33;border-radius:8px;background:#ffffff0d;color:#fff;cursor:pointer}
988
- .note{text-align:center;color:#ffffff66;font-size:11px;margin-top:7px}
989
- pre{margin:7px 0 0;max-height:200px;overflow:auto;background:#000000a6;border:1px solid #ffffff26;border-radius:6px;padding:9px;font:12px ui-monospace;color:#ffffffd9;white-space:pre-wrap}
990
- .x{color:#ffffff73;cursor:pointer}.x:hover{color:#fff}
991
- </style></head>
992
- <body>
993
- <header><b>motion-cart</b><small>${opts.framework} &middot; ${opts.target}</small></header>
994
- <div class="body">
995
- <iframe id="site" src="${opts.proxyUrl}"></iframe>
996
- <aside>
997
- <div style="padding:11px;border-bottom:1px solid #ffffff1a">
998
- <input id="q" placeholder="Search animations\u2026">
999
- <div class="hint">Click an animation to add it &middot; then optionally pick the elements to target.</div>
1000
- </div>
1001
- <div class="cat" id="cat"></div>
1002
- <footer><div id="cart"></div>
1003
- <div class="row2">
1004
- <button class="apply" id="apply" disabled>Apply with AI</button>
1005
- <button class="stop" id="stop" style="display:none">Stop</button>
1006
- </div>
1007
- <div class="note">Runs a real Claude agent on your project &middot; ~30s &middot; uses AI credits<span id="sess"></span></div>
1008
- <pre id="log" style="display:none"></pre>
1009
- </footer>
1010
- </aside>
1011
- </div>
1012
- <script>
1013
- const MC="motion-cart", PROXY=${JSON.stringify(opts.proxyUrl)}, TOKEN=${JSON.stringify(opts.token)};
1014
- let MENU=[],GROUPS=[],cart=[],pickingFor=null,running=false,q="",sessionCost=0,abort=null;
1015
- const site=document.getElementById('site');
1016
- function send(kind){ try{ site.contentWindow.postMessage({source:MC,kind},PROXY);}catch(e){} }
1017
- window.addEventListener('message',e=>{
1018
- if(e.origin!==PROXY) return;
1019
- const d=e.data; if(!d||d.source!==MC) return;
1020
- if(d.kind==='picker:ready' && pickingFor) send('picker:enable');
1021
- if(d.kind==='picker:selected' && pickingFor){
1022
- const it=cart.find(c=>c.id===pickingFor); if(!it) return;
1023
- if(!it.targets.some(t=>t.animId===d.target.animId)) it.targets.push(d.target);
1024
- renderCart();
1025
- }
1026
- });
1027
- fetch('/api/catalogue',{headers:{'x-mc-token':TOKEN}}).then(r=>r.json()).then(d=>{MENU=d.items;GROUPS=d.groups;renderCat();});
1028
- function inCart(id){return cart.some(c=>c.id===id)}
1029
- function toggle(id){ cart=inCart(id)?cart.filter(c=>c.id!==id):cart.concat([{id,targets:[]}]); if(pickingFor===id)setPicking(null); renderCat();renderCart(); }
1030
- function setPicking(id){ pickingFor=id; send(id?'picker:enable':'picker:disable'); renderCart(); }
1031
- function pickFor(id){ if(!inCart(id))cart.push({id,targets:[]}); setPicking(pickingFor===id?null:id); }
1032
- function title(id){const m=MENU.find(x=>x.id===id);return m?m.title:id}
1033
- function renderCat(){
1034
- const cat=document.getElementById('cat');cat.innerHTML='';
1035
- const matched=GROUPS.map(g=>({g,items:MENU.filter(m=>m.group===g && (!q || (m.title+' '+g).toLowerCase().includes(q)))})).filter(x=>x.items.length);
1036
- if(!matched.length){ cat.innerHTML='<p class="empty">No animations match "'+q+'".</p>'; return; }
1037
- matched.forEach(({g,items})=>{
1038
- const h=document.createElement('div');h.className='grp';h.textContent=g;cat.appendChild(h);
1039
- items.forEach(m=>{const r=document.createElement('div');r.className='row'+(inCart(m.id)?' in':'');
1040
- r.innerHTML='<span>'+m.title+'</span><span>'+(inCart(m.id)?'\\u2713':'+')+'</span>';
1041
- r.onclick=()=>toggle(m.id);cat.appendChild(r);});
1042
- });
1043
- }
1044
- function renderCart(){
1045
- const c=document.getElementById('cart');c.innerHTML='';
1046
- if(!cart.length){ c.innerHTML='<div class="empty">Your cart is empty. Add animations from the catalogue above.</div>'; }
1047
- cart.forEach(it=>{const d=document.createElement('div');d.className='item'+(pickingFor===it.id?' pick':'');
1048
- const picking=pickingFor===it.id;
1049
- d.innerHTML='<div class="top"><span>'+title(it.id)+'</span><span>'+
1050
- '<button class="btn '+(picking?'on':'')+'" data-pick="'+it.id+'">'+(picking?'Done picking':('Target'+(it.targets.length?' ('+it.targets.length+')':'')))+'</button> '+
1051
- '<span class="x" data-rm="'+it.id+'">\\u2715</span></span></div>'+
1052
- (it.targets.length?'<div>'+it.targets.map(t=>'<span class="chip">'+t.tag+(t.text?'\\u00b7'+t.text.slice(0,14):'')+'</span>').join('')+'</div>':'');
1053
- c.appendChild(d);});
1054
- c.querySelectorAll('[data-pick]').forEach(b=>b.onclick=()=>pickFor(b.getAttribute('data-pick')));
1055
- c.querySelectorAll('[data-rm]').forEach(b=>b.onclick=()=>toggle(b.getAttribute('data-rm')));
1056
- document.getElementById('apply').disabled=!cart.length||running;
1057
- }
1058
- document.getElementById('q').oninput=e=>{q=e.target.value.trim().toLowerCase();renderCat();};
1059
- document.getElementById('stop').onclick=()=>{ if(abort) abort.abort(); };
1060
- document.getElementById('apply').onclick=async()=>{
1061
- if(!cart.length||running)return; running=true; setPicking(null);
1062
- const log=document.getElementById('log');log.style.display='block';log.textContent='Sending cart to the agent\u2026\\n';
1063
- document.getElementById('apply').disabled=true; document.getElementById('stop').style.display='inline-block';
1064
- const started=Date.now();
1065
- abort=new AbortController();
1066
- try{
1067
- const res=await fetch('/api/apply',{method:'POST',headers:{'content-type':'application/json','x-mc-token':TOKEN},body:JSON.stringify({items:cart}),signal:abort.signal});
1068
- if(!res.ok||!res.body){ log.textContent+='Error: server returned '+res.status+'\\n'; return; }
1069
- const rd=res.body.getReader();const dec=new TextDecoder();let buf='';
1070
- for(;;){const{value,done}=await rd.read();
1071
- buf+=done?dec.decode():dec.decode(value,{stream:true});
1072
- const fr=buf.split('\\n\\n');buf=done?'':fr.pop();
1073
- for(const f of fr){const l=f.replace(/^data: /,'').trim();if(!l||l[0]===':')continue;let ev;try{ev=JSON.parse(l)}catch{continue}
1074
- if(ev.type==='status')log.textContent+=ev.message+'\\n';
1075
- else if(ev.type==='tool')log.textContent+=(ev.tool==='Read'?'read ':(/Edit|Write/.test(ev.tool)?'edit ':ev.tool.toLowerCase()+' '))+(ev.detail||'')+'\\n';
1076
- else if(ev.type==='assistant')log.textContent+='AI: '+ev.text.slice(0,200)+'\\n';
1077
- else if(ev.type==='done'){log.textContent+='Done'+(ev.turns?' \\u00b7 '+ev.turns+' turns':'')+(ev.cost?' \\u00b7 $'+ev.cost.toFixed(3):'')+'\\n'; if(ev.cost){sessionCost+=ev.cost; document.getElementById('sess').textContent=' \\u00b7 session: $'+sessionCost.toFixed(3);}}
1078
- else if(ev.type==='error')log.textContent+='Error: '+ev.message+'\\n';
1079
- log.scrollTop=log.scrollHeight;}
1080
- if(done)break;
1081
- }
1082
- }catch(err){ log.textContent+=(err&&err.name==='AbortError'?'Stopped':'Error: '+err)+'\\n'; }
1083
- finally{
1084
- running=false;abort=null;
1085
- document.getElementById('apply').disabled=false; document.getElementById('stop').style.display='none';
1086
- const secs=Math.round((Date.now()-started)/1000); log.textContent+='('+secs+'s)\\n';
1087
- setTimeout(()=>{site.src=site.src;},400);
1088
- }
1089
- };
1090
- </script>
1091
- </body></html>`;
1092
- }
1093
-
1094
- // src/server.ts
1095
- var MAX_BODY = 256 * 1024;
1096
- function readBody(req) {
1097
- return new Promise((resolve, reject) => {
1098
- let b = "";
1099
- let size = 0;
1100
- req.on("data", (c) => {
1101
- size += c.length;
1102
- if (size > MAX_BODY) {
1103
- reject(new Error("body too large"));
1104
- req.destroy();
1105
- return;
1106
- }
1107
- b += c;
1108
- });
1109
- req.on("end", () => resolve(b));
1110
- req.on("error", reject);
1111
- });
1112
- }
1413
+ import path4 from "path";
1414
+ import { fileURLToPath } from "url";
1415
+ var PREVIEWS_DIR = path4.resolve(path4.dirname(fileURLToPath(import.meta.url)), "../previews");
1113
1416
  function startControlServer(cfg, port2) {
1114
1417
  const panelOrigin = `http://localhost:${port2}`;
1115
- const sessionToken = randomUUID();
1116
- let sessionBranch = null;
1117
- let baseBranch = "HEAD";
1118
- async function ensureSession() {
1119
- if (sessionBranch) return;
1120
- if (!await isGitRepo(cfg.targetDir)) throw new Error("target is not a git repository");
1121
- if (!await isClean(cfg.targetDir))
1122
- throw new Error("target git tree has uncommitted changes \u2014 commit or stash first");
1123
- baseBranch = await currentBranch(cfg.targetDir);
1124
- sessionBranch = `motion-cart/session-${Date.now()}`;
1125
- await checkoutNewBranch(cfg.targetDir, sessionBranch);
1126
- }
1127
- function authed(req) {
1128
- const origin = req.headers["origin"];
1129
- if (origin && origin !== panelOrigin) return false;
1130
- return req.headers["x-mc-token"] === sessionToken;
1131
- }
1418
+ const api = createNodeDevApi({
1419
+ targetDir: cfg.targetDir,
1420
+ framework: cfg.framework,
1421
+ iframeSrc: cfg.proxyUrl,
1422
+ // the panel embeds the proxied target site
1423
+ base: "",
1424
+ previewsDir: PREVIEWS_DIR,
1425
+ token: cfg.token,
1426
+ baseUrl: cfg.baseUrl,
1427
+ apiKey: cfg.apiKey,
1428
+ model: cfg.model
1429
+ });
1132
1430
  const server = http2.createServer(async (req, res) => {
1133
- const url = new URL(req.url || "/", panelOrigin);
1134
- if (req.method === "GET" && url.pathname === "/") {
1135
- res.writeHead(200, { "content-type": "text/html; charset=utf-8" });
1136
- return res.end(
1137
- panelHtml({
1138
- proxyUrl: cfg.proxyUrl,
1139
- target: cfg.targetDir,
1140
- framework: cfg.framework,
1141
- token: sessionToken
1142
- })
1143
- );
1144
- }
1145
- if (url.pathname.startsWith("/api/")) {
1146
- if (!authed(req)) {
1147
- res.writeHead(403, { "content-type": "application/json" });
1148
- return res.end(JSON.stringify({ error: "forbidden" }));
1149
- }
1150
- }
1151
- if (req.method === "GET" && url.pathname === "/api/catalogue") {
1152
- res.writeHead(200, { "content-type": "application/json" });
1153
- return res.end(JSON.stringify({ groups: MENU_GROUPS, items: MENU }));
1154
- }
1155
- if (req.method === "POST" && url.pathname === "/api/apply") {
1156
- let items = [];
1157
- try {
1158
- items = JSON.parse(await readBody(req)).items ?? [];
1159
- } catch {
1160
- }
1161
- res.writeHead(200, {
1162
- "content-type": "text/event-stream; charset=utf-8",
1163
- "cache-control": "no-cache, no-transform",
1164
- connection: "keep-alive"
1165
- });
1166
- const send = (e) => res.write(`data: ${JSON.stringify(e)}
1167
-
1168
- `);
1169
- const heartbeat = setInterval(() => res.write(`: ping
1170
-
1171
- `), 15e3);
1172
- try {
1173
- if (!items.length) throw new Error("cart is empty");
1174
- await ensureSession();
1175
- send({
1176
- type: "status",
1177
- message: `Editing on branch ${sessionBranch} (branched from ${baseBranch}; your ${baseBranch} branch is untouched)`
1178
- });
1179
- for await (const ev of runAnimationAgent({
1180
- root: cfg.targetDir,
1181
- framework: cfg.framework,
1182
- items,
1183
- motionToken: cfg.token
1184
- })) {
1185
- send(ev);
1186
- }
1187
- const stat = await diffStat(cfg.targetDir);
1188
- const committed = await commitAll(cfg.targetDir, `motion-cart: applied ${items.length} animation(s)`);
1189
- if (committed && stat) send({ type: "status", message: `Committed:
1190
- ${stat}` });
1191
- send({ type: "status", message: `Review: git diff ${baseBranch}..${sessionBranch}` });
1192
- } catch (err) {
1193
- send({ type: "error", message: err instanceof Error ? err.message : String(err) });
1194
- } finally {
1195
- clearInterval(heartbeat);
1196
- send({ type: "end" });
1197
- res.end();
1198
- }
1199
- return;
1431
+ if (req.url?.startsWith("/__mc/previews/")) req.url = req.url.replace("/__mc/previews/", "/previews/");
1432
+ if (!await api.handle(req, res)) {
1433
+ res.writeHead(404);
1434
+ res.end();
1200
1435
  }
1201
- res.writeHead(404);
1202
- res.end();
1203
1436
  });
1204
1437
  server.listen(port2, "127.0.0.1");
1205
1438
  return { server, url: panelOrigin };
1206
1439
  }
1207
1440
 
1441
+ // src/config.ts
1442
+ import { promises as fs3 } from "fs";
1443
+ import os from "os";
1444
+ import path5 from "path";
1445
+ var DIR = path5.join(os.homedir(), ".motion-cart");
1446
+ var FILE = path5.join(DIR, "config.json");
1447
+ function configPath() {
1448
+ return FILE;
1449
+ }
1450
+ async function loadConfig() {
1451
+ try {
1452
+ return JSON.parse(await fs3.readFile(FILE, "utf8"));
1453
+ } catch {
1454
+ return {};
1455
+ }
1456
+ }
1457
+ async function saveConfig(patch) {
1458
+ const current = await loadConfig();
1459
+ const merged = { ...current };
1460
+ for (const [k, v] of Object.entries(patch)) {
1461
+ if (v != null && v !== "") merged[k] = v;
1462
+ }
1463
+ await fs3.mkdir(DIR, { recursive: true });
1464
+ await fs3.writeFile(FILE, JSON.stringify(merged, null, 2), { mode: 384 });
1465
+ try {
1466
+ await fs3.chmod(FILE, 384);
1467
+ } catch {
1468
+ }
1469
+ return merged;
1470
+ }
1471
+ async function clearConfig() {
1472
+ try {
1473
+ await fs3.rm(FILE);
1474
+ } catch {
1475
+ }
1476
+ }
1477
+
1208
1478
  // src/index.ts
1209
1479
  function arg(name, fallback) {
1210
1480
  const i = process.argv.indexOf(`--${name}`);
@@ -1214,17 +1484,38 @@ function arg(name, fallback) {
1214
1484
  }
1215
1485
  return fallback;
1216
1486
  }
1487
+ async function runConfig() {
1488
+ if (process.argv.includes("--clear")) {
1489
+ await clearConfig();
1490
+ console.log("motion-cart: provider config cleared.");
1491
+ return;
1492
+ }
1493
+ const patch = { baseUrl: arg("base-url"), apiKey: arg("api-key"), model: arg("model") };
1494
+ const cfg = Object.values(patch).some((v) => v) ? await saveConfig(patch) : await loadConfig();
1495
+ const key = cfg.apiKey ? `${cfg.apiKey.slice(0, 6)}\u2026 (set)` : "(unset)";
1496
+ console.log(`motion-cart provider config \u2014 ${configPath()}`);
1497
+ console.log(` base-url ${cfg.baseUrl || "(unset \u2192 Claude Code login)"}`);
1498
+ console.log(` api-key ${key}`);
1499
+ console.log(` model ${cfg.model || "(default)"}`);
1500
+ console.log(`
1501
+ Flags and MOTION_CART_* env vars override these per run.`);
1502
+ }
1217
1503
  function port(name, def) {
1218
1504
  const v = Number(arg(name, String(def)));
1219
1505
  return Number.isInteger(v) && v > 0 && v < 65536 ? v : def;
1220
1506
  }
1221
1507
  var LOOPBACK = /* @__PURE__ */ new Set(["localhost", "127.0.0.1", "0.0.0.0", "[::1]", "::1"]);
1222
1508
  async function main() {
1223
- const target = path3.resolve(arg("target", process.cwd()));
1509
+ if (process.argv[2] === "config") return runConfig();
1510
+ const saved = await loadConfig();
1511
+ const target = path6.resolve(arg("target", process.cwd()));
1224
1512
  const devUrl = arg("dev-url");
1225
1513
  const panelPort = port("port", 7100);
1226
1514
  const proxyPort = port("proxy-port", 7101);
1227
1515
  const token = arg("token", process.env.MOTION_PLUS_TOKEN);
1516
+ const baseUrl = arg("base-url", process.env.MOTION_CART_BASE_URL ?? saved.baseUrl);
1517
+ const apiKey = arg("api-key", process.env.MOTION_CART_API_KEY ?? saved.apiKey);
1518
+ const model = arg("model", process.env.MOTION_CART_MODEL ?? saved.model);
1228
1519
  if (!devUrl) {
1229
1520
  console.error(
1230
1521
  "Usage: motion-cart --dev-url <url> [--target <dir>] [--port 7100] [--proxy-port 7101] [--token <motion+ token>]\n --dev-url URL of your already-running LOCAL dev server (e.g. http://localhost:5173)\n --target project directory the agent will edit (default: cwd)"
@@ -1247,11 +1538,16 @@ async function main() {
1247
1538
  const framework = await detectFramework(target);
1248
1539
  const proxyUrl = `http://localhost:${proxyPort}`;
1249
1540
  startProxy(devUrl, proxyPort, `http://localhost:${panelPort}`);
1250
- const { url } = startControlServer({ targetDir: target, devUrl, proxyUrl, framework, token }, panelPort);
1541
+ const { url } = startControlServer(
1542
+ { targetDir: target, devUrl, proxyUrl, framework, token, baseUrl, apiKey, model },
1543
+ panelPort
1544
+ );
1545
+ const provider = baseUrl ? `${new URL(baseUrl).host}${model ? ` (${model})` : ""}` : apiKey ? "custom api key" : "Claude Code login";
1251
1546
  console.log(`
1252
1547
  motion-cart`);
1253
1548
  console.log(` target ${target} (${framework})`);
1254
1549
  console.log(` dev-url ${devUrl} -> proxied at ${proxyUrl}`);
1550
+ console.log(` provider ${provider}`);
1255
1551
  console.log(` MCP ${token ? "Motion AI Kit enabled" : "bundled guidance (set --token to enable)"}`);
1256
1552
  console.log(`
1257
1553
  Open the control panel: ${url}