storyforge 0.4.4 → 0.4.6

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 (2) hide show
  1. package/dist/index.js +257 -22
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -5,7 +5,7 @@ import * as fs2 from "fs";
5
5
  import * as os2 from "os";
6
6
  import * as path2 from "path";
7
7
  import * as http from "http";
8
- import { execFile, exec as execCb, execSync } from "child_process";
8
+ import { execFile, exec as execCb, execSync, spawn } from "child_process";
9
9
  import { promisify } from "util";
10
10
 
11
11
  // src/utils/log.ts
@@ -88,16 +88,188 @@ ${args.referenceScript.slice(0, 8e3)}
88
88
  === END SAMPLE ===
89
89
 
90
90
  Use this ONLY to calibrate the channel's voice \u2014 sentence rhythm, tolerance for technical detail, willingness to name real things. Do NOT copy its structure. If the sample opens with historical examples and this topic works better with a counter-intuitive question or a single unfolding metaphor, choose the format that serves the topic. The best script for THIS topic may look structurally different from the sample, and that's correct.` : "";
91
+ const guidanceBlock = (args.additionalGuidance ?? "").trim() ? `
92
+
93
+ \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550
94
+ VIEWER'S SPECIFIC ANGLE (RULE -2 \u2014 overrides everything below)
95
+ \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550
96
+
97
+ The person commissioning this script has spelled out the lens they
98
+ want. This is NOT a hint. It is the deciding constraint when you pick
99
+ structural format, dominant metaphor, named characters, antagonist,
100
+ and the open-loop. If the LLM's default instinct conflicts with this
101
+ guidance, the guidance wins.
102
+
103
+ >>> ${args.additionalGuidance.trim().replace(/\n/g, "\n>>> ")}
104
+
105
+ Before you commit to a format / metaphor / payoff, re-read the lens
106
+ above. Every chunk should serve it. Reject any structural choice that
107
+ doesn't.
108
+ ` : "";
91
109
  return `You are a senior documentary scriptwriter for ${channel} (${handle}). The channel's promise: "${tagline}".
92
110
 
93
- Your job is to write a script that EARNS attention from second 0 to the end. Every single chunk must justify its existence by either building comprehension or holding attention \u2014 ideally both.
111
+ Your job is to write a script that is UNPUTDOWNABLE \u2014 emotionally engaging, viscerally specific, and dense with surprise. Every chunk must earn the next click. "Technically accurate but academic" = FAILURE. "Comprehensive but boring" = FAILURE. The bar is: would a smart 22-year-old skip this video at second 30, or would they keep watching at minute 18?
94
112
 
95
113
  TOPIC: "${args.topic}"
96
114
  STYLE: ${args.style} \u2014 ${args.styleGuide}
97
- TARGET LENGTH: ${minMin}-${maxMin} minutes (write to the topic's natural length \u2014 pad nothing, rush nothing).
115
+ TARGET LENGTH: ${minMin}-${maxMin} minutes (write to the topic's natural length \u2014 pad nothing, rush nothing).${guidanceBlock}
116
+
117
+ \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550
118
+ ENGAGEMENT BAR (this overrides everything below \u2014 RULE -1)
119
+ \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550
120
+
121
+ A previous KV Cache script we got back was technically accurate but
122
+ DEAD on arrival. It read like a textbook chapter. It was a 1/10 on
123
+ retention. Why? It made these mistakes \u2014 DO NOT REPEAT THEM:
124
+
125
+ 1. NO NAMED CHARACTERS. Just "researchers" and "engineers". Real
126
+ scripts have humans with names doing things in specific places.
127
+
128
+ 2. NO SCENES. Just told us "imagine the librarian". Real scripts
129
+ SHOW: time of day, location, what's on the screen, what someone
130
+ said in a meeting, the GPU temperature when something broke.
131
+
132
+ 3. NO SECOND PERSON. Wrote in detached third person throughout.
133
+ Engaging scripts say "your AI assistant", "you're typing into
134
+ ChatGPT and..." \u2014 pull the viewer into the scene.
135
+
136
+ 4. ACADEMIC SENTENCE STRUCTURE. Long compound sentences with
137
+ multiple subordinate clauses. "Production inference has two
138
+ very different phases, and KV Cache explains why they feel
139
+ different to users." That reads like a paper. The same
140
+ information as: "Two phases. They feel completely different.
141
+ KV Cache explains why."
142
+
143
+ 5. NO ANTAGONIST / NO TENSION. Tesla had Waymo. KV Cache had no
144
+ foil. Every great explainer has a "vs" \u2014 brute force vs
145
+ elegance, the old way vs the new way, the hopeful believer vs
146
+ the cynical realist.
147
+
148
+ 6. STALE CITATIONS. Cited Gemini 1.5 (2024), Llama 2 (2023), GPT-3
149
+ (2020). The video is being made in April 2026. If you cite
150
+ anything older than 6 months as the "current" example, you have
151
+ FAILED. Use web_search.
152
+
153
+ 7. FORMULAIC RE-HOOKS. Repeated "Remember the open loop:" verbatim
154
+ three times. Real re-hooks vary in voice: "Pause for a second.",
155
+ "Here's where it gets weird.", "Wait. Why does that matter?"
156
+
157
+ \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550
158
+ BORING vs ENGAGING \u2014 concrete A/B examples
159
+ \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550
160
+
161
+ HOOK
162
+ Bad (academic):
163
+ "In 2024, Google DeepMind showed Gemini 1.5 Pro reading up to
164
+ 1 million tokens. That sounds like a bigger brain. It is not."
165
+ Good (specific scene + character + stakes):
166
+ "It's 3 AM in March 2026. A junior engineer at OpenAI watches
167
+ her dashboard turn red as a single user pastes the entire
168
+ Twilight saga into GPT-5.5. Latency hits 41 seconds. The cost
169
+ counter ticks past 12 dollars for ONE response. She stares at
170
+ the number that explains why AI is getting more expensive even
171
+ though the models are getting cheaper. It's called KV Cache.
172
+ And right now, it's eating the company alive."
173
+
174
+ MECHANISM EXPLANATION
175
+ Bad: "Each new token has to compare against a long history."
176
+ Good: "Imagine typing the next word of an email, but before each
177
+ keystroke your laptop has to re-read every email you've sent
178
+ this year. That's what a transformer would do without KV Cache.
179
+ Now imagine your laptop wrote a single Post-it note for every
180
+ important word and stuck them on a desk. To type the next word,
181
+ it just glances at the desk. That's KV Cache. The Post-its are
182
+ the desk's secret. They're also the reason your AI bill has
183
+ exploded."
184
+
185
+ SECTION TRANSITION (re-hook)
186
+ Bad: "Remember the open loop. We asked..."
187
+ Good: "Stop for a second. The thing we just described \u2014 that
188
+ desk full of Post-its \u2014 is now eating 80% of an H200's memory
189
+ on every long conversation. THAT'S the bill nobody talks about."
98
190
 
99
191
  \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550
100
- HARD WORD-COUNT CONTRACT (this is RULE ZERO \u2014 non-negotiable)
192
+ NON-NEGOTIABLE ENGAGEMENT RULES
193
+ \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550
194
+
195
+ A. CHARACTERS: At least 2 named humans appear in scenes. Examples:
196
+ "Aman Goel, a Stanford PhD student, was debugging a vLLM crash
197
+ at 2 AM when..." or "Mira Murati, then OpenAI's CTO, told a
198
+ stage at AI Engineer Summit in November 2024 that..." Use real
199
+ people the web_search tool surfaces. If you cannot name a real
200
+ person, write a CHARACTERIZED engineer ("the inference team's
201
+ on-call engineer in San Francisco at 3 AM") \u2014 never anonymous
202
+ "researchers".
203
+
204
+ B. SCENES: At least 1 chunk per ~5 minutes contains a SCENE \u2014 time
205
+ of day, location, what is on the screen, what a specific person
206
+ said or did. Cinematic. Visual.
207
+
208
+ C. SECOND PERSON: At least 30% of paragraphs use "you" / "your" to
209
+ pull the viewer into a felt situation. Not every paragraph, but
210
+ peppered throughout to break the academic third person.
211
+
212
+ D. SENTENCE RHYTHM: Mix LONG sentences with SHORT punches. After a
213
+ 30-word explanation, drop a 4-word sentence. "And it broke."
214
+ "Memory was the killer." "Nobody saw it coming." Frequency:
215
+ at least 1 punch sentence per 80 words.
216
+
217
+ E. ANTAGONIST: Every script has a "vs". Choose one before writing:
218
+ - The old way vs the new way
219
+ - The optimist's claim vs the engineer's reality
220
+ - The marketing pitch vs the inference bill
221
+ - Brute force vs elegance
222
+ - The competitor's approach vs ours
223
+ Name it explicitly in chunk 1 and revisit it 3+ times.
224
+
225
+ F. SURPRISES: At least 3 reversals where you set up an expectation,
226
+ then break it. "You'd think the cost goes up linearly. It doesn't.
227
+ It goes up quadratically \u2014 and here's why that almost killed
228
+ ChatGPT in 2023." This is what keeps people watching.
229
+
230
+ G. RE-HOOK VARIETY: NEVER repeat the same re-hook phrasing. Vary it:
231
+ "Pause for a second."
232
+ "Here's where it gets weird."
233
+ "Wait. Why does that matter?"
234
+ "Look at the bill."
235
+ "Now ask yourself..."
236
+ Banned: "Remember the open loop:" or any verbatim re-hook
237
+ appearing more than once in the script.
238
+
239
+ H. CURRENT FACTS via web_search: BEFORE writing each chunk that
240
+ names a model / paper / number / product, USE WEB_SEARCH. If
241
+ you cite GPT-3, GPT-4, Gemini 1.5, Llama 2, Claude Sonnet 3.5,
242
+ or any other model older than 6 months as your "current example",
243
+ you have FAILED the brief.
244
+
245
+ Current as of April 2026:
246
+ OpenAI flagship \u2192 GPT-5.5 (released April 23 2026)
247
+ Anthropic flagship \u2192 Claude Opus 4.7
248
+ Meta open-weights \u2192 Llama 4
249
+ Google \u2192 Gemini 2.x family
250
+ USE WEB_SEARCH to verify. Don't trust your memory.
251
+
252
+ \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550
253
+ ACADEMIC PATTERNS \u2014 BANNED
254
+ \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550
255
+
256
+ The following sentence patterns will cause the script to read like a
257
+ textbook. Avoid all of them:
258
+
259
+ - "X has Y, and Y explains why..." (compound-clause explainer voice)
260
+ - "It is..." / "There is..." / "There exists..." as opener
261
+ - "In essence" / "essentially" / "fundamentally" / "ultimately"
262
+ - "Let me explain" / "let's break this down" / "in other words"
263
+ - "It is worth noting that" / "it should be observed"
264
+ - "On the other hand" used as a section pivot
265
+ - Three sentences in a row that all start with "The"
266
+ - Three sentences in a row over 30 words each
267
+ - Any paragraph that doesn't contain a concrete noun (a thing you
268
+ can point at \u2014 a building, a person, a product, a number with
269
+ a unit). If a paragraph is all abstractions, REWRITE it.
270
+
271
+ \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550
272
+ HARD WORD-COUNT CONTRACT (RULE ZERO)
101
273
  \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550
102
274
 
103
275
  Each chunk's narrationText MUST contain at least (durationEstimate \xD7 2.5)
@@ -347,6 +519,46 @@ Now write the script.`;
347
519
  // src/commands/dev.ts
348
520
  var exec = promisify(execCb);
349
521
  var PORT = 4444;
522
+ function runCliPipingStdin(cmd, args, stdinData, opts = {}) {
523
+ const maxBytes = (opts.maxBufferMB ?? 16) * 1024 * 1024;
524
+ return new Promise((resolve2, reject) => {
525
+ const proc = spawn(cmd, args, { stdio: ["pipe", "pipe", "pipe"] });
526
+ let stdout = "";
527
+ let stderr = "";
528
+ let total = 0;
529
+ const timer = opts.timeoutMs ? setTimeout(() => {
530
+ try {
531
+ proc.kill("SIGKILL");
532
+ } catch {
533
+ }
534
+ reject(new Error(`${cmd} timed out after ${opts.timeoutMs}ms`));
535
+ }, opts.timeoutMs) : null;
536
+ proc.stdout.on("data", (d) => {
537
+ total += d.length;
538
+ if (total > maxBytes) {
539
+ try {
540
+ proc.kill("SIGKILL");
541
+ } catch {
542
+ }
543
+ reject(new Error(`${cmd} stdout exceeded ${opts.maxBufferMB ?? 16}MB`));
544
+ return;
545
+ }
546
+ stdout += d.toString("utf-8");
547
+ });
548
+ proc.stderr.on("data", (d) => {
549
+ stderr += d.toString("utf-8");
550
+ });
551
+ proc.on("error", (err) => {
552
+ if (timer) clearTimeout(timer);
553
+ reject(err);
554
+ });
555
+ proc.on("close", (code) => {
556
+ if (timer) clearTimeout(timer);
557
+ resolve2({ stdout, stderr, code });
558
+ });
559
+ proc.stdin.end(stdinData);
560
+ });
561
+ }
350
562
  var WEB_URL = "https://forge.algo-thinker.com";
351
563
  function getApiConfig() {
352
564
  const creds = loadCredentials();
@@ -611,6 +823,14 @@ async function devCommand(options) {
611
823
  const url = new URL(req.url || "/", `http://localhost:${port}`);
612
824
  const pathname = url.pathname;
613
825
  if (pathname === "/api/health") {
826
+ const probeCli = (binary) => {
827
+ try {
828
+ const out = execSync(`which ${binary}`, { stdio: ["ignore", "pipe", "ignore"], timeout: 2e3 }).toString().trim();
829
+ return { available: !!out, path: out || null };
830
+ } catch {
831
+ return { available: false, path: null };
832
+ }
833
+ };
614
834
  res.writeHead(200, { ...CORS_HEADERS, "Content-Type": "application/json" });
615
835
  res.end(JSON.stringify({
616
836
  status: "ok",
@@ -618,7 +838,14 @@ async function devCommand(options) {
618
838
  channelId: meta.channelId,
619
839
  title: meta.title,
620
840
  channelSlug: meta.channelSlug,
621
- dir
841
+ dir,
842
+ // CLI bridge surface — mirrors the production VPS bridge's /health
843
+ // shape so the web UI can use the same code path for both.
844
+ bridge: {
845
+ source: "local-forge-dev",
846
+ claude: probeCli("claude"),
847
+ codex: probeCli("codex")
848
+ }
622
849
  }));
623
850
  return;
624
851
  }
@@ -781,35 +1008,41 @@ async function devCommand(options) {
781
1008
  forbiddenVisuals: body.forbiddenVisuals,
782
1009
  referenceScript: body.referenceScript,
783
1010
  targetMinutesMin: body.targetMinutesMin,
784
- targetMinutesMax: body.targetMinutesMax
1011
+ targetMinutesMax: body.targetMinutesMax,
1012
+ additionalGuidance: body.additionalGuidance
785
1013
  });
786
- const tmpFile = path2.join(os2.tmpdir(), `forge-script-${Date.now()}.txt`);
787
- fs2.writeFileSync(tmpFile, prompt2, "utf-8");
788
- const cleanup = () => {
789
- if (fs2.existsSync(tmpFile)) fs2.unlinkSync(tmpFile);
790
- };
791
- const candidates = ["claude-opus-4-7", "claude-sonnet-4-6"];
1014
+ const cli = body.cli === "codex" ? "codex" : "claude";
1015
+ const candidates = cli === "codex" ? ["gpt-5.5", "gpt-5.4", "gpt-5.4-mini"] : ["claude-opus-4-7", "claude-sonnet-4-6"];
792
1016
  let raw = "";
793
1017
  let modelUsed = "";
794
1018
  let lastErr;
795
1019
  for (const model of candidates) {
1020
+ if (!/^[a-z0-9.\-]+$/i.test(model)) {
1021
+ lastErr = `model alias "${model}" rejected by validator`;
1022
+ continue;
1023
+ }
796
1024
  try {
797
- log.info(`[script-gen] Generating via Claude CLI \xB7 model=${model}`);
798
- const { stdout } = await exec(
799
- `cat "${tmpFile}" | claude -p --model ${model} --no-session-persistence`,
800
- { maxBuffer: 16 * 1024 * 1024, timeout: 3e5 }
801
- );
1025
+ log.info(`[script-gen] Generating via ${cli} CLI \xB7 model=${model}`);
1026
+ const args = cli === "codex" ? ["exec", "-", "--model", model] : ["-p", "--model", model, "--no-session-persistence"];
1027
+ const { stdout, code, stderr } = await runCliPipingStdin(cli, args, prompt2, {
1028
+ timeoutMs: 3e5,
1029
+ maxBufferMB: 16
1030
+ });
1031
+ if (code !== 0) {
1032
+ lastErr = `${cli}/${model} exit ${code}: ${stderr.slice(-300)}`;
1033
+ log.warn(`[script-gen] ${lastErr.slice(0, 120)}`);
1034
+ continue;
1035
+ }
802
1036
  raw = (stdout ?? "").trim();
803
1037
  if (raw) {
804
- modelUsed = model;
1038
+ modelUsed = `${cli}:${model}`;
805
1039
  break;
806
1040
  }
807
1041
  } catch (err) {
808
1042
  lastErr = err instanceof Error ? err.message : String(err);
809
- log.warn(`[script-gen] ${model} failed: ${lastErr.slice(0, 120)}`);
1043
+ log.warn(`[script-gen] ${cli}/${model} failed: ${lastErr.slice(0, 120)}`);
810
1044
  }
811
1045
  }
812
- cleanup();
813
1046
  if (!raw) {
814
1047
  res.writeHead(502, { ...CORS_HEADERS, "Content-Type": "application/json" });
815
1048
  res.end(JSON.stringify({
@@ -849,8 +1082,10 @@ async function devCommand(options) {
849
1082
  res.end(JSON.stringify({
850
1083
  chunks,
851
1084
  model: modelUsed,
852
- provider: "claude-cli",
853
- total_sec: totalSec
1085
+ provider: cli === "codex" ? "codex-cli" : "claude-cli",
1086
+ cli,
1087
+ total_sec: totalSec,
1088
+ renderedPrompt: prompt2
854
1089
  }));
855
1090
  });
856
1091
  req.on("error", () => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "storyforge",
3
- "version": "0.4.4",
3
+ "version": "0.4.6",
4
4
  "description": "StoryForge — local bridge for the Forge video production web app. Zero runtime dependencies.",
5
5
  "type": "module",
6
6
  "bin": {