@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.
- package/LICENSE +201 -21
- package/NOTICE +12 -0
- package/README.md +21 -40
- package/SUBTITLES.md +1 -1
- package/dist/adapter-registry.js +11 -1
- package/dist/adapters/alignment/cache.js +4 -1
- package/dist/adapters/alignment/ctc.js +25 -23
- package/dist/adapters/alignment/index.js +37 -6
- package/dist/adapters/alignment/multilingual.js +267 -0
- package/dist/adapters/alignment/protocol.js +11 -0
- package/dist/adapters/alignment/quality.js +4 -3
- package/dist/adapters/alignment/sentences.js +95 -0
- package/dist/adapters/alignment/spec.js +24 -0
- package/dist/adapters/alignment/spell.js +769 -0
- package/dist/adapters/alignment/text.js +56 -9
- package/dist/adapters/alignment/window.js +34 -30
- package/dist/adapters/alignment/worker.js +23 -21
- package/dist/adapters/explain.js +11 -11
- package/dist/adapters/host-cli/index.js +55 -5
- package/dist/adapters/host-cli/transport.js +13 -6
- package/dist/adapters/image/bytes.js +2 -2
- package/dist/adapters/image/codex-output.js +60 -18
- package/dist/adapters/image/codex.js +166 -25
- package/dist/adapters/image/fal.js +31 -3
- package/dist/adapters/image/google.js +17 -2
- package/dist/adapters/image/openai.js +45 -16
- package/dist/adapters/image/reference.js +41 -0
- package/dist/adapters/image/replicate.js +14 -2
- package/dist/adapters/image/video.js +2 -2
- package/dist/adapters/key-probes.js +36 -0
- package/dist/adapters/llm/claude-code.js +168 -32
- package/dist/adapters/llm/cli-login-error.js +1 -1
- package/dist/adapters/llm/codex-limits.js +157 -0
- package/dist/adapters/llm/codex-models.js +2 -1
- package/dist/adapters/llm/codex-server-models.js +2 -1
- package/dist/adapters/llm/codex.js +99 -12
- package/dist/adapters/llm/document-workspace.js +2 -2
- package/dist/adapters/llm/gemini-workspace.js +1 -1
- package/dist/adapters/llm/gemini.js +16 -3
- package/dist/adapters/llm/image-workspace.js +58 -0
- package/dist/adapters/llm/openrouter.js +11 -3
- package/dist/adapters/llm/run-cli.js +26 -9
- package/dist/adapters/tts/cartesia.js +10 -0
- package/dist/adapters/tts/elevenlabs.js +31 -9
- package/dist/adapters/tts/inworld-async.js +9 -5
- package/dist/adapters/tts/inworld.js +17 -3
- package/dist/adapters/tts/openai.js +3 -0
- package/dist/adapters/tts/voice-languages.js +80 -0
- package/dist/assets/fonts/noto/NotoSans-Regular.ttf +0 -0
- package/dist/assets/fonts/noto/NotoSansArabic-Regular.ttf +0 -0
- package/dist/assets/fonts/noto/NotoSansDevanagari-Regular.ttf +0 -0
- package/dist/assets/fonts/noto/NotoSansHebrew-Regular.ttf +0 -0
- package/dist/assets/fonts/noto/NotoSansThai-Regular.ttf +0 -0
- package/dist/assets/fonts/noto/OFL.txt +93 -0
- package/dist/assets/fonts/noto/SOURCE.txt +18 -0
- package/dist/assets/models.yaml +7 -0
- package/dist/assets/sample/sample-audiobook.tar +0 -0
- package/dist/assets/sample/sample-podcast.tar +0 -0
- package/dist/assets/sample/sample-project.tar +0 -0
- package/dist/catalog/merge.js +132 -0
- package/dist/catalog/registry.js +33 -12
- package/dist/catalog/runtime-models.js +1 -1
- package/dist/catalog/schema.js +12 -1
- package/dist/catalog/store.js +147 -8
- package/dist/catalog/validate.js +17 -2
- package/dist/compose.yaml +71 -0
- package/dist/edge/autostart/docker-record.js +32 -0
- package/dist/edge/autostart/docker.js +50 -0
- package/dist/edge/autostart/index.js +78 -0
- package/dist/edge/autostart/launcher.js +176 -0
- package/dist/edge/autostart/model.js +3 -0
- package/dist/edge/autostart/native.js +210 -0
- package/dist/edge/autostart/prompt.js +66 -0
- package/dist/edge/autostart/service.js +81 -0
- package/dist/edge/cli.js +57 -51
- package/dist/edge/docker-install/activation.js +56 -0
- package/dist/edge/docker-install/apply.js +458 -0
- package/dist/edge/docker-install/engine.js +362 -0
- package/dist/edge/docker-install/run.js +154 -0
- package/dist/edge/docker-install/state.js +146 -0
- package/dist/edge/{docker-projects → docker-install}/volume.js +1 -5
- package/dist/edge/docker.js +14 -22
- package/dist/edge/events/hub.js +11 -0
- package/dist/edge/host-cli.js +1 -1
- package/dist/edge/http/actions.js +32 -10
- package/dist/edge/http/app.js +60 -2
- package/dist/edge/http/auditions.js +70 -0
- package/dist/edge/http/autostart.js +37 -0
- package/dist/edge/http/backups.js +40 -0
- package/dist/edge/http/channel-memory.js +92 -0
- package/dist/edge/http/channels.js +128 -0
- package/dist/edge/http/entries.js +22 -3
- package/dist/edge/http/files.js +1 -1
- package/dist/edge/http/folder-location.js +1 -1
- package/dist/edge/http/home.js +39 -0
- package/dist/edge/http/host-cli.js +45 -6
- package/dist/edge/http/narration-peaks.js +76 -0
- package/dist/edge/http/onboarding.js +152 -0
- package/dist/edge/http/open-folder.js +1 -1
- package/dist/edge/http/patch-notes.js +45 -0
- package/dist/edge/http/planning.js +4 -2
- package/dist/edge/http/problem.js +1 -0
- package/dist/edge/http/project-create.js +87 -0
- package/dist/edge/http/projects.js +31 -99
- package/dist/edge/http/prompts.js +40 -3
- package/dist/edge/http/pronunciations.js +23 -4
- package/dist/edge/http/providers.js +86 -0
- package/dist/edge/http/reviews.js +121 -0
- package/dist/edge/http/revision-files.js +1 -1
- package/dist/edge/http/revisions.js +1 -1
- package/dist/edge/http/run-cost.js +25 -0
- package/dist/edge/http/schedules.js +111 -6
- package/dist/edge/http/settings.js +128 -6
- package/dist/edge/http/storage-files.js +50 -0
- package/dist/edge/http/storage.js +29 -2
- package/dist/edge/http/studio.js +207 -0
- package/dist/edge/http/style-preview.js +100 -0
- package/dist/edge/http/subtitles.js +1 -1
- package/dist/edge/http/trash.js +69 -0
- package/dist/edge/http/update.js +24 -3
- package/dist/edge/http/whats-new.js +15 -0
- package/dist/edge/http/youtube-edits.js +83 -0
- package/dist/edge/native-update.js +62 -0
- package/dist/host-cli/open-folder.js +10 -10
- package/dist/host-cli/runtime.js +12 -3
- package/dist/kernel/db/migrations/0022-drop-duplicate-held-work.sql +56 -0
- package/dist/kernel/db/migrations/0024-reference-attachments.sql +18 -0
- package/dist/kernel/db/migrations/0025-automatic-reviews.sql +40 -0
- package/dist/kernel/db/migrations/0026-schedule-topic-generation.sql +23 -0
- package/dist/kernel/db/migrations/0027-run-cost.sql +58 -0
- package/dist/kernel/db/migrations/0030-automatic-retries.sql +18 -0
- package/dist/kernel/db/migrations/0031-prompt-history-and-description-edits.sql +33 -0
- package/dist/kernel/db/migrations/0032-channels.sql +65 -0
- package/dist/kernel/db/migrations/0034-script-prompts.sql +23 -0
- package/dist/kernel/db/migrations/0035-narration-aliases.sql +13 -0
- package/dist/kernel/db/migrations/0037-project-uploads.sql +8 -0
- package/dist/kernel/db/migrations/0038-voice-languages.sql +7 -0
- package/dist/kernel/db/migrations/0039-channel-essentials.sql +64 -0
- package/dist/kernel/db/migrations/0040-standalone-usage.sql +35 -0
- package/dist/kernel/paths.js +27 -6
- package/dist/kernel/pipeline.js +4 -1
- package/dist/kernel/ports/host-cli.js +139 -16
- package/dist/kernel/ports/image.js +3 -1
- package/dist/kernel/ports/key-probe.js +1 -0
- package/dist/kernel/ports/languages.js +157 -0
- package/dist/kernel/ports/llm.js +3 -1
- package/dist/kernel/ports/model.js +5 -2
- package/dist/kernel/ports/narration-aliases.js +60 -0
- package/dist/kernel/ports/plan-limits.js +36 -0
- package/dist/kernel/runner/attempt.js +18 -9
- package/dist/kernel/runner/audition.js +36 -0
- package/dist/kernel/runner/graph.js +24 -5
- package/dist/kernel/runner/index.js +35 -5
- package/dist/kernel/runner/meter.js +1 -0
- package/dist/kernel/runner/providers.js +164 -9
- package/dist/kernel/runner/retry-policy.js +43 -0
- package/dist/kernel/runner/standalone.js +134 -0
- package/dist/kernel/runner/work-authority.js +28 -2
- package/dist/main.js +321 -20
- package/dist/patch-notes/0.4.2-to-1.0.md +93 -0
- package/dist/patch-notes/1.0-to-2.0.md +74 -0
- package/dist/patch-notes/2.0-to-3.0.md +257 -0
- package/dist/patch-notes/3.0.0.md +70 -0
- package/dist/patch-notes/index.json +26 -0
- package/dist/slices/admission/model.js +35 -0
- package/dist/slices/admission/repo.js +24 -4
- package/dist/slices/admission/rules.js +126 -1
- package/dist/slices/admission/schema.js +68 -1
- package/dist/slices/admission/short-mode.js +47 -0
- package/dist/slices/admission/start.js +90 -16
- package/dist/slices/article/continuation.js +2 -2
- package/dist/slices/article/provided-entries.js +2 -2
- package/dist/slices/article/run.js +6 -2
- package/dist/slices/article/segments.js +3 -3
- package/dist/slices/backups/files.js +135 -0
- package/dist/slices/backups/folder.js +43 -0
- package/dist/slices/backups/model.js +65 -0
- package/dist/slices/backups/repo.js +28 -0
- package/dist/slices/backups/schedule.js +60 -0
- package/dist/slices/backups/service.js +311 -0
- package/dist/slices/batch/index.js +9 -2
- package/dist/slices/cancel/index.js +1 -1
- package/dist/slices/channels/cast-images.js +110 -0
- package/dist/slices/channels/cast-match.js +32 -0
- package/dist/slices/channels/images.js +19 -0
- package/dist/slices/channels/model.js +5 -0
- package/dist/slices/channels/rebrand.js +100 -0
- package/dist/slices/channels/repo.js +218 -0
- package/dist/slices/channels/runs.js +129 -0
- package/dist/slices/channels/schema.js +129 -0
- package/dist/slices/channels/service.js +180 -0
- package/dist/slices/channels/studio-csv.js +110 -0
- package/dist/slices/channels/videos.js +120 -0
- package/dist/slices/checkpoints/recovery.js +10 -0
- package/dist/slices/control/index.js +2 -1
- package/dist/slices/document/fonts.js +1 -1
- package/dist/slices/document/render.js +1 -1
- package/dist/slices/episodes/related.js +90 -0
- package/dist/slices/episodes/repo.js +60 -0
- package/dist/slices/episodes/service.js +58 -0
- package/dist/slices/episodes/summarize.js +170 -0
- package/dist/slices/estimate/index.js +113 -24
- package/dist/slices/estimate/requests.js +60 -2
- package/dist/slices/fixes/rules.js +73 -0
- package/dist/slices/fonts/coverage.js +82 -0
- package/dist/slices/fonts/sfnt.js +74 -0
- package/dist/slices/images/run.js +8 -5
- package/dist/slices/images/scale.js +84 -0
- package/dist/slices/library/diff.js +122 -0
- package/dist/slices/library/history.js +100 -0
- package/dist/slices/library/model.js +3 -0
- package/dist/slices/library/photorealistic.js +49 -0
- package/dist/slices/library/repo.js +19 -14
- package/dist/slices/library/save.js +62 -12
- package/dist/slices/library/slots.js +15 -3
- package/dist/slices/library/snapshot.js +8 -2
- package/dist/slices/library/used-by.js +153 -0
- package/dist/slices/loudness/level-pieces.js +41 -0
- package/dist/slices/loudness/loudnorm.js +164 -0
- package/dist/slices/loudness/model.js +153 -0
- package/dist/slices/model-upkeep/model.js +10 -0
- package/dist/slices/model-upkeep/switch.js +217 -0
- package/dist/slices/model-upkeep/usage.js +183 -0
- package/dist/slices/narration/aliases-library.js +31 -0
- package/dist/slices/narration/aliases-schema.js +53 -0
- package/dist/slices/narration/aliases.js +49 -0
- package/dist/slices/narration/blocks.js +352 -0
- package/dist/slices/narration/chunk.js +19 -7
- package/dist/slices/narration/concat.js +2 -2
- package/dist/slices/narration/describe.js +45 -0
- package/dist/slices/narration/pauses-model.js +63 -0
- package/dist/slices/narration/pauses.js +269 -0
- package/dist/slices/narration/peaks-model.js +1 -0
- package/dist/slices/narration/peaks.js +94 -0
- package/dist/slices/narration/preparation.js +21 -3
- package/dist/slices/narration/pronunciation-chunks.js +3 -3
- package/dist/slices/narration/pronunciation.js +72 -25
- package/dist/slices/narration/run.js +6 -6
- package/dist/slices/narration/shared-glossary.js +1 -1
- package/dist/slices/narration/steering.js +15 -4
- package/dist/slices/notifications/notifier.js +94 -0
- package/dist/slices/notifications/rules.js +102 -0
- package/dist/slices/notifications/send.js +40 -0
- package/dist/slices/notifications/settings.js +34 -0
- package/dist/slices/onboarding/copy.js +143 -0
- package/dist/slices/onboarding/first-run.js +46 -0
- package/dist/slices/onboarding/install.js +119 -0
- package/dist/slices/onboarding/model.js +64 -0
- package/dist/slices/onboarding/packs.js +491 -0
- package/dist/slices/onboarding/quick-short.js +129 -0
- package/dist/slices/onboarding/sample.js +132 -0
- package/dist/slices/onboarding/state.js +68 -0
- package/dist/slices/onboarding/template.js +73 -0
- package/dist/slices/patch-notes/library.js +73 -0
- package/dist/slices/patch-notes/seen.js +77 -0
- package/dist/slices/play-drafts/convert.js +160 -9
- package/dist/slices/play-drafts/readiness.js +3 -0
- package/dist/slices/play-drafts/repo.js +9 -1
- package/dist/slices/play-drafts/review-inputs.js +22 -3
- package/dist/slices/play-drafts/schema.js +109 -2
- package/dist/slices/play-drafts/service.js +10 -1
- package/dist/slices/play-drafts/start.js +43 -18
- package/dist/slices/play-drafts/uploads.js +2 -1
- package/dist/slices/project-templates/from-project.js +70 -4
- package/dist/slices/project-templates/one-off.js +26 -0
- package/dist/slices/project-templates/repo.js +2 -1
- package/dist/slices/project-templates/service.js +15 -4
- package/dist/slices/project-templates/setup.js +29 -2
- package/dist/slices/rebuild/admission-repo.js +1 -1
- package/dist/slices/rebuild/model.js +6 -0
- package/dist/slices/rebuild/preview-details.js +67 -2
- package/dist/slices/rebuild/preview-plan.js +13 -6
- package/dist/slices/rebuild/recipe-audio-parts.js +40 -7
- package/dist/slices/rebuild/recipe-audio.js +66 -12
- package/dist/slices/rebuild/recipe-build.js +15 -5
- package/dist/slices/rebuild/recipe-cast.js +18 -0
- package/dist/slices/rebuild/recipe-checkpoints.js +11 -9
- package/dist/slices/rebuild/recipe-describe.js +93 -0
- package/dist/slices/rebuild/recipe-edit.js +13 -1
- package/dist/slices/rebuild/recipe-exports.js +62 -8
- package/dist/slices/rebuild/recipe-input-schema.js +43 -1
- package/dist/slices/rebuild/recipe-legacy.js +12 -0
- package/dist/slices/rebuild/recipe-loudness.js +45 -0
- package/dist/slices/rebuild/recipe-model.js +25 -6
- package/dist/slices/rebuild/recipe-pauses.js +7 -0
- package/dist/slices/rebuild/recipe-preparation.js +3 -3
- package/dist/slices/rebuild/recipe-provider-choice.js +11 -1
- package/dist/slices/rebuild/recipe-reference.js +58 -0
- package/dist/slices/rebuild/recipe-reviews.js +117 -0
- package/dist/slices/rebuild/recipe-save.js +4 -1
- package/dist/slices/rebuild/recipe-shorts.js +32 -5
- package/dist/slices/rebuild/recipe-text.js +114 -19
- package/dist/slices/rebuild/recipe-validation.js +29 -3
- package/dist/slices/rebuild/recipe-visual.js +104 -29
- package/dist/slices/rebuild/recipe-voices.js +286 -0
- package/dist/slices/rebuild/recipe-work.js +77 -6
- package/dist/slices/rebuild/recipe-youtube.js +2 -0
- package/dist/slices/rebuild/recovery-conflict.js +7 -3
- package/dist/slices/rebuild/recovery-model.js +3 -0
- package/dist/slices/rebuild/recovery-selection.js +28 -2
- package/dist/slices/rebuild/recovery.js +35 -17
- package/dist/slices/rebuild/repo.js +3 -3
- package/dist/slices/rebuild/retry.js +18 -0
- package/dist/slices/rebuild/review-redo.js +110 -0
- package/dist/slices/rebuild/runtime-actions.js +14 -2
- package/dist/slices/rebuild/runtime-admission.js +4 -1
- package/dist/slices/rebuild/runtime-animate.js +4 -4
- package/dist/slices/rebuild/runtime-article.js +5 -5
- package/dist/slices/rebuild/runtime-document.js +4 -4
- package/dist/slices/rebuild/runtime-export-bed.js +43 -0
- package/dist/slices/rebuild/runtime-export-edit.js +70 -11
- package/dist/slices/rebuild/runtime-export-inputs.js +52 -10
- package/dist/slices/rebuild/runtime-export-short.js +85 -0
- package/dist/slices/rebuild/runtime-export.js +84 -19
- package/dist/slices/rebuild/runtime-figure-card.js +87 -0
- package/dist/slices/rebuild/runtime-image.js +69 -0
- package/dist/slices/rebuild/runtime-local.js +154 -27
- package/dist/slices/rebuild/runtime-materialize.js +8 -0
- package/dist/slices/rebuild/runtime-narration-reuse.js +2 -1
- package/dist/slices/rebuild/runtime-narration-text.js +7 -7
- package/dist/slices/rebuild/runtime-pictures.js +28 -0
- package/dist/slices/rebuild/runtime-piece-label.js +29 -0
- package/dist/slices/rebuild/runtime-provider.js +134 -19
- package/dist/slices/rebuild/runtime-publication.js +21 -5
- package/dist/slices/rebuild/runtime-review.js +237 -0
- package/dist/slices/rebuild/runtime-run.js +17 -11
- package/dist/slices/rebuild/runtime-shorts.js +48 -23
- package/dist/slices/rebuild/runtime-store.js +39 -10
- package/dist/slices/rebuild/runtime-subtitles.js +72 -19
- package/dist/slices/rebuild/runtime-voices.js +103 -0
- package/dist/slices/rebuild/runtime-youtube.js +7 -6
- package/dist/slices/rebuild/service-readiness.js +15 -1
- package/dist/slices/rebuild/soften.js +49 -0
- package/dist/slices/rebuild/transition-repo.js +10 -3
- package/dist/slices/research/run.js +4 -4
- package/dist/slices/research/synthesis.js +2 -2
- package/dist/slices/reviews/model.js +105 -0
- package/dist/slices/reviews/outcome.js +22 -0
- package/dist/slices/reviews/repo.js +106 -0
- package/dist/slices/reviews/rules.js +63 -0
- package/dist/slices/reviews/verdict.js +71 -0
- package/dist/slices/reviews/view.js +30 -0
- package/dist/slices/revisions/adopt-content.js +12 -2
- package/dist/slices/revisions/adopt.js +9 -2
- package/dist/slices/revisions/downloads.js +7 -1
- package/dist/slices/revisions/image-plan.js +183 -0
- package/dist/slices/revisions/mutation-assets.js +43 -9
- package/dist/slices/revisions/mutation-cues.js +71 -0
- package/dist/slices/revisions/mutation-prepare.js +8 -10
- package/dist/slices/revisions/mutation-request.js +2 -1
- package/dist/slices/revisions/mutations.js +97 -3
- package/dist/slices/revisions/schema.js +16 -2
- package/dist/slices/run-cost/limits.js +128 -0
- package/dist/slices/run-cost/meter.js +47 -0
- package/dist/slices/run-cost/panel.js +234 -0
- package/dist/slices/run-cost/pricing.js +92 -0
- package/dist/slices/run-cost/week.js +108 -0
- package/dist/slices/schedules/agenda.js +158 -0
- package/dist/slices/schedules/repo.js +58 -18
- package/dist/slices/schedules/scheduler.js +67 -16
- package/dist/slices/schedules/schema.js +138 -1
- package/dist/slices/schedules/service.js +48 -4
- package/dist/slices/schedules/similar.js +90 -0
- package/dist/slices/schedules/topic-list.js +163 -0
- package/dist/slices/schedules/topics.js +422 -0
- package/dist/slices/settings/first-run.js +102 -0
- package/dist/slices/settings/health.js +190 -0
- package/dist/slices/settings/key-guides.js +128 -0
- package/dist/slices/settings/key-test.js +85 -0
- package/dist/slices/settings/model.js +11 -1
- package/dist/slices/settings/playback.js +15 -0
- package/dist/slices/settings/repo.js +17 -3
- package/dist/slices/settings/voices.js +64 -4
- package/dist/slices/settings/whats-new.js +64 -0
- package/dist/slices/shorts/captions.js +9 -0
- package/dist/slices/shorts/pick.js +3 -3
- package/dist/slices/shorts/prompts.js +1 -1
- package/dist/slices/shorts/render.js +54 -5
- package/dist/slices/storage/asset-name.js +3 -0
- package/dist/slices/storage/backup-export.js +48 -9
- package/dist/slices/storage/backup-format.js +38 -1
- package/dist/slices/storage/backup-import.js +191 -19
- package/dist/slices/storage/delete-project.js +7 -4
- package/dist/slices/storage/documents.js +79 -0
- package/dist/slices/storage/files-location.js +531 -0
- package/dist/slices/storage/layout.js +20 -0
- package/dist/slices/storage/model.js +14 -0
- package/dist/slices/storage/portable.js +95 -12
- package/dist/slices/storage/reconcile.js +19 -2
- package/dist/slices/storage/repo.js +4 -1
- package/dist/slices/storage/schema.js +11 -0
- package/dist/slices/storage/trim.js +146 -0
- package/dist/slices/studio/disclosure.js +74 -0
- package/dist/slices/studio/model.js +28 -0
- package/dist/slices/studio/pack.js +209 -0
- package/dist/slices/studio/settings.js +133 -0
- package/dist/slices/style-preview/images.js +61 -0
- package/dist/slices/style-preview/render.js +161 -0
- package/dist/slices/style-preview/schema.js +45 -0
- package/dist/slices/style-preview/service.js +93 -0
- package/dist/slices/style-preview/settings.js +89 -0
- package/dist/slices/subtitles/captions.js +42 -9
- package/dist/slices/subtitles/model.js +22 -0
- package/dist/slices/subtitles/prepare.js +27 -16
- package/dist/slices/subtitles/transcript.js +1 -1
- package/dist/slices/thumbnail/run.js +7 -7
- package/dist/slices/trash/model.js +25 -0
- package/dist/slices/trash/service.js +273 -0
- package/dist/slices/uploads/repo.js +20 -0
- package/dist/slices/video/ambient-bed-schema.js +15 -0
- package/dist/slices/video/ambient-bed.js +95 -0
- package/dist/slices/video/ambient-mix.js +72 -0
- package/dist/slices/video/audio-inputs.js +2 -2
- package/dist/slices/video/cards.js +5 -2
- package/dist/slices/video/edit-list.js +23 -2
- package/dist/slices/video/edit-settings.js +13 -3
- package/dist/slices/video/ffmpeg.js +63 -9
- package/dist/slices/video/figure-card.js +492 -0
- package/dist/slices/video/figure-spans.js +51 -0
- package/dist/slices/video/plan.js +125 -13
- package/dist/slices/video/run.js +14 -12
- package/dist/slices/video/slideshow.js +39 -4
- package/dist/slices/voices/attribution.js +69 -0
- package/dist/slices/voices/audio-files.js +82 -0
- package/dist/slices/voices/audition.js +25 -0
- package/dist/slices/voices/cast.js +56 -0
- package/dist/slices/voices/grouping.js +49 -0
- package/dist/slices/voices/join.js +35 -0
- package/dist/slices/voices/languages.js +25 -0
- package/dist/slices/voices/model.js +179 -0
- package/dist/slices/voices/palette.js +26 -0
- package/dist/slices/voices/panel.js +91 -0
- package/dist/slices/voices/portraits.js +55 -0
- package/dist/slices/voices/script.js +136 -0
- package/dist/slices/voices/timing.js +58 -0
- package/dist/slices/youtube/answer.js +1 -1
- package/dist/slices/youtube/chapters.js +126 -0
- package/dist/slices/youtube/edits-repo.js +69 -0
- package/dist/slices/youtube/edits.js +89 -0
- package/dist/slices/youtube/placeholders.js +98 -0
- package/dist/slices/youtube/transcript.js +11 -4
- package/dist/updater/service.js +66 -21
- package/dist/web/assets/index-CUA2sUxU.css +1 -0
- package/dist/web/assets/index-CX590kE-.js +303 -0
- package/dist/web/assets/jetbrains-mono-cyrillic-400-normal-BEIGL1Tu.woff2 +0 -0
- package/dist/web/assets/jetbrains-mono-cyrillic-400-normal-ugxPyKxw.woff +0 -0
- package/dist/web/assets/jetbrains-mono-greek-400-normal-B9oWc5Lo.woff +0 -0
- package/dist/web/assets/jetbrains-mono-greek-400-normal-C190GLew.woff2 +0 -0
- package/dist/web/assets/jetbrains-mono-latin-400-normal-6-qcROiO.woff +0 -0
- package/dist/web/assets/jetbrains-mono-latin-400-normal-V6pRDFza.woff2 +0 -0
- package/dist/web/assets/jetbrains-mono-latin-ext-400-normal-Bc8Ftmh3.woff2 +0 -0
- package/dist/web/assets/jetbrains-mono-latin-ext-400-normal-fXTG6kC5.woff +0 -0
- package/dist/web/assets/jetbrains-mono-vietnamese-400-normal-CqNFfHCs.woff +0 -0
- package/dist/web/assets/{pdf-CAJNT254.js → pdf-BG8n_uc5.js} +1 -1
- package/dist/web/index.html +2 -2
- package/package.json +4 -4
- package/dist/edge/docker-launch.js +0 -81
- package/dist/edge/docker-projects/activation.js +0 -35
- package/dist/edge/docker-projects/claims.js +0 -85
- package/dist/edge/docker-projects/committed.js +0 -40
- package/dist/edge/docker-projects/engine.js +0 -461
- package/dist/edge/docker-projects/install.js +0 -254
- package/dist/edge/docker-projects/prune.js +0 -66
- package/dist/edge/docker-projects/recover.js +0 -100
- package/dist/edge/docker-projects/state.js +0 -225
- package/dist/web/assets/index-BGgYMm2u.css +0 -1
- package/dist/web/assets/index-BVVgtntL.js +0 -171
- package/scripts/docker-run.sh +0 -3
- /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
|
-
|
|
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
|
|
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
|
|
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
|
|
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 {
|
|
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
|
-
|
|
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
|
-
...(
|
|
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
|
-
|
|
62
|
-
...(documents ? ["--mcp-config", documents.config
|
|
63
|
-
...(
|
|
64
|
-
|
|
65
|
-
|
|
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({
|
|
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({
|
|
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
|
-
|
|
105
|
-
|
|
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:
|
|
152
|
+
cwd: folder,
|
|
108
153
|
env: {
|
|
109
154
|
...(deps.env ?? process.env),
|
|
110
155
|
CLAUDE_CODE_SAFE_MODE: "0",
|
|
111
|
-
PWD:
|
|
156
|
+
PWD: folder,
|
|
112
157
|
},
|
|
113
158
|
}
|
|
114
159
|
: {}),
|
|
115
|
-
stdin: cliInput([documents?.instructions, promptOf(req.messages)]
|
|
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
|
|
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 (
|
|
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
|
|
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:
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
233
|
-
|
|
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
|
-
|
|
239
|
-
|
|
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
|
|
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
|
+
}
|