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.
@@ -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, (c2) => c2.toUpperCase());
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((c2) => c2.name === name);
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", (c2) => bodyChunks.push(c2));
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, c2) => s + (c2.durationEstimate ?? 0), 0);
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", (c2) => bodyChunks.push(c2));
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", (c2) => bodyChunks.push(c2));
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((c2) => c2.assemblyId === assemblyId);
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", (c2) => chunks.push(Buffer.from(c2)));
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("");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "storyforge",
3
- "version": "0.4.6",
3
+ "version": "0.4.7",
4
4
  "description": "StoryForge — local bridge for the Forge video production web app. Zero runtime dependencies.",
5
5
  "type": "module",
6
6
  "bin": {