storyforge 0.4.5 → 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 +101 -20
- 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,13 +88,31 @@ ${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
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}
|
|
98
116
|
|
|
99
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
|
|
100
118
|
ENGAGEMENT BAR (this overrides everything below \u2014 RULE -1)
|
|
@@ -501,6 +519,46 @@ Now write the script.`;
|
|
|
501
519
|
// src/commands/dev.ts
|
|
502
520
|
var exec = promisify(execCb);
|
|
503
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
|
+
}
|
|
504
562
|
var WEB_URL = "https://forge.algo-thinker.com";
|
|
505
563
|
function getApiConfig() {
|
|
506
564
|
const creds = loadCredentials();
|
|
@@ -765,6 +823,14 @@ async function devCommand(options) {
|
|
|
765
823
|
const url = new URL(req.url || "/", `http://localhost:${port}`);
|
|
766
824
|
const pathname = url.pathname;
|
|
767
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
|
+
};
|
|
768
834
|
res.writeHead(200, { ...CORS_HEADERS, "Content-Type": "application/json" });
|
|
769
835
|
res.end(JSON.stringify({
|
|
770
836
|
status: "ok",
|
|
@@ -772,7 +838,14 @@ async function devCommand(options) {
|
|
|
772
838
|
channelId: meta.channelId,
|
|
773
839
|
title: meta.title,
|
|
774
840
|
channelSlug: meta.channelSlug,
|
|
775
|
-
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
|
+
}
|
|
776
849
|
}));
|
|
777
850
|
return;
|
|
778
851
|
}
|
|
@@ -935,35 +1008,41 @@ async function devCommand(options) {
|
|
|
935
1008
|
forbiddenVisuals: body.forbiddenVisuals,
|
|
936
1009
|
referenceScript: body.referenceScript,
|
|
937
1010
|
targetMinutesMin: body.targetMinutesMin,
|
|
938
|
-
targetMinutesMax: body.targetMinutesMax
|
|
1011
|
+
targetMinutesMax: body.targetMinutesMax,
|
|
1012
|
+
additionalGuidance: body.additionalGuidance
|
|
939
1013
|
});
|
|
940
|
-
const
|
|
941
|
-
|
|
942
|
-
const cleanup = () => {
|
|
943
|
-
if (fs2.existsSync(tmpFile)) fs2.unlinkSync(tmpFile);
|
|
944
|
-
};
|
|
945
|
-
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"];
|
|
946
1016
|
let raw = "";
|
|
947
1017
|
let modelUsed = "";
|
|
948
1018
|
let lastErr;
|
|
949
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
|
+
}
|
|
950
1024
|
try {
|
|
951
|
-
log.info(`[script-gen] Generating via
|
|
952
|
-
const
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
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
|
+
}
|
|
956
1036
|
raw = (stdout ?? "").trim();
|
|
957
1037
|
if (raw) {
|
|
958
|
-
modelUsed = model
|
|
1038
|
+
modelUsed = `${cli}:${model}`;
|
|
959
1039
|
break;
|
|
960
1040
|
}
|
|
961
1041
|
} catch (err) {
|
|
962
1042
|
lastErr = err instanceof Error ? err.message : String(err);
|
|
963
|
-
log.warn(`[script-gen] ${model} failed: ${lastErr.slice(0, 120)}`);
|
|
1043
|
+
log.warn(`[script-gen] ${cli}/${model} failed: ${lastErr.slice(0, 120)}`);
|
|
964
1044
|
}
|
|
965
1045
|
}
|
|
966
|
-
cleanup();
|
|
967
1046
|
if (!raw) {
|
|
968
1047
|
res.writeHead(502, { ...CORS_HEADERS, "Content-Type": "application/json" });
|
|
969
1048
|
res.end(JSON.stringify({
|
|
@@ -1003,8 +1082,10 @@ async function devCommand(options) {
|
|
|
1003
1082
|
res.end(JSON.stringify({
|
|
1004
1083
|
chunks,
|
|
1005
1084
|
model: modelUsed,
|
|
1006
|
-
provider: "claude-cli",
|
|
1007
|
-
|
|
1085
|
+
provider: cli === "codex" ? "codex-cli" : "claude-cli",
|
|
1086
|
+
cli,
|
|
1087
|
+
total_sec: totalSec,
|
|
1088
|
+
renderedPrompt: prompt2
|
|
1008
1089
|
}));
|
|
1009
1090
|
});
|
|
1010
1091
|
req.on("error", () => {
|