storyforge 0.4.6 → 0.4.7
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 +34 -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, "");
|
|
@@ -912,7 +903,7 @@ async function devCommand(options) {
|
|
|
912
903
|
return;
|
|
913
904
|
}
|
|
914
905
|
const projectComps = findProjectCompositions(dir);
|
|
915
|
-
const found = projectComps.find((
|
|
906
|
+
const found = projectComps.find((c) => c.name === name);
|
|
916
907
|
if (found) {
|
|
917
908
|
res.writeHead(200, { ...CORS_HEADERS, "Content-Type": "text/plain; charset=utf-8" });
|
|
918
909
|
res.end(fs2.readFileSync(found.absolutePath, "utf-8"));
|
|
@@ -982,7 +973,7 @@ async function devCommand(options) {
|
|
|
982
973
|
}
|
|
983
974
|
if (pathname === "/api/script-gen" && req.method === "POST") {
|
|
984
975
|
const bodyChunks = [];
|
|
985
|
-
req.on("data", (
|
|
976
|
+
req.on("data", (c) => bodyChunks.push(c));
|
|
986
977
|
req.on("end", async () => {
|
|
987
978
|
let body;
|
|
988
979
|
try {
|
|
@@ -1076,7 +1067,7 @@ async function devCommand(options) {
|
|
|
1076
1067
|
}));
|
|
1077
1068
|
return;
|
|
1078
1069
|
}
|
|
1079
|
-
const totalSec = chunks.reduce((s,
|
|
1070
|
+
const totalSec = chunks.reduce((s, c) => s + (c.durationEstimate ?? 0), 0);
|
|
1080
1071
|
log.success(`[script-gen] ${chunks.length} chunks \xB7 ~${(totalSec / 60).toFixed(1)} min via ${modelUsed}`);
|
|
1081
1072
|
res.writeHead(200, { ...CORS_HEADERS, "Content-Type": "application/json" });
|
|
1082
1073
|
res.end(JSON.stringify({
|
|
@@ -1096,7 +1087,7 @@ async function devCommand(options) {
|
|
|
1096
1087
|
}
|
|
1097
1088
|
if (pathname === "/api/ai-edit" && req.method === "POST") {
|
|
1098
1089
|
const bodyChunks = [];
|
|
1099
|
-
req.on("data", (
|
|
1090
|
+
req.on("data", (c) => bodyChunks.push(c));
|
|
1100
1091
|
req.on("end", async () => {
|
|
1101
1092
|
let parsed;
|
|
1102
1093
|
try {
|
|
@@ -1256,7 +1247,7 @@ Return ONLY the complete updated TSX. No markdown fences, no explanation.`;
|
|
|
1256
1247
|
}
|
|
1257
1248
|
if (pathname === "/api/chunk-compositions/append-images" && req.method === "POST") {
|
|
1258
1249
|
const bodyChunks = [];
|
|
1259
|
-
req.on("data", (
|
|
1250
|
+
req.on("data", (c) => bodyChunks.push(c));
|
|
1260
1251
|
req.on("end", () => {
|
|
1261
1252
|
try {
|
|
1262
1253
|
const { assemblyId, paths } = JSON.parse(Buffer.concat(bodyChunks).toString("utf-8"));
|
|
@@ -1267,7 +1258,7 @@ Return ONLY the complete updated TSX. No markdown fences, no explanation.`;
|
|
|
1267
1258
|
return;
|
|
1268
1259
|
}
|
|
1269
1260
|
const doc = JSON.parse(fs2.readFileSync(compPath, "utf-8"));
|
|
1270
|
-
const scene = doc.scenes?.find((
|
|
1261
|
+
const scene = doc.scenes?.find((c) => c.assemblyId === assemblyId);
|
|
1271
1262
|
if (!scene) {
|
|
1272
1263
|
res.writeHead(404, CORS_HEADERS);
|
|
1273
1264
|
res.end(JSON.stringify({ error: `scene ${assemblyId} not found` }));
|
|
@@ -1421,7 +1412,7 @@ Return ONLY the complete updated TSX. No markdown fences, no explanation.`;
|
|
|
1421
1412
|
const boundary = boundaryM[1];
|
|
1422
1413
|
const chunks = [];
|
|
1423
1414
|
await new Promise((resolve2, reject) => {
|
|
1424
|
-
req.on("data", (
|
|
1415
|
+
req.on("data", (c) => chunks.push(Buffer.from(c)));
|
|
1425
1416
|
req.on("end", resolve2);
|
|
1426
1417
|
req.on("error", reject);
|
|
1427
1418
|
});
|
|
@@ -1520,6 +1511,29 @@ Return ONLY the complete updated TSX. No markdown fences, no explanation.`;
|
|
|
1520
1511
|
console.log(` Script generation \u2192 ${scriptGenModel}`);
|
|
1521
1512
|
console.log(` POST http://localhost:${port}/api/script-gen`);
|
|
1522
1513
|
}
|
|
1514
|
+
const bridgeUrl = process.env.FORGE_BRIDGE_URL ?? WEB_URL;
|
|
1515
|
+
const bridgeToken = process.env.FORGE_BRIDGE_TOKEN ?? process.env.BRIDGE_TOKEN ?? "";
|
|
1516
|
+
if (bridgeToken) {
|
|
1517
|
+
const pkgVersion = (() => {
|
|
1518
|
+
try {
|
|
1519
|
+
const here = path2.dirname(new URL(import.meta.url).pathname);
|
|
1520
|
+
const pkgPath = path2.resolve(here, "..", "package.json");
|
|
1521
|
+
if (fs2.existsSync(pkgPath)) return JSON.parse(fs2.readFileSync(pkgPath, "utf-8")).version;
|
|
1522
|
+
} catch {
|
|
1523
|
+
}
|
|
1524
|
+
return "0.0.0";
|
|
1525
|
+
})();
|
|
1526
|
+
void (async () => {
|
|
1527
|
+
const { BridgePoller } = await import("./bridge-poller-5FFV2LML.js");
|
|
1528
|
+
const poller = new BridgePoller({ baseUrl: bridgeUrl, token: bridgeToken, clientVersion: `storyforge ${pkgVersion}` });
|
|
1529
|
+
poller.start();
|
|
1530
|
+
})();
|
|
1531
|
+
console.log("");
|
|
1532
|
+
console.log(` Bridge tunnel \u2192 ${bridgeUrl}/api/cli-bridge/* (outbound polling)`);
|
|
1533
|
+
} else {
|
|
1534
|
+
console.log("");
|
|
1535
|
+
console.log(" Bridge tunnel disabled \u2014 set FORGE_BRIDGE_TOKEN to expose your CLI seats to forge.algo-thinker.com");
|
|
1536
|
+
}
|
|
1523
1537
|
console.log("");
|
|
1524
1538
|
console.log(" Ctrl+C to stop");
|
|
1525
1539
|
console.log("");
|