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.
- package/dist/index.js +257 -22
- 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
|
|
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
|
-
|
|
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
|
|
787
|
-
|
|
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
|
|
798
|
-
const
|
|
799
|
-
|
|
800
|
-
|
|
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
|
-
|
|
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", () => {
|