@gentbajko/slopify 0.8.4 → 1.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.
- package/README.md +28 -6
- package/dist/adapters/llm/claude-code.js +6 -3
- package/dist/adapters/llm/codex.js +102 -12
- package/dist/adapters/llm/gemini.js +11 -8
- package/dist/adapters/llm/run-cli.js +61 -6
- package/dist/adapters/tts/inworld-async.js +4 -4
- package/dist/adapters/tts/inworld.js +73 -68
- package/dist/catalog/registry.js +13 -73
- package/dist/edge/cli.js +5 -2
- package/dist/edge/events/hub.js +7 -2
- package/dist/edge/events/preview-cache.js +7 -4
- package/dist/edge/events/visibility.js +33 -0
- package/dist/edge/http/actions.js +74 -79
- package/dist/edge/http/app.js +39 -6
- package/dist/edge/http/audio-preview.js +9 -1
- package/dist/edge/http/checkpoints.js +95 -0
- package/dist/edge/http/diagnostics.js +31 -0
- package/dist/edge/http/draft-files.js +69 -0
- package/dist/edge/http/draft-problem.js +15 -0
- package/dist/edge/http/drafts.js +95 -0
- package/dist/edge/http/multipart.js +86 -0
- package/dist/edge/http/mutations.js +49 -0
- package/dist/edge/http/planning.js +12 -1
- package/dist/edge/http/project-templates.js +74 -0
- package/dist/edge/http/projects.js +19 -2
- package/dist/edge/http/revision-files.js +84 -0
- package/dist/edge/http/revisions.js +103 -0
- package/dist/edge/http/schedules.js +100 -0
- package/dist/edge/http/staging.js +11 -52
- package/dist/edge/http/storage.js +110 -0
- package/dist/edge/http/subtitles.js +9 -79
- package/dist/edge/http/tutorial.js +26 -0
- package/dist/edge/signal-shutdown.js +23 -0
- package/dist/kernel/audio-preview.js +11 -5
- package/dist/kernel/db/migrations/0004-project-revisions.sql +91 -0
- package/dist/kernel/db/migrations/0005-revision-work.sql +103 -0
- package/dist/kernel/db/migrations/0006-play-drafts.sql +38 -0
- package/dist/kernel/db/migrations/0007-review-checkpoints.sql +28 -0
- package/dist/kernel/db/migrations/0008-project-templates.sql +21 -0
- package/dist/kernel/db/migrations/0009-scheduled-jobs.sql +37 -0
- package/dist/kernel/db/migrations/0010-retain-schedule-history.sql +62 -0
- package/dist/kernel/ports/model.js +5 -0
- package/dist/kernel/runner/attempt-repo.js +15 -0
- package/dist/kernel/runner/attempt.js +14 -2
- package/dist/kernel/runner/checkpoint-authority.js +23 -0
- package/dist/kernel/runner/index.js +35 -11
- package/dist/kernel/runner/providers.js +18 -8
- package/dist/kernel/runner/work-attempt.js +47 -0
- package/dist/kernel/runner/work-authority.js +37 -0
- package/dist/kernel/runner/work.js +23 -0
- package/dist/main.js +227 -69
- package/dist/slices/admission/repo.js +5 -56
- package/dist/slices/admission/schema.js +64 -0
- package/dist/slices/admission/start.js +13 -4
- package/dist/slices/article/continuation.js +9 -3
- package/dist/slices/article/provided-entries.js +7 -3
- package/dist/slices/article/run.js +20 -7
- package/dist/slices/article/segments.js +13 -4
- package/dist/slices/batch/index.js +8 -11
- package/dist/slices/cancel/index.js +78 -34
- package/dist/slices/checkpoints/change.js +250 -0
- package/dist/slices/checkpoints/fingerprint.js +33 -0
- package/dist/slices/checkpoints/index.js +2 -0
- package/dist/slices/checkpoints/model.js +1 -0
- package/dist/slices/checkpoints/recovery.js +116 -0
- package/dist/slices/checkpoints/repo.js +132 -0
- package/dist/slices/checkpoints/rules.js +30 -0
- package/dist/slices/checkpoints/schema.js +59 -0
- package/dist/slices/control/index.js +88 -22
- package/dist/slices/control/providers.js +3 -2
- package/dist/slices/control/revision-control-schema.js +7 -0
- package/dist/slices/control/revision-control.js +59 -0
- package/dist/slices/estimate/index.js +77 -59
- package/dist/slices/estimate/requests.js +102 -0
- package/dist/slices/images/run.js +14 -3
- package/dist/slices/library/slots.js +11 -11
- package/dist/slices/library/snapshot.js +33 -0
- package/dist/slices/narration/legacy-plan.js +58 -0
- package/dist/slices/narration/live.js +2 -2
- package/dist/slices/narration/plan.js +48 -54
- package/dist/slices/narration/run.js +21 -8
- package/dist/slices/play-drafts/convert.js +120 -0
- package/dist/slices/play-drafts/model.js +1 -0
- package/dist/slices/play-drafts/readiness.js +80 -0
- package/dist/slices/play-drafts/repo.js +76 -0
- package/dist/slices/play-drafts/review-inputs.js +150 -0
- package/dist/slices/play-drafts/review.js +109 -0
- package/dist/slices/play-drafts/schema.js +218 -0
- package/dist/slices/play-drafts/service.js +242 -0
- package/dist/slices/play-drafts/start-repo.js +113 -0
- package/dist/slices/play-drafts/start.js +148 -0
- package/dist/slices/play-drafts/uploads.js +67 -0
- package/dist/slices/project-templates/from-project.js +159 -0
- package/dist/slices/project-templates/model.js +1 -0
- package/dist/slices/project-templates/repo.js +41 -0
- package/dist/slices/project-templates/schema.js +37 -0
- package/dist/slices/project-templates/service.js +136 -0
- package/dist/slices/project-templates/setup.js +55 -0
- package/dist/slices/rebuild/admission-repo.js +120 -0
- package/dist/slices/rebuild/dependencies.js +33 -0
- package/dist/slices/rebuild/model.js +94 -0
- package/dist/slices/rebuild/narration-history.js +43 -0
- package/dist/slices/rebuild/narration-reuse.js +20 -0
- package/dist/slices/rebuild/preview-details.js +144 -0
- package/dist/slices/rebuild/preview-plan.js +223 -0
- package/dist/slices/rebuild/preview-retained.js +128 -0
- package/dist/slices/rebuild/provided-review.js +32 -0
- package/dist/slices/rebuild/recipe-audio.js +172 -0
- package/dist/slices/rebuild/recipe-build.js +17 -0
- package/dist/slices/rebuild/recipe-checkpoints.js +75 -0
- package/dist/slices/rebuild/recipe-exports.js +65 -0
- package/dist/slices/rebuild/recipe-fixture.js +150 -0
- package/dist/slices/rebuild/recipe-input-schema.js +82 -0
- package/dist/slices/rebuild/recipe-legacy.js +135 -0
- package/dist/slices/rebuild/recipe-model.js +77 -0
- package/dist/slices/rebuild/recipe-provider-choice.js +14 -0
- package/dist/slices/rebuild/recipe-save.js +221 -0
- package/dist/slices/rebuild/recipe-text.js +170 -0
- package/dist/slices/rebuild/recipe-validation.js +86 -0
- package/dist/slices/rebuild/recipe-visual.js +111 -0
- package/dist/slices/rebuild/recipe-work.js +212 -0
- package/dist/slices/rebuild/recipes.js +4 -0
- package/dist/slices/rebuild/repo.js +64 -0
- package/dist/slices/rebuild/runtime-actions.js +76 -0
- package/dist/slices/rebuild/runtime-admission.js +71 -0
- package/dist/slices/rebuild/runtime-article.js +117 -0
- package/dist/slices/rebuild/runtime-checkpoints.js +45 -0
- package/dist/slices/rebuild/runtime-export-inputs.js +109 -0
- package/dist/slices/rebuild/runtime-export.js +139 -0
- package/dist/slices/rebuild/runtime-instructions.js +51 -0
- package/dist/slices/rebuild/runtime-local.js +137 -0
- package/dist/slices/rebuild/runtime-materialize.js +105 -0
- package/dist/slices/rebuild/runtime-narration-reuse.js +134 -0
- package/dist/slices/rebuild/runtime-plan.js +103 -0
- package/dist/slices/rebuild/runtime-provider.js +184 -0
- package/dist/slices/rebuild/runtime-publication.js +172 -0
- package/dist/slices/rebuild/runtime-run.js +35 -0
- package/dist/slices/rebuild/runtime-store.js +166 -0
- package/dist/slices/rebuild/runtime-subtitles.js +202 -0
- package/dist/slices/rebuild/service-readiness.js +110 -0
- package/dist/slices/rebuild/service.js +149 -0
- package/dist/slices/rebuild/transition-repo.js +108 -0
- package/dist/slices/rebuild/work-records.js +44 -0
- package/dist/slices/reruns/index.js +5 -0
- package/dist/slices/research/run.js +33 -12
- package/dist/slices/revisions/adopt-assets.js +31 -0
- package/dist/slices/revisions/adopt-content.js +112 -0
- package/dist/slices/revisions/adopt.js +100 -0
- package/dist/slices/revisions/downloads.js +80 -0
- package/dist/slices/revisions/index.js +5 -0
- package/dist/slices/revisions/manifest-repo.js +125 -0
- package/dist/slices/revisions/model.js +1 -0
- package/dist/slices/revisions/mutation-assets.js +143 -0
- package/dist/slices/revisions/mutation-cues.js +95 -0
- package/dist/slices/revisions/mutation-prepare.js +182 -0
- package/dist/slices/revisions/mutation-request.js +58 -0
- package/dist/slices/revisions/mutation-work.js +47 -0
- package/dist/slices/revisions/mutations.js +194 -0
- package/dist/slices/revisions/projection.js +82 -0
- package/dist/slices/revisions/publication-model.js +1 -0
- package/dist/slices/revisions/publication-rules.js +166 -0
- package/dist/slices/revisions/publish.js +115 -0
- package/dist/slices/revisions/repo.js +83 -0
- package/dist/slices/revisions/restore.js +60 -0
- package/dist/slices/revisions/rules.js +99 -0
- package/dist/slices/revisions/schema.js +160 -0
- package/dist/slices/revisions/view.js +44 -0
- package/dist/slices/schedules/calendar.js +47 -0
- package/dist/slices/schedules/model.js +1 -0
- package/dist/slices/schedules/repo.js +281 -0
- package/dist/slices/schedules/scheduler.js +144 -0
- package/dist/slices/schedules/schema.js +80 -0
- package/dist/slices/schedules/service.js +178 -0
- package/dist/slices/settings/cli-paths.js +1 -1
- package/dist/slices/settings/cli-status.js +45 -5
- package/dist/slices/settings/tutorial-schema.js +39 -0
- package/dist/slices/settings/tutorial.js +66 -0
- package/dist/slices/storage/assets.js +108 -0
- package/dist/slices/storage/delete-project.js +2 -1
- package/dist/slices/storage/downloads.js +4 -1
- package/dist/slices/storage/portable.js +871 -0
- package/dist/slices/storage/prepare.js +67 -0
- package/dist/slices/storage/reconcile.js +25 -5
- package/dist/slices/storage/repo.js +2 -15
- package/dist/slices/storage/schema.js +35 -0
- package/dist/slices/storage/staging-refs.js +26 -0
- package/dist/slices/storage/staging.js +16 -11
- package/dist/slices/thumbnail/run.js +26 -9
- package/dist/slices/video/audio-export-args.js +33 -0
- package/dist/slices/video/audio-export.js +13 -36
- package/dist/slices/video/audio-inputs.js +4 -3
- package/dist/slices/video/ffmpeg.js +6 -0
- package/dist/slices/video/reuse-audio.js +11 -4
- package/dist/updater/install-flow.js +27 -2
- package/dist/updater/plan.js +28 -2
- package/dist/updater/service.js +27 -2
- package/dist/updater/worker.js +2 -1
- package/dist/web/assets/index-DX1_chKs.css +1 -0
- package/dist/web/assets/index-DwM-Y6nw.js +137 -0
- package/dist/web/index.html +2 -2
- package/package.json +2 -2
- package/dist/adapters/tts/inworld-text.js +0 -32
- package/dist/web/assets/index-Ce2HApus.css +0 -1
- package/dist/web/assets/index-xbD0MZOY.js +0 -130
package/README.md
CHANGED
|
@@ -13,17 +13,39 @@
|
|
|
13
13
|
|
|
14
14
|
A prompt and a few keywords in. A narrated slideshow video out. Your keys, your machine, free.
|
|
15
15
|
|
|
16
|
+
*Start contributing to the internet's enshittification today!*
|
|
17
|
+
|
|
16
18
|
```sh
|
|
17
19
|
npx @gentbajko/slopify@latest
|
|
18
20
|
```
|
|
19
21
|
|
|
20
|
-
That opens a browser at `http://127.0.0.1:6969`.
|
|
21
|
-
|
|
22
|
-
|
|
22
|
+
That opens a browser at `http://127.0.0.1:6969`. Node 26 or newer is required.
|
|
23
|
+
|
|
24
|
+
For a persistent install that makes `slopify` available everywhere:
|
|
25
|
+
|
|
26
|
+
```sh
|
|
27
|
+
npm install -g @gentbajko/slopify
|
|
28
|
+
slopify
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
Both commands launch the same local app.
|
|
32
|
+
|
|
33
|
+
## How to use it
|
|
34
|
+
|
|
35
|
+
1. Add provider keys and voices in Settings. They stay in your local Slopify database.
|
|
36
|
+
2. Write reusable article, image and thumbnail prompts in Prompts, using `{{keywords}}`
|
|
37
|
+
where each video's subject belongs.
|
|
38
|
+
3. In Play, build an autosaved setup across Content, Outputs, Style and Review. Supply
|
|
39
|
+
or skip optional stages, preview subtitles, and inspect the estimated cost before Start.
|
|
40
|
+
4. Follow total and per-stage progress in the project workspace. Pause unfinished work
|
|
41
|
+
to change providers, add review checkpoints, or edit a paused or completed project as
|
|
42
|
+
a retained revision. Saving does not rebuild until you explicitly ask.
|
|
43
|
+
5. Save Play setups or project revisions as templates. Apply one to a fresh draft or
|
|
44
|
+
schedule one-off, daily or weekly jobs with keyword variants and a spend ceiling.
|
|
23
45
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
46
|
+
The article is the only required output. Slopify can make a narrated or silent slideshow,
|
|
47
|
+
an audio-only WAV, locally timed English subtitles, or just the intermediate results you
|
|
48
|
+
select. Uploaded and already completed outputs are reused when their inputs still match.
|
|
27
49
|
|
|
28
50
|
## Inworld narration
|
|
29
51
|
|
|
@@ -1,7 +1,7 @@
|
|
|
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 { cliEvent, cliShaped, endedWithout, promptOf } from "./run-cli.js";
|
|
4
|
+
import { cliEvent, cliShaped, endedWithout, promptOf, stopCliRun } from "./run-cli.js";
|
|
5
5
|
import { lines } from "./sse-lines.js";
|
|
6
6
|
// The local-agent adapter for Claude Code: spawned non-interactively, authenticated by the
|
|
7
7
|
// CLI's own login, no key anywhere in this file. Readiness is not computed here - `adapters/**`
|
|
@@ -76,6 +76,7 @@ export function claudeCodeLlm(deps) {
|
|
|
76
76
|
const binary = deps.binary ?? claudeCodeBinary;
|
|
77
77
|
async function* complete(req) {
|
|
78
78
|
const run = deps.run(binary, claudeCodeArgs(req), req.signal);
|
|
79
|
+
let ended;
|
|
79
80
|
try {
|
|
80
81
|
for await (const line of lines(run.stdout, req.signal)) {
|
|
81
82
|
if (line.trim() === "") {
|
|
@@ -123,7 +124,7 @@ export function claudeCodeLlm(deps) {
|
|
|
123
124
|
finally {
|
|
124
125
|
// The consumer can also abandon the generator - a retry, a timeout - and an agent
|
|
125
126
|
// session left running would keep spending the user's subscription.
|
|
126
|
-
run
|
|
127
|
+
ended = await stopCliRun(run);
|
|
127
128
|
}
|
|
128
129
|
// A cancelled run ends its stream the same way an exhausted one does: the child was
|
|
129
130
|
// killed, so stdout simply stopped. An aborted call counts as nothing, so it must not
|
|
@@ -132,7 +133,9 @@ export function claudeCodeLlm(deps) {
|
|
|
132
133
|
// The stream ended with no result event at all.
|
|
133
134
|
throw providerError({
|
|
134
135
|
kind: "other",
|
|
135
|
-
message:
|
|
136
|
+
message: ended === undefined
|
|
137
|
+
? `the ${binary} CLI did not stop after forced termination`
|
|
138
|
+
: endedWithout(binary, ended, run.stderr()),
|
|
136
139
|
});
|
|
137
140
|
}
|
|
138
141
|
return {
|
|
@@ -1,7 +1,10 @@
|
|
|
1
|
+
import { mkdtempSync, rmSync } from "node:fs";
|
|
2
|
+
import { tmpdir } from "node:os";
|
|
3
|
+
import { join } from "node:path";
|
|
1
4
|
import { z } from "zod";
|
|
2
5
|
import { redact } from "../../kernel/log.js";
|
|
3
6
|
import { providerError } from "../../kernel/ports/model.js";
|
|
4
|
-
import { cliEvent, cliShaped, endedWithout, promptOf } from "./run-cli.js";
|
|
7
|
+
import { cliEvent, cliShaped, endedWithout, promptOf, stopCliRun } from "./run-cli.js";
|
|
5
8
|
import { lines } from "./sse-lines.js";
|
|
6
9
|
// The local-agent adapter for the Codex CLI. Same shape as Claude Code's and a different
|
|
7
10
|
// vocabulary: Codex writes a JSONL thread of `thread.started`, `item.*` and `turn.*`
|
|
@@ -10,19 +13,73 @@ export const codexBinary = "codex";
|
|
|
10
13
|
// Codex publishes its account-specific picker in models_cache.json. With no
|
|
11
14
|
// cache, offer custom entry rather than pretending a pinned model is current.
|
|
12
15
|
export const codexModels = [];
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
//
|
|
20
|
-
|
|
16
|
+
const writingRole = "You are the writing and research component of Slopify, a video creation app. " +
|
|
17
|
+
"Produce the text requested in the supplied conversation: articles, narration scripts, " +
|
|
18
|
+
"research notes or image prompts. Follow the requested topic, language, tone, length and format. " +
|
|
19
|
+
"Return only the requested content, without progress updates or introductory remarks. " +
|
|
20
|
+
"Use web search when it is available and needed for the requested research. " +
|
|
21
|
+
"Do not inspect or modify local files.";
|
|
22
|
+
// Codex is a coding agent by default, while this adapter is a content provider. These supported
|
|
23
|
+
// feature gates remove every local or account-connected tool exposed by 0.149.1. Native web
|
|
24
|
+
// search is controlled separately below, so grounded research still works without a shell.
|
|
25
|
+
const disabledFeatures = [
|
|
26
|
+
"shell_tool",
|
|
27
|
+
"unified_exec",
|
|
28
|
+
"code_mode_host",
|
|
29
|
+
"hooks",
|
|
30
|
+
"apps",
|
|
31
|
+
"plugins",
|
|
32
|
+
"remote_plugin",
|
|
33
|
+
"skill_search",
|
|
34
|
+
"multi_agent",
|
|
35
|
+
"computer_use",
|
|
36
|
+
"browser_use",
|
|
37
|
+
"image_generation",
|
|
38
|
+
"view_image",
|
|
39
|
+
"workspace_dependencies",
|
|
40
|
+
];
|
|
41
|
+
// `codex exec --help` (0.149.1) for the flags. `--ignore-user-config` retains authentication
|
|
42
|
+
// while excluding config.toml, `--ignore-rules` excludes user/project exec policy, and strict
|
|
43
|
+
// config makes an older CLI fail closed if it cannot apply a hardening override. A private cwd
|
|
44
|
+
// plus a zero project-doc budget excludes AGENTS.md and project `.codex` context. Read-only is
|
|
45
|
+
// defense in depth if a future release exposes a new filesystem tool. The TOML strings keep their
|
|
46
|
+
// quotes because each `-c` value is parsed as TOML rather than as a shell expression.
|
|
47
|
+
export function codexArgs(req, directory) {
|
|
21
48
|
return [
|
|
22
49
|
"exec",
|
|
23
50
|
"--json",
|
|
24
51
|
"--ephemeral",
|
|
52
|
+
"--ignore-user-config",
|
|
53
|
+
"--ignore-rules",
|
|
54
|
+
"--strict-config",
|
|
25
55
|
"--skip-git-repo-check",
|
|
56
|
+
"--sandbox",
|
|
57
|
+
"read-only",
|
|
58
|
+
"--cd",
|
|
59
|
+
directory,
|
|
60
|
+
...disabledFeatures.flatMap((feature) => ["--disable", feature]),
|
|
61
|
+
"-c",
|
|
62
|
+
"project_doc_max_bytes=0",
|
|
63
|
+
"-c",
|
|
64
|
+
"skills.include_instructions=false",
|
|
65
|
+
"-c",
|
|
66
|
+
"skills.bundled.enabled=false",
|
|
67
|
+
"-c",
|
|
68
|
+
"orchestrator.skills.enabled=false",
|
|
69
|
+
"-c",
|
|
70
|
+
"orchestrator.mcp.enabled=false",
|
|
71
|
+
"-c",
|
|
72
|
+
"include_permissions_instructions=false",
|
|
73
|
+
"-c",
|
|
74
|
+
"include_apps_instructions=false",
|
|
75
|
+
"-c",
|
|
76
|
+
"include_collaboration_mode_instructions=false",
|
|
77
|
+
"-c",
|
|
78
|
+
"include_environment_context=false",
|
|
79
|
+
"-c",
|
|
80
|
+
'shell_environment_policy.inherit="none"',
|
|
81
|
+
"-c",
|
|
82
|
+
`instructions=${JSON.stringify(writingRole)}`,
|
|
26
83
|
"-c",
|
|
27
84
|
`web_search="${req.webSearch === true ? "live" : "disabled"}"`,
|
|
28
85
|
...(req.thinkingConfig?.effort
|
|
@@ -49,8 +106,12 @@ const errorEvent = z.object({ message: z.string() });
|
|
|
49
106
|
export function codexLlm(deps) {
|
|
50
107
|
const binary = deps.binary ?? codexBinary;
|
|
51
108
|
async function* complete(req) {
|
|
52
|
-
|
|
109
|
+
req.signal.throwIfAborted();
|
|
110
|
+
const workspace = codexWorkspace();
|
|
111
|
+
let run;
|
|
112
|
+
let ended;
|
|
53
113
|
try {
|
|
114
|
+
run = deps.run(binary, codexArgs(req, workspace.directory), req.signal, workspace.options);
|
|
54
115
|
for await (const line of lines(run.stdout, req.signal)) {
|
|
55
116
|
if (line.trim() === "") {
|
|
56
117
|
continue;
|
|
@@ -96,7 +157,13 @@ export function codexLlm(deps) {
|
|
|
96
157
|
throw error;
|
|
97
158
|
}
|
|
98
159
|
finally {
|
|
99
|
-
|
|
160
|
+
try {
|
|
161
|
+
if (run !== undefined)
|
|
162
|
+
ended = await stopCliRun(run);
|
|
163
|
+
}
|
|
164
|
+
finally {
|
|
165
|
+
workspace.remove();
|
|
166
|
+
}
|
|
100
167
|
}
|
|
101
168
|
// A cancelled run ends its stream the same way an exhausted one does: the child was
|
|
102
169
|
// killed, so stdout simply stopped. An aborted call counts as nothing, so it must not
|
|
@@ -105,7 +172,9 @@ export function codexLlm(deps) {
|
|
|
105
172
|
// The stream ended with neither a completed turn nor a failure.
|
|
106
173
|
throw providerError({
|
|
107
174
|
kind: "other",
|
|
108
|
-
message:
|
|
175
|
+
message: ended === undefined
|
|
176
|
+
? `the ${binary} CLI did not stop after forced termination`
|
|
177
|
+
: endedWithout(binary, ended, run.stderr()),
|
|
109
178
|
});
|
|
110
179
|
}
|
|
111
180
|
return {
|
|
@@ -116,6 +185,27 @@ export function codexLlm(deps) {
|
|
|
116
185
|
complete,
|
|
117
186
|
};
|
|
118
187
|
}
|
|
188
|
+
function codexWorkspace() {
|
|
189
|
+
const directory = mkdtempSync(join(tmpdir(), "slopify-codex-"));
|
|
190
|
+
// npm sets INIT_CWD to the directory from which the app was launched. Do not hand that path,
|
|
191
|
+
// an old shell directory or Git's explicit worktree pointers to the content subprocess.
|
|
192
|
+
const env = { ...process.env };
|
|
193
|
+
delete env.INIT_CWD;
|
|
194
|
+
delete env.OLDPWD;
|
|
195
|
+
delete env.GIT_DIR;
|
|
196
|
+
delete env.GIT_WORK_TREE;
|
|
197
|
+
env.PWD = directory;
|
|
198
|
+
return {
|
|
199
|
+
directory,
|
|
200
|
+
options: { cwd: directory, env },
|
|
201
|
+
remove: () => rmSync(directory, {
|
|
202
|
+
recursive: true,
|
|
203
|
+
force: true,
|
|
204
|
+
maxRetries: 3,
|
|
205
|
+
retryDelay: 50,
|
|
206
|
+
}),
|
|
207
|
+
};
|
|
208
|
+
}
|
|
119
209
|
function usageOf(usage) {
|
|
120
210
|
return usage === null || usage === undefined
|
|
121
211
|
? null
|
|
@@ -3,7 +3,7 @@ import { redact } from "../../kernel/log.js";
|
|
|
3
3
|
import { providerError } from "../../kernel/ports/model.js";
|
|
4
4
|
import { geminiModels } from "./gemini-models.js";
|
|
5
5
|
import { geminiWorkspace } from "./gemini-workspace.js";
|
|
6
|
-
import { cliEvent, cliShaped, endedWithout, promptOf } from "./run-cli.js";
|
|
6
|
+
import { cliEvent, cliShaped, endedWithout, promptOf, stopCliRun } from "./run-cli.js";
|
|
7
7
|
import { lines } from "./sse-lines.js";
|
|
8
8
|
export const geminiBinary = "gemini";
|
|
9
9
|
export { geminiModels } from "./gemini-models.js";
|
|
@@ -45,6 +45,7 @@ export function geminiLlm(deps) {
|
|
|
45
45
|
req.signal.throwIfAborted();
|
|
46
46
|
const workspace = geminiWorkspace(req.webSearch === true, req);
|
|
47
47
|
let run;
|
|
48
|
+
let ended;
|
|
48
49
|
try {
|
|
49
50
|
run = deps.run(binary, geminiArgs(req, workspace.mcpAllowlist), req.signal, workspace.options);
|
|
50
51
|
for await (const line of lines(authChecked(run.stdout), req.signal)) {
|
|
@@ -80,22 +81,24 @@ export function geminiLlm(deps) {
|
|
|
80
81
|
return;
|
|
81
82
|
}
|
|
82
83
|
}
|
|
83
|
-
req.signal.throwIfAborted();
|
|
84
|
-
const ended = await run.ended;
|
|
85
|
-
if (authRequired(run.stderr()))
|
|
86
|
-
throw authFailure();
|
|
87
|
-
throw failure(endedWithout(binary, ended, run.stderr()));
|
|
88
84
|
}
|
|
89
85
|
catch (error) {
|
|
90
86
|
req.signal.throwIfAborted();
|
|
91
87
|
throw error;
|
|
92
88
|
}
|
|
93
89
|
finally {
|
|
94
|
-
run?.kill();
|
|
95
90
|
if (run !== undefined)
|
|
96
|
-
await run
|
|
91
|
+
ended = await stopCliRun(run);
|
|
97
92
|
workspace.remove();
|
|
98
93
|
}
|
|
94
|
+
req.signal.throwIfAborted();
|
|
95
|
+
if (run === undefined)
|
|
96
|
+
throw failure(`the ${binary} CLI could not be started`);
|
|
97
|
+
if (authRequired(run.stderr()))
|
|
98
|
+
throw authFailure();
|
|
99
|
+
throw failure(ended === undefined
|
|
100
|
+
? `the ${binary} CLI did not stop after forced termination`
|
|
101
|
+
: endedWithout(binary, ended, run.stderr()));
|
|
99
102
|
}
|
|
100
103
|
return {
|
|
101
104
|
id: "gemini",
|
|
@@ -8,6 +8,7 @@ import { providerError } from "../../kernel/ports/model.js";
|
|
|
8
8
|
// says why it stopped. Streaming stderr to the log is the upgrade if a provider ever
|
|
9
9
|
// buries its reason in the first line of a long report.
|
|
10
10
|
export const stderrMax = 8192;
|
|
11
|
+
export const cliTerminationGraceMs = 1_000;
|
|
11
12
|
export function nodeRunCli(binary, args, signal, options) {
|
|
12
13
|
// An argument array, never a shell string: a prompt carrying backticks, `$(...)`, quotes or
|
|
13
14
|
// newlines is one argv element and nothing in it can become a command. stdin is /dev/null
|
|
@@ -16,9 +17,10 @@ export function nodeRunCli(binary, args, signal, options) {
|
|
|
16
17
|
// SIGTERM when the stage is cancelled, so no agent session outlives the run that started it.
|
|
17
18
|
const command = cliCommand(binary);
|
|
18
19
|
const child = spawn(command.file, [...command.args, ...args], {
|
|
19
|
-
signal,
|
|
20
20
|
stdio: ["ignore", "pipe", "pipe"],
|
|
21
21
|
windowsHide: true,
|
|
22
|
+
// A private POSIX process group lets force cleanup include helpers spawned by a CLI.
|
|
23
|
+
detached: process.platform !== "win32",
|
|
22
24
|
...options,
|
|
23
25
|
});
|
|
24
26
|
let stderr = "";
|
|
@@ -36,25 +38,78 @@ export function nodeRunCli(binary, args, signal, options) {
|
|
|
36
38
|
child.on("error", (error) => {
|
|
37
39
|
spawnError = error;
|
|
38
40
|
});
|
|
41
|
+
let settled = false;
|
|
39
42
|
const ended = new Promise((resolve) => {
|
|
40
43
|
const settle = (code) => {
|
|
44
|
+
settled = true;
|
|
41
45
|
resolve({ code, error: spawnError });
|
|
42
46
|
};
|
|
43
47
|
child.once("close", settle);
|
|
44
48
|
// Close follows an error too; waiting for it keeps cleanup behind process exit.
|
|
45
49
|
});
|
|
50
|
+
const terminate = (force = false) => {
|
|
51
|
+
if (settled)
|
|
52
|
+
return;
|
|
53
|
+
const pid = child.pid;
|
|
54
|
+
if (process.platform === "win32") {
|
|
55
|
+
if (force && pid !== undefined) {
|
|
56
|
+
const killer = spawn("taskkill", ["/pid", String(pid), "/t", "/f"], {
|
|
57
|
+
stdio: "ignore",
|
|
58
|
+
windowsHide: true,
|
|
59
|
+
});
|
|
60
|
+
killer.on("error", () => { });
|
|
61
|
+
killer.unref();
|
|
62
|
+
}
|
|
63
|
+
child.kill(force ? "SIGKILL" : "SIGTERM");
|
|
64
|
+
}
|
|
65
|
+
else if (pid !== undefined) {
|
|
66
|
+
try {
|
|
67
|
+
process.kill(-pid, force ? "SIGKILL" : "SIGTERM");
|
|
68
|
+
}
|
|
69
|
+
catch {
|
|
70
|
+
child.kill(force ? "SIGKILL" : "SIGTERM");
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
if (force) {
|
|
74
|
+
child.stdout?.destroy();
|
|
75
|
+
child.stderr?.destroy();
|
|
76
|
+
}
|
|
77
|
+
};
|
|
78
|
+
const abort = () => terminate();
|
|
79
|
+
if (signal.aborted)
|
|
80
|
+
abort();
|
|
81
|
+
else
|
|
82
|
+
signal.addEventListener("abort", abort, { once: true });
|
|
83
|
+
void ended.then(() => signal.removeEventListener("abort", abort));
|
|
46
84
|
return {
|
|
47
85
|
pid: child.pid,
|
|
48
86
|
stdout: child.stdout === null ? nothing : bytesOf(child.stdout),
|
|
49
87
|
stderr: () => stderr,
|
|
50
88
|
ended,
|
|
51
|
-
kill:
|
|
52
|
-
if (child.exitCode === null && child.signalCode === null) {
|
|
53
|
-
child.kill();
|
|
54
|
-
}
|
|
55
|
-
},
|
|
89
|
+
kill: terminate,
|
|
56
90
|
};
|
|
57
91
|
}
|
|
92
|
+
/** Stop a CLI without letting a resistant process wedge pause, cancel, or app shutdown. */
|
|
93
|
+
export async function stopCliRun(run, graceMs = cliTerminationGraceMs, forceMs = cliTerminationGraceMs) {
|
|
94
|
+
run.kill();
|
|
95
|
+
const graceful = await endedWithin(run.ended, graceMs);
|
|
96
|
+
if (graceful !== undefined)
|
|
97
|
+
return graceful;
|
|
98
|
+
run.kill(true);
|
|
99
|
+
return endedWithin(run.ended, forceMs);
|
|
100
|
+
}
|
|
101
|
+
async function endedWithin(ended, timeoutMs) {
|
|
102
|
+
let timer;
|
|
103
|
+
const result = await Promise.race([
|
|
104
|
+
ended,
|
|
105
|
+
new Promise((resolve) => {
|
|
106
|
+
timer = setTimeout(() => resolve(undefined), timeoutMs);
|
|
107
|
+
}),
|
|
108
|
+
]);
|
|
109
|
+
if (timer !== undefined)
|
|
110
|
+
clearTimeout(timer);
|
|
111
|
+
return result;
|
|
112
|
+
}
|
|
58
113
|
// ceiling: both CLIs take one prompt string, so a system or assistant turn is flattened
|
|
59
114
|
// into it under a header. Claude Code's `--append-system-prompt` is the upgrade if a stage
|
|
60
115
|
// ever needs a real system turn; today research and the article each compose one message.
|
|
@@ -13,16 +13,16 @@ const operationSchema = z.object({
|
|
|
13
13
|
// A continuation survives automatic retries of this call; pausing/restarting a
|
|
14
14
|
// stage stops local polling but cannot cancel an already accepted remote job.
|
|
15
15
|
export async function* inworldAsync(deps, request, key) {
|
|
16
|
-
|
|
16
|
+
const saved = request.continuation?.read();
|
|
17
|
+
if (saved !== undefined && !operationName.test(saved))
|
|
18
|
+
throw invalid("returned an invalid operation name");
|
|
19
|
+
if (saved === undefined && request.text.length > 100_000) {
|
|
17
20
|
throw providerError({
|
|
18
21
|
kind: "unsupported",
|
|
19
22
|
message: "Inworld async accepts up to 100,000 characters per request (10,000 for On-Demand accounts). Select paragraph chunking for longer articles.",
|
|
20
23
|
});
|
|
21
24
|
}
|
|
22
25
|
const headers = { Authorization: `Basic ${key}`, "Content-Type": "application/json" };
|
|
23
|
-
const saved = request.continuation?.read();
|
|
24
|
-
if (saved && !operationName.test(saved))
|
|
25
|
-
throw invalid("returned an invalid operation name");
|
|
26
26
|
request.signal.throwIfAborted();
|
|
27
27
|
let response = await deps.fetch(saved
|
|
28
28
|
? `https://api.inworld.ai/lro/v1alpha/${saved}`
|
|
@@ -3,7 +3,6 @@ import { redact } from "../../kernel/log.js";
|
|
|
3
3
|
import { providerError } from "../../kernel/ports/model.js";
|
|
4
4
|
import { retryAfter } from "../retry-after.js";
|
|
5
5
|
import { inworldAsync } from "./inworld-async.js";
|
|
6
|
-
import { inworldTextParts } from "./inworld-text.js";
|
|
7
6
|
export const inworldModels = [
|
|
8
7
|
{ id: "inworld-tts-2", name: "Realtime TTS-2" },
|
|
9
8
|
{ id: "inworld-tts-2-flash", name: "Realtime TTS-2 Flash" },
|
|
@@ -26,7 +25,7 @@ export function inworldTts(deps) {
|
|
|
26
25
|
throw providerError({ kind: "missing_key", message: "No Inworld API key is stored." });
|
|
27
26
|
const cancel = new AbortController();
|
|
28
27
|
const signal = AbortSignal.any([request.signal, cancel.signal]);
|
|
29
|
-
const iterator =
|
|
28
|
+
const iterator = synthesizeRequest(deps, { ...request, signal }, key);
|
|
30
29
|
return {
|
|
31
30
|
container: "mp3",
|
|
32
31
|
audio: new ReadableStream({
|
|
@@ -51,85 +50,91 @@ export function inworldTts(deps) {
|
|
|
51
50
|
},
|
|
52
51
|
};
|
|
53
52
|
}
|
|
54
|
-
async function*
|
|
55
|
-
if (
|
|
53
|
+
async function* synthesizeRequest(deps, request, key) {
|
|
54
|
+
if (request.continuation?.read() !== undefined) {
|
|
56
55
|
yield* inworldAsync(deps, request, key);
|
|
57
56
|
return;
|
|
58
57
|
}
|
|
59
|
-
|
|
60
|
-
if (parts.length === 0)
|
|
58
|
+
if (request.text.trim().length === 0)
|
|
61
59
|
throw providerError({ kind: "unsupported", message: "Inworld needs text to narrate." });
|
|
62
|
-
|
|
63
|
-
request
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
text,
|
|
71
|
-
voiceId: request.voiceId,
|
|
72
|
-
modelId: request.model ?? "inworld-tts-2",
|
|
73
|
-
audioConfig: { audioEncoding: "MP3", sampleRateHertz: 48_000, bitRate: 128_000 },
|
|
74
|
-
}),
|
|
60
|
+
if ((request.model ?? "inworld-tts-2") === "inworld-tts-2" && request.text.length > 4000) {
|
|
61
|
+
yield* inworldAsync(deps, request, key);
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
if (request.text.length > 4000)
|
|
65
|
+
throw providerError({
|
|
66
|
+
kind: "unsupported",
|
|
67
|
+
message: "Inworld streaming accepts up to 4,000 characters per physical request. Rebuild narration with a supported request limit.",
|
|
75
68
|
});
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
69
|
+
request.signal.throwIfAborted();
|
|
70
|
+
const response = await deps.fetch("https://api.inworld.ai/tts/v1/voice:stream", {
|
|
71
|
+
method: "POST",
|
|
72
|
+
redirect: "error",
|
|
73
|
+
signal: request.signal,
|
|
74
|
+
headers: { Authorization: `Basic ${key}`, "Content-Type": "application/json" },
|
|
75
|
+
body: JSON.stringify({
|
|
76
|
+
text: request.text,
|
|
77
|
+
voiceId: request.voiceId,
|
|
78
|
+
modelId: request.model ?? "inworld-tts-2",
|
|
79
|
+
audioConfig: { audioEncoding: "MP3", sampleRateHertz: 48_000, bitRate: 128_000 },
|
|
80
|
+
}),
|
|
81
|
+
});
|
|
82
|
+
if (!response.ok) {
|
|
83
|
+
const detail = clean(await response.text().catch(() => ""), key);
|
|
84
|
+
const retryAfterMs = retryAfter(response.headers.get("retry-after"));
|
|
85
|
+
throw providerError({
|
|
86
|
+
kind: response.status === 401 || response.status === 403
|
|
87
|
+
? "auth"
|
|
88
|
+
: response.status === 429
|
|
89
|
+
? "rate_limit"
|
|
90
|
+
: response.status === 400 || response.status === 404
|
|
91
|
+
? "unsupported"
|
|
92
|
+
: "other",
|
|
93
|
+
message: clean(`Inworld answered ${response.status} for voice ${request.voiceId}: ${detail || response.statusText}`, key),
|
|
94
|
+
...(retryAfterMs === undefined ? {} : { retryAfterMs }),
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
if (response.body === null)
|
|
98
|
+
throw invalid("answered with no audio");
|
|
99
|
+
let heard = false;
|
|
100
|
+
for await (const line of lines(response.body)) {
|
|
101
|
+
request.signal.throwIfAborted();
|
|
102
|
+
let raw;
|
|
103
|
+
try {
|
|
104
|
+
raw = JSON.parse(line);
|
|
105
|
+
}
|
|
106
|
+
catch {
|
|
107
|
+
throw invalid("sent an unreadable audio chunk");
|
|
108
|
+
}
|
|
109
|
+
const parsed = envelope.safeParse(raw);
|
|
110
|
+
if (!parsed.success || (!parsed.data.result && !parsed.data.error))
|
|
111
|
+
throw invalid("sent an unreadable audio chunk");
|
|
112
|
+
if (parsed.data.error) {
|
|
113
|
+
const { code, message } = parsed.data.error;
|
|
79
114
|
throw providerError({
|
|
80
|
-
kind:
|
|
115
|
+
kind: code === 16 || code === 7
|
|
81
116
|
? "auth"
|
|
82
|
-
:
|
|
117
|
+
: code === 8
|
|
83
118
|
? "rate_limit"
|
|
84
|
-
:
|
|
119
|
+
: code === 3 || code === 5
|
|
85
120
|
? "unsupported"
|
|
86
121
|
: "other",
|
|
87
|
-
message:
|
|
88
|
-
...(retryAfterMs === undefined ? {} : { retryAfterMs }),
|
|
122
|
+
message: `Inworld stream failed: ${clean(message ?? "audio generation stopped", key)}`,
|
|
89
123
|
});
|
|
90
124
|
}
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
throw invalid("sent an unreadable audio chunk");
|
|
102
|
-
}
|
|
103
|
-
const parsed = envelope.safeParse(raw);
|
|
104
|
-
if (!parsed.success || (!parsed.data.result && !parsed.data.error))
|
|
105
|
-
throw invalid("sent an unreadable audio chunk");
|
|
106
|
-
if (parsed.data.error) {
|
|
107
|
-
const { code, message } = parsed.data.error;
|
|
108
|
-
throw providerError({
|
|
109
|
-
kind: code === 16 || code === 7
|
|
110
|
-
? "auth"
|
|
111
|
-
: code === 8
|
|
112
|
-
? "rate_limit"
|
|
113
|
-
: code === 3 || code === 5
|
|
114
|
-
? "unsupported"
|
|
115
|
-
: "other",
|
|
116
|
-
message: `Inworld stream failed: ${clean(message ?? "audio generation stopped", key)}`,
|
|
117
|
-
});
|
|
118
|
-
}
|
|
119
|
-
const encoded = parsed.data.result?.audioContent;
|
|
120
|
-
if (!encoded)
|
|
121
|
-
continue;
|
|
122
|
-
if (!/^[A-Za-z0-9+/]*={0,2}$/.test(encoded) || encoded.length % 4 === 1)
|
|
123
|
-
throw invalid("sent invalid encoded audio");
|
|
124
|
-
const audio = Buffer.from(encoded, "base64");
|
|
125
|
-
if (audio.length === 0)
|
|
126
|
-
continue;
|
|
127
|
-
heard = true;
|
|
128
|
-
yield audio;
|
|
129
|
-
}
|
|
130
|
-
if (!heard)
|
|
131
|
-
throw invalid("answered with no audio");
|
|
125
|
+
const encoded = parsed.data.result?.audioContent;
|
|
126
|
+
if (!encoded)
|
|
127
|
+
continue;
|
|
128
|
+
if (!/^[A-Za-z0-9+/]*={0,2}$/.test(encoded) || encoded.length % 4 === 1)
|
|
129
|
+
throw invalid("sent invalid encoded audio");
|
|
130
|
+
const audio = Buffer.from(encoded, "base64");
|
|
131
|
+
if (audio.length === 0)
|
|
132
|
+
continue;
|
|
133
|
+
heard = true;
|
|
134
|
+
yield audio;
|
|
132
135
|
}
|
|
136
|
+
if (!heard)
|
|
137
|
+
throw invalid("answered with no audio");
|
|
133
138
|
}
|
|
134
139
|
async function* lines(body) {
|
|
135
140
|
const reader = body.getReader();
|