@gentbajko/slopify 2.5.0 → 3.0.1

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 (542) hide show
  1. package/LICENSE +201 -21
  2. package/NOTICE +12 -0
  3. package/README.md +24 -41
  4. package/SUBTITLES.md +1 -1
  5. package/dist/adapter-registry.js +31 -0
  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 +44 -5
  20. package/dist/adapters/host-cli/transport.js +7 -5
  21. package/dist/adapters/image/bytes.js +2 -2
  22. package/dist/adapters/image/codex-output.js +1 -1
  23. package/dist/adapters/image/codex.js +87 -22
  24. package/dist/adapters/image/fal.js +13 -5
  25. package/dist/adapters/image/google.js +11 -8
  26. package/dist/adapters/image/openai.js +14 -7
  27. package/dist/adapters/image/reference.js +34 -0
  28. package/dist/adapters/image/replicate.js +7 -2
  29. package/dist/adapters/image/video.js +2 -2
  30. package/dist/adapters/key-probes.js +85 -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.js +99 -12
  35. package/dist/adapters/llm/document-workspace.js +2 -2
  36. package/dist/adapters/llm/gemini-workspace.js +1 -1
  37. package/dist/adapters/llm/gemini.js +16 -3
  38. package/dist/adapters/llm/image-workspace.js +58 -0
  39. package/dist/adapters/llm/openrouter.js +11 -3
  40. package/dist/adapters/llm/run-cli.js +12 -7
  41. package/dist/adapters/tts/cartesia.js +10 -0
  42. package/dist/adapters/tts/elevenlabs.js +31 -9
  43. package/dist/adapters/tts/gemini.js +206 -0
  44. package/dist/adapters/tts/inworld-async.js +9 -5
  45. package/dist/adapters/tts/inworld.js +17 -3
  46. package/dist/adapters/tts/openai.js +3 -0
  47. package/dist/adapters/tts/pcm-mp3.js +62 -0
  48. package/dist/adapters/tts/system.js +147 -0
  49. package/dist/adapters/tts/voice-languages.js +80 -0
  50. package/dist/assets/fonts/noto/NotoSans-Regular.ttf +0 -0
  51. package/dist/assets/fonts/noto/NotoSansArabic-Regular.ttf +0 -0
  52. package/dist/assets/fonts/noto/NotoSansDevanagari-Regular.ttf +0 -0
  53. package/dist/assets/fonts/noto/NotoSansHebrew-Regular.ttf +0 -0
  54. package/dist/assets/fonts/noto/NotoSansThai-Regular.ttf +0 -0
  55. package/dist/assets/fonts/noto/OFL.txt +93 -0
  56. package/dist/assets/fonts/noto/SOURCE.txt +18 -0
  57. package/dist/assets/models.yaml +38 -0
  58. package/dist/assets/sample/sample-audiobook.tar +0 -0
  59. package/dist/assets/sample/sample-podcast.tar +0 -0
  60. package/dist/assets/sample/sample-project.tar +0 -0
  61. package/dist/assets/style-preview/narration.mp3 +0 -0
  62. package/dist/assets/style-preview/tall-1.jpg +0 -0
  63. package/dist/assets/style-preview/tall-2.jpg +0 -0
  64. package/dist/assets/style-preview/tall-3.jpg +0 -0
  65. package/dist/assets/style-preview/wide-1.jpg +0 -0
  66. package/dist/assets/style-preview/wide-2.jpg +0 -0
  67. package/dist/assets/style-preview/wide-3.jpg +0 -0
  68. package/dist/catalog/merge.js +132 -0
  69. package/dist/catalog/registry.js +17 -14
  70. package/dist/catalog/schema.js +2 -0
  71. package/dist/catalog/store.js +147 -8
  72. package/dist/catalog/validate.js +2 -2
  73. package/dist/compose.yaml +71 -0
  74. package/dist/edge/autostart/docker-record.js +32 -0
  75. package/dist/edge/autostart/docker.js +118 -0
  76. package/dist/edge/autostart/index.js +78 -0
  77. package/dist/edge/autostart/launcher.js +176 -0
  78. package/dist/edge/autostart/model.js +3 -0
  79. package/dist/edge/autostart/native.js +210 -0
  80. package/dist/edge/autostart/prompt.js +72 -0
  81. package/dist/edge/autostart/service.js +90 -0
  82. package/dist/edge/cli-args.js +71 -0
  83. package/dist/edge/cli.js +64 -63
  84. package/dist/edge/docker-install/activation.js +56 -0
  85. package/dist/edge/docker-install/apply.js +458 -0
  86. package/dist/edge/docker-install/engine.js +362 -0
  87. package/dist/edge/docker-install/run.js +160 -0
  88. package/dist/edge/docker-install/state.js +146 -0
  89. package/dist/edge/{docker-projects → docker-install}/volume.js +1 -5
  90. package/dist/edge/docker.js +14 -22
  91. package/dist/edge/events/hub.js +3 -1
  92. package/dist/edge/host-cli.js +1 -1
  93. package/dist/edge/http/actions.js +38 -11
  94. package/dist/edge/http/app.js +60 -2
  95. package/dist/edge/http/auditions.js +70 -0
  96. package/dist/edge/http/autostart.js +37 -0
  97. package/dist/edge/http/byte-range.js +42 -0
  98. package/dist/edge/http/channel-memory.js +92 -0
  99. package/dist/edge/http/channels.js +128 -0
  100. package/dist/edge/http/diagnostics.js +1 -0
  101. package/dist/edge/http/draft-files.js +2 -4
  102. package/dist/edge/http/entries.js +22 -3
  103. package/dist/edge/http/files.js +11 -7
  104. package/dist/edge/http/folder-location.js +1 -1
  105. package/dist/edge/http/home.js +39 -0
  106. package/dist/edge/http/host-cli.js +29 -5
  107. package/dist/edge/http/narration-peaks.js +76 -0
  108. package/dist/edge/http/onboarding.js +212 -0
  109. package/dist/edge/http/open-folder.js +1 -1
  110. package/dist/edge/http/patch-notes.js +45 -0
  111. package/dist/edge/http/planning.js +4 -2
  112. package/dist/edge/http/project-create.js +87 -0
  113. package/dist/edge/http/project-templates.js +19 -2
  114. package/dist/edge/http/projects.js +37 -100
  115. package/dist/edge/http/prompts.js +40 -3
  116. package/dist/edge/http/pronunciations.js +23 -4
  117. package/dist/edge/http/providers.js +119 -3
  118. package/dist/edge/http/reviews.js +131 -0
  119. package/dist/edge/http/revision-files.js +3 -5
  120. package/dist/edge/http/revisions.js +1 -1
  121. package/dist/edge/http/run-cost.js +25 -0
  122. package/dist/edge/http/schedules.js +115 -6
  123. package/dist/edge/http/settings.js +84 -6
  124. package/dist/edge/http/storage-files.js +50 -0
  125. package/dist/edge/http/storage.js +43 -2
  126. package/dist/edge/http/studio.js +287 -0
  127. package/dist/edge/http/style-preview.js +61 -0
  128. package/dist/edge/http/subtitles.js +1 -1
  129. package/dist/edge/http/trash.js +69 -0
  130. package/dist/edge/http/tutorials.js +59 -0
  131. package/dist/edge/http/update.js +24 -3
  132. package/dist/edge/http/whats-new.js +15 -0
  133. package/dist/edge/http/youtube-edits.js +92 -0
  134. package/dist/edge/native-update.js +62 -0
  135. package/dist/extension/slopify-studio-chrome.zip +0 -0
  136. package/dist/extension/slopify-studio-firefox.zip +0 -0
  137. package/dist/host-cli/gemini-login.js +83 -0
  138. package/dist/host-cli/open-folder.js +10 -10
  139. package/dist/host-cli/runtime.js +9 -1
  140. package/dist/host-cli/status.js +4 -2
  141. package/dist/kernel/db/migrations/0025-automatic-reviews.sql +40 -0
  142. package/dist/kernel/db/migrations/0026-schedule-topic-generation.sql +23 -0
  143. package/dist/kernel/db/migrations/0027-run-cost.sql +58 -0
  144. package/dist/kernel/db/migrations/0030-automatic-retries.sql +18 -0
  145. package/dist/kernel/db/migrations/0031-prompt-history-and-description-edits.sql +33 -0
  146. package/dist/kernel/db/migrations/0032-channels.sql +65 -0
  147. package/dist/kernel/db/migrations/0034-script-prompts.sql +23 -0
  148. package/dist/kernel/db/migrations/0035-narration-aliases.sql +13 -0
  149. package/dist/kernel/db/migrations/0037-project-uploads.sql +8 -0
  150. package/dist/kernel/db/migrations/0038-voice-languages.sql +7 -0
  151. package/dist/kernel/db/migrations/0039-channel-essentials.sql +64 -0
  152. package/dist/kernel/db/migrations/0040-standalone-usage.sql +35 -0
  153. package/dist/kernel/db/migrations/0041-cast-hosts.sql +3 -0
  154. package/dist/kernel/paths.js +27 -6
  155. package/dist/kernel/pipeline.js +4 -1
  156. package/dist/kernel/ports/host-cli.js +122 -14
  157. package/dist/kernel/ports/key-probe.js +1 -0
  158. package/dist/kernel/ports/languages.js +157 -0
  159. package/dist/kernel/ports/model.js +10 -5
  160. package/dist/kernel/ports/narration-aliases.js +60 -0
  161. package/dist/kernel/ports/plan-limits.js +36 -0
  162. package/dist/kernel/ports/system-speech.js +203 -0
  163. package/dist/kernel/runner/attempt.js +17 -8
  164. package/dist/kernel/runner/audition.js +36 -0
  165. package/dist/kernel/runner/graph.js +40 -5
  166. package/dist/kernel/runner/index.js +39 -9
  167. package/dist/kernel/runner/meter.js +1 -0
  168. package/dist/kernel/runner/providers.js +142 -7
  169. package/dist/kernel/runner/retry-policy.js +43 -0
  170. package/dist/kernel/runner/standalone.js +134 -0
  171. package/dist/kernel/runner/work-authority.js +28 -2
  172. package/dist/main.js +277 -16
  173. package/dist/model-catalog.js +2 -0
  174. package/dist/patch-notes/0.4.2-to-1.0.md +93 -0
  175. package/dist/patch-notes/1.0-to-2.0.md +74 -0
  176. package/dist/patch-notes/2.0-to-3.0.md +257 -0
  177. package/dist/patch-notes/3.0.0.md +70 -0
  178. package/dist/patch-notes/3.0.1.md +105 -0
  179. package/dist/patch-notes/index.json +32 -0
  180. package/dist/slices/admission/model.js +27 -0
  181. package/dist/slices/admission/repo.js +24 -4
  182. package/dist/slices/admission/rules.js +80 -2
  183. package/dist/slices/admission/schema.js +58 -1
  184. package/dist/slices/admission/short-mode.js +47 -0
  185. package/dist/slices/admission/start.js +42 -6
  186. package/dist/slices/article/continuation.js +2 -2
  187. package/dist/slices/article/provided-entries.js +2 -2
  188. package/dist/slices/article/run.js +6 -2
  189. package/dist/slices/article/segments.js +3 -3
  190. package/dist/slices/backups/folder.js +14 -6
  191. package/dist/slices/batch/index.js +7 -2
  192. package/dist/slices/cancel/index.js +1 -1
  193. package/dist/slices/channels/cast-images.js +110 -0
  194. package/dist/slices/channels/cast-match.js +32 -0
  195. package/dist/slices/channels/images.js +19 -0
  196. package/dist/slices/channels/model.js +5 -0
  197. package/dist/slices/channels/rebrand.js +100 -0
  198. package/dist/slices/channels/repo.js +221 -0
  199. package/dist/slices/channels/runs.js +129 -0
  200. package/dist/slices/channels/schema.js +152 -0
  201. package/dist/slices/channels/service.js +180 -0
  202. package/dist/slices/channels/studio-csv.js +110 -0
  203. package/dist/slices/channels/videos.js +120 -0
  204. package/dist/slices/checkpoints/recovery.js +10 -0
  205. package/dist/slices/control/index.js +5 -2
  206. package/dist/slices/control/providers.js +4 -1
  207. package/dist/slices/document/fonts.js +1 -1
  208. package/dist/slices/document/render.js +1 -1
  209. package/dist/slices/episodes/related.js +90 -0
  210. package/dist/slices/episodes/repo.js +60 -0
  211. package/dist/slices/episodes/service.js +58 -0
  212. package/dist/slices/episodes/summarize.js +170 -0
  213. package/dist/slices/estimate/index.js +109 -28
  214. package/dist/slices/estimate/requests.js +68 -2
  215. package/dist/slices/eta/history.js +56 -0
  216. package/dist/slices/eta/model.js +63 -0
  217. package/dist/slices/eta/view.js +25 -0
  218. package/dist/slices/fixes/rules.js +73 -0
  219. package/dist/slices/fonts/coverage.js +82 -0
  220. package/dist/slices/fonts/sfnt.js +74 -0
  221. package/dist/slices/images/run.js +8 -5
  222. package/dist/slices/images/scale.js +84 -0
  223. package/dist/slices/library/diff.js +122 -0
  224. package/dist/slices/library/history.js +100 -0
  225. package/dist/slices/library/model.js +3 -0
  226. package/dist/slices/library/photorealistic.js +49 -0
  227. package/dist/slices/library/repo.js +19 -14
  228. package/dist/slices/library/save.js +62 -12
  229. package/dist/slices/library/slots.js +7 -1
  230. package/dist/slices/library/used-by.js +153 -0
  231. package/dist/slices/loudness/level-pieces.js +41 -0
  232. package/dist/slices/loudness/loudnorm.js +164 -0
  233. package/dist/slices/loudness/model.js +153 -0
  234. package/dist/slices/model-upkeep/model.js +10 -0
  235. package/dist/slices/model-upkeep/switch.js +217 -0
  236. package/dist/slices/model-upkeep/usage.js +185 -0
  237. package/dist/slices/narration/aliases-library.js +31 -0
  238. package/dist/slices/narration/aliases-schema.js +53 -0
  239. package/dist/slices/narration/aliases.js +49 -0
  240. package/dist/slices/narration/blocks.js +352 -0
  241. package/dist/slices/narration/chunk.js +19 -7
  242. package/dist/slices/narration/concat.js +2 -2
  243. package/dist/slices/narration/describe.js +45 -0
  244. package/dist/slices/narration/pauses-model.js +63 -0
  245. package/dist/slices/narration/pauses.js +269 -0
  246. package/dist/slices/narration/peaks-model.js +1 -0
  247. package/dist/slices/narration/peaks.js +94 -0
  248. package/dist/slices/narration/preparation.js +21 -3
  249. package/dist/slices/narration/pronunciation-chunks.js +3 -3
  250. package/dist/slices/narration/pronunciation.js +72 -25
  251. package/dist/slices/narration/run.js +6 -6
  252. package/dist/slices/narration/shared-glossary.js +1 -1
  253. package/dist/slices/narration/steering.js +15 -4
  254. package/dist/slices/notifications/notifier.js +47 -3
  255. package/dist/slices/notifications/rules.js +48 -3
  256. package/dist/slices/onboarding/copy.js +143 -0
  257. package/dist/slices/onboarding/first-run.js +72 -0
  258. package/dist/slices/onboarding/full-video.js +81 -0
  259. package/dist/slices/onboarding/install.js +119 -0
  260. package/dist/slices/onboarding/model.js +85 -0
  261. package/dist/slices/onboarding/packs.js +491 -0
  262. package/dist/slices/onboarding/quick-short.js +144 -0
  263. package/dist/slices/onboarding/sample.js +132 -0
  264. package/dist/slices/onboarding/state.js +68 -0
  265. package/dist/slices/onboarding/template.js +73 -0
  266. package/dist/slices/patch-notes/library.js +73 -0
  267. package/dist/slices/patch-notes/seen.js +77 -0
  268. package/dist/slices/play-drafts/convert.js +127 -8
  269. package/dist/slices/play-drafts/readiness.js +4 -1
  270. package/dist/slices/play-drafts/repo.js +2 -0
  271. package/dist/slices/play-drafts/review-inputs.js +20 -3
  272. package/dist/slices/play-drafts/schema.js +89 -0
  273. package/dist/slices/play-drafts/service.js +4 -1
  274. package/dist/slices/play-drafts/start.js +43 -18
  275. package/dist/slices/project-templates/from-project.js +59 -6
  276. package/dist/slices/project-templates/next-chapter.js +94 -0
  277. package/dist/slices/project-templates/one-off.js +26 -0
  278. package/dist/slices/project-templates/repo.js +2 -1
  279. package/dist/slices/project-templates/schema.js +7 -1
  280. package/dist/slices/project-templates/service.js +15 -4
  281. package/dist/slices/project-templates/setup.js +18 -2
  282. package/dist/slices/rebuild/admission-repo.js +1 -1
  283. package/dist/slices/rebuild/model.js +6 -0
  284. package/dist/slices/rebuild/preview-details.js +46 -3
  285. package/dist/slices/rebuild/preview-plan.js +13 -6
  286. package/dist/slices/rebuild/recipe-audio-parts.js +40 -7
  287. package/dist/slices/rebuild/recipe-audio.js +66 -12
  288. package/dist/slices/rebuild/recipe-build.js +8 -4
  289. package/dist/slices/rebuild/recipe-cast.js +18 -0
  290. package/dist/slices/rebuild/recipe-checkpoints.js +11 -9
  291. package/dist/slices/rebuild/recipe-describe.js +93 -0
  292. package/dist/slices/rebuild/recipe-edit.js +13 -1
  293. package/dist/slices/rebuild/recipe-exports.js +67 -8
  294. package/dist/slices/rebuild/recipe-input-schema.js +35 -0
  295. package/dist/slices/rebuild/recipe-legacy.js +9 -0
  296. package/dist/slices/rebuild/recipe-loudness.js +45 -0
  297. package/dist/slices/rebuild/recipe-model.js +25 -6
  298. package/dist/slices/rebuild/recipe-pauses.js +7 -0
  299. package/dist/slices/rebuild/recipe-preparation.js +3 -3
  300. package/dist/slices/rebuild/recipe-provider-choice.js +11 -1
  301. package/dist/slices/rebuild/recipe-reference.js +15 -1
  302. package/dist/slices/rebuild/recipe-reviews.js +117 -0
  303. package/dist/slices/rebuild/recipe-save.js +4 -1
  304. package/dist/slices/rebuild/recipe-shorts.js +23 -3
  305. package/dist/slices/rebuild/recipe-text.js +114 -19
  306. package/dist/slices/rebuild/recipe-validation.js +14 -2
  307. package/dist/slices/rebuild/recipe-visual.js +96 -33
  308. package/dist/slices/rebuild/recipe-voices.js +286 -0
  309. package/dist/slices/rebuild/recipe-work.js +77 -6
  310. package/dist/slices/rebuild/recipe-youtube.js +2 -0
  311. package/dist/slices/rebuild/recovery-conflict.js +7 -3
  312. package/dist/slices/rebuild/recovery-model.js +3 -0
  313. package/dist/slices/rebuild/recovery-selection.js +28 -2
  314. package/dist/slices/rebuild/recovery.js +35 -17
  315. package/dist/slices/rebuild/repo.js +3 -3
  316. package/dist/slices/rebuild/retry.js +18 -0
  317. package/dist/slices/rebuild/review-redo.js +125 -0
  318. package/dist/slices/rebuild/runtime-actions.js +5 -0
  319. package/dist/slices/rebuild/runtime-admission.js +4 -1
  320. package/dist/slices/rebuild/runtime-animate.js +4 -4
  321. package/dist/slices/rebuild/runtime-article.js +5 -5
  322. package/dist/slices/rebuild/runtime-document.js +4 -4
  323. package/dist/slices/rebuild/runtime-export-bed.js +43 -0
  324. package/dist/slices/rebuild/runtime-export-edit.js +70 -11
  325. package/dist/slices/rebuild/runtime-export-inputs.js +52 -10
  326. package/dist/slices/rebuild/runtime-export-short.js +85 -0
  327. package/dist/slices/rebuild/runtime-export.js +84 -19
  328. package/dist/slices/rebuild/runtime-figure-card.js +87 -0
  329. package/dist/slices/rebuild/runtime-image.js +21 -2
  330. package/dist/slices/rebuild/runtime-local.js +148 -24
  331. package/dist/slices/rebuild/runtime-materialize.js +8 -0
  332. package/dist/slices/rebuild/runtime-narration-reuse.js +2 -1
  333. package/dist/slices/rebuild/runtime-narration-text.js +7 -7
  334. package/dist/slices/rebuild/runtime-pictures.js +28 -0
  335. package/dist/slices/rebuild/runtime-piece-label.js +27 -0
  336. package/dist/slices/rebuild/runtime-provider.js +126 -18
  337. package/dist/slices/rebuild/runtime-publication.js +21 -5
  338. package/dist/slices/rebuild/runtime-review.js +267 -0
  339. package/dist/slices/rebuild/runtime-run.js +17 -11
  340. package/dist/slices/rebuild/runtime-shorts.js +44 -17
  341. package/dist/slices/rebuild/runtime-store.js +62 -11
  342. package/dist/slices/rebuild/runtime-subtitles.js +72 -19
  343. package/dist/slices/rebuild/runtime-voices.js +103 -0
  344. package/dist/slices/rebuild/runtime-youtube.js +7 -6
  345. package/dist/slices/rebuild/service-readiness.js +3 -3
  346. package/dist/slices/rebuild/soften.js +49 -0
  347. package/dist/slices/rebuild/transition-repo.js +9 -2
  348. package/dist/slices/research/run.js +4 -4
  349. package/dist/slices/research/synthesis.js +2 -2
  350. package/dist/slices/reviews/model.js +105 -0
  351. package/dist/slices/reviews/outcome.js +22 -0
  352. package/dist/slices/reviews/repo.js +118 -0
  353. package/dist/slices/reviews/rules.js +63 -0
  354. package/dist/slices/reviews/verdict.js +71 -0
  355. package/dist/slices/reviews/view.js +30 -0
  356. package/dist/slices/revisions/adopt-content.js +5 -2
  357. package/dist/slices/revisions/adopt.js +3 -1
  358. package/dist/slices/revisions/downloads.js +7 -1
  359. package/dist/slices/revisions/image-plan.js +183 -0
  360. package/dist/slices/revisions/mutation-assets.js +10 -0
  361. package/dist/slices/revisions/mutation-cues.js +71 -0
  362. package/dist/slices/revisions/mutation-request.js +2 -1
  363. package/dist/slices/revisions/mutations.js +97 -3
  364. package/dist/slices/revisions/schema.js +11 -1
  365. package/dist/slices/run-cost/limits.js +128 -0
  366. package/dist/slices/run-cost/meter.js +47 -0
  367. package/dist/slices/run-cost/panel.js +256 -0
  368. package/dist/slices/run-cost/pricing.js +95 -0
  369. package/dist/slices/run-cost/week.js +108 -0
  370. package/dist/slices/schedules/agenda.js +222 -0
  371. package/dist/slices/schedules/repo.js +58 -18
  372. package/dist/slices/schedules/scheduler.js +68 -17
  373. package/dist/slices/schedules/schema.js +179 -1
  374. package/dist/slices/schedules/service.js +43 -4
  375. package/dist/slices/schedules/similar.js +128 -0
  376. package/dist/slices/schedules/topic-list.js +163 -0
  377. package/dist/slices/schedules/topics.js +523 -0
  378. package/dist/slices/settings/first-run.js +106 -0
  379. package/dist/slices/settings/health.js +247 -0
  380. package/dist/slices/settings/key-guides.js +138 -0
  381. package/dist/slices/settings/key-test.js +93 -0
  382. package/dist/slices/settings/keys.js +10 -5
  383. package/dist/slices/settings/model-reach.js +52 -0
  384. package/dist/slices/settings/model.js +55 -1
  385. package/dist/slices/settings/models.js +5 -1
  386. package/dist/slices/settings/playback.js +15 -0
  387. package/dist/slices/settings/readiness.js +14 -2
  388. package/dist/slices/settings/repo.js +17 -3
  389. package/dist/slices/settings/tutorial-schema.js +5 -0
  390. package/dist/slices/settings/voices.js +64 -4
  391. package/dist/slices/settings/whats-new.js +64 -0
  392. package/dist/slices/shorts/captions.js +9 -0
  393. package/dist/slices/shorts/pick.js +3 -3
  394. package/dist/slices/shorts/prompts.js +1 -1
  395. package/dist/slices/shorts/render.js +63 -13
  396. package/dist/slices/storage/asset-name.js +3 -0
  397. package/dist/slices/storage/backup-export.js +48 -9
  398. package/dist/slices/storage/backup-format.js +38 -1
  399. package/dist/slices/storage/backup-import.js +191 -19
  400. package/dist/slices/storage/delete-project.js +7 -4
  401. package/dist/slices/storage/documents.js +87 -0
  402. package/dist/slices/storage/downloads.js +33 -1
  403. package/dist/slices/storage/files-location.js +531 -0
  404. package/dist/slices/storage/layout.js +14 -4
  405. package/dist/slices/storage/model.js +11 -0
  406. package/dist/slices/storage/portable.js +94 -12
  407. package/dist/slices/storage/reconcile.js +14 -1
  408. package/dist/slices/storage/repo.js +4 -1
  409. package/dist/slices/storage/schema.js +11 -0
  410. package/dist/slices/storage/trim.js +146 -0
  411. package/dist/slices/studio/disclosure.js +74 -0
  412. package/dist/slices/studio/model.js +30 -0
  413. package/dist/slices/studio/pack.js +211 -0
  414. package/dist/slices/studio/queue.js +69 -0
  415. package/dist/slices/studio/settings.js +170 -0
  416. package/dist/slices/style-preview/images.js +61 -0
  417. package/dist/slices/style-preview/narration.js +33 -0
  418. package/dist/slices/style-preview/render.js +148 -0
  419. package/dist/slices/style-preview/schema.js +59 -0
  420. package/dist/slices/style-preview/service.js +92 -0
  421. package/dist/slices/style-preview/settings.js +139 -0
  422. package/dist/slices/subtitles/captions.js +42 -9
  423. package/dist/slices/subtitles/model.js +22 -0
  424. package/dist/slices/subtitles/prepare.js +27 -16
  425. package/dist/slices/subtitles/transcript.js +1 -1
  426. package/dist/slices/thumbnail/run.js +7 -7
  427. package/dist/slices/trash/model.js +25 -0
  428. package/dist/slices/trash/service.js +273 -0
  429. package/dist/slices/tutorials/anchors.js +57 -0
  430. package/dist/slices/tutorials/library.js +185 -0
  431. package/dist/slices/uploads/repo.js +20 -0
  432. package/dist/slices/video/ambient-bed-schema.js +15 -0
  433. package/dist/slices/video/ambient-bed.js +95 -0
  434. package/dist/slices/video/ambient-mix.js +72 -0
  435. package/dist/slices/video/audio-inputs.js +2 -2
  436. package/dist/slices/video/cards.js +5 -2
  437. package/dist/slices/video/edit-list.js +23 -2
  438. package/dist/slices/video/edit-settings.js +13 -3
  439. package/dist/slices/video/ffmpeg.js +63 -9
  440. package/dist/slices/video/figure-card.js +492 -0
  441. package/dist/slices/video/figure-spans.js +51 -0
  442. package/dist/slices/video/plan.js +125 -13
  443. package/dist/slices/video/run.js +14 -12
  444. package/dist/slices/video/slideshow.js +39 -4
  445. package/dist/slices/voices/attribution.js +69 -0
  446. package/dist/slices/voices/audio-files.js +87 -0
  447. package/dist/slices/voices/audition.js +25 -0
  448. package/dist/slices/voices/cast.js +74 -0
  449. package/dist/slices/voices/grouping.js +59 -0
  450. package/dist/slices/voices/join.js +35 -0
  451. package/dist/slices/voices/languages.js +25 -0
  452. package/dist/slices/voices/model.js +218 -0
  453. package/dist/slices/voices/palette.js +26 -0
  454. package/dist/slices/voices/panel.js +91 -0
  455. package/dist/slices/voices/portraits.js +55 -0
  456. package/dist/slices/voices/script.js +136 -0
  457. package/dist/slices/voices/timing.js +58 -0
  458. package/dist/slices/youtube/answer.js +1 -1
  459. package/dist/slices/youtube/chapters.js +126 -0
  460. package/dist/slices/youtube/edits-repo.js +84 -0
  461. package/dist/slices/youtube/edits.js +89 -0
  462. package/dist/slices/youtube/placeholders.js +98 -0
  463. package/dist/slices/youtube/transcript.js +11 -4
  464. package/dist/tutorials/AI-CLIs.md +146 -0
  465. package/dist/tutorials/Backups.md +121 -0
  466. package/dist/tutorials/Calendar.md +103 -0
  467. package/dist/tutorials/Cast-Library.md +112 -0
  468. package/dist/tutorials/Channels.md +148 -0
  469. package/dist/tutorials/Costs-and-Run-Cost.md +136 -0
  470. package/dist/tutorials/Docker.md +171 -0
  471. package/dist/tutorials/Document-Themes.md +207 -0
  472. package/dist/tutorials/Editing-a-Project.md +204 -0
  473. package/dist/tutorials/FAQ.md +134 -0
  474. package/dist/tutorials/First-Launch-and-Welcome.md +153 -0
  475. package/dist/tutorials/Home-and-Projects.md +174 -0
  476. package/dist/tutorials/Home.md +119 -0
  477. package/dist/tutorials/Install.md +154 -0
  478. package/dist/tutorials/Intros-and-Outros.md +86 -0
  479. package/dist/tutorials/Keyboard-Shortcuts-and-Command-Palette.md +215 -0
  480. package/dist/tutorials/Library-Overview.md +75 -0
  481. package/dist/tutorials/Models.md +133 -0
  482. package/dist/tutorials/Multiple-Voices.md +161 -0
  483. package/dist/tutorials/Narration-Aliases-and-Glossary.md +129 -0
  484. package/dist/tutorials/Notifications.md +86 -0
  485. package/dist/tutorials/Other-Languages.md +110 -0
  486. package/dist/tutorials/PDF-Documents.md +100 -0
  487. package/dist/tutorials/Play-Images.md +135 -0
  488. package/dist/tutorials/Play-Narration.md +167 -0
  489. package/dist/tutorials/Play-Outputs.md +109 -0
  490. package/dist/tutorials/Play-Overview.md +216 -0
  491. package/dist/tutorials/Play-Title-and-Article.md +135 -0
  492. package/dist/tutorials/Play-Video-and-Style.md +143 -0
  493. package/dist/tutorials/Project-Page.md +228 -0
  494. package/dist/tutorials/Prompts.md +136 -0
  495. package/dist/tutorials/Providers-and-Keys.md +290 -0
  496. package/dist/tutorials/Publishing-to-YouTube.md +135 -0
  497. package/dist/tutorials/Recovery-and-Retries.md +141 -0
  498. package/dist/tutorials/Reviews-and-Checkpoints.md +143 -0
  499. package/dist/tutorials/Schedules.md +154 -0
  500. package/dist/tutorials/Settings-Reference.md +258 -0
  501. package/dist/tutorials/Shorts.md +116 -0
  502. package/dist/tutorials/Start-at-Login.md +126 -0
  503. package/dist/tutorials/Studio-Extension.md +146 -0
  504. package/dist/tutorials/Templates.md +146 -0
  505. package/dist/tutorials/Trash.md +87 -0
  506. package/dist/tutorials/Troubleshooting.md +149 -0
  507. package/dist/tutorials/Uninstalling-and-Moving.md +113 -0
  508. package/dist/tutorials/Updating-and-Patch-Notes.md +120 -0
  509. package/dist/tutorials/Video-Editing.md +135 -0
  510. package/dist/tutorials/Where-Your-Files-Live.md +149 -0
  511. package/dist/tutorials/YouTube-Description.md +132 -0
  512. package/dist/tutorials/Your-First-Short.md +125 -0
  513. package/dist/tutorials/_Footer.md +1 -0
  514. package/dist/tutorials/_Sidebar.md +69 -0
  515. package/dist/updater/service.js +66 -21
  516. package/dist/web/assets/index-DBLSfqA-.css +1 -0
  517. package/dist/web/assets/index-DFdIhmm0.js +307 -0
  518. package/dist/web/assets/jetbrains-mono-cyrillic-400-normal-BEIGL1Tu.woff2 +0 -0
  519. package/dist/web/assets/jetbrains-mono-cyrillic-400-normal-ugxPyKxw.woff +0 -0
  520. package/dist/web/assets/jetbrains-mono-greek-400-normal-B9oWc5Lo.woff +0 -0
  521. package/dist/web/assets/jetbrains-mono-greek-400-normal-C190GLew.woff2 +0 -0
  522. package/dist/web/assets/jetbrains-mono-latin-400-normal-6-qcROiO.woff +0 -0
  523. package/dist/web/assets/jetbrains-mono-latin-400-normal-V6pRDFza.woff2 +0 -0
  524. package/dist/web/assets/jetbrains-mono-latin-ext-400-normal-Bc8Ftmh3.woff2 +0 -0
  525. package/dist/web/assets/jetbrains-mono-latin-ext-400-normal-fXTG6kC5.woff +0 -0
  526. package/dist/web/assets/jetbrains-mono-vietnamese-400-normal-CqNFfHCs.woff +0 -0
  527. package/dist/web/assets/{pdf-BLF2i8c2.js → pdf-B9OY1ioX.js} +1 -1
  528. package/dist/web/index.html +2 -2
  529. package/package.json +5 -5
  530. package/dist/edge/docker-launch.js +0 -81
  531. package/dist/edge/docker-projects/activation.js +0 -35
  532. package/dist/edge/docker-projects/claims.js +0 -85
  533. package/dist/edge/docker-projects/committed.js +0 -40
  534. package/dist/edge/docker-projects/engine.js +0 -461
  535. package/dist/edge/docker-projects/install.js +0 -254
  536. package/dist/edge/docker-projects/prune.js +0 -66
  537. package/dist/edge/docker-projects/recover.js +0 -100
  538. package/dist/edge/docker-projects/state.js +0 -225
  539. package/dist/web/assets/index-CktpxlJd.js +0 -171
  540. package/dist/web/assets/index-DB-vOnGU.css +0 -1
  541. package/scripts/docker-run.sh +0 -3
  542. /package/dist/edge/{docker-projects → docker-install}/tree.js +0 -0
@@ -0,0 +1,62 @@
1
+ import { setTimeout as delay } from "node:timers/promises";
2
+ /**
3
+ * `slopify update` for a native install: asks the running app to update itself, exactly as the
4
+ * Update button in Settings does (wait for running work, install, back up the database, start the
5
+ * new version, roll back if it doesn't come up), and follows it until it has finished.
6
+ */
7
+ export async function runNativeUpdate(deps) {
8
+ const poll = deps.pollMs ?? 2000;
9
+ const read = async (refresh = false) => {
10
+ const response = await deps.fetch(`${deps.origin}/api/update${refresh ? "?refresh=1" : ""}`);
11
+ if (!response.ok)
12
+ throw new Error(`Slopify answered ${response.status}.`);
13
+ return (await response.json());
14
+ };
15
+ let info;
16
+ try {
17
+ info = await read(true);
18
+ }
19
+ catch {
20
+ throw new Error(`Slopify isn't running at ${deps.origin}, so there is nothing to update. Start it (npx @gentbajko/slopify) and run slopify update again; if it runs on another port, add --port <number>. To just start the newest version, run npx @gentbajko/slopify@latest.`);
21
+ }
22
+ if (info.status === "error" && info.error !== undefined && info.latestVersion === null)
23
+ throw new Error(info.error);
24
+ if (!info.available) {
25
+ deps.report(`Slopify ${info.currentVersion} is already the newest version.`);
26
+ return;
27
+ }
28
+ const target = info.latestVersion;
29
+ let told = false;
30
+ while (info.busy) {
31
+ if (!told)
32
+ deps.report("Slopify is generating right now. Waiting for running work to finish before updating (press Ctrl+C to cancel; nothing has been changed yet)...");
33
+ told = true;
34
+ await delay(poll);
35
+ info = await read();
36
+ }
37
+ if (!info.canUpdate)
38
+ throw new Error(info.blockedReason ??
39
+ info.error ??
40
+ "Slopify can't update right now. Open Settings in Slopify to see why.");
41
+ const started = await deps.fetch(`${deps.origin}/api/update`, { method: "POST" });
42
+ if (!started.ok) {
43
+ const body = (await started.json().catch(() => ({})));
44
+ throw new Error(body.detail ?? `Slopify refused to start the update (${started.status}).`);
45
+ }
46
+ deps.report(`Updating Slopify ${info.currentVersion} to ${target}...`);
47
+ const deadline = Date.now() + (deps.restartMs ?? 10 * 60_000);
48
+ while (Date.now() < deadline) {
49
+ await delay(poll);
50
+ const now = await read().catch(() => undefined);
51
+ if (now === undefined || now.status === "installing" || now.status === "restarting")
52
+ continue;
53
+ if (now.currentVersion === target && now.status !== "error") {
54
+ deps.report(`Slopify ${target} is running at ${deps.origin}`);
55
+ return;
56
+ }
57
+ if (now.status === "error" || now.error !== undefined)
58
+ throw new Error(now.error ??
59
+ "The update did not finish, so Slopify kept or put back the previous version. The reason is in logs/updates.log inside your Slopify data folder.");
60
+ }
61
+ throw new Error(`Slopify didn't answer at ${deps.origin} within 10 minutes after the update started. Check it in the browser; the updater puts the previous version back on its own if the new one doesn't start, and the reason is in logs/updates.log inside your Slopify data folder.`);
62
+ }
@@ -0,0 +1,83 @@
1
+ import { readFile } from "node:fs/promises";
2
+ import { homedir } from "node:os";
3
+ import { join } from "node:path";
4
+ export function nodeGeminiLoginHost() {
5
+ return {
6
+ home: homedir(),
7
+ env: process.env,
8
+ read: (path) => readFile(path, "utf8").catch(() => undefined),
9
+ };
10
+ }
11
+ export async function readGeminiLogin(host) {
12
+ const folder = join(host.home, ".gemini");
13
+ const raw = await host.read(join(folder, "settings.json"));
14
+ const settings = parse(raw);
15
+ const method = selectedAuth(settings);
16
+ const hasKey = async (name) => nonEmpty(host.env[name]) || dotenvHas(await host.read(join(folder, ".env")), name);
17
+ const oauth = async () => {
18
+ const creds = parse(await host.read(join(folder, "oauth_creds.json")));
19
+ return typeof creds?.refresh_token === "string" && creds.refresh_token !== "";
20
+ };
21
+ switch (method) {
22
+ case "oauth-personal":
23
+ case "login-with-google":
24
+ return (await oauth()) ? "signed-in" : "signed-out";
25
+ case "gemini-api-key":
26
+ return (await hasKey("GEMINI_API_KEY")) ? "signed-in" : "signed-out";
27
+ case "vertex-ai":
28
+ return (await hasKey("GOOGLE_API_KEY")) || (await hasKey("GOOGLE_CLOUD_PROJECT"))
29
+ ? "signed-in"
30
+ : "signed-out";
31
+ case "cloud-shell":
32
+ return "signed-in";
33
+ case undefined:
34
+ // No choice saved yet: the CLI still starts without asking when a key or an earlier
35
+ // Google sign-in is there; otherwise it opens its sign-in menu.
36
+ if ((await hasKey("GEMINI_API_KEY")) || (await oauth()))
37
+ return "signed-in";
38
+ // Nothing saved says nothing either: the CLI can be signed in some other way (Vertex or
39
+ // a key in its own environment), so only a sign-in the files prove missing blocks a run.
40
+ return "unknown";
41
+ default:
42
+ // A method this version of Slopify doesn't know.
43
+ return "unknown";
44
+ }
45
+ }
46
+ function parse(text) {
47
+ if (text === undefined)
48
+ return undefined;
49
+ try {
50
+ const value = JSON.parse(text);
51
+ return typeof value === "object" && value !== null && !Array.isArray(value)
52
+ ? value
53
+ : undefined;
54
+ }
55
+ catch {
56
+ return undefined;
57
+ }
58
+ }
59
+ function selectedAuth(settings) {
60
+ const security = settings?.security;
61
+ const auth = typeof security === "object" && security !== null
62
+ ? security.auth
63
+ : undefined;
64
+ const nested = typeof auth === "object" && auth !== null
65
+ ? auth.selectedType
66
+ : undefined;
67
+ if (typeof nested === "string" && nested !== "")
68
+ return nested;
69
+ const flat = settings?.selectedAuthType;
70
+ return typeof flat === "string" && flat !== "" ? flat : undefined;
71
+ }
72
+ function nonEmpty(value) {
73
+ return value !== undefined && value.trim() !== "";
74
+ }
75
+ // `NAME=value` in a .env file, optionally quoted or exported.
76
+ function dotenvHas(text, name) {
77
+ if (text === undefined)
78
+ return false;
79
+ return text.split(/\r?\n/).some((line) => {
80
+ const match = /^\s*(?:export\s+)?([A-Za-z_][A-Za-z0-9_]*)\s*=\s*(.*)$/.exec(line);
81
+ return match?.[1] === name && nonEmpty((match[2] ?? "").replace(/^["']|["']$/g, ""));
82
+ });
83
+ }
@@ -1,10 +1,10 @@
1
1
  import { spawn } from "node:child_process";
2
2
  import { lstat, readdir } from "node:fs/promises";
3
3
  import { isAbsolute, join, relative, sep } from "node:path";
4
- import { readState, receiptSchema } from "../edge/docker-projects/state.js";
4
+ import { installSchema, readState } from "../edge/docker-install/state.js";
5
5
  import { HostFolderRefused } from "../kernel/ports/host-cli.js";
6
6
  import { hasCode } from "./paths.js";
7
- const again = "run the Docker launcher again (npx @gentbajko/slopify@latest --docker)";
7
+ const again = "run the Docker install again (npx @gentbajko/slopify@latest --docker)";
8
8
  /** Starts the desktop's opener and returns once it is running; the file manager is not awaited. */
9
9
  export function launchXdgOpen(directory) {
10
10
  return new Promise((resolve, reject) => {
@@ -24,7 +24,7 @@ export function launchXdgOpen(directory) {
24
24
  });
25
25
  });
26
26
  }
27
- /** Only folders inside a launcher receipt's project folder, reached through real folders you own, are opened. */
27
+ /** Only folders inside an installation's recorded project folder, reached through real folders you own, are opened. */
28
28
  export function createHostFolderOpener(deps) {
29
29
  async function receipts() {
30
30
  const found = [];
@@ -42,9 +42,9 @@ export function createHostFolderOpener(deps) {
42
42
  if (!entry.isDirectory())
43
43
  continue;
44
44
  try {
45
- const receipt = await readState(join(deps.root, entry.name, "receipt.json"), receiptSchema, deps.uid);
46
- if (receipt)
47
- found.push(receipt);
45
+ const install = await readState(join(deps.root, entry.name, "install.json"), installSchema, deps.uid);
46
+ if (install)
47
+ found.push(install);
48
48
  }
49
49
  catch {
50
50
  // An unreadable or damaged receipt grants nothing; the others still count.
@@ -59,13 +59,13 @@ export function createHostFolderOpener(deps) {
59
59
  root.uid !== BigInt(deps.uid) ||
60
60
  String(root.dev) !== identity.dev ||
61
61
  String(root.ino) !== identity.ino)
62
- throw new HostFolderRefused(`The Slopify host helper did not open this folder because the project folder ${projects} was moved or replaced since the Docker launcher set it up. Put the original folder back, or ${again}.`);
62
+ throw new HostFolderRefused(`The Slopify host helper did not open this folder because the project folder ${projects} was moved or replaced since the Docker install set it up. Put the original folder back, or ${again}.`);
63
63
  let current = projects;
64
64
  for (const part of r === "" ? [] : r.split(sep)) {
65
65
  current = join(current, part);
66
66
  const s = await lstat(current).catch(() => undefined);
67
67
  if (!s?.isDirectory() || s.isSymbolicLink() || s.uid !== deps.uid)
68
- throw new HostFolderRefused(`The Slopify host helper did not open ${current} because it is missing, a link or a file, or isn't owned by you. Use Re-run section on the project page to make the file again, or copy the path into your file manager.`);
68
+ throw new HostFolderRefused(`The Slopify host helper did not open ${current} because it is missing, a link or a file, or isn't owned by you. Use More → make it again in its section on the project page to make the file again, or copy the path into your file manager.`);
69
69
  }
70
70
  }
71
71
  return async (path) => {
@@ -77,7 +77,7 @@ export function createHostFolderOpener(deps) {
77
77
  if (r === ".." || r.startsWith(`..${sep}`) || isAbsolute(r))
78
78
  continue;
79
79
  try {
80
- await verify(receipt.projects, receipt.directoryIdentity, r);
80
+ await verify(receipt.projects, receipt.projectsIdentity, r);
81
81
  }
82
82
  catch (error) {
83
83
  if (!(error instanceof HostFolderRefused))
@@ -90,6 +90,6 @@ export function createHostFolderOpener(deps) {
90
90
  return;
91
91
  }
92
92
  throw (refusal ??
93
- new HostFolderRefused(`The Slopify host helper did not open ${path} because it isn't inside a project folder set up by the Docker launcher. Copy the path into your file manager, or if you moved your project folder, ${again}.`));
93
+ new HostFolderRefused(`The Slopify host helper did not open ${path} because it isn't inside a project folder set up by the Docker install. Copy the path into your file manager, or if you moved your project folder, ${again}.`));
94
94
  };
95
95
  }
@@ -5,6 +5,7 @@ import { codexImage, codexImageTimeoutMs } from "../adapters/image/codex.js";
5
5
  import { claudeCodeLlm } from "../adapters/llm/claude-code.js";
6
6
  import { nodeClaudeCodeModels } from "../adapters/llm/claude-code-models.js";
7
7
  import { codexLlm } from "../adapters/llm/codex.js";
8
+ import { nodeCodexLimits } from "../adapters/llm/codex-limits.js";
8
9
  import { nodeCodexModels } from "../adapters/llm/codex-models.js";
9
10
  import { geminiLlm } from "../adapters/llm/gemini.js";
10
11
  import { nodeGeminiModels } from "../adapters/llm/gemini-models.js";
@@ -12,6 +13,7 @@ import { providerError } from "../kernel/ports/model.js";
12
13
  import { createHostFolderOpener } from "./open-folder.js";
13
14
  import { createHostStatus } from "./status.js";
14
15
  export function createHostRuntime(deps) {
16
+ const readCodexLimits = deps.readCodexLimits ?? nodeCodexLimits;
15
17
  const status = createHostStatus(deps);
16
18
  async function command(id) {
17
19
  const ready = await status(id);
@@ -56,6 +58,7 @@ export function createHostRuntime(deps) {
56
58
  binary,
57
59
  env: deps.env,
58
60
  readModels: () => models(id),
61
+ ...(id === "codex" ? { readLimits: () => readCodexLimits(binary, deps.env) } : {}),
59
62
  }).complete({
60
63
  ...request,
61
64
  ...(id === "codex" && request.thinking !== undefined
@@ -79,7 +82,12 @@ export function createHostRuntime(deps) {
79
82
  models: () => codexImage({ run: deps.run, readModels: () => models("codex") }).models(),
80
83
  generate: async (request) => {
81
84
  const binary = await command("codex-image");
82
- return codexImage({ run: deps.run, binary, env: deps.env }).generate(request);
85
+ return codexImage({
86
+ run: deps.run,
87
+ binary,
88
+ env: deps.env,
89
+ readLimits: () => readCodexLimits(binary, deps.env),
90
+ }).generate(request);
83
91
  },
84
92
  },
85
93
  };
@@ -6,6 +6,7 @@ import { z } from "zod";
6
6
  import { cliLoginError } from "../adapters/llm/cli-login-error.js";
7
7
  import { cliCommand } from "../kernel/cli-command.js";
8
8
  import { cliProbeTimeoutMs, readinessFromProbe, } from "../slices/settings/cli-status.js";
9
+ import { nodeGeminiLoginHost, readGeminiLogin } from "./gemini-login.js";
9
10
  export function hostCommandName(id) {
10
11
  return id === "claude-code" ? "claude" : id === "codex-image" ? "codex" : id;
11
12
  }
@@ -48,9 +49,10 @@ export function parseHostLogin(id, stdout, stderr) {
48
49
  return "unknown";
49
50
  }
50
51
  }
51
- export async function readHostLogin(binary, id, signal) {
52
+ export async function readHostLogin(binary, id, signal, gemini = nodeGeminiLoginHost()) {
53
+ // The Gemini CLI has no status command; its settings and credential files answer instead.
52
54
  if (id === "gemini")
53
- return "unknown";
55
+ return readGeminiLogin(gemini);
54
56
  const command = cliCommand(binary);
55
57
  return new Promise((resolve) => {
56
58
  execFile(command.file, [...command.args, ...(id === "claude-code" ? ["auth", "status"] : ["login", "status"])], { timeout: cliProbeTimeoutMs, maxBuffer: 64 * 1024, signal, windowsHide: true }, (error, stdout, stderr) => {
@@ -0,0 +1,40 @@
1
+ -- Automatic reviews. Library prompts of the new "review" kind are what a reviewer model is
2
+ -- asked per stage; SQLite cannot change a CHECK in place, so the table is copied into one whose
3
+ -- CHECK lists the new kind, like 0019 did for "shorts".
4
+ CREATE TABLE prompts_review_upgrade (
5
+ id TEXT PRIMARY KEY,
6
+ kind TEXT NOT NULL CHECK (kind IN ('article','image','thumbnail','narration','description','shorts','review')),
7
+ name TEXT NOT NULL,
8
+ body TEXT NOT NULL,
9
+ slots TEXT NOT NULL,
10
+ updated_at TEXT NOT NULL
11
+ );
12
+ INSERT INTO prompts_review_upgrade (id,kind,name,body,slots,updated_at)
13
+ SELECT id,kind,name,body,slots,updated_at FROM prompts;
14
+ DROP TABLE prompts;
15
+ ALTER TABLE prompts_review_upgrade RENAME TO prompts;
16
+ CREATE UNIQUE INDEX prompts_name ON prompts(kind,lower(name));
17
+ -- Every verdict a reviewer gave, with its reasons, what Slopify did about it (passed, flagged,
18
+ -- or sent back to be made again) and what the person did (overruled it, or had it redone).
19
+ -- One row per review step fingerprint: the same review of the same output is not asked twice.
20
+ CREATE TABLE review_verdicts (
21
+ id TEXT PRIMARY KEY,
22
+ project_id TEXT NOT NULL REFERENCES projects(id) ON DELETE CASCADE,
23
+ revision_id TEXT NOT NULL,
24
+ item_key TEXT NOT NULL,
25
+ stage TEXT NOT NULL CHECK(stage IN ('article','images','narration','thumbnail','shorts')),
26
+ item_fingerprint TEXT NOT NULL,
27
+ review_fingerprint TEXT NOT NULL,
28
+ passed INTEGER NOT NULL CHECK(passed IN (0,1)),
29
+ reasons TEXT NOT NULL,
30
+ outcome TEXT NOT NULL CHECK(outcome IN ('passed','flagged','redo')),
31
+ attempt INTEGER NOT NULL CHECK(attempt >= 1),
32
+ action TEXT CHECK(action IN ('overruled','redone')),
33
+ action_at TEXT,
34
+ redo_state TEXT CHECK(redo_state IN ('pending','started','failed')),
35
+ redo_error TEXT,
36
+ created_at TEXT NOT NULL,
37
+ UNIQUE(project_id,review_fingerprint)
38
+ );
39
+ CREATE INDEX review_verdicts_item ON review_verdicts(project_id,item_key);
40
+ CREATE INDEX review_verdicts_redo ON review_verdicts(redo_state);
@@ -0,0 +1,23 @@
1
+ -- A schedule can find its own topics: a series brief, a generation mode and how many topics to
2
+ -- keep queued. `topic_llm_json` is NULL for the template's own LLM. The generating/failed
3
+ -- columns are the generation's own bookkeeping and never bump the schedule's version.
4
+ ALTER TABLE schedules ADD COLUMN brief TEXT;
5
+ ALTER TABLE schedules ADD COLUMN topic_mode TEXT NOT NULL DEFAULT 'off' CHECK(topic_mode IN ('off','queue','hold'));
6
+ ALTER TABLE schedules ADD COLUMN topic_min INTEGER NOT NULL DEFAULT 10 CHECK(topic_min BETWEEN 1 AND 100);
7
+ ALTER TABLE schedules ADD COLUMN topic_llm_json TEXT CHECK(topic_llm_json IS NULL OR json_valid(topic_llm_json));
8
+ ALTER TABLE schedules ADD COLUMN topics_generating_at TEXT;
9
+ ALTER TABLE schedules ADD COLUMN topics_generated_at TEXT;
10
+ ALTER TABLE schedules ADD COLUMN topics_failed_at TEXT;
11
+ ALTER TABLE schedules ADD COLUMN topics_error TEXT;
12
+ -- Topics outside the queue: generated ones held for approval, ones the user rejected, and ones
13
+ -- a run used. Rejected and used rows stay so a later generation never suggests them again.
14
+ CREATE TABLE schedule_topics (
15
+ id TEXT PRIMARY KEY,
16
+ schedule_id TEXT NOT NULL REFERENCES schedules(id) ON DELETE CASCADE,
17
+ title TEXT NOT NULL CHECK(length(trim(title)) BETWEEN 1 AND 200),
18
+ state TEXT NOT NULL CHECK(state IN ('held','rejected','used')),
19
+ rank INTEGER NOT NULL,
20
+ created_at TEXT NOT NULL,
21
+ decided_at TEXT
22
+ );
23
+ CREATE INDEX schedule_topics_schedule ON schedule_topics(schedule_id, state, rank);
@@ -0,0 +1,58 @@
1
+ -- What every successful provider call used and cost, for the project's Run cost tab. The price
2
+ -- is worked out when the call lands and stored with the rates it used, so a later catalogue
3
+ -- change never rewrites what a finished run cost. A CLI call costs nothing on the user's plan
4
+ -- (`on_plan`); `api_cost` is what the same tokens would have cost through the API.
5
+ CREATE TABLE provider_usage (
6
+ id TEXT PRIMARY KEY,
7
+ project_id TEXT NOT NULL REFERENCES projects(id) ON DELETE CASCADE,
8
+ stage TEXT NOT NULL,
9
+ kind TEXT NOT NULL CHECK(kind IN ('llm','tts','image','video')),
10
+ provider TEXT NOT NULL,
11
+ model TEXT NOT NULL,
12
+ tokens_in INTEGER,
13
+ tokens_out INTEGER,
14
+ tokens_cached INTEGER,
15
+ characters INTEGER,
16
+ images INTEGER,
17
+ seconds REAL,
18
+ size TEXT,
19
+ quality TEXT,
20
+ wall_ms INTEGER NOT NULL,
21
+ on_plan INTEGER NOT NULL DEFAULT 0 CHECK(on_plan IN (0,1)),
22
+ cost REAL,
23
+ api_model TEXT,
24
+ api_cost REAL,
25
+ price_json TEXT CHECK(price_json IS NULL OR json_valid(price_json)),
26
+ created_at TEXT NOT NULL
27
+ );
28
+ CREATE INDEX provider_usage_project ON provider_usage(project_id, created_at);
29
+
30
+ -- The plan windows a CLI reported around a project's calls, in the order they arrived, so the
31
+ -- tab can say how much of the 5-hour and weekly allowance the run took.
32
+ CREATE TABLE plan_limit_readings (
33
+ id TEXT PRIMARY KEY,
34
+ project_id TEXT NOT NULL REFERENCES projects(id) ON DELETE CASCADE,
35
+ account TEXT NOT NULL,
36
+ reading_json TEXT NOT NULL CHECK(json_valid(reading_json)),
37
+ created_at TEXT NOT NULL
38
+ );
39
+ CREATE INDEX plan_limit_readings_project ON plan_limit_readings(project_id, created_at);
40
+
41
+ -- A CLI whose plan allowance is used up, until when. One row per account: every project's
42
+ -- calls to it wait on the same reset.
43
+ CREATE TABLE plan_limit_waits (
44
+ account TEXT PRIMARY KEY,
45
+ resets_at TEXT,
46
+ retry_at TEXT NOT NULL,
47
+ detected_at TEXT NOT NULL
48
+ );
49
+
50
+ -- The stages waiting on an account. They outlive a restart: the boot resumes these projects
51
+ -- instead of leaving the stage interrupted.
52
+ CREATE TABLE plan_limit_waiters (
53
+ project_id TEXT NOT NULL REFERENCES projects(id) ON DELETE CASCADE,
54
+ stage TEXT NOT NULL,
55
+ account TEXT NOT NULL,
56
+ since TEXT NOT NULL,
57
+ PRIMARY KEY(project_id, stage, account)
58
+ );
@@ -0,0 +1,18 @@
1
+ -- A step that failed on something time can fix (a rate limit, a timeout, a dropped connection
2
+ -- or CLI) waits and runs again by itself. The wait is kept on the row, so a restart keeps it,
3
+ -- and the count is what caps it. `failure_kind` is the provider error's kind, which picks the
4
+ -- fix-it button a failed step shows.
5
+ ALTER TABLE revision_work ADD COLUMN auto_retries INTEGER NOT NULL DEFAULT 0;
6
+ ALTER TABLE revision_work ADD COLUMN retry_at TEXT;
7
+ ALTER TABLE revision_work ADD COLUMN failure_kind TEXT;
8
+ CREATE INDEX revision_work_retry ON revision_work(retry_at) WHERE retry_at IS NOT NULL;
9
+ ALTER TABLE stages ADD COLUMN retry_at TEXT;
10
+ ALTER TABLE stages ADD COLUMN failure_kind TEXT;
11
+ -- Soften and retry: the image step whose prompt a content filter refused rewords it with the
12
+ -- project's AI model before drawing again. One request per step, cleared once it succeeds.
13
+ CREATE TABLE prompt_softening (
14
+ project_id TEXT NOT NULL REFERENCES projects(id) ON DELETE CASCADE,
15
+ work_key TEXT NOT NULL,
16
+ requested_at TEXT NOT NULL,
17
+ PRIMARY KEY(project_id, work_key)
18
+ );
@@ -0,0 +1,33 @@
1
+ -- Every save of a Library prompt or intro/outro keeps a version, so History can show and
2
+ -- restore any of them. One table serves both libraries: `item_kind` says which, `kind` holds the
3
+ -- prompt's kind or the entry's category, and `mode` the entry's mode (NULL for a prompt). No
4
+ -- foreign key: the row it belongs to lives in one of two tables, and the save path deletes the
5
+ -- versions with the prompt or entry.
6
+ CREATE TABLE library_versions (
7
+ item_kind TEXT NOT NULL CHECK(item_kind IN ('prompt','entry')),
8
+ item_id TEXT NOT NULL,
9
+ version INTEGER NOT NULL CHECK(version >= 1),
10
+ kind TEXT NOT NULL,
11
+ mode TEXT CHECK(mode IS NULL OR mode IN ('text','llm')),
12
+ name TEXT NOT NULL,
13
+ body TEXT NOT NULL,
14
+ author TEXT NOT NULL,
15
+ restored_from INTEGER,
16
+ created_at TEXT NOT NULL,
17
+ PRIMARY KEY(item_kind,item_id,version)
18
+ );
19
+ -- What is saved today becomes version 1 of each.
20
+ INSERT INTO library_versions (item_kind,item_id,version,kind,mode,name,body,author,restored_from,created_at)
21
+ SELECT 'prompt',id,1,kind,NULL,name,body,'you',NULL,updated_at FROM prompts;
22
+ INSERT INTO library_versions (item_kind,item_id,version,kind,mode,name,body,author,restored_from,created_at)
23
+ SELECT 'entry',id,1,category,mode,name,body,'you',NULL,updated_at FROM entries;
24
+ -- The project page's hand edits to the YouTube description, chapters, hashtags and tags. Each
25
+ -- field keeps the user's text and the generated text it was edited from, so a regenerated
26
+ -- description can tell whether the user changed that field. `links_json` holds this project's
27
+ -- own channel links (such as "Previous video"), which win over the Settings list.
28
+ CREATE TABLE youtube_description_edits (
29
+ project_id TEXT PRIMARY KEY REFERENCES projects(id) ON DELETE CASCADE,
30
+ fields_json TEXT NOT NULL CHECK(json_valid(fields_json)),
31
+ links_json TEXT NOT NULL CHECK(json_valid(links_json)),
32
+ updated_at TEXT NOT NULL
33
+ );
@@ -0,0 +1,65 @@
1
+ -- Channels: the brand kit, the cast library and the series brief that templates, schedules
2
+ -- and projects belong to. Every install gets one default channel, "My channel", with a fixed
3
+ -- id so a backup made on one install names the same default on another, and everything that
4
+ -- exists moves into it. Nothing about a project's config changes, so no fingerprint does.
5
+ --
6
+ -- A template's channel is a plain column and a project's is a row of project_channels, neither
7
+ -- with a foreign key to channels: a backup can bring in a row naming a channel this install
8
+ -- never had, and such a row (like none at all) reads as the default channel. The projects
9
+ -- table itself is left as it is. A schedule's channel is its template's.
10
+ CREATE TABLE channels (
11
+ id TEXT PRIMARY KEY,
12
+ name TEXT NOT NULL CHECK(length(trim(name)) BETWEEN 1 AND 200),
13
+ is_default INTEGER NOT NULL DEFAULT 0 CHECK(is_default IN (0,1)),
14
+ brand_json TEXT NOT NULL DEFAULT '{}' CHECK(json_valid(brand_json)),
15
+ series_brief TEXT NOT NULL DEFAULT '',
16
+ version INTEGER NOT NULL DEFAULT 1 CHECK(version >= 1),
17
+ created_at TEXT NOT NULL,
18
+ updated_at TEXT NOT NULL
19
+ );
20
+ CREATE UNIQUE INDEX channels_one_default ON channels(is_default) WHERE is_default = 1;
21
+ INSERT INTO channels (id, name, is_default, created_at, updated_at) VALUES (
22
+ '00000000-0000-4000-8000-000000000001', 'My channel', 1,
23
+ strftime('%Y-%m-%dT%H:%M:%fZ', 'now'), strftime('%Y-%m-%dT%H:%M:%fZ', 'now')
24
+ );
25
+ -- The characters, creatures, places and objects a channel keeps drawing the same way.
26
+ CREATE TABLE cast_members (
27
+ id TEXT PRIMARY KEY,
28
+ channel_id TEXT NOT NULL REFERENCES channels(id) ON DELETE CASCADE,
29
+ kind TEXT NOT NULL CHECK(kind IN ('character','creature','place','object')),
30
+ name TEXT NOT NULL CHECK(length(trim(name)) BETWEEN 1 AND 200),
31
+ aliases_json TEXT NOT NULL DEFAULT '[]' CHECK(json_valid(aliases_json)),
32
+ description TEXT NOT NULL DEFAULT '',
33
+ version INTEGER NOT NULL DEFAULT 1 CHECK(version >= 1),
34
+ created_at TEXT NOT NULL,
35
+ updated_at TEXT NOT NULL
36
+ );
37
+ CREATE INDEX cast_members_channel ON cast_members(channel_id);
38
+ -- Picture bytes by content hash: cast reference images and the brand kit's end screen image.
39
+ -- A project names the hashes it was started with, so a row is never removed while it may be
40
+ -- named; the bytes are in the database so every database backup carries them.
41
+ CREATE TABLE image_blobs (
42
+ sha256 TEXT PRIMARY KEY,
43
+ mime TEXT NOT NULL CHECK(mime IN ('image/png','image/jpeg')),
44
+ bytes BLOB NOT NULL,
45
+ created_at TEXT NOT NULL
46
+ );
47
+ CREATE TABLE cast_images (
48
+ id TEXT PRIMARY KEY,
49
+ member_id TEXT NOT NULL REFERENCES cast_members(id) ON DELETE CASCADE,
50
+ source TEXT NOT NULL CHECK(source IN ('upload','generate')),
51
+ prompt TEXT,
52
+ state TEXT NOT NULL CHECK(state IN ('ready','generating','failed')),
53
+ error TEXT,
54
+ sha256 TEXT REFERENCES image_blobs(sha256),
55
+ created_at TEXT NOT NULL
56
+ );
57
+ CREATE INDEX cast_images_member ON cast_images(member_id);
58
+ ALTER TABLE project_templates ADD COLUMN channel_id TEXT;
59
+ CREATE TABLE project_channels (
60
+ project_id TEXT PRIMARY KEY REFERENCES projects(id) ON DELETE CASCADE,
61
+ channel_id TEXT NOT NULL
62
+ );
63
+ UPDATE project_templates SET channel_id = '00000000-0000-4000-8000-000000000001';
64
+ INSERT INTO project_channels (project_id, channel_id)
65
+ SELECT id, '00000000-0000-4000-8000-000000000001' FROM projects;
@@ -0,0 +1,23 @@
1
+ -- Multiple voices.
2
+ --
3
+ -- Library prompts of the new "script" kind: what a multi-voice run (audiobook, podcast, radio
4
+ -- drama, interview) asks the text model for in place of an article - speaker turns. SQLite
5
+ -- cannot change a CHECK in place, so the table is copied into one whose CHECK lists the new
6
+ -- kind, like 0025 did for "review".
7
+ CREATE TABLE prompts_script_upgrade (
8
+ id TEXT PRIMARY KEY,
9
+ kind TEXT NOT NULL CHECK (kind IN ('article','image','thumbnail','narration','description','shorts','review','script')),
10
+ name TEXT NOT NULL,
11
+ body TEXT NOT NULL,
12
+ slots TEXT NOT NULL,
13
+ updated_at TEXT NOT NULL
14
+ );
15
+ INSERT INTO prompts_script_upgrade (id,kind,name,body,slots,updated_at)
16
+ SELECT id,kind,name,body,slots,updated_at FROM prompts;
17
+ DROP TABLE prompts;
18
+ ALTER TABLE prompts_script_upgrade RENAME TO prompts;
19
+ CREATE UNIQUE INDEX prompts_name ON prompts(kind,lower(name));
20
+ -- A cast member's voice: provider, model, voice, pace and pronunciations, so a character or a
21
+ -- channel's host sounds the same in every episode. NULL is no voice, which every member saved
22
+ -- before this has.
23
+ ALTER TABLE cast_members ADD COLUMN voice_json TEXT CHECK (voice_json IS NULL OR json_valid(voice_json));
@@ -0,0 +1,13 @@
1
+ -- Library → Aliases: words the narrator says differently from how they are written ("Dr." as
2
+ -- "Doctor"). A project keeps its own copy of the list it started with, so nothing references
3
+ -- these rows and editing them never changes a project behind its back. `position` is the
4
+ -- order the editor shows them in.
5
+ CREATE TABLE narration_aliases (
6
+ id TEXT PRIMARY KEY,
7
+ position INTEGER NOT NULL,
8
+ written TEXT NOT NULL CHECK (length(trim(written)) > 0),
9
+ spoken TEXT NOT NULL CHECK (length(trim(spoken)) > 0),
10
+ whole_word INTEGER NOT NULL CHECK (whole_word IN (0, 1)),
11
+ case_sensitive INTEGER NOT NULL CHECK (case_sensitive IN (0, 1)),
12
+ updated_at TEXT NOT NULL
13
+ );
@@ -0,0 +1,8 @@
1
+ -- "Mark uploaded" on Home: the person says a finished video is on YouTube, so it leaves Ready
2
+ -- to upload. A row of its own rather than a projects column, like project_channels: nothing
3
+ -- about the project's config changes, so no fingerprint does, and deleting the project takes
4
+ -- the row with it.
5
+ CREATE TABLE project_uploads (
6
+ project_id TEXT PRIMARY KEY REFERENCES projects(id) ON DELETE CASCADE,
7
+ uploaded_at TEXT NOT NULL
8
+ );
@@ -0,0 +1,7 @@
1
+ -- Other languages.
2
+ --
3
+ -- The languages a saved voice speaks, as a JSON array of primary language codes (["es"]):
4
+ -- looked up from the provider when the voice is added, or typed in Settings → Voices. Play
5
+ -- lists a project's voices by it. NULL is unknown, which every voice saved before this is,
6
+ -- and an unknown voice is offered for every language.
7
+ ALTER TABLE voices ADD COLUMN languages_json TEXT CHECK (languages_json IS NULL OR json_valid(languages_json));
@@ -0,0 +1,64 @@
1
+ -- Channel essentials.
2
+ --
3
+ -- Trash: deleting a project, a Library prompt or intro/outro, or a template now only stamps
4
+ -- `deleted_at`; Settings → Trash lists the stamped rows for 30 days, and a daily purge removes
5
+ -- them for good. Every list and lookup skips stamped rows. The name indexes only count rows
6
+ -- that are not in the trash, so a name can be reused while the old one waits there. A later
7
+ -- migration that rebuilds one of these tables must carry `deleted_at` over.
8
+ --
9
+ -- A project's stamp lives in its own table rather than a column: the projects table is read
10
+ -- and written positionally in many places, and a project has no name index to relax.
11
+ CREATE TABLE project_trash (
12
+ project_id TEXT PRIMARY KEY REFERENCES projects(id) ON DELETE CASCADE,
13
+ deleted_at TEXT NOT NULL
14
+ );
15
+ ALTER TABLE prompts ADD COLUMN deleted_at TEXT;
16
+ ALTER TABLE entries ADD COLUMN deleted_at TEXT;
17
+ ALTER TABLE project_templates ADD COLUMN deleted_at TEXT;
18
+ DROP INDEX prompts_name;
19
+ CREATE UNIQUE INDEX prompts_name ON prompts(kind,lower(name)) WHERE deleted_at IS NULL;
20
+ DROP INDEX entries_name;
21
+ CREATE UNIQUE INDEX entries_name ON entries(category,lower(name)) WHERE deleted_at IS NULL;
22
+ -- A schedule already kept a `deleted_at` (0010: its history outlives it). `purged_at` marks one
23
+ -- that has left the trash; every schedule deleted before the trash existed counts as left.
24
+ ALTER TABLE schedules ADD COLUMN purged_at TEXT;
25
+ UPDATE schedules SET purged_at = deleted_at WHERE deleted_at IS NOT NULL;
26
+
27
+ -- Episode memory: a short summary each finished project leaves on its channel, read back into
28
+ -- the article and script prompts of later related episodes while the channel's setting is on.
29
+ -- No foreign key to projects: the memory outlives the project it came from.
30
+ CREATE TABLE episode_memories (
31
+ id TEXT PRIMARY KEY,
32
+ channel_id TEXT NOT NULL REFERENCES channels(id) ON DELETE CASCADE,
33
+ project_id TEXT NOT NULL,
34
+ title TEXT NOT NULL,
35
+ summary TEXT NOT NULL CHECK(length(trim(summary)) > 0),
36
+ cast_json TEXT NOT NULL DEFAULT '[]' CHECK(json_valid(cast_json)),
37
+ source TEXT NOT NULL CHECK(source IN ('generated','edited')),
38
+ created_at TEXT NOT NULL,
39
+ updated_at TEXT NOT NULL
40
+ );
41
+ CREATE UNIQUE INDEX episode_memories_project ON episode_memories(project_id);
42
+ CREATE INDEX episode_memories_channel ON episode_memories(channel_id, created_at);
43
+ -- Off for every channel that exists; a channel made from now on starts with it on. A fresh
44
+ -- install (no project yet) counts its default channel as new.
45
+ ALTER TABLE channels ADD COLUMN episode_memory INTEGER NOT NULL DEFAULT 0 CHECK(episode_memory IN (0,1));
46
+ UPDATE channels SET episode_memory = 1 WHERE NOT EXISTS (SELECT 1 FROM projects);
47
+ -- YouTube's "Altered or synthetic content" answer Studio prep gives: 'auto' follows the rule in
48
+ -- slices/studio/disclosure.ts, 'yes' and 'no' override it for the channel.
49
+ ALTER TABLE channels ADD COLUMN ai_disclosure TEXT NOT NULL DEFAULT 'auto' CHECK(ai_disclosure IN ('auto','yes','no'));
50
+
51
+ -- The channel's videos made before (or outside) Slopify: titles pasted or imported from a
52
+ -- YouTube Studio CSV, so topic suggestions and duplicate checks skip them too.
53
+ CREATE TABLE channel_videos (
54
+ id TEXT PRIMARY KEY,
55
+ channel_id TEXT NOT NULL REFERENCES channels(id) ON DELETE CASCADE,
56
+ title TEXT NOT NULL CHECK(length(trim(title)) BETWEEN 1 AND 500),
57
+ created_at TEXT NOT NULL
58
+ );
59
+ CREATE UNIQUE INDEX channel_videos_title ON channel_videos(channel_id, lower(title));
60
+
61
+ -- Episode memory (appended): what a generated summary was made from, a SHA-256 of the prompt
62
+ -- version, title and article text, so finishing the same article again asks no LLM. A summary
63
+ -- the user edited is never replaced by a later finish.
64
+ ALTER TABLE episode_memories ADD COLUMN recipe TEXT;