@autoaiai/aicut 0.1.7 → 0.1.13

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 (306) hide show
  1. package/MANUAL.md +94 -84
  2. package/package.json +1 -1
  3. package/product.json +2 -1
  4. package/skills/aicut/SKILL.md +4 -4
  5. package/skills/aicut/references/commands.md +15 -15
  6. package/skills/aicut/references/film-format.md +1 -1
  7. package/skills/aicut/references/recipes.md +4 -4
  8. package/skills/aicut/references/troubleshooting.md +2 -2
  9. package/skills/aicut-motion/SKILL.md +75 -9
  10. package/skills/aicut-motion/THIRD_PARTY_NOTICES.md +14 -0
  11. package/skills/aicut-motion/assets/atlas/lib/harness.js +116 -0
  12. package/skills/aicut-motion/assets/atlas/lib/sound.js +274 -0
  13. package/skills/aicut-motion/assets/atlas/styles/001-flat-vector-science/code.js +937 -0
  14. package/skills/aicut-motion/assets/atlas/styles/001-flat-vector-science/poster.jpg +0 -0
  15. package/skills/aicut-motion/assets/atlas/styles/001-flat-vector-science/style.json +13 -0
  16. package/skills/aicut-motion/assets/atlas/styles/002-watercolor-wash/code.js +909 -0
  17. package/skills/aicut-motion/assets/atlas/styles/002-watercolor-wash/poster.jpg +0 -0
  18. package/skills/aicut-motion/assets/atlas/styles/002-watercolor-wash/style.json +13 -0
  19. package/skills/aicut-motion/assets/atlas/styles/003-shadow-puppet-theatre/code.js +881 -0
  20. package/skills/aicut-motion/assets/atlas/styles/003-shadow-puppet-theatre/poster.jpg +0 -0
  21. package/skills/aicut-motion/assets/atlas/styles/003-shadow-puppet-theatre/style.json +13 -0
  22. package/skills/aicut-motion/assets/atlas/styles/004-whiteboard-explainer/code.js +897 -0
  23. package/skills/aicut-motion/assets/atlas/styles/004-whiteboard-explainer/poster.jpg +0 -0
  24. package/skills/aicut-motion/assets/atlas/styles/004-whiteboard-explainer/style.json +13 -0
  25. package/skills/aicut-motion/assets/atlas/styles/005-psychedelic-poster/code.js +814 -0
  26. package/skills/aicut-motion/assets/atlas/styles/005-psychedelic-poster/poster.jpg +0 -0
  27. package/skills/aicut-motion/assets/atlas/styles/005-psychedelic-poster/style.json +13 -0
  28. package/skills/aicut-motion/assets/atlas/styles/006-tech-glow-explainer/code.js +990 -0
  29. package/skills/aicut-motion/assets/atlas/styles/006-tech-glow-explainer/poster.jpg +0 -0
  30. package/skills/aicut-motion/assets/atlas/styles/006-tech-glow-explainer/style.json +13 -0
  31. package/skills/aicut-motion/assets/atlas/styles/007-papercraft-models/code.js +994 -0
  32. package/skills/aicut-motion/assets/atlas/styles/007-papercraft-models/poster.jpg +0 -0
  33. package/skills/aicut-motion/assets/atlas/styles/007-papercraft-models/style.json +13 -0
  34. package/skills/aicut-motion/assets/atlas/styles/008-1940s-film-noir/code.js +792 -0
  35. package/skills/aicut-motion/assets/atlas/styles/008-1940s-film-noir/poster.jpg +0 -0
  36. package/skills/aicut-motion/assets/atlas/styles/008-1940s-film-noir/style.json +13 -0
  37. package/skills/aicut-motion/assets/atlas/styles/009-manim-style-math/code.js +840 -0
  38. package/skills/aicut-motion/assets/atlas/styles/009-manim-style-math/poster.jpg +0 -0
  39. package/skills/aicut-motion/assets/atlas/styles/009-manim-style-math/style.json +13 -0
  40. package/skills/aicut-motion/assets/atlas/styles/010-illuminated-manuscript/code.js +1126 -0
  41. package/skills/aicut-motion/assets/atlas/styles/010-illuminated-manuscript/poster.jpg +0 -0
  42. package/skills/aicut-motion/assets/atlas/styles/010-illuminated-manuscript/style.json +13 -0
  43. package/skills/aicut-motion/assets/atlas/styles/011-editorial-essay-collage/code.js +854 -0
  44. package/skills/aicut-motion/assets/atlas/styles/011-editorial-essay-collage/poster.jpg +0 -0
  45. package/skills/aicut-motion/assets/atlas/styles/011-editorial-essay-collage/style.json +13 -0
  46. package/skills/aicut-motion/assets/atlas/styles/012-bioluminescent-deep-sea/code.js +1124 -0
  47. package/skills/aicut-motion/assets/atlas/styles/012-bioluminescent-deep-sea/poster.jpg +0 -0
  48. package/skills/aicut-motion/assets/atlas/styles/012-bioluminescent-deep-sea/style.json +13 -0
  49. package/skills/aicut-motion/assets/atlas/styles/013-product-launch-reveal/code.js +728 -0
  50. package/skills/aicut-motion/assets/atlas/styles/013-product-launch-reveal/poster.jpg +0 -0
  51. package/skills/aicut-motion/assets/atlas/styles/013-product-launch-reveal/style.json +13 -0
  52. package/skills/aicut-motion/assets/atlas/styles/014-halftone-pop-panel/code.js +664 -0
  53. package/skills/aicut-motion/assets/atlas/styles/014-halftone-pop-panel/poster.jpg +0 -0
  54. package/skills/aicut-motion/assets/atlas/styles/014-halftone-pop-panel/style.json +13 -0
  55. package/skills/aicut-motion/assets/atlas/styles/015-isometric-pastel/code.js +991 -0
  56. package/skills/aicut-motion/assets/atlas/styles/015-isometric-pastel/poster.jpg +0 -0
  57. package/skills/aicut-motion/assets/atlas/styles/015-isometric-pastel/style.json +13 -0
  58. package/skills/aicut-motion/assets/atlas/styles/016-oscilloscope-vector-art/code.js +801 -0
  59. package/skills/aicut-motion/assets/atlas/styles/016-oscilloscope-vector-art/poster.jpg +0 -0
  60. package/skills/aicut-motion/assets/atlas/styles/016-oscilloscope-vector-art/style.json +13 -0
  61. package/skills/aicut-motion/assets/atlas/styles/017-risograph-zine/code.js +692 -0
  62. package/skills/aicut-motion/assets/atlas/styles/017-risograph-zine/poster.jpg +0 -0
  63. package/skills/aicut-motion/assets/atlas/styles/017-risograph-zine/style.json +13 -0
  64. package/skills/aicut-motion/assets/atlas/styles/018-claymation-set/code.js +756 -0
  65. package/skills/aicut-motion/assets/atlas/styles/018-claymation-set/poster.jpg +0 -0
  66. package/skills/aicut-motion/assets/atlas/styles/018-claymation-set/style.json +13 -0
  67. package/skills/aicut-motion/assets/atlas/styles/019-neon-sign-at-night/code.js +922 -0
  68. package/skills/aicut-motion/assets/atlas/styles/019-neon-sign-at-night/poster.jpg +0 -0
  69. package/skills/aicut-motion/assets/atlas/styles/019-neon-sign-at-night/style.json +13 -0
  70. package/skills/aicut-motion/assets/atlas/styles/020-kawaii-anime/code.js +852 -0
  71. package/skills/aicut-motion/assets/atlas/styles/020-kawaii-anime/poster.jpg +0 -0
  72. package/skills/aicut-motion/assets/atlas/styles/020-kawaii-anime/style.json +13 -0
  73. package/skills/aicut-motion/assets/atlas/styles/021-ukiyo-e-woodblock/code.js +724 -0
  74. package/skills/aicut-motion/assets/atlas/styles/021-ukiyo-e-woodblock/poster.jpg +0 -0
  75. package/skills/aicut-motion/assets/atlas/styles/021-ukiyo-e-woodblock/style.json +13 -0
  76. package/skills/aicut-motion/assets/atlas/styles/022-16-bit-pixel-odyssey/code.js +793 -0
  77. package/skills/aicut-motion/assets/atlas/styles/022-16-bit-pixel-odyssey/poster.jpg +0 -0
  78. package/skills/aicut-motion/assets/atlas/styles/022-16-bit-pixel-odyssey/style.json +13 -0
  79. package/skills/aicut-motion/assets/atlas/styles/023-chalkboard-lesson/code.js +846 -0
  80. package/skills/aicut-motion/assets/atlas/styles/023-chalkboard-lesson/poster.jpg +0 -0
  81. package/skills/aicut-motion/assets/atlas/styles/023-chalkboard-lesson/style.json +13 -0
  82. package/skills/aicut-motion/assets/atlas/styles/024-cut-paper-diorama/code.js +1008 -0
  83. package/skills/aicut-motion/assets/atlas/styles/024-cut-paper-diorama/poster.jpg +0 -0
  84. package/skills/aicut-motion/assets/atlas/styles/024-cut-paper-diorama/style.json +13 -0
  85. package/skills/aicut-motion/assets/atlas/styles/025-cathedral-glass/code.js +680 -0
  86. package/skills/aicut-motion/assets/atlas/styles/025-cathedral-glass/poster.jpg +0 -0
  87. package/skills/aicut-motion/assets/atlas/styles/025-cathedral-glass/style.json +13 -0
  88. package/skills/aicut-motion/assets/atlas/styles/026-editorial-chart/code.js +633 -0
  89. package/skills/aicut-motion/assets/atlas/styles/026-editorial-chart/poster.jpg +0 -0
  90. package/skills/aicut-motion/assets/atlas/styles/026-editorial-chart/style.json +13 -0
  91. package/skills/aicut-motion/assets/atlas/styles/027-synthwave-outrun/code.js +757 -0
  92. package/skills/aicut-motion/assets/atlas/styles/027-synthwave-outrun/poster.jpg +0 -0
  93. package/skills/aicut-motion/assets/atlas/styles/027-synthwave-outrun/style.json +13 -0
  94. package/skills/aicut-motion/assets/atlas/styles/028-origami-studio/code.js +828 -0
  95. package/skills/aicut-motion/assets/atlas/styles/028-origami-studio/poster.jpg +0 -0
  96. package/skills/aicut-motion/assets/atlas/styles/028-origami-studio/style.json +13 -0
  97. package/skills/aicut-motion/assets/atlas/styles/029-linocut-relief-print/code.js +697 -0
  98. package/skills/aicut-motion/assets/atlas/styles/029-linocut-relief-print/poster.jpg +0 -0
  99. package/skills/aicut-motion/assets/atlas/styles/029-linocut-relief-print/style.json +13 -0
  100. package/skills/aicut-motion/assets/atlas/styles/030-holographic-foil-card/code.js +687 -0
  101. package/skills/aicut-motion/assets/atlas/styles/030-holographic-foil-card/poster.jpg +0 -0
  102. package/skills/aicut-motion/assets/atlas/styles/030-holographic-foil-card/style.json +13 -0
  103. package/skills/aicut-motion/assets/atlas/styles/031-voxel-diorama/code.js +727 -0
  104. package/skills/aicut-motion/assets/atlas/styles/031-voxel-diorama/poster.jpg +0 -0
  105. package/skills/aicut-motion/assets/atlas/styles/031-voxel-diorama/style.json +13 -0
  106. package/skills/aicut-motion/assets/atlas/styles/032-sumi-e-ink-wash/code.js +903 -0
  107. package/skills/aicut-motion/assets/atlas/styles/032-sumi-e-ink-wash/poster.jpg +0 -0
  108. package/skills/aicut-motion/assets/atlas/styles/032-sumi-e-ink-wash/style.json +13 -0
  109. package/skills/aicut-motion/assets/atlas/styles/033-etch-a-sketch-toy/code.js +662 -0
  110. package/skills/aicut-motion/assets/atlas/styles/033-etch-a-sketch-toy/poster.jpg +0 -0
  111. package/skills/aicut-motion/assets/atlas/styles/033-etch-a-sketch-toy/style.json +13 -0
  112. package/skills/aicut-motion/assets/atlas/styles/034-cosmic-star-chart/code.js +951 -0
  113. package/skills/aicut-motion/assets/atlas/styles/034-cosmic-star-chart/poster.jpg +0 -0
  114. package/skills/aicut-motion/assets/atlas/styles/034-cosmic-star-chart/style.json +13 -0
  115. package/skills/aicut-motion/assets/atlas/styles/035-punk-zine-collage/code.js +759 -0
  116. package/skills/aicut-motion/assets/atlas/styles/035-punk-zine-collage/poster.jpg +0 -0
  117. package/skills/aicut-motion/assets/atlas/styles/035-punk-zine-collage/style.json +13 -0
  118. package/skills/aicut-motion/assets/atlas/styles/036-thermal-camera/code.js +738 -0
  119. package/skills/aicut-motion/assets/atlas/styles/036-thermal-camera/poster.jpg +0 -0
  120. package/skills/aicut-motion/assets/atlas/styles/036-thermal-camera/style.json +13 -0
  121. package/skills/aicut-motion/assets/atlas/styles/037-engineering-blueprint/code.js +793 -0
  122. package/skills/aicut-motion/assets/atlas/styles/037-engineering-blueprint/poster.jpg +0 -0
  123. package/skills/aicut-motion/assets/atlas/styles/037-engineering-blueprint/style.json +13 -0
  124. package/skills/aicut-motion/assets/atlas/styles/038-impasto-night/code.js +673 -0
  125. package/skills/aicut-motion/assets/atlas/styles/038-impasto-night/poster.jpg +0 -0
  126. package/skills/aicut-motion/assets/atlas/styles/038-impasto-night/style.json +13 -0
  127. package/skills/aicut-motion/assets/atlas/styles/039-win95-desktop/code.js +907 -0
  128. package/skills/aicut-motion/assets/atlas/styles/039-win95-desktop/poster.jpg +0 -0
  129. package/skills/aicut-motion/assets/atlas/styles/039-win95-desktop/style.json +13 -0
  130. package/skills/aicut-motion/assets/atlas/styles/040-low-poly-faceted/code.js +833 -0
  131. package/skills/aicut-motion/assets/atlas/styles/040-low-poly-faceted/poster.jpg +0 -0
  132. package/skills/aicut-motion/assets/atlas/styles/040-low-poly-faceted/style.json +13 -0
  133. package/skills/aicut-motion/assets/atlas/styles/041-manga-page/code.js +845 -0
  134. package/skills/aicut-motion/assets/atlas/styles/041-manga-page/poster.jpg +0 -0
  135. package/skills/aicut-motion/assets/atlas/styles/041-manga-page/style.json +13 -0
  136. package/skills/aicut-motion/assets/atlas/styles/042-wood-type-letterpress/code.js +496 -0
  137. package/skills/aicut-motion/assets/atlas/styles/042-wood-type-letterpress/poster.jpg +0 -0
  138. package/skills/aicut-motion/assets/atlas/styles/042-wood-type-letterpress/style.json +13 -0
  139. package/skills/aicut-motion/assets/atlas/styles/043-light-painting/code.js +635 -0
  140. package/skills/aicut-motion/assets/atlas/styles/043-light-painting/poster.jpg +0 -0
  141. package/skills/aicut-motion/assets/atlas/styles/043-light-painting/style.json +13 -0
  142. package/skills/aicut-motion/assets/atlas/styles/044-felt-patchwork/code.js +799 -0
  143. package/skills/aicut-motion/assets/atlas/styles/044-felt-patchwork/poster.jpg +0 -0
  144. package/skills/aicut-motion/assets/atlas/styles/044-felt-patchwork/style.json +13 -0
  145. package/skills/aicut-motion/assets/atlas/styles/045-phosphor-terminal/code.js +547 -0
  146. package/skills/aicut-motion/assets/atlas/styles/045-phosphor-terminal/poster.jpg +0 -0
  147. package/skills/aicut-motion/assets/atlas/styles/045-phosphor-terminal/style.json +13 -0
  148. package/skills/aicut-motion/assets/atlas/styles/046-glassmorphism-ui/code.js +592 -0
  149. package/skills/aicut-motion/assets/atlas/styles/046-glassmorphism-ui/poster.jpg +0 -0
  150. package/skills/aicut-motion/assets/atlas/styles/046-glassmorphism-ui/style.json +13 -0
  151. package/skills/aicut-motion/assets/atlas/styles/047-tarot-card/code.js +768 -0
  152. package/skills/aicut-motion/assets/atlas/styles/047-tarot-card/poster.jpg +0 -0
  153. package/skills/aicut-motion/assets/atlas/styles/047-tarot-card/style.json +13 -0
  154. package/skills/aicut-motion/assets/atlas/styles/048-transit-map/code.js +666 -0
  155. package/skills/aicut-motion/assets/atlas/styles/048-transit-map/poster.jpg +0 -0
  156. package/skills/aicut-motion/assets/atlas/styles/048-transit-map/style.json +13 -0
  157. package/skills/aicut-motion/assets/atlas/styles/049-gouache-travel-poster/code.js +559 -0
  158. package/skills/aicut-motion/assets/atlas/styles/049-gouache-travel-poster/poster.jpg +0 -0
  159. package/skills/aicut-motion/assets/atlas/styles/049-gouache-travel-poster/style.json +13 -0
  160. package/skills/aicut-motion/assets/atlas/styles/050-napkin-sketch/code.js +943 -0
  161. package/skills/aicut-motion/assets/atlas/styles/050-napkin-sketch/poster.jpg +0 -0
  162. package/skills/aicut-motion/assets/atlas/styles/050-napkin-sketch/style.json +13 -0
  163. package/skills/aicut-motion/assets/atlas/styles/051-art-deco-foil/code.js +606 -0
  164. package/skills/aicut-motion/assets/atlas/styles/051-art-deco-foil/poster.jpg +0 -0
  165. package/skills/aicut-motion/assets/atlas/styles/051-art-deco-foil/style.json +13 -0
  166. package/skills/aicut-motion/assets/atlas/styles/052-radiograph-on-lightbox/code.js +730 -0
  167. package/skills/aicut-motion/assets/atlas/styles/052-radiograph-on-lightbox/poster.jpg +0 -0
  168. package/skills/aicut-motion/assets/atlas/styles/052-radiograph-on-lightbox/style.json +13 -0
  169. package/skills/aicut-motion/assets/atlas/styles/053-vaporwave/code.js +782 -0
  170. package/skills/aicut-motion/assets/atlas/styles/053-vaporwave/poster.jpg +0 -0
  171. package/skills/aicut-motion/assets/atlas/styles/053-vaporwave/style.json +13 -0
  172. package/skills/aicut-motion/assets/atlas/styles/054-ebru-paper-marbling/code.js +504 -0
  173. package/skills/aicut-motion/assets/atlas/styles/054-ebru-paper-marbling/poster.jpg +0 -0
  174. package/skills/aicut-motion/assets/atlas/styles/054-ebru-paper-marbling/style.json +13 -0
  175. package/skills/aicut-motion/assets/atlas/styles/055-isotype-pictograms/code.js +517 -0
  176. package/skills/aicut-motion/assets/atlas/styles/055-isotype-pictograms/poster.jpg +0 -0
  177. package/skills/aicut-motion/assets/atlas/styles/055-isotype-pictograms/style.json +13 -0
  178. package/skills/aicut-motion/assets/atlas/styles/056-soft-3d-clay/code.js +713 -0
  179. package/skills/aicut-motion/assets/atlas/styles/056-soft-3d-clay/poster.jpg +0 -0
  180. package/skills/aicut-motion/assets/atlas/styles/056-soft-3d-clay/style.json +13 -0
  181. package/skills/aicut-motion/assets/atlas/styles/057-cross-stitch-sampler/code.js +717 -0
  182. package/skills/aicut-motion/assets/atlas/styles/057-cross-stitch-sampler/poster.jpg +0 -0
  183. package/skills/aicut-motion/assets/atlas/styles/057-cross-stitch-sampler/style.json +13 -0
  184. package/skills/aicut-motion/assets/atlas/styles/058-constructivist-propaganda/code.js +540 -0
  185. package/skills/aicut-motion/assets/atlas/styles/058-constructivist-propaganda/poster.jpg +0 -0
  186. package/skills/aicut-motion/assets/atlas/styles/058-constructivist-propaganda/style.json +13 -0
  187. package/skills/aicut-motion/assets/atlas/styles/059-pocket-monochrome/code.js +524 -0
  188. package/skills/aicut-motion/assets/atlas/styles/059-pocket-monochrome/poster.jpg +0 -0
  189. package/skills/aicut-motion/assets/atlas/styles/059-pocket-monochrome/style.json +13 -0
  190. package/skills/aicut-motion/assets/atlas/styles/060-botanical-plate/code.js +934 -0
  191. package/skills/aicut-motion/assets/atlas/styles/060-botanical-plate/poster.jpg +0 -0
  192. package/skills/aicut-motion/assets/atlas/styles/060-botanical-plate/style.json +13 -0
  193. package/skills/aicut-motion/assets/atlas/styles/061-kinetic-type/code.js +690 -0
  194. package/skills/aicut-motion/assets/atlas/styles/061-kinetic-type/poster.jpg +0 -0
  195. package/skills/aicut-motion/assets/atlas/styles/061-kinetic-type/style.json +13 -0
  196. package/skills/aicut-motion/assets/atlas/styles/062-byzantine-mosaic/code.js +612 -0
  197. package/skills/aicut-motion/assets/atlas/styles/062-byzantine-mosaic/poster.jpg +0 -0
  198. package/skills/aicut-motion/assets/atlas/styles/062-byzantine-mosaic/style.json +13 -0
  199. package/skills/aicut-motion/assets/atlas/styles/063-y2k-liquid-chrome/code.js +666 -0
  200. package/skills/aicut-motion/assets/atlas/styles/063-y2k-liquid-chrome/poster.jpg +0 -0
  201. package/skills/aicut-motion/assets/atlas/styles/063-y2k-liquid-chrome/style.json +13 -0
  202. package/skills/aicut-motion/assets/atlas/styles/064-sticky-note-wall/code.js +651 -0
  203. package/skills/aicut-motion/assets/atlas/styles/064-sticky-note-wall/poster.jpg +0 -0
  204. package/skills/aicut-motion/assets/atlas/styles/064-sticky-note-wall/style.json +13 -0
  205. package/skills/aicut-motion/assets/atlas/styles/065-vintage-newsprint/code.js +656 -0
  206. package/skills/aicut-motion/assets/atlas/styles/065-vintage-newsprint/poster.jpg +0 -0
  207. package/skills/aicut-motion/assets/atlas/styles/065-vintage-newsprint/style.json +13 -0
  208. package/skills/aicut-motion/assets/atlas/styles/066-hidden-line-wireframe/code.js +672 -0
  209. package/skills/aicut-motion/assets/atlas/styles/066-hidden-line-wireframe/poster.jpg +0 -0
  210. package/skills/aicut-motion/assets/atlas/styles/066-hidden-line-wireframe/style.json +13 -0
  211. package/skills/aicut-motion/assets/atlas/styles/067-memphis-group/code.js +548 -0
  212. package/skills/aicut-motion/assets/atlas/styles/067-memphis-group/poster.jpg +0 -0
  213. package/skills/aicut-motion/assets/atlas/styles/067-memphis-group/style.json +13 -0
  214. package/skills/aicut-motion/assets/atlas/styles/068-ballpoint-notebook/code.js +989 -0
  215. package/skills/aicut-motion/assets/atlas/styles/068-ballpoint-notebook/poster.jpg +0 -0
  216. package/skills/aicut-motion/assets/atlas/styles/068-ballpoint-notebook/style.json +13 -0
  217. package/skills/aicut-motion/assets/atlas/styles/069-op-art-sphere/code.js +440 -0
  218. package/skills/aicut-motion/assets/atlas/styles/069-op-art-sphere/poster.jpg +0 -0
  219. package/skills/aicut-motion/assets/atlas/styles/069-op-art-sphere/style.json +13 -0
  220. package/skills/aicut-motion/assets/atlas/styles/070-topographic-survey/code.js +895 -0
  221. package/skills/aicut-motion/assets/atlas/styles/070-topographic-survey/poster.jpg +0 -0
  222. package/skills/aicut-motion/assets/atlas/styles/070-topographic-survey/style.json +13 -0
  223. package/skills/aicut-motion/assets/atlas/styles/071-pointillist-afternoon/code.js +563 -0
  224. package/skills/aicut-motion/assets/atlas/styles/071-pointillist-afternoon/poster.jpg +0 -0
  225. package/skills/aicut-motion/assets/atlas/styles/071-pointillist-afternoon/style.json +13 -0
  226. package/skills/aicut-motion/assets/atlas/styles/072-vector-arcade-1980/code.js +627 -0
  227. package/skills/aicut-motion/assets/atlas/styles/072-vector-arcade-1980/poster.jpg +0 -0
  228. package/skills/aicut-motion/assets/atlas/styles/072-vector-arcade-1980/style.json +13 -0
  229. package/skills/aicut-motion/assets/atlas/styles/073-patent-drawing/code.js +1096 -0
  230. package/skills/aicut-motion/assets/atlas/styles/073-patent-drawing/poster.jpg +0 -0
  231. package/skills/aicut-motion/assets/atlas/styles/073-patent-drawing/style.json +13 -0
  232. package/skills/aicut-motion/assets/atlas/styles/074-bauhaus-poster/code.js +595 -0
  233. package/skills/aicut-motion/assets/atlas/styles/074-bauhaus-poster/poster.jpg +0 -0
  234. package/skills/aicut-motion/assets/atlas/styles/074-bauhaus-poster/style.json +13 -0
  235. package/skills/aicut-motion/assets/atlas/styles/075-bitmap-paint-doodle/code.js +665 -0
  236. package/skills/aicut-motion/assets/atlas/styles/075-bitmap-paint-doodle/poster.jpg +0 -0
  237. package/skills/aicut-motion/assets/atlas/styles/075-bitmap-paint-doodle/style.json +13 -0
  238. package/skills/aicut-motion/assets/atlas/styles/076-nail-thread-string-art/code.js +645 -0
  239. package/skills/aicut-motion/assets/atlas/styles/076-nail-thread-string-art/poster.jpg +0 -0
  240. package/skills/aicut-motion/assets/atlas/styles/076-nail-thread-string-art/style.json +13 -0
  241. package/skills/aicut-motion/assets/atlas/styles/077-ridgeline/code.js +436 -0
  242. package/skills/aicut-motion/assets/atlas/styles/077-ridgeline/poster.jpg +0 -0
  243. package/skills/aicut-motion/assets/atlas/styles/077-ridgeline/style.json +13 -0
  244. package/skills/aicut-motion/assets/atlas/styles/078-sketchbook-graphite/code.js +1237 -0
  245. package/skills/aicut-motion/assets/atlas/styles/078-sketchbook-graphite/poster.jpg +0 -0
  246. package/skills/aicut-motion/assets/atlas/styles/078-sketchbook-graphite/style.json +13 -0
  247. package/skills/aicut-motion/assets/atlas/styles/079-teletext-page/code.js +622 -0
  248. package/skills/aicut-motion/assets/atlas/styles/079-teletext-page/poster.jpg +0 -0
  249. package/skills/aicut-motion/assets/atlas/styles/079-teletext-page/style.json +13 -0
  250. package/skills/aicut-motion/assets/atlas/styles/080-silkscreen-grid/code.js +524 -0
  251. package/skills/aicut-motion/assets/atlas/styles/080-silkscreen-grid/poster.jpg +0 -0
  252. package/skills/aicut-motion/assets/atlas/styles/080-silkscreen-grid/style.json +13 -0
  253. package/skills/aicut-motion/assets/atlas/styles/081-guilloch-security-print/code.js +689 -0
  254. package/skills/aicut-motion/assets/atlas/styles/081-guilloch-security-print/poster.jpg +0 -0
  255. package/skills/aicut-motion/assets/atlas/styles/081-guilloch-security-print/style.json +13 -0
  256. package/skills/aicut-motion/assets/atlas/styles/082-ascii-demoscene/code.js +574 -0
  257. package/skills/aicut-motion/assets/atlas/styles/082-ascii-demoscene/poster.jpg +0 -0
  258. package/skills/aicut-motion/assets/atlas/styles/082-ascii-demoscene/style.json +13 -0
  259. package/skills/aicut-motion/assets/atlas/styles/083-international-typographic-style/code.js +469 -0
  260. package/skills/aicut-motion/assets/atlas/styles/083-international-typographic-style/poster.jpg +0 -0
  261. package/skills/aicut-motion/assets/atlas/styles/083-international-typographic-style/style.json +13 -0
  262. package/skills/aicut-motion/assets/atlas/styles/084-flow-field/code.js +531 -0
  263. package/skills/aicut-motion/assets/atlas/styles/084-flow-field/poster.jpg +0 -0
  264. package/skills/aicut-motion/assets/atlas/styles/084-flow-field/style.json +13 -0
  265. package/skills/aicut-motion/assets/atlas/styles/085-macpaint-1984/code.js +675 -0
  266. package/skills/aicut-motion/assets/atlas/styles/085-macpaint-1984/poster.jpg +0 -0
  267. package/skills/aicut-motion/assets/atlas/styles/085-macpaint-1984/style.json +13 -0
  268. package/skills/aicut-motion/assets/atlas/styles/086-multi-scale-truchet/code.js +609 -0
  269. package/skills/aicut-motion/assets/atlas/styles/086-multi-scale-truchet/poster.jpg +0 -0
  270. package/skills/aicut-motion/assets/atlas/styles/086-multi-scale-truchet/style.json +13 -0
  271. package/skills/aicut-motion/references/design-systems/README.md +1 -1
  272. package/skills/aicut-motion/references/motion-delivery.md +55 -0
  273. package/skills/aicut-motion/references/pages.md +1 -1
  274. package/skills/aicut-motion/references/style-index.json +690 -0
  275. package/skills/aicut-motion/references/style-research.md +73 -0
  276. package/skills/aicut-motion/references/style-selection.md +71 -0
  277. package/skills/aicut-motion/scripts/atlas-viewer.html +195 -0
  278. package/skills/aicut-motion/scripts/atlas.mjs +109 -0
  279. package/studio/client.mjs +3 -3
  280. package/studio/server/fetch-github.mjs +14 -3
  281. package/studio/server/server.mjs +71 -6
  282. package/studio/server/skill-summaries.mjs +60 -0
  283. package/studio/server/skills.mjs +121 -7
  284. package/studio/server/styles-store.mjs +235 -0
  285. package/studio/server/template-posters.mjs +2 -2
  286. package/studio/server/templates-page.mjs +93 -93
  287. package/studio/server/templates.mjs +32 -7
  288. package/studio/ui/dist/assets/ExportCenter-DasulhAv.js +1 -0
  289. package/studio/ui/dist/assets/SettingsModal-BZw0Hz2_.js +2 -0
  290. package/studio/ui/dist/assets/apple-touch-icon-ClxQ7j-O.png +0 -0
  291. package/studio/ui/dist/assets/es-C8XPZARI.js +1 -0
  292. package/studio/ui/dist/assets/favicon-Blwx8Trs.png +0 -0
  293. package/studio/ui/dist/assets/index-BjJVmBzB.js +1623 -0
  294. package/studio/ui/dist/assets/index-C3BfKZFO.css +1 -0
  295. package/studio/ui/dist/assets/ja-BeNkdaEj.js +1 -0
  296. package/studio/ui/dist/assets/ko-DcTVhdzK.js +1 -0
  297. package/studio/ui/dist/assets/zh-CN-Cg51ctbL.js +1 -0
  298. package/studio/ui/dist/index.html +4 -3
  299. package/studio/ui/dist/assets/ExportCenter-F-2vF-lQ.js +0 -1
  300. package/studio/ui/dist/assets/SettingsModal-BgVYguNu.js +0 -2
  301. package/studio/ui/dist/assets/es-BRUzvuoM.js +0 -1
  302. package/studio/ui/dist/assets/index-Bhg-9dA3.css +0 -1
  303. package/studio/ui/dist/assets/index-CWur1Iw9.js +0 -1625
  304. package/studio/ui/dist/assets/ja-Cth-uDcW.js +0 -1
  305. package/studio/ui/dist/assets/ko-upvw591D.js +0 -1
  306. package/studio/ui/dist/assets/zh-CN-BCfJFazB.js +0 -1
@@ -19,7 +19,7 @@ import { fileURLToPath } from 'node:url';
19
19
  import { createServer } from 'node:http';
20
20
  import { mkdir, readFile } from 'node:fs/promises';
21
21
  import { createHash } from 'node:crypto';
22
- import { spawn } from 'node:child_process';
22
+ import { execFile, spawn } from 'node:child_process';
23
23
  import { EditError, clipOverrides, editProjectFilm, readProjectFilm } from './film.mjs';
24
24
  import { FileError, bringIn, collect, extractAudio, freezeFrame, hideFile, importFile, inside, listFiles, makeFolder, moveCheck, moveFile, readable, readPage, relink } from './files.mjs';
25
25
  import { ExportError, createExports, draftApps, exportCapabilities, exportsRoot, projectSizes } from './exports.mjs';
@@ -41,7 +41,8 @@ import { listSfx, sfxJob, startSfx } from './sfx.mjs';
41
41
  import { listExamples } from './examples.mjs';
42
42
  import { createFromTemplate, listTemplates } from './templates.mjs';
43
43
  import { templateCover } from './template-posters.mjs';
44
- import { addSkillsFromGitHub, installSkills, listSkills } from './skills.mjs';
44
+ import { addSkillsFromGitHub, installSkills, listSkills, listSkillsFor, removeSkill } from './skills.mjs';
45
+ import { addUserStyles, removeUserStyle, userStyles } from './styles-store.mjs';
45
46
  import { pickFiles, pickFolder } from './folder-picker.mjs';
46
47
  import { readPrefs, writePrefs } from './prefs.mjs';
47
48
  import { ensureProxies } from './proxies.mjs';
@@ -223,6 +224,33 @@ export async function startStudio({ port = 4747, key, openBrowser = systemBrowse
223
224
  if (req.method !== 'GET' && req.method !== 'HEAD' && req.headers.origin !== origin) throw new HttpError(403, 'not from Studio');
224
225
  }
225
226
 
227
+ /**
228
+ * Whether ffmpeg and ffprobe answer here — video pictures, sound waves, exports, speech to text — asked in the
229
+ * background and never on a request: /api/health answers at once even while a first `ffmpeg -version` is still
230
+ * running (a scanner reading a new 80 MB file takes seconds, and a health that waits for it looks like a Studio that
231
+ * never came up). `null` until the first answer; asked again after a minute, or after a few seconds when it was no
232
+ * (the desktop app downloads its own while Studio runs, and they work the moment they land). Whoever starts Studio
233
+ * reads it to know whether to hand the address on (web.mjs does, for `AiCut.exe --web`).
234
+ * @returns {boolean | null}
235
+ */
236
+ let toolsAskedAt = 0;
237
+ /** @type {boolean | null} */
238
+ let toolsThere = null;
239
+ let toolsChecking = false;
240
+ function ffmpegHere() {
241
+ if (!toolsChecking && Date.now() - toolsAskedAt >= (toolsThere === true ? 60_000 : 20_000)) {
242
+ toolsAskedAt = Date.now();
243
+ toolsChecking = true;
244
+ const answers = (/** @type {string} */ bin) => new Promise((done) => {
245
+ execFile(bin, ['-version'], { timeout: 30_000, windowsHide: true }, (error) => done(!error));
246
+ });
247
+ void Promise.all(['ffmpeg', 'ffprobe'].map(answers))
248
+ .then((both) => { toolsThere = both.every(Boolean); })
249
+ .finally(() => { toolsChecking = false; });
250
+ }
251
+ return toolsThere;
252
+ }
253
+
226
254
  /*
227
255
  * The editor pages' streams of events, over WebSockets (websocket.mjs): `/api/events`, what the CLI's `open` sends
228
256
  * (and how many pages are open), and `/api/projects/:id/events`, a project's changes and the questions for the
@@ -248,8 +276,9 @@ export async function startStudio({ port = 4747, key, openBrowser = systemBrowse
248
276
  const path = url.pathname;
249
277
 
250
278
  /* which Studio this is: the CLI replaces an older one, and finds its own home's when run.json is gone. Asked
251
- without the key, so it says nothing of the machine: its home only as a digest (homeTag), no process id */
252
- if (path === '/api/health') return json(res, 200, { product: 'aicut-studio', version: VERSION, home: homeTag(), filmOrigin });
279
+ without the key, so it says nothing of the machine: its home only as a digest (homeTag), no process id — and
280
+ whether ffmpeg is here, which `AiCut.exe --web` needs to know before it hands the address to a browser */
281
+ if (path === '/api/health') return json(res, 200, { product: 'aicut-studio', version: VERSION, home: homeTag(), filmOrigin, ffmpeg: ffmpegHere() });
253
282
  const signIn = [`${COOKIE}=${key}; Path=/; HttpOnly; SameSite=Strict`, `${FRAMED}=${key}; Path=/; HttpOnly; Secure; SameSite=None; Partitioned`];
254
283
  /** A page's address with the key in it (the address bar is what a person copies). */
255
284
  const keyed = (/** @type {string} */ to) => {
@@ -406,7 +435,7 @@ export async function startStudio({ port = 4747, key, openBrowser = systemBrowse
406
435
  /* the example films (examples.mjs): none when the index cannot be read */
407
436
  if (parts[0] === 'examples' && parts.length === 1 && req.method === 'GET') return json(res, 200, { examples: await listExamples() });
408
437
  /* the home page's starter templates (templates.mjs): what they are; their files stay on this side */
409
- if (parts[0] === 'templates' && parts.length === 1 && req.method === 'GET') return json(res, 200, { templates: listTemplates() });
438
+ if (parts[0] === 'templates' && parts.length === 1 && req.method === 'GET') return json(res, 200, { templates: listTemplates(url.searchParams.get('lang') ?? undefined) });
410
439
  /* a template's cover: its own film drawn once, then cached (template-posters.mjs) */
411
440
  if (parts[0] === 'templates' && parts.length === 3 && parts[2] === 'cover' && req.method === 'GET') {
412
441
  const jpeg = await templateCover(parts[1], pages).catch((e) => {
@@ -419,7 +448,16 @@ export async function startStudio({ port = 4747, key, openBrowser = systemBrowse
419
448
  /* the skills a new project may start with (skills.mjs): the ones this copy of AICUT brings, and the ones fetched
420
449
  from a repository before. POST `skills/github` { repo } fetches a repository's skills into AICUT's home, where the
421
450
  next GET lists them beside the app's own */
422
- if (parts[0] === 'skills' && parts.length === 1 && req.method === 'GET') return json(res, 200, { skills: listSkills() });
451
+ if (parts[0] === 'skills' && parts.length === 1 && req.method === 'GET') return json(res, 200, { skills: listSkillsFor(url.searchParams.get('lang') ?? undefined) });
452
+ /* DELETE `skills/:id` takes a skill off the machine (skills.mjs): the folder goes from the library it came from, and
453
+ its style card goes with it — a tile that starts a project with a skill that is not there starts nothing. A project
454
+ that already carries a copy under `.pi/skills` keeps it. The app's own two are refused (409). */
455
+ if (parts[0] === 'skills' && parts.length === 2 && req.method === 'DELETE') {
456
+ const removed = removeSkill(parts[1]);
457
+ const card = userStyles().find((one) => one.skill === removed.id);
458
+ if (card) removeUserStyle(card.id);
459
+ return json(res, 200, { ...removed, card: card?.id ?? null });
460
+ }
423
461
  if (parts[0] === 'skills' && parts[1] === 'github' && parts.length === 2 && req.method === 'POST') {
424
462
  const { repo } = /** @type {{ repo?: string }} */ (await readJson(req));
425
463
  try {
@@ -430,6 +468,33 @@ export async function startStudio({ port = 4747, key, openBrowser = systemBrowse
430
468
  }
431
469
  }
432
470
 
471
+ /* the styles a person added from a repository (styles-store.mjs): POST `styles/github` { repo } fetches the
472
+ repository's skills (skills.mjs) and makes one style card of the Templates page per skill — the project it
473
+ starts carries that skill, and the card's picture is the starter film drawn for real. DELETE `styles/:id`
474
+ takes a card away; its skill stays in the library. */
475
+ if (parts[0] === 'styles' && parts[1] === 'github' && parts.length === 2 && req.method === 'POST') {
476
+ const { repo } = /** @type {{ repo?: string }} */ (await readJson(req));
477
+ try {
478
+ const { skills } = await addSkillsFromGitHub(String(repo ?? ''));
479
+ return json(res, 200, { styles: addUserStyles(skills) });
480
+ } catch (e) {
481
+ if (e instanceof FetchError) throw new HttpError(e.status, e.message);
482
+ throw e;
483
+ }
484
+ }
485
+ /* one skill made a style card of its own (the Skills page's switch): the skill is already in the library — the ones
486
+ AICUT brings and the ones fetched — so this only decides whether the Templates page offers a film in its name. */
487
+ if (parts[0] === 'styles' && parts.length === 1 && req.method === 'POST') {
488
+ const { skill } = /** @type {{ skill?: string }} */ (await readJson(req));
489
+ const asked = listSkills().find((one) => one.id === String(skill ?? ''));
490
+ if (!asked) throw new HttpError(404, 'no such skill');
491
+ addUserStyles([asked]);
492
+ return json(res, 200, { styles: userStyles() });
493
+ }
494
+ if (parts[0] === 'styles' && parts.length === 2 && req.method === 'DELETE') {
495
+ return json(res, 200, { removed: removeUserStyle(parts[1]) });
496
+ }
497
+
433
498
  /* the Projects window's recycle bin (recycle.mjs): where a deleted project waits 30 days before it is gone for good.
434
499
  GET recycle → { entries }, newest first, each with the days it has left
435
500
  POST recycle/:id/restore → { project }: its folder back where it was, and on the list again
@@ -0,0 +1,60 @@
1
+ // @ts-check
2
+ /**
3
+ * One line of each skill's own words, in a language a person reads — the Skills page and the style cards on the
4
+ * Templates page say this instead of the frontmatter's paragraph.
5
+ *
6
+ * A skill's `description` in its SKILL.md is written for the agent: the whole of what it covers, in the upstream
7
+ * project's own language, several hundred characters of it. That is what the agent needs and a person scanning a list
8
+ * does not. So each row says one line here (`zh-CN` written by hand, for the skills AICUT ships), and anything without a
9
+ * line of its own falls back to the first sentence of its description — never to nothing.
10
+ *
11
+ * A skill fetched from a repository later has no line: nobody has read it yet. `firstSentence` still gives it a short
12
+ * line, in its own words, which is the honest thing to show.
13
+ */
14
+
15
+ /**
16
+ * Hand-written lines, by language, by skill id. Only the languages actually written appear: a language with no entry
17
+ * falls through to the description.
18
+ * @type {Record<string, Record<string, string>>}
19
+ */
20
+ export const SKILL_SUMMARIES = {
21
+ 'zh-CN': {
22
+ aicut: '做片、剪辑、检查、渲染,一条龙:写 film.html、逐段检查画面、导出成片,也能直接开 Studio。',
23
+ 'aicut-motion': '给片子做美术指导:配色、字体、真转场、按节拍排布;选题只有一句话时,先从 86 套归档风格挑三种摆成卡让你选。',
24
+ },
25
+ };
26
+
27
+ /** The first sentence of a text, short enough for one line: the fallback for a skill with no line of its own.
28
+ * @param {string} [text] @param {number} [limit] @returns {string} */
29
+ export function firstSentence(text, limit = 64) {
30
+ const one = String(text ?? '').replace(/\s+/g, ' ').trim();
31
+ if (!one) return '';
32
+ /* a sentence ends at a CJK full stop, or at a Western one that ends a word; `e.g.`, `i.e.` and `1.5` do not */
33
+ let at = -1;
34
+ const cjk = /[。!?]/.exec(one);
35
+ if (cjk) at = cjk.index;
36
+ const western = /[.!?](?=\s|$)/g;
37
+ let found;
38
+ while ((found = western.exec(one))) {
39
+ const tail = one.slice(Math.max(0, found.index - 3), found.index + 1);
40
+ if (/^[A-Za-z]\.[A-Za-z]\.$/.test(tail)) continue;
41
+ at = at < 0 ? found.index : Math.min(at, found.index);
42
+ break;
43
+ }
44
+ const cut = at >= 0 ? one.slice(0, at + 1) : one;
45
+ return cut.length > limit ? `${cut.slice(0, limit).trimEnd()}…` : cut;
46
+ }
47
+
48
+ /**
49
+ * What a skill says of itself, one line, in `lang` when a line was written: the hand-written one, else its own first
50
+ * sentence. @param {string} id @param {string} description @param {string} [lang] @returns {string}
51
+ */
52
+ export function summaryFor(id, description, lang = '') {
53
+ return SKILL_SUMMARIES[lang]?.[id] ?? firstSentence(description);
54
+ }
55
+
56
+ /** Whether a hand-written line exists for this skill in this language (tests and the Skills page's hint use it).
57
+ * @param {string} id @param {string} lang @returns {boolean} */
58
+ export function hasSummary(id, lang) {
59
+ return Boolean(SKILL_SUMMARIES[lang]?.[id]);
60
+ }
@@ -16,8 +16,9 @@ import { copyFileSync, existsSync, mkdirSync, readFileSync, readdirSync, rmSync,
16
16
  import { homedir } from 'node:os';
17
17
  import { basename, dirname, join, resolve, sep } from 'node:path';
18
18
  import { fileURLToPath } from 'node:url';
19
- import { downloadFiles, listFiles, resolveCommit } from './fetch-github.mjs';
19
+ import { downloadFiles, GITHUB_RAW, listFiles, rawUrl, resolveCommit, within } from './fetch-github.mjs';
20
20
  import { HttpError } from './http.mjs';
21
+ import { summaryFor } from './skill-summaries.mjs';
21
22
 
22
23
  /** Where a skill lands in a project: Pi's own folder for a project's skills (relative to the project). */
23
24
  export const SKILLS_IN_PROJECT = join('.pi', 'skills');
@@ -124,6 +125,16 @@ export function listSkills() {
124
125
  return skills;
125
126
  }
126
127
 
128
+ /**
129
+ * What the Skills page shows: every skill, each with one line of its own in `lang` (skill-summaries.mjs) — the
130
+ * hand-written line where there is one, else the first sentence of its description. `listSkills` stays the plain list,
131
+ * because that is what the rest of the server works with.
132
+ * @param {string} [lang] @returns {(Skill & { summary: string })[]}
133
+ */
134
+ export function listSkillsFor(lang) {
135
+ return listSkills().map((skill) => ({ ...skill, summary: summaryFor(skill.id, skill.description, lang ?? '') }));
136
+ }
137
+
127
138
  /** The folders in `root`, sorted. @param {string} root @returns {string[]} */
128
139
  function folders(root) {
129
140
  try {
@@ -213,6 +224,43 @@ export function installSkills(project, ids) {
213
224
  return { installed, missing };
214
225
  }
215
226
 
227
+ /**
228
+ * The skills the app is made of: `aicut` is the manual its agent reads in every project, and `aicut-motion` is the
229
+ * art direction every starter film follows (templates.mjs) — taking either away leaves a library that looks complete
230
+ * and is not, so they are the two the Skills page will not remove.
231
+ */
232
+ export const CORE_SKILLS = /** @type {const} */ (['aicut', 'aicut-motion']);
233
+
234
+ /**
235
+ * Remove a skill from the library: its folder goes from disk — from `~/.aicut/skill-library` for one fetched here, from
236
+ * the `skills/` beside the package for one the app brings. A project that already carries a copy under `.pi/skills`
237
+ * keeps it: that copy is the project's, and a film has to open the same way in a year.
238
+ *
239
+ * The one `CORE_SKILLS` is refused (409): the app is made of it, and a reinstall would only put it back.
240
+ * @param {string} id the skill's folder
241
+ * @returns {{ id: string, source: Skill['source'] }}
242
+ */
243
+ export function removeSkill(id) {
244
+ const skill = listSkills().find((one) => one.id === id);
245
+ if (!skill) throw new HttpError(404, 'no such skill');
246
+ if (CORE_SKILLS.includes(/** @type {any} */ (id))) throw new HttpError(409, `${id} is part of the app and cannot be removed`);
247
+ const root = skill.source === 'app' ? appSkillsFolder() : downloadedFolder();
248
+ if (!root) throw new HttpError(404, 'no such skill');
249
+ const dir = resolve(root, id);
250
+ /* whatever a request says, the folder removed is inside the library it came from */
251
+ if (!`${dir}${sep}`.startsWith(`${resolve(root)}${sep}`)) throw new HttpError(400, 'that is not a skill folder');
252
+ if (!existsSync(join(dir, 'SKILL.md'))) throw new HttpError(404, 'no such skill');
253
+ rmSync(dir, { recursive: true, force: true });
254
+ if (skill.source === 'github') {
255
+ const sources = readSources();
256
+ if (sources[id]) {
257
+ delete sources[id];
258
+ writeSources(sources);
259
+ }
260
+ }
261
+ return { id, source: skill.source };
262
+ }
263
+
216
264
  /* ── a skill from a GitHub repository (fetch-github.mjs: the API names the commit and lists the files, raw sends them) ── */
217
265
 
218
266
  /** Files bigger than this are not part of a skill (a skill is words; a picture in one is small). */
@@ -245,8 +293,9 @@ export function parseRepo(input) {
245
293
  }
246
294
 
247
295
  /**
248
- * The ways GitHub is asked, in order: itself, then the mirror that carries it (the same one the sounds come through) —
249
- * on a network where the API is not reachable, a fetch that only knows github.com is a fetch that never works.
296
+ * The ways GitHub is asked, in order: itself, then the mirrors that carry it (the ones the sounds come through) — on a
297
+ * network where GitHub is not reachable, a fetch that only knows github.com is a fetch that never works. A mirror may
298
+ * carry one of the two hosts and not the other, so each entry says what it can do and the callers pick their own.
250
299
  * `AICUT_SKILLS_API` and `AICUT_SKILLS_RAW` name hosts of one's own instead (a test's stub among them).
251
300
  * @returns {{ api?: string, raw?: string }[]}
252
301
  */
@@ -263,17 +312,78 @@ function githubHosts() {
263
312
  return [
264
313
  {},
265
314
  { api: 'https://gh-proxy.com/https://api.github.com', raw: 'https://gh-proxy.com/https://raw.githubusercontent.com' },
315
+ /* the file host only: the API of this one is not known to answer, and the listing needs no mirror once it is read */
316
+ { raw: 'https://ghfast.top/https://raw.githubusercontent.com' },
266
317
  ];
267
318
  }
268
319
 
320
+ /** How long a file host is given to show it is there, before the next one is asked. */
321
+ const PROBE_MS = 6000;
322
+
323
+ /**
324
+ * The file host the download will go to. The API and the files are different addresses, and which one listed the
325
+ * repository says nothing about which one can send the bytes: on some networks `api.github.com` answers while
326
+ * `raw.githubusercontent.com` never does. So each host is asked once for a byte of the skill's own `SKILL.md` — cheap,
327
+ * and the answer settles it — and the whole download then goes to the first that answers, the listing's host first.
328
+ * @param {{ owner: string, repo: string }} at @param {string} commit @param {string[]} folder
329
+ * @param {{ path: string, size: number }[]} files
330
+ * @param {{ signal?: AbortSignal, hosts: { api?: string, raw?: string } }} options
331
+ * @returns {Promise<{ api?: string, raw?: string }>}
332
+ */
333
+ async function rawThatAnswers(at, commit, folder, files, { signal, hosts }) {
334
+ const probe = files.find((file) => file.path === 'SKILL.md') ?? files[0];
335
+ if (!probe) return hosts;
336
+ const parts = [...folder, ...probe.path.split('/')];
337
+ /* the host that listed it first, then the rest — a host that says nothing means GitHub's own file host */
338
+ const asked = [hosts.raw, ...githubHosts().map((one) => one.raw)];
339
+ for (const raw of asked.filter((one, i) => asked.indexOf(one) === i)) {
340
+ try {
341
+ const res = await fetch(rawUrl(raw ?? GITHUB_RAW, at, commit, parts), {
342
+ headers: { range: 'bytes=0-0', 'user-agent': 'AICUT-Studio' },
343
+ signal: within(signal, PROBE_MS),
344
+ });
345
+ await res.body?.cancel();
346
+ if (res.ok) return { ...hosts, ...(raw ? { raw } : {}) };
347
+ } catch { /* nothing there: the next host is asked */ }
348
+ }
349
+ /* none of them answered: the download says so, in the words it already has */
350
+ return hosts;
351
+ }
352
+
353
+ /**
354
+ * A host that answered one byte may still fail the many — a mirror that throttles, a link that drops under four files
355
+ * at once — and which host answered the byte says nothing about which can send the rest. So a file host that fails on
356
+ * the network is passed over for the next, and only the network: a 404 or a 422 is the repository's answer and would be
357
+ * the next host's too.
358
+ * @param {{ owner: string, repo: string }} at @param {string} commit @param {string[]} folder
359
+ * @param {{ path: string, size: number }[]} files @param {string} stage
360
+ * @param {{ signal?: AbortSignal, hosts: { api?: string, raw?: string } }} options
361
+ */
362
+ async function downloadFromAnywhere(at, commit, folder, files, stage, { signal, hosts }) {
363
+ const asked = [hosts.raw, ...githubHosts().map((one) => one.raw)];
364
+ /** @type {unknown} */
365
+ let last = null;
366
+ for (const raw of asked.filter((one, i) => asked.indexOf(one) === i)) {
367
+ try {
368
+ await downloadFiles(at, commit, folder, files, { dir: stage, signal, hosts: { ...hosts, raw } });
369
+ return;
370
+ } catch (e) {
371
+ if (/** @type {{ code?: string }} */ (e)?.code !== 'network' || signal?.aborted) throw e;
372
+ last = e;
373
+ rmSync(stage, { recursive: true, force: true });
374
+ mkdirSync(stage, { recursive: true });
375
+ }
376
+ }
377
+ throw last ?? new HttpError(502, 'GitHub could not be reached');
378
+ }
379
+
269
380
  /**
270
381
  * Fetch a repository's skills into AICUT's home, one folder per skill (every folder holding a `SKILL.md`). They are then
271
382
  * offered like the app's own (`listSkills`), and a project can be made with them.
272
383
  * @param {string} input a repository, or a link to a folder in one
273
384
  * @param {{ signal?: AbortSignal }} [options]
274
385
  * @returns {Promise<{ skills: Skill[], commit: string }>}
275
- */
276
- export async function addSkillsFromGitHub(input, { signal } = {}) {
386
+ */export async function addSkillsFromGitHub(input, { signal } = {}) {
277
387
  const { owner, repo, folder } = parseRepo(input);
278
388
  const at = { owner, repo };
279
389
  /* the repository as it is now: the commit, and every file in it */
@@ -281,7 +391,9 @@ export async function addSkillsFromGitHub(input, { signal } = {}) {
281
391
  let read = null;
282
392
  /** @type {unknown} */
283
393
  let last = null;
284
- for (const hosts of githubHosts()) {
394
+ for (const [i, hosts] of githubHosts().entries()) {
395
+ /* an entry that only knows a file host has nothing to say about listing */
396
+ if (i > 0 && !hosts.api) continue;
285
397
  try {
286
398
  const { commit } = await resolveCommit(at, folder.length ? folder : null, false, { signal, hosts });
287
399
  read = { commit, files: await listFiles(at, commit, [], { signal, hosts }), hosts };
@@ -327,7 +439,9 @@ export async function addSkillsFromGitHub(input, { signal } = {}) {
327
439
  const stage = join(downloadedFolder(), `.fetch-${process.pid}-${Date.now()}`);
328
440
  try {
329
441
  rmSync(stage, { recursive: true, force: true });
330
- await downloadFiles(at, commit, dir ? dir.split('/') : [], inside, { dir: stage, signal, hosts });
442
+ const folderPath = dir ? dir.split('/') : [];
443
+ const send = await rawThatAnswers(at, commit, folderPath, inside, { signal, hosts });
444
+ await downloadFromAnywhere(at, commit, folderPath, inside, stage, { signal, hosts: send });
331
445
  const said = frontmatter(readFileSync(join(stage, 'SKILL.md'), 'utf8'));
332
446
  const id = skillId(said.name || basename(dir || repo));
333
447
  const to = join(downloadedFolder(), id);
@@ -0,0 +1,235 @@
1
+ // @ts-check
2
+ /**
3
+ * The styles a person added from a repository (`POST /api/styles/github`), kept beside them
4
+ * (`~/.aicut/style-library/styles.json`): each is a whole starter film like the built-in ones (templates.mjs), whose
5
+ * project carries the skill it came from — the skill the film's next editor follows. The page itself is generated here,
6
+ * in a palette chosen from the skill's id, so the card shows a real frame (template-posters.mjs) and a project made
7
+ * from it already draws; the skill is what says how the film should be restyled.
8
+ *
9
+ * Not `~/.aicut/skills`: that is the name the agent's own scanner walks. This is AICUT's list of style cards only.
10
+ */
11
+ import { mkdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
12
+ import { homedir } from 'node:os';
13
+ import { join } from 'node:path';
14
+ import { filmShell, motionMath, pageHead } from './templates-page.mjs';
15
+
16
+ /** Where the list lives. */
17
+ const stylesFile = () => join(studioHome(), 'style-library', 'styles.json');
18
+
19
+ /** The home this list sits in (projects.mjs's, so a test's AICUT_HOME keeps it apart too). */
20
+ const studioHome = () => process.env.AICUT_HOME || join(homedir(), '.aicut');
21
+
22
+ /** What a style says of itself on disk. @typedef {{ id: string, title: string, hint: string, skill: string,
23
+ * repo: string, seconds: number, width: number, height: number, at: string }} StyleEntry */
24
+
25
+ /** The styles this person added, oldest last. @returns {StyleEntry[]} */
26
+ export function userStyles() {
27
+ try {
28
+ const list = JSON.parse(readFileSync(stylesFile(), 'utf8'));
29
+ return Array.isArray(list) ? list.filter((one) => one && typeof one.id === 'string' && typeof one.skill === 'string') : [];
30
+ } catch {
31
+ return [];
32
+ }
33
+ }
34
+
35
+ /** The folder-name part of a style's id: lowercase letters, digits and hyphens, short. */
36
+ const slug = (/** @type {string} */ text) => text.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, '').slice(0, 40).replace(/-+$/g, '') || 'style';
37
+
38
+ /**
39
+ * Register what a repository's skills became (`skills.mjs` fetched them): one style per skill, the skill's own name and
40
+ * words on the card. A skill already a style of is replaced — fetching it again is how its card is refreshed.
41
+ * @param {{ id: string, name?: string, description?: string, from?: string }[]} skills
42
+ * @returns {StyleEntry[]} the entries now in the list, the new ones last
43
+ */
44
+ export function addUserStyles(skills) {
45
+ const kept = userStyles().filter((one) => !skills.some((skill) => one.skill === skill.id));
46
+ const now = new Date().toISOString();
47
+ const added = skills.map((skill) => ({
48
+ id: slug(`gh-${skill.id}`),
49
+ title: skill.name || skill.id,
50
+ hint: skill.description || '',
51
+ skill: skill.id,
52
+ repo: skill.from || '',
53
+ seconds: 6,
54
+ width: 1920,
55
+ height: 1080,
56
+ at: now,
57
+ }));
58
+ const list = [...kept, ...added];
59
+ writeList(list);
60
+ return list;
61
+ }
62
+
63
+ /** A style gone from the list (its skill stays in the skill library). @param {string} id @returns {boolean} */
64
+ export function removeUserStyle(id) {
65
+ const list = userStyles();
66
+ const rest = list.filter((one) => one.id !== id);
67
+ if (rest.length === list.length) return false;
68
+ writeList(rest);
69
+ rmSync(join(studioHome(), 'template-covers', id), { recursive: true, force: true });
70
+ return true;
71
+ }
72
+
73
+ /** @param {StyleEntry[]} list */
74
+ function writeList(list) {
75
+ mkdirSync(join(studioHome(), 'style-library'), { recursive: true });
76
+ writeFileSync(stylesFile(), `${JSON.stringify(list, null, 2)}\n`);
77
+ }
78
+
79
+ /** A colour and a composition chosen from a skill's id: the same skill lands on the same palette every time. */
80
+ const HUES = [14, 42, 96, 152, 188, 214, 262, 292, 328];
81
+ const hashOf = (/** @type {string} */ id) => {
82
+ let hash = 0;
83
+ for (let i = 0; i < id.length; i++) hash = (hash * 31 + id.charCodeAt(i)) >>> 0;
84
+ return hash;
85
+ };
86
+ const hueOf = (/** @type {string} */ id) => HUES[hashOf(id) % HUES.length];
87
+
88
+ /** The three compositions a style card can be drawn in, picked by the same hash as its colour. */
89
+ const VARIANTS = /** @type {const} */ (['rings', 'bars', 'grid']);
90
+
91
+ /**
92
+ * How big the name is set: a name is one line, and one line has to fit the column whatever it says (a 4-character
93
+ * Chinese name and a 22-character id are not the same picture). Counted in ems — a CJK character is one, a Latin one
94
+ * about half.
95
+ */
96
+ function nameSize(/** @type {string} */ title) {
97
+ let ems = 0;
98
+ for (const ch of String(title ?? '')) ems += /[\u2e80-\uffff]/.test(ch) ? 1 : /[A-Z]/.test(ch) ? 0.62 : 0.53;
99
+ return Math.max(44, Math.min(116, Math.round(920 / Math.max(3, ems))));
100
+ }
101
+
102
+ /**
103
+ * The art a card is drawn with: one shape language per variant, all of it in the skill's own hue. Every element is in
104
+ * place at t=0 (nothing is grown from nothing), because the cover is a frame of this page at its first moment.
105
+ * @param {string} variant @param {number} hue @returns {string}
106
+ */
107
+ function artMarkup(variant, hue) {
108
+ if (variant === 'rings') {
109
+ return `<svg class="art" id="art" viewBox="0 0 620 620" width="620" height="620" fill="none">
110
+ <circle id="a1" cx="310" cy="310" r="252" stroke="hsl(${hue} 62% 62%)" stroke-width="2" stroke-dasharray="3 16" opacity="0.9"/>
111
+ <circle id="a2" cx="310" cy="310" r="184" stroke="hsl(${hue} 70% 64%)" stroke-width="2" opacity="0.55"/>
112
+ <circle id="a3" cx="310" cy="310" r="104" fill="hsl(${hue} 74% 56%)"/>
113
+ <line id="a4" x1="310" y1="18" x2="310" y2="602" stroke="hsl(${hue} 55% 78%)" stroke-width="1" opacity="0.4"/>
114
+ </svg>`;
115
+ }
116
+ if (variant === 'bars') {
117
+ const heights = [168, 268, 372, 470, 330, 214];
118
+ const bars = heights.map((h, i) =>
119
+ `<rect id="b${i}" x="${28 + i * 96}" y="${596 - h}" width="60" height="${h}" fill="hsl(${hue} ${i === 3 ? 76 : 58}% ${i === 3 ? 60 : 44}%)"/>`).join('\n ');
120
+ return `<svg class="art" id="art" viewBox="0 0 620 620" width="620" height="620">
121
+ ${bars}
122
+ <line x1="16" y1="596" x2="604" y2="596" stroke="hsl(${hue} 40% 70%)" stroke-width="1.5" opacity="0.5"/>
123
+ </svg>`;
124
+ }
125
+ const lines = [];
126
+ for (let i = 0; i <= 6; i++) {
127
+ lines.push(`<line x1="${24 + i * 95}" y1="16" x2="${24 + i * 95}" y2="604" stroke="hsl(${hue} 40% 72%)" stroke-width="1" opacity="0.28"/>`);
128
+ lines.push(`<line x1="24" y1="${16 + i * 98}" x2="594" y2="${16 + i * 98}" stroke="hsl(${hue} 40% 72%)" stroke-width="1" opacity="0.28"/>`);
129
+ }
130
+ const cells = [[1, 1], [3, 1], [4, 2], [2, 3], [5, 4]]
131
+ .map(([col, row], i) => `<rect id="c${i}" x="${29 + col * 95}" y="${21 + row * 98}" width="86" height="88" fill="hsl(${hue} ${70 + i * 2}% ${58 - i * 3}%)" opacity="${0.9 - i * 0.09}"/>`)
132
+ .join('\n ');
133
+ return `<svg class="art" id="art" viewBox="0 0 620 620" width="620" height="620">
134
+ <g id="lattice">
135
+ ${lines.join('\n ')}
136
+ </g>
137
+ ${cells}
138
+ </svg>`;
139
+ }
140
+
141
+ /**
142
+ * The starter film a style's project is made of: one page that already draws in a palette and a composition the style
143
+ * owns, and the film's shell around it. It is a real still — a ground with a light in it, a shape language in the
144
+ * accent colour, the name set to the column, a rule and a foot — not a coloured band with a label: a card whose picture
145
+ * is this page's first frame has to look like the first frame of a film somebody could have made. What the look *means*
146
+ * — what to restyle it into — is the skill the project carries.
147
+ * @param {StyleEntry} entry
148
+ * @returns {{ 'film.html': string, [page: string]: string }}
149
+ */
150
+ export function styleFiles(entry) {
151
+ const hue = hueOf(entry.id);
152
+ const variant = VARIANTS[hashOf(entry.id) % VARIANTS.length];
153
+ const size = nameSize(entry.title);
154
+ const page = `${pageHead(escapeHtml(entry.title))}
155
+ <style>
156
+ body { background: #0A0B0D; color: hsl(${hue} 30% 96%); }
157
+ .ground { position: absolute; inset: 0; background:
158
+ radial-gradient(760px 760px at 76% 44%, hsl(${hue} 52% 26% / 0.85), transparent 66%),
159
+ radial-gradient(900px 620px at 6% 96%, hsl(${hue} 62% 40% / 0.22), transparent 62%),
160
+ linear-gradient(158deg, hsl(${hue} 22% 13%), hsl(${hue} 26% 7%) 72%); }
161
+ .art { position: absolute; right: 108px; top: 50%; margin-top: -310px; }
162
+ .copy { position: absolute; left: 118px; top: 50%; margin-top: -168px; width: 900px; }
163
+ .kicker { margin: 0 0 26px; font-size: 21px; font-weight: 600; letter-spacing: 0.36em; text-transform: uppercase;
164
+ color: hsl(${hue} 44% 72%); }
165
+ .name { margin: 0; font-size: ${size}px; line-height: 0.98; font-weight: 800; letter-spacing: -0.025em;
166
+ color: hsl(${hue} 26% 98%); word-break: break-word; }
167
+ .rule { height: 3px; width: 268px; margin: 36px 0 0; background: hsl(${hue} 76% 58%); transform-origin: left center; }
168
+ .foot { position: absolute; left: 120px; right: 120px; bottom: 62px; display: flex; justify-content: flex-end;
169
+ font-size: 18px; font-weight: 600; letter-spacing: 0.24em; color: hsl(${hue} 18% 56%); }
170
+ .bar { position: absolute; left: 0; bottom: 0; width: 100%; height: 6px; transform-origin: left center;
171
+ background: hsl(${hue} 76% 58%); }
172
+ </style>
173
+ </head>
174
+ <body>
175
+ <div class="ground"></div>
176
+ ${artMarkup(variant, hue)}
177
+ <div class="copy">
178
+ <p class="kicker" id="kicker">${escapeHtml(entry.repo || 'AICUT')}</p>
179
+ <h1 class="name" id="name">${escapeHtml(entry.title)}</h1>
180
+ <div class="rule" id="rule"></div>
181
+ </div>
182
+ <div class="foot"><span id="count">${entry.seconds}s</span></div>
183
+ <div class="bar" id="bar"></div>
184
+ <script>
185
+ var DUR = ${entry.seconds};
186
+ var VARIANT = ${JSON.stringify(variant)};
187
+ ${motionMath}
188
+ /* Nothing here hides at t=0: a cover is this page's first frame, and a project opens on it. What moves is the shape
189
+ language (a slow turn, a scale, a stagger), the name's rise of a few pixels, and the hairline at the foot. */
190
+ function frame(t) {
191
+ var p = prog(t, 0, DUR, EASE.linear);
192
+ el('bar').style.transform = 'scaleX(' + p.toFixed(4) + ')';
193
+ el('rule').style.transform = 'scaleX(' + mix(0.62, 1, prog(t, 0.15, 1.5, EASE.out)).toFixed(3) + ')';
194
+ el('name').style.transform = 'translateY(' + mix(14, 0, prog(t, 0.1, 1.6, EASE.out)).toFixed(2) + 'px)';
195
+ el('kicker').style.opacity = String(mix(0.7, 1, prog(t, 0.4, 1.5, EASE.smooth)));
196
+ if (VARIANT === 'rings') {
197
+ el('a1').style.transform = 'rotate(' + (t * 9).toFixed(2) + 'deg)';
198
+ el('a1').style.transformOrigin = '310px 310px';
199
+ el('a3').style.transform = 'scale(' + mix(0.9, 1.06, prog(t, 0, DUR, EASE.inOut)).toFixed(4) + ')';
200
+ el('a3').style.transformOrigin = '310px 310px';
201
+ el('a4').style.transform = 'scaleY(' + mix(0.55, 1, prog(t, 0.2, 1.8, EASE.out)).toFixed(3) + ')';
202
+ el('a4').style.transformOrigin = '310px 310px';
203
+ }
204
+ if (VARIANT === 'bars') {
205
+ for (var i = 0; i < 6; i++) {
206
+ var one = el('b' + i);
207
+ one.style.transform = 'scaleY(' + mix(0.58, 1, prog(t, i * 0.09, 1.5, EASE.out)).toFixed(3) + ')';
208
+ one.style.transformOrigin = '0px 596px';
209
+ }
210
+ }
211
+ if (VARIANT === 'grid') {
212
+ el('lattice').style.opacity = String(mix(0.55, 1, prog(t, 0, 1.4, EASE.smooth)));
213
+ for (var c = 0; c < 5; c++) {
214
+ var cell = el('c' + c);
215
+ cell.style.transform = 'scale(' + mix(0.86, 1, prog(t, 0.12 + c * 0.11, 0.9, EASE.snap)).toFixed(3) + ')';
216
+ cell.style.transformOrigin = 'center';
217
+ }
218
+ }
219
+ }
220
+ window.film = { duration: DUR, frame: frame, ready: Promise.resolve() };
221
+ </script>
222
+ </body>
223
+ </html>
224
+ `;
225
+ return {
226
+ 'film.html': filmShell(escapeHtml(entry.title), entry.width, entry.height, entry.id, entry.seconds),
227
+ [`pages/${entry.id}.html`]: page,
228
+ };
229
+ }
230
+
231
+ /** Text that comes from a repository goes into a page: a name with `<` in it would be markup there, not a name.
232
+ * @param {string} text */
233
+ function escapeHtml(text) {
234
+ return String(text ?? '').replace(/[&<>"']/g, (c) => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' }[c] ?? c));
235
+ }
@@ -17,7 +17,7 @@ import { dirname, join } from 'node:path';
17
17
  import { writeAtomic } from './atomic.mjs';
18
18
  import { pageFrame } from './pageframes.mjs';
19
19
  import { studioHome } from './projects.mjs';
20
- import { TEMPLATES } from './templates.mjs';
20
+ import { templateById } from './templates.mjs';
21
21
 
22
22
  /** The width a cover is drawn at: a card's picture at twice its size. */
23
23
  const COVER_W = 480;
@@ -51,7 +51,7 @@ async function folderOf(template) {
51
51
  * @returns {Promise<Buffer | null>}
52
52
  */
53
53
  export async function templateCover(id, pages) {
54
- const template = TEMPLATES.find((t) => t.id === id);
54
+ const template = templateById(id);
55
55
  if (!template) return null;
56
56
  let filling_ = filling.get(id);
57
57
  if (!filling_) {