@gentbajko/slopify 2.0.2 → 2.0.3
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/dist/adapters/alignment/protocol.js +11 -1
- package/dist/adapters/alignment/runner.js +4 -1
- package/dist/adapters/alignment/worker.js +28 -5
- package/dist/adapters/explain.js +93 -0
- package/dist/adapters/ffmpeg.js +3 -3
- package/dist/adapters/host-cli/index.js +9 -2
- package/dist/adapters/host-cli/transport.js +4 -4
- package/dist/adapters/image/bytes.js +2 -2
- package/dist/adapters/image/codex-output.js +12 -12
- package/dist/adapters/image/codex.js +24 -8
- package/dist/adapters/image/fal.js +12 -6
- package/dist/adapters/image/google.js +18 -8
- package/dist/adapters/image/models.js +8 -2
- package/dist/adapters/image/openai.js +15 -6
- package/dist/adapters/image/replicate.js +16 -7
- package/dist/adapters/llm/claude-code-models.js +1 -1
- package/dist/adapters/llm/claude-code.js +16 -7
- package/dist/adapters/llm/cli-login-error.js +2 -1
- package/dist/adapters/llm/codex-models.js +1 -1
- package/dist/adapters/llm/codex.js +5 -6
- package/dist/adapters/llm/document-workspace.js +2 -2
- package/dist/adapters/llm/gemini-models.js +1 -1
- package/dist/adapters/llm/gemini-workspace.js +1 -1
- package/dist/adapters/llm/gemini.js +15 -11
- package/dist/adapters/llm/openrouter.js +21 -9
- package/dist/adapters/llm/run-cli.js +14 -11
- package/dist/adapters/tts/cartesia.js +10 -3
- package/dist/adapters/tts/elevenlabs.js +18 -6
- package/dist/adapters/tts/inworld-async.js +47 -16
- package/dist/adapters/tts/inworld.js +25 -15
- package/dist/adapters/tts/openai.js +18 -6
- package/dist/catalog/registry.js +13 -10
- package/dist/catalog/store.js +1 -1
- package/dist/catalog/validate.js +13 -4
- package/dist/edge/cli.js +33 -7
- package/dist/edge/docker-launch.js +6 -6
- package/dist/edge/docker-projects/engine.js +34 -13
- package/dist/edge/docker-projects/install.js +8 -8
- package/dist/edge/docker-projects/state.js +6 -6
- package/dist/edge/docker-projects/tree.js +6 -6
- package/dist/edge/docker.js +13 -13
- package/dist/edge/http/actions.js +27 -25
- package/dist/edge/http/app.js +3 -3
- package/dist/edge/http/audio-preview.js +1 -1
- package/dist/edge/http/checkpoints.js +5 -1
- package/dist/edge/http/diagnostics.js +3 -1
- package/dist/edge/http/draft-files.js +2 -2
- package/dist/edge/http/draft-problem.js +11 -1
- package/dist/edge/http/files.js +4 -4
- package/dist/edge/http/folder-location.js +3 -3
- package/dist/edge/http/fonts.js +22 -10
- package/dist/edge/http/host-cli.js +34 -12
- package/dist/edge/http/multipart.js +7 -5
- package/dist/edge/http/open-folder.js +2 -2
- package/dist/edge/http/planning.js +4 -4
- package/dist/edge/http/problem.js +2 -2
- package/dist/edge/http/project-templates.js +7 -3
- package/dist/edge/http/projects.js +8 -8
- package/dist/edge/http/prompts.js +9 -4
- package/dist/edge/http/providers.js +15 -7
- package/dist/edge/http/revision-files.js +3 -3
- package/dist/edge/http/revisions.js +30 -4
- package/dist/edge/http/schedules.js +9 -9
- package/dist/edge/http/settings.js +15 -9
- package/dist/edge/http/staging.js +5 -5
- package/dist/edge/http/storage.js +4 -4
- package/dist/edge/http/subtitles.js +1 -1
- package/dist/edge/http/tutorial.js +2 -2
- package/dist/edge/http/update.js +3 -3
- package/dist/host-cli/install.js +28 -6
- package/dist/host-cli/paths.js +6 -6
- package/dist/host-cli/server.js +1 -1
- package/dist/host-cli/service.js +11 -11
- package/dist/host-cli/status.js +7 -3
- package/dist/kernel/config/index.js +6 -4
- package/dist/kernel/db/migrate.js +2 -1
- package/dist/kernel/db/migrations/0013-schedule-topic-queue.sql +4 -0
- package/dist/kernel/lock.js +4 -3
- package/dist/kernel/ports/subtitles.js +15 -1
- package/dist/kernel/runner/attempt.js +26 -2
- package/dist/main.js +2 -2
- package/dist/slices/admission/rules.js +32 -20
- package/dist/slices/article/continuation.js +4 -2
- package/dist/slices/article/plain.js +2 -2
- package/dist/slices/article/provided-entries.js +3 -3
- package/dist/slices/article/run.js +2 -2
- package/dist/slices/article/segments.js +5 -3
- package/dist/slices/control/providers.js +20 -7
- package/dist/slices/fonts/catalog.js +1 -1
- package/dist/slices/fonts/upload.js +1 -1
- package/dist/slices/images/run.js +5 -5
- package/dist/slices/library/lint.js +13 -7
- package/dist/slices/library/slots.js +2 -2
- package/dist/slices/narration/concat.js +1 -1
- package/dist/slices/narration/pronunciation-chunks.js +3 -3
- package/dist/slices/narration/pronunciation.js +2 -2
- package/dist/slices/narration/run.js +7 -7
- package/dist/slices/narration/steering.js +3 -3
- package/dist/slices/play-drafts/convert.js +5 -4
- package/dist/slices/play-drafts/readiness.js +12 -6
- package/dist/slices/play-drafts/review-inputs.js +7 -4
- package/dist/slices/play-drafts/review.js +3 -3
- package/dist/slices/play-drafts/service.js +3 -1
- package/dist/slices/play-drafts/uploads.js +1 -1
- package/dist/slices/rebuild/admission-repo.js +2 -2
- package/dist/slices/rebuild/recipe-legacy.js +1 -1
- package/dist/slices/rebuild/recipe-narration-text.js +1 -1
- package/dist/slices/rebuild/recipe-visual.js +1 -1
- package/dist/slices/rebuild/recovery.js +2 -2
- package/dist/slices/rebuild/repo.js +3 -3
- package/dist/slices/rebuild/runtime-admission.js +4 -4
- package/dist/slices/rebuild/runtime-article.js +5 -5
- package/dist/slices/rebuild/runtime-checkpoints.js +4 -4
- package/dist/slices/rebuild/runtime-export-inputs.js +6 -6
- package/dist/slices/rebuild/runtime-export.js +4 -4
- package/dist/slices/rebuild/runtime-local.js +6 -6
- package/dist/slices/rebuild/runtime-narration-text.js +46 -7
- package/dist/slices/rebuild/runtime-piece-label.js +62 -0
- package/dist/slices/rebuild/runtime-provider.js +6 -4
- package/dist/slices/rebuild/runtime-publication.js +3 -3
- package/dist/slices/rebuild/runtime-run.js +11 -1
- package/dist/slices/rebuild/runtime-subtitles.js +78 -15
- package/dist/slices/rebuild/transition-repo.js +6 -6
- package/dist/slices/reruns/index.js +1 -1
- package/dist/slices/research/run.js +9 -7
- package/dist/slices/research/synthesis.js +4 -2
- package/dist/slices/revisions/mutation-assets.js +26 -8
- package/dist/slices/revisions/mutation-cues.js +1 -1
- package/dist/slices/revisions/rules.js +18 -9
- package/dist/slices/revisions/schema.js +1 -1
- package/dist/slices/schedules/repo.js +55 -11
- package/dist/slices/schedules/scheduler.js +25 -8
- package/dist/slices/schedules/schema.js +19 -4
- package/dist/slices/schedules/service.js +2 -0
- package/dist/slices/settings/cli-paths.js +7 -4
- package/dist/slices/settings/models.js +4 -4
- package/dist/slices/settings/playback.js +2 -2
- package/dist/slices/storage/assets.js +5 -5
- package/dist/slices/storage/layout.js +1 -1
- package/dist/slices/subtitles/captions.js +1 -1
- package/dist/slices/subtitles/prepare.js +5 -5
- package/dist/slices/subtitles/transcript.js +1 -1
- package/dist/slices/thumbnail/run.js +8 -8
- package/dist/slices/video/audio-inputs.js +2 -2
- package/dist/slices/video/ffmpeg.js +6 -4
- package/dist/slices/video/plan.js +1 -1
- package/dist/slices/video/run.js +2 -2
- package/dist/updater/plan.js +1 -1
- package/dist/updater/service.js +4 -3
- package/dist/updater/worker.js +6 -4
- package/dist/web/assets/{index-CZNiHm8P.css → index-Bwtmu9ei.css} +1 -1
- package/dist/web/assets/index-DjhMJqMN.js +141 -0
- package/dist/web/index.html +2 -2
- package/package.json +1 -1
- package/dist/web/assets/index-DPgGLX5m.js +0 -137
|
@@ -64,9 +64,15 @@ export async function discoverGoogleImages(deps) {
|
|
|
64
64
|
function requireKey(deps) {
|
|
65
65
|
const key = deps.key();
|
|
66
66
|
if (key === undefined || key === "")
|
|
67
|
-
throw providerError({
|
|
67
|
+
throw providerError({
|
|
68
|
+
kind: "missing_key",
|
|
69
|
+
message: "Save this provider's API key in Settings → Providers to load its models.",
|
|
70
|
+
});
|
|
68
71
|
return key;
|
|
69
72
|
}
|
|
70
73
|
function unavailable() {
|
|
71
|
-
return providerError({
|
|
74
|
+
return providerError({
|
|
75
|
+
kind: "other",
|
|
76
|
+
message: "Slopify could not load this provider's model list. Check the API key in Settings → Providers and your internet connection, then refresh the list.",
|
|
77
|
+
});
|
|
72
78
|
}
|
|
@@ -1,6 +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 { httpFailure, missingKey, noImage, refusedImage, unreadable } from "../explain.js";
|
|
4
5
|
import { retryAfter } from "../retry-after.js";
|
|
5
6
|
import { describeBytes, sniffImage } from "./bytes.js";
|
|
6
7
|
import { discoverOpenAiImages } from "./models.js";
|
|
@@ -72,7 +73,7 @@ export function openAiImage(deps) {
|
|
|
72
73
|
}
|
|
73
74
|
const first = parse(await response.text()).data[0]?.b64_json;
|
|
74
75
|
if (first === undefined || first === null || first === "") {
|
|
75
|
-
throw providerError({ kind: "other", message: "OpenAI
|
|
76
|
+
throw providerError({ kind: "other", message: noImage("OpenAI") });
|
|
76
77
|
}
|
|
77
78
|
return decode(first);
|
|
78
79
|
},
|
|
@@ -87,7 +88,7 @@ function decode(b64) {
|
|
|
87
88
|
if (mime === undefined) {
|
|
88
89
|
throw providerError({
|
|
89
90
|
kind: "other",
|
|
90
|
-
message: `OpenAI
|
|
91
|
+
message: `OpenAI sent back something that is not a PNG or JPEG image (${describeBytes(bytes)}). Use Retry stage; if it keeps happening, choose another image model in the Providers section of Edit project.`,
|
|
91
92
|
});
|
|
92
93
|
}
|
|
93
94
|
return { bytes, mime };
|
|
@@ -96,7 +97,7 @@ function keyOf(deps) {
|
|
|
96
97
|
// `missing_key`, not `auth`, because that rule makes it terminal.
|
|
97
98
|
const key = deps.key();
|
|
98
99
|
if (key === undefined || key === "") {
|
|
99
|
-
throw providerError({ kind: "missing_key", message: "
|
|
100
|
+
throw providerError({ kind: "missing_key", message: missingKey("OpenAI") });
|
|
100
101
|
}
|
|
101
102
|
return key;
|
|
102
103
|
}
|
|
@@ -122,9 +123,17 @@ async function failure(response) {
|
|
|
122
123
|
// The provider's own words, through the redactor - an error body may quote the key back.
|
|
123
124
|
const message = redact(parsed.success ? parsed.data.error.message : text.trim() || response.statusText);
|
|
124
125
|
const retryAfterMs = retryAfter(response.headers.get("retry-after"));
|
|
126
|
+
const kind = kindOf(response.status, parsed.success ? parsed.data.error.code : undefined);
|
|
125
127
|
return providerError({
|
|
126
|
-
kind
|
|
127
|
-
message:
|
|
128
|
+
kind,
|
|
129
|
+
message: kind === "refusal"
|
|
130
|
+
? refusedImage("OpenAI", message)
|
|
131
|
+
: httpFailure({
|
|
132
|
+
provider: "OpenAI",
|
|
133
|
+
status: response.status,
|
|
134
|
+
detail: message,
|
|
135
|
+
subject: "image request",
|
|
136
|
+
}),
|
|
128
137
|
...(retryAfterMs === undefined ? {} : { retryAfterMs }),
|
|
129
138
|
});
|
|
130
139
|
}
|
|
@@ -133,7 +142,7 @@ function parse(text) {
|
|
|
133
142
|
if (!parsed.success) {
|
|
134
143
|
throw providerError({
|
|
135
144
|
kind: "other",
|
|
136
|
-
message: "OpenAI
|
|
145
|
+
message: unreadable("OpenAI"),
|
|
137
146
|
});
|
|
138
147
|
}
|
|
139
148
|
return parsed.data;
|
|
@@ -1,6 +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 { httpFailure, missingKey, noImage, providerSaid, refusedImage, unreadable, } from "../explain.js";
|
|
4
5
|
import { retryAfter } from "../retry-after.js";
|
|
5
6
|
import { downloadImage } from "./bytes.js";
|
|
6
7
|
// The HTTP gateway adapter for Replicate: `fetch`, the injected clock and the downloader
|
|
@@ -81,7 +82,7 @@ async function settle(deps, first, signal) {
|
|
|
81
82
|
if (next === undefined) {
|
|
82
83
|
throw providerError({
|
|
83
84
|
kind: "other",
|
|
84
|
-
message:
|
|
85
|
+
message: unreadable("Replicate"),
|
|
85
86
|
});
|
|
86
87
|
}
|
|
87
88
|
await deps.clock.sleep(replicatePollMs, signal);
|
|
@@ -100,7 +101,7 @@ function outputOf(current) {
|
|
|
100
101
|
const output = current.output;
|
|
101
102
|
const url = typeof output === "string" ? output : output?.[0];
|
|
102
103
|
if (url === undefined || url === "") {
|
|
103
|
-
throw providerError({ kind: "other", message: "Replicate
|
|
104
|
+
throw providerError({ kind: "other", message: noImage("Replicate") });
|
|
104
105
|
}
|
|
105
106
|
return url;
|
|
106
107
|
}
|
|
@@ -114,16 +115,19 @@ function declined(current) {
|
|
|
114
115
|
// ceiling: read off the sentence, because a failed prediction carries no machine-readable
|
|
115
116
|
// reason. A phrase the list does not know costs the user three more images; the upgrade
|
|
116
117
|
// is a structured field, when Replicate ships one.
|
|
118
|
+
const refusal = refusalWords.test(message);
|
|
117
119
|
return providerError({
|
|
118
|
-
kind:
|
|
119
|
-
message:
|
|
120
|
+
kind: refusal ? "refusal" : "other",
|
|
121
|
+
message: refusal
|
|
122
|
+
? refusedImage("Replicate", message)
|
|
123
|
+
: providerSaid("Replicate", "could not make the image", message, "Use Retry stage; if it keeps happening, reword the image prompt in the Images section of Edit project or choose another image model in its Providers section."),
|
|
120
124
|
});
|
|
121
125
|
}
|
|
122
126
|
function auth(deps) {
|
|
123
127
|
// `missing_key`, not `auth`, because that rule makes it terminal.
|
|
124
128
|
const key = deps.key();
|
|
125
129
|
if (key === undefined || key === "") {
|
|
126
|
-
throw providerError({ kind: "missing_key", message: "
|
|
130
|
+
throw providerError({ kind: "missing_key", message: missingKey("Replicate") });
|
|
127
131
|
}
|
|
128
132
|
return { Authorization: `Bearer ${key}` };
|
|
129
133
|
}
|
|
@@ -149,7 +153,12 @@ async function failure(response) {
|
|
|
149
153
|
const retryAfterMs = retryAfter(response.headers.get("retry-after"));
|
|
150
154
|
return providerError({
|
|
151
155
|
kind: kindOf(response.status),
|
|
152
|
-
message:
|
|
156
|
+
message: httpFailure({
|
|
157
|
+
provider: "Replicate",
|
|
158
|
+
status: response.status,
|
|
159
|
+
detail: message || response.statusText,
|
|
160
|
+
subject: "image request",
|
|
161
|
+
}),
|
|
153
162
|
...(retryAfterMs === undefined ? {} : { retryAfterMs }),
|
|
154
163
|
});
|
|
155
164
|
}
|
|
@@ -158,7 +167,7 @@ function parse(text) {
|
|
|
158
167
|
if (!parsed.success) {
|
|
159
168
|
throw providerError({
|
|
160
169
|
kind: "other",
|
|
161
|
-
message: "Replicate
|
|
170
|
+
message: unreadable("Replicate"),
|
|
162
171
|
});
|
|
163
172
|
}
|
|
164
173
|
return parsed.data;
|
|
@@ -38,7 +38,7 @@ const responseSchema = z.object({
|
|
|
38
38
|
});
|
|
39
39
|
const supportedEfforts = ["low", "medium", "high", "xhigh"];
|
|
40
40
|
function unavailable() {
|
|
41
|
-
return new Error("Claude Code
|
|
41
|
+
return new Error("Slopify could not get the model list from the Claude Code CLI. Check the CLI is installed and signed in (Settings → Providers), then refresh the list, or type an exact model ID.");
|
|
42
42
|
}
|
|
43
43
|
function discoveryEnv() {
|
|
44
44
|
const names = [
|
|
@@ -1,10 +1,11 @@
|
|
|
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 { cliCheck, cliReported, commandOf } from "../explain.js";
|
|
4
5
|
import { nodeClaudeCodeModels } from "./claude-code-models.js";
|
|
5
6
|
import { cliLoginError } from "./cli-login-error.js";
|
|
6
7
|
import { documentToolName, documentWorkspace, } from "./document-workspace.js";
|
|
7
|
-
import { cliEvent, cliInput, cliShaped, endedWithout, promptOf, stopCliRun } from "./run-cli.js";
|
|
8
|
+
import { cliEvent, cliInput, cliShaped, endedWithout, promptOf, stopCliRun, stuckCli, } from "./run-cli.js";
|
|
8
9
|
import { lines } from "./sse-lines.js";
|
|
9
10
|
// The local-agent adapter for Claude Code: spawned non-interactively, authenticated by the
|
|
10
11
|
// CLI's own login, no key anywhere in this file. Readiness is not computed here - `adapters/**`
|
|
@@ -142,7 +143,7 @@ export function claudeCodeLlm(deps) {
|
|
|
142
143
|
catch {
|
|
143
144
|
throw providerError({
|
|
144
145
|
kind: "unavailable",
|
|
145
|
-
message: "
|
|
146
|
+
message: "Slopify could not hand the whole prompt to the Claude Code CLI, so it cannot tell what the CLI worked on (it may already have used your quota). Use Retry stage to run it again.",
|
|
146
147
|
});
|
|
147
148
|
}
|
|
148
149
|
if (result.is_error === true || result.subtype !== "success") {
|
|
@@ -151,7 +152,7 @@ export function claudeCodeLlm(deps) {
|
|
|
151
152
|
throw login;
|
|
152
153
|
throw providerError({
|
|
153
154
|
kind: kindOf(result.api_error_status ?? null),
|
|
154
|
-
message: redact(result.result ?? result.subtype),
|
|
155
|
+
message: cliReported(binary, redact(result.result ?? result.subtype), nextStep(binary, result.api_error_status ?? null)),
|
|
155
156
|
});
|
|
156
157
|
}
|
|
157
158
|
documents?.verifyRead();
|
|
@@ -188,15 +189,13 @@ export function claudeCodeLlm(deps) {
|
|
|
188
189
|
req.signal.throwIfAborted();
|
|
189
190
|
// The stream ended with no result event at all.
|
|
190
191
|
if (!run)
|
|
191
|
-
throw new Error("Claude Code could not be started.");
|
|
192
|
+
throw new Error("The Claude Code CLI could not be started. Check it is installed and set up in Settings → Providers, then use Retry stage.");
|
|
192
193
|
const login = cliLoginError("claude-code", run.stderr());
|
|
193
194
|
if (login)
|
|
194
195
|
throw login;
|
|
195
196
|
throw providerError({
|
|
196
197
|
kind: "other",
|
|
197
|
-
message: ended === undefined
|
|
198
|
-
? `the ${binary} CLI did not stop after forced termination`
|
|
199
|
-
: endedWithout(binary, ended, run.stderr()),
|
|
198
|
+
message: ended === undefined ? stuckCli(binary) : endedWithout(binary, ended, run.stderr()),
|
|
200
199
|
});
|
|
201
200
|
}
|
|
202
201
|
return {
|
|
@@ -218,6 +217,16 @@ function kindOf(status) {
|
|
|
218
217
|
}
|
|
219
218
|
return "other";
|
|
220
219
|
}
|
|
220
|
+
// What most often fixes the upstream status the CLI passed on.
|
|
221
|
+
function nextStep(binary, status) {
|
|
222
|
+
if (status === 401 || status === 403)
|
|
223
|
+
return `Run "${commandOf(binary)} auth login" in a terminal to sign in again, then use Retry stage.`;
|
|
224
|
+
if (status === 429)
|
|
225
|
+
return "Your Claude usage limit may be used up: wait until it resets, then use Retry stage.";
|
|
226
|
+
if (status === 404)
|
|
227
|
+
return "The chosen model may not exist or not be available to your account: choose another in the Providers section of Edit project, then use Retry stage.";
|
|
228
|
+
return cliCheck(binary);
|
|
229
|
+
}
|
|
221
230
|
// `modelUsage` is keyed by model id and a run may touch more than one - a fallback model,
|
|
222
231
|
// a sub-agent - so the counts are summed. An empty object means the CLI reported none.
|
|
223
232
|
function usageOf(usage) {
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { providerError } from "../../kernel/ports/model.js";
|
|
2
|
+
import { cliName } from "../explain.js";
|
|
2
3
|
export function cliLoginError(provider, text) {
|
|
3
4
|
if (/invalid api key/i.test(text))
|
|
4
5
|
return undefined;
|
|
@@ -11,6 +12,6 @@ export function cliLoginError(provider, text) {
|
|
|
11
12
|
: "codex login";
|
|
12
13
|
return providerError({
|
|
13
14
|
kind: "missing_key",
|
|
14
|
-
message:
|
|
15
|
+
message: `The ${cliName(provider)} is not signed in, or its sign-in has expired. Open a terminal on the computer running the CLI, run "${command}" and sign in, then use Retry stage.`,
|
|
15
16
|
});
|
|
16
17
|
}
|
|
@@ -58,7 +58,7 @@ export async function nodeCodexModels(env = process.env, binary = "codex", timeo
|
|
|
58
58
|
}
|
|
59
59
|
catch {
|
|
60
60
|
// Do not expose cache contents, home paths or raw JSON errors to the browser.
|
|
61
|
-
throw new Error("
|
|
61
|
+
throw new Error("Slopify could not get the model list from the Codex CLI. Check the CLI is installed and signed in (Settings → Providers), then refresh the list, or type an exact model ID.");
|
|
62
62
|
}
|
|
63
63
|
}
|
|
64
64
|
}
|
|
@@ -4,10 +4,11 @@ import { join } from "node:path";
|
|
|
4
4
|
import { z } from "zod";
|
|
5
5
|
import { redact } from "../../kernel/log.js";
|
|
6
6
|
import { providerError } from "../../kernel/ports/model.js";
|
|
7
|
+
import { cliCheck, cliReported } from "../explain.js";
|
|
7
8
|
import { cliLoginError } from "./cli-login-error.js";
|
|
8
9
|
import { nodeCodexModels } from "./codex-models.js";
|
|
9
10
|
import { documentServerName, documentTool, documentWorkspace, } from "./document-workspace.js";
|
|
10
|
-
import { cliEvent, cliInput, cliShaped, deliveredInput, endedWithout, promptOf, stopCliRun, } from "./run-cli.js";
|
|
11
|
+
import { cliEvent, cliInput, cliShaped, deliveredInput, endedWithout, promptOf, stopCliRun, stuckCli, } from "./run-cli.js";
|
|
11
12
|
import { lines } from "./sse-lines.js";
|
|
12
13
|
// The local-agent adapter for the Codex CLI. Same shape as Claude Code's and a different
|
|
13
14
|
// vocabulary: Codex writes a JSONL thread of `thread.started`, `item.*` and `turn.*`
|
|
@@ -164,7 +165,7 @@ export function codexLlm(deps) {
|
|
|
164
165
|
throw login;
|
|
165
166
|
throw providerError({
|
|
166
167
|
kind: "other",
|
|
167
|
-
message: redact(cliShaped(binary, turnFailed, event.value).error.message),
|
|
168
|
+
message: cliReported(binary, redact(cliShaped(binary, turnFailed, event.value).error.message), cliCheck(binary)),
|
|
168
169
|
});
|
|
169
170
|
}
|
|
170
171
|
if (event.type === "error") {
|
|
@@ -173,7 +174,7 @@ export function codexLlm(deps) {
|
|
|
173
174
|
throw login;
|
|
174
175
|
throw providerError({
|
|
175
176
|
kind: "other",
|
|
176
|
-
message: redact(cliShaped(binary, errorEvent, event.value).message),
|
|
177
|
+
message: cliReported(binary, redact(cliShaped(binary, errorEvent, event.value).message), cliCheck(binary)),
|
|
177
178
|
});
|
|
178
179
|
}
|
|
179
180
|
}
|
|
@@ -202,9 +203,7 @@ export function codexLlm(deps) {
|
|
|
202
203
|
throw login;
|
|
203
204
|
throw providerError({
|
|
204
205
|
kind: "other",
|
|
205
|
-
message: ended === undefined
|
|
206
|
-
? `the ${binary} CLI did not stop after forced termination`
|
|
207
|
-
: endedWithout(binary, ended, run.stderr()),
|
|
206
|
+
message: ended === undefined ? stuckCli(binary) : endedWithout(binary, ended, run.stderr()),
|
|
208
207
|
});
|
|
209
208
|
}
|
|
210
209
|
return {
|
|
@@ -16,7 +16,7 @@ export function documentWorkspace(input) {
|
|
|
16
16
|
if (!parsed.success)
|
|
17
17
|
throw providerError({
|
|
18
18
|
kind: "unsupported",
|
|
19
|
-
message: "
|
|
19
|
+
message: "The research notes are too large, or not in a form the AI model can be given, so nothing was sent. Make the inputs shorter in Edit project, then use Retry stage; if it keeps happening, use Download diagnostics in Settings and report it.",
|
|
20
20
|
});
|
|
21
21
|
const documents = parsed.data;
|
|
22
22
|
const directory = mkdtempSync(join(tmpdir(), "slopify-documents-"));
|
|
@@ -82,6 +82,6 @@ export function documentWorkspace(input) {
|
|
|
82
82
|
function unread() {
|
|
83
83
|
return providerError({
|
|
84
84
|
kind: "unavailable",
|
|
85
|
-
message: "The
|
|
85
|
+
message: "The AI model did not read all of the research notes, so its answer was not used (the attempt may still have used your quota). Use Retry stage; if it keeps happening, choose another model in the Providers section of Edit project.",
|
|
86
86
|
});
|
|
87
87
|
}
|
|
@@ -38,7 +38,7 @@ export async function nodeGeminiModels(binary) {
|
|
|
38
38
|
throw new Error("No installed model metadata");
|
|
39
39
|
}
|
|
40
40
|
catch {
|
|
41
|
-
throw new Error("Gemini CLI
|
|
41
|
+
throw new Error("Slopify could not get the model list from the Gemini CLI. Check the CLI is installed (Settings → Providers), then refresh the list, or type a model name the Gemini CLI accepts.");
|
|
42
42
|
}
|
|
43
43
|
}
|
|
44
44
|
async function bundledModels(entry) {
|
|
@@ -33,7 +33,7 @@ export function geminiWorkspace(webSearch, request, documents, env = process.env
|
|
|
33
33
|
catch {
|
|
34
34
|
throw providerError({
|
|
35
35
|
kind: "unavailable",
|
|
36
|
-
message: "Gemini CLI
|
|
36
|
+
message: "Slopify could not read the Gemini CLI's settings file (settings.json in the .gemini folder of your home directory), which says how it signs in. Fix or remove that file, run gemini in a terminal to sign in again, then use Retry stage.",
|
|
37
37
|
});
|
|
38
38
|
}
|
|
39
39
|
const oauth = join(originalState, "oauth_creds.json");
|
|
@@ -1,10 +1,11 @@
|
|
|
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 { cliCheck, cliName, cliReported } from "../explain.js";
|
|
4
5
|
import { documentWorkspace } from "./document-workspace.js";
|
|
5
6
|
import { nodeGeminiModels } from "./gemini-models.js";
|
|
6
7
|
import { geminiWorkspace } from "./gemini-workspace.js";
|
|
7
|
-
import { cliEvent, cliInput, cliShaped, deliveredInput, endedWithout, promptOf, stopCliRun, } from "./run-cli.js";
|
|
8
|
+
import { cliEvent, cliInput, cliShaped, deliveredInput, endedWithout, promptOf, stopCliRun, stuckCli, } from "./run-cli.js";
|
|
8
9
|
import { lines } from "./sse-lines.js";
|
|
9
10
|
export const geminiBinary = "gemini";
|
|
10
11
|
// Verified against installed Gemini CLI 0.16.0's config/nonInteractiveCli sources.
|
|
@@ -65,12 +66,12 @@ export function geminiLlm(deps) {
|
|
|
65
66
|
else if (event.type === "error") {
|
|
66
67
|
const error = cliShaped(binary, errorSchema, event.value);
|
|
67
68
|
if (error.severity !== "warning")
|
|
68
|
-
throw failure(error.message);
|
|
69
|
+
throw failure(binary, error.message);
|
|
69
70
|
}
|
|
70
71
|
else if (event.type === "result") {
|
|
71
72
|
const result = cliShaped(binary, resultSchema, event.value);
|
|
72
73
|
if (result.status !== "success")
|
|
73
|
-
throw failure(result.error?.message ?? result.status);
|
|
74
|
+
throw failure(binary, result.error?.message ?? result.status);
|
|
74
75
|
await deliveredInput(run);
|
|
75
76
|
documents?.verifyRead();
|
|
76
77
|
yield {
|
|
@@ -103,12 +104,10 @@ export function geminiLlm(deps) {
|
|
|
103
104
|
}
|
|
104
105
|
req.signal.throwIfAborted();
|
|
105
106
|
if (run === undefined)
|
|
106
|
-
throw failure(`
|
|
107
|
+
throw failure(binary, "", `The ${cliName(binary)} could not be started. Check it is installed and set up in Settings → Providers, then use Retry stage.`);
|
|
107
108
|
if (authRequired(run.stderr()))
|
|
108
109
|
throw authFailure();
|
|
109
|
-
throw failure(ended === undefined
|
|
110
|
-
? `the ${binary} CLI did not stop after forced termination`
|
|
111
|
-
: endedWithout(binary, ended, run.stderr()));
|
|
110
|
+
throw failure(binary, run.stderr(), ended === undefined ? stuckCli(binary) : endedWithout(binary, ended, run.stderr()));
|
|
112
111
|
}
|
|
113
112
|
return {
|
|
114
113
|
id: "gemini",
|
|
@@ -117,16 +116,21 @@ export function geminiLlm(deps) {
|
|
|
117
116
|
complete,
|
|
118
117
|
};
|
|
119
118
|
}
|
|
120
|
-
|
|
119
|
+
// `message` is what the CLI said, read for the two failures that need the user's hand;
|
|
120
|
+
// `sentence` replaces the default wording when Slopify already knows what happened.
|
|
121
|
+
function failure(binary, message, sentence) {
|
|
121
122
|
if (/(?:#?3501\b|do not have a valid licen[cs]e of this product)/i.test(message)) {
|
|
122
123
|
return providerError({
|
|
123
124
|
kind: "unsupported",
|
|
124
|
-
message: "Gemini CLI
|
|
125
|
+
message: "Google refused the Gemini CLI because the signed-in account has no licence for it (#3501). Update the Gemini CLI, run gemini in a terminal to sign in again, then use Retry stage; for a work or school account, ask your administrator for a Gemini licence.",
|
|
125
126
|
});
|
|
126
127
|
}
|
|
127
128
|
return authRequired(message)
|
|
128
129
|
? authFailure()
|
|
129
|
-
: providerError({
|
|
130
|
+
: providerError({
|
|
131
|
+
kind: "other",
|
|
132
|
+
message: sentence ?? cliReported(binary, redact(message), cliCheck(binary)),
|
|
133
|
+
});
|
|
130
134
|
}
|
|
131
135
|
function authRequired(message) {
|
|
132
136
|
return /Code Assist login required|authorize the application|authorization code|Please set an Auth method|re-authenticate|reauthenticate/i.test(message);
|
|
@@ -134,7 +138,7 @@ function authRequired(message) {
|
|
|
134
138
|
function authFailure() {
|
|
135
139
|
return providerError({
|
|
136
140
|
kind: "missing_key",
|
|
137
|
-
message:
|
|
141
|
+
message: 'The Gemini CLI is not signed in, or its sign-in has expired. Open a terminal on the computer running the CLI, run "gemini" and sign in, then use Retry stage.',
|
|
138
142
|
});
|
|
139
143
|
}
|
|
140
144
|
async function* authChecked(source) {
|
|
@@ -2,6 +2,7 @@ import { z } from "zod";
|
|
|
2
2
|
import { redact } from "../../kernel/log.js";
|
|
3
3
|
import { documentMessages } from "../../kernel/ports/llm-documents.js";
|
|
4
4
|
import { providerError } from "../../kernel/ports/model.js";
|
|
5
|
+
import { droppedStream, httpFailure, missingKey, providerSaid, refreshList, unreadable, } from "../explain.js";
|
|
5
6
|
import { retryAfter } from "../retry-after.js";
|
|
6
7
|
import { sseData } from "./sse-lines.js";
|
|
7
8
|
// The HTTP gateway adapter: the platform's own `fetch` plus the line reader beside this
|
|
@@ -62,7 +63,7 @@ export function openRouterLlm(deps) {
|
|
|
62
63
|
throw await failure(response);
|
|
63
64
|
}
|
|
64
65
|
if (response.body === null) {
|
|
65
|
-
throw providerError({ kind: "other", message: "OpenRouter
|
|
66
|
+
throw providerError({ kind: "other", message: droppedStream("OpenRouter") });
|
|
66
67
|
}
|
|
67
68
|
let usage = null;
|
|
68
69
|
let finishReason = null;
|
|
@@ -81,7 +82,7 @@ export function openRouterLlm(deps) {
|
|
|
81
82
|
// frame; without this the stage would store half an article as a success.
|
|
82
83
|
throw providerError({
|
|
83
84
|
kind: kindOf(statusOf(chunk.error.code)),
|
|
84
|
-
message: redact(chunk.error.message),
|
|
85
|
+
message: streamFailure(statusOf(chunk.error.code), redact(chunk.error.message)),
|
|
85
86
|
});
|
|
86
87
|
}
|
|
87
88
|
const choice = chunk.choices?.[0];
|
|
@@ -108,7 +109,7 @@ export function openRouterLlm(deps) {
|
|
|
108
109
|
// Saying so is what stops a truncated answer being stored as a whole one.
|
|
109
110
|
throw providerError({
|
|
110
111
|
kind: "other",
|
|
111
|
-
message: "OpenRouter
|
|
112
|
+
message: droppedStream("OpenRouter"),
|
|
112
113
|
});
|
|
113
114
|
}
|
|
114
115
|
yield { type: "done", usage, finishReason };
|
|
@@ -122,13 +123,13 @@ export function openRouterLlm(deps) {
|
|
|
122
123
|
headers: headers(deps.key()),
|
|
123
124
|
});
|
|
124
125
|
if (!response.ok) {
|
|
125
|
-
throw await failure(response);
|
|
126
|
+
throw await failure(response, refreshList);
|
|
126
127
|
}
|
|
127
128
|
const parsed = modelList.safeParse(await response.json());
|
|
128
129
|
if (!parsed.success) {
|
|
129
130
|
throw providerError({
|
|
130
131
|
kind: "other",
|
|
131
|
-
message: "OpenRouter
|
|
132
|
+
message: unreadable("OpenRouter"),
|
|
132
133
|
});
|
|
133
134
|
}
|
|
134
135
|
// /models defaults to text output. Also reject an explicit non-text row
|
|
@@ -146,7 +147,7 @@ function headers(key) {
|
|
|
146
147
|
// rather than `auth` because the same rule makes it terminal: there is nothing to
|
|
147
148
|
// retry until the user saves a key, and the wrapper is where that is decided.
|
|
148
149
|
if (key === undefined || key === "") {
|
|
149
|
-
throw providerError({ kind: "missing_key", message: "
|
|
150
|
+
throw providerError({ kind: "missing_key", message: missingKey("OpenRouter") });
|
|
150
151
|
}
|
|
151
152
|
return { Authorization: `Bearer ${key}`, ...appHeaders };
|
|
152
153
|
}
|
|
@@ -163,7 +164,7 @@ function kindOf(status) {
|
|
|
163
164
|
// error contract first, which is not this adapter's to widen.
|
|
164
165
|
return "other";
|
|
165
166
|
}
|
|
166
|
-
async function failure(response) {
|
|
167
|
+
async function failure(response, next) {
|
|
167
168
|
const text = await response.text().catch(() => "");
|
|
168
169
|
const parsed = errorBody.safeParse(safeJson(text));
|
|
169
170
|
// The provider's own words, through the redactor - an error body may quote the key back.
|
|
@@ -171,10 +172,21 @@ async function failure(response) {
|
|
|
171
172
|
const retryAfterMs = retryAfter(response.headers.get("retry-after"));
|
|
172
173
|
return providerError({
|
|
173
174
|
kind: kindOf(response.status),
|
|
174
|
-
message:
|
|
175
|
+
message: httpFailure({
|
|
176
|
+
provider: "OpenRouter",
|
|
177
|
+
status: response.status,
|
|
178
|
+
detail: message,
|
|
179
|
+
next,
|
|
180
|
+
}),
|
|
175
181
|
...(retryAfterMs === undefined ? {} : { retryAfterMs }),
|
|
176
182
|
});
|
|
177
183
|
}
|
|
184
|
+
// A mid-stream error carries an HTTP-like code; with none, the provider's words are all there is.
|
|
185
|
+
function streamFailure(status, detail) {
|
|
186
|
+
return status >= 400
|
|
187
|
+
? httpFailure({ provider: "OpenRouter", status, detail })
|
|
188
|
+
: providerSaid("OpenRouter", "stopped part-way through the answer", detail, "Use Retry stage; if it keeps happening, choose another model in the Providers section of Edit project.");
|
|
189
|
+
}
|
|
178
190
|
function statusOf(code) {
|
|
179
191
|
return typeof code === "number" ? code : Number(code ?? 0);
|
|
180
192
|
}
|
|
@@ -186,7 +198,7 @@ function parseChunk(data) {
|
|
|
186
198
|
// a half-written frame is noise, and the wrapper stores whatever is thrown.
|
|
187
199
|
throw providerError({
|
|
188
200
|
kind: "other",
|
|
189
|
-
message: "OpenRouter
|
|
201
|
+
message: unreadable("OpenRouter"),
|
|
190
202
|
});
|
|
191
203
|
}
|
|
192
204
|
return parsed.data;
|
|
@@ -3,6 +3,7 @@ import { z } from "zod";
|
|
|
3
3
|
import { cliCommand } from "../../kernel/cli-command.js";
|
|
4
4
|
import { redact } from "../../kernel/log.js";
|
|
5
5
|
import { providerError } from "../../kernel/ports/model.js";
|
|
6
|
+
import { cliName, commandOf, quoted } from "../explain.js";
|
|
6
7
|
// ceiling: the last 8 KiB of stderr is kept. A CLI that writes megabytes of progress there
|
|
7
8
|
// would otherwise be held in memory for the length of a run, and the tail is the part that
|
|
8
9
|
// says why it stopped. Streaming stderr to the log is the upgrade if a provider ever
|
|
@@ -117,7 +118,7 @@ export async function deliveredInput(run) {
|
|
|
117
118
|
catch {
|
|
118
119
|
throw providerError({
|
|
119
120
|
kind: "unavailable",
|
|
120
|
-
message: "
|
|
121
|
+
message: "Slopify could not hand the whole prompt to the CLI, so it cannot tell what the CLI worked on (it may already have used your quota). Use Retry stage to run it again.",
|
|
121
122
|
});
|
|
122
123
|
}
|
|
123
124
|
}
|
|
@@ -126,7 +127,7 @@ export function cliInput(text) {
|
|
|
126
127
|
if (Buffer.byteLength(text) >= 8 * 1024 * 1024)
|
|
127
128
|
throw providerError({
|
|
128
129
|
kind: "unsupported",
|
|
129
|
-
message: "
|
|
130
|
+
message: "This prompt is larger than the CLI accepts (8 MB), so nothing was sent. Make the inputs shorter in Edit project, or choose an API provider such as OpenRouter in its Providers section, then use Retry stage.",
|
|
130
131
|
});
|
|
131
132
|
return text;
|
|
132
133
|
}
|
|
@@ -151,15 +152,17 @@ export function promptOf(messages) {
|
|
|
151
152
|
.join("\n\n");
|
|
152
153
|
}
|
|
153
154
|
// A bare non-zero exit tells the user nothing, so the stage shows whatever the CLI put on
|
|
154
|
-
// stderr - the provider's error text, verbatim.
|
|
155
|
+
// stderr - the provider's error text, verbatim - after a sentence saying what to do.
|
|
155
156
|
export function endedWithout(binary, ended, stderr) {
|
|
156
157
|
const said = redact(stderr.trim());
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
return said
|
|
161
|
-
|
|
162
|
-
|
|
158
|
+
if (ended.error !== null)
|
|
159
|
+
return `The ${cliName(binary)} could not be started${quoted("", redact(ended.error.message))}. Check it is installed and set up in Settings → Providers, then use Retry stage.`;
|
|
160
|
+
const how = ended.code === null ? "killed by the system" : `exit code ${String(ended.code)}`;
|
|
161
|
+
return `The ${cliName(binary)} stopped without answering${quoted(how, said)}. Run ${commandOf(binary)} in a terminal to check it works and is signed in, then use Retry stage.`;
|
|
162
|
+
}
|
|
163
|
+
// The run outlived both the polite and the forced stop.
|
|
164
|
+
export function stuckCli(binary) {
|
|
165
|
+
return `The ${cliName(binary)} stopped responding and could not be shut down. Close any leftover ${commandOf(binary)} processes or restart your computer, then use Retry stage.`;
|
|
163
166
|
}
|
|
164
167
|
// Unreachable while stdio names `pipe` for fd 1; it is here so the type carries no null
|
|
165
168
|
// and no non-null assertion is needed to read the stream.
|
|
@@ -180,7 +183,7 @@ export function cliEvent(binary, line) {
|
|
|
180
183
|
// know. The text is not echoed back: half a JSON object is noise to the user.
|
|
181
184
|
throw providerError({
|
|
182
185
|
kind: "other",
|
|
183
|
-
message: `
|
|
186
|
+
message: `The ${cliName(binary)} sent output Slopify could not read. Update it to the latest version, then use Retry stage.`,
|
|
184
187
|
});
|
|
185
188
|
}
|
|
186
189
|
return { type: parsed.data.type, value };
|
|
@@ -190,7 +193,7 @@ export function cliShaped(binary, schema, value) {
|
|
|
190
193
|
if (!parsed.success) {
|
|
191
194
|
throw providerError({
|
|
192
195
|
kind: "other",
|
|
193
|
-
message: `
|
|
196
|
+
message: `The ${cliName(binary)} sent output Slopify could not read. Update it to the latest version, then use Retry stage.`,
|
|
194
197
|
});
|
|
195
198
|
}
|
|
196
199
|
return parsed.data;
|
|
@@ -1,6 +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 { httpFailure, missingKey, noAudio, voiceFix } from "../explain.js";
|
|
4
5
|
import { retryAfter } from "../retry-after.js";
|
|
5
6
|
// The HTTP gateway adapter for Cartesia. `fetch` and
|
|
6
7
|
// nothing else; `/tts/bytes` streams the audio it renders, so the response body is
|
|
@@ -54,7 +55,7 @@ export function cartesiaTts(deps) {
|
|
|
54
55
|
throw await failure(response, req.voiceId);
|
|
55
56
|
}
|
|
56
57
|
if (response.body === null) {
|
|
57
|
-
throw providerError({ kind: "other", message: "Cartesia
|
|
58
|
+
throw providerError({ kind: "other", message: noAudio("Cartesia") });
|
|
58
59
|
}
|
|
59
60
|
return { audio: response.body, container: "mp3" };
|
|
60
61
|
},
|
|
@@ -64,7 +65,7 @@ function keyOf(deps) {
|
|
|
64
65
|
const key = deps.key();
|
|
65
66
|
// An absent key is terminal, so it never becomes a request.
|
|
66
67
|
if (key === undefined || key === "") {
|
|
67
|
-
throw providerError({ kind: "missing_key", message: "
|
|
68
|
+
throw providerError({ kind: "missing_key", message: missingKey("Cartesia") });
|
|
68
69
|
}
|
|
69
70
|
return key;
|
|
70
71
|
}
|
|
@@ -88,7 +89,13 @@ async function failure(response, voiceId) {
|
|
|
88
89
|
kind: kindOf(response.status),
|
|
89
90
|
// `voice_not_found` has to say which voice was asked for; Cartesia's
|
|
90
91
|
// own message does not repeat the id.
|
|
91
|
-
message:
|
|
92
|
+
message: httpFailure({
|
|
93
|
+
provider: "Cartesia",
|
|
94
|
+
status: response.status,
|
|
95
|
+
detail: message,
|
|
96
|
+
subject: `narration request for voice "${voiceId}"`,
|
|
97
|
+
fix: voiceFix(voiceId),
|
|
98
|
+
}),
|
|
92
99
|
...(retryAfterMs === undefined ? {} : { retryAfterMs }),
|
|
93
100
|
});
|
|
94
101
|
}
|