@1agh/maude 0.42.0 → 0.44.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 (332) hide show
  1. package/apps/studio/.ai/cache/_stats.json +7 -0
  2. package/apps/studio/acp/bootstrap-brief.ts +12 -2
  3. package/apps/studio/acp/bridge.ts +44 -10
  4. package/apps/studio/acp/env.ts +23 -0
  5. package/apps/studio/acp/login-state.ts +263 -0
  6. package/apps/studio/acp/plugin-bootstrap.ts +31 -25
  7. package/apps/studio/acp/probe.ts +152 -1
  8. package/apps/studio/annotations-layer.tsx +1451 -324
  9. package/apps/studio/annotations-model.ts +58 -0
  10. package/apps/studio/api.ts +568 -24
  11. package/apps/studio/bin/_audio-search.mjs +140 -0
  12. package/apps/studio/bin/_import-asset-pdf-worker.mjs +34 -0
  13. package/apps/studio/bin/_import-asset.mjs +815 -0
  14. package/apps/studio/bin/_import-brand.mjs +635 -0
  15. package/apps/studio/bin/_import-tokens-alias-resolver.mjs +95 -0
  16. package/apps/studio/bin/_import-tokens.mjs +1201 -0
  17. package/apps/studio/bin/_ingest-footage.mjs +386 -0
  18. package/apps/studio/bin/_probe-footage-playwright.mjs +269 -0
  19. package/apps/studio/bin/_transcribe.mjs +419 -0
  20. package/apps/studio/bin/_transcribe.test.mjs +80 -0
  21. package/apps/studio/bin/_video-playwright.mjs +141 -71
  22. package/apps/studio/bin/audio-search.sh +28 -0
  23. package/apps/studio/bin/generate.sh +154 -0
  24. package/apps/studio/bin/import-asset.sh +34 -0
  25. package/apps/studio/bin/import-brand.sh +29 -0
  26. package/apps/studio/bin/import-tokens.sh +33 -0
  27. package/apps/studio/bin/ingest-footage.sh +27 -0
  28. package/apps/studio/bin/photo-adjust.sh +163 -0
  29. package/apps/studio/bin/photo-bg-remove.sh +258 -0
  30. package/apps/studio/bin/probe-footage.sh +28 -0
  31. package/apps/studio/bin/read-annotations.mjs +125 -2
  32. package/apps/studio/bin/transcribe.sh +39 -0
  33. package/apps/studio/canvas-edit.ts +656 -12
  34. package/apps/studio/canvas-lib.tsx +637 -10
  35. package/apps/studio/canvas-pipeline.ts +65 -7
  36. package/apps/studio/canvas-shell.tsx +156 -16
  37. package/apps/studio/client/app.jsx +2385 -699
  38. package/apps/studio/client/export-center.jsx +35 -1
  39. package/apps/studio/client/generate-dialog.jsx +352 -0
  40. package/apps/studio/client/github.js +18 -0
  41. package/apps/studio/client/inspector-controls.jsx +781 -0
  42. package/apps/studio/client/panels/BrandUploadPanel.jsx +233 -0
  43. package/apps/studio/client/panels/ChatPanel.jsx +15 -33
  44. package/apps/studio/client/panels/GitPanel.jsx +8 -0
  45. package/apps/studio/client/panels/IntroVideoDialog.jsx +44 -0
  46. package/apps/studio/client/panels/OnboardingWizard.jsx +12 -0
  47. package/apps/studio/client/panels/ReadinessList.jsx +227 -7
  48. package/apps/studio/client/panels/RepoBranchSwitcher.jsx +133 -21
  49. package/apps/studio/client/panels/SettingsPanel.jsx +828 -0
  50. package/apps/studio/client/panels/SetupChecklist.jsx +223 -0
  51. package/apps/studio/client/panels/StickerPicker.jsx +160 -0
  52. package/apps/studio/client/panels/TimelinePanel.jsx +15 -2
  53. package/apps/studio/client/photo-knobs.jsx +432 -0
  54. package/apps/studio/client/styles/3-shell-maude.css +376 -28
  55. package/apps/studio/client/styles/4-components.css +223 -0
  56. package/apps/studio/client/styles/6-acp-chat.css +86 -0
  57. package/apps/studio/client/tour/quick-setup-tour.js +39 -0
  58. package/apps/studio/commands/annotation-strokes-command.ts +13 -3
  59. package/apps/studio/commands/edit-source-command.ts +31 -4
  60. package/apps/studio/config.schema.json +70 -0
  61. package/apps/studio/context-menu.tsx +42 -8
  62. package/apps/studio/context.ts +16 -0
  63. package/apps/studio/design-setup-readiness.ts +115 -0
  64. package/apps/studio/dist/client.bundle.js +5572 -21
  65. package/apps/studio/dist/comment-mount.js +2 -2
  66. package/apps/studio/dist/runtime/.min-sizes.json +1 -0
  67. package/apps/studio/dist/runtime/@imgly_background-removal.js +5543 -0
  68. package/apps/studio/dist/runtime/pixi-js.js +519 -519
  69. package/apps/studio/dist/styles.css +1 -1
  70. package/apps/studio/dom-selection.ts +25 -0
  71. package/apps/studio/draw/palette.ts +34 -0
  72. package/apps/studio/exporters/jobs.ts +47 -5
  73. package/apps/studio/exporters/video.ts +24 -4
  74. package/apps/studio/footage/schema.test.ts +287 -0
  75. package/apps/studio/footage/schema.ts +662 -0
  76. package/apps/studio/footage-store.ts +235 -0
  77. package/apps/studio/fs-watch.ts +10 -0
  78. package/apps/studio/generation/adapters/elevenlabs.ts +370 -0
  79. package/apps/studio/generation/adapters/gemini.ts +542 -0
  80. package/apps/studio/generation/adapters/groq.test.ts +80 -0
  81. package/apps/studio/generation/adapters/groq.ts +247 -0
  82. package/apps/studio/generation/audio-library.test.ts +95 -0
  83. package/apps/studio/generation/audio-library.ts +156 -0
  84. package/apps/studio/generation/captions.test.ts +78 -0
  85. package/apps/studio/generation/captions.ts +132 -0
  86. package/apps/studio/generation/download.ts +76 -0
  87. package/apps/studio/generation/elevenlabs.test.ts +223 -0
  88. package/apps/studio/generation/gemini.test.ts +350 -0
  89. package/apps/studio/generation/jobs.test.ts +128 -0
  90. package/apps/studio/generation/jobs.ts +243 -0
  91. package/apps/studio/generation/keys.test.ts +75 -0
  92. package/apps/studio/generation/keys.ts +162 -0
  93. package/apps/studio/generation/prefs.test.ts +95 -0
  94. package/apps/studio/generation/prefs.ts +82 -0
  95. package/apps/studio/generation/registry.ts +88 -0
  96. package/apps/studio/generation/types.test.ts +123 -0
  97. package/apps/studio/generation/types.ts +266 -0
  98. package/apps/studio/generation/whisper-models.test.ts +152 -0
  99. package/apps/studio/generation/whisper-models.ts +257 -0
  100. package/apps/studio/generation/whisper-spike-results.md +61 -0
  101. package/apps/studio/git/endpoints.ts +68 -7
  102. package/apps/studio/git/service.ts +199 -21
  103. package/apps/studio/github/service.ts +67 -0
  104. package/apps/studio/handoff.ts +3 -2
  105. package/apps/studio/http.ts +1039 -22
  106. package/apps/studio/input-router.tsx +10 -0
  107. package/apps/studio/inspect.ts +53 -1
  108. package/apps/studio/media/intro.mp4 +0 -0
  109. package/apps/studio/media-commit-chain.ts +119 -0
  110. package/apps/studio/paths.ts +20 -0
  111. package/apps/studio/photo/filters.ts +285 -0
  112. package/apps/studio/photo/pipeline.ts +607 -0
  113. package/apps/studio/photo/schema.ts +367 -0
  114. package/apps/studio/photo-store.ts +154 -0
  115. package/apps/studio/readiness.ts +108 -20
  116. package/apps/studio/runtime-bundle.ts +29 -6
  117. package/apps/studio/scaffold-design.ts +1145 -0
  118. package/apps/studio/server.ts +18 -2
  119. package/apps/studio/stickers/figjam-doodle/manifest.json +104 -0
  120. package/apps/studio/stickers/figjam-doodle/slice1.png +0 -0
  121. package/apps/studio/stickers/figjam-doodle/slice10.png +0 -0
  122. package/apps/studio/stickers/figjam-doodle/slice11.png +0 -0
  123. package/apps/studio/stickers/figjam-doodle/slice12.png +0 -0
  124. package/apps/studio/stickers/figjam-doodle/slice13.png +0 -0
  125. package/apps/studio/stickers/figjam-doodle/slice14.png +0 -0
  126. package/apps/studio/stickers/figjam-doodle/slice15.png +0 -0
  127. package/apps/studio/stickers/figjam-doodle/slice16.png +0 -0
  128. package/apps/studio/stickers/figjam-doodle/slice17.png +0 -0
  129. package/apps/studio/stickers/figjam-doodle/slice18.png +0 -0
  130. package/apps/studio/stickers/figjam-doodle/slice19.png +0 -0
  131. package/apps/studio/stickers/figjam-doodle/slice2.png +0 -0
  132. package/apps/studio/stickers/figjam-doodle/slice20.png +0 -0
  133. package/apps/studio/stickers/figjam-doodle/slice21.png +0 -0
  134. package/apps/studio/stickers/figjam-doodle/slice22.png +0 -0
  135. package/apps/studio/stickers/figjam-doodle/slice23.png +0 -0
  136. package/apps/studio/stickers/figjam-doodle/slice24.png +0 -0
  137. package/apps/studio/stickers/figjam-doodle/slice3.png +0 -0
  138. package/apps/studio/stickers/figjam-doodle/slice4.png +0 -0
  139. package/apps/studio/stickers/figjam-doodle/slice5.png +0 -0
  140. package/apps/studio/stickers/figjam-doodle/slice6.png +0 -0
  141. package/apps/studio/stickers/figjam-doodle/slice7.png +0 -0
  142. package/apps/studio/stickers/figjam-doodle/slice8.png +0 -0
  143. package/apps/studio/stickers/figjam-doodle/slice9.png +0 -0
  144. package/apps/studio/stickers/life-style/best.png +0 -0
  145. package/apps/studio/stickers/life-style/come-on.png +0 -0
  146. package/apps/studio/stickers/life-style/cut.png +0 -0
  147. package/apps/studio/stickers/life-style/detox.png +0 -0
  148. package/apps/studio/stickers/life-style/just.png +0 -0
  149. package/apps/studio/stickers/life-style/manifest.json +68 -0
  150. package/apps/studio/stickers/life-style/mirror.png +0 -0
  151. package/apps/studio/stickers/life-style/nice.png +0 -0
  152. package/apps/studio/stickers/life-style/objects.png +0 -0
  153. package/apps/studio/stickers/life-style/ok.png +0 -0
  154. package/apps/studio/stickers/life-style/queen.png +0 -0
  155. package/apps/studio/stickers/life-style/star.png +0 -0
  156. package/apps/studio/stickers/life-style/sun.png +0 -0
  157. package/apps/studio/stickers/life-style/water.png +0 -0
  158. package/apps/studio/stickers/life-style/yeah.png +0 -0
  159. package/apps/studio/stickers/life-style/you-can.png +0 -0
  160. package/apps/studio/stickers/opposing-thoughts/check-the-deets.png +0 -0
  161. package/apps/studio/stickers/opposing-thoughts/cut-it.png +0 -0
  162. package/apps/studio/stickers/opposing-thoughts/dope.png +0 -0
  163. package/apps/studio/stickers/opposing-thoughts/fresh.png +0 -0
  164. package/apps/studio/stickers/opposing-thoughts/hot.png +0 -0
  165. package/apps/studio/stickers/opposing-thoughts/idea.png +0 -0
  166. package/apps/studio/stickers/opposing-thoughts/keep-exploring.png +0 -0
  167. package/apps/studio/stickers/opposing-thoughts/killed-it.png +0 -0
  168. package/apps/studio/stickers/opposing-thoughts/love-it.png +0 -0
  169. package/apps/studio/stickers/opposing-thoughts/manifest.json +88 -0
  170. package/apps/studio/stickers/opposing-thoughts/not-sure.png +0 -0
  171. package/apps/studio/stickers/opposing-thoughts/ok.png +0 -0
  172. package/apps/studio/stickers/opposing-thoughts/pure-gold.png +0 -0
  173. package/apps/studio/stickers/opposing-thoughts/save-for-later.png +0 -0
  174. package/apps/studio/stickers/opposing-thoughts/steal-this.png +0 -0
  175. package/apps/studio/stickers/opposing-thoughts/take-a-peek.png +0 -0
  176. package/apps/studio/stickers/opposing-thoughts/this-or-that.png +0 -0
  177. package/apps/studio/stickers/opposing-thoughts/thoughts.png +0 -0
  178. package/apps/studio/stickers/opposing-thoughts/vibes.png +0 -0
  179. package/apps/studio/stickers/opposing-thoughts/winner.png +0 -0
  180. package/apps/studio/stickers/opposing-thoughts/wip.png +0 -0
  181. package/apps/studio/stickers/project-status/group-100.png +0 -0
  182. package/apps/studio/stickers/project-status/group-101.png +0 -0
  183. package/apps/studio/stickers/project-status/group-102.png +0 -0
  184. package/apps/studio/stickers/project-status/group-103.png +0 -0
  185. package/apps/studio/stickers/project-status/group-104.png +0 -0
  186. package/apps/studio/stickers/project-status/group-105.png +0 -0
  187. package/apps/studio/stickers/project-status/group-106.png +0 -0
  188. package/apps/studio/stickers/project-status/group-107.png +0 -0
  189. package/apps/studio/stickers/project-status/group-108.png +0 -0
  190. package/apps/studio/stickers/project-status/group-109.png +0 -0
  191. package/apps/studio/stickers/project-status/group-110.png +0 -0
  192. package/apps/studio/stickers/project-status/group-111.png +0 -0
  193. package/apps/studio/stickers/project-status/group-112.png +0 -0
  194. package/apps/studio/stickers/project-status/group-113.png +0 -0
  195. package/apps/studio/stickers/project-status/group-114.png +0 -0
  196. package/apps/studio/stickers/project-status/group-115.png +0 -0
  197. package/apps/studio/stickers/project-status/group-117.png +0 -0
  198. package/apps/studio/stickers/project-status/group-118.png +0 -0
  199. package/apps/studio/stickers/project-status/group-119.png +0 -0
  200. package/apps/studio/stickers/project-status/group-120.png +0 -0
  201. package/apps/studio/stickers/project-status/group-121.png +0 -0
  202. package/apps/studio/stickers/project-status/group-122.png +0 -0
  203. package/apps/studio/stickers/project-status/group-41.png +0 -0
  204. package/apps/studio/stickers/project-status/group-42.png +0 -0
  205. package/apps/studio/stickers/project-status/group-43.png +0 -0
  206. package/apps/studio/stickers/project-status/group-44.png +0 -0
  207. package/apps/studio/stickers/project-status/group-45.png +0 -0
  208. package/apps/studio/stickers/project-status/group-46.png +0 -0
  209. package/apps/studio/stickers/project-status/group-47.png +0 -0
  210. package/apps/studio/stickers/project-status/group-48.png +0 -0
  211. package/apps/studio/stickers/project-status/group-49.png +0 -0
  212. package/apps/studio/stickers/project-status/group-50.png +0 -0
  213. package/apps/studio/stickers/project-status/group-51.png +0 -0
  214. package/apps/studio/stickers/project-status/group-52.png +0 -0
  215. package/apps/studio/stickers/project-status/group-53.png +0 -0
  216. package/apps/studio/stickers/project-status/group-54.png +0 -0
  217. package/apps/studio/stickers/project-status/group-55.png +0 -0
  218. package/apps/studio/stickers/project-status/group-56.png +0 -0
  219. package/apps/studio/stickers/project-status/group-57.png +0 -0
  220. package/apps/studio/stickers/project-status/group-58.png +0 -0
  221. package/apps/studio/stickers/project-status/group-59.png +0 -0
  222. package/apps/studio/stickers/project-status/group-60.png +0 -0
  223. package/apps/studio/stickers/project-status/group-61.png +0 -0
  224. package/apps/studio/stickers/project-status/group-62.png +0 -0
  225. package/apps/studio/stickers/project-status/group-63.png +0 -0
  226. package/apps/studio/stickers/project-status/group-64.png +0 -0
  227. package/apps/studio/stickers/project-status/group-65.png +0 -0
  228. package/apps/studio/stickers/project-status/group-66.png +0 -0
  229. package/apps/studio/stickers/project-status/group-67.png +0 -0
  230. package/apps/studio/stickers/project-status/group-68.png +0 -0
  231. package/apps/studio/stickers/project-status/group-69.png +0 -0
  232. package/apps/studio/stickers/project-status/group-70.png +0 -0
  233. package/apps/studio/stickers/project-status/group-71.png +0 -0
  234. package/apps/studio/stickers/project-status/group-72.png +0 -0
  235. package/apps/studio/stickers/project-status/group-73.png +0 -0
  236. package/apps/studio/stickers/project-status/group-74.png +0 -0
  237. package/apps/studio/stickers/project-status/group-75.png +0 -0
  238. package/apps/studio/stickers/project-status/group-76.png +0 -0
  239. package/apps/studio/stickers/project-status/group-77.png +0 -0
  240. package/apps/studio/stickers/project-status/group-78.png +0 -0
  241. package/apps/studio/stickers/project-status/group-79.png +0 -0
  242. package/apps/studio/stickers/project-status/group-80.png +0 -0
  243. package/apps/studio/stickers/project-status/group-81.png +0 -0
  244. package/apps/studio/stickers/project-status/group-82.png +0 -0
  245. package/apps/studio/stickers/project-status/group-83.png +0 -0
  246. package/apps/studio/stickers/project-status/group-84.png +0 -0
  247. package/apps/studio/stickers/project-status/group-85.png +0 -0
  248. package/apps/studio/stickers/project-status/group-86.png +0 -0
  249. package/apps/studio/stickers/project-status/group-87.png +0 -0
  250. package/apps/studio/stickers/project-status/group-88.png +0 -0
  251. package/apps/studio/stickers/project-status/group-89.png +0 -0
  252. package/apps/studio/stickers/project-status/group-90.png +0 -0
  253. package/apps/studio/stickers/project-status/group-91.png +0 -0
  254. package/apps/studio/stickers/project-status/group-92.png +0 -0
  255. package/apps/studio/stickers/project-status/group-93.png +0 -0
  256. package/apps/studio/stickers/project-status/group-94.png +0 -0
  257. package/apps/studio/stickers/project-status/group-96.png +0 -0
  258. package/apps/studio/stickers/project-status/group-97.png +0 -0
  259. package/apps/studio/stickers/project-status/group-98.png +0 -0
  260. package/apps/studio/stickers/project-status/group-99.png +0 -0
  261. package/apps/studio/stickers/project-status/manifest.json +328 -0
  262. package/apps/studio/test/_helpers.ts +15 -2
  263. package/apps/studio/test/acp-bootstrap-brief.test.ts +11 -0
  264. package/apps/studio/test/acp-bridge.test.ts +34 -1
  265. package/apps/studio/test/acp-commands.test.ts +2 -1
  266. package/apps/studio/test/acp-env.test.ts +30 -0
  267. package/apps/studio/test/acp-plugin-bootstrap.test.ts +23 -57
  268. package/apps/studio/test/acp-session-plugins.test.ts +72 -1
  269. package/apps/studio/test/annotation-strokes-command.test.ts +17 -0
  270. package/apps/studio/test/annotations-layer.test.ts +174 -0
  271. package/apps/studio/test/asset-api.test.ts +3 -3
  272. package/apps/studio/test/canvas-edit.test.ts +17 -6
  273. package/apps/studio/test/canvas-origin-gate.test.ts +50 -0
  274. package/apps/studio/test/csp-canvas-shell.test.ts +11 -0
  275. package/apps/studio/test/csrf-write-guard.test.ts +23 -1
  276. package/apps/studio/test/design-setup-readiness.test.ts +155 -0
  277. package/apps/studio/test/dynamic-text-edit.test.ts +162 -0
  278. package/apps/studio/test/edit-source-command.test.ts +23 -0
  279. package/apps/studio/test/element-structural-edit.test.ts +154 -0
  280. package/apps/studio/test/fixtures/fake-claude-auth.mjs +13 -0
  281. package/apps/studio/test/footage-store.test.ts +100 -0
  282. package/apps/studio/test/generate-route.test.ts +106 -0
  283. package/apps/studio/test/git-branches.test.ts +97 -0
  284. package/apps/studio/test/github-api.test.ts +56 -1
  285. package/apps/studio/test/import-asset-browser.test.ts +64 -0
  286. package/apps/studio/test/import-asset-route.test.ts +71 -0
  287. package/apps/studio/test/import-asset.test.ts +399 -0
  288. package/apps/studio/test/import-brand-browser.test.ts +88 -0
  289. package/apps/studio/test/import-brand-route.test.ts +120 -0
  290. package/apps/studio/test/import-brand.test.ts +206 -0
  291. package/apps/studio/test/import-tokens.test.ts +722 -0
  292. package/apps/studio/test/input-router.test.ts +33 -0
  293. package/apps/studio/test/inspect-script-syntax.test.ts +53 -0
  294. package/apps/studio/test/media-commit-chain.test.ts +210 -0
  295. package/apps/studio/test/photo-bg-remove-validation.test.ts +71 -0
  296. package/apps/studio/test/photo-canvas-bundle.test.ts +61 -0
  297. package/apps/studio/test/photo-edit-api.test.ts +201 -0
  298. package/apps/studio/test/photo-filters.test.ts +149 -0
  299. package/apps/studio/test/photo-pipeline.test.ts +106 -0
  300. package/apps/studio/test/photo-taxonomy.test.ts +96 -0
  301. package/apps/studio/test/read-annotations.test.ts +164 -0
  302. package/apps/studio/test/readiness.test.ts +13 -11
  303. package/apps/studio/test/scaffold-design.test.ts +122 -0
  304. package/apps/studio/test/shell-importmap.test.ts +49 -0
  305. package/apps/studio/test/text-editability-stamp.test.ts +131 -0
  306. package/apps/studio/test/timeline-parse.test.ts +57 -0
  307. package/apps/studio/test/tool-palette-insert-anchor.test.ts +5 -3
  308. package/apps/studio/test/tour-overlay.test.tsx +16 -0
  309. package/apps/studio/test/undo-stack.test.ts +33 -0
  310. package/apps/studio/test/video-asset.test.ts +5 -5
  311. package/apps/studio/test/video-render-bridge.test.ts +23 -1
  312. package/apps/studio/text-caret.ts +239 -0
  313. package/apps/studio/tool-palette.tsx +49 -21
  314. package/apps/studio/ui-prefs.ts +130 -0
  315. package/apps/studio/undo-stack.ts +94 -2
  316. package/apps/studio/use-annotation-resize.tsx +4 -4
  317. package/apps/studio/use-canvas-media-drop.tsx +40 -1
  318. package/apps/studio/use-chrome-visibility.tsx +8 -2
  319. package/apps/studio/use-selection-set.tsx +21 -0
  320. package/apps/studio/video-comp.tsx +48 -7
  321. package/apps/studio/whats-new.json +149 -0
  322. package/apps/studio/ws.ts +5 -0
  323. package/cli/bin/maude.mjs +6 -6
  324. package/cli/commands/design.mjs +67 -3
  325. package/cli/lib/gitignore-block.mjs +2 -0
  326. package/cli/lib/pkg-root.mjs +107 -0
  327. package/cli/lib/pkg-root.test.mjs +79 -0
  328. package/cli/lib/reconstruct-toolset.test.mjs +194 -0
  329. package/cli/lib/update-check.mjs +8 -0
  330. package/package.json +13 -11
  331. package/plugins/design/dependencies.json +18 -0
  332. package/plugins/design/templates/_shell.html +1 -0
@@ -23,6 +23,7 @@ import {
23
23
  insertArtboard,
24
24
  insertClip,
25
25
  insertElement,
26
+ insertElementIntoArtboard,
26
27
  type MovePosition,
27
28
  moveElement,
28
29
  removeAttribute,
@@ -33,10 +34,19 @@ import {
33
34
  retimeSequence,
34
35
  retimeSequenceByClip,
35
36
  editText as runEditText,
37
+ setArtboardHug,
38
+ setArtboardStyle,
36
39
  toggleClipHidden,
37
40
  } from './canvas-edit.ts';
38
41
  import type { Context } from './context.ts';
42
+ import {
43
+ type AudioMatch,
44
+ type Candidate,
45
+ rankMatches,
46
+ sanitizeReuseText,
47
+ } from './generation/audio-library.ts';
39
48
  import { createHistory } from './history.ts';
49
+ import { STICKERS_DIR } from './paths.ts';
40
50
 
41
51
  // Directories that never hold user-facing canvases. Exported so the
42
52
  // external-canvas watcher (`canvas-list-watch.ts`) shares one source instead of
@@ -255,6 +265,32 @@ export interface Api {
255
265
  saveAsset(bytes: Uint8Array): Promise<SaveAssetResult>;
256
266
  /** Stage F1 — list content-addressed image/video assets for the AssetPicker. */
257
267
  listAssets(): Promise<{ ok: true; assets: AssetListing[] }>;
268
+ /** feature-ai-media-generation (Task 1.2) — read a content-addressed
269
+ * `assets/<sha8>.<ext>` source's bytes + sniffed mime for the image-edit /
270
+ * image-to-video generation flows. Contained to <designRoot>/assets/;
271
+ * null for an unknown/contained-out path. */
272
+ readAssetBytes(rel: unknown): Promise<{ bytes: Uint8Array; mime: string } | null>;
273
+ /** feature-ai-media-generation (Task 2.6) — write a caption sidecar
274
+ * (`assets/<sha8>.srt|.vtt`) next to a content-addressed source, so a cloud
275
+ * STT result lands where the local whisper path also writes it. Contained to
276
+ * <designRoot>/assets/; text byte-capped; format allowlisted. */
277
+ writeCaptionSidecar(
278
+ sourceRel: unknown,
279
+ format: unknown,
280
+ text: unknown
281
+ ): Promise<{ ok: boolean; path?: string; error?: string }>;
282
+ /** feature-ai-media-generation (Task 2.5) — write the audio-intent sidecar
283
+ * (`assets/<sha8>.audio.json`) for reuse-before-you-pay search. */
284
+ writeAudioIntent(
285
+ assetRel: unknown,
286
+ meta: { kind?: string; prompt?: string; provider?: string; model?: string; at?: string }
287
+ ): Promise<{ ok: boolean; path?: string; error?: string }>;
288
+ /** feature-ai-media-generation (Task 2.5) — keyword-search the project's own
289
+ * generated audio by recorded intent; ranked reuse candidates. */
290
+ searchAudioLibrary(query: unknown, limit?: number): Promise<AudioMatch[]>;
291
+ /** Phase 4 (feature-whiteboard-annotation-improvements) — the bundled sticker
292
+ * catalogue (MAUDE's own, not the served project's) for the StickerPicker. */
293
+ listStickers(): Promise<{ ok: true; packs: StickerPack[] }>;
258
294
  /** DDR-148 — streaming variant for the HTTP route (100 MB video without a
259
295
  * full in-RAM buffer). Sniffs + caps + content-addresses like saveAsset. */
260
296
  saveAssetFromStream(stream: ReadableStream<Uint8Array>): Promise<SaveAssetResult>;
@@ -288,7 +324,13 @@ export interface Api {
288
324
  idIndex?: unknown;
289
325
  }): Promise<EditOpResult>;
290
326
  // Phase 12 (DDR-103) — inline text-content edit (POST /_api/edit-text). Main-origin only.
291
- editText(input: { canvas?: unknown; id?: unknown; text?: unknown }): Promise<EditOpResult>;
327
+ editText(input: {
328
+ canvas?: unknown;
329
+ id?: unknown;
330
+ text?: unknown;
331
+ occurrence?: unknown;
332
+ before?: unknown;
333
+ }): Promise<EditOpResult>;
292
334
  // Phase 12.2 (DDR-104) — custom HTML attribute edit (POST /_api/edit-attr). Main-origin
293
335
  // only. The CSS panel's "custom HTML attribute" escape hatch (data-*, aria-*, role, …);
294
336
  // writes a plain JSX attribute via editAttribute's non-`style.` path.
@@ -396,10 +438,16 @@ export interface Api {
396
438
  }): Promise<
397
439
  { ok: true; deletedId: string; seq?: number } | { ok: false; status: number; error: string }
398
440
  >;
399
- /** Insert a synthesized div/text/image relative to a reference element. */
441
+ /**
442
+ * Insert a synthesized div/text/image relative to a reference element, OR —
443
+ * when the artboard has no element to anchor on yet — as a direct child of
444
+ * `artboardId` (the tool-palette "+ Element" empty-artboard fallback).
445
+ * Exactly one of `refId` / `artboardId` must be provided.
446
+ */
400
447
  insertElementOp(input: {
401
448
  canvas?: unknown;
402
449
  refId?: unknown;
450
+ artboardId?: unknown;
403
451
  position?: unknown;
404
452
  kind?: unknown;
405
453
  src?: unknown;
@@ -424,6 +472,22 @@ export interface Api {
424
472
  width?: unknown;
425
473
  height?: unknown;
426
474
  }): Promise<{ ok: true; seq?: number } | { ok: false; status: number; error: string }>;
475
+ /** Toggle an artboard's Hug/Fixed height sizing mode (CSS-panel control). */
476
+ setArtboardHugOp(input: {
477
+ canvas?: unknown;
478
+ artboardId?: unknown;
479
+ fixed?: unknown;
480
+ freezeHeight?: unknown;
481
+ }): Promise<{ ok: true; seq?: number } | { ok: false; status: number; error: string }>;
482
+ /** Set artboard "more settings" — background / padding / layout / gap. */
483
+ setArtboardStyleOp(input: {
484
+ canvas?: unknown;
485
+ artboardId?: unknown;
486
+ background?: unknown;
487
+ padding?: unknown;
488
+ layout?: unknown;
489
+ gap?: unknown;
490
+ }): Promise<{ ok: true; seq?: number } | { ok: false; status: number; error: string }>;
427
491
  /** Duplicate an element (Cmd+D) — a copy as the next sibling, whole-file undo. */
428
492
  duplicateElementOp(input: {
429
493
  canvas?: unknown;
@@ -472,8 +536,19 @@ export { ASSET_IMAGE_HREF_RE, sanitizeAnnotationSvg } from './annotations-model.
472
536
 
473
537
  import { sanitizeAnnotationSvg } from './annotations-model.ts';
474
538
 
475
- /** Phase 23 — hard ceiling on a single uploaded asset (10 MB). */
476
- export const ASSET_MAX_BYTES = 10 * 1024 * 1024;
539
+ /**
540
+ * Phase 23 — per-file ceiling for a still image. Raised 10 MB 50 MB (still
541
+ * well under the video cap / MAX_REQUEST_BODY headroom) after a real drone
542
+ * photo tripped the old ceiling. Overridable via `MAUDE_ASSET_MAX_IMAGE_BYTES`
543
+ * (bytes), mirroring {@link ASSET_MAX_VIDEO_BYTES}'s override. The route lives
544
+ * on the (untrusted) canvas origin (DDR-088), and images stream through the
545
+ * same category-capped writer as video/audio (`saveAssetFromStream`), so
546
+ * raising this doesn't reintroduce the memory-amplification risk DDR-088 capped.
547
+ */
548
+ export const ASSET_MAX_BYTES = (() => {
549
+ const env = Number(process.env.MAUDE_ASSET_MAX_IMAGE_BYTES);
550
+ return Number.isFinite(env) && env > 0 ? env : 50 * 1024 * 1024;
551
+ })();
477
552
 
478
553
  /**
479
554
  * Phase 23 security review (DDR-088 follow-up) — aggregate per-server-instance
@@ -560,11 +635,29 @@ export interface AssetListing {
560
635
  path: string;
561
636
  name: string;
562
637
  ext: string;
563
- kind: 'image' | 'video';
638
+ kind: 'image' | 'video' | 'audio';
564
639
  size: number;
565
640
  mtimeMs: number;
566
641
  }
567
642
 
643
+ /** Phase 4 (feature-whiteboard-annotation-improvements) — one bundled sticker. */
644
+ export interface StickerItem {
645
+ file: string;
646
+ keywords: string[];
647
+ /** Servable path — `/_stickers/<pack>/<file>` (main-origin static route). */
648
+ url: string;
649
+ }
650
+
651
+ /** A bundled sticker pack — one `apps/studio/stickers/<slug>/manifest.json`. */
652
+ export interface StickerPack {
653
+ slug: string;
654
+ name: string;
655
+ author: string;
656
+ attributionUrl: string;
657
+ license: string;
658
+ stickers: StickerItem[];
659
+ }
660
+
568
661
  /** DDR-148 — media category, decides which per-file cap applies. */
569
662
  export type AssetCategory = 'image' | 'video' | 'audio';
570
663
 
@@ -576,7 +669,7 @@ export interface AssetTypeInfo {
576
669
 
577
670
  /**
578
671
  * DDR-148 — per-file ceiling for time-based media (video + audio). Images keep
579
- * the tighter {@link ASSET_MAX_BYTES} 10 MB cap. Overridable via
672
+ * the tighter {@link ASSET_MAX_BYTES} cap. Overridable via
580
673
  * `MAUDE_ASSET_MAX_VIDEO_BYTES` (bytes) for power users. The route lives on the
581
674
  * (untrusted) canvas origin, so this cap + the session budget + the streamed
582
675
  * write are the trust mitigation, exactly like the image caps (DDR-088).
@@ -595,7 +688,9 @@ const UNSUPPORTED_ASSET_MSG =
595
688
  'unsupported media type — png/jpeg/gif/webp images or mp4/mov/webm/mp3/wav/m4a media only (SVG/script rejected)';
596
689
 
597
690
  function capError(category?: AssetCategory): string {
598
- if (category === 'image') return 'image exceeds the 10 MB cap';
691
+ if (category === 'image') {
692
+ return `image exceeds the ${Math.round(ASSET_MAX_BYTES / (1024 * 1024))} MB cap`;
693
+ }
599
694
  const mb = Math.round(ASSET_MAX_VIDEO_BYTES / (1024 * 1024));
600
695
  return `media exceeds the ${mb} MB cap`;
601
696
  }
@@ -1316,8 +1411,9 @@ export function createApi(ctx: Context, hooks: ApiHooks): Api {
1316
1411
  // optional (DDR Task 9):
1317
1412
  // • magic-byte sniff → true type ∈ {png,jpg,gif,webp}; a header lie or an
1318
1413
  // SVG (script-bearing vector) is rejected — bytes decide, name is ignored.
1319
- // • 10 MB ceiling (assets get their OWN cap; never routed through the 1 MB
1320
- // SVG-text gate in saveAnnotations).
1414
+ // • per-category ceiling ({@link ASSET_MAX_BYTES} for images; assets get
1415
+ // their OWN cap; never routed through the 1 MB SVG-text gate in
1416
+ // saveAnnotations).
1321
1417
  // • content-addressed name `assets/<sha8-of-bytes>.<ext>` → identical drops
1322
1418
  // dedupe → a malicious canvas can't fill the disk with N copies of one
1323
1419
  // image, and orphan-on-delete is safe (shared content survives).
@@ -1331,8 +1427,8 @@ export function createApi(ctx: Context, hooks: ApiHooks): Api {
1331
1427
  * request body chunk-by-chunk so a 100 MB video never lands as a single
1332
1428
  * ArrayBuffer in RAM (the memory-amplification vector DDR-088 §review flagged
1333
1429
  * for the untrusted canvas origin — worse now that the per-file cap is 100 MB,
1334
- * not 10 MB). Sniffs the type from the head, applies the CATEGORY cap while
1335
- * streaming (image 10 MB · video/audio {@link ASSET_MAX_VIDEO_BYTES}), writes
1430
+ * not a few MB). Sniffs the type from the head, applies the CATEGORY cap while
1431
+ * streaming (image {@link ASSET_MAX_BYTES} · video/audio {@link ASSET_MAX_VIDEO_BYTES}), writes
1336
1432
  * to a temp file, then content-addresses (sha8) → dedupe-rename. Nothing is
1337
1433
  * ever parsed — only magic bytes are read. `saveAsset(bytes)` wraps this so
1338
1434
  * both the route and any programmatic caller share ONE tested path.
@@ -1482,6 +1578,10 @@ export function createApi(ctx: Context, hooks: ApiHooks): Api {
1482
1578
  const assetsDir = path.join(paths.designRoot, 'assets');
1483
1579
  const IMG = new Set(['.png', '.jpg', '.jpeg', '.gif', '.webp', '.avif', '.svg']);
1484
1580
  const VID = new Set(['.mp4', '.webm', '.mov', '.m4v', '.ogg']);
1581
+ // feature-ai-media-generation Phase 2 — generated audio (ElevenLabs music /
1582
+ // SFX / voiceover) is content-addressed under assets/ like any other media;
1583
+ // surface it in the picker so a generated track can be dropped into an EDL.
1584
+ const AUD = new Set(['.mp3', '.wav', '.m4a', '.aac', '.flac', '.oga', '.opus']);
1485
1585
  let entries: string[] = [];
1486
1586
  try {
1487
1587
  entries = await readdir(assetsDir);
@@ -1491,7 +1591,7 @@ export function createApi(ctx: Context, hooks: ApiHooks): Api {
1491
1591
  const out: AssetListing[] = [];
1492
1592
  for (const name of entries) {
1493
1593
  const ext = path.extname(name).toLowerCase();
1494
- const kind = IMG.has(ext) ? 'image' : VID.has(ext) ? 'video' : null;
1594
+ const kind = IMG.has(ext) ? 'image' : VID.has(ext) ? 'video' : AUD.has(ext) ? 'audio' : null;
1495
1595
  if (!kind) continue;
1496
1596
  let size = 0;
1497
1597
  let mtimeMs = 0;
@@ -1512,6 +1612,253 @@ export function createApi(ctx: Context, hooks: ApiHooks): Api {
1512
1612
  return { ok: true, assets: out.slice(0, 500) };
1513
1613
  }
1514
1614
 
1615
+ // feature-ai-media-generation (Task 1.2) — read a content-addressed source
1616
+ // asset's bytes + authoritative (magic-byte-SNIFFED, not extension-trusted)
1617
+ // mime, for the image-edit / image-to-video generation flows (Nano Banana
1618
+ // maskless edit reads the source into an inlineData part). The provider adapter
1619
+ // never touches the filesystem — the generation route wires this onto
1620
+ // AdapterContext.readSourceAsset. Contained to <designRoot>/assets/ (an lstat
1621
+ // isFile guard defeats a planted symlink, mirroring listAssets); returns null
1622
+ // for an unknown / traversing / oversized path. Capped at ASSET_MAX_BYTES so a
1623
+ // huge source can't be buffered into RAM before an outbound provider POST.
1624
+ async function readAssetBytes(rel: unknown): Promise<{ bytes: Uint8Array; mime: string } | null> {
1625
+ if (typeof rel !== 'string' || !rel.startsWith('assets/')) return null;
1626
+ const name = rel.slice('assets/'.length);
1627
+ if (
1628
+ !name ||
1629
+ name.includes('/') ||
1630
+ name.includes('\\') ||
1631
+ name.includes('..') ||
1632
+ name.startsWith('_')
1633
+ )
1634
+ return null;
1635
+ const assetsDir = path.join(paths.designRoot, 'assets');
1636
+ const fileAbs = path.join(assetsDir, name);
1637
+ // Containment backstop — the name is validated above; assert the join anyway.
1638
+ if (path.resolve(fileAbs) !== path.join(path.resolve(assetsDir), name)) return null;
1639
+ try {
1640
+ const st = await lstat(fileAbs);
1641
+ if (!st.isFile() || st.size > ASSET_MAX_BYTES) return null;
1642
+ } catch {
1643
+ return null;
1644
+ }
1645
+ const bytes = new Uint8Array(await readFile(fileAbs));
1646
+ const info = sniffAssetType(bytes);
1647
+ if (!info) return null; // not a recognised raster/video/audio — reject
1648
+ const mime =
1649
+ info.ext === 'jpg'
1650
+ ? 'image/jpeg'
1651
+ : info.category === 'image'
1652
+ ? `image/${info.ext}`
1653
+ : info.category === 'video'
1654
+ ? `video/${info.ext}`
1655
+ : `audio/${info.ext}`;
1656
+ return { bytes, mime };
1657
+ }
1658
+
1659
+ // feature-ai-media-generation (Task 2.6, DDR-164) — write a caption SIDECAR
1660
+ // (SRT/VTT text) next to a content-addressed source asset, so a CLOUD STT
1661
+ // result (ElevenLabs Scribe / Groq) lands in the SAME place the local whisper
1662
+ // path writes it: `assets/<sha8>.srt` beside `assets/<sha8>.<ext>`. Local +
1663
+ // cloud subtitles therefore live at one predictable path (the EDL caption
1664
+ // track / a video-comp `<track>` reads `assets/<sha8>.srt`). The caption text
1665
+ // is NOT run through the media magic-byte store (it isn't media) — it is a
1666
+ // versioned text sidecar, mirroring `.meta.json` / `.annotations.svg`.
1667
+ //
1668
+ // Contained to <designRoot>/assets/ exactly like readAssetBytes: the source
1669
+ // must be a validated `assets/<sha8>.<ext>` name, the format is allowlisted,
1670
+ // and the text is byte-capped. Returns the sidecar rel path or null on a bad
1671
+ // source / format.
1672
+ async function writeCaptionSidecar(
1673
+ sourceRel: unknown,
1674
+ format: unknown,
1675
+ text: unknown
1676
+ ): Promise<{ ok: boolean; path?: string; error?: string }> {
1677
+ if (typeof sourceRel !== 'string' || !sourceRel.startsWith('assets/'))
1678
+ return { ok: false, error: 'source must be an assets/<sha8>.<ext> path' };
1679
+ const srcName = sourceRel.slice('assets/'.length);
1680
+ if (
1681
+ !srcName ||
1682
+ srcName.includes('/') ||
1683
+ srcName.includes('\\') ||
1684
+ srcName.includes('..') ||
1685
+ srcName.startsWith('_')
1686
+ )
1687
+ return { ok: false, error: 'invalid source asset name' };
1688
+ const fmt = format === 'vtt' ? 'vtt' : format === 'srt' ? 'srt' : null;
1689
+ if (!fmt) return { ok: false, error: 'format must be srt or vtt' };
1690
+ if (typeof text !== 'string' || !text.trim()) return { ok: false, error: 'empty caption text' };
1691
+ if (text.length > 4 * 1024 * 1024) return { ok: false, error: 'caption text too large' };
1692
+
1693
+ // Strip the source extension → `<sha8>` (or the whole name if none), append
1694
+ // the caption format. A dotless source name keeps its whole name as the base.
1695
+ const dot = srcName.lastIndexOf('.');
1696
+ const base = dot > 0 ? srcName.slice(0, dot) : srcName;
1697
+ const outName = `${base}.${fmt}`;
1698
+ const assetsDir = path.join(paths.designRoot, 'assets');
1699
+ const fileAbs = path.join(assetsDir, outName);
1700
+ if (path.resolve(fileAbs) !== path.join(path.resolve(assetsDir), outName))
1701
+ return { ok: false, error: 'resolved sidecar path escapes assets dir' };
1702
+ try {
1703
+ await mkdir(assetsDir, { recursive: true });
1704
+ await Bun.write(fileAbs, text);
1705
+ return { ok: true, path: `assets/${outName}` };
1706
+ } catch (err) {
1707
+ return { ok: false, error: err instanceof Error ? err.message : 'sidecar write failed' };
1708
+ }
1709
+ }
1710
+
1711
+ // feature-ai-media-generation (Task 2.5, DDR-164) — write the AUDIO INTENT
1712
+ // sidecar (`assets/<sha8>.audio.json`) next to a generated audio asset: the
1713
+ // durable, semantic "what was this audio FOR" index that reuse-before-you-pay
1714
+ // searches. Byte-content-addressing already dedups identical outputs; this adds
1715
+ // the "do we already have a warm lo-fi loop?" lookup. Contained to assets/ like
1716
+ // the caption sidecar; the intent is inert non-secret metadata.
1717
+ async function writeAudioIntent(
1718
+ assetRel: unknown,
1719
+ meta: { kind?: string; prompt?: string; provider?: string; model?: string; at?: string }
1720
+ ): Promise<{ ok: boolean; path?: string; error?: string }> {
1721
+ if (typeof assetRel !== 'string' || !assetRel.startsWith('assets/'))
1722
+ return { ok: false, error: 'asset must be an assets/<sha8>.<ext> path' };
1723
+ const name = assetRel.slice('assets/'.length);
1724
+ if (
1725
+ !name ||
1726
+ name.includes('/') ||
1727
+ name.includes('\\') ||
1728
+ name.includes('..') ||
1729
+ name.startsWith('_')
1730
+ )
1731
+ return { ok: false, error: 'invalid asset name' };
1732
+ const dot = name.lastIndexOf('.');
1733
+ const base = dot > 0 ? name.slice(0, dot) : name;
1734
+ const outName = `${base}.audio.json`;
1735
+ const assetsDir = path.join(paths.designRoot, 'assets');
1736
+ const fileAbs = path.join(assetsDir, outName);
1737
+ if (path.resolve(fileAbs) !== path.join(path.resolve(assetsDir), outName))
1738
+ return { ok: false, error: 'resolved sidecar path escapes assets dir' };
1739
+ const record = {
1740
+ asset: assetRel,
1741
+ kind: meta.kind,
1742
+ prompt: typeof meta.prompt === 'string' ? meta.prompt.slice(0, 8000) : undefined,
1743
+ provider: meta.provider,
1744
+ model: meta.model,
1745
+ at: meta.at ?? new Date().toISOString(),
1746
+ };
1747
+ try {
1748
+ await mkdir(assetsDir, { recursive: true });
1749
+ await Bun.write(fileAbs, `${JSON.stringify(record, null, 2)}\n`);
1750
+ return { ok: true, path: `assets/${outName}` };
1751
+ } catch (err) {
1752
+ return { ok: false, error: err instanceof Error ? err.message : 'intent write failed' };
1753
+ }
1754
+ }
1755
+
1756
+ // feature-ai-media-generation (Task 2.5) — search the project's OWN generated
1757
+ // audio by intent (keyword overlap against the recorded prompt/kind), so the
1758
+ // reuse-first path can offer an existing track before paying for a new
1759
+ // generation. Reads the `assets/<sha8>.audio.json` sidecars; ranks with the
1760
+ // shared pure scorer (generation/audio-library.ts) so local + provider-history
1761
+ // results are comparable. Best-effort — a malformed sidecar is skipped.
1762
+ async function searchAudioLibrary(query: unknown, limit = 10): Promise<AudioMatch[]> {
1763
+ if (typeof query !== 'string' || !query.trim()) return [];
1764
+ const assetsDir = path.join(paths.designRoot, 'assets');
1765
+ let entries: string[];
1766
+ try {
1767
+ entries = await readdir(assetsDir);
1768
+ } catch {
1769
+ return [];
1770
+ }
1771
+ // F2 (ethical-hacker) — an intent sidecar is a peer-synced, hence UNTRUSTED
1772
+ // (DDR-054), file: a hostile branch peer could commit a giant `.audio.json`
1773
+ // or thousands of them to OOM the reader (the F1 RAM-DoS class, on disk). A
1774
+ // real intent record is tiny → cap the per-file read AND the number scanned.
1775
+ const MAX_SIDECAR_BYTES = 256 * 1024;
1776
+ const MAX_SIDECARS = 4000;
1777
+ const candidates: Candidate[] = [];
1778
+ let scanned = 0;
1779
+ for (const name of entries) {
1780
+ if (!name.endsWith('.audio.json')) continue;
1781
+ if (++scanned > MAX_SIDECARS) break;
1782
+ try {
1783
+ const fileAbs = path.join(assetsDir, name);
1784
+ const st = await lstat(fileAbs);
1785
+ if (!st.isFile() || st.size > MAX_SIDECAR_BYTES) continue;
1786
+ const raw = await readFile(fileAbs, 'utf8');
1787
+ const intent = JSON.parse(raw) as Record<string, unknown>;
1788
+ const asset =
1789
+ typeof intent.asset === 'string'
1790
+ ? intent.asset
1791
+ : `assets/${name.replace(/\.audio\.json$/, '')}`;
1792
+ candidates.push({
1793
+ source: 'local',
1794
+ ref: asset,
1795
+ // Sanitize the peer-synced (untrusted) prompt before it can be echoed
1796
+ // into an agent context (F3).
1797
+ text: sanitizeReuseText(intent.prompt),
1798
+ kind: typeof intent.kind === 'string' ? intent.kind : undefined,
1799
+ provider: typeof intent.provider === 'string' ? intent.provider : undefined,
1800
+ at: typeof intent.at === 'string' ? intent.at : undefined,
1801
+ });
1802
+ } catch {
1803
+ /* skip a malformed sidecar */
1804
+ }
1805
+ }
1806
+ return rankMatches(query, candidates, { limit });
1807
+ }
1808
+
1809
+ // Phase 4 (feature-whiteboard-annotation-improvements) — the bundled sticker
1810
+ // catalogue for the StickerPicker. Reads from MAUDE's OWN `STICKERS_DIR`
1811
+ // (paths.ts, DDR-045) — never the served project's designRoot, unlike
1812
+ // listAssets above. A pack directory missing or with an invalid
1813
+ // manifest.json is skipped, not fatal — one bad pack shouldn't blank the
1814
+ // whole picker. MAIN-ORIGIN ONLY at the route layer (the picker is shell UI,
1815
+ // same posture as listAssets/AssetPicker).
1816
+ async function listStickers(): Promise<{ ok: true; packs: StickerPack[] }> {
1817
+ let packDirs: string[] = [];
1818
+ try {
1819
+ const entries = await readdir(STICKERS_DIR, { withFileTypes: true });
1820
+ packDirs = entries.filter((e) => e.isDirectory()).map((e) => e.name);
1821
+ } catch {
1822
+ return { ok: true, packs: [] }; // no bundled stickers in this layout
1823
+ }
1824
+ const packs: StickerPack[] = [];
1825
+ for (const slug of packDirs) {
1826
+ try {
1827
+ const manifestPath = path.join(STICKERS_DIR, slug, 'manifest.json');
1828
+ const raw = await readFile(manifestPath, 'utf8');
1829
+ const manifest = JSON.parse(raw) as {
1830
+ name?: string;
1831
+ author?: string;
1832
+ attributionUrl?: string;
1833
+ license?: string;
1834
+ stickers?: Array<{ file?: string; keywords?: string[] }>;
1835
+ };
1836
+ const stickers: StickerItem[] = (manifest.stickers ?? [])
1837
+ .filter(
1838
+ (s): s is { file: string; keywords?: string[] } =>
1839
+ typeof s.file === 'string' && !!s.file
1840
+ )
1841
+ .map((s) => ({
1842
+ file: s.file,
1843
+ keywords: Array.isArray(s.keywords)
1844
+ ? s.keywords.filter((k) => typeof k === 'string')
1845
+ : [],
1846
+ url: `/_stickers/${slug}/${encodeURIComponent(s.file)}`,
1847
+ }));
1848
+ packs.push({
1849
+ slug,
1850
+ name: manifest.name ?? slug,
1851
+ author: manifest.author ?? '',
1852
+ attributionUrl: manifest.attributionUrl ?? '',
1853
+ license: manifest.license ?? '',
1854
+ stickers,
1855
+ });
1856
+ } catch {}
1857
+ }
1858
+ packs.sort((a, b) => a.name.localeCompare(b.name));
1859
+ return { ok: true, packs };
1860
+ }
1861
+
1515
1862
  // Phase 31 follow-up — persist an image pasted straight from the clipboard into
1516
1863
  // the ACP composer (a screenshot has no path yet), so the chip can point Claude
1517
1864
  // at a real file to Read. Sibling of saveAsset, but:
@@ -1519,13 +1866,17 @@ export function createApi(ctx: Context, hooks: ApiHooks): Api {
1519
1866
  // the versioned `assets/`; pasted screenshots are ephemeral, not canvas media.
1520
1867
  // • returns an ABSOLUTE path — Claude runs with its own cwd, so a project-
1521
1868
  // relative string could miss; an absolute path always resolves.
1522
- // Same load-bearing caps as saveAsset (magic-byte sniff → no SVG/script, 10 MB,
1523
- // content-addressed name, shared session write budget). MAIN-ORIGIN ONLY at the
1524
- // route layer (the untrusted canvas can't reach it).
1869
+ // Same load-bearing caps as saveAsset (magic-byte sniff → no SVG/script,
1870
+ // ASSET_MAX_BYTES, content-addressed name, shared session write budget).
1871
+ // MAIN-ORIGIN ONLY at the route layer (the untrusted canvas can't reach it).
1525
1872
  async function saveChatAttachment(bytes: Uint8Array): Promise<SaveAssetResult> {
1526
1873
  if (!bytes || bytes.length === 0) return { ok: false, status: 400, error: 'empty body' };
1527
1874
  if (bytes.length > ASSET_MAX_BYTES) {
1528
- return { ok: false, status: 413, error: 'attachment exceeds the 10 MB cap' };
1875
+ return {
1876
+ ok: false,
1877
+ status: 413,
1878
+ error: `attachment exceeds the ${Math.round(ASSET_MAX_BYTES / (1024 * 1024))} MB cap`,
1879
+ };
1529
1880
  }
1530
1881
  const kind = sniffImageType(bytes);
1531
1882
  if (!kind) {
@@ -2009,6 +2360,8 @@ export function createApi(ctx: Context, hooks: ApiHooks): Api {
2009
2360
  canvas?: unknown;
2010
2361
  id?: unknown;
2011
2362
  text?: unknown;
2363
+ occurrence?: unknown;
2364
+ before?: unknown;
2012
2365
  }): Promise<EditOpResult> {
2013
2366
  const r = resolveCanvasAbs(input.canvas);
2014
2367
  if (!r.ok) return r;
@@ -2017,7 +2370,23 @@ export function createApi(ctx: Context, hooks: ApiHooks): Api {
2017
2370
  if (typeof input.text !== 'string') return { ok: false, status: 400, error: 'text required' };
2018
2371
  if (input.text.length > 5000) return { ok: false, status: 413, error: 'text too long' };
2019
2372
  const text = input.text;
2020
- return suppressedEdit(r.abs, () => runEditText(r.abs, id, text), 'edit failed');
2373
+ // Optional context for editing `{variable}` text (unified-text-editing
2374
+ // follow-up): which rendered instance, and its pre-edit text — both used
2375
+ // only to target the right source string; a bad/absent value just makes an
2376
+ // unresolvable expression edit fail loud (route to /design:edit).
2377
+ const occurrence =
2378
+ typeof input.occurrence === 'number' &&
2379
+ Number.isInteger(input.occurrence) &&
2380
+ input.occurrence >= 0
2381
+ ? input.occurrence
2382
+ : undefined;
2383
+ const before =
2384
+ typeof input.before === 'string' && input.before.length <= 5000 ? input.before : undefined;
2385
+ return suppressedEdit(
2386
+ r.abs,
2387
+ () => runEditText(r.abs, id, text, { occurrence, before }),
2388
+ 'edit failed'
2389
+ );
2021
2390
  }
2022
2391
 
2023
2392
  async function editAttr(input: {
@@ -2347,10 +2716,12 @@ export function createApi(ctx: Context, hooks: ApiHooks): Api {
2347
2716
  }
2348
2717
  }
2349
2718
 
2350
- /** Insert a synthesized element (div/text/image) relative to `refId`. */
2719
+ /** Insert a synthesized element (div/text/image) relative to `refId`, or —
2720
+ * empty-artboard fallback — as a direct child of `artboardId`. */
2351
2721
  async function insertElementOp(input: {
2352
2722
  canvas?: unknown;
2353
2723
  refId?: unknown;
2724
+ artboardId?: unknown;
2354
2725
  position?: unknown;
2355
2726
  kind?: unknown;
2356
2727
  src?: unknown;
@@ -2361,14 +2732,30 @@ export function createApi(ctx: Context, hooks: ApiHooks): Api {
2361
2732
  const r = resolveCanvasAbs(input.canvas);
2362
2733
  if (!r.ok) return r;
2363
2734
  if (!takeStructuralToken()) return RATE_LIMITED;
2364
- const refId = typeof input.refId === 'string' ? input.refId.trim() : '';
2365
- if (!CD_ID_RE.test(refId)) {
2735
+ const hasRefId = typeof input.refId === 'string' && input.refId.trim() !== '';
2736
+ const hasArtboardId = typeof input.artboardId === 'string' && input.artboardId.trim() !== '';
2737
+ if (hasRefId === hasArtboardId) {
2738
+ return { ok: false, status: 400, error: 'provide exactly one of refId / artboardId' };
2739
+ }
2740
+ const refId = hasRefId ? (input.refId as string).trim() : '';
2741
+ if (hasRefId && !CD_ID_RE.test(refId)) {
2366
2742
  return { ok: false, status: 400, error: 'invalid reference data-cd-id' };
2367
2743
  }
2744
+ const artboardId = hasArtboardId ? (input.artboardId as string).trim() : '';
2745
+ if (hasArtboardId && !/^[A-Za-z][\w-]{0,63}$/.test(artboardId)) {
2746
+ return { ok: false, status: 400, error: 'invalid artboard id' };
2747
+ }
2368
2748
  const position = input.position;
2369
2749
  if (typeof position !== 'string' || !MOVE_POSITIONS.has(position)) {
2370
2750
  return { ok: false, status: 400, error: 'invalid position' };
2371
2751
  }
2752
+ if (hasArtboardId && position !== 'inside-start' && position !== 'inside-end') {
2753
+ return {
2754
+ ok: false,
2755
+ status: 400,
2756
+ error: 'artboardId insert requires inside-start/inside-end',
2757
+ };
2758
+ }
2372
2759
  const kind = input.kind;
2373
2760
  if (kind !== 'div' && kind !== 'text' && kind !== 'image') {
2374
2761
  return { ok: false, status: 400, error: 'invalid kind (div|text|image)' };
@@ -2383,10 +2770,18 @@ export function createApi(ctx: Context, hooks: ApiHooks): Api {
2383
2770
  ctx.bus.emit('activity:unsuppress', rel);
2384
2771
  return { ok: false, status: 413, error: 'canvas source too large to grow' };
2385
2772
  }
2386
- const res = await insertElement(r.abs, refId, position as MovePosition, kind as InsertKind, {
2387
- src,
2388
- occurrence: refIndex,
2389
- });
2773
+ const res = hasArtboardId
2774
+ ? await insertElementIntoArtboard(
2775
+ r.abs,
2776
+ artboardId,
2777
+ position as 'inside-start' | 'inside-end',
2778
+ kind as InsertKind,
2779
+ { src }
2780
+ )
2781
+ : await insertElement(r.abs, refId, position as MovePosition, kind as InsertKind, {
2782
+ src,
2783
+ occurrence: refIndex,
2784
+ });
2390
2785
  const after = await Bun.file(r.abs).text();
2391
2786
  if (after === before) {
2392
2787
  ctx.bus.emit('activity:unsuppress', rel);
@@ -2510,6 +2905,137 @@ export function createApi(ctx: Context, hooks: ApiHooks): Api {
2510
2905
  }
2511
2906
  }
2512
2907
 
2908
+ /** Toggle an artboard's Hug/Fixed height sizing mode (CSS-panel control). */
2909
+ async function setArtboardHugOp(input: {
2910
+ canvas?: unknown;
2911
+ artboardId?: unknown;
2912
+ fixed?: unknown;
2913
+ freezeHeight?: unknown;
2914
+ }): Promise<{ ok: true; seq?: number } | { ok: false; status: number; error: string }> {
2915
+ const r = resolveCanvasAbs(input.canvas);
2916
+ if (!r.ok) return r;
2917
+ if (!takeStructuralToken()) return RATE_LIMITED;
2918
+ const artboardId = typeof input.artboardId === 'string' ? input.artboardId.trim() : '';
2919
+ if (!/^[A-Za-z][\w-]{0,63}$/.test(artboardId)) {
2920
+ return { ok: false, status: 400, error: 'invalid artboard id' };
2921
+ }
2922
+ if (typeof input.fixed !== 'boolean') {
2923
+ return { ok: false, status: 400, error: 'fixed (boolean) required' };
2924
+ }
2925
+ const fixed = input.fixed;
2926
+ const freezeHeight = Number.isFinite(Number(input.freezeHeight))
2927
+ ? Math.max(64, Math.min(8192, Math.round(Number(input.freezeHeight))))
2928
+ : undefined;
2929
+ const rel = path.relative(paths.designRoot, r.abs);
2930
+ ctx.bus.emit('activity:suppress', rel);
2931
+ try {
2932
+ const before = await Bun.file(r.abs).text();
2933
+ await setArtboardHug(r.abs, artboardId, fixed, freezeHeight);
2934
+ const after = await Bun.file(r.abs).text();
2935
+ if (after === before) {
2936
+ ctx.bus.emit('activity:unsuppress', rel);
2937
+ return { ok: true };
2938
+ }
2939
+ try {
2940
+ await history.writeSnapshot(rel, before, 'pre-set-artboard-hug');
2941
+ } catch {
2942
+ /* snapshot best-effort */
2943
+ }
2944
+ return { ok: true, seq: logUndo(r.abs, before, after) };
2945
+ } catch (err) {
2946
+ ctx.bus.emit('activity:unsuppress', rel);
2947
+ return {
2948
+ ok: false,
2949
+ status: err instanceof CanvasEditError ? 422 : 500,
2950
+ error: err instanceof Error ? err.message : 'set-artboard-hug failed',
2951
+ };
2952
+ }
2953
+ }
2954
+
2955
+ const ARTBOARD_LAYOUT_VALUES = new Set(['block', 'flex-col', 'flex-row', 'grid']);
2956
+
2957
+ /** Set artboard "more settings" — background / padding / layout / gap. */
2958
+ async function setArtboardStyleOp(input: {
2959
+ canvas?: unknown;
2960
+ artboardId?: unknown;
2961
+ background?: unknown;
2962
+ padding?: unknown;
2963
+ layout?: unknown;
2964
+ gap?: unknown;
2965
+ }): Promise<{ ok: true; seq?: number } | { ok: false; status: number; error: string }> {
2966
+ const r = resolveCanvasAbs(input.canvas);
2967
+ if (!r.ok) return r;
2968
+ if (!takeStructuralToken()) return RATE_LIMITED;
2969
+ const artboardId = typeof input.artboardId === 'string' ? input.artboardId.trim() : '';
2970
+ if (!/^[A-Za-z][\w-]{0,63}$/.test(artboardId)) {
2971
+ return { ok: false, status: 400, error: 'invalid artboard id' };
2972
+ }
2973
+ const patch: {
2974
+ background?: string | null;
2975
+ padding?: number | null;
2976
+ layout?: string | null;
2977
+ gap?: number | null;
2978
+ } = {};
2979
+ if ('background' in input) {
2980
+ if (input.background === null) patch.background = null;
2981
+ else if (typeof input.background === 'string' && input.background.length <= 256) {
2982
+ patch.background = input.background;
2983
+ } else {
2984
+ return { ok: false, status: 400, error: 'invalid background' };
2985
+ }
2986
+ }
2987
+ if ('layout' in input) {
2988
+ if (input.layout === null) patch.layout = null;
2989
+ else if (typeof input.layout === 'string' && ARTBOARD_LAYOUT_VALUES.has(input.layout)) {
2990
+ patch.layout = input.layout;
2991
+ } else {
2992
+ return { ok: false, status: 400, error: 'invalid layout' };
2993
+ }
2994
+ }
2995
+ const clampBoxDim = (v: unknown): number | null | undefined => {
2996
+ if (v === null) return null;
2997
+ if (v === undefined) return undefined;
2998
+ return Number.isFinite(Number(v)) ? Math.max(0, Math.min(512, Math.round(Number(v)))) : NaN;
2999
+ };
3000
+ if ('padding' in input) {
3001
+ const p = clampBoxDim(input.padding);
3002
+ if (Number.isNaN(p)) return { ok: false, status: 400, error: 'invalid padding' };
3003
+ patch.padding = p;
3004
+ }
3005
+ if ('gap' in input) {
3006
+ const g = clampBoxDim(input.gap);
3007
+ if (Number.isNaN(g)) return { ok: false, status: 400, error: 'invalid gap' };
3008
+ patch.gap = g;
3009
+ }
3010
+ if (Object.keys(patch).length === 0) {
3011
+ return { ok: false, status: 400, error: 'no style props given' };
3012
+ }
3013
+ const rel = path.relative(paths.designRoot, r.abs);
3014
+ ctx.bus.emit('activity:suppress', rel);
3015
+ try {
3016
+ const before = await Bun.file(r.abs).text();
3017
+ await setArtboardStyle(r.abs, artboardId, patch);
3018
+ const after = await Bun.file(r.abs).text();
3019
+ if (after === before) {
3020
+ ctx.bus.emit('activity:unsuppress', rel);
3021
+ return { ok: true };
3022
+ }
3023
+ try {
3024
+ await history.writeSnapshot(rel, before, 'pre-set-artboard-style');
3025
+ } catch {
3026
+ /* snapshot best-effort */
3027
+ }
3028
+ return { ok: true, seq: logUndo(r.abs, before, after) };
3029
+ } catch (err) {
3030
+ ctx.bus.emit('activity:unsuppress', rel);
3031
+ return {
3032
+ ok: false,
3033
+ status: err instanceof CanvasEditError ? 422 : 500,
3034
+ error: err instanceof Error ? err.message : 'set-artboard-style failed',
3035
+ };
3036
+ }
3037
+ }
3038
+
2513
3039
  /** Duplicate an element (Cmd+D) — insert a copy as the next sibling. */
2514
3040
  async function duplicateElementOp(input: {
2515
3041
  canvas?: unknown;
@@ -2853,6 +3379,14 @@ export function createApi(ctx: Context, hooks: ApiHooks): Api {
2853
3379
  // instead of unconditionally designSystems[0]. Populated from the canvas
2854
3380
  // groups below; returned on the payload and folded into the client cfg.
2855
3381
  const canvasDesignSystems: Record<string, string> = {};
3382
+ // DDR-174 (T15) — surface a canvas's `.meta.json` `kind` in the file tree
3383
+ // when it's a value the user should notice at a glance (today: only
3384
+ // `reconstructed-experimental`, so this map stays empty on every project
3385
+ // that hasn't run `/design:import --reconstruct`). Piggybacks on the same
3386
+ // `loadCanvasMeta` call the DDR-093 DS-map loop below already makes for
3387
+ // non-path-owned (`ui/`) canvases — no extra I/O for the common case.
3388
+ const canvasKinds: Record<string, string> = {};
3389
+ const NOTABLE_KINDS = new Set(['reconstructed-experimental']);
2856
3390
  const defaultDs = cfg.defaultDesignSystem || cfg.designSystems?.[0]?.name || 'project';
2857
3391
  // A file under `system/<folder>/` belongs to the DS that owns that folder —
2858
3392
  // path-authoritative, because specimens/ui_kits rarely carry a sidecar
@@ -2926,6 +3460,8 @@ export function createApi(ctx: Context, hooks: ApiHooks): Api {
2926
3460
  const meta = await loadCanvasMeta(fp);
2927
3461
  const declared = meta?.designSystem;
2928
3462
  dsName = typeof declared === 'string' && declared.trim() ? declared : defaultDs;
3463
+ const kind = meta?.kind;
3464
+ if (typeof kind === 'string' && NOTABLE_KINDS.has(kind)) canvasKinds[fp] = kind;
2929
3465
  }
2930
3466
  canvasDesignSystems[fp] = dsName;
2931
3467
  }
@@ -2990,6 +3526,7 @@ export function createApi(ctx: Context, hooks: ApiHooks): Api {
2990
3526
  designRoot: paths.designRel,
2991
3527
  groups,
2992
3528
  canvasDesignSystems,
3529
+ canvasKinds,
2993
3530
  };
2994
3531
  }
2995
3532
 
@@ -3208,6 +3745,11 @@ export function createApi(ctx: Context, hooks: ApiHooks): Api {
3208
3745
  saveAnnotations,
3209
3746
  saveAsset,
3210
3747
  listAssets,
3748
+ readAssetBytes,
3749
+ writeCaptionSidecar,
3750
+ writeAudioIntent,
3751
+ searchAudioLibrary,
3752
+ listStickers,
3211
3753
  saveAssetFromStream,
3212
3754
  saveChatAttachment,
3213
3755
  resolveChatAttachment,
@@ -3228,6 +3770,8 @@ export function createApi(ctx: Context, hooks: ApiHooks): Api {
3228
3770
  insertElementOp,
3229
3771
  insertArtboardOp,
3230
3772
  resizeArtboardOp,
3773
+ setArtboardHugOp,
3774
+ setArtboardStyleOp,
3231
3775
  deleteArtboardOp,
3232
3776
  duplicateElementOp,
3233
3777
  editScopeOp,