@mevdragon/vidfarm-devcli 0.13.0 → 0.15.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 (427) hide show
  1. package/.agents/skills/embedded-captions/CATALOG.md +93 -0
  2. package/.agents/skills/embedded-captions/SKILL.md +286 -0
  3. package/.agents/skills/embedded-captions/assets/brand/CDPR-fankit-terms.txt +35 -0
  4. package/.agents/skills/embedded-captions/assets/brand/cyberpunk-widths.json +115 -0
  5. package/.agents/skills/embedded-captions/assets/fonts/char-widths.json +2211 -0
  6. package/.agents/skills/embedded-captions/assets/strokefonts/HersheyScript1.svg +242 -0
  7. package/.agents/skills/embedded-captions/assets/strokefonts/HersheyScriptMed.svg +242 -0
  8. package/.agents/skills/embedded-captions/dna/README.md +148 -0
  9. package/.agents/skills/embedded-captions/dna/chrome.json +68 -0
  10. package/.agents/skills/embedded-captions/dna/cream.json +69 -0
  11. package/.agents/skills/embedded-captions/dna/documentary.json +62 -0
  12. package/.agents/skills/embedded-captions/dna/editorial.json +70 -0
  13. package/.agents/skills/embedded-captions/dna/glitch.json +82 -0
  14. package/.agents/skills/embedded-captions/dna/ink.json +65 -0
  15. package/.agents/skills/embedded-captions/dna/keynote.json +62 -0
  16. package/.agents/skills/embedded-captions/dna/loud.json +75 -0
  17. package/.agents/skills/embedded-captions/dna/neon.json +58 -0
  18. package/.agents/skills/embedded-captions/dna/velocity.json +89 -0
  19. package/.agents/skills/embedded-captions/modes/cinematic/README.md +48 -0
  20. package/.agents/skills/embedded-captions/modes/cinematic/_archive/champion/spec.md +98 -0
  21. package/.agents/skills/embedded-captions/modes/cinematic/_archive/champion/template.html +188 -0
  22. package/.agents/skills/embedded-captions/modes/cinematic/_archive/memory-wall/spec.md +134 -0
  23. package/.agents/skills/embedded-captions/modes/cinematic/_archive/memory-wall/template.html +179 -0
  24. package/.agents/skills/embedded-captions/modes/cinematic/_archive/portrait-header/spec.md +81 -0
  25. package/.agents/skills/embedded-captions/modes/cinematic/_archive/portrait-header/template.html +170 -0
  26. package/.agents/skills/embedded-captions/modes/cinematic/cinematic-cream/spec.md +16 -0
  27. package/.agents/skills/embedded-captions/modes/cinematic/cinematic-cream/template.html +226 -0
  28. package/.agents/skills/embedded-captions/modes/cinematic/engine.html +355 -0
  29. package/.agents/skills/embedded-captions/modes/standard/_anatomy.md +233 -0
  30. package/.agents/skills/embedded-captions/modes/standard/_motion.md +192 -0
  31. package/.agents/skills/embedded-captions/modes/standard/fonts/build-fonts-css.cjs +97 -0
  32. package/.agents/skills/embedded-captions/references/aesthetic-principles.md +168 -0
  33. package/.agents/skills/embedded-captions/references/anti-patterns.md +188 -0
  34. package/.agents/skills/embedded-captions/references/bespoke-vs-presets.md +178 -0
  35. package/.agents/skills/embedded-captions/references/caption-grouping.md +130 -0
  36. package/.agents/skills/embedded-captions/references/composition-craft.md +522 -0
  37. package/.agents/skills/embedded-captions/references/direction-catalog.md +138 -0
  38. package/.agents/skills/embedded-captions/references/example-renders/champion.html +371 -0
  39. package/.agents/skills/embedded-captions/references/example-renders/memory-wall.html +334 -0
  40. package/.agents/skills/embedded-captions/references/failure-modes.md +145 -0
  41. package/.agents/skills/embedded-captions/references/layout-heuristics.md +238 -0
  42. package/.agents/skills/embedded-captions/references/motion-vocabulary.md +148 -0
  43. package/.agents/skills/embedded-captions/references/rail.md +68 -0
  44. package/.agents/skills/embedded-captions/references/reference-bar.md +47 -0
  45. package/.agents/skills/embedded-captions/references/scene-types.md +94 -0
  46. package/.agents/skills/embedded-captions/references/test-set.md +66 -0
  47. package/.agents/skills/embedded-captions/references/typographic-moves.md +266 -0
  48. package/.agents/skills/embedded-captions/references/typography-presets.md +63 -0
  49. package/.agents/skills/embedded-captions/scripts/audio-envelope.cjs +95 -0
  50. package/.agents/skills/embedded-captions/scripts/check-occlusion.cjs +250 -0
  51. package/.agents/skills/embedded-captions/scripts/check-overflow.cjs +194 -0
  52. package/.agents/skills/embedded-captions/scripts/check-rail-climax.cjs +233 -0
  53. package/.agents/skills/embedded-captions/scripts/check-timing.cjs +173 -0
  54. package/.agents/skills/embedded-captions/scripts/fill-timings.cjs +124 -0
  55. package/.agents/skills/embedded-captions/scripts/fit-fonts.cjs +158 -0
  56. package/.agents/skills/embedded-captions/scripts/gen-stroke-path.py +46 -0
  57. package/.agents/skills/embedded-captions/scripts/inject-fonts.cjs +169 -0
  58. package/.agents/skills/embedded-captions/scripts/lib-dna.cjs +201 -0
  59. package/.agents/skills/embedded-captions/scripts/make-cinematic.cjs +1205 -0
  60. package/.agents/skills/embedded-captions/scripts/make-composition.cjs +404 -0
  61. package/.agents/skills/embedded-captions/scripts/make-theme.cjs +8781 -0
  62. package/.agents/skills/embedded-captions/scripts/matte.cjs +294 -0
  63. package/.agents/skills/embedded-captions/scripts/measure-layout.cjs +293 -0
  64. package/.agents/skills/embedded-captions/scripts/prepare.sh +39 -0
  65. package/.agents/skills/embedded-captions/scripts/preview-frames.cjs +268 -0
  66. package/.agents/skills/embedded-captions/scripts/render-and-composite.sh +509 -0
  67. package/.agents/skills/embedded-captions/scripts/render-theme.sh +34 -0
  68. package/.agents/skills/embedded-captions/scripts/safe-zones.cjs +813 -0
  69. package/.agents/skills/embedded-captions/scripts/transcribe.cjs +349 -0
  70. package/.agents/skills/embedded-captions/themes/PORTING.md +49 -0
  71. package/.agents/skills/embedded-captions/themes/README.md +244 -0
  72. package/.agents/skills/embedded-captions/themes/anchor.json +27 -0
  73. package/.agents/skills/embedded-captions/themes/arcade.json +51 -0
  74. package/.agents/skills/embedded-captions/themes/aurora.json +41 -0
  75. package/.agents/skills/embedded-captions/themes/biolume.json +35 -0
  76. package/.agents/skills/embedded-captions/themes/brush.json +74 -0
  77. package/.agents/skills/embedded-captions/themes/chalkboard.json +52 -0
  78. package/.agents/skills/embedded-captions/themes/dossier.json +56 -0
  79. package/.agents/skills/embedded-captions/themes/graffiti.json +62 -0
  80. package/.agents/skills/embedded-captions/themes/hologram.json +41 -0
  81. package/.agents/skills/embedded-captions/themes/inkwater.json +43 -0
  82. package/.agents/skills/embedded-captions/themes/laser.json +46 -0
  83. package/.agents/skills/embedded-captions/themes/lastpage.json +33 -0
  84. package/.agents/skills/embedded-captions/themes/neonsign.json +53 -0
  85. package/.agents/skills/embedded-captions/themes/nightcity.json +56 -0
  86. package/.agents/skills/embedded-captions/themes/ordnance.json +54 -0
  87. package/.agents/skills/embedded-captions/themes/papercut.json +37 -0
  88. package/.agents/skills/embedded-captions/themes/popup.json +46 -0
  89. package/.agents/skills/embedded-captions/themes/ransom.json +62 -0
  90. package/.agents/skills/embedded-captions/themes/scoreboard.json +54 -0
  91. package/.agents/skills/embedded-captions/themes/spectrum.json +46 -0
  92. package/.agents/skills/embedded-captions/themes/stardust.json +29 -0
  93. package/.agents/skills/embedded-captions/themes/stomp.json +43 -0
  94. package/.agents/skills/embedded-captions/themes/terminal.json +49 -0
  95. package/.agents/skills/embedded-captions/themes/thunder.json +44 -0
  96. package/.agents/skills/embedded-captions/themes/transit.json +44 -0
  97. package/.agents/skills/embedded-captions/themes/vhs.json +47 -0
  98. package/.agents/skills/faceless-explainer/SKILL.md +209 -0
  99. package/.agents/skills/faceless-explainer/references/cut-catalog.md +215 -0
  100. package/.agents/skills/faceless-explainer/references/motion-language.md +156 -0
  101. package/.agents/skills/faceless-explainer/references/story-design.md +248 -0
  102. package/.agents/skills/faceless-explainer/references/visual-design.md +146 -0
  103. package/.agents/skills/faceless-explainer/scripts/assemble-index.mjs +570 -0
  104. package/.agents/skills/faceless-explainer/scripts/audio.mjs +260 -0
  105. package/.agents/skills/faceless-explainer/scripts/build-frame.mjs +536 -0
  106. package/.agents/skills/faceless-explainer/scripts/captions.mjs +508 -0
  107. package/.agents/skills/faceless-explainer/scripts/lib/assets.mjs +55 -0
  108. package/.agents/skills/faceless-explainer/scripts/lib/dimensions.mjs +45 -0
  109. package/.agents/skills/faceless-explainer/scripts/lib/storyboard.mjs +249 -0
  110. package/.agents/skills/faceless-explainer/scripts/lib/tokens.mjs +204 -0
  111. package/.agents/skills/faceless-explainer/scripts/lib/transition-registry.mjs +38 -0
  112. package/.agents/skills/faceless-explainer/scripts/lib/transitions.json +71 -0
  113. package/.agents/skills/faceless-explainer/scripts/transitions.mjs +320 -0
  114. package/.agents/skills/faceless-explainer/sub-agents/frame-worker.md +75 -0
  115. package/.agents/skills/flockposter/LICENSE +17 -0
  116. package/.agents/skills/flockposter/README.md +72 -0
  117. package/.agents/skills/flockposter/SKILL.md +562 -0
  118. package/.agents/skills/flockposter/examples/cli-instagram-reel.sh +21 -0
  119. package/.agents/skills/flockposter/examples/cli-multi-platform.json +69 -0
  120. package/.agents/skills/flockposter/examples/cli-tiktok-upload-only.sh +22 -0
  121. package/.agents/skills/flockposter/examples/cli-x-thread.sh +17 -0
  122. package/.agents/skills/flockposter/examples/cli-youtube-short.sh +21 -0
  123. package/.agents/skills/flockposter/examples/public-api-instagram-reel.json +28 -0
  124. package/.agents/skills/general-video/SKILL.md +143 -0
  125. package/.agents/skills/hyperframes/SKILL.md +160 -0
  126. package/.agents/skills/hyperframes-animation/SKILL.md +84 -0
  127. package/.agents/skills/hyperframes-animation/adapters/animate-text.md +64 -0
  128. package/.agents/skills/hyperframes-animation/adapters/animejs.md +114 -0
  129. package/.agents/skills/hyperframes-animation/adapters/css-animations.md +143 -0
  130. package/.agents/skills/hyperframes-animation/adapters/gsap-easing-and-stagger.md +118 -0
  131. package/.agents/skills/hyperframes-animation/adapters/gsap-timeline-and-labels.md +96 -0
  132. package/.agents/skills/hyperframes-animation/adapters/gsap-transforms-and-perf.md +96 -0
  133. package/.agents/skills/hyperframes-animation/adapters/gsap.md +105 -0
  134. package/.agents/skills/hyperframes-animation/adapters/html-in-canvas-patterns.md +507 -0
  135. package/.agents/skills/hyperframes-animation/adapters/lottie.md +117 -0
  136. package/.agents/skills/hyperframes-animation/adapters/three.md +131 -0
  137. package/.agents/skills/hyperframes-animation/adapters/typegpu.md +178 -0
  138. package/.agents/skills/hyperframes-animation/adapters/waapi.md +101 -0
  139. package/.agents/skills/hyperframes-animation/blueprints/comparison-split.md +27 -0
  140. package/.agents/skills/hyperframes-animation/blueprints/constellation-hub.md +44 -0
  141. package/.agents/skills/hyperframes-animation/blueprints/cta-morph-press.md +28 -0
  142. package/.agents/skills/hyperframes-animation/blueprints/cursor-ui-demo.md +57 -0
  143. package/.agents/skills/hyperframes-animation/blueprints/dataviz-countup.md +46 -0
  144. package/.agents/skills/hyperframes-animation/blueprints/device-surface-showcase.md +53 -0
  145. package/.agents/skills/hyperframes-animation/blueprints/grid-card-assemble.md +66 -0
  146. package/.agents/skills/hyperframes-animation/blueprints/kinetic-type-beats.md +78 -0
  147. package/.agents/skills/hyperframes-animation/blueprints/logo-assemble-lockup.md +69 -0
  148. package/.agents/skills/hyperframes-animation/blueprints/overwhelm-surround.md +28 -0
  149. package/.agents/skills/hyperframes-animation/blueprints/spatial-pan-stations.md +37 -0
  150. package/.agents/skills/hyperframes-animation/blueprints/ticker-takeover.md +29 -0
  151. package/.agents/skills/hyperframes-animation/blueprints/titlecard-reveal.md +42 -0
  152. package/.agents/skills/hyperframes-animation/blueprints/typewriter-reveal.md +51 -0
  153. package/.agents/skills/hyperframes-animation/blueprints/video-text-pivot.md +30 -0
  154. package/.agents/skills/hyperframes-animation/blueprints-index.md +146 -0
  155. package/.agents/skills/hyperframes-animation/examples/assets/avatars/02.avif +0 -0
  156. package/.agents/skills/hyperframes-animation/examples/assets/brands/github.avif +0 -0
  157. package/.agents/skills/hyperframes-animation/examples/assets/brands/nvidia.avif +0 -0
  158. package/.agents/skills/hyperframes-animation/examples/assets/brands/visa.avif +0 -0
  159. package/.agents/skills/hyperframes-animation/examples/assets/brands/zoominfo.avif +0 -0
  160. package/.agents/skills/hyperframes-animation/examples/brand-reveal-assemble-zoom.html +382 -0
  161. package/.agents/skills/hyperframes-animation/examples/comparison-split-cards.html +649 -0
  162. package/.agents/skills/hyperframes-animation/examples/concept-demo-decode-pan.html +520 -0
  163. package/.agents/skills/hyperframes-animation/examples/cta-morph-press.html +468 -0
  164. package/.agents/skills/hyperframes-animation/examples/cta-orbit-collapse.html +1298 -0
  165. package/.agents/skills/hyperframes-animation/examples/demo-page-scroll-spotlight.html +759 -0
  166. package/.agents/skills/hyperframes-animation/examples/hook-counter-burst.html +729 -0
  167. package/.agents/skills/hyperframes-animation/examples/messaging-multi-phrase.html +352 -0
  168. package/.agents/skills/hyperframes-animation/examples/metric-video-text-pivot.html +779 -0
  169. package/.agents/skills/hyperframes-animation/examples/problem-mockup-overwhelm.html +1374 -0
  170. package/.agents/skills/hyperframes-animation/examples/proof-logo-chain.html +861 -0
  171. package/.agents/skills/hyperframes-animation/examples/takeover-ticker-displace.html +347 -0
  172. package/.agents/skills/hyperframes-animation/examples/workflow-approve-press.html +606 -0
  173. package/.agents/skills/hyperframes-animation/rules/3d-page-scroll.md +227 -0
  174. package/.agents/skills/hyperframes-animation/rules/3d-text-depth-layers.md +297 -0
  175. package/.agents/skills/hyperframes-animation/rules/ai-tracking-box.md +382 -0
  176. package/.agents/skills/hyperframes-animation/rules/ambient-glow-bloom.md +305 -0
  177. package/.agents/skills/hyperframes-animation/rules/asr-keyword-glow.md +286 -0
  178. package/.agents/skills/hyperframes-animation/rules/avatar-cloud-network.md +371 -0
  179. package/.agents/skills/hyperframes-animation/rules/camera-cursor-tracking.md +246 -0
  180. package/.agents/skills/hyperframes-animation/rules/card-morph-anchor.md +267 -0
  181. package/.agents/skills/hyperframes-animation/rules/center-outward-expansion.md +227 -0
  182. package/.agents/skills/hyperframes-animation/rules/context-sensitive-cursor.md +257 -0
  183. package/.agents/skills/hyperframes-animation/rules/coordinate-target-zoom.md +332 -0
  184. package/.agents/skills/hyperframes-animation/rules/counting-dynamic-scale.md +283 -0
  185. package/.agents/skills/hyperframes-animation/rules/css-marker-patterns.md +373 -0
  186. package/.agents/skills/hyperframes-animation/rules/cursor-click-ripple.md +262 -0
  187. package/.agents/skills/hyperframes-animation/rules/depth-of-field-blur.md +313 -0
  188. package/.agents/skills/hyperframes-animation/rules/depth-scatter-assemble.md +303 -0
  189. package/.agents/skills/hyperframes-animation/rules/discrete-text-sequence.md +273 -0
  190. package/.agents/skills/hyperframes-animation/rules/dynamic-content-sequencing.md +307 -0
  191. package/.agents/skills/hyperframes-animation/rules/gsap-effects.md +299 -0
  192. package/.agents/skills/hyperframes-animation/rules/hacker-flip-3d.md +223 -0
  193. package/.agents/skills/hyperframes-animation/rules/kinetic-beat-slam.md +183 -0
  194. package/.agents/skills/hyperframes-animation/rules/motion-blur-streak.md +328 -0
  195. package/.agents/skills/hyperframes-animation/rules/multi-phase-camera.md +273 -0
  196. package/.agents/skills/hyperframes-animation/rules/orbit-3d-entry.md +301 -0
  197. package/.agents/skills/hyperframes-animation/rules/physics-press-reaction.md +350 -0
  198. package/.agents/skills/hyperframes-animation/rules/press-release-spring.md +296 -0
  199. package/.agents/skills/hyperframes-animation/rules/reactive-displacement.md +277 -0
  200. package/.agents/skills/hyperframes-animation/rules/scale-swap-transition.md +298 -0
  201. package/.agents/skills/hyperframes-animation/rules/sine-wave-loop.md +278 -0
  202. package/.agents/skills/hyperframes-animation/rules/split-tilt-cards.md +277 -0
  203. package/.agents/skills/hyperframes-animation/rules/spring-pop-entrance.md +273 -0
  204. package/.agents/skills/hyperframes-animation/rules/stat-bars-and-fills.md +156 -0
  205. package/.agents/skills/hyperframes-animation/rules/svg-icon-enrichment.md +329 -0
  206. package/.agents/skills/hyperframes-animation/rules/svg-path-draw.md +274 -0
  207. package/.agents/skills/hyperframes-animation/rules/vertical-spring-ticker.md +239 -0
  208. package/.agents/skills/hyperframes-animation/rules/viewport-change.md +349 -0
  209. package/.agents/skills/hyperframes-animation/rules-index.md +86 -0
  210. package/.agents/skills/hyperframes-animation/scripts/animation-map.mjs +606 -0
  211. package/.agents/skills/hyperframes-animation/scripts/package-loader.mjs +288 -0
  212. package/.agents/skills/hyperframes-animation/scripts/package-loader.test.mjs +62 -0
  213. package/.agents/skills/hyperframes-animation/techniques.md +507 -0
  214. package/.agents/skills/hyperframes-animation/transitions/TRANSITION-REGISTRY.md +167 -0
  215. package/.agents/skills/hyperframes-animation/transitions/catalog.md +127 -0
  216. package/.agents/skills/hyperframes-animation/transitions/css-3d.md +12 -0
  217. package/.agents/skills/hyperframes-animation/transitions/css-blur.md +51 -0
  218. package/.agents/skills/hyperframes-animation/transitions/css-cover.md +43 -0
  219. package/.agents/skills/hyperframes-animation/transitions/css-destruction.md +95 -0
  220. package/.agents/skills/hyperframes-animation/transitions/css-dissolve.md +66 -0
  221. package/.agents/skills/hyperframes-animation/transitions/css-distortion.md +45 -0
  222. package/.agents/skills/hyperframes-animation/transitions/css-grid.md +10 -0
  223. package/.agents/skills/hyperframes-animation/transitions/css-light.md +49 -0
  224. package/.agents/skills/hyperframes-animation/transitions/css-mechanical.md +30 -0
  225. package/.agents/skills/hyperframes-animation/transitions/css-other.md +25 -0
  226. package/.agents/skills/hyperframes-animation/transitions/css-push.md +41 -0
  227. package/.agents/skills/hyperframes-animation/transitions/css-radial.md +37 -0
  228. package/.agents/skills/hyperframes-animation/transitions/css-scale.md +24 -0
  229. package/.agents/skills/hyperframes-animation/transitions/overview.md +153 -0
  230. package/.agents/skills/hyperframes-cli/SKILL.md +113 -0
  231. package/.agents/skills/hyperframes-cli/references/doctor-browser.md +45 -0
  232. package/.agents/skills/hyperframes-cli/references/init-and-scaffold.md +55 -0
  233. package/.agents/skills/hyperframes-cli/references/lambda.md +132 -0
  234. package/.agents/skills/hyperframes-cli/references/lint-validate-inspect.md +121 -0
  235. package/.agents/skills/hyperframes-cli/references/preview-render.md +149 -0
  236. package/.agents/skills/hyperframes-cli/references/upgrade-info-misc.md +75 -0
  237. package/.agents/skills/hyperframes-core/SKILL.md +78 -0
  238. package/.agents/skills/hyperframes-core/references/composition-patterns.md +263 -0
  239. package/.agents/skills/hyperframes-core/references/data-attributes.md +70 -0
  240. package/.agents/skills/hyperframes-core/references/determinism-rules.md +68 -0
  241. package/.agents/skills/hyperframes-core/references/full-screen-motion.md +62 -0
  242. package/.agents/skills/hyperframes-core/references/minimal-composition.md +66 -0
  243. package/.agents/skills/hyperframes-core/references/script-format.md +49 -0
  244. package/.agents/skills/hyperframes-core/references/storyboard-format.md +95 -0
  245. package/.agents/skills/hyperframes-core/references/sub-compositions.md +237 -0
  246. package/.agents/skills/hyperframes-core/references/subagent-dispatch.md +41 -0
  247. package/.agents/skills/hyperframes-core/references/tailwind.md +125 -0
  248. package/.agents/skills/hyperframes-core/references/tracks-and-clips.md +76 -0
  249. package/.agents/skills/hyperframes-core/references/variables-and-media.md +90 -0
  250. package/.agents/skills/hyperframes-creative/SKILL.md +70 -0
  251. package/.agents/skills/hyperframes-creative/frame-presets/biennale-yellow/FRAME.md +289 -0
  252. package/.agents/skills/hyperframes-creative/frame-presets/biennale-yellow/caption-skin.html +221 -0
  253. package/.agents/skills/hyperframes-creative/frame-presets/biennale-yellow/frame-showcase.html +1214 -0
  254. package/.agents/skills/hyperframes-creative/frame-presets/blockframe/FRAME.md +275 -0
  255. package/.agents/skills/hyperframes-creative/frame-presets/blockframe/caption-skin.html +224 -0
  256. package/.agents/skills/hyperframes-creative/frame-presets/blockframe/frame-showcase.html +1347 -0
  257. package/.agents/skills/hyperframes-creative/frame-presets/blue-professional/FRAME.md +307 -0
  258. package/.agents/skills/hyperframes-creative/frame-presets/blue-professional/caption-skin.html +223 -0
  259. package/.agents/skills/hyperframes-creative/frame-presets/blue-professional/frame-showcase.html +1372 -0
  260. package/.agents/skills/hyperframes-creative/frame-presets/bold-poster/FRAME.md +275 -0
  261. package/.agents/skills/hyperframes-creative/frame-presets/bold-poster/caption-skin.html +230 -0
  262. package/.agents/skills/hyperframes-creative/frame-presets/bold-poster/frame-showcase.html +1140 -0
  263. package/.agents/skills/hyperframes-creative/frame-presets/broadside/FRAME.md +284 -0
  264. package/.agents/skills/hyperframes-creative/frame-presets/broadside/caption-skin.html +225 -0
  265. package/.agents/skills/hyperframes-creative/frame-presets/broadside/frame-showcase.html +1166 -0
  266. package/.agents/skills/hyperframes-creative/frame-presets/capsule/FRAME.md +284 -0
  267. package/.agents/skills/hyperframes-creative/frame-presets/capsule/caption-skin.html +224 -0
  268. package/.agents/skills/hyperframes-creative/frame-presets/capsule/frame-showcase.html +1465 -0
  269. package/.agents/skills/hyperframes-creative/frame-presets/cartesian/FRAME.md +278 -0
  270. package/.agents/skills/hyperframes-creative/frame-presets/cartesian/caption-skin.html +222 -0
  271. package/.agents/skills/hyperframes-creative/frame-presets/cartesian/frame-showcase.html +1263 -0
  272. package/.agents/skills/hyperframes-creative/frame-presets/claude/FRAME.md +277 -0
  273. package/.agents/skills/hyperframes-creative/frame-presets/claude/caption-skin.html +224 -0
  274. package/.agents/skills/hyperframes-creative/frame-presets/claude/frame-showcase.html +1439 -0
  275. package/.agents/skills/hyperframes-creative/frame-presets/cobalt-grid/FRAME.md +265 -0
  276. package/.agents/skills/hyperframes-creative/frame-presets/cobalt-grid/caption-skin.html +238 -0
  277. package/.agents/skills/hyperframes-creative/frame-presets/cobalt-grid/frame-showcase.html +1468 -0
  278. package/.agents/skills/hyperframes-creative/frame-presets/coral/FRAME.md +314 -0
  279. package/.agents/skills/hyperframes-creative/frame-presets/coral/caption-skin.html +226 -0
  280. package/.agents/skills/hyperframes-creative/frame-presets/coral/frame-showcase.html +1268 -0
  281. package/.agents/skills/hyperframes-creative/frame-presets/creative-mode/FRAME.md +333 -0
  282. package/.agents/skills/hyperframes-creative/frame-presets/creative-mode/caption-skin.html +227 -0
  283. package/.agents/skills/hyperframes-creative/frame-presets/creative-mode/frame-showcase.html +1330 -0
  284. package/.agents/skills/hyperframes-creative/frame-presets/daisy-days/FRAME.md +274 -0
  285. package/.agents/skills/hyperframes-creative/frame-presets/daisy-days/caption-skin.html +230 -0
  286. package/.agents/skills/hyperframes-creative/frame-presets/daisy-days/frame-showcase.html +1444 -0
  287. package/.agents/skills/hyperframes-creative/frame-presets/editorial-forest/FRAME.md +265 -0
  288. package/.agents/skills/hyperframes-creative/frame-presets/editorial-forest/caption-skin.html +223 -0
  289. package/.agents/skills/hyperframes-creative/frame-presets/editorial-forest/frame-showcase.html +1273 -0
  290. package/.agents/skills/hyperframes-creative/palettes/bold-energetic.md +14 -0
  291. package/.agents/skills/hyperframes-creative/palettes/clean-corporate.md +14 -0
  292. package/.agents/skills/hyperframes-creative/palettes/dark-premium.md +14 -0
  293. package/.agents/skills/hyperframes-creative/palettes/jewel-rich.md +14 -0
  294. package/.agents/skills/hyperframes-creative/palettes/monochrome.md +14 -0
  295. package/.agents/skills/hyperframes-creative/palettes/nature-earth.md +14 -0
  296. package/.agents/skills/hyperframes-creative/palettes/neon-electric.md +14 -0
  297. package/.agents/skills/hyperframes-creative/palettes/pastel-soft.md +14 -0
  298. package/.agents/skills/hyperframes-creative/palettes/warm-editorial.md +14 -0
  299. package/.agents/skills/hyperframes-creative/references/audio-reactive.md +76 -0
  300. package/.agents/skills/hyperframes-creative/references/beat-direction.md +164 -0
  301. package/.agents/skills/hyperframes-creative/references/composition-patterns.md +199 -0
  302. package/.agents/skills/hyperframes-creative/references/data-in-motion.md +19 -0
  303. package/.agents/skills/hyperframes-creative/references/design-adherence.md +19 -0
  304. package/.agents/skills/hyperframes-creative/references/design-picker.md +123 -0
  305. package/.agents/skills/hyperframes-creative/references/design-spec.md +55 -0
  306. package/.agents/skills/hyperframes-creative/references/house-style.md +73 -0
  307. package/.agents/skills/hyperframes-creative/references/motion-principles.md +150 -0
  308. package/.agents/skills/hyperframes-creative/references/narration.md +92 -0
  309. package/.agents/skills/hyperframes-creative/references/prompt-expansion.md +68 -0
  310. package/.agents/skills/hyperframes-creative/references/typography.md +218 -0
  311. package/.agents/skills/hyperframes-creative/references/video-composition.md +63 -0
  312. package/.agents/skills/hyperframes-creative/references/visual-styles.md +457 -0
  313. package/.agents/skills/hyperframes-creative/scripts/contrast-report.mjs +353 -0
  314. package/.agents/skills/hyperframes-creative/scripts/extract-audio-data.py +188 -0
  315. package/.agents/skills/hyperframes-creative/scripts/package-loader.mjs +288 -0
  316. package/.agents/skills/hyperframes-creative/scripts/package-loader.test.mjs +62 -0
  317. package/.agents/skills/hyperframes-creative/templates/design-picker.html +1432 -0
  318. package/.agents/skills/hyperframes-keyframes/SKILL.md +237 -0
  319. package/.agents/skills/hyperframes-keyframes/agents/openai.yaml +4 -0
  320. package/.agents/skills/hyperframes-keyframes/references/keyframe-patterns.md +106 -0
  321. package/.agents/skills/motion-graphics/SKILL.md +172 -0
  322. package/.agents/skills/motion-graphics/agents/builder.md +40 -0
  323. package/.agents/skills/motion-graphics/agents/director.md +53 -0
  324. package/.agents/skills/motion-graphics/agents/finalize.md +17 -0
  325. package/.agents/skills/motion-graphics/catalog-map.md +40 -0
  326. package/.agents/skills/motion-graphics/categories/asset-fusion/module.md +37 -0
  327. package/.agents/skills/motion-graphics/categories/charts/module.md +21 -0
  328. package/.agents/skills/motion-graphics/categories/kinetic-type/module.md +19 -0
  329. package/.agents/skills/motion-graphics/categories/logo-reveal/module.md +17 -0
  330. package/.agents/skills/motion-graphics/categories/lower-thirds/module.md +16 -0
  331. package/.agents/skills/motion-graphics/categories/maps/bake-basemap.mjs +273 -0
  332. package/.agents/skills/motion-graphics/categories/maps/module.md +61 -0
  333. package/.agents/skills/motion-graphics/categories/news/module.md +56 -0
  334. package/.agents/skills/motion-graphics/categories/stat/module.md +21 -0
  335. package/.agents/skills/motion-graphics/categories/tweet/module.md +16 -0
  336. package/.agents/skills/motion-graphics/categories/webpage/module.md +21 -0
  337. package/.agents/skills/motion-graphics/grounding/PROTOCOL.md +64 -0
  338. package/.agents/skills/motion-graphics/grounding/locate.mjs +227 -0
  339. package/.agents/skills/motion-graphics/phases/source/guide.md +21 -0
  340. package/.agents/skills/motion-graphics/references/builder-contract.md +38 -0
  341. package/.agents/skills/motion-graphics/references/motion-vocabulary.md +43 -0
  342. package/.agents/skills/motion-graphics/references/shot-plan-ir.md +54 -0
  343. package/.agents/skills/motion-graphics/samples/asset-fusion/_ref-circle-highlight.html +306 -0
  344. package/.agents/skills/product-launch-video/SKILL.md +208 -0
  345. package/.agents/skills/product-launch-video/references/cut-catalog.md +220 -0
  346. package/.agents/skills/product-launch-video/references/motion-language.md +156 -0
  347. package/.agents/skills/product-launch-video/references/story-design.md +373 -0
  348. package/.agents/skills/product-launch-video/references/visual-design.md +126 -0
  349. package/.agents/skills/product-launch-video/scripts/assemble-index.mjs +570 -0
  350. package/.agents/skills/product-launch-video/scripts/audio.mjs +260 -0
  351. package/.agents/skills/product-launch-video/scripts/build-frame.mjs +536 -0
  352. package/.agents/skills/product-launch-video/scripts/captions.mjs +508 -0
  353. package/.agents/skills/product-launch-video/scripts/lib/assets.mjs +55 -0
  354. package/.agents/skills/product-launch-video/scripts/lib/dimensions.mjs +45 -0
  355. package/.agents/skills/product-launch-video/scripts/lib/pad-frame-duration.mjs +36 -0
  356. package/.agents/skills/product-launch-video/scripts/lib/pad-frame-duration.test.mjs +76 -0
  357. package/.agents/skills/product-launch-video/scripts/lib/storyboard.mjs +249 -0
  358. package/.agents/skills/product-launch-video/scripts/lib/tokens.mjs +204 -0
  359. package/.agents/skills/product-launch-video/scripts/lib/transition-registry.mjs +38 -0
  360. package/.agents/skills/product-launch-video/scripts/lib/transitions.json +71 -0
  361. package/.agents/skills/product-launch-video/scripts/stage-assets.mjs +39 -0
  362. package/.agents/skills/product-launch-video/scripts/transitions.mjs +327 -0
  363. package/.agents/skills/product-launch-video/sub-agents/frame-worker.md +75 -0
  364. package/.agents/skills/slideshow/SKILL.md +550 -0
  365. package/.agents/skills/slideshow/references/standalone-harness.md +1020 -0
  366. package/.agents/skills/talking-head-recut/NOTICE.md +39 -0
  367. package/.agents/skills/talking-head-recut/SKILL.md +1195 -0
  368. package/.agents/skills/talking-head-recut/assets/vendor/gsap.min.js +10 -0
  369. package/.agents/skills/talking-head-recut/references/DESIGN_INDEX.md +145 -0
  370. package/.agents/skills/talking-head-recut/references/frames/clean.html +92 -0
  371. package/.agents/skills/talking-head-recut/references/frames/hairline.html +171 -0
  372. package/.agents/skills/talking-head-recut/references/frames/polaroid.html +131 -0
  373. package/.agents/skills/talking-head-recut/references/layouts/overlay.html +164 -0
  374. package/.agents/skills/talking-head-recut/references/layouts/pip.html +143 -0
  375. package/.agents/skills/talking-head-recut/references/layouts/split.html +121 -0
  376. package/.agents/skills/talking-head-recut/references/layouts/stack.html +109 -0
  377. package/.agents/skills/talking-head-recut/references/styles/academic.html +172 -0
  378. package/.agents/skills/talking-head-recut/references/styles/audit.html +170 -0
  379. package/.agents/skills/talking-head-recut/references/styles/editorial.html +189 -0
  380. package/.agents/skills/talking-head-recut/references/styles/geom.html +181 -0
  381. package/.agents/skills/talking-head-recut/references/styles/minimal.html +130 -0
  382. package/.agents/skills/talking-head-recut/references/styles/spotlight.html +142 -0
  383. package/.agents/skills/talking-head-recut/references/styles/swiss.html +188 -0
  384. package/.agents/skills/talking-head-recut/references/styles/terminal.html +196 -0
  385. package/.agents/skills/talking-head-recut/references/styles/whiteboard.html +170 -0
  386. package/.agents/skills/talking-head-recut/references/styles/xhs.html +206 -0
  387. package/.agents/skills/vidfarm-media/SKILL.md +85 -0
  388. package/.agents/skills/vidfarm-media/references/tts.md +54 -0
  389. package/.agents/skills/vidfarm-media/scripts/audio.mjs +628 -0
  390. package/.agents/skills/vidfarm-media/scripts/wait-bgm.mjs +31 -0
  391. package/.agents/skills/website-to-video/SKILL.md +145 -0
  392. package/.agents/skills/website-to-video/assets/sfx/CREDITS.md +35 -0
  393. package/.agents/skills/website-to-video/assets/sfx/manifest.json +97 -0
  394. package/.agents/skills/website-to-video/references/beat-builder-guide.md +263 -0
  395. package/.agents/skills/website-to-video/references/capabilities.md +713 -0
  396. package/.agents/skills/website-to-video/references/step-0-capture.md +55 -0
  397. package/.agents/skills/website-to-video/references/step-1-design.md +333 -0
  398. package/.agents/skills/website-to-video/references/step-2-brief.md +178 -0
  399. package/.agents/skills/website-to-video/references/step-3-storyboard.md +572 -0
  400. package/.agents/skills/website-to-video/references/step-4-vo.md +225 -0
  401. package/.agents/skills/website-to-video/references/step-5-build.md +483 -0
  402. package/.agents/skills/website-to-video/references/step-6-validate.md +341 -0
  403. package/.agents/skills/website-to-video/scripts/w2h-verify.mjs +759 -0
  404. package/SKILL.director.md +60 -4
  405. package/SKILL.platform.md +7 -2
  406. package/demo/dist/app.css +1 -1
  407. package/demo/dist/app.js +485 -86
  408. package/dist/src/account-pages-legacy.js +9 -14
  409. package/dist/src/app.js +659 -159
  410. package/dist/src/cli.js +618 -17
  411. package/dist/src/composition-runtime.js +286 -6
  412. package/dist/src/devcli/composition-edit.js +176 -8
  413. package/dist/src/devcli/doctor.js +192 -0
  414. package/dist/src/devcli/hyperframes-cli.js +118 -0
  415. package/dist/src/devcli/skills.js +476 -0
  416. package/dist/src/devcli/stills.js +249 -0
  417. package/dist/src/devcli/transitions.js +205 -0
  418. package/dist/src/editor-chat.js +260 -4
  419. package/dist/src/frontend/template-editor-chat.js +6 -5
  420. package/dist/src/homepage.js +2 -48
  421. package/dist/src/hyperframes/composition.js +188 -7
  422. package/dist/src/services/composition-lint.js +199 -0
  423. package/dist/src/services/hyperframes.js +127 -28
  424. package/dist/src/services/serverless-records.js +7 -0
  425. package/dist/src/template-editor-shell.js +87 -48
  426. package/package.json +2 -1
  427. package/public/assets/page-runtime-client-app.js +31 -31
package/dist/src/app.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import { createHash, randomBytes, randomUUID } from "node:crypto";
2
- import { createWriteStream, existsSync, mkdirSync, readFileSync, rmSync, statSync } from "node:fs";
2
+ import { createWriteStream, existsSync, mkdirSync, readFileSync, readdirSync, rmSync, statSync } from "node:fs";
3
3
  import path from "node:path";
4
4
  import { Readable } from "node:stream";
5
5
  import { pipeline as streamPipeline } from "node:stream/promises";
@@ -23,7 +23,7 @@ import { writeForkManifest } from "./services/fork-manifest.js";
23
23
  import { renderHelpPage } from "./help-page.js";
24
24
  import { renderHomepage } from "./homepage.js";
25
25
  import { COMPOSITION_RUNTIME_SCRIPT_BODY } from "./composition-runtime.js";
26
- import { CAPTION_ANIM_CSS, CAPTION_STYLE_MARKER, KEN_BURNS_CSS, KEN_BURNS_STYLE_MARKER, TRANSITION_CSS, TRANSITION_STYLE_MARKER } from "./hyperframes/composition.js";
26
+ import { CAPTION_ANIM_CSS, CAPTION_STYLE_MARKER, KEN_BURNS_CSS, KEN_BURNS_STYLE_MARKER, TRANSITION_CSS, TRANSITION_STYLE_MARKER, upgradeLegacyTransitionCss } from "./hyperframes/composition.js";
27
27
  import { normalizeCompositionZIndexHtml, sanitizeCompositionHtml } from "./services/composition-sanitize.js";
28
28
  import { applyMarkupUsd } from "./services/billing-pricing.js";
29
29
  import { primitiveRegistry } from "./primitive-registry.js";
@@ -35,7 +35,7 @@ import { resolveRootFileCandidates } from "./lib/package-root.js";
35
35
  import { stripTrackingParams } from "./lib/url-clean.js";
36
36
  import { addSeconds, nowIso } from "./lib/time.js";
37
37
  import { templateRegistry } from "./registry.js";
38
- import { buildEditorChatSystemInstruction, compactEditorChatModelMessages, defaultEditorChatModel, EDITOR_CHAT_PROVIDER_PRIORITY, extractEditorChatCompositionBrief, formatEditorChatCurrentDateTime, toEditorChatModelMessages } from "./editor-chat.js";
38
+ import { buildAsyncJobHandoffText, buildEditorChatGuardrailNotice, buildEditorChatSystemInstruction, collectVerifiedHttpJobIds, compactEditorChatModelMessages, createLoadSkillTool, createStreamingJobIdRedactor, defaultEditorChatModel, EDITOR_CHAT_PROVIDER_PRIORITY, extractEditorChatCompositionBrief, formatEditorChatCurrentDateTime, lintReplaceCompositionAction, readAsyncJobHandoff, toEditorChatModelMessages } from "./editor-chat.js";
39
39
  import { apiCallHistoryStore } from "./services/api-call-history.js";
40
40
  import { AuthService } from "./services/auth.js";
41
41
  import { BillingService } from "./services/billing.js";
@@ -54,7 +54,7 @@ import { loadSpeechAudioLocally, MAX_LOCAL_TRANSCRIBE_AUDIO_BYTES } from "./devc
54
54
  import { DescribeExecutionCommand, SFNClient, StartExecutionCommand } from "@aws-sdk/client-sfn";
55
55
  import { clipRecords, makeUserPreset } from "./services/clip-records.js";
56
56
  import { matchScenesToClips, searchUserClips, vectorIndex } from "./services/clip-search.js";
57
- import { BUILTIN_PRESETS, buildEffectiveGuidance, ClipModelClient, detectLocalAgent, effectiveHuntDurationSec, estimateScanCostFromDuration, hasFfmpeg, LocalAgentClipClient, normalizeCropFocus, normalizeHuntSpec, parseClipHuntPrompt, probeVideo, resolveDurationBand, scanVideo } from "./services/clip-curation/index.js";
57
+ import { BUILTIN_PRESETS, buildClipEmbeddingText, buildEffectiveGuidance, ClipModelClient, cosineSimilarity, detectLocalAgent, effectiveHuntDurationSec, estimateScanCostFromDuration, hasFfmpeg, LocalAgentClipClient, normalizeCropFocus, normalizeHuntSpec, parseClipHuntPrompt, probeVideo, resolveDurationBand, scanVideo } from "./services/clip-curation/index.js";
58
58
  import { analyzeCastMembers, annotateScenesFromSource, buildSmartDecomposedHtml, HyperframesService, smartDecomposeVideo, stampCompositionIdentity } from "./services/hyperframes.js";
59
59
  import { buildPendingSceneAnnotationsState } from "./services/scene-annotations.js";
60
60
  import { createPrimitiveJobContext } from "./primitive-context.js";
@@ -401,7 +401,16 @@ const userAttachmentFinalizeSchema = z.object({
401
401
  content_type: z.string().trim().min(1).max(255),
402
402
  size_bytes: z.coerce.number().int().min(1).max(1024 * 1024 * 1024),
403
403
  storage_key: z.string().min(1).max(2000),
404
- folder_path: z.string().trim().max(500).optional()
404
+ folder_path: z.string().trim().max(500).optional(),
405
+ notes: z.string().trim().max(4000).optional()
406
+ });
407
+ const userAttachmentNotesSchema = z.object({
408
+ notes: z.string().max(4000).nullable().optional()
409
+ });
410
+ const userAttachmentSearchSchema = z.object({
411
+ query: z.string().min(1).max(2000),
412
+ folder_path: z.string().trim().max(500).optional(),
413
+ limit: z.coerce.number().int().min(1).max(100).optional()
405
414
  });
406
415
  const userFileFolderSchema = z.object({
407
416
  folder_path: z.string().trim().min(1).max(500)
@@ -2917,6 +2926,9 @@ function serializeUserAttachment(c, attachment) {
2917
2926
  sizeBytes: attachment.sizeBytes,
2918
2927
  createdAt: attachment.createdAt,
2919
2928
  folderPath: attachment.folderPath ?? "",
2929
+ notes: attachment.notes ?? null,
2930
+ // Flag only — the raw vector never ships to the client (mirrors serializeClip).
2931
+ hasNotesEmbedding: Boolean(attachment.notesEmbedding && attachment.notesEmbedding.length > 0),
2920
2932
  viewUrl: attachment.publicUrl || storage.getPublicUrl(attachment.storageKey) || buildUserAttachmentViewUrl(c, attachment.storageKey)
2921
2933
  };
2922
2934
  }
@@ -3597,7 +3609,12 @@ function normalizeEditorChatPath(input) {
3597
3609
  // and NEVER runs GhostCut on the long-form source — "no text" is a
3598
3610
  // scene-selection filter; caption removal stays a per-finished-clip
3599
3611
  // primitive (/api/v1/primitives/videos/remove-captions).
3600
- CLIPS_PREFIX
3612
+ CLIPS_PREFIX,
3613
+ // Public skill knowledge: the vendored skill packs (load_skill fetches
3614
+ // these too) and per-template SKILL.md manifests. Read-only markdown.
3615
+ "/skill/",
3616
+ "/skill-pack/",
3617
+ "/discover/skills/"
3601
3618
  ];
3602
3619
  const parsed = trimmed.startsWith("http://") || trimmed.startsWith("https://")
3603
3620
  ? new URL(trimmed)
@@ -3975,16 +3992,19 @@ function browseFilesTextContentType(fileName) {
3975
3992
  }
3976
3993
  function createBrowseFilesTool(input) {
3977
3994
  return tool({
3978
- description: "Browse AND write the user's My Files filesystem — their personal library of uploaded assets (mp4, mov, webm, png, jpg, jpeg, gif, webp, svg, mp3, wav, m4a, aac, pdf, md, txt, csv, and more) organized into virtual folders. Use action=list to enumerate files and virtual folders so you can discover what assets exist; pass folder_path to scope the listing to one folder (omit or empty for the root). Use action=read to fetch one file by file_id (preferred; copy it from a prior list result) or by file_name: it returns the file's durable view_url plus metadata, and for text files (md, txt, csv, srt, vtt, json) it also returns the full text_content inline so you can read it. For images, video, audio, and PDFs it returns view_url only — reference that URL directly (drop it into composition layers, or pass it into primitive routes like images/edit source_image_url, videos/generate input_references, or videos/download); you cannot read their raw bytes as text. Use action=write to SAVE a text file into My Files — pass file_name (e.g. About.md), content (the full text), and optional folder_path to namescope it under a product/offer folder. This is how you persist Getting Started context the user can reuse later: a product About.md or Interview.md, and awareness-levels.md / persuasive-angles.md / ad-hooks.md distilled from brainstorm output. write only accepts text files (md, txt, csv, json, srt, vtt); to save generated images/video/audio use the primitive routes and reference their durable URLs. write returns the new file_id and view_url. Always list before you read so you use a real file_id; never invent file ids or view URLs.",
3995
+ description: "Browse AND write the user's My Files filesystem — their personal library of uploaded assets (mp4, mov, webm, png, jpg, jpeg, gif, webp, svg, mp3, wav, m4a, aac, pdf, md, txt, csv, and more) organized into virtual folders. Use action=list to enumerate files and virtual folders so you can discover what assets exist; pass folder_path to scope the listing to one folder (omit or empty for the root). Use action=search to find files by MEANING when you don't know where something lives: pass query (plain language, e.g. 'character reference sheet for the mascot' or 'logo on transparent background') and it matches keywords AND vector-embedded metadata notes across every file's name, folder, and notes — prefer search over paging through list when My Files is large or the user references an asset vaguely. Use action=read to fetch one file by file_id (preferred; copy it from a prior list/search result) or by file_name: it returns the file's durable view_url plus metadata, and for text files (md, txt, csv, srt, vtt, json) it also returns the full text_content inline so you can read it. For images, video, audio, and PDFs it returns view_url only — reference that URL directly (drop it into composition layers, or pass it into primitive routes like images/edit source_image_url, videos/generate input_references, or videos/download); you cannot read their raw bytes as text. Use action=write to SAVE a text file into My Files — pass file_name (e.g. About.md), content (the full text), optional folder_path to namescope it under a product/offer folder, and optional notes. This is how you persist Getting Started context the user can reuse later: a product About.md or Interview.md, and awareness-levels.md / persuasive-angles.md / ad-hooks.md distilled from brainstorm output. write saves text files (md, txt, csv, json, srt, vtt) via content, or IMPORTS any media asset into the library by passing its durable URL as source_url — e.g. save a finished image job's output as character_sprite_card.png so it persists in My Files instead of living only in a job result. write returns the new file_id and view_url. Use action=annotate to set metadata notes on ANY existing file (including binary assets): notes say what the file is, who/what it depicts, and when to use it, and they are vector-embedded so future sessions can search their way back to the asset. Annotate every asset worth finding again — especially character references: keep a character_sprite_card.png plus an about_character.md per recurring character in that character's folder, annotate both, and pass the sprite card's view_url into generation reference inputs (images prompt_attachments / videos input_references) for character consistency. Always list or search before you read so you use a real file_id; never invent file ids or view URLs.",
3979
3996
  inputSchema: z.object({
3980
- action: z.enum(["list", "read", "write"]).describe("list = enumerate files and folders; read = fetch one file's view_url and, for text files, its contents; write = save a text file into My Files."),
3981
- folder_path: z.string().optional().describe("Folder to scope a list to, the folder of the file to read, or the folder to write into. Omit or leave empty for the root."),
3982
- file_id: z.string().optional().describe("Attachment id to read, copied from a prior list result. Preferred over file_name."),
3983
- file_name: z.string().optional().describe("File name to read when you do not have the id, or the name to save under for write (e.g. About.md). For read it is matched within folder_path when one is provided."),
3984
- content: z.string().optional().describe("For write: the full text content of the file to save (e.g. Markdown for About.md). Required for write; ignored otherwise."),
3985
- explanation: z.string().optional().describe("Short reason for the browse or save.")
3997
+ action: z.enum(["list", "read", "write", "annotate", "search"]).describe("list = enumerate files and folders; read = fetch one file's view_url and, for text files, its contents; write = save a text file into My Files; annotate = set the metadata notes on one existing file; search = find files by meaning across names, folders, and notes."),
3998
+ folder_path: z.string().optional().describe("Folder to scope a list or search to, the folder of the file to read, or the folder to write into. Omit or leave empty for the root."),
3999
+ file_id: z.string().optional().describe("Attachment id to read or annotate, copied from a prior list/search result. Preferred over file_name."),
4000
+ file_name: z.string().optional().describe("File name to read/annotate when you do not have the id, or the name to save under for write (e.g. About.md). Matched within folder_path when one is provided."),
4001
+ content: z.string().optional().describe("For write: the full text content of the file to save (e.g. Markdown for About.md). Required for text writes; mutually exclusive with source_url."),
4002
+ source_url: z.string().optional().describe("For write: a durable media URL (from a finished primitive job result or an existing asset) to IMPORT into My Files as file_name — this is how you save a generated character_sprite_card.png or other binary asset into the library. Mutually exclusive with content."),
4003
+ notes: z.string().optional().describe("For annotate (required) or write (optional): metadata notes describing what the file is, who/what it depicts, and when to use it — e.g. 'Sprite card for Zara, our mascot: front/side/back views, teal jacket. Use as the reference image whenever generating Zara.' Notes are vector-embedded so search finds the file by meaning. For annotate, an empty string clears existing notes."),
4004
+ query: z.string().optional().describe("For search: what you're looking for, in plain language. Required for search; ignored otherwise."),
4005
+ explanation: z.string().optional().describe("Short reason for the browse, save, annotation, or search.")
3986
4006
  }),
3987
- execute: async ({ action, folder_path, file_id, file_name, content, explanation }) => {
4007
+ execute: async ({ action, folder_path, file_id, file_name, content, source_url, notes, query, explanation }) => {
3988
4008
  const listUrl = new URL(`${USER_PREFIX}/me/attachments`, config.PUBLIC_BASE_URL);
3989
4009
  const headers = new Headers();
3990
4010
  headers.set("vidfarm-api-key", input.vidfarmApiKey);
@@ -3997,22 +4017,59 @@ function createBrowseFilesTool(input) {
3997
4017
  if (action === "write") {
3998
4018
  const name = (file_name ?? "").trim();
3999
4019
  if (!name) {
4000
- return { action, explanation: explanation ?? null, status: 400, error: "write requires file_name (e.g. About.md)." };
4020
+ return { action, explanation: explanation ?? null, status: 400, error: "write requires file_name (e.g. About.md, or character_sprite_card.png with source_url)." };
4001
4021
  }
4002
- if (typeof content !== "string") {
4003
- return { action, explanation: explanation ?? null, status: 400, error: "write requires content (the full text of the file)." };
4022
+ const importSrc = (source_url ?? "").trim();
4023
+ let fileBlob;
4024
+ if (importSrc) {
4025
+ // Import a durable media URL (e.g. a finished primitive job's output)
4026
+ // into My Files. The api-key header only travels to our own origin.
4027
+ let sourceUrl;
4028
+ try {
4029
+ sourceUrl = new URL(importSrc, config.PUBLIC_BASE_URL);
4030
+ }
4031
+ catch {
4032
+ return { action, explanation: explanation ?? null, status: 400, error: `source_url is not a valid URL: ${importSrc.slice(0, 200)}` };
4033
+ }
4034
+ const sourceHeaders = new Headers();
4035
+ if (sourceUrl.origin === new URL(config.PUBLIC_BASE_URL).origin) {
4036
+ sourceHeaders.set("vidfarm-api-key", input.vidfarmApiKey);
4037
+ }
4038
+ const sourceResponse = await fetch(sourceUrl, {
4039
+ method: "GET",
4040
+ headers: sourceHeaders,
4041
+ signal: mergeAbortSignals(input.abortSignal, timeoutController.signal)
4042
+ });
4043
+ if (!sourceResponse.ok) {
4044
+ return { action, explanation: explanation ?? null, status: sourceResponse.status, error: `Could not fetch source_url (${sourceResponse.status}). Use a durable URL from a finished job result or an existing asset.` };
4045
+ }
4046
+ const sourceBytes = await sourceResponse.arrayBuffer();
4047
+ if (sourceBytes.byteLength > 50 * 1024 * 1024) {
4048
+ return { action, explanation: explanation ?? null, status: 400, error: "source_url asset is larger than the 50 MB My Files limit." };
4049
+ }
4050
+ const sourceType = (sourceResponse.headers.get("content-type") ?? "").split(";")[0].trim();
4051
+ fileBlob = new Blob([sourceBytes], sourceType ? { type: sourceType } : undefined);
4004
4052
  }
4005
- if (!isBrowseFilesTextFile(name, "")) {
4006
- return { action, explanation: explanation ?? null, status: 400, error: `write only supports text files (md, txt, csv, json, srt, vtt). "${name}" is not one; save generated media via primitive routes and reference their view_url instead.` };
4053
+ else {
4054
+ if (typeof content !== "string") {
4055
+ return { action, explanation: explanation ?? null, status: 400, error: "write requires content (the full text of the file) or source_url (a durable media URL to import)." };
4056
+ }
4057
+ if (!isBrowseFilesTextFile(name, "")) {
4058
+ return { action, explanation: explanation ?? null, status: 400, error: `write with content only supports text files (md, txt, csv, json, srt, vtt). To save media like "${name}", pass its durable URL as source_url instead.` };
4059
+ }
4060
+ fileBlob = new Blob([content], { type: browseFilesTextContentType(name) });
4007
4061
  }
4008
4062
  const uploadUrl = new URL(`${USER_PREFIX}/me/attachments/upload`, config.PUBLIC_BASE_URL);
4009
4063
  const uploadHeaders = new Headers();
4010
4064
  uploadHeaders.set("vidfarm-api-key", input.vidfarmApiKey);
4011
4065
  const form = new FormData();
4012
- form.append("file", new Blob([content], { type: browseFilesTextContentType(name) }), name);
4066
+ form.append("file", fileBlob, name);
4013
4067
  if (normalizedFolder) {
4014
4068
  form.append("folder_path", normalizedFolder);
4015
4069
  }
4070
+ if (typeof notes === "string" && notes.trim()) {
4071
+ form.append("notes", notes.trim());
4072
+ }
4016
4073
  const writeResponse = await fetch(uploadUrl, {
4017
4074
  method: "POST",
4018
4075
  headers: uploadHeaders,
@@ -4047,11 +4104,65 @@ function createBrowseFilesTool(input) {
4047
4104
  content_type: saved.contentType,
4048
4105
  size_bytes: saved.sizeBytes,
4049
4106
  folder_path: saved.folderPath ?? "",
4107
+ notes: saved.notes ?? null,
4050
4108
  view_url: saved.viewUrl,
4051
4109
  created_at: saved.createdAt
4052
4110
  }
4053
4111
  };
4054
4112
  }
4113
+ if (action === "search") {
4114
+ const q = (query ?? "").trim();
4115
+ if (!q) {
4116
+ return { action, explanation: explanation ?? null, status: 400, error: "search requires query (what you're looking for, in plain language)." };
4117
+ }
4118
+ const searchUrl = new URL(`${USER_PREFIX}/me/attachments/search`, config.PUBLIC_BASE_URL);
4119
+ const searchHeaders = new Headers();
4120
+ searchHeaders.set("vidfarm-api-key", input.vidfarmApiKey);
4121
+ searchHeaders.set("Content-Type", "application/json");
4122
+ const searchResponse = await fetch(searchUrl, {
4123
+ method: "POST",
4124
+ headers: searchHeaders,
4125
+ body: JSON.stringify({ query: q, ...(normalizedFolder ? { folder_path: normalizedFolder } : {}), limit: 20 }),
4126
+ signal: mergeAbortSignals(input.abortSignal, timeoutController.signal)
4127
+ });
4128
+ const searchText = await searchResponse.text();
4129
+ let searchJson = null;
4130
+ try {
4131
+ searchJson = searchText ? JSON.parse(searchText) : null;
4132
+ }
4133
+ catch {
4134
+ searchJson = null;
4135
+ }
4136
+ if (!searchResponse.ok || !Array.isArray(searchJson?.results)) {
4137
+ return {
4138
+ action,
4139
+ explanation: explanation ?? null,
4140
+ status: searchResponse.status,
4141
+ error: `My Files search failed (${searchResponse.status}). ${(searchJson?.error ?? searchText).slice(0, 500)}`
4142
+ };
4143
+ }
4144
+ return {
4145
+ action,
4146
+ explanation: explanation ?? null,
4147
+ status: 200,
4148
+ query: q,
4149
+ folder_path: normalizedFolder,
4150
+ semantic: Boolean(searchJson.semantic),
4151
+ result_count: searchJson.results.length,
4152
+ results: searchJson.results.map((file) => ({
4153
+ file_id: file.id,
4154
+ file_name: file.fileName,
4155
+ content_type: file.contentType,
4156
+ size_bytes: file.sizeBytes,
4157
+ folder_path: file.folderPath ?? "",
4158
+ notes: file.notes ?? null,
4159
+ similarity: file.similarity ?? null,
4160
+ keyword_match: Boolean(file.keyword_match),
4161
+ view_url: file.viewUrl,
4162
+ created_at: file.createdAt
4163
+ }))
4164
+ };
4165
+ }
4055
4166
  const listResponse = await fetch(listUrl, {
4056
4167
  method: "GET",
4057
4168
  headers,
@@ -4086,6 +4197,7 @@ function createBrowseFilesTool(input) {
4086
4197
  content_type: file.contentType,
4087
4198
  size_bytes: file.sizeBytes,
4088
4199
  folder_path: file.folderPath ?? "",
4200
+ notes: file.notes ?? null,
4089
4201
  view_url: file.viewUrl,
4090
4202
  created_at: file.createdAt
4091
4203
  }))
@@ -4104,7 +4216,56 @@ function createBrowseFilesTool(input) {
4104
4216
  status: 404,
4105
4217
  error: file_id || file_name
4106
4218
  ? "No matching file in My Files. Call action=list first to copy an exact file_id."
4107
- : "read requires file_id (preferred) or file_name."
4219
+ : `${action} requires file_id (preferred) or file_name.`
4220
+ };
4221
+ }
4222
+ if (action === "annotate") {
4223
+ if (typeof notes !== "string") {
4224
+ return { action, explanation: explanation ?? null, status: 400, error: "annotate requires notes (the metadata to attach; empty string clears)." };
4225
+ }
4226
+ const annotateUrl = new URL(`${USER_PREFIX}/me/attachments/${encodeURIComponent(match.id)}`, config.PUBLIC_BASE_URL);
4227
+ const annotateHeaders = new Headers();
4228
+ annotateHeaders.set("vidfarm-api-key", input.vidfarmApiKey);
4229
+ annotateHeaders.set("Content-Type", "application/json");
4230
+ const annotateResponse = await fetch(annotateUrl, {
4231
+ method: "PATCH",
4232
+ headers: annotateHeaders,
4233
+ body: JSON.stringify({ notes }),
4234
+ signal: mergeAbortSignals(input.abortSignal, timeoutController.signal)
4235
+ });
4236
+ const annotateText = await annotateResponse.text();
4237
+ let annotateJson = null;
4238
+ try {
4239
+ annotateJson = annotateText ? JSON.parse(annotateText) : null;
4240
+ }
4241
+ catch {
4242
+ annotateJson = null;
4243
+ }
4244
+ if (!annotateResponse.ok || !annotateJson?.attachment) {
4245
+ return {
4246
+ action,
4247
+ explanation: explanation ?? null,
4248
+ status: annotateResponse.status,
4249
+ error: `Could not annotate file (${annotateResponse.status}). ${(annotateJson?.error ?? annotateText).slice(0, 500)}`
4250
+ };
4251
+ }
4252
+ const annotated = annotateJson.attachment;
4253
+ return {
4254
+ action,
4255
+ explanation: explanation ?? null,
4256
+ status: 200,
4257
+ annotated: true,
4258
+ notes_embedded: Boolean(annotateJson.notes_embedded),
4259
+ file: {
4260
+ file_id: annotated.id,
4261
+ file_name: annotated.fileName,
4262
+ content_type: annotated.contentType,
4263
+ size_bytes: annotated.sizeBytes,
4264
+ folder_path: annotated.folderPath ?? "",
4265
+ notes: annotated.notes ?? null,
4266
+ view_url: annotated.viewUrl,
4267
+ created_at: annotated.createdAt
4268
+ }
4108
4269
  };
4109
4270
  }
4110
4271
  const fileInfo = {
@@ -4113,6 +4274,7 @@ function createBrowseFilesTool(input) {
4113
4274
  content_type: match.contentType,
4114
4275
  size_bytes: match.sizeBytes,
4115
4276
  folder_path: match.folderPath ?? "",
4277
+ notes: match.notes ?? null,
4116
4278
  view_url: match.viewUrl,
4117
4279
  created_at: match.createdAt
4118
4280
  };
@@ -4231,8 +4393,9 @@ function createEditorActionTool() {
4231
4393
  "replace_composition_html",
4232
4394
  "export_composition",
4233
4395
  "generate_layer",
4234
- "set_captions"
4235
- ]).describe("Which mutation to perform. Use generate_layer to AI-generate a video/image and auto-place it on the timeline (fill a gap or replace a scene). Use export_composition to trigger the same Export button available in the editor UI — this queues an AWS Lambda render and the finished MP4 URL will surface in the next editor_context as last_export_url. Use set_captions to lay down a full run of ANIMATED word-by-word captions (TikTok/CapCut style): pass captions[] cues (or text + start/duration to auto-page), plus caption_style; it replaces any existing animated caption layers. To restyle existing captions without changing text/timing, use set_layer_style with caption_* fields on each caption layer."),
4396
+ "set_captions",
4397
+ "set_transitions"
4398
+ ]).describe("Which mutation to perform. Use generate_layer to AI-generate a video/image and auto-place it on the timeline (fill a gap or replace a scene). Use export_composition to trigger the same Export button available in the editor UI — this queues an AWS Lambda render and the finished MP4 URL will surface in the next editor_context as last_export_url. Use set_captions to lay down a full run of ANIMATED word-by-word captions (TikTok/CapCut style): pass captions[] cues (or text + start/duration to auto-page), plus caption_style; it replaces any existing animated caption layers. To restyle existing captions without changing text/timing, use set_layer_style with caption_* fields on each caption layer. Use set_transitions to configure scene transitions across the WHOLE timeline in one call: transition = the junction preset applied at every cut (every scene clip except each track's first; 'none' clears), transition_intro = the first clip's entrance, transition_outro = the last clip's exit."),
4236
4399
  layer_key: z.string().optional().describe("Existing layer key from editor_context (required for remove/set/duplicate/split). MAY be either the element_id (data-hf-id) or the slug (data-hf-slug). For add_layer, optionally provide a stable id (starts with a letter, [A-Za-z0-9_-], <=64 chars) to reuse in later tool calls this turn."),
4237
4400
  slug: z.string().optional().describe("snake_case slug for the layer. On add_layer, seeds data-hf-slug. On set_layer_identity, sets it. Slugs give viral DNA a stable handle for the element."),
4238
4401
  note: z.string().optional().describe("Human-facing note explaining the layer's role, referenced by viral DNA. On add_layer seeds data-hf-note. On set_layer_identity, sets it. Empty string removes."),
@@ -4265,8 +4428,12 @@ function createEditorActionTool() {
4265
4428
  object_position: z.string().optional().describe("CSS object-position for image/video (e.g., center, top, bottom left)."),
4266
4429
  ken_burns: z.enum(["zoom-in", "zoom-out", "pan-left", "pan-right", "pan-up", "pan-down", "zoom-in-left", "zoom-in-right", "none"]).optional().describe("Ken Burns motion for still-image layers: a slow pan/zoom spanning the clip's full duration. Apply with set_layer_media on an existing image, or seed it on add_layer/generate_layer when kind/media_type is image. 'none' removes the effect. When the user says 'animate the images' / 'ken burns', apply it to every still image, varying presets across adjacent clips (zoom-in, pan-left, zoom-out, ...) for a documentary feel."),
4267
4430
  ken_burns_intensity: z.number().optional().describe("Ken Burns strength: extra zoom / pan travel as a fraction of the frame, 0.04-0.5 (default 0.18; ~0.1 subtle, ~0.3 dramatic). Only meaningful alongside ken_burns."),
4268
- transition: z.enum(["crossfade", "fade-black", "slide-left", "slide-right", "slide-up", "slide-down", "wipe-left", "wipe-right", "wipe-up", "wipe-down", "zoom-in", "zoom-out", "circle-open", "none"]).optional().describe("Scene transition INTO this clip at its start, from the previous clip on the same track. Set it on the INCOMING clip (the one after the cut) with set_layer_media, or seed it on add_layer/generate_layer. Works on video and image scene clips; 'none' removes it. The previous butt-cut clip is automatically held on screen beneath the incoming one for the transition window — do not retime anything yourself. When the user asks for 'transitions between scenes' / 'smooth cuts', apply one to EVERY scene clip except the first, varying or matching presets to the template's energy (crossfade/fade-black = calm, slide/wipe = energetic, zoom/circle-open = punchy)."),
4269
- transition_duration: z.number().optional().describe("Transition length in seconds, 0.1-2.5 (default 0.5; ~0.3 snappy, ~1.0 slow cinematic). Only meaningful alongside transition."),
4431
+ transition: z.enum(["crossfade", "fade-black", "fade-white", "flash", "smoke", "blur", "slide-left", "slide-right", "slide-up", "slide-down", "whip-left", "whip-right", "wipe-left", "wipe-right", "wipe-up", "wipe-down", "zoom-in", "zoom-out", "circle-open", "none"]).optional().describe("Scene transition INTO this clip at its start, from the previous clip on the same track. Set it on the INCOMING clip (the one after the cut) with set_layer_media, or seed it on add_layer/generate_layer. On set_transitions it is the junction preset applied at EVERY cut. Works on video and image scene clips; 'none' removes it. The previous butt-cut clip is automatically held on screen beneath the incoming one for the transition window — do not retime anything yourself. Preset energy: crossfade/fade-black/fade-white/blur = calm, smoke = dreamy, slide/wipe = energetic, whip/flash = fast-paced social, zoom/circle-open = punchy."),
4432
+ transition_duration: z.number().optional().describe("Transition length in seconds, 0.1-2.5 (default 0.5; ~0.3 snappy, ~1.0 slow cinematic). Only meaningful alongside transition/transition_intro."),
4433
+ transition_out: z.enum(["fade", "fade-black", "fade-white", "flash", "smoke", "blur", "slide-left", "slide-right", "slide-up", "slide-down", "wipe-left", "wipe-right", "wipe-up", "wipe-down", "zoom-in", "zoom-out", "circle-close", "none"]).optional().describe("Exit transition OUT of this clip at its end (fade to black, slide away, ...). Set it with set_layer_media on the clip that should animate away, or seed it on add_layer/generate_layer. The animation plays over the clip's last transition_out_duration seconds; no retiming needed. Most cuts only need the incoming clip's transition — use transition_out for ending a video (fade to black), for clips followed by a gap, or for deliberate dip-to-black moments (outgoing fade-black + incoming fade-black)."),
4434
+ transition_out_duration: z.number().optional().describe("Exit transition length in seconds, 0.1-2.5 (default 0.5). Only meaningful alongside transition_out/transition_outro."),
4435
+ transition_intro: z.enum(["crossfade", "fade-black", "fade-white", "flash", "smoke", "blur", "slide-left", "slide-right", "slide-up", "slide-down", "whip-left", "whip-right", "wipe-left", "wipe-right", "wipe-up", "wipe-down", "zoom-in", "zoom-out", "circle-open", "none"]).optional().describe("set_transitions only: entrance for the very FIRST scene clip (fade in from black = fade-black). 'none' clears it."),
4436
+ transition_outro: z.enum(["fade", "fade-black", "fade-white", "flash", "smoke", "blur", "slide-left", "slide-right", "slide-up", "slide-down", "wipe-left", "wipe-right", "wipe-up", "wipe-down", "zoom-in", "zoom-out", "circle-close", "none"]).optional().describe("set_transitions only: exit for the very LAST scene clip (fade to black = fade-black). 'none' clears it."),
4270
4437
  caption_style: z.enum(["spotlight", "karaoke", "word-pop", "stack-up", "neon", "bounce"]).optional().describe("Animated caption preset look: spotlight (active word gets a highlight pill, bold outline text), karaoke (words fill with color as spoken), word-pop (one word at a time, punchy scale-in), stack-up (words fade/rise in and stay), neon (active word glow pulse), bounce (active word bounces). Applies animation + colors + background treatment in one go; individual caption_*/color/background/background_style fields override specifics."),
4271
4438
  caption_animation: z.enum(["highlight-pop", "karaoke-fill", "word-pop", "cumulative-reveal", "glow-pulse", "bounce-wave", "none"]).optional().describe("Word-by-word caption animation, if you want to set it directly instead of via caption_style. Works on set_captions, add_layer (kind=text), and set_layer_style. 'none' removes the animation and flattens the layer back to static text."),
4272
4439
  caption_active_color: z.string().optional().describe("CSS color the active word takes while it is spoken (karaoke fill color, highlight text color, glow color)."),
@@ -4301,95 +4468,14 @@ function createEditorActionTool() {
4301
4468
  explanation: z.string().describe("One-sentence reason shown in the UI.")
4302
4469
  }),
4303
4470
  execute: async (args) => {
4471
+ const linted = lintReplaceCompositionAction(args);
4472
+ if (linted) {
4473
+ return linted;
4474
+ }
4304
4475
  return { ...args };
4305
4476
  }
4306
4477
  });
4307
4478
  }
4308
- const JOB_ID_PATTERN = /\bjob_[a-z0-9]+\b/gi;
4309
- const HTTP_TOOL_CLAIM_PATTERN = /\b(?:http_request|REST API call|API call|GET\s+https?:\/\/|POST\s+https?:\/\/|GET\s+\/api\/|POST\s+\/api\/)/i;
4310
- function collectJobIdsFromUnknown(value, output = new Set()) {
4311
- if (typeof value === "string") {
4312
- for (const match of value.matchAll(JOB_ID_PATTERN)) {
4313
- output.add(match[0]);
4314
- }
4315
- return output;
4316
- }
4317
- if (Array.isArray(value)) {
4318
- for (const entry of value) {
4319
- collectJobIdsFromUnknown(entry, output);
4320
- }
4321
- return output;
4322
- }
4323
- if (value && typeof value === "object") {
4324
- for (const entry of Object.values(value)) {
4325
- collectJobIdsFromUnknown(entry, output);
4326
- }
4327
- }
4328
- return output;
4329
- }
4330
- function collectVerifiedHttpJobIds(toolResult) {
4331
- if (!toolResult || typeof toolResult !== "object") {
4332
- return new Set();
4333
- }
4334
- const record = toolResult;
4335
- return collectJobIdsFromUnknown(record.response?.body ?? record.result?.body);
4336
- }
4337
- function readAsyncJobHandoff(output) {
4338
- if (!output || typeof output !== "object") {
4339
- return null;
4340
- }
4341
- const record = output;
4342
- const body = record.response?.body;
4343
- if (!body || typeof body !== "object") {
4344
- return null;
4345
- }
4346
- const job = body;
4347
- if (typeof job.job_id !== "string" || typeof job.status !== "string") {
4348
- return null;
4349
- }
4350
- if (job.status === "succeeded" || job.status === "failed" || job.status === "cancelled") {
4351
- return null;
4352
- }
4353
- return {
4354
- jobId: job.job_id,
4355
- tracer: typeof job.tracer === "string" ? job.tracer : null,
4356
- status: job.status,
4357
- isNewQueuedJob: record.request?.method === "POST" && record.response?.status === 202
4358
- };
4359
- }
4360
- function buildAsyncJobHandoffText(input) {
4361
- if (!input.isNewQueuedJob) {
4362
- return `Render \`${input.jobId}\` is ${input.status}.`;
4363
- }
4364
- return [
4365
- `Started render \`${input.jobId}\`${input.tracer ? ` with tracer \`${input.tracer}\`` : ""}.`,
4366
- "If it takes a while, ask for an update later and I can check it for you."
4367
- ].join("\n\n");
4368
- }
4369
- function buildEditorChatGuardrailNotice(input) {
4370
- const mentionedJobIds = collectJobIdsFromUnknown(input.assistantText);
4371
- const unverifiedJobIds = [...mentionedJobIds].filter((jobId) => !input.verifiedJobIds.has(jobId));
4372
- const notices = [];
4373
- if (input.redactedJobIds.size) {
4374
- notices.push("Guardrail: I withheld unverified job ID text because it did not come from a visible HTTP response card.");
4375
- }
4376
- else if (unverifiedJobIds.length) {
4377
- notices.push("Guardrail: I mentioned an unverified job ID. Ignore it unless it appears in a visible HTTP response card.");
4378
- }
4379
- if (!input.hasHttpRequestResult && HTTP_TOOL_CLAIM_PATTERN.test(input.assistantText)) {
4380
- notices.push("Guardrail: no `http_request` tool result was recorded for this response, so any claimed REST/API call should be treated as not executed.");
4381
- }
4382
- return notices.length ? `\n\n${notices.join("\n\n")}` : "";
4383
- }
4384
- function redactUnverifiedJobIds(input) {
4385
- return input.text.replace(JOB_ID_PATTERN, (jobId) => {
4386
- if (input.verifiedJobIds.has(jobId)) {
4387
- return jobId;
4388
- }
4389
- input.redactedJobIds.add(jobId);
4390
- return "[unverified job id]";
4391
- });
4392
- }
4393
4479
  function normalizeHistoryAttachments(attachments) {
4394
4480
  return Array.isArray(attachments)
4395
4481
  ? attachments.map((attachment) => ({
@@ -4433,6 +4519,10 @@ async function streamEditorChatAgent(input, send) {
4433
4519
  vidfarmApiKey: input.vidfarmApiKey,
4434
4520
  abortSignal: input.abortSignal
4435
4521
  }),
4522
+ load_skill: createLoadSkillTool({
4523
+ publicBaseUrl: config.PUBLIC_BASE_URL,
4524
+ abortSignal: input.abortSignal
4525
+ }),
4436
4526
  frontend_action: createEditorFrontendActionTool({
4437
4527
  tracerRegistry
4438
4528
  }),
@@ -4459,6 +4549,7 @@ async function streamEditorChatAgent(input, send) {
4459
4549
  let hasHttpRequestResult = false;
4460
4550
  const verifiedJobIds = new Set();
4461
4551
  const redactedJobIds = new Set();
4552
+ const jobIdRedactor = createStreamingJobIdRedactor({ verifiedJobIds, redactedJobIds });
4462
4553
  const toolExchangeOrder = [];
4463
4554
  const toolExchangeMap = new Map();
4464
4555
  const httpExchanges = [];
@@ -4473,16 +4564,14 @@ async function streamEditorChatAgent(input, send) {
4473
4564
  case "text-delta":
4474
4565
  assistantTextForAudit += part.text;
4475
4566
  {
4476
- const safeText = redactUnverifiedJobIds({
4477
- text: part.text,
4478
- verifiedJobIds,
4479
- redactedJobIds
4480
- });
4481
- assistantTextForUser += safeText;
4482
- send({
4483
- type: "text-delta",
4484
- delta: safeText
4485
- });
4567
+ const safeText = jobIdRedactor.push(part.text);
4568
+ if (safeText) {
4569
+ assistantTextForUser += safeText;
4570
+ send({
4571
+ type: "text-delta",
4572
+ delta: safeText
4573
+ });
4574
+ }
4486
4575
  }
4487
4576
  break;
4488
4577
  case "tool-call":
@@ -4576,6 +4665,14 @@ async function streamEditorChatAgent(input, send) {
4576
4665
  break;
4577
4666
  case "finish":
4578
4667
  {
4668
+ const heldText = jobIdRedactor.flush();
4669
+ if (heldText) {
4670
+ assistantTextForUser += heldText;
4671
+ send({
4672
+ type: "text-delta",
4673
+ delta: heldText
4674
+ });
4675
+ }
4579
4676
  const guardrailNotice = buildEditorChatGuardrailNotice({
4580
4677
  assistantText: assistantTextForAudit,
4581
4678
  verifiedJobIds,
@@ -4691,7 +4788,11 @@ function rewriteHyperframesDraftCompositionHtml(html, draftPath) {
4691
4788
  }
4692
4789
  }
4693
4790
  // And the scene-transition keyframes: toggling data-transition on a clip
4694
- // must animate immediately on older stored compositions.
4791
+ // must animate immediately on older stored compositions. Compositions that
4792
+ // embed the V1 block get it swapped for the current one in place; if the
4793
+ // swap misses (drifted block), the marker check below appends V2 after it,
4794
+ // which wins by source order.
4795
+ rewritten = upgradeLegacyTransitionCss(rewritten);
4695
4796
  if (!rewritten.includes(TRANSITION_STYLE_MARKER)) {
4696
4797
  const transitionTag = `<style data-vf-transitions="true">${TRANSITION_CSS}</style>`;
4697
4798
  if (/<\/head>/i.test(rewritten)) {
@@ -5511,21 +5612,193 @@ const PUBLIC_SKILL_FILES = {
5511
5612
  "vidfarm-director": "SKILL.director.md",
5512
5613
  "vidfarm-platform": "SKILL.platform.md"
5513
5614
  };
5615
+ // Vendored skill packs (the heygen-scrubbed hyperframes pack + vidfarm-media)
5616
+ // mirrored from .agents/skills. Vidfarm is the distribution origin: the devcli
5617
+ // (`vidfarm skills add`) and the chat copilot's load_skill tool both pull from
5618
+ // these routes, so customers never fetch skills from an upstream vendor org.
5619
+ const SKILL_PACK_NAME_PATTERN = /^[a-z0-9][a-z0-9_-]{0,64}$/;
5620
+ const SKILL_PACK_IGNORED_ENTRIES = new Set([".DS_Store", "node_modules", ".git"]);
5621
+ function resolveSkillPackDir(name) {
5622
+ if (!SKILL_PACK_NAME_PATTERN.test(name)) {
5623
+ return null;
5624
+ }
5625
+ for (const candidate of resolveRootFileCandidates(path.join(".agents", "skills", name))) {
5626
+ try {
5627
+ if (statSync(candidate).isDirectory()) {
5628
+ return candidate;
5629
+ }
5630
+ }
5631
+ catch {
5632
+ continue;
5633
+ }
5634
+ }
5635
+ return null;
5636
+ }
5637
+ function listSkillPackFiles(dir, prefix = "") {
5638
+ const files = [];
5639
+ for (const entry of readdirSync(dir, { withFileTypes: true })) {
5640
+ if (SKILL_PACK_IGNORED_ENTRIES.has(entry.name)) {
5641
+ continue;
5642
+ }
5643
+ const relative = prefix ? `${prefix}/${entry.name}` : entry.name;
5644
+ const absolute = path.join(dir, entry.name);
5645
+ if (entry.isDirectory()) {
5646
+ files.push(...listSkillPackFiles(absolute, relative));
5647
+ }
5648
+ else if (entry.isFile()) {
5649
+ files.push(relative);
5650
+ }
5651
+ }
5652
+ return files.sort();
5653
+ }
5654
+ function readSkillPackDescription(dir) {
5655
+ try {
5656
+ const skillMd = readFileSync(path.join(dir, "SKILL.md"), "utf8");
5657
+ const match = skillMd.match(/^description:\s*(.+)$/m);
5658
+ return match ? match[1].trim().slice(0, 300) : "";
5659
+ }
5660
+ catch {
5661
+ return "";
5662
+ }
5663
+ }
5514
5664
  app.get("/skill/:name", (c) => {
5515
5665
  const name = c.req.param("name").replace(/\.md$/i, "");
5516
5666
  const filename = PUBLIC_SKILL_FILES[name];
5517
- if (!filename) {
5518
- return c.text("Unknown skill", 404);
5519
- }
5520
- const contents = readRootSkillFile(filename);
5667
+ const contents = filename
5668
+ ? readRootSkillFile(filename)
5669
+ : (() => {
5670
+ const packDir = resolveSkillPackDir(name);
5671
+ if (!packDir) {
5672
+ return "";
5673
+ }
5674
+ try {
5675
+ return readFileSync(path.join(packDir, "SKILL.md"), "utf8");
5676
+ }
5677
+ catch {
5678
+ return "";
5679
+ }
5680
+ })();
5521
5681
  if (!contents) {
5522
- return c.text("Skill file unavailable", 404);
5682
+ return c.text(filename ? "Skill file unavailable" : "Unknown skill", 404);
5523
5683
  }
5524
5684
  return c.body(contents, 200, {
5525
5685
  "content-type": "text/markdown; charset=utf-8",
5526
5686
  "cache-control": "no-cache"
5527
5687
  });
5528
5688
  });
5689
+ app.get("/skill-pack/index.json", (c) => {
5690
+ const skills = [];
5691
+ for (const candidate of resolveRootFileCandidates(path.join(".agents", "skills"))) {
5692
+ let entries;
5693
+ try {
5694
+ entries = readdirSync(candidate);
5695
+ }
5696
+ catch {
5697
+ continue;
5698
+ }
5699
+ for (const entry of entries.sort()) {
5700
+ if (!SKILL_PACK_NAME_PATTERN.test(entry) || skills.some((skill) => skill.name === entry)) {
5701
+ continue;
5702
+ }
5703
+ const dir = path.join(candidate, entry);
5704
+ try {
5705
+ if (!statSync(dir).isDirectory()) {
5706
+ continue;
5707
+ }
5708
+ }
5709
+ catch {
5710
+ continue;
5711
+ }
5712
+ skills.push({ name: entry, description: readSkillPackDescription(dir) });
5713
+ }
5714
+ break;
5715
+ }
5716
+ return c.json({ skills });
5717
+ });
5718
+ // Manifest hashing reads every file in a pack (hyperframes-animation alone is
5719
+ // ~100 files) — memoize per pack dir, invalidated by a cheap stat signature,
5720
+ // so `vidfarm skills update` sweeps don't re-hash 5MB per request.
5721
+ const skillPackManifestCache = new Map();
5722
+ app.get("/skill-pack/:name/manifest.json", (c) => {
5723
+ const packDir = resolveSkillPackDir(c.req.param("name"));
5724
+ if (!packDir) {
5725
+ return c.json({ error: "Unknown skill pack." }, 404);
5726
+ }
5727
+ const relativePaths = listSkillPackFiles(packDir);
5728
+ const signature = relativePaths
5729
+ .map((relative) => {
5730
+ try {
5731
+ const stats = statSync(path.join(packDir, relative));
5732
+ return `${relative}:${stats.size}:${stats.mtimeMs}`;
5733
+ }
5734
+ catch {
5735
+ return `${relative}:missing`;
5736
+ }
5737
+ })
5738
+ .join("|");
5739
+ const cached = skillPackManifestCache.get(packDir);
5740
+ if (cached && cached.signature === signature) {
5741
+ return c.json(cached.manifest);
5742
+ }
5743
+ const files = relativePaths.map((relative) => {
5744
+ const bytes = readFileSync(path.join(packDir, relative));
5745
+ return {
5746
+ path: relative,
5747
+ size: bytes.length,
5748
+ sha256: createHash("sha256").update(bytes).digest("hex")
5749
+ };
5750
+ });
5751
+ const manifest = { name: path.basename(packDir), files };
5752
+ skillPackManifestCache.set(packDir, { signature, manifest });
5753
+ return c.json(manifest);
5754
+ });
5755
+ app.get("/skill-pack/:name/files/*", (c) => {
5756
+ const packDir = resolveSkillPackDir(c.req.param("name"));
5757
+ if (!packDir) {
5758
+ return c.text("Unknown skill pack.", 404);
5759
+ }
5760
+ const marker = "/files/";
5761
+ const rawPath = c.req.path.slice(c.req.path.indexOf(marker) + marker.length);
5762
+ let relative;
5763
+ try {
5764
+ relative = rawPath
5765
+ .split("/")
5766
+ .map((segment) => decodeURIComponent(segment))
5767
+ .join("/");
5768
+ }
5769
+ catch {
5770
+ return c.text("Bad file path.", 400);
5771
+ }
5772
+ const resolved = path.resolve(packDir, relative);
5773
+ if (resolved !== packDir && !resolved.startsWith(packDir + path.sep)) {
5774
+ return c.text("Bad file path.", 400);
5775
+ }
5776
+ let bytes;
5777
+ try {
5778
+ bytes = readFileSync(resolved);
5779
+ }
5780
+ catch {
5781
+ return c.text("File not found.", 404);
5782
+ }
5783
+ const ext = path.extname(resolved).toLowerCase();
5784
+ const contentType = ext === ".md" || ext === ".markdown" ? "text/markdown; charset=utf-8"
5785
+ : ext === ".json" ? "application/json; charset=utf-8"
5786
+ : ext === ".mjs" || ext === ".cjs" || ext === ".js" ? "text/javascript; charset=utf-8"
5787
+ : ext === ".html" ? "text/html; charset=utf-8"
5788
+ : ext === ".css" ? "text/css; charset=utf-8"
5789
+ : ext === ".txt" || ext === ".srt" || ext === ".vtt" || ext === ".sh" || ext === ".yaml" || ext === ".yml" ? "text/plain; charset=utf-8"
5790
+ : ext === ".png" ? "image/png"
5791
+ : ext === ".avif" ? "image/avif"
5792
+ : ext === ".webp" ? "image/webp"
5793
+ : ext === ".jpg" || ext === ".jpeg" ? "image/jpeg"
5794
+ : ext === ".mp3" ? "audio/mpeg"
5795
+ : ext === ".woff2" ? "font/woff2"
5796
+ : "application/octet-stream";
5797
+ return c.body(new Uint8Array(bytes), 200, {
5798
+ "content-type": contentType,
5799
+ "cache-control": "no-cache"
5800
+ });
5801
+ });
5529
5802
  if (config.SENTRY_DEBUG_ROUTE === "true") {
5530
5803
  app.get("/debug-sentry", () => {
5531
5804
  Sentry.logger.info("User triggered test error", {
@@ -6435,7 +6708,13 @@ app.post(`${COMPOSITIONS_PREFIX}/:forkId/captions`, async (c) => {
6435
6708
  const text = str("text");
6436
6709
  if (text) {
6437
6710
  const windowStart = Math.max(0, num("start") ?? 0);
6438
- const windowDuration = num("duration") ?? Math.max(0.4, compositionDuration - windowStart);
6711
+ const availableWindow = compositionDuration - windowStart;
6712
+ // When the composition declares no usable window (missing data-duration,
6713
+ // or windowStart at/after the end), fall back to a word-count estimate
6714
+ // (~0.32s/word, same as the /audio/captions primitive) instead of
6715
+ // cramming the whole script into a 0.4s window of overlapping cues.
6716
+ const estimatedWindow = Math.max(0.4, text.trim().split(/\s+/).filter(Boolean).length * 0.32);
6717
+ const windowDuration = num("duration") ?? (availableWindow > 0.4 ? availableWindow : estimatedWindow);
6439
6718
  cues = cuesFromText(text, windowStart + offsetSec, windowDuration, { maxWordsPerCue: maxWords });
6440
6719
  sourceNote = "text";
6441
6720
  }
@@ -14772,6 +15051,77 @@ app.get(`${USER_PREFIX}/me/generated-media`, async (c) => {
14772
15051
  next_cursor: page.nextCursor
14773
15052
  });
14774
15053
  });
15054
+ // OpenAI's embedding models are named text-embedding-3-* / text-embedding-ada-*;
15055
+ // Gemini's family INCLUDES text-embedding-004 (plus gemini-embedding-*), so a
15056
+ // bare startsWith("text-embedding") misroutes a Gemini-built library to OpenAI
15057
+ // and returns cross-space (garbage) cosine scores. Match the OpenAI naming
15058
+ // precisely; everything else is gemini.
15059
+ function inferEmbeddingProviderFromModel(model) {
15060
+ return model && /^text-embedding-(3|ada)/i.test(model) ? "openai" : "gemini";
15061
+ }
15062
+ // Embed-capable client for populating/searching a clip library. Matches the
15063
+ // library's existing embedding space when one exists (so vectors stay
15064
+ // cosine-comparable); otherwise takes the first embed-capable saved key
15065
+ // (gemini → openai; openrouter can't embed). Null → no key, keyword-only.
15066
+ async function buildClipLibraryEmbedder(customerId) {
15067
+ const existing = await clipRecords.getLibraryEmbeddingModel(customerId);
15068
+ const providers = existing ? [inferEmbeddingProviderFromModel(existing)] : ["gemini", "openai"];
15069
+ for (const provider of providers) {
15070
+ const apiKey = await resolveCustomerProviderKey(customerId, provider);
15071
+ if (!apiKey)
15072
+ continue;
15073
+ const client = new ClipModelClient({ provider, apiKey });
15074
+ if (client.canEmbed)
15075
+ return client;
15076
+ }
15077
+ return null;
15078
+ }
15079
+ // Embedder for My Files notes — the same BYOK seam clip search uses. Prefer the
15080
+ // provider matching the embedding model the file library was built with (from
15081
+ // the newest annotated attachment) so query vectors stay cosine-comparable; a
15082
+ // fresh library takes the first embed-capable key (gemini → openai; openrouter
15083
+ // can't embed). Null (no key) fail-softs notes search to keyword-only.
15084
+ async function buildFileNotesEmbedder(customerId) {
15085
+ const attachments = await serverlessRecords.listUserAttachments(customerId);
15086
+ const libModel = attachments.find((a) => a.notesEmbeddingModel && Array.isArray(a.notesEmbedding) && a.notesEmbedding.length > 0)?.notesEmbeddingModel ?? null;
15087
+ const providers = libModel ? [inferEmbeddingProviderFromModel(libModel)] : ["gemini", "openai"];
15088
+ for (const provider of providers) {
15089
+ const apiKey = await resolveCustomerProviderKey(customerId, provider);
15090
+ if (!apiKey)
15091
+ continue;
15092
+ const client = new ClipModelClient({ provider, apiKey });
15093
+ if (client.canEmbed)
15094
+ return client;
15095
+ }
15096
+ return null;
15097
+ }
15098
+ // What we embed for a My Files entry: folder + file name + the notes, so
15099
+ // "the sprite card for our mascot" is findable from any phrasing. Fail-soft:
15100
+ // no key or an embed error still saves the notes (keyword-searchable).
15101
+ async function embedAttachmentNotes(customerId, input) {
15102
+ if (!input.notes.trim())
15103
+ return null;
15104
+ try {
15105
+ const embedder = await buildFileNotesEmbedder(customerId);
15106
+ if (!embedder?.embeddingModelId)
15107
+ return null;
15108
+ const text = [input.folderPath, input.fileName, input.notes].filter(Boolean).join("\n");
15109
+ const embedding = await embedder.embedQuery(text);
15110
+ return embedding && embedding.length > 0 ? { embedding, model: embedder.embeddingModelId } : null;
15111
+ }
15112
+ catch (error) {
15113
+ devLog("attachment_notes.embed_failed", devErrorFields(error));
15114
+ return null;
15115
+ }
15116
+ }
15117
+ function attachmentKeywordMatch(attachment, q) {
15118
+ const hay = `${attachment.fileName} ${attachment.folderPath ?? ""} ${attachment.notes ?? ""}`.toLowerCase();
15119
+ return q
15120
+ .toLowerCase()
15121
+ .split(/\s+/)
15122
+ .filter(Boolean)
15123
+ .every((term) => hay.includes(term));
15124
+ }
14775
15125
  app.get(`${USER_PREFIX}/me/attachments`, async (c) => {
14776
15126
  const customer = requireCustomer(c);
14777
15127
  const attachments = (await serverlessRecords.listUserAttachments(customer.id)).map((attachment) => serializeUserAttachment(c, attachment));
@@ -14850,6 +15200,7 @@ app.post(`${USER_PREFIX}/me/attachments/upload`, async (c) => {
14850
15200
  const fileName = sanitizeFileName(file.name || "upload.bin");
14851
15201
  const contentType = file.type || inferAttachmentContentType(fileName);
14852
15202
  const folderPath = sanitizeStorageSubpath(String(formData.get("folder_path") ?? ""));
15203
+ const notes = String(formData.get("notes") ?? "").trim().slice(0, 4000) || null;
14853
15204
  if (!isAllowedAttachment(fileName, contentType)) {
14854
15205
  return c.json({ error: `Unsupported attachment type: ${fileName}` }, 400);
14855
15206
  }
@@ -14861,6 +15212,7 @@ app.post(`${USER_PREFIX}/me/attachments/upload`, async (c) => {
14861
15212
  const attachmentId = randomUUID();
14862
15213
  const storageKey = storage.userAttachmentKey(customer.id, attachmentId, fileName, folderPath);
14863
15214
  const stored = await storage.putBuffer(storageKey, buffer, contentType);
15215
+ const embedded = notes ? await embedAttachmentNotes(customer.id, { fileName, folderPath, notes }) : null;
14864
15216
  await serverlessRecords.createUserAttachment({
14865
15217
  id: attachmentId,
14866
15218
  customerId: customer.id,
@@ -14869,7 +15221,10 @@ app.post(`${USER_PREFIX}/me/attachments/upload`, async (c) => {
14869
15221
  sizeBytes: buffer.byteLength,
14870
15222
  storageKey,
14871
15223
  folderPath,
14872
- publicUrl: stored.url || storage.getPublicUrl(storageKey) || buildUserAttachmentViewUrl(c, storageKey)
15224
+ publicUrl: stored.url || storage.getPublicUrl(storageKey) || buildUserAttachmentViewUrl(c, storageKey),
15225
+ notes,
15226
+ notesEmbedding: embedded?.embedding ?? null,
15227
+ notesEmbeddingModel: embedded?.model ?? null
14873
15228
  });
14874
15229
  const attachment = await serverlessRecords.getUserAttachment(customer.id, attachmentId);
14875
15230
  if (!attachment) {
@@ -14898,6 +15253,8 @@ app.post(`${USER_PREFIX}/me/attachments`, async (c) => {
14898
15253
  if (!isAllowedAttachment(fileName, contentType)) {
14899
15254
  return c.json({ error: `Unsupported attachment type: ${fileName}` }, 400);
14900
15255
  }
15256
+ const notes = body.notes?.trim() || null;
15257
+ const embedded = notes ? await embedAttachmentNotes(customer.id, { fileName, folderPath, notes }) : null;
14901
15258
  await serverlessRecords.createUserAttachment({
14902
15259
  id: body.attachment_id,
14903
15260
  customerId: customer.id,
@@ -14906,7 +15263,10 @@ app.post(`${USER_PREFIX}/me/attachments`, async (c) => {
14906
15263
  sizeBytes: body.size_bytes,
14907
15264
  storageKey: expectedStorageKey,
14908
15265
  folderPath,
14909
- publicUrl: storage.getPublicUrl(expectedStorageKey) || buildUserAttachmentViewUrl(c, expectedStorageKey)
15266
+ publicUrl: storage.getPublicUrl(expectedStorageKey) || buildUserAttachmentViewUrl(c, expectedStorageKey),
15267
+ notes,
15268
+ notesEmbedding: embedded?.embedding ?? null,
15269
+ notesEmbeddingModel: embedded?.model ?? null
14910
15270
  });
14911
15271
  const attachment = await serverlessRecords.getUserAttachment(customer.id, body.attachment_id);
14912
15272
  if (!attachment) {
@@ -14940,6 +15300,89 @@ app.patch(`${USER_PREFIX}/me/attachments/folders`, async (c) => {
14940
15300
  await serverlessRecords.moveUserAttachmentFolder(customer.id, fromFolder, toFolder);
14941
15301
  return c.json({ ok: true, from_folder_path: fromFolder, to_folder_path: toFolder });
14942
15302
  });
15303
+ // PATCH /me/attachments/:attachmentId — set/clear the metadata notes on one My
15304
+ // Files entry and (re)embed them for vector search. Notes are what make assets
15305
+ // findable by MEANING later — e.g. annotate a character_sprite_card.png with
15306
+ // who the character is so /me/attachments/search finds it from any phrasing.
15307
+ // (Registered after the /folders PATCH so "folders" never matches as an id.)
15308
+ app.patch(`${USER_PREFIX}/me/attachments/:attachmentId`, async (c) => {
15309
+ const customer = requireCustomer(c);
15310
+ try {
15311
+ const body = userAttachmentNotesSchema.parse(await c.req.json());
15312
+ const attachment = await serverlessRecords.getUserAttachment(customer.id, c.req.param("attachmentId"));
15313
+ if (!attachment) {
15314
+ return c.json({ error: "Attachment not found." }, 404);
15315
+ }
15316
+ const notes = (body.notes ?? "").trim() || null;
15317
+ const embedded = notes
15318
+ ? await embedAttachmentNotes(customer.id, { fileName: attachment.fileName, folderPath: attachment.folderPath, notes })
15319
+ : null;
15320
+ const updated = await serverlessRecords.updateUserAttachmentNotes(customer.id, attachment.id, {
15321
+ notes,
15322
+ notesEmbedding: embedded?.embedding ?? null,
15323
+ notesEmbeddingModel: embedded?.model ?? null
15324
+ });
15325
+ if (!updated) {
15326
+ return c.json({ error: "Attachment not found." }, 404);
15327
+ }
15328
+ return c.json({ ok: true, attachment: serializeUserAttachment(c, updated), notes_embedded: Boolean(embedded) });
15329
+ }
15330
+ catch (error) {
15331
+ return c.json({ error: error instanceof Error ? error.message : "Unable to update attachment notes." }, 400);
15332
+ }
15333
+ });
15334
+ // POST /me/attachments/search — find My Files entries by meaning, mirroring
15335
+ // /clips/search: keyword filter over name/folder/notes, semantically re-ranked
15336
+ // via the notes embeddings; when nothing keyword-matches, relax to a pure
15337
+ // semantic pass. Embeddings are BYOK — no saved key fail-softs to keyword-only.
15338
+ app.post(`${USER_PREFIX}/me/attachments/search`, async (c) => {
15339
+ const customer = requireCustomer(c);
15340
+ try {
15341
+ const body = userAttachmentSearchSchema.parse(await c.req.json());
15342
+ const query = body.query.trim();
15343
+ if (!query) {
15344
+ return c.json({ error: "Provide a search query." }, 400);
15345
+ }
15346
+ const folderPath = sanitizeStorageSubpath(body.folder_path);
15347
+ const limit = Math.max(1, Math.min(100, body.limit ?? 20));
15348
+ const all = await serverlessRecords.listUserAttachments(customer.id);
15349
+ const scoped = folderPath ? all.filter((a) => (a.folderPath ?? "") === folderPath) : all;
15350
+ let queryEmbedding;
15351
+ try {
15352
+ const embedder = await buildFileNotesEmbedder(customer.id);
15353
+ if (embedder)
15354
+ queryEmbedding = await embedder.embedQuery(query);
15355
+ }
15356
+ catch (error) {
15357
+ devLog("attachment_search.embed_failed", devErrorFields(error));
15358
+ }
15359
+ const keywordIds = new Set(scoped.filter((a) => attachmentKeywordMatch(a, query)).map((a) => a.id));
15360
+ const pool = keywordIds.size > 0 ? scoped.filter((a) => keywordIds.has(a.id)) : scoped;
15361
+ const hits = pool
15362
+ .map((a) => ({
15363
+ attachment: a,
15364
+ keyword: keywordIds.has(a.id),
15365
+ similarity: queryEmbedding && Array.isArray(a.notesEmbedding) && a.notesEmbedding.length > 0
15366
+ ? cosineSimilarity(a.notesEmbedding, queryEmbedding)
15367
+ : undefined
15368
+ }))
15369
+ .filter((h) => h.keyword || (h.similarity ?? 0) > 0)
15370
+ .sort((x, y) => ((y.similarity ?? 0) + (y.keyword ? 0.2 : 0)) - ((x.similarity ?? 0) + (x.keyword ? 0.2 : 0)))
15371
+ .slice(0, limit);
15372
+ return c.json({
15373
+ results: hits.map((h) => ({
15374
+ ...serializeUserAttachment(c, h.attachment),
15375
+ similarity: h.similarity ?? null,
15376
+ keyword_match: h.keyword
15377
+ })),
15378
+ semantic: Boolean(queryEmbedding),
15379
+ folder_path: folderPath || null
15380
+ });
15381
+ }
15382
+ catch (error) {
15383
+ return c.json({ error: error instanceof Error ? error.message : "My Files search failed." }, 400);
15384
+ }
15385
+ });
14943
15386
  app.delete(`${USER_PREFIX}/me/attachments/folders`, async (c) => {
14944
15387
  const customer = requireCustomer(c);
14945
15388
  const body = userFileFolderSchema.parse(await c.req.json());
@@ -15519,20 +15962,31 @@ function renderClipsPage(input) {
15519
15962
  .clips-import-note[data-tone="success"] { color: #3f7d55; }
15520
15963
  .clips-import-actions { display: flex; justify-content: flex-end; gap: 10px; }
15521
15964
 
15522
- @media (max-width: 1180px) {
15965
+ @media (min-width: 1025px) and (max-width: 1180px) {
15966
+ /* Rail stacks inline below the content in this band only; at ≤1024px
15967
+ the shared shell turns the rail into the FAB + popup overlay. */
15523
15968
  .clips-frame { padding-left: 0; }
15524
15969
  .editor-right-rail { position: relative; inset: auto; width: 100%; height: 540px; margin-top: 14px; border-radius: 24px; overflow: hidden; }
15525
15970
  .editor-chat-panel { border-right: 0; border-radius: 24px; }
15526
- }
15527
- @media (min-width: 1025px) and (max-width: 1180px) {
15528
15971
  html, body { height: auto; min-height: 100%; overflow: auto; overflow-x: hidden; }
15529
15972
  .app-shell { height: auto; min-height: 100svh; }
15530
15973
  .page-stack, .frame { height: auto; min-height: 0; overflow: visible; }
15531
15974
  .clips-scroll { overflow: visible; padding-right: 0; }
15532
15975
  }
15533
15976
  @media (max-width: 1024px) {
15977
+ .clips-frame { padding-left: 0; }
15534
15978
  .clips-shell { grid-template-rows: auto auto; }
15535
15979
  .clips-scroll { overflow: visible; padding-right: 0; }
15980
+ .clips-toolbar-row { gap: 8px; }
15981
+ .clips-search { flex: 1 1 100%; min-width: 0; }
15982
+ .clips-search input { min-width: 0; font-size: 16px; }
15983
+ .clips-import-form input,
15984
+ .clips-import-form textarea,
15985
+ .clips-import-form select { font-size: 16px; }
15986
+ }
15987
+ @media (max-width: 560px) {
15988
+ .clips-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 10px; }
15989
+ .clip-body { padding: 10px; }
15536
15990
  }
15537
15991
  `,
15538
15992
  body: `
@@ -15846,7 +16300,7 @@ async function pickClipProviderFromKeys(customerId) {
15846
16300
  */
15847
16301
  async function buildMatchEmbedder(customerId) {
15848
16302
  const libModel = await clipRecords.getLibraryEmbeddingModel(customerId);
15849
- const provider = libModel && libModel.startsWith("text-embedding") ? "openai" : "gemini";
16303
+ const provider = inferEmbeddingProviderFromModel(libModel);
15850
16304
  const key = await resolveCustomerProviderKey(customerId, provider);
15851
16305
  if (!key)
15852
16306
  return null;
@@ -16216,29 +16670,54 @@ app.post(`${CLIPS_PREFIX}/scan`, async (c) => {
16216
16670
  const computeEstimate = effectiveDurationSec
16217
16671
  ? estimateClipScanComputeUsd(effectiveDurationSec, huntSpec.duration_band?.target_sec)
16218
16672
  : null;
16219
- const res = await clipScanSfn.send(new StartExecutionCommand({
16220
- stateMachineArn: config.VIDFARM_CLIP_SCAN_STATE_MACHINE_ARN,
16221
- name: scanId.replace(/[^A-Za-z0-9-_]/g, "-").slice(0, 80),
16222
- input: JSON.stringify({
16223
- scan_id: scanId,
16224
- owner_id: customer.id,
16225
- source_video_id: sourceVideoId,
16226
- source_filename: filename,
16227
- raw_s3_uri: rawS3Uri ?? "",
16228
- tier,
16229
- provider,
16230
- tag_api_key: tagApiKey,
16231
- embedding_provider: client.embeddingProvider ?? "",
16232
- embedding_api_key: embeddingKey ?? "",
16233
- guidance_prompt: guidancePrompt,
16234
- frames_per_scene: framesPerScene,
16235
- include_audio: includeAudio,
16236
- hunt_spec: huntSpec,
16237
- tracer
16238
- })
16239
- }));
16240
- await putScanRecords({ scanStatus: "running", executionArn: res.executionArn });
16241
- return scanResponse({ status: "running", executionArn: res.executionArn, pipeline: "cloud", estimate, computeEstimate });
16673
+ // Records first: a Dynamo failure here aborts submission before anything
16674
+ // is billed, and the warm probe lambda's later status:"scanning" write is
16675
+ // never clobbered back to "pending" by a post-start putScanRecords.
16676
+ await putScanRecords({ scanStatus: "running" });
16677
+ let executionArn;
16678
+ try {
16679
+ const res = await clipScanSfn.send(new StartExecutionCommand({
16680
+ stateMachineArn: config.VIDFARM_CLIP_SCAN_STATE_MACHINE_ARN,
16681
+ name: scanId.replace(/[^A-Za-z0-9-_]/g, "-").slice(0, 80),
16682
+ input: JSON.stringify({
16683
+ scan_id: scanId,
16684
+ owner_id: customer.id,
16685
+ source_video_id: sourceVideoId,
16686
+ source_filename: filename,
16687
+ raw_s3_uri: rawS3Uri ?? "",
16688
+ tier,
16689
+ provider,
16690
+ tag_api_key: tagApiKey,
16691
+ embedding_provider: client.embeddingProvider ?? "",
16692
+ embedding_api_key: embeddingKey ?? "",
16693
+ guidance_prompt: guidancePrompt,
16694
+ frames_per_scene: framesPerScene,
16695
+ include_audio: includeAudio,
16696
+ hunt_spec: huntSpec,
16697
+ tracer
16698
+ })
16699
+ }));
16700
+ executionArn = res.executionArn;
16701
+ }
16702
+ catch (error) {
16703
+ // The execution never started — roll the scan (and its source) to failed
16704
+ // so pollers do not spin on a "running" record that will never progress,
16705
+ // mirroring the lazy-reconcile failure path.
16706
+ const orphan = await clipRecords.getScan(customer.id, scanId);
16707
+ if (orphan)
16708
+ await clipRecords.putScan({ ...orphan, status: "failed", error: "Failed to start the scan pipeline.", updated_at: nowIso() });
16709
+ const orphanSource = await clipRecords.getSource(customer.id, sourceVideoId);
16710
+ if (orphanSource && orphanSource.status !== "complete")
16711
+ await clipRecords.putSource({ ...orphanSource, status: "failed", updated_at: nowIso() });
16712
+ throw error;
16713
+ }
16714
+ // Attach the execution ARN for lazy SFN reconciliation (best-effort — the
16715
+ // record is already pollable; spread the current record so a runner that
16716
+ // has already advanced the status is not regressed).
16717
+ const startedScan = await clipRecords.getScan(customer.id, scanId);
16718
+ if (startedScan)
16719
+ await clipRecords.putScan({ ...startedScan, execution_arn: executionArn, updated_at: nowIso() });
16720
+ return scanResponse({ status: "running", executionArn, pipeline: "cloud", estimate, computeEstimate });
16242
16721
  }
16243
16722
  // ── vidfarm serve (no deployed pipeline): LOCAL-FIRST, cloud as backup ──
16244
16723
  // Same policy as `vidfarm clips scan`: the user's local claude/codex CLI
@@ -16540,6 +17019,11 @@ async function mirrorUpstreamClipsToLocal(input) {
16540
17019
  if (!feed.ok || !feed.json)
16541
17020
  throw new Error(`Upstream clip feed unavailable (HTTP ${feed.status}).`);
16542
17021
  let stored = 0;
17022
+ // Upstream /clips/feed strips the embedding vector, so re-embed each mirrored
17023
+ // clip on the owner's BYOK key — otherwise it lands with no embedding and is
17024
+ // permanently invisible to semantic clip search (keyword-only). Fail-soft:
17025
+ // no embed-capable key → store without embedding (keyword-only), as before.
17026
+ const embedder = await buildClipLibraryEmbedder(ownerId);
16543
17027
  for (const raw of feed.json.clips ?? []) {
16544
17028
  const clipId = typeof raw.clip_id === "string" ? raw.clip_id : null;
16545
17029
  const viewUrl = typeof raw.view_url === "string" ? raw.view_url : null;
@@ -16559,16 +17043,32 @@ async function mirrorUpstreamClipsToLocal(input) {
16559
17043
  await storage.putBuffer(thumbKey, new Uint8Array(await thumbRes.arrayBuffer()), "image/jpeg");
16560
17044
  }
16561
17045
  }
16562
- const { view_url: _v, thumbnail_url: _t, ...rest } = raw;
17046
+ const { view_url: _v, thumbnail_url: _t, embedding: _e, embedding_model: _em, ...rest } = raw;
16563
17047
  void _v;
16564
17048
  void _t;
16565
- await clipRecords.putClip({
17049
+ void _e;
17050
+ void _em;
17051
+ const clip = {
16566
17052
  ...rest,
16567
17053
  owner_id: ownerId,
16568
17054
  source_video_id: scan.source_video_id,
16569
17055
  file_path: clipKey,
16570
17056
  thumbnail_path: thumbKey
16571
- });
17057
+ };
17058
+ if (embedder?.embeddingModelId) {
17059
+ try {
17060
+ const [vector] = await embedder.embedDocuments([buildClipEmbeddingText(clip.tags, clip.description)]);
17061
+ if (vector?.length) {
17062
+ clip.embedding = vector;
17063
+ clip.embedding_model = embedder.embeddingModelId;
17064
+ }
17065
+ }
17066
+ catch (error) {
17067
+ devLog("clip_scan.upstream_mirror_embed_failed", { scan_id: scan.scan_id, clip_id: clipId, ...devErrorFields(error) }, "warn");
17068
+ }
17069
+ }
17070
+ await clipRecords.putClip(clip);
17071
+ await vectorIndex.upsert(ownerId, clip);
16572
17072
  stored++;
16573
17073
  }
16574
17074
  const source = await clipRecords.getSource(ownerId, scan.source_video_id);