storyforge 0.4.6 → 0.4.8
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/bridge-poller-5FFV2LML.js +199 -0
- package/dist/chunk-GJQ45C5W.js +17 -0
- package/dist/index.js +45 -20
- package/package.json +1 -1
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
log
|
|
4
|
+
} from "./chunk-GJQ45C5W.js";
|
|
5
|
+
|
|
6
|
+
// src/bridge-poller.ts
|
|
7
|
+
import { spawn, spawnSync } from "child_process";
|
|
8
|
+
import * as crypto from "crypto";
|
|
9
|
+
var HEARTBEAT_INTERVAL_MS = 3e4;
|
|
10
|
+
var POLL_INTERVAL_MS = 3e3;
|
|
11
|
+
var CLI_TIMEOUT_MS = 5 * 60 * 1e3;
|
|
12
|
+
var BridgePoller = class {
|
|
13
|
+
baseUrl;
|
|
14
|
+
token;
|
|
15
|
+
clientVersion;
|
|
16
|
+
instanceId;
|
|
17
|
+
heartbeatTimer = null;
|
|
18
|
+
pollTimer = null;
|
|
19
|
+
inFlight = /* @__PURE__ */ new Set();
|
|
20
|
+
stopped = false;
|
|
21
|
+
constructor(opts) {
|
|
22
|
+
this.baseUrl = opts.baseUrl.replace(/\/$/, "");
|
|
23
|
+
this.token = opts.token;
|
|
24
|
+
this.clientVersion = opts.clientVersion;
|
|
25
|
+
this.instanceId = `${process.pid}-${crypto.randomBytes(2).toString("hex")}-${Date.now()}`;
|
|
26
|
+
}
|
|
27
|
+
start() {
|
|
28
|
+
if (this.heartbeatTimer || this.pollTimer) return;
|
|
29
|
+
log.info(`[bridge] poller starting \xB7 ${this.baseUrl} \xB7 instance=${this.instanceId}`);
|
|
30
|
+
void this.heartbeat();
|
|
31
|
+
this.heartbeatTimer = setInterval(() => {
|
|
32
|
+
void this.heartbeat();
|
|
33
|
+
}, HEARTBEAT_INTERVAL_MS);
|
|
34
|
+
this.pollTimer = setInterval(() => {
|
|
35
|
+
void this.poll();
|
|
36
|
+
}, POLL_INTERVAL_MS);
|
|
37
|
+
}
|
|
38
|
+
async stop() {
|
|
39
|
+
this.stopped = true;
|
|
40
|
+
if (this.heartbeatTimer) clearInterval(this.heartbeatTimer);
|
|
41
|
+
if (this.pollTimer) clearInterval(this.pollTimer);
|
|
42
|
+
this.heartbeatTimer = null;
|
|
43
|
+
this.pollTimer = null;
|
|
44
|
+
}
|
|
45
|
+
probeBinary(name) {
|
|
46
|
+
if (!/^[a-z][a-z0-9-]*$/i.test(name)) return { available: false, path: null };
|
|
47
|
+
const r = spawnSync("which", [name], { encoding: "utf-8", timeout: 2e3 });
|
|
48
|
+
if (r.status !== 0) return { available: false, path: null };
|
|
49
|
+
const path = (r.stdout ?? "").trim();
|
|
50
|
+
return { available: !!path, path: path || null };
|
|
51
|
+
}
|
|
52
|
+
async heartbeat() {
|
|
53
|
+
if (this.stopped) return;
|
|
54
|
+
try {
|
|
55
|
+
const resp = await fetch(`${this.baseUrl}/api/cli-bridge/heartbeat`, {
|
|
56
|
+
method: "POST",
|
|
57
|
+
headers: {
|
|
58
|
+
Authorization: `Bearer ${this.token}`,
|
|
59
|
+
"Content-Type": "application/json"
|
|
60
|
+
},
|
|
61
|
+
body: JSON.stringify({
|
|
62
|
+
availableClis: {
|
|
63
|
+
claude: this.probeBinary("claude"),
|
|
64
|
+
codex: this.probeBinary("codex")
|
|
65
|
+
},
|
|
66
|
+
clientVersion: this.clientVersion,
|
|
67
|
+
instanceId: this.instanceId
|
|
68
|
+
}),
|
|
69
|
+
signal: AbortSignal.timeout(1e4)
|
|
70
|
+
});
|
|
71
|
+
if (!resp.ok) {
|
|
72
|
+
const text = await resp.text().catch(() => "");
|
|
73
|
+
log.warn(`[bridge] heartbeat HTTP ${resp.status}: ${text.slice(0, 120)}`);
|
|
74
|
+
}
|
|
75
|
+
} catch (err) {
|
|
76
|
+
log.warn(`[bridge] heartbeat failed: ${err.message}`);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
async poll() {
|
|
80
|
+
if (this.stopped) return;
|
|
81
|
+
let jobs = [];
|
|
82
|
+
try {
|
|
83
|
+
const resp = await fetch(`${this.baseUrl}/api/cli-bridge/claim`, {
|
|
84
|
+
method: "POST",
|
|
85
|
+
headers: {
|
|
86
|
+
Authorization: `Bearer ${this.token}`,
|
|
87
|
+
"Content-Type": "application/json"
|
|
88
|
+
},
|
|
89
|
+
body: JSON.stringify({ instanceId: this.instanceId, limit: 3 }),
|
|
90
|
+
signal: AbortSignal.timeout(1e4)
|
|
91
|
+
});
|
|
92
|
+
if (!resp.ok) {
|
|
93
|
+
if (resp.status === 401) {
|
|
94
|
+
log.warn("[bridge] 401 \u2014 bridge token rejected. Stopping poller. Set BRIDGE_TOKEN to the value Vercel has and restart.");
|
|
95
|
+
await this.stop();
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
const text = await resp.text().catch(() => "");
|
|
99
|
+
log.warn(`[bridge] claim HTTP ${resp.status}: ${text.slice(0, 120)}`);
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
const data = await resp.json();
|
|
103
|
+
jobs = data.jobs ?? [];
|
|
104
|
+
} catch (err) {
|
|
105
|
+
log.warn(`[bridge] claim failed: ${err.message}`);
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
if (jobs.length === 0) return;
|
|
109
|
+
await Promise.all(jobs.map((job) => this.runJob(job)));
|
|
110
|
+
}
|
|
111
|
+
async runJob(job) {
|
|
112
|
+
if (this.inFlight.has(job.id)) return;
|
|
113
|
+
this.inFlight.add(job.id);
|
|
114
|
+
log.info(`[bridge] running job ${job.id.slice(0, 8)} \xB7 ${job.cli}${job.model ? `:${job.model}` : ""}`);
|
|
115
|
+
const startedAt = Date.now();
|
|
116
|
+
try {
|
|
117
|
+
const { text, modelUsed, error } = await this.invokeCli(job.cli, job.model, job.prompt);
|
|
118
|
+
const latencyMs = Date.now() - startedAt;
|
|
119
|
+
await this.respond(job.id, { text, modelUsed, latencyMs, error });
|
|
120
|
+
if (error) {
|
|
121
|
+
log.warn(`[bridge] job ${job.id.slice(0, 8)} failed (${latencyMs}ms): ${error.slice(0, 120)}`);
|
|
122
|
+
} else {
|
|
123
|
+
log.success(`[bridge] job ${job.id.slice(0, 8)} done (${latencyMs}ms \xB7 ${modelUsed})`);
|
|
124
|
+
}
|
|
125
|
+
} catch (err) {
|
|
126
|
+
const latencyMs = Date.now() - startedAt;
|
|
127
|
+
await this.respond(job.id, { error: err.message, latencyMs });
|
|
128
|
+
} finally {
|
|
129
|
+
this.inFlight.delete(job.id);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
async invokeCli(cli, model, prompt) {
|
|
133
|
+
const candidates = cli === "codex" ? model && /^[a-z0-9.\-]+$/i.test(model) ? [model] : ["gpt-5.5", "gpt-5.4", "gpt-5.4-mini"] : model && /^[a-z0-9-]+$/i.test(model) ? [model] : ["opus", "sonnet"];
|
|
134
|
+
let lastErr = "";
|
|
135
|
+
for (const m of candidates) {
|
|
136
|
+
try {
|
|
137
|
+
const args = cli === "codex" ? ["exec", "-", "--model", m] : ["-p", "--model", m, "--no-session-persistence"];
|
|
138
|
+
const text = await runSpawn(cli, args, prompt, CLI_TIMEOUT_MS);
|
|
139
|
+
if (text.trim()) return { text, modelUsed: `${cli}:${m}` };
|
|
140
|
+
lastErr = `${cli}/${m} returned empty stdout`;
|
|
141
|
+
} catch (err) {
|
|
142
|
+
lastErr = `${cli}/${m}: ${err.message}`;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
return { error: lastErr || "no model produced output" };
|
|
146
|
+
}
|
|
147
|
+
async respond(jobId, body) {
|
|
148
|
+
try {
|
|
149
|
+
const resp = await fetch(`${this.baseUrl}/api/cli-bridge/respond`, {
|
|
150
|
+
method: "POST",
|
|
151
|
+
headers: {
|
|
152
|
+
Authorization: `Bearer ${this.token}`,
|
|
153
|
+
"Content-Type": "application/json"
|
|
154
|
+
},
|
|
155
|
+
body: JSON.stringify({ jobId, ...body }),
|
|
156
|
+
signal: AbortSignal.timeout(15e3)
|
|
157
|
+
});
|
|
158
|
+
if (!resp.ok) {
|
|
159
|
+
const text = await resp.text().catch(() => "");
|
|
160
|
+
log.warn(`[bridge] respond HTTP ${resp.status}: ${text.slice(0, 120)}`);
|
|
161
|
+
}
|
|
162
|
+
} catch (err) {
|
|
163
|
+
log.warn(`[bridge] respond failed for ${jobId.slice(0, 8)}: ${err.message}`);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
};
|
|
167
|
+
function runSpawn(cmd, args, stdin, timeoutMs) {
|
|
168
|
+
return new Promise((resolve, reject) => {
|
|
169
|
+
const proc = spawn(cmd, args, { stdio: ["pipe", "pipe", "pipe"] });
|
|
170
|
+
let stdout = "";
|
|
171
|
+
let stderr = "";
|
|
172
|
+
const timer = setTimeout(() => {
|
|
173
|
+
try {
|
|
174
|
+
proc.kill("SIGKILL");
|
|
175
|
+
} catch {
|
|
176
|
+
}
|
|
177
|
+
reject(new Error(`${cmd} timed out after ${timeoutMs}ms`));
|
|
178
|
+
}, timeoutMs);
|
|
179
|
+
proc.stdout.on("data", (d) => {
|
|
180
|
+
stdout += d.toString("utf-8");
|
|
181
|
+
});
|
|
182
|
+
proc.stderr.on("data", (d) => {
|
|
183
|
+
stderr += d.toString("utf-8");
|
|
184
|
+
});
|
|
185
|
+
proc.on("error", (err) => {
|
|
186
|
+
clearTimeout(timer);
|
|
187
|
+
reject(err);
|
|
188
|
+
});
|
|
189
|
+
proc.on("close", (code) => {
|
|
190
|
+
clearTimeout(timer);
|
|
191
|
+
if (code !== 0) reject(new Error(`${cmd} exit ${code}: ${stderr.slice(-300)}`));
|
|
192
|
+
else resolve(stdout);
|
|
193
|
+
});
|
|
194
|
+
proc.stdin.end(stdin);
|
|
195
|
+
});
|
|
196
|
+
}
|
|
197
|
+
export {
|
|
198
|
+
BridgePoller
|
|
199
|
+
};
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/utils/log.ts
|
|
4
|
+
var supportsColor = process.stdout.isTTY && process.env.TERM !== "dumb" && !process.env.NO_COLOR;
|
|
5
|
+
var c = supportsColor ? { blue: "\x1B[34m", green: "\x1B[32m", yellow: "\x1B[33m", red: "\x1B[31m", gray: "\x1B[90m", bold: "\x1B[1m", reset: "\x1B[0m" } : { blue: "", green: "", yellow: "", red: "", gray: "", bold: "", reset: "" };
|
|
6
|
+
var log = {
|
|
7
|
+
info: (msg) => console.log(c.blue + "i" + c.reset, msg),
|
|
8
|
+
success: (msg) => console.log(c.green + "\u2713" + c.reset, msg),
|
|
9
|
+
warn: (msg) => console.log(c.yellow + "!" + c.reset, msg),
|
|
10
|
+
error: (msg) => console.error(c.red + "\u2717" + c.reset, msg),
|
|
11
|
+
step: (n, total, msg) => console.log(c.gray + `[${n}/${total}]` + c.reset, msg),
|
|
12
|
+
banner: (msg) => console.log(c.bold + msg + c.reset)
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
export {
|
|
16
|
+
log
|
|
17
|
+
};
|
package/dist/index.js
CHANGED
|
@@ -1,4 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
log
|
|
4
|
+
} from "./chunk-GJQ45C5W.js";
|
|
2
5
|
|
|
3
6
|
// src/commands/dev.ts
|
|
4
7
|
import * as fs2 from "fs";
|
|
@@ -8,18 +11,6 @@ import * as http from "http";
|
|
|
8
11
|
import { execFile, exec as execCb, execSync, spawn } from "child_process";
|
|
9
12
|
import { promisify } from "util";
|
|
10
13
|
|
|
11
|
-
// src/utils/log.ts
|
|
12
|
-
var supportsColor = process.stdout.isTTY && process.env.TERM !== "dumb" && !process.env.NO_COLOR;
|
|
13
|
-
var c = supportsColor ? { blue: "\x1B[34m", green: "\x1B[32m", yellow: "\x1B[33m", red: "\x1B[31m", gray: "\x1B[90m", bold: "\x1B[1m", reset: "\x1B[0m" } : { blue: "", green: "", yellow: "", red: "", gray: "", bold: "", reset: "" };
|
|
14
|
-
var log = {
|
|
15
|
-
info: (msg) => console.log(c.blue + "i" + c.reset, msg),
|
|
16
|
-
success: (msg) => console.log(c.green + "\u2713" + c.reset, msg),
|
|
17
|
-
warn: (msg) => console.log(c.yellow + "!" + c.reset, msg),
|
|
18
|
-
error: (msg) => console.error(c.red + "\u2717" + c.reset, msg),
|
|
19
|
-
step: (n, total, msg) => console.log(c.gray + `[${n}/${total}]` + c.reset, msg),
|
|
20
|
-
banner: (msg) => console.log(c.bold + msg + c.reset)
|
|
21
|
-
};
|
|
22
|
-
|
|
23
14
|
// src/config.ts
|
|
24
15
|
import * as fs from "fs";
|
|
25
16
|
import * as path from "path";
|
|
@@ -589,7 +580,7 @@ function saveMeta(dir, meta) {
|
|
|
589
580
|
fs2.writeFileSync(metaPath(dir), JSON.stringify(meta, null, 2));
|
|
590
581
|
}
|
|
591
582
|
function detectFolderName(dir) {
|
|
592
|
-
return path2.basename(path2.resolve(dir)).replace(/[-_]/g, " ").replace(/\b\w/g, (
|
|
583
|
+
return path2.basename(path2.resolve(dir)).replace(/[-_]/g, " ").replace(/\b\w/g, (c) => c.toUpperCase());
|
|
593
584
|
}
|
|
594
585
|
function slugify(text) {
|
|
595
586
|
return text.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/(^-|-$)/g, "");
|
|
@@ -698,8 +689,19 @@ async function findOrCreateProject(dir) {
|
|
|
698
689
|
};
|
|
699
690
|
saveMeta(dir, meta);
|
|
700
691
|
log.success(`Saved ${metaPath(dir)}`);
|
|
692
|
+
ensureProjectAssetTree(dir, channel.slug, project.slug);
|
|
701
693
|
return meta;
|
|
702
694
|
}
|
|
695
|
+
function ensureProjectAssetTree(dir, channelSlug, projectSlug) {
|
|
696
|
+
const base = path2.join(dir, "channels", channelSlug, "projects", projectSlug);
|
|
697
|
+
const kinds = ["images", "audio", "clips", "scripts", "brand", "renders", "shorts", "gemini-scenes"];
|
|
698
|
+
for (const k of kinds) {
|
|
699
|
+
const p = path2.join(base, k);
|
|
700
|
+
fs2.mkdirSync(p, { recursive: true });
|
|
701
|
+
const keep = path2.join(p, ".gitkeep");
|
|
702
|
+
if (!fs2.existsSync(keep)) fs2.writeFileSync(keep, "");
|
|
703
|
+
}
|
|
704
|
+
}
|
|
703
705
|
function findMonorepoRoot(startDir) {
|
|
704
706
|
let d = startDir;
|
|
705
707
|
for (let i = 0; i < 6; i++) {
|
|
@@ -912,7 +914,7 @@ async function devCommand(options) {
|
|
|
912
914
|
return;
|
|
913
915
|
}
|
|
914
916
|
const projectComps = findProjectCompositions(dir);
|
|
915
|
-
const found = projectComps.find((
|
|
917
|
+
const found = projectComps.find((c) => c.name === name);
|
|
916
918
|
if (found) {
|
|
917
919
|
res.writeHead(200, { ...CORS_HEADERS, "Content-Type": "text/plain; charset=utf-8" });
|
|
918
920
|
res.end(fs2.readFileSync(found.absolutePath, "utf-8"));
|
|
@@ -982,7 +984,7 @@ async function devCommand(options) {
|
|
|
982
984
|
}
|
|
983
985
|
if (pathname === "/api/script-gen" && req.method === "POST") {
|
|
984
986
|
const bodyChunks = [];
|
|
985
|
-
req.on("data", (
|
|
987
|
+
req.on("data", (c) => bodyChunks.push(c));
|
|
986
988
|
req.on("end", async () => {
|
|
987
989
|
let body;
|
|
988
990
|
try {
|
|
@@ -1076,7 +1078,7 @@ async function devCommand(options) {
|
|
|
1076
1078
|
}));
|
|
1077
1079
|
return;
|
|
1078
1080
|
}
|
|
1079
|
-
const totalSec = chunks.reduce((s,
|
|
1081
|
+
const totalSec = chunks.reduce((s, c) => s + (c.durationEstimate ?? 0), 0);
|
|
1080
1082
|
log.success(`[script-gen] ${chunks.length} chunks \xB7 ~${(totalSec / 60).toFixed(1)} min via ${modelUsed}`);
|
|
1081
1083
|
res.writeHead(200, { ...CORS_HEADERS, "Content-Type": "application/json" });
|
|
1082
1084
|
res.end(JSON.stringify({
|
|
@@ -1096,7 +1098,7 @@ async function devCommand(options) {
|
|
|
1096
1098
|
}
|
|
1097
1099
|
if (pathname === "/api/ai-edit" && req.method === "POST") {
|
|
1098
1100
|
const bodyChunks = [];
|
|
1099
|
-
req.on("data", (
|
|
1101
|
+
req.on("data", (c) => bodyChunks.push(c));
|
|
1100
1102
|
req.on("end", async () => {
|
|
1101
1103
|
let parsed;
|
|
1102
1104
|
try {
|
|
@@ -1256,7 +1258,7 @@ Return ONLY the complete updated TSX. No markdown fences, no explanation.`;
|
|
|
1256
1258
|
}
|
|
1257
1259
|
if (pathname === "/api/chunk-compositions/append-images" && req.method === "POST") {
|
|
1258
1260
|
const bodyChunks = [];
|
|
1259
|
-
req.on("data", (
|
|
1261
|
+
req.on("data", (c) => bodyChunks.push(c));
|
|
1260
1262
|
req.on("end", () => {
|
|
1261
1263
|
try {
|
|
1262
1264
|
const { assemblyId, paths } = JSON.parse(Buffer.concat(bodyChunks).toString("utf-8"));
|
|
@@ -1267,7 +1269,7 @@ Return ONLY the complete updated TSX. No markdown fences, no explanation.`;
|
|
|
1267
1269
|
return;
|
|
1268
1270
|
}
|
|
1269
1271
|
const doc = JSON.parse(fs2.readFileSync(compPath, "utf-8"));
|
|
1270
|
-
const scene = doc.scenes?.find((
|
|
1272
|
+
const scene = doc.scenes?.find((c) => c.assemblyId === assemblyId);
|
|
1271
1273
|
if (!scene) {
|
|
1272
1274
|
res.writeHead(404, CORS_HEADERS);
|
|
1273
1275
|
res.end(JSON.stringify({ error: `scene ${assemblyId} not found` }));
|
|
@@ -1421,7 +1423,7 @@ Return ONLY the complete updated TSX. No markdown fences, no explanation.`;
|
|
|
1421
1423
|
const boundary = boundaryM[1];
|
|
1422
1424
|
const chunks = [];
|
|
1423
1425
|
await new Promise((resolve2, reject) => {
|
|
1424
|
-
req.on("data", (
|
|
1426
|
+
req.on("data", (c) => chunks.push(Buffer.from(c)));
|
|
1425
1427
|
req.on("end", resolve2);
|
|
1426
1428
|
req.on("error", reject);
|
|
1427
1429
|
});
|
|
@@ -1520,6 +1522,29 @@ Return ONLY the complete updated TSX. No markdown fences, no explanation.`;
|
|
|
1520
1522
|
console.log(` Script generation \u2192 ${scriptGenModel}`);
|
|
1521
1523
|
console.log(` POST http://localhost:${port}/api/script-gen`);
|
|
1522
1524
|
}
|
|
1525
|
+
const bridgeUrl = process.env.FORGE_BRIDGE_URL ?? WEB_URL;
|
|
1526
|
+
const bridgeToken = process.env.FORGE_BRIDGE_TOKEN ?? process.env.BRIDGE_TOKEN ?? "";
|
|
1527
|
+
if (bridgeToken) {
|
|
1528
|
+
const pkgVersion = (() => {
|
|
1529
|
+
try {
|
|
1530
|
+
const here = path2.dirname(new URL(import.meta.url).pathname);
|
|
1531
|
+
const pkgPath = path2.resolve(here, "..", "package.json");
|
|
1532
|
+
if (fs2.existsSync(pkgPath)) return JSON.parse(fs2.readFileSync(pkgPath, "utf-8")).version;
|
|
1533
|
+
} catch {
|
|
1534
|
+
}
|
|
1535
|
+
return "0.0.0";
|
|
1536
|
+
})();
|
|
1537
|
+
void (async () => {
|
|
1538
|
+
const { BridgePoller } = await import("./bridge-poller-5FFV2LML.js");
|
|
1539
|
+
const poller = new BridgePoller({ baseUrl: bridgeUrl, token: bridgeToken, clientVersion: `storyforge ${pkgVersion}` });
|
|
1540
|
+
poller.start();
|
|
1541
|
+
})();
|
|
1542
|
+
console.log("");
|
|
1543
|
+
console.log(` Bridge tunnel \u2192 ${bridgeUrl}/api/cli-bridge/* (outbound polling)`);
|
|
1544
|
+
} else {
|
|
1545
|
+
console.log("");
|
|
1546
|
+
console.log(" Bridge tunnel disabled \u2014 set FORGE_BRIDGE_TOKEN to expose your CLI seats to forge.algo-thinker.com");
|
|
1547
|
+
}
|
|
1523
1548
|
console.log("");
|
|
1524
1549
|
console.log(" Ctrl+C to stop");
|
|
1525
1550
|
console.log("");
|