@gentbajko/slopify 2.0.2 → 2.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (224) hide show
  1. package/SUBTITLES.md +1 -1
  2. package/dist/adapters/alignment/protocol.js +11 -1
  3. package/dist/adapters/alignment/runner.js +4 -1
  4. package/dist/adapters/alignment/threads.js +11 -0
  5. package/dist/adapters/alignment/worker.js +66 -9
  6. package/dist/adapters/explain.js +93 -0
  7. package/dist/adapters/ffmpeg.js +3 -3
  8. package/dist/adapters/host-cli/index.js +34 -3
  9. package/dist/adapters/host-cli/transport.js +4 -4
  10. package/dist/adapters/image/bytes.js +2 -2
  11. package/dist/adapters/image/codex-output.js +12 -12
  12. package/dist/adapters/image/codex.js +24 -8
  13. package/dist/adapters/image/fal.js +12 -6
  14. package/dist/adapters/image/google.js +18 -8
  15. package/dist/adapters/image/models.js +8 -2
  16. package/dist/adapters/image/openai.js +15 -6
  17. package/dist/adapters/image/replicate.js +16 -7
  18. package/dist/adapters/llm/claude-code-models.js +1 -1
  19. package/dist/adapters/llm/claude-code.js +16 -7
  20. package/dist/adapters/llm/cli-login-error.js +2 -1
  21. package/dist/adapters/llm/codex-models.js +1 -1
  22. package/dist/adapters/llm/codex.js +5 -6
  23. package/dist/adapters/llm/document-workspace.js +2 -2
  24. package/dist/adapters/llm/gemini-models.js +1 -1
  25. package/dist/adapters/llm/gemini-workspace.js +1 -1
  26. package/dist/adapters/llm/gemini.js +15 -11
  27. package/dist/adapters/llm/openrouter.js +21 -9
  28. package/dist/adapters/llm/run-cli.js +14 -11
  29. package/dist/adapters/tts/cartesia.js +10 -3
  30. package/dist/adapters/tts/elevenlabs.js +18 -6
  31. package/dist/adapters/tts/inworld-async.js +47 -16
  32. package/dist/adapters/tts/inworld.js +25 -15
  33. package/dist/adapters/tts/openai.js +18 -6
  34. package/dist/assets/document/SOURCE.txt +7 -0
  35. package/dist/assets/document/background.jpg +0 -0
  36. package/dist/assets/document/fonts/Cinzel-Black.ttf +0 -0
  37. package/dist/assets/document/fonts/Cinzel-Bold.ttf +0 -0
  38. package/dist/assets/document/fonts/Cinzel-Medium.ttf +0 -0
  39. package/dist/assets/document/fonts/Cinzel-Regular.ttf +0 -0
  40. package/dist/assets/document/fonts/OFL.txt +93 -0
  41. package/dist/assets/document/fonts/SOURCE.txt +11 -0
  42. package/dist/catalog/registry.js +13 -10
  43. package/dist/catalog/store.js +1 -1
  44. package/dist/catalog/validate.js +16 -6
  45. package/dist/edge/cli.js +33 -7
  46. package/dist/edge/docker-launch.js +6 -6
  47. package/dist/edge/docker-projects/engine.js +34 -13
  48. package/dist/edge/docker-projects/install.js +8 -8
  49. package/dist/edge/docker-projects/state.js +11 -7
  50. package/dist/edge/docker-projects/tree.js +6 -6
  51. package/dist/edge/docker.js +13 -13
  52. package/dist/edge/host-cli.js +14 -0
  53. package/dist/edge/http/actions.js +27 -25
  54. package/dist/edge/http/app.js +3 -3
  55. package/dist/edge/http/audio-preview.js +1 -1
  56. package/dist/edge/http/checkpoints.js +5 -1
  57. package/dist/edge/http/diagnostics.js +3 -1
  58. package/dist/edge/http/draft-files.js +2 -2
  59. package/dist/edge/http/draft-problem.js +11 -1
  60. package/dist/edge/http/files.js +4 -4
  61. package/dist/edge/http/folder-location-schema.js +5 -8
  62. package/dist/edge/http/folder-location.js +13 -8
  63. package/dist/edge/http/fonts.js +22 -10
  64. package/dist/edge/http/host-cli.js +61 -14
  65. package/dist/edge/http/multipart.js +7 -5
  66. package/dist/edge/http/open-folder.js +2 -2
  67. package/dist/edge/http/planning.js +4 -4
  68. package/dist/edge/http/problem.js +2 -2
  69. package/dist/edge/http/project-templates.js +7 -3
  70. package/dist/edge/http/projects.js +8 -8
  71. package/dist/edge/http/prompts.js +9 -4
  72. package/dist/edge/http/providers.js +15 -7
  73. package/dist/edge/http/revision-files.js +8 -4
  74. package/dist/edge/http/revisions.js +30 -4
  75. package/dist/edge/http/schedules.js +9 -9
  76. package/dist/edge/http/settings.js +15 -9
  77. package/dist/edge/http/staging.js +5 -5
  78. package/dist/edge/http/storage.js +4 -4
  79. package/dist/edge/http/subtitles.js +1 -1
  80. package/dist/edge/http/tutorial.js +2 -2
  81. package/dist/edge/http/update.js +3 -3
  82. package/dist/host-cli/install.js +28 -6
  83. package/dist/host-cli/open-folder.js +95 -0
  84. package/dist/host-cli/paths.js +6 -6
  85. package/dist/host-cli/runtime.js +2 -0
  86. package/dist/host-cli/server.js +1 -1
  87. package/dist/host-cli/service.js +11 -11
  88. package/dist/host-cli/status.js +7 -3
  89. package/dist/kernel/config/index.js +6 -4
  90. package/dist/kernel/db/migrate.js +27 -2
  91. package/dist/kernel/db/migrations/0013-schedule-topic-queue.sql +4 -0
  92. package/dist/kernel/db/migrations/0014-document-stage.sql +44 -0
  93. package/dist/kernel/db/migrations/0015-description-prompts.sql +13 -0
  94. package/dist/kernel/lock.js +4 -3
  95. package/dist/kernel/pipeline.js +11 -1
  96. package/dist/kernel/ports/host-cli.js +14 -0
  97. package/dist/kernel/ports/subtitles.js +15 -1
  98. package/dist/kernel/runner/attempt.js +26 -2
  99. package/dist/kernel/runner/graph.js +6 -1
  100. package/dist/main.js +8 -4
  101. package/dist/slices/admission/model.js +7 -1
  102. package/dist/slices/admission/rules.js +98 -22
  103. package/dist/slices/admission/schema.js +16 -3
  104. package/dist/slices/admission/start.js +3 -2
  105. package/dist/slices/article/continuation.js +4 -2
  106. package/dist/slices/article/plain.js +2 -2
  107. package/dist/slices/article/provided-entries.js +3 -3
  108. package/dist/slices/article/run.js +2 -2
  109. package/dist/slices/article/segments.js +5 -3
  110. package/dist/slices/control/providers.js +20 -7
  111. package/dist/slices/document/blocks.js +129 -0
  112. package/dist/slices/document/flow.js +117 -0
  113. package/dist/slices/document/fonts.js +54 -0
  114. package/dist/slices/document/model.js +11 -0
  115. package/dist/slices/document/pages.js +198 -0
  116. package/dist/slices/document/render.js +242 -0
  117. package/dist/slices/document/sources.js +35 -0
  118. package/dist/slices/document/theme.js +158 -0
  119. package/dist/slices/document/writer.js +66 -0
  120. package/dist/slices/estimate/index.js +14 -2
  121. package/dist/slices/fonts/catalog.js +1 -1
  122. package/dist/slices/fonts/upload.js +1 -1
  123. package/dist/slices/images/run.js +5 -5
  124. package/dist/slices/library/lint.js +13 -7
  125. package/dist/slices/library/model.js +1 -1
  126. package/dist/slices/library/slots.js +6 -3
  127. package/dist/slices/narration/concat.js +1 -1
  128. package/dist/slices/narration/pronunciation-chunks.js +3 -3
  129. package/dist/slices/narration/pronunciation.js +2 -2
  130. package/dist/slices/narration/run.js +7 -7
  131. package/dist/slices/narration/steering.js +3 -3
  132. package/dist/slices/play-drafts/convert.js +35 -5
  133. package/dist/slices/play-drafts/readiness.js +15 -8
  134. package/dist/slices/play-drafts/review-inputs.js +7 -4
  135. package/dist/slices/play-drafts/review.js +3 -3
  136. package/dist/slices/play-drafts/schema.js +22 -5
  137. package/dist/slices/play-drafts/service.js +3 -1
  138. package/dist/slices/play-drafts/uploads.js +1 -1
  139. package/dist/slices/project-templates/from-project.js +10 -0
  140. package/dist/slices/project-templates/setup.js +1 -0
  141. package/dist/slices/rebuild/admission-repo.js +2 -2
  142. package/dist/slices/rebuild/preview-details.js +11 -1
  143. package/dist/slices/rebuild/recipe-audio.js +3 -0
  144. package/dist/slices/rebuild/recipe-build.js +6 -1
  145. package/dist/slices/rebuild/recipe-document.js +36 -0
  146. package/dist/slices/rebuild/recipe-exports.js +14 -2
  147. package/dist/slices/rebuild/recipe-fixture.js +4 -0
  148. package/dist/slices/rebuild/recipe-legacy.js +6 -1
  149. package/dist/slices/rebuild/recipe-model.js +2 -0
  150. package/dist/slices/rebuild/recipe-narration-text.js +1 -1
  151. package/dist/slices/rebuild/recipe-provider-choice.js +3 -0
  152. package/dist/slices/rebuild/recipe-save.js +15 -3
  153. package/dist/slices/rebuild/recipe-validation.js +15 -2
  154. package/dist/slices/rebuild/recipe-visual.js +11 -2
  155. package/dist/slices/rebuild/recipe-work.js +15 -0
  156. package/dist/slices/rebuild/recipe-youtube.js +33 -0
  157. package/dist/slices/rebuild/recovery-selection.js +2 -0
  158. package/dist/slices/rebuild/recovery.js +12 -7
  159. package/dist/slices/rebuild/repo.js +3 -3
  160. package/dist/slices/rebuild/runtime-actions.js +2 -0
  161. package/dist/slices/rebuild/runtime-admission.js +4 -4
  162. package/dist/slices/rebuild/runtime-article.js +5 -5
  163. package/dist/slices/rebuild/runtime-checkpoints.js +4 -4
  164. package/dist/slices/rebuild/runtime-document.js +85 -0
  165. package/dist/slices/rebuild/runtime-export-inputs.js +7 -6
  166. package/dist/slices/rebuild/runtime-export.js +39 -23
  167. package/dist/slices/rebuild/runtime-local.js +6 -6
  168. package/dist/slices/rebuild/runtime-narration-text.js +46 -7
  169. package/dist/slices/rebuild/runtime-piece-label.js +66 -0
  170. package/dist/slices/rebuild/runtime-provider.js +6 -4
  171. package/dist/slices/rebuild/runtime-publication.js +4 -3
  172. package/dist/slices/rebuild/runtime-run.js +22 -4
  173. package/dist/slices/rebuild/runtime-store.js +7 -2
  174. package/dist/slices/rebuild/runtime-subtitles.js +82 -16
  175. package/dist/slices/rebuild/runtime-youtube.js +84 -0
  176. package/dist/slices/rebuild/transition-repo.js +8 -6
  177. package/dist/slices/reruns/cascade.js +6 -4
  178. package/dist/slices/reruns/index.js +1 -1
  179. package/dist/slices/research/run.js +9 -7
  180. package/dist/slices/research/synthesis.js +4 -2
  181. package/dist/slices/revisions/mutation-assets.js +26 -8
  182. package/dist/slices/revisions/mutation-cues.js +4 -2
  183. package/dist/slices/revisions/projection.js +2 -1
  184. package/dist/slices/revisions/rules.js +18 -9
  185. package/dist/slices/revisions/schema.js +1 -1
  186. package/dist/slices/schedules/repo.js +55 -11
  187. package/dist/slices/schedules/scheduler.js +25 -8
  188. package/dist/slices/schedules/schema.js +19 -4
  189. package/dist/slices/schedules/service.js +2 -0
  190. package/dist/slices/settings/cli-paths.js +7 -4
  191. package/dist/slices/settings/models.js +4 -4
  192. package/dist/slices/settings/playback.js +2 -2
  193. package/dist/slices/storage/assets.js +5 -5
  194. package/dist/slices/storage/downloads.js +1 -0
  195. package/dist/slices/storage/layout.js +7 -1
  196. package/dist/slices/storage/model.js +3 -0
  197. package/dist/slices/subtitles/captions.js +1 -1
  198. package/dist/slices/subtitles/prepare.js +7 -6
  199. package/dist/slices/subtitles/transcript.js +1 -1
  200. package/dist/slices/telemetry/model.js +1 -1
  201. package/dist/slices/thumbnail/run.js +8 -8
  202. package/dist/slices/video/audio-export.js +11 -3
  203. package/dist/slices/video/audio-inputs.js +2 -2
  204. package/dist/slices/video/edit-list.js +73 -0
  205. package/dist/slices/video/ffmpeg.js +129 -61
  206. package/dist/slices/video/motion.js +51 -0
  207. package/dist/slices/video/plan.js +62 -28
  208. package/dist/slices/video/reuse-audio.js +2 -0
  209. package/dist/slices/video/run.js +23 -12
  210. package/dist/slices/video/slideshow.js +48 -0
  211. package/dist/slices/video/write-export.js +17 -25
  212. package/dist/slices/youtube/answer.js +199 -0
  213. package/dist/slices/youtube/model.js +27 -0
  214. package/dist/slices/youtube/timestamps.js +24 -0
  215. package/dist/slices/youtube/transcript.js +47 -0
  216. package/dist/updater/plan.js +1 -1
  217. package/dist/updater/service.js +4 -3
  218. package/dist/updater/worker.js +6 -4
  219. package/dist/web/assets/index-B6NbhpjD.css +1 -0
  220. package/dist/web/assets/index-ComF5gwP.js +142 -0
  221. package/dist/web/index.html +2 -2
  222. package/package.json +4 -2
  223. package/dist/web/assets/index-CZNiHm8P.css +0 -1
  224. package/dist/web/assets/index-DPgGLX5m.js +0 -137
@@ -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, 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 fal.ai: `fetch` and the downloader beside this file, no SDK.
@@ -88,12 +89,12 @@ export function falImage(deps) {
88
89
  refused(answer, req.prompt);
89
90
  const first = answer.images[0];
90
91
  if (first === undefined) {
91
- throw providerError({ kind: "other", message: "fal answered with no image" });
92
+ throw providerError({ kind: "other", message: noImage("fal.ai") });
92
93
  }
93
94
  // The download rides inside the attempt: a link that 404s is a failed attempt.
94
95
  return await downloadImage({
95
96
  fetch: deps.fetch,
96
- provider: "fal",
97
+ provider: "fal.ai",
97
98
  url: first.url,
98
99
  signal: req.signal,
99
100
  });
@@ -104,7 +105,7 @@ function keyOf(deps) {
104
105
  // `missing_key`, not `auth`, because that rule makes it terminal.
105
106
  const key = deps.key();
106
107
  if (key === undefined || key === "") {
107
- throw providerError({ kind: "missing_key", message: "no fal key is stored" });
108
+ throw providerError({ kind: "missing_key", message: missingKey("fal.ai") });
108
109
  }
109
110
  return key;
110
111
  }
@@ -116,7 +117,7 @@ function refused(answer, prompt) {
116
117
  if (flags.length > 0 && flags.every((flagged) => flagged)) {
117
118
  throw providerError({
118
119
  kind: "refusal",
119
- message: `fal's safety checker rejected every image for this prompt: ${redact(prompt)}`,
120
+ message: `fal.ai's safety checker blocked the image for this prompt: "${redact(prompt)}". Reword the image prompt in the Images section of Edit project, or choose another image provider in its Providers section.`,
120
121
  });
121
122
  }
122
123
  }
@@ -140,7 +141,12 @@ async function failure(response) {
140
141
  const retryAfterMs = retryAfter(response.headers.get("retry-after"));
141
142
  return providerError({
142
143
  kind: kindOf(response.status),
143
- message: `fal answered ${String(response.status)}: ${message}`,
144
+ message: httpFailure({
145
+ provider: "fal.ai",
146
+ status: response.status,
147
+ detail: message,
148
+ subject: "image request",
149
+ }),
144
150
  ...(retryAfterMs === undefined ? {} : { retryAfterMs }),
145
151
  });
146
152
  }
@@ -160,7 +166,7 @@ function parse(text) {
160
166
  if (!parsed.success) {
161
167
  throw providerError({
162
168
  kind: "other",
163
- message: "fal's answer was not in the shape this app can read",
169
+ message: unreadable("fal.ai"),
164
170
  });
165
171
  }
166
172
  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, quoted, refusedImage, unreadable } from "../explain.js";
4
5
  import { retryAfter } from "../retry-after.js";
5
6
  import { describeBytes, sniffImage } from "./bytes.js";
6
7
  import { discoverGoogleImages } from "./models.js";
@@ -92,7 +93,7 @@ function imageOf(answer) {
92
93
  }
93
94
  }
94
95
  }
95
- throw providerError({ kind: "other", message: "Google answered with no image" });
96
+ throw providerError({ kind: "other", message: noImage("Google") });
96
97
  }
97
98
  // The bytes arrive with the call. They are still sniffed rather than trusted: what the port
98
99
  // stores is a PNG or a JPEG, and a truncated payload that decodes to something else must
@@ -104,7 +105,7 @@ function decode(part) {
104
105
  const claimed = part.mime_type ?? "nothing";
105
106
  throw providerError({
106
107
  kind: "other",
107
- message: `Google called it ${claimed} but it decoded to ${describeBytes(bytes)} rather than a PNG or a JPEG`,
108
+ message: `Google sent back something that is not a PNG or JPEG image (labelled ${claimed}, but ${describeBytes(bytes)}). Use Retry stage; if it keeps happening, choose another image model in the Providers section of Edit project.`,
108
109
  });
109
110
  }
110
111
  return { bytes, mime };
@@ -113,7 +114,7 @@ function keyOf(deps) {
113
114
  // `missing_key`, not `auth`, because that rule makes it terminal.
114
115
  const key = deps.key();
115
116
  if (key === undefined || key === "") {
116
- throw providerError({ kind: "missing_key", message: "no Google key is stored" });
117
+ throw providerError({ kind: "missing_key", message: missingKey("Google") });
117
118
  }
118
119
  return key;
119
120
  }
@@ -155,12 +156,21 @@ async function failure(response) {
155
156
  ? parsed.data.error.details?.find((one) => one["@type"] === "type.googleapis.com/google.rpc.RetryInfo")?.retryDelay
156
157
  : undefined;
157
158
  const retryAfterMs = retryAfter(response.headers.get("retry-after")) ?? googleRetryDelay(delay, message);
158
- const guidance = kind === "unsupported"
159
- ? "Google reports zero image quota for this model. Check the API key's project, billing and model quota in Google AI Studio, or choose another image provider. Retrying will not help until quota is available. "
160
- : "";
159
+ const detail = message || response.statusText;
161
160
  return providerError({
162
161
  kind,
163
- message: `${guidance}Google answered ${String(response.status)}: ${message || response.statusText}`,
162
+ message: kind === "unsupported"
163
+ ? `Google gives this API key no image quota for this model${quoted(`error ${String(response.status)}`, detail)}. Retrying will not help: turn on billing for the key's project in Google AI Studio, or choose another image provider in the Providers section of Edit project.`
164
+ : // PERMISSION_DENIED and FAILED_PRECONDITION also cover keys without access, billing
165
+ // and region limits, so they read as a content refusal only when Google says so.
166
+ kind === "refusal" && refusalWords.test(message)
167
+ ? refusedImage("Google", detail)
168
+ : httpFailure({
169
+ provider: "Google",
170
+ status: response.status,
171
+ detail,
172
+ subject: "image request",
173
+ }),
164
174
  ...(retryAfterMs === undefined ? {} : { retryAfterMs }),
165
175
  });
166
176
  }
@@ -177,7 +187,7 @@ function parse(text) {
177
187
  if (!parsed.success) {
178
188
  throw providerError({
179
189
  kind: "other",
180
- message: "Google's answer was not in the shape this app can read",
190
+ message: unreadable("Google"),
181
191
  });
182
192
  }
183
193
  return parsed.data;
@@ -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({ kind: "missing_key", message: "Save an API key to load models." });
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({ kind: "other", message: "The provider model list could not be loaded." });
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 answered with no image" });
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's image decoded to ${describeBytes(bytes)} rather than a PNG or a JPEG`,
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: "no OpenAI image key is stored" });
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: kindOf(response.status, parsed.success ? parsed.data.error.code : undefined),
127
- message: `OpenAI answered ${String(response.status)}: ${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's answer was not in the shape this app can read",
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: `Replicate left the prediction ${current.status} and named no address to read it from`,
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 succeeded with no image" });
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: refusalWords.test(message) ? "refusal" : "other",
119
- message: `Replicate answered: ${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: "no Replicate key is stored" });
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: `Replicate answered ${String(response.status)}: ${message || response.statusText}`,
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's answer was not in the shape this app can read",
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 model discovery is unavailable. Refresh the list or enter an exact model ID.");
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: "The full prompt could not be delivered to Claude Code. Review before retrying.",
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: `${provider}: sign in on the machine running the CLI with ${command}, then review the affected rebuild.`,
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("Codex model metadata is unavailable. Check the CLI and refresh, or enter a custom model ID.");
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: "Research documents are invalid or exceed the supported request size. No document was truncated.",
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 CLI did not read every research document in full. Review the work before retrying; the provider may have charged for this attempt.",
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 model metadata is unavailable. Use a documented CLI alias or enter a custom model ID.");
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 login settings could not be read. Check its settings.json before retrying.",
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(`the ${binary} CLI could not be started`);
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
- function failure(message) {
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 access was denied by Google's license check (#3501). Update Gemini CLI and sign in again. For a managed account, contact your administrator to request a license.",
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({ kind: "other", message: redact(message) });
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: "Gemini CLI needs sign-in. Run gemini in your terminal, complete login, then retry in Slopify.",
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 answered with no body" });
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's stream ended before the response was complete",
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's model list was not in the shape this app can read",
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: "no OpenRouter key is stored" });
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: `OpenRouter answered ${response.status}: ${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 sent a stream frame this app could not read",
201
+ message: unreadable("OpenRouter"),
190
202
  });
191
203
  }
192
204
  return parsed.data;