@gentbajko/slopify 2.4.0 → 3.0.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 (469) hide show
  1. package/LICENSE +201 -21
  2. package/NOTICE +12 -0
  3. package/README.md +21 -40
  4. package/SUBTITLES.md +1 -1
  5. package/dist/adapter-registry.js +11 -1
  6. package/dist/adapters/alignment/cache.js +4 -1
  7. package/dist/adapters/alignment/ctc.js +25 -23
  8. package/dist/adapters/alignment/index.js +37 -6
  9. package/dist/adapters/alignment/multilingual.js +267 -0
  10. package/dist/adapters/alignment/protocol.js +11 -0
  11. package/dist/adapters/alignment/quality.js +4 -3
  12. package/dist/adapters/alignment/sentences.js +95 -0
  13. package/dist/adapters/alignment/spec.js +24 -0
  14. package/dist/adapters/alignment/spell.js +769 -0
  15. package/dist/adapters/alignment/text.js +56 -9
  16. package/dist/adapters/alignment/window.js +34 -30
  17. package/dist/adapters/alignment/worker.js +23 -21
  18. package/dist/adapters/explain.js +11 -11
  19. package/dist/adapters/host-cli/index.js +55 -5
  20. package/dist/adapters/host-cli/transport.js +13 -6
  21. package/dist/adapters/image/bytes.js +2 -2
  22. package/dist/adapters/image/codex-output.js +60 -18
  23. package/dist/adapters/image/codex.js +166 -25
  24. package/dist/adapters/image/fal.js +31 -3
  25. package/dist/adapters/image/google.js +17 -2
  26. package/dist/adapters/image/openai.js +45 -16
  27. package/dist/adapters/image/reference.js +41 -0
  28. package/dist/adapters/image/replicate.js +14 -2
  29. package/dist/adapters/image/video.js +2 -2
  30. package/dist/adapters/key-probes.js +36 -0
  31. package/dist/adapters/llm/claude-code.js +168 -32
  32. package/dist/adapters/llm/cli-login-error.js +1 -1
  33. package/dist/adapters/llm/codex-limits.js +157 -0
  34. package/dist/adapters/llm/codex-models.js +2 -1
  35. package/dist/adapters/llm/codex-server-models.js +2 -1
  36. package/dist/adapters/llm/codex.js +99 -12
  37. package/dist/adapters/llm/document-workspace.js +2 -2
  38. package/dist/adapters/llm/gemini-workspace.js +1 -1
  39. package/dist/adapters/llm/gemini.js +16 -3
  40. package/dist/adapters/llm/image-workspace.js +58 -0
  41. package/dist/adapters/llm/openrouter.js +11 -3
  42. package/dist/adapters/llm/run-cli.js +26 -9
  43. package/dist/adapters/tts/cartesia.js +10 -0
  44. package/dist/adapters/tts/elevenlabs.js +31 -9
  45. package/dist/adapters/tts/inworld-async.js +9 -5
  46. package/dist/adapters/tts/inworld.js +17 -3
  47. package/dist/adapters/tts/openai.js +3 -0
  48. package/dist/adapters/tts/voice-languages.js +80 -0
  49. package/dist/assets/fonts/noto/NotoSans-Regular.ttf +0 -0
  50. package/dist/assets/fonts/noto/NotoSansArabic-Regular.ttf +0 -0
  51. package/dist/assets/fonts/noto/NotoSansDevanagari-Regular.ttf +0 -0
  52. package/dist/assets/fonts/noto/NotoSansHebrew-Regular.ttf +0 -0
  53. package/dist/assets/fonts/noto/NotoSansThai-Regular.ttf +0 -0
  54. package/dist/assets/fonts/noto/OFL.txt +93 -0
  55. package/dist/assets/fonts/noto/SOURCE.txt +18 -0
  56. package/dist/assets/models.yaml +7 -0
  57. package/dist/assets/sample/sample-audiobook.tar +0 -0
  58. package/dist/assets/sample/sample-podcast.tar +0 -0
  59. package/dist/assets/sample/sample-project.tar +0 -0
  60. package/dist/catalog/merge.js +132 -0
  61. package/dist/catalog/registry.js +33 -12
  62. package/dist/catalog/runtime-models.js +1 -1
  63. package/dist/catalog/schema.js +12 -1
  64. package/dist/catalog/store.js +147 -8
  65. package/dist/catalog/validate.js +17 -2
  66. package/dist/compose.yaml +71 -0
  67. package/dist/edge/autostart/docker-record.js +32 -0
  68. package/dist/edge/autostart/docker.js +50 -0
  69. package/dist/edge/autostart/index.js +78 -0
  70. package/dist/edge/autostart/launcher.js +176 -0
  71. package/dist/edge/autostart/model.js +3 -0
  72. package/dist/edge/autostart/native.js +210 -0
  73. package/dist/edge/autostart/prompt.js +66 -0
  74. package/dist/edge/autostart/service.js +81 -0
  75. package/dist/edge/cli.js +57 -51
  76. package/dist/edge/docker-install/activation.js +56 -0
  77. package/dist/edge/docker-install/apply.js +458 -0
  78. package/dist/edge/docker-install/engine.js +362 -0
  79. package/dist/edge/docker-install/run.js +154 -0
  80. package/dist/edge/docker-install/state.js +146 -0
  81. package/dist/edge/{docker-projects → docker-install}/volume.js +1 -5
  82. package/dist/edge/docker.js +14 -22
  83. package/dist/edge/events/hub.js +11 -0
  84. package/dist/edge/host-cli.js +1 -1
  85. package/dist/edge/http/actions.js +32 -10
  86. package/dist/edge/http/app.js +60 -2
  87. package/dist/edge/http/auditions.js +70 -0
  88. package/dist/edge/http/autostart.js +37 -0
  89. package/dist/edge/http/backups.js +40 -0
  90. package/dist/edge/http/channel-memory.js +92 -0
  91. package/dist/edge/http/channels.js +128 -0
  92. package/dist/edge/http/entries.js +22 -3
  93. package/dist/edge/http/files.js +1 -1
  94. package/dist/edge/http/folder-location.js +1 -1
  95. package/dist/edge/http/home.js +39 -0
  96. package/dist/edge/http/host-cli.js +45 -6
  97. package/dist/edge/http/narration-peaks.js +76 -0
  98. package/dist/edge/http/onboarding.js +152 -0
  99. package/dist/edge/http/open-folder.js +1 -1
  100. package/dist/edge/http/patch-notes.js +45 -0
  101. package/dist/edge/http/planning.js +4 -2
  102. package/dist/edge/http/problem.js +1 -0
  103. package/dist/edge/http/project-create.js +87 -0
  104. package/dist/edge/http/projects.js +31 -99
  105. package/dist/edge/http/prompts.js +40 -3
  106. package/dist/edge/http/pronunciations.js +23 -4
  107. package/dist/edge/http/providers.js +86 -0
  108. package/dist/edge/http/reviews.js +121 -0
  109. package/dist/edge/http/revision-files.js +1 -1
  110. package/dist/edge/http/revisions.js +1 -1
  111. package/dist/edge/http/run-cost.js +25 -0
  112. package/dist/edge/http/schedules.js +111 -6
  113. package/dist/edge/http/settings.js +128 -6
  114. package/dist/edge/http/storage-files.js +50 -0
  115. package/dist/edge/http/storage.js +29 -2
  116. package/dist/edge/http/studio.js +207 -0
  117. package/dist/edge/http/style-preview.js +100 -0
  118. package/dist/edge/http/subtitles.js +1 -1
  119. package/dist/edge/http/trash.js +69 -0
  120. package/dist/edge/http/update.js +24 -3
  121. package/dist/edge/http/whats-new.js +15 -0
  122. package/dist/edge/http/youtube-edits.js +83 -0
  123. package/dist/edge/native-update.js +62 -0
  124. package/dist/host-cli/open-folder.js +10 -10
  125. package/dist/host-cli/runtime.js +12 -3
  126. package/dist/kernel/db/migrations/0022-drop-duplicate-held-work.sql +56 -0
  127. package/dist/kernel/db/migrations/0024-reference-attachments.sql +18 -0
  128. package/dist/kernel/db/migrations/0025-automatic-reviews.sql +40 -0
  129. package/dist/kernel/db/migrations/0026-schedule-topic-generation.sql +23 -0
  130. package/dist/kernel/db/migrations/0027-run-cost.sql +58 -0
  131. package/dist/kernel/db/migrations/0030-automatic-retries.sql +18 -0
  132. package/dist/kernel/db/migrations/0031-prompt-history-and-description-edits.sql +33 -0
  133. package/dist/kernel/db/migrations/0032-channels.sql +65 -0
  134. package/dist/kernel/db/migrations/0034-script-prompts.sql +23 -0
  135. package/dist/kernel/db/migrations/0035-narration-aliases.sql +13 -0
  136. package/dist/kernel/db/migrations/0037-project-uploads.sql +8 -0
  137. package/dist/kernel/db/migrations/0038-voice-languages.sql +7 -0
  138. package/dist/kernel/db/migrations/0039-channel-essentials.sql +64 -0
  139. package/dist/kernel/db/migrations/0040-standalone-usage.sql +35 -0
  140. package/dist/kernel/paths.js +27 -6
  141. package/dist/kernel/pipeline.js +4 -1
  142. package/dist/kernel/ports/host-cli.js +139 -16
  143. package/dist/kernel/ports/image.js +3 -1
  144. package/dist/kernel/ports/key-probe.js +1 -0
  145. package/dist/kernel/ports/languages.js +157 -0
  146. package/dist/kernel/ports/llm.js +3 -1
  147. package/dist/kernel/ports/model.js +5 -2
  148. package/dist/kernel/ports/narration-aliases.js +60 -0
  149. package/dist/kernel/ports/plan-limits.js +36 -0
  150. package/dist/kernel/runner/attempt.js +18 -9
  151. package/dist/kernel/runner/audition.js +36 -0
  152. package/dist/kernel/runner/graph.js +24 -5
  153. package/dist/kernel/runner/index.js +35 -5
  154. package/dist/kernel/runner/meter.js +1 -0
  155. package/dist/kernel/runner/providers.js +164 -9
  156. package/dist/kernel/runner/retry-policy.js +43 -0
  157. package/dist/kernel/runner/standalone.js +134 -0
  158. package/dist/kernel/runner/work-authority.js +28 -2
  159. package/dist/main.js +321 -20
  160. package/dist/patch-notes/0.4.2-to-1.0.md +93 -0
  161. package/dist/patch-notes/1.0-to-2.0.md +74 -0
  162. package/dist/patch-notes/2.0-to-3.0.md +257 -0
  163. package/dist/patch-notes/3.0.0.md +70 -0
  164. package/dist/patch-notes/index.json +26 -0
  165. package/dist/slices/admission/model.js +35 -0
  166. package/dist/slices/admission/repo.js +24 -4
  167. package/dist/slices/admission/rules.js +126 -1
  168. package/dist/slices/admission/schema.js +68 -1
  169. package/dist/slices/admission/short-mode.js +47 -0
  170. package/dist/slices/admission/start.js +90 -16
  171. package/dist/slices/article/continuation.js +2 -2
  172. package/dist/slices/article/provided-entries.js +2 -2
  173. package/dist/slices/article/run.js +6 -2
  174. package/dist/slices/article/segments.js +3 -3
  175. package/dist/slices/backups/files.js +135 -0
  176. package/dist/slices/backups/folder.js +43 -0
  177. package/dist/slices/backups/model.js +65 -0
  178. package/dist/slices/backups/repo.js +28 -0
  179. package/dist/slices/backups/schedule.js +60 -0
  180. package/dist/slices/backups/service.js +311 -0
  181. package/dist/slices/batch/index.js +9 -2
  182. package/dist/slices/cancel/index.js +1 -1
  183. package/dist/slices/channels/cast-images.js +110 -0
  184. package/dist/slices/channels/cast-match.js +32 -0
  185. package/dist/slices/channels/images.js +19 -0
  186. package/dist/slices/channels/model.js +5 -0
  187. package/dist/slices/channels/rebrand.js +100 -0
  188. package/dist/slices/channels/repo.js +218 -0
  189. package/dist/slices/channels/runs.js +129 -0
  190. package/dist/slices/channels/schema.js +129 -0
  191. package/dist/slices/channels/service.js +180 -0
  192. package/dist/slices/channels/studio-csv.js +110 -0
  193. package/dist/slices/channels/videos.js +120 -0
  194. package/dist/slices/checkpoints/recovery.js +10 -0
  195. package/dist/slices/control/index.js +2 -1
  196. package/dist/slices/document/fonts.js +1 -1
  197. package/dist/slices/document/render.js +1 -1
  198. package/dist/slices/episodes/related.js +90 -0
  199. package/dist/slices/episodes/repo.js +60 -0
  200. package/dist/slices/episodes/service.js +58 -0
  201. package/dist/slices/episodes/summarize.js +170 -0
  202. package/dist/slices/estimate/index.js +113 -24
  203. package/dist/slices/estimate/requests.js +60 -2
  204. package/dist/slices/fixes/rules.js +73 -0
  205. package/dist/slices/fonts/coverage.js +82 -0
  206. package/dist/slices/fonts/sfnt.js +74 -0
  207. package/dist/slices/images/run.js +8 -5
  208. package/dist/slices/images/scale.js +84 -0
  209. package/dist/slices/library/diff.js +122 -0
  210. package/dist/slices/library/history.js +100 -0
  211. package/dist/slices/library/model.js +3 -0
  212. package/dist/slices/library/photorealistic.js +49 -0
  213. package/dist/slices/library/repo.js +19 -14
  214. package/dist/slices/library/save.js +62 -12
  215. package/dist/slices/library/slots.js +15 -3
  216. package/dist/slices/library/snapshot.js +8 -2
  217. package/dist/slices/library/used-by.js +153 -0
  218. package/dist/slices/loudness/level-pieces.js +41 -0
  219. package/dist/slices/loudness/loudnorm.js +164 -0
  220. package/dist/slices/loudness/model.js +153 -0
  221. package/dist/slices/model-upkeep/model.js +10 -0
  222. package/dist/slices/model-upkeep/switch.js +217 -0
  223. package/dist/slices/model-upkeep/usage.js +183 -0
  224. package/dist/slices/narration/aliases-library.js +31 -0
  225. package/dist/slices/narration/aliases-schema.js +53 -0
  226. package/dist/slices/narration/aliases.js +49 -0
  227. package/dist/slices/narration/blocks.js +352 -0
  228. package/dist/slices/narration/chunk.js +19 -7
  229. package/dist/slices/narration/concat.js +2 -2
  230. package/dist/slices/narration/describe.js +45 -0
  231. package/dist/slices/narration/pauses-model.js +63 -0
  232. package/dist/slices/narration/pauses.js +269 -0
  233. package/dist/slices/narration/peaks-model.js +1 -0
  234. package/dist/slices/narration/peaks.js +94 -0
  235. package/dist/slices/narration/preparation.js +21 -3
  236. package/dist/slices/narration/pronunciation-chunks.js +3 -3
  237. package/dist/slices/narration/pronunciation.js +72 -25
  238. package/dist/slices/narration/run.js +6 -6
  239. package/dist/slices/narration/shared-glossary.js +1 -1
  240. package/dist/slices/narration/steering.js +15 -4
  241. package/dist/slices/notifications/notifier.js +94 -0
  242. package/dist/slices/notifications/rules.js +102 -0
  243. package/dist/slices/notifications/send.js +40 -0
  244. package/dist/slices/notifications/settings.js +34 -0
  245. package/dist/slices/onboarding/copy.js +143 -0
  246. package/dist/slices/onboarding/first-run.js +46 -0
  247. package/dist/slices/onboarding/install.js +119 -0
  248. package/dist/slices/onboarding/model.js +64 -0
  249. package/dist/slices/onboarding/packs.js +491 -0
  250. package/dist/slices/onboarding/quick-short.js +129 -0
  251. package/dist/slices/onboarding/sample.js +132 -0
  252. package/dist/slices/onboarding/state.js +68 -0
  253. package/dist/slices/onboarding/template.js +73 -0
  254. package/dist/slices/patch-notes/library.js +73 -0
  255. package/dist/slices/patch-notes/seen.js +77 -0
  256. package/dist/slices/play-drafts/convert.js +160 -9
  257. package/dist/slices/play-drafts/readiness.js +3 -0
  258. package/dist/slices/play-drafts/repo.js +9 -1
  259. package/dist/slices/play-drafts/review-inputs.js +22 -3
  260. package/dist/slices/play-drafts/schema.js +109 -2
  261. package/dist/slices/play-drafts/service.js +10 -1
  262. package/dist/slices/play-drafts/start.js +43 -18
  263. package/dist/slices/play-drafts/uploads.js +2 -1
  264. package/dist/slices/project-templates/from-project.js +70 -4
  265. package/dist/slices/project-templates/one-off.js +26 -0
  266. package/dist/slices/project-templates/repo.js +2 -1
  267. package/dist/slices/project-templates/service.js +15 -4
  268. package/dist/slices/project-templates/setup.js +29 -2
  269. package/dist/slices/rebuild/admission-repo.js +1 -1
  270. package/dist/slices/rebuild/model.js +6 -0
  271. package/dist/slices/rebuild/preview-details.js +67 -2
  272. package/dist/slices/rebuild/preview-plan.js +13 -6
  273. package/dist/slices/rebuild/recipe-audio-parts.js +40 -7
  274. package/dist/slices/rebuild/recipe-audio.js +66 -12
  275. package/dist/slices/rebuild/recipe-build.js +15 -5
  276. package/dist/slices/rebuild/recipe-cast.js +18 -0
  277. package/dist/slices/rebuild/recipe-checkpoints.js +11 -9
  278. package/dist/slices/rebuild/recipe-describe.js +93 -0
  279. package/dist/slices/rebuild/recipe-edit.js +13 -1
  280. package/dist/slices/rebuild/recipe-exports.js +62 -8
  281. package/dist/slices/rebuild/recipe-input-schema.js +43 -1
  282. package/dist/slices/rebuild/recipe-legacy.js +12 -0
  283. package/dist/slices/rebuild/recipe-loudness.js +45 -0
  284. package/dist/slices/rebuild/recipe-model.js +25 -6
  285. package/dist/slices/rebuild/recipe-pauses.js +7 -0
  286. package/dist/slices/rebuild/recipe-preparation.js +3 -3
  287. package/dist/slices/rebuild/recipe-provider-choice.js +11 -1
  288. package/dist/slices/rebuild/recipe-reference.js +58 -0
  289. package/dist/slices/rebuild/recipe-reviews.js +117 -0
  290. package/dist/slices/rebuild/recipe-save.js +4 -1
  291. package/dist/slices/rebuild/recipe-shorts.js +32 -5
  292. package/dist/slices/rebuild/recipe-text.js +114 -19
  293. package/dist/slices/rebuild/recipe-validation.js +29 -3
  294. package/dist/slices/rebuild/recipe-visual.js +104 -29
  295. package/dist/slices/rebuild/recipe-voices.js +286 -0
  296. package/dist/slices/rebuild/recipe-work.js +77 -6
  297. package/dist/slices/rebuild/recipe-youtube.js +2 -0
  298. package/dist/slices/rebuild/recovery-conflict.js +7 -3
  299. package/dist/slices/rebuild/recovery-model.js +3 -0
  300. package/dist/slices/rebuild/recovery-selection.js +28 -2
  301. package/dist/slices/rebuild/recovery.js +35 -17
  302. package/dist/slices/rebuild/repo.js +3 -3
  303. package/dist/slices/rebuild/retry.js +18 -0
  304. package/dist/slices/rebuild/review-redo.js +110 -0
  305. package/dist/slices/rebuild/runtime-actions.js +14 -2
  306. package/dist/slices/rebuild/runtime-admission.js +4 -1
  307. package/dist/slices/rebuild/runtime-animate.js +4 -4
  308. package/dist/slices/rebuild/runtime-article.js +5 -5
  309. package/dist/slices/rebuild/runtime-document.js +4 -4
  310. package/dist/slices/rebuild/runtime-export-bed.js +43 -0
  311. package/dist/slices/rebuild/runtime-export-edit.js +70 -11
  312. package/dist/slices/rebuild/runtime-export-inputs.js +52 -10
  313. package/dist/slices/rebuild/runtime-export-short.js +85 -0
  314. package/dist/slices/rebuild/runtime-export.js +84 -19
  315. package/dist/slices/rebuild/runtime-figure-card.js +87 -0
  316. package/dist/slices/rebuild/runtime-image.js +69 -0
  317. package/dist/slices/rebuild/runtime-local.js +154 -27
  318. package/dist/slices/rebuild/runtime-materialize.js +8 -0
  319. package/dist/slices/rebuild/runtime-narration-reuse.js +2 -1
  320. package/dist/slices/rebuild/runtime-narration-text.js +7 -7
  321. package/dist/slices/rebuild/runtime-pictures.js +28 -0
  322. package/dist/slices/rebuild/runtime-piece-label.js +29 -0
  323. package/dist/slices/rebuild/runtime-provider.js +134 -19
  324. package/dist/slices/rebuild/runtime-publication.js +21 -5
  325. package/dist/slices/rebuild/runtime-review.js +237 -0
  326. package/dist/slices/rebuild/runtime-run.js +17 -11
  327. package/dist/slices/rebuild/runtime-shorts.js +48 -23
  328. package/dist/slices/rebuild/runtime-store.js +39 -10
  329. package/dist/slices/rebuild/runtime-subtitles.js +72 -19
  330. package/dist/slices/rebuild/runtime-voices.js +103 -0
  331. package/dist/slices/rebuild/runtime-youtube.js +7 -6
  332. package/dist/slices/rebuild/service-readiness.js +15 -1
  333. package/dist/slices/rebuild/soften.js +49 -0
  334. package/dist/slices/rebuild/transition-repo.js +10 -3
  335. package/dist/slices/research/run.js +4 -4
  336. package/dist/slices/research/synthesis.js +2 -2
  337. package/dist/slices/reviews/model.js +105 -0
  338. package/dist/slices/reviews/outcome.js +22 -0
  339. package/dist/slices/reviews/repo.js +106 -0
  340. package/dist/slices/reviews/rules.js +63 -0
  341. package/dist/slices/reviews/verdict.js +71 -0
  342. package/dist/slices/reviews/view.js +30 -0
  343. package/dist/slices/revisions/adopt-content.js +12 -2
  344. package/dist/slices/revisions/adopt.js +9 -2
  345. package/dist/slices/revisions/downloads.js +7 -1
  346. package/dist/slices/revisions/image-plan.js +183 -0
  347. package/dist/slices/revisions/mutation-assets.js +43 -9
  348. package/dist/slices/revisions/mutation-cues.js +71 -0
  349. package/dist/slices/revisions/mutation-prepare.js +8 -10
  350. package/dist/slices/revisions/mutation-request.js +2 -1
  351. package/dist/slices/revisions/mutations.js +97 -3
  352. package/dist/slices/revisions/schema.js +16 -2
  353. package/dist/slices/run-cost/limits.js +128 -0
  354. package/dist/slices/run-cost/meter.js +47 -0
  355. package/dist/slices/run-cost/panel.js +234 -0
  356. package/dist/slices/run-cost/pricing.js +92 -0
  357. package/dist/slices/run-cost/week.js +108 -0
  358. package/dist/slices/schedules/agenda.js +158 -0
  359. package/dist/slices/schedules/repo.js +58 -18
  360. package/dist/slices/schedules/scheduler.js +67 -16
  361. package/dist/slices/schedules/schema.js +138 -1
  362. package/dist/slices/schedules/service.js +48 -4
  363. package/dist/slices/schedules/similar.js +90 -0
  364. package/dist/slices/schedules/topic-list.js +163 -0
  365. package/dist/slices/schedules/topics.js +422 -0
  366. package/dist/slices/settings/first-run.js +102 -0
  367. package/dist/slices/settings/health.js +190 -0
  368. package/dist/slices/settings/key-guides.js +128 -0
  369. package/dist/slices/settings/key-test.js +85 -0
  370. package/dist/slices/settings/model.js +11 -1
  371. package/dist/slices/settings/playback.js +15 -0
  372. package/dist/slices/settings/repo.js +17 -3
  373. package/dist/slices/settings/voices.js +64 -4
  374. package/dist/slices/settings/whats-new.js +64 -0
  375. package/dist/slices/shorts/captions.js +9 -0
  376. package/dist/slices/shorts/pick.js +3 -3
  377. package/dist/slices/shorts/prompts.js +1 -1
  378. package/dist/slices/shorts/render.js +54 -5
  379. package/dist/slices/storage/asset-name.js +3 -0
  380. package/dist/slices/storage/backup-export.js +48 -9
  381. package/dist/slices/storage/backup-format.js +38 -1
  382. package/dist/slices/storage/backup-import.js +191 -19
  383. package/dist/slices/storage/delete-project.js +7 -4
  384. package/dist/slices/storage/documents.js +79 -0
  385. package/dist/slices/storage/files-location.js +531 -0
  386. package/dist/slices/storage/layout.js +20 -0
  387. package/dist/slices/storage/model.js +14 -0
  388. package/dist/slices/storage/portable.js +95 -12
  389. package/dist/slices/storage/reconcile.js +19 -2
  390. package/dist/slices/storage/repo.js +4 -1
  391. package/dist/slices/storage/schema.js +11 -0
  392. package/dist/slices/storage/trim.js +146 -0
  393. package/dist/slices/studio/disclosure.js +74 -0
  394. package/dist/slices/studio/model.js +28 -0
  395. package/dist/slices/studio/pack.js +209 -0
  396. package/dist/slices/studio/settings.js +133 -0
  397. package/dist/slices/style-preview/images.js +61 -0
  398. package/dist/slices/style-preview/render.js +161 -0
  399. package/dist/slices/style-preview/schema.js +45 -0
  400. package/dist/slices/style-preview/service.js +93 -0
  401. package/dist/slices/style-preview/settings.js +89 -0
  402. package/dist/slices/subtitles/captions.js +42 -9
  403. package/dist/slices/subtitles/model.js +22 -0
  404. package/dist/slices/subtitles/prepare.js +27 -16
  405. package/dist/slices/subtitles/transcript.js +1 -1
  406. package/dist/slices/thumbnail/run.js +7 -7
  407. package/dist/slices/trash/model.js +25 -0
  408. package/dist/slices/trash/service.js +273 -0
  409. package/dist/slices/uploads/repo.js +20 -0
  410. package/dist/slices/video/ambient-bed-schema.js +15 -0
  411. package/dist/slices/video/ambient-bed.js +95 -0
  412. package/dist/slices/video/ambient-mix.js +72 -0
  413. package/dist/slices/video/audio-inputs.js +2 -2
  414. package/dist/slices/video/cards.js +5 -2
  415. package/dist/slices/video/edit-list.js +23 -2
  416. package/dist/slices/video/edit-settings.js +13 -3
  417. package/dist/slices/video/ffmpeg.js +63 -9
  418. package/dist/slices/video/figure-card.js +492 -0
  419. package/dist/slices/video/figure-spans.js +51 -0
  420. package/dist/slices/video/plan.js +125 -13
  421. package/dist/slices/video/run.js +14 -12
  422. package/dist/slices/video/slideshow.js +39 -4
  423. package/dist/slices/voices/attribution.js +69 -0
  424. package/dist/slices/voices/audio-files.js +82 -0
  425. package/dist/slices/voices/audition.js +25 -0
  426. package/dist/slices/voices/cast.js +56 -0
  427. package/dist/slices/voices/grouping.js +49 -0
  428. package/dist/slices/voices/join.js +35 -0
  429. package/dist/slices/voices/languages.js +25 -0
  430. package/dist/slices/voices/model.js +179 -0
  431. package/dist/slices/voices/palette.js +26 -0
  432. package/dist/slices/voices/panel.js +91 -0
  433. package/dist/slices/voices/portraits.js +55 -0
  434. package/dist/slices/voices/script.js +136 -0
  435. package/dist/slices/voices/timing.js +58 -0
  436. package/dist/slices/youtube/answer.js +1 -1
  437. package/dist/slices/youtube/chapters.js +126 -0
  438. package/dist/slices/youtube/edits-repo.js +69 -0
  439. package/dist/slices/youtube/edits.js +89 -0
  440. package/dist/slices/youtube/placeholders.js +98 -0
  441. package/dist/slices/youtube/transcript.js +11 -4
  442. package/dist/updater/service.js +66 -21
  443. package/dist/web/assets/index-CUA2sUxU.css +1 -0
  444. package/dist/web/assets/index-CX590kE-.js +303 -0
  445. package/dist/web/assets/jetbrains-mono-cyrillic-400-normal-BEIGL1Tu.woff2 +0 -0
  446. package/dist/web/assets/jetbrains-mono-cyrillic-400-normal-ugxPyKxw.woff +0 -0
  447. package/dist/web/assets/jetbrains-mono-greek-400-normal-B9oWc5Lo.woff +0 -0
  448. package/dist/web/assets/jetbrains-mono-greek-400-normal-C190GLew.woff2 +0 -0
  449. package/dist/web/assets/jetbrains-mono-latin-400-normal-6-qcROiO.woff +0 -0
  450. package/dist/web/assets/jetbrains-mono-latin-400-normal-V6pRDFza.woff2 +0 -0
  451. package/dist/web/assets/jetbrains-mono-latin-ext-400-normal-Bc8Ftmh3.woff2 +0 -0
  452. package/dist/web/assets/jetbrains-mono-latin-ext-400-normal-fXTG6kC5.woff +0 -0
  453. package/dist/web/assets/jetbrains-mono-vietnamese-400-normal-CqNFfHCs.woff +0 -0
  454. package/dist/web/assets/{pdf-CAJNT254.js → pdf-BG8n_uc5.js} +1 -1
  455. package/dist/web/index.html +2 -2
  456. package/package.json +4 -4
  457. package/dist/edge/docker-launch.js +0 -81
  458. package/dist/edge/docker-projects/activation.js +0 -35
  459. package/dist/edge/docker-projects/claims.js +0 -85
  460. package/dist/edge/docker-projects/committed.js +0 -40
  461. package/dist/edge/docker-projects/engine.js +0 -461
  462. package/dist/edge/docker-projects/install.js +0 -254
  463. package/dist/edge/docker-projects/prune.js +0 -66
  464. package/dist/edge/docker-projects/recover.js +0 -100
  465. package/dist/edge/docker-projects/state.js +0 -225
  466. package/dist/web/assets/index-BGgYMm2u.css +0 -1
  467. package/dist/web/assets/index-BVVgtntL.js +0 -171
  468. package/scripts/docker-run.sh +0 -3
  469. /package/dist/edge/{docker-projects → docker-install}/tree.js +0 -0
@@ -0,0 +1,41 @@
1
+ // What an image API is told about the establishing image sent with a request, ahead of the
2
+ // brief. An edit endpoint otherwise treats its input as the picture to change, so it says the
3
+ // input is a reference for the look and the cast, not the composition to keep.
4
+ export const referenceNote = "The attached image is a visual reference only: keep its characters, rendering style and colour palette, but do not copy its composition, pose or framing. Compose a new image from this brief:";
5
+ export function withReferenceNote(prompt, reference) {
6
+ return reference === undefined ? prompt : `${referenceNote}\n\n${prompt}`;
7
+ }
8
+ // Every picture a request carries, in the order they are attached: the establishing image,
9
+ // then each mentioned cast member's pictures.
10
+ export function referencePictures(req) {
11
+ return [
12
+ ...(req.reference === undefined ? [] : [{ image: req.reference }]),
13
+ ...(req.cast ?? []).flatMap((member) => member.images.map((image) => ({ image, member: member.name }))),
14
+ ];
15
+ }
16
+ // The prompt a provider sends with the first `limit` of `referencePictures(req)` attached. A
17
+ // request without cast keeps exactly the wording it always had. Cast members whose pictures
18
+ // did not fit (a provider that takes fewer input images, or none) are described in words.
19
+ export function withReferences(prompt, req, limit = Number.POSITIVE_INFINITY) {
20
+ if (req.cast === undefined || req.cast.length === 0)
21
+ return limit === 0 ? prompt : withReferenceNote(prompt, req.reference);
22
+ const sent = referencePictures(req).slice(0, limit);
23
+ const lines = [];
24
+ if (sent.length > 0)
25
+ lines.push("The attached images are visual references only: do not copy their composition, pose or framing.");
26
+ for (const [index, picture] of sent.entries()) {
27
+ const member = req.cast.find((row) => row.name === picture.member);
28
+ lines.push(member === undefined
29
+ ? `Image ${String(index + 1)} is the establishing image: keep its characters, rendering style and colour palette.`
30
+ : `Image ${String(index + 1)} shows ${member.name}${describe(member.description)}: draw ${member.name} to look exactly like this.`);
31
+ }
32
+ const shown = new Set(sent.flatMap((picture) => picture.member ?? []));
33
+ for (const member of req.cast)
34
+ if (!shown.has(member.name))
35
+ lines.push(`${member.name} appears in this image${describe(member.description)}.`);
36
+ lines.push("Compose a new image from this brief:");
37
+ return `${lines.join("\n")}\n\n${prompt}`;
38
+ }
39
+ function describe(description) {
40
+ return description.trim() === "" ? "" : ` (${description.trim()})`;
41
+ }
@@ -4,6 +4,7 @@ import { providerError } from "../../kernel/ports/model.js";
4
4
  import { httpFailure, missingKey, noImage, providerSaid, refusedImage, unreadable, } from "../explain.js";
5
5
  import { retryAfter } from "../retry-after.js";
6
6
  import { downloadImage } from "./bytes.js";
7
+ import { withReferences } from "./reference.js";
7
8
  import { dataUri, downloadVideo } from "./video.js";
8
9
  // The HTTP gateway adapter for Replicate: `fetch`, the injected clock and the downloader
9
10
  // beside this file, no SDK. The `replicate` package wraps exactly the two requests below, so
@@ -41,6 +42,13 @@ export function replicateImage(deps) {
41
42
  id: "replicate",
42
43
  models: () => Promise.resolve(replicateModels),
43
44
  generate: async (req) => {
45
+ // The Replicate models listed take no input image to draw from (the FLUX image inputs
46
+ // copy the picture's composition), so an establishing image is refused, not dropped.
47
+ if (req.reference !== undefined)
48
+ throw providerError({
49
+ kind: "unsupported",
50
+ message: "Replicate's image models can't use an establishing image as a reference. Choose Codex CLI, OpenAI, Google or fal.ai under Images → Provider on Play or in Edit project → Providers, or set Establishing image to Off in the Images section.",
51
+ });
44
52
  const response = await deps.fetch(`${replicateBase}/models/${req.model}/predictions`, {
45
53
  method: "POST",
46
54
  signal: req.signal,
@@ -51,7 +59,8 @@ export function replicateImage(deps) {
51
59
  },
52
60
  body: JSON.stringify({
53
61
  input: {
54
- prompt: req.prompt,
62
+ // These models take no input image, so the cast is described in words.
63
+ prompt: withReferences(req.prompt, req, 0),
55
64
  // Replicate takes the aspect, so the closest size is exact.
56
65
  aspect_ratio: req.aspect,
57
66
  // The port stores PNG or JPEG and these models default to WebP. Nothing else
@@ -159,7 +168,7 @@ function declined(current) {
159
168
  kind: refusal ? "refusal" : "other",
160
169
  message: refusal
161
170
  ? refusedImage("Replicate", message)
162
- : providerSaid("Replicate", "could not make the image", message, "Use Retry stage; if it keeps happening, reword the image prompt in the Images section of Edit project or choose another image model in its Providers section."),
171
+ : providerSaid("Replicate", "could not make the image", message, "Use Try again; if it keeps happening, reword the image prompt in the Images section of Edit project or choose another image model in its Providers section."),
163
172
  });
164
173
  }
165
174
  function auth(deps) {
@@ -181,6 +190,9 @@ function kindOf(status) {
181
190
  // ceiling: everything else is `other` and is retried, so a 402 with no credit left fails the
182
191
  // same way four times over. A terminal "this will never work" kind has to reach the port's
183
192
  // error contract first, which is not this adapter's to widen.
193
+ // The provider's own server failed; the same request may well succeed later.
194
+ if (status >= 500)
195
+ return "dropped";
184
196
  return "other";
185
197
  }
186
198
  async function failure(response, subject = "image request") {
@@ -16,13 +16,13 @@ export async function downloadVideo(download) {
16
16
  // The link is not quoted back: a delivery link carries its own signature.
17
17
  throw providerError({
18
18
  kind: "other",
19
- message: `${download.provider} made the video clip, but Slopify could not download it (error ${String(response.status)}). Check your internet connection, then use Retry stage.`,
19
+ message: `${download.provider} made the video clip, but Slopify could not download it (error ${String(response.status)}). Check your internet connection, then use Try again.`,
20
20
  });
21
21
  const bytes = new Uint8Array(await response.arrayBuffer());
22
22
  if (!sniffVideo(bytes))
23
23
  throw providerError({
24
24
  kind: "other",
25
- message: `${download.provider} sent back something that is not an MP4 video clip (${describeBytes(bytes)}). Use Retry stage; if it keeps happening, choose another image-to-video model under Animate images in Edit project → Inputs → Look.`,
25
+ message: `${download.provider} sent back something that is not an MP4 video clip (${describeBytes(bytes)}). Use Try again; if it keeps happening, choose another image-to-video model under Animate images in Edit project → Inputs → Look.`,
26
26
  });
27
27
  return { bytes, mime: "video/mp4" };
28
28
  }
@@ -0,0 +1,36 @@
1
+ import { cartesiaVersion } from "./tts/cartesia.js";
2
+ // Settings → Providers' Test button, per provider. Some answer a key they do not recognise
3
+ // with something other than 401: Google with 400 API_KEY_INVALID, ElevenLabs with 400
4
+ // invalid_api_key, Inworld with 403 "does not exist". Observed on 2026-09-27; their
5
+ // documentation says 401.
6
+ const bearer = (key) => ({ Authorization: `Bearer ${key}` });
7
+ export const keyProbes = {
8
+ openrouter: { url: "https://openrouter.ai/api/v1/key", headers: bearer },
9
+ "openai-tts": { url: "https://api.openai.com/v1/models", headers: bearer },
10
+ "openai-image": { url: "https://api.openai.com/v1/models", headers: bearer },
11
+ "google-image": {
12
+ url: "https://generativelanguage.googleapis.com/v1beta/models?pageSize=1",
13
+ headers: (key) => ({ "x-goog-api-key": key }),
14
+ badKey: (status, body) => status === 400 && body.includes("API_KEY_INVALID"),
15
+ },
16
+ // fal.ai's model list answers without a key; its price lookup requires one.
17
+ fal: {
18
+ url: "https://api.fal.ai/v1/models/pricing?endpoint_id=fal-ai/flux/dev",
19
+ headers: (key) => ({ Authorization: `Key ${key}` }),
20
+ },
21
+ replicate: { url: "https://api.replicate.com/v1/account", headers: bearer },
22
+ elevenlabs: {
23
+ url: "https://api.elevenlabs.io/v1/models",
24
+ headers: (key) => ({ "xi-api-key": key }),
25
+ badKey: (status, body) => status === 400 && body.includes("invalid_api_key"),
26
+ },
27
+ cartesia: {
28
+ url: "https://api.cartesia.ai/voices?limit=1",
29
+ headers: (key) => ({ "X-API-Key": key, "Cartesia-Version": cartesiaVersion }),
30
+ },
31
+ inworld: {
32
+ url: "https://api.inworld.ai/voices/v1/voices?pageSize=1",
33
+ headers: (key) => ({ Authorization: `Basic ${key}` }),
34
+ badKey: (status, body) => status === 403 && /does not exist|was deleted/i.test(body),
35
+ },
36
+ };
@@ -1,11 +1,13 @@
1
1
  import { z } from "zod";
2
2
  import { redact } from "../../kernel/log.js";
3
3
  import { providerError } from "../../kernel/ports/model.js";
4
+ import { isoOfEpoch } from "../../kernel/ports/plan-limits.js";
4
5
  import { cliCheck, cliReported, commandOf } from "../explain.js";
5
6
  import { nodeClaudeCodeModels } from "./claude-code-models.js";
6
7
  import { cliLoginError } from "./cli-login-error.js";
7
8
  import { documentToolName, documentWorkspace, } from "./document-workspace.js";
8
- import { cliEvent, cliInput, cliShaped, endedWithout, promptOf, stopCliRun, stuckCli, } from "./run-cli.js";
9
+ import { imageWorkspace, readFileName, unseenImages, } from "./image-workspace.js";
10
+ import { cliEvent, cliInput, cliShaped, endedKind, endedWithout, promptOf, stopCliRun, stuckCli, } from "./run-cli.js";
9
11
  import { lines } from "./sse-lines.js";
10
12
  // The local-agent adapter for Claude Code: spawned non-interactively, authenticated by the
11
13
  // CLI's own login, no key anywhere in this file. Readiness is not computed here - `adapters/**`
@@ -24,7 +26,14 @@ const writingRole = "You are the writing and research component of Slopify, a vi
24
26
  // never something a stage does quietly, and no stage should touch the disk. `--tools ""`
25
27
  // empties the built-in set and `--strict-mcp-config` drops the user's MCP servers; the init
26
28
  // event of a run with both reports `"tools":[]` and `"mcp_servers":[]`.
27
- export function claudeCodeArgs(req, documents) {
29
+ //
30
+ // Pictures (a review's) are copied into a private folder that becomes the working directory,
31
+ // and only the Read tool is allowed: `claude --help` 2.1.281 says `--restricted` confines the
32
+ // file tools to the working directories, and Read hands an image file to the model as an
33
+ // image. The run's tool calls are checked afterwards: every picture must have been read.
34
+ export function claudeCodeArgs(req, documents, images) {
35
+ const isolated = documents !== undefined || images !== undefined;
36
+ const tools = [req.webSearch === true ? "WebSearch" : "", images ? "Read" : ""].filter(Boolean);
28
37
  return [
29
38
  "-p",
30
39
  "--output-format",
@@ -34,7 +43,7 @@ export function claudeCodeArgs(req, documents) {
34
43
  "--include-partial-messages",
35
44
  // `claude --help` 2.1.263: safe mode excludes CLAUDE.md, output styles, skills and
36
45
  // hooks while retaining login and managed policy. --bare would discard OAuth.
37
- ...(documents
46
+ ...(isolated
38
47
  ? [
39
48
  "--restricted",
40
49
  "--setting-sources",
@@ -55,18 +64,17 @@ export function claudeCodeArgs(req, documents) {
55
64
  writingRole +
56
65
  (documents
57
66
  ? " Use the explicitly supplied research MCP tool to read request documents; it is the only permitted local reference source."
67
+ : "") +
68
+ (images
69
+ ? " Open the supplied image files in the current folder with the Read tool; they are the only local files you may open."
58
70
  : ""),
59
71
  "--strict-mcp-config",
60
72
  "--tools",
61
- req.webSearch === true ? "WebSearch" : "",
62
- ...(documents ? ["--mcp-config", documents.config, "--permission-mode", "dontAsk"] : []),
63
- ...(req.webSearch === true || documents
64
- ? [
65
- "--allowedTools",
66
- [req.webSearch === true ? "WebSearch" : "", documents ? documentToolName : ""]
67
- .filter(Boolean)
68
- .join(","),
69
- ]
73
+ tools.join(","),
74
+ ...(documents ? ["--mcp-config", documents.config] : []),
75
+ ...(isolated ? ["--permission-mode", "dontAsk"] : []),
76
+ ...(tools.length > 0 || documents
77
+ ? ["--allowedTools", [...tools, documents ? documentToolName : ""].filter(Boolean).join(",")]
70
78
  : []),
71
79
  ...(req.model === "" ? [] : ["--model", req.model]),
72
80
  ...(req.thinking !== undefined && req.thinking !== "off" ? ["--effort", req.thinking] : []),
@@ -76,7 +84,20 @@ export function claudeCodeArgs(req, documents) {
76
84
  }
77
85
  const assistantEvent = z.object({
78
86
  message: z.object({
79
- content: z.array(z.object({ type: z.string(), text: z.string().optional() })),
87
+ content: z.array(z.object({
88
+ type: z.string(),
89
+ text: z.string().optional(),
90
+ name: z.string().optional(),
91
+ input: z.object({ file_path: z.unknown().optional() }).loose().optional(),
92
+ })),
93
+ }),
94
+ });
95
+ // `--include-partial-messages` streams the model's own events; a text block grows by
96
+ // `content_block_delta` events carrying a `text_delta`. Anything else is only a heartbeat.
97
+ const partialText = z.object({
98
+ event: z.object({
99
+ type: z.literal("content_block_delta"),
100
+ delta: z.object({ type: z.literal("text_delta"), text: z.string() }),
80
101
  }),
81
102
  });
82
103
  // The result event as 2.1.258 writes it. `subtype` stays "success" even for a failed run -
@@ -90,29 +111,55 @@ const resultEvent = z.object({
90
111
  stop_reason: z.string().nullish(),
91
112
  api_error_status: z.number().nullish(),
92
113
  modelUsage: z
93
- .record(z.string(), z.object({ inputTokens: z.number(), outputTokens: z.number() }))
114
+ .record(z.string(), z.object({
115
+ inputTokens: z.number(),
116
+ outputTokens: z.number(),
117
+ cacheReadInputTokens: z.number().optional(),
118
+ cacheCreationInputTokens: z.number().optional(),
119
+ }))
94
120
  .optional(),
95
121
  });
122
+ // The plan windows as 2.1.258 reports them around every call, in `rate_limit_event`:
123
+ // `utilization` is the used fraction of the window, `resetsAt` epoch seconds, and `status`
124
+ // turns "rejected" when the allowance is used up.
125
+ const limitWindow = z.object({ utilization: z.number(), resetsAt: z.number().nullish() }).partial();
126
+ const rateLimitEvent = z.object({
127
+ rate_limit_info: z.object({
128
+ status: z.string().optional(),
129
+ resetsAt: z.number().nullish(),
130
+ unifiedWindows: z
131
+ .object({ five_hour: limitWindow.optional(), seven_day: limitWindow.optional() })
132
+ .partial()
133
+ .optional(),
134
+ }),
135
+ });
96
136
  export function claudeCodeLlm(deps) {
97
137
  const binary = deps.binary ?? claudeCodeBinary;
98
138
  async function* complete(req) {
99
139
  req.signal.throwIfAborted();
100
140
  const documents = documentWorkspace(req.documents);
141
+ let images;
142
+ const read = new Set();
101
143
  let run;
102
144
  let ended;
145
+ let limits;
103
146
  try {
104
- run = deps.run(binary, claudeCodeArgs(req, documents), req.signal, {
105
- ...(documents
147
+ images = imageWorkspace(req.images);
148
+ const folder = images?.directory ?? documents?.directory;
149
+ run = deps.run(binary, claudeCodeArgs(req, documents, images), req.signal, {
150
+ ...(folder !== undefined
106
151
  ? {
107
- cwd: documents.directory,
152
+ cwd: folder,
108
153
  env: {
109
154
  ...(deps.env ?? process.env),
110
155
  CLAUDE_CODE_SAFE_MODE: "0",
111
- PWD: documents.directory,
156
+ PWD: folder,
112
157
  },
113
158
  }
114
159
  : {}),
115
- stdin: cliInput([documents?.instructions, promptOf(req.messages)].filter(Boolean).join("\n\n")),
160
+ stdin: cliInput([documents?.instructions, images?.instructions, promptOf(req.messages)]
161
+ .filter(Boolean)
162
+ .join("\n\n")),
116
163
  });
117
164
  for await (const line of lines(run.stdout, req.signal)) {
118
165
  if (line.trim() === "") {
@@ -120,9 +167,34 @@ export function claudeCodeLlm(deps) {
120
167
  }
121
168
  const event = cliEvent(binary, line);
122
169
  // Partial text/thinking and tool events are a heartbeat. Full assistant
123
- // messages remain the sole prose source so text is never appended twice.
170
+ // messages remain the sole prose source so text is never appended twice; partial
171
+ // prose is only shown on the live panel.
124
172
  yield { type: "activity" };
125
173
  req.signal.throwIfAborted();
174
+ if (event.type === "stream_event" && !documents) {
175
+ const typed = partialText.safeParse(event.value);
176
+ if (typed.success && typed.data.event.delta.text !== "")
177
+ yield { type: "partial", text: typed.data.event.delta.text };
178
+ continue;
179
+ }
180
+ if (event.type === "assistant" && images) {
181
+ for (const block of cliShaped(binary, assistantEvent, event.value).message.content) {
182
+ const file = block.type === "tool_use" && block.name === "Read"
183
+ ? readFileName(block.input?.file_path)
184
+ : undefined;
185
+ if (file !== undefined)
186
+ read.add(file);
187
+ }
188
+ continue;
189
+ }
190
+ if (event.type === "rate_limit_event") {
191
+ // A shape this version does not know is not a failure: the plan windows are
192
+ // reported beside the answer, never needed for it.
193
+ const parsed = rateLimitEvent.safeParse(event.value);
194
+ if (parsed.success)
195
+ limits = parsed.data.rate_limit_info;
196
+ continue;
197
+ }
126
198
  if (event.type === "assistant" && !documents) {
127
199
  for (const block of cliShaped(binary, assistantEvent, event.value).message.content) {
128
200
  // A turn also carries `thinking` and `tool_use` blocks; only the prose is the
@@ -143,25 +215,36 @@ export function claudeCodeLlm(deps) {
143
215
  catch {
144
216
  throw providerError({
145
217
  kind: "unavailable",
146
- message: "Slopify could not hand the whole prompt to the Claude Code CLI, so it cannot tell what the CLI worked on (it may already have used your quota). Use Retry stage to run it again.",
218
+ message: "Slopify could not hand the whole prompt to the Claude Code CLI, so it cannot tell what the CLI worked on (it may already have used your quota). Use Try again to run it again.",
147
219
  });
148
220
  }
149
221
  if (result.is_error === true || result.subtype !== "success") {
150
222
  const login = cliLoginError("claude-code", result.result ?? result.subtype);
151
223
  if (login)
152
224
  throw login;
225
+ const planLimit = claudePlanLimit(result.result ?? result.subtype, result.api_error_status ?? null, limits);
226
+ if (planLimit !== undefined)
227
+ throw providerError({
228
+ kind: "rate_limit",
229
+ message: `Your Claude plan's usage limit is used up (Claude Code said: ${redact(result.result ?? result.subtype)}). Slopify waits for it to reset and then carries on by itself.`,
230
+ planLimit,
231
+ });
153
232
  throw providerError({
154
233
  kind: kindOf(result.api_error_status ?? null),
155
234
  message: cliReported(binary, redact(result.result ?? result.subtype), nextStep(binary, result.api_error_status ?? null)),
156
235
  });
157
236
  }
158
237
  documents?.verifyRead();
159
- if (documents && result.result)
238
+ if (images && images.unread(read).length > 0)
239
+ throw unseenImages();
240
+ if ((documents || images) && result.result)
160
241
  yield { type: "delta", text: result.result };
242
+ const windows = limits === undefined ? [] : claudeWindows(limits);
161
243
  yield {
162
244
  type: "done",
163
245
  usage: usageOf(result.modelUsage),
164
246
  finishReason: result.stop_reason ?? null,
247
+ ...(windows.length === 0 ? {} : { limits: { after: windows } }),
165
248
  };
166
249
  return;
167
250
  }
@@ -181,6 +264,7 @@ export function claudeCodeLlm(deps) {
181
264
  }
182
265
  finally {
183
266
  documents?.remove();
267
+ images?.remove();
184
268
  }
185
269
  }
186
270
  // A cancelled run ends its stream the same way an exhausted one does: the child was
@@ -189,19 +273,19 @@ export function claudeCodeLlm(deps) {
189
273
  req.signal.throwIfAborted();
190
274
  // The stream ended with no result event at all.
191
275
  if (!run)
192
- throw new Error("The Claude Code CLI could not be started. Check it is installed and set up in Settings → Providers, then use Retry stage.");
276
+ throw new Error("The Claude Code CLI could not be started. Check it is installed and set up in Settings → Providers, then use Try again.");
193
277
  const login = cliLoginError("claude-code", run.stderr());
194
278
  if (login)
195
279
  throw login;
196
280
  throw providerError({
197
- kind: "other",
281
+ kind: endedKind(ended),
198
282
  message: ended === undefined ? stuckCli(binary) : endedWithout(binary, ended, run.stderr()),
199
283
  });
200
284
  }
201
285
  return {
202
286
  id: "claude-code",
203
287
  // Partial messages keep the deadline alive; complete assistant turns supply prose.
204
- capabilities: { streams: true, reportsUsage: true, webSearch: true },
288
+ capabilities: { streams: true, reportsUsage: true, webSearch: true, images: true },
205
289
  models: deps.readModels ?? (() => nodeClaudeCodeModels(binary)),
206
290
  complete,
207
291
  };
@@ -215,27 +299,79 @@ function kindOf(status) {
215
299
  if (status === 429) {
216
300
  return "rate_limit";
217
301
  }
302
+ // The provider's own server failed; the same request may well succeed later.
303
+ if (status !== null && status >= 500)
304
+ return "dropped";
218
305
  return "other";
219
306
  }
220
307
  // What most often fixes the upstream status the CLI passed on.
221
308
  function nextStep(binary, status) {
222
309
  if (status === 401 || status === 403)
223
- return `Run "${commandOf(binary)} auth login" in a terminal to sign in again, then use Retry stage.`;
310
+ return `Run "${commandOf(binary)} auth login" in a terminal to sign in again, then use Try again.`;
224
311
  if (status === 429)
225
- return "Your Claude usage limit may be used up: wait until it resets, then use Retry stage.";
312
+ return "Your Claude usage limit may be used up: wait until it resets, then use Try again.";
226
313
  if (status === 404)
227
- return "The chosen model may not exist or not be available to your account: choose another in the Providers section of Edit project, then use Retry stage.";
314
+ return "The chosen model may not exist or not be available to your account: choose another in the Providers section of Edit project, then use Try again.";
228
315
  return cliCheck(binary);
229
316
  }
230
317
  // `modelUsage` is keyed by model id and a run may touch more than one - a fallback model,
231
318
  // a sub-agent - so the counts are summed. An empty object means the CLI reported none.
232
- function usageOf(usage) {
233
- const entries = Object.values(usage ?? {});
319
+ // The CLI's `inputTokens` leaves out the prompt it read from or wrote to its cache, which on
320
+ // a short call is nearly all of it, so both are added back: tokens in is every input token.
321
+ export function usageOf(usage) {
322
+ const entries = Object.entries(usage ?? {});
234
323
  if (entries.length === 0) {
235
324
  return null;
236
325
  }
326
+ const cached = entries.reduce((total, [, one]) => total + (one.cacheReadInputTokens ?? 0), 0);
327
+ return {
328
+ inputTokens: entries.reduce((total, [, one]) => total +
329
+ one.inputTokens +
330
+ (one.cacheReadInputTokens ?? 0) +
331
+ (one.cacheCreationInputTokens ?? 0), 0),
332
+ outputTokens: entries.reduce((total, [, one]) => total + one.outputTokens, 0),
333
+ ...(cached === 0 ? {} : { cachedInputTokens: cached }),
334
+ // Only a single model is named: a run split across two has no one price.
335
+ ...(entries.length === 1 && entries[0] !== undefined ? { model: entries[0][0] } : {}),
336
+ };
337
+ }
338
+ // The 5-hour and 7-day windows, used fraction turned into a percentage.
339
+ export function claudeWindows(info) {
340
+ const windows = [];
341
+ const named = [
342
+ ["five_hour", info.unifiedWindows?.five_hour],
343
+ ["weekly", info.unifiedWindows?.seven_day],
344
+ ];
345
+ for (const [kind, one] of named) {
346
+ if (one?.utilization === undefined)
347
+ continue;
348
+ windows.push({
349
+ kind,
350
+ // Rounded to hundredths of a percent, so 0.07 reads 7 rather than 7.000000000000001.
351
+ usedPercent: Math.max(0, Math.min(100, Math.round(one.utilization * 10_000) / 100)),
352
+ resetsAt: isoOfEpoch(one.resetsAt),
353
+ });
354
+ }
355
+ return windows;
356
+ }
357
+ // Claude Code ends a run refused for the plan's allowance with a 429 whose text is
358
+ // "Claude AI usage limit reached|<epoch seconds>" (the recorded shape), and says so again in
359
+ // the `rate_limit_event` it wrote before it, whose status is then "rejected". A 429 that is
360
+ // neither is an ordinary rate limit and is retried as one.
361
+ export function claudePlanLimit(text, status, info) {
362
+ const stamped = /usage limit reached\|(\d{9,13})/i.exec(text);
363
+ if (stamped?.[1] !== undefined)
364
+ return { account: "claude-code", resetsAt: isoOfEpoch(Number(stamped[1])) };
365
+ const rejected = info?.status === "rejected";
366
+ if (!rejected && !(status === 429 && /usage limit|hit your limit|limit reached/i.test(text)))
367
+ return undefined;
368
+ const full = [info?.unifiedWindows?.five_hour, info?.unifiedWindows?.seven_day]
369
+ .filter((one) => (one?.utilization ?? 0) >= 1)
370
+ .map((one) => isoOfEpoch(one?.resetsAt))
371
+ .filter((at) => at !== null)
372
+ .toSorted();
237
373
  return {
238
- inputTokens: entries.reduce((total, one) => total + one.inputTokens, 0),
239
- outputTokens: entries.reduce((total, one) => total + one.outputTokens, 0),
374
+ account: "claude-code",
375
+ resetsAt: full.at(-1) ?? (rejected ? isoOfEpoch(info?.resetsAt) : null),
240
376
  };
241
377
  }
@@ -12,6 +12,6 @@ export function cliLoginError(provider, text) {
12
12
  : "codex login";
13
13
  return providerError({
14
14
  kind: "missing_key",
15
- message: `The ${cliName(provider)} is not signed in, or its sign-in has expired. Open a terminal on the computer running the CLI, run "${command}" and sign in, then use Retry stage.`,
15
+ message: `The ${cliName(provider)} is not signed in, or its sign-in has expired. Open a terminal on the computer running the CLI, run "${command}" and sign in, then use Try again.`,
16
16
  });
17
17
  }
@@ -0,0 +1,157 @@
1
+ import { spawn } from "node:child_process";
2
+ import { mkdtemp, rm } from "node:fs/promises";
3
+ import { tmpdir } from "node:os";
4
+ import { join } from "node:path";
5
+ import { z } from "zod";
6
+ import { cliCommand } from "../../kernel/cli-command.js";
7
+ import { isoOfEpoch, windowKindOf, } from "../../kernel/ports/plan-limits.js";
8
+ // The Codex plan windows. `codex exec --json` reports none, so they are asked of the CLI's
9
+ // app server with `account/rateLimits/read` - a metadata-only exchange that starts no thread
10
+ // and spends nothing. Measured on 0.155.1: the reply carries `rateLimits.primary` and
11
+ // `.secondary`, each `{ usedPercent, windowDurationMins, resetsAt }` with `resetsAt` in epoch
12
+ // seconds; a ChatGPT Pro plan answered a weekly (10080-minute) primary and no secondary.
13
+ const windowSchema = z
14
+ .object({
15
+ usedPercent: z.number(),
16
+ windowDurationMins: z.number().nullish(),
17
+ resetsAt: z.number().nullish(),
18
+ })
19
+ .nullish();
20
+ const readSchema = z.object({
21
+ rateLimits: z.object({ primary: windowSchema, secondary: windowSchema }),
22
+ });
23
+ export function codexWindows(result) {
24
+ const parsed = readSchema.safeParse(result);
25
+ if (!parsed.success)
26
+ return [];
27
+ const windows = [];
28
+ for (const one of [parsed.data.rateLimits.primary, parsed.data.rateLimits.secondary]) {
29
+ if (one === null || one === undefined)
30
+ continue;
31
+ windows.push({
32
+ kind: windowKindOf(one.windowDurationMins),
33
+ usedPercent: Math.max(0, Math.min(100, one.usedPercent)),
34
+ resetsAt: isoOfEpoch(one.resetsAt),
35
+ });
36
+ }
37
+ return windows;
38
+ }
39
+ // Null whenever the answer cannot be had - no CLI, signed out, an older version without the
40
+ // method, a slow start: the windows are reported beside a run, never needed for it.
41
+ export async function nodeCodexLimits(binary, env, timeoutMs = 10_000) {
42
+ const command = cliCommand(binary);
43
+ const directory = await mkdtemp(join(tmpdir(), "slopify-codex-limits-"));
44
+ const child = spawn(command.file, [...command.args, "app-server", "--listen", "stdio://"], {
45
+ cwd: directory,
46
+ env: { ...env, PWD: directory },
47
+ stdio: ["pipe", "pipe", "pipe"],
48
+ windowsHide: true,
49
+ detached: process.platform !== "win32",
50
+ });
51
+ child.on("error", () => { });
52
+ child.stdin.on("error", () => { });
53
+ child.stderr.resume();
54
+ const closed = new Promise((resolve) => child.once("close", () => resolve()));
55
+ const terminate = (signal) => {
56
+ if (process.platform !== "win32" && child.pid !== undefined) {
57
+ try {
58
+ process.kill(-child.pid, signal);
59
+ return;
60
+ }
61
+ catch {
62
+ /* The process may already have exited. */
63
+ }
64
+ }
65
+ child.kill(signal);
66
+ };
67
+ const timer = setTimeout(() => terminate("SIGKILL"), timeoutMs);
68
+ const send = (message) => child.stdin.write(`${JSON.stringify(message)}\n`);
69
+ let buffer = "";
70
+ try {
71
+ send({
72
+ id: 1,
73
+ method: "initialize",
74
+ params: { clientInfo: { name: "slopify_limits", version: "1.0" } },
75
+ });
76
+ child.stdout.setEncoding("utf8");
77
+ for await (const piece of child.stdout) {
78
+ buffer += piece;
79
+ if (buffer.length > 1024 * 1024)
80
+ return null;
81
+ let newline = buffer.indexOf("\n");
82
+ while (newline >= 0) {
83
+ const line = buffer.slice(0, newline).trim();
84
+ buffer = buffer.slice(newline + 1);
85
+ newline = buffer.indexOf("\n");
86
+ if (line === "")
87
+ continue;
88
+ const reply = z
89
+ .object({ id: z.unknown().optional(), result: z.unknown().optional() })
90
+ .safeParse(JSON.parse(line));
91
+ if (!reply.success)
92
+ continue;
93
+ if (reply.data.id === 1) {
94
+ send({ method: "initialized" });
95
+ send({ id: 2, method: "account/rateLimits/read" });
96
+ }
97
+ else if (reply.data.id === 2) {
98
+ const windows = codexWindows(reply.data.result);
99
+ return windows.length === 0 ? null : windows;
100
+ }
101
+ }
102
+ }
103
+ return null;
104
+ }
105
+ catch {
106
+ return null;
107
+ }
108
+ finally {
109
+ clearTimeout(timer);
110
+ terminate("SIGTERM");
111
+ let grace;
112
+ await Promise.race([
113
+ closed,
114
+ new Promise((resolve) => {
115
+ grace = setTimeout(() => {
116
+ terminate("SIGKILL");
117
+ resolve();
118
+ }, 1000);
119
+ }),
120
+ ]);
121
+ clearTimeout(grace);
122
+ await rm(directory, { recursive: true, force: true });
123
+ }
124
+ }
125
+ const months = ["jan", "feb", "mar", "apr", "may", "jun", "jul", "aug", "sep", "oct", "nov", "dec"];
126
+ // Codex refuses a turn over the plan's allowance with "You've hit your usage limit. … Try
127
+ // again at <time>." (the messages in the 0.155.1 binary). The time is local and written either
128
+ // as "3:04 PM" alone or with a date, "Sep 28th, 2026 3:04 PM". A usage-limit text with no time
129
+ // still counts: the wait then checks again later instead of at a known reset.
130
+ export function codexPlanLimit(message, now) {
131
+ if (!/hit your usage limit|usage limit reached|usage_limit_reached/i.test(message))
132
+ return undefined;
133
+ return { account: "codex", resetsAt: codexRetryAt(message, now) };
134
+ }
135
+ function codexRetryAt(message, now) {
136
+ const dated = /try again at\s+([A-Za-z]{3})[a-z]*\.?\s+(\d{1,2})(?:st|nd|rd|th)?,?\s+(\d{4})\s+(\d{1,2}):(\d{2})\s*(AM|PM)/i.exec(message);
137
+ if (dated !== null) {
138
+ const month = months.indexOf((dated[1] ?? "").toLowerCase());
139
+ if (month < 0)
140
+ return null;
141
+ const at = new Date(Number(dated[3]), month, Number(dated[2]), hour24(Number(dated[4]), dated[6] ?? ""), Number(dated[5]));
142
+ return Number.isNaN(at.getTime()) ? null : at.toISOString();
143
+ }
144
+ const timed = /try again at\s+(\d{1,2}):(\d{2})\s*(AM|PM)/i.exec(message);
145
+ if (timed === null)
146
+ return null;
147
+ const at = new Date(now);
148
+ at.setHours(hour24(Number(timed[1]), timed[3] ?? ""), Number(timed[2]), 0, 0);
149
+ // A time already past today is tomorrow's.
150
+ if (at.getTime() <= now.getTime())
151
+ at.setDate(at.getDate() + 1);
152
+ return at.toISOString();
153
+ }
154
+ function hour24(hour, meridiem) {
155
+ const pm = meridiem.toUpperCase() === "PM";
156
+ return (hour % 12) + (pm ? 12 : 0);
157
+ }