storyforge 0.18.0 → 0.18.1

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 CHANGED
@@ -2,6 +2,10 @@
2
2
  import {
3
3
  log
4
4
  } from "./chunk-GJQ45C5W.js";
5
+ import {
6
+ concatWithExtras
7
+ } from "./chunk-7KBSDIP5.js";
8
+ import "./chunk-ZHECC3YI.js";
5
9
  import "./chunk-NSPRIPOP.js";
6
10
 
7
11
  // src/commands/dev.ts
@@ -9,9 +13,18 @@ import * as fs2 from "fs";
9
13
  import * as os2 from "os";
10
14
  import * as path2 from "path";
11
15
  import * as http from "http";
12
- import { execFile, exec as execCb, execSync, spawn } from "child_process";
16
+ import { exec as execCb, execSync, spawn } from "child_process";
13
17
  import { promisify } from "util";
14
18
 
19
+ // src/utils/open-browser.ts
20
+ import { execFile } from "child_process";
21
+ function openBrowser(url) {
22
+ const cmd = process.platform === "darwin" ? "open" : process.platform === "win32" ? "start" : "xdg-open";
23
+ execFile(cmd, [url], (err) => {
24
+ if (err) log.warn(`Could not open browser automatically. Open manually: ${url}`);
25
+ });
26
+ }
27
+
15
28
  // src/config.ts
16
29
  import * as fs from "fs";
17
30
  import * as path from "path";
@@ -24,13 +37,30 @@ var FORGE_ENV_PATHS = [
24
37
  path.join(os.homedir(), ".forge.env")
25
38
  // ~/.forge.env (legacy short form)
26
39
  ];
27
- function getForgeDir() {
28
- fs.mkdirSync(FORGE_DIR, { recursive: true });
29
- return FORGE_DIR;
40
+ function normalizeBridgeUrl(url) {
41
+ return url.replace(/\/+$/, "");
30
42
  }
31
- function saveCredentials(creds) {
32
- getForgeDir();
33
- fs.writeFileSync(CREDS_PATH, JSON.stringify(creds, null, 2));
43
+ function readCredsFile(credsPath) {
44
+ try {
45
+ const parsed = JSON.parse(fs.readFileSync(credsPath, "utf-8"));
46
+ return parsed && typeof parsed === "object" ? parsed : {};
47
+ } catch {
48
+ return {};
49
+ }
50
+ }
51
+ function readLinks(file) {
52
+ const raw = file.links;
53
+ if (!Array.isArray(raw)) return [];
54
+ return raw.filter(
55
+ (l) => !!l && typeof l === "object" && typeof l.url === "string" && typeof l.workerToken === "string" && typeof l.label === "string"
56
+ );
57
+ }
58
+ function saveCredentials(creds, credsPath = CREDS_PATH) {
59
+ fs.mkdirSync(path.dirname(credsPath), { recursive: true });
60
+ const links = readLinks(readCredsFile(credsPath));
61
+ const out = { ...creds };
62
+ if (links.length > 0) out.links = links;
63
+ fs.writeFileSync(credsPath, JSON.stringify(out, null, 2));
34
64
  }
35
65
  function loadCredentials() {
36
66
  try {
@@ -39,6 +69,35 @@ function loadCredentials() {
39
69
  return null;
40
70
  }
41
71
  }
72
+ function saveLink(entry, credsPath = CREDS_PATH) {
73
+ fs.mkdirSync(path.dirname(credsPath), { recursive: true });
74
+ const file = readCredsFile(credsPath);
75
+ const url = normalizeBridgeUrl(entry.url);
76
+ const links = readLinks(file).filter((l) => normalizeBridgeUrl(l.url) !== url);
77
+ links.push({ url, workerToken: entry.workerToken, label: entry.label });
78
+ file.links = links;
79
+ fs.writeFileSync(credsPath, JSON.stringify(file, null, 2), { mode: 384 });
80
+ try {
81
+ fs.chmodSync(credsPath, 384);
82
+ } catch {
83
+ }
84
+ }
85
+ function findLink(url, credsPath = CREDS_PATH) {
86
+ const target = normalizeBridgeUrl(url);
87
+ return readLinks(readCredsFile(credsPath)).find((l) => normalizeBridgeUrl(l.url) === target) ?? null;
88
+ }
89
+ function resolveBridgeToken(opts) {
90
+ const env = opts.env ?? process.env;
91
+ const envToken = env.FORGE_BRIDGE_TOKEN ?? env.BRIDGE_TOKEN;
92
+ if (envToken) return { token: envToken, source: "env" };
93
+ const link = findLink(opts.url, opts.credsPath);
94
+ if (link?.workerToken) return { token: link.workerToken, source: "link", label: link.label };
95
+ return null;
96
+ }
97
+ function resolveBridgeTokenForCommand(opts) {
98
+ if (opts.explicit) return opts.explicit;
99
+ return resolveBridgeToken({ url: opts.url, env: opts.env, credsPath: opts.credsPath })?.token ?? null;
100
+ }
42
101
  function findEnvFilesWalkingUp(startDir) {
43
102
  const found = [];
44
103
  const home = os.homedir();
@@ -886,12 +945,6 @@ function collectFiles(dir, base = "") {
886
945
  function stripFences(text) {
887
946
  return text.replace(/^```(?:tsx?|jsx?|typescript|javascript)?\n?/m, "").replace(/\n?```\s*$/m, "").trim();
888
947
  }
889
- function openBrowser(url) {
890
- const cmd = process.platform === "darwin" ? "open" : process.platform === "win32" ? "start" : "xdg-open";
891
- execFile(cmd, [url], (err) => {
892
- if (err) log.warn(`Could not open browser automatically. Open manually: ${url}`);
893
- });
894
- }
895
948
  async function devCommand(options) {
896
949
  const env = loadDotEnv();
897
950
  const port = parseInt(options.port || String(PORT), 10);
@@ -901,10 +954,10 @@ async function devCommand(options) {
901
954
  if (env.loaded.length > 0) {
902
955
  log.info(`Loaded env: ${env.loaded.join(", ")} from ${env.sources.join(", ")}`);
903
956
  }
904
- const bridgeToken = process.env.FORGE_BRIDGE_TOKEN ?? process.env.BRIDGE_TOKEN ?? "";
905
- if (bridgeToken) {
906
- const apiUrl = process.env.FORGE_BRIDGE_URL ?? WEB_URL;
907
- const credResult = await fetchBridgeCredentials({ apiUrl, token: bridgeToken });
957
+ const bridgeApiUrl = process.env.FORGE_BRIDGE_URL ?? WEB_URL;
958
+ const bridge = resolveBridgeToken({ url: bridgeApiUrl });
959
+ if (bridge) {
960
+ const credResult = await fetchBridgeCredentials({ apiUrl: bridgeApiUrl, token: bridge.token });
908
961
  if (credResult.error) {
909
962
  log.info(`Bridge cred fetch skipped: ${credResult.error}`);
910
963
  } else if (credResult.loaded.length > 0) {
@@ -1676,9 +1729,7 @@ Return ONLY the complete updated TSX. No markdown fences, no explanation.`;
1676
1729
  console.log(` Script generation \u2192 ${scriptGenModel}`);
1677
1730
  console.log(` POST http://localhost:${port}/api/script-gen`);
1678
1731
  }
1679
- const bridgeUrl = process.env.FORGE_BRIDGE_URL ?? WEB_URL;
1680
- const bridgeToken2 = process.env.FORGE_BRIDGE_TOKEN ?? process.env.BRIDGE_TOKEN ?? "";
1681
- if (bridgeToken2) {
1732
+ if (bridge) {
1682
1733
  const pkgVersion = (() => {
1683
1734
  try {
1684
1735
  const here = path2.dirname(new URL(import.meta.url).pathname);
@@ -1689,20 +1740,21 @@ Return ONLY the complete updated TSX. No markdown fences, no explanation.`;
1689
1740
  return "0.0.0";
1690
1741
  })();
1691
1742
  void (async () => {
1692
- const { BridgePoller } = await import("./bridge-poller-WSSTXFPC.js");
1693
- const poller = new BridgePoller({ baseUrl: bridgeUrl, token: bridgeToken2, clientVersion: `storyforge ${pkgVersion}` });
1743
+ const { BridgePoller } = await import("./bridge-poller-67VQZZ3U.js");
1744
+ const poller = new BridgePoller({ baseUrl: bridgeApiUrl, token: bridge.token, clientVersion: `storyforge ${pkgVersion}` });
1694
1745
  poller.start();
1695
1746
  })();
1696
1747
  console.log("");
1697
- console.log(` Bridge tunnel \u2192 ${bridgeUrl}/api/cli-bridge/* (outbound polling)`);
1748
+ const via = bridge.source === "link" ? ` (linked as ${bridge.label})` : "";
1749
+ console.log(` Bridge tunnel \u2192 ${bridgeApiUrl}/api/cli-bridge/* (outbound polling)${via}`);
1698
1750
  } else {
1699
1751
  console.log("");
1700
- console.log(" Bridge tunnel disabled \u2014 set FORGE_BRIDGE_TOKEN to expose your CLI seats.");
1701
- console.log(" Vercel env vars do NOT reach this laptop. Pick ONE of these:");
1752
+ log.warn("Bridge tunnel disabled \u2014 no worker token for this deployment.");
1753
+ console.log(` Run storyforge link to pair this machine with ${bridgeApiUrl} (recommended).`);
1754
+ console.log(" Or provide FORGE_BRIDGE_TOKEN yourself:");
1702
1755
  console.log(" a) export FORGE_BRIDGE_TOKEN=xxx in your shell, then re-run storyforge");
1703
1756
  console.log(" b) drop FORGE_BRIDGE_TOKEN=xxx into ./.env (cwd) or ~/.forge/.env");
1704
1757
  console.log(" c) one-shot: FORGE_BRIDGE_TOKEN=xxx npx storyforge");
1705
- console.log(` Use the SAME token value you set on Vercel as FORGE_BRIDGE_TOKEN.`);
1706
1758
  }
1707
1759
  console.log("");
1708
1760
  console.log(" Ctrl+C to stop");
@@ -1753,17 +1805,146 @@ async function loginCommand() {
1753
1805
  }
1754
1806
  }
1755
1807
 
1808
+ // src/commands/link.ts
1809
+ import * as os3 from "os";
1810
+
1811
+ // package.json
1812
+ var package_default = {
1813
+ name: "storyforge",
1814
+ version: "0.18.1",
1815
+ description: "StoryForge \u2014 local bridge for the Forge video production web app. Parallel clip-render orchestrator (Remotion 4 + Manim + HyperFrames + ffmpeg) + final video stitcher + dependency doctor.",
1816
+ type: "module",
1817
+ bin: {
1818
+ storyforge: "./dist/index.js"
1819
+ },
1820
+ files: [
1821
+ "dist/",
1822
+ "README.md"
1823
+ ],
1824
+ scripts: {
1825
+ build: "tsup",
1826
+ lint: "tsc --noEmit",
1827
+ test: "vitest run",
1828
+ prepublishOnly: "npm run build"
1829
+ },
1830
+ dependencies: {
1831
+ "@ccusage/codex": "18.0.11",
1832
+ "@mozilla/readability": "0.6.0",
1833
+ ccusage: "18.0.11",
1834
+ jsdom: "29.1.1",
1835
+ "react-tweet": "3.3.0"
1836
+ },
1837
+ devDependencies: {
1838
+ tsup: "8.5.0",
1839
+ typescript: "5.8.3"
1840
+ },
1841
+ publishConfig: {
1842
+ access: "public"
1843
+ },
1844
+ keywords: [
1845
+ "video",
1846
+ "remotion",
1847
+ "storyforge",
1848
+ "cli",
1849
+ "forge"
1850
+ ],
1851
+ license: "MIT",
1852
+ engines: {
1853
+ node: ">=18.0.0"
1854
+ },
1855
+ repository: {
1856
+ type: "git",
1857
+ url: "git+https://github.com/backspacevenkat/forge.git",
1858
+ directory: "packages/cli"
1859
+ }
1860
+ };
1861
+
1862
+ // src/commands/link.ts
1863
+ var DEFAULT_BRIDGE_URL = "https://forge.algo-thinker.com";
1864
+ var sleep = (ms) => new Promise((r) => setTimeout(r, ms));
1865
+ async function linkCommand(opts = {}) {
1866
+ const baseUrl = normalizeBridgeUrl(opts.baseUrl ?? process.env.FORGE_BRIDGE_URL ?? DEFAULT_BRIDGE_URL);
1867
+ log.banner("\nStoryForge \u2014 Link this machine\n");
1868
+ let start;
1869
+ try {
1870
+ const resp = await fetch(`${baseUrl}/api/cli-bridge/link/start`, {
1871
+ method: "POST",
1872
+ headers: { "Content-Type": "application/json", Accept: "application/json" },
1873
+ body: JSON.stringify({ cliVersion: package_default.version, host: os3.hostname() })
1874
+ });
1875
+ if (!resp.ok) {
1876
+ const text = await resp.text().catch(() => "");
1877
+ log.error(`Could not start link (${resp.status}) at ${baseUrl}: ${text.slice(0, 200)}`);
1878
+ process.exit(1);
1879
+ }
1880
+ start = await resp.json();
1881
+ } catch (err) {
1882
+ log.error(`Could not reach ${baseUrl}: ${err.message}`);
1883
+ process.exit(1);
1884
+ }
1885
+ console.log(` Verification code: ${start.code}`);
1886
+ console.log(` Approve at: ${start.approveUrl}`);
1887
+ console.log("");
1888
+ console.log(" Check that the code above matches the one shown in your browser,");
1889
+ console.log(" then approve. Waiting for approval\u2026");
1890
+ console.log("");
1891
+ if (!opts.noOpen) openBrowser(start.approveUrl);
1892
+ const pollUrl = `${baseUrl}/api/cli-bridge/link/poll?linkId=${encodeURIComponent(start.linkId)}`;
1893
+ const intervalMs = Math.max(1e3, start.pollIntervalMs || 2e3);
1894
+ const expiresAtMs = Date.parse(start.expiresAt);
1895
+ const deadline = Number.isFinite(expiresAtMs) ? expiresAtMs : Date.now() + 10 * 60 * 1e3;
1896
+ while (Date.now() < deadline) {
1897
+ await sleep(intervalMs);
1898
+ let body;
1899
+ try {
1900
+ const resp = await fetch(pollUrl, { headers: { Accept: "application/json" } });
1901
+ if (!resp.ok) continue;
1902
+ body = await resp.json();
1903
+ } catch {
1904
+ continue;
1905
+ }
1906
+ switch (body.status) {
1907
+ case "pending":
1908
+ continue;
1909
+ case "approved":
1910
+ if (!body.workerToken || !body.label) {
1911
+ log.error("Approval response was missing the worker token. Run `storyforge link` again.");
1912
+ process.exit(1);
1913
+ }
1914
+ saveLink({ url: baseUrl, workerToken: body.workerToken, label: body.label });
1915
+ log.success(`Linked as ${body.label}`);
1916
+ return;
1917
+ case "denied":
1918
+ log.error("Link request was denied in the browser.");
1919
+ process.exit(1);
1920
+ return;
1921
+ case "consumed":
1922
+ log.error("Link request was already used. Run `storyforge link` again.");
1923
+ process.exit(1);
1924
+ return;
1925
+ case "expired":
1926
+ log.error("Link request expired, run `storyforge link` again.");
1927
+ process.exit(1);
1928
+ return;
1929
+ default:
1930
+ continue;
1931
+ }
1932
+ }
1933
+ log.error("Link request expired, run `storyforge link` again.");
1934
+ process.exit(1);
1935
+ }
1936
+
1756
1937
  // src/commands/doctor.ts
1757
1938
  import { spawn as spawn2 } from "child_process";
1758
1939
  import { promisify as promisify2 } from "util";
1759
1940
  import { execFile as execFileCb } from "child_process";
1760
- import os3 from "os";
1941
+ import os4 from "os";
1761
1942
  var execFile2 = promisify2(execFileCb);
1762
1943
  async function runDoctor() {
1763
- const platform = os3.platform();
1764
- const arch = os3.arch();
1765
- const cores = os3.cpus().length;
1766
- const ramGb = Math.round(os3.totalmem() / 1024 ** 3);
1944
+ const platform = os4.platform();
1945
+ const arch = os4.arch();
1946
+ const cores = os4.cpus().length;
1947
+ const ramGb = Math.round(os4.totalmem() / 1024 ** 3);
1767
1948
  const renderers = await Promise.all([
1768
1949
  checkFfmpeg(platform),
1769
1950
  checkPython(),
@@ -1834,7 +2015,7 @@ async function checkPython() {
1834
2015
  installed: probe.installed,
1835
2016
  version: probe.version,
1836
2017
  rationale: "manim engine runtime",
1837
- installCommand: os3.platform() === "darwin" ? "brew install python@3.12" : "sudo apt install -y python3",
2018
+ installCommand: os4.platform() === "darwin" ? "brew install python@3.12" : "sudo apt install -y python3",
1838
2019
  required: false
1839
2020
  };
1840
2021
  }
@@ -1936,7 +2117,7 @@ async function doctorCommand() {
1936
2117
  import { spawn as spawn3 } from "child_process";
1937
2118
  import readline2 from "readline/promises";
1938
2119
  import { stdin as input, stdout as output } from "process";
1939
- import os4 from "os";
2120
+ import os5 from "os";
1940
2121
  function planInstall(missing, platform) {
1941
2122
  const steps = [];
1942
2123
  for (const r of missing) {
@@ -2066,15 +2247,13 @@ async function installRenderersCommand(opts = {}) {
2066
2247
  }
2067
2248
 
2068
2249
  // src/commands/render.ts
2069
- import { spawn as spawn4 } from "child_process";
2070
2250
  import * as fs5 from "fs";
2071
- import * as os6 from "os";
2072
2251
  import * as path5 from "path";
2073
2252
 
2074
2253
  // src/lib/manifest.ts
2075
2254
  import * as fs3 from "fs";
2076
2255
  import * as path3 from "path";
2077
- import * as os5 from "os";
2256
+ import * as os6 from "os";
2078
2257
  var DEFAULT_BASE_URL = "https://forge.algo-thinker.com";
2079
2258
  function localCacheRelPath(artifact) {
2080
2259
  if (artifact.kind === "clips" && artifact.aspect) {
@@ -2087,7 +2266,7 @@ function resolveCacheRoot(slug, override) {
2087
2266
  if (override) return path3.resolve(override, slug);
2088
2267
  const envOverride = process.env.FORGE_CACHE_DIR;
2089
2268
  if (envOverride) return path3.resolve(envOverride, slug);
2090
- return path3.join(os5.homedir(), ".forge", "cache", slug);
2269
+ return path3.join(os6.homedir(), ".forge", "cache", slug);
2091
2270
  }
2092
2271
  function resolveBaseUrl(override) {
2093
2272
  return (override ?? process.env.FORGE_BASE_URL ?? DEFAULT_BASE_URL).replace(/\/$/, "");
@@ -2206,6 +2385,16 @@ async function downloadFile(url, absPath, signal) {
2206
2385
  function clipsInOrder(ready, aspect) {
2207
2386
  return ready.filter(({ artifact }) => artifact.kind === "clips" && (artifact.aspect ?? "16:9") === aspect).map(({ absPath }) => absPath);
2208
2387
  }
2388
+ function brandExtrasFromPull(ready) {
2389
+ let watermarkPath;
2390
+ let brandAudioPath;
2391
+ for (const { artifact, absPath } of ready) {
2392
+ if (artifact.kind !== "brand") continue;
2393
+ if (artifact.brandSlot === "watermark") watermarkPath = absPath;
2394
+ else if (artifact.brandSlot === "brand_audio") brandAudioPath = absPath;
2395
+ }
2396
+ return { watermarkPath, brandAudioPath };
2397
+ }
2209
2398
  function formatPullSummary(result) {
2210
2399
  const parts = [
2211
2400
  `${result.fresh} cached`,
@@ -2228,8 +2417,11 @@ async function renderCommand(opts) {
2228
2417
  const outPath = path5.resolve(opts.out ?? "out.mp4");
2229
2418
  log.info(`render \xB7 slug=${opts.slug} \xB7 aspect=${aspect}`);
2230
2419
  log.info(`cache \xB7 ${cacheRoot}`);
2420
+ loadDotEnv();
2421
+ const baseUrl = resolveBaseUrl(opts.baseUrl);
2422
+ const token = resolveBridgeTokenForCommand({ url: baseUrl, explicit: opts.token }) ?? void 0;
2231
2423
  log.info(`fetching manifest\u2026`);
2232
- const manifest = await fetchManifest(opts.slug, { baseUrl: opts.baseUrl, token: opts.token });
2424
+ const manifest = await fetchManifest(opts.slug, { baseUrl, token });
2233
2425
  log.info(`manifest \xB7 ${manifest.artifacts.length} artifact(s) \xB7 updated ${manifest.updatedAt}`);
2234
2426
  if (opts.clean && fs5.existsSync(cacheRoot)) {
2235
2427
  log.info(`--clean: wiping cache dir`);
@@ -2254,41 +2446,23 @@ async function renderCommand(opts) {
2254
2446
  if (clipPaths.length === 0) {
2255
2447
  throw new Error(`no clips for aspect ${aspect} \u2014 manifest may be empty or all pulls failed`);
2256
2448
  }
2257
- log.info(`stitching ${clipPaths.length} clip(s) \u2192 ${outPath}`);
2258
- await ffmpegConcat(clipPaths, outPath);
2449
+ const brand = brandExtrasFromPull(pullResult.ready);
2450
+ const brandNote = [
2451
+ brand.watermarkPath ? "watermark" : null,
2452
+ brand.brandAudioPath ? "audio bed" : null
2453
+ ].filter(Boolean).join(" + ");
2454
+ log.info(`stitching ${clipPaths.length} clip(s)${brandNote ? ` + brand (${brandNote})` : ""} \u2192 ${outPath}`);
2455
+ await ffmpegConcat(clipPaths, outPath, brand);
2259
2456
  const finalStat = fs5.statSync(outPath);
2260
2457
  log.success(`done \xB7 ${outPath} (${(finalStat.size / 1024 / 1024).toFixed(1)} MB)`);
2261
2458
  }
2262
- async function ffmpegConcat(inputs, outPath) {
2263
- const tmpDir = fs5.mkdtempSync(path5.join(os6.tmpdir(), "storyforge-render-"));
2264
- const listPath = path5.join(tmpDir, "concat.txt");
2265
- const listBody = inputs.map((p) => `file '${p.replace(/'/g, "'\\''")}'`).join("\n") + "\n";
2266
- fs5.writeFileSync(listPath, listBody);
2459
+ async function ffmpegConcat(inputs, outPath, brand = {}) {
2267
2460
  fs5.mkdirSync(path5.dirname(outPath), { recursive: true });
2268
- await new Promise((resolve6, reject) => {
2269
- const proc = spawn4("ffmpeg", [
2270
- "-y",
2271
- "-f",
2272
- "concat",
2273
- "-safe",
2274
- "0",
2275
- "-i",
2276
- listPath,
2277
- "-c",
2278
- "copy",
2279
- "-movflags",
2280
- "+faststart",
2281
- outPath
2282
- ], { stdio: ["ignore", "inherit", "inherit"] });
2283
- proc.on("error", reject);
2284
- proc.on("close", (code) => {
2285
- try {
2286
- fs5.rmSync(tmpDir, { recursive: true, force: true });
2287
- } catch {
2288
- }
2289
- if (code === 0) resolve6();
2290
- else reject(new Error(`ffmpeg exited ${code}`));
2291
- });
2461
+ await concatWithExtras({
2462
+ segmentPaths: inputs,
2463
+ outPath,
2464
+ watermarkPath: brand.watermarkPath,
2465
+ brandAudioPath: brand.brandAudioPath
2292
2466
  });
2293
2467
  }
2294
2468
 
@@ -2297,13 +2471,14 @@ import * as fs6 from "fs";
2297
2471
  import * as path6 from "path";
2298
2472
  import * as readline3 from "readline";
2299
2473
  async function pushCommand(opts) {
2300
- const token = resolveBridgeToken(opts.token);
2474
+ loadDotEnv();
2475
+ const baseUrl = resolveBaseUrl(opts.baseUrl);
2476
+ const token = resolveBridgeTokenForCommand({ url: baseUrl, explicit: opts.token });
2301
2477
  if (!token) {
2302
2478
  throw new Error(
2303
- "push requires a bridge token. Run `storyforge login`, set BRIDGE_TOKEN, or pass --token <bearer>."
2479
+ "push requires a bridge token. Run `storyforge link`, set BRIDGE_TOKEN, or pass --token <bearer>."
2304
2480
  );
2305
2481
  }
2306
- const baseUrl = resolveBaseUrl(opts.baseUrl);
2307
2482
  const cacheRoot = resolveCacheRoot(opts.slug, opts.cache);
2308
2483
  log.info(`push \xB7 slug=${opts.slug}`);
2309
2484
  log.info(`cache \xB7 ${cacheRoot}`);
@@ -2421,13 +2596,6 @@ async function postFinalize(slug, baseUrl, token, args) {
2421
2596
  if (!resp.ok) throw new Error(`finalize HTTP ${resp.status}: ${text.slice(0, 300)}`);
2422
2597
  return JSON.parse(text);
2423
2598
  }
2424
- function resolveBridgeToken(explicit) {
2425
- if (explicit) return explicit;
2426
- if (process.env.FORGE_BRIDGE_TOKEN) return process.env.FORGE_BRIDGE_TOKEN;
2427
- if (process.env.BRIDGE_TOKEN) return process.env.BRIDGE_TOKEN;
2428
- loadDotEnv();
2429
- return process.env.FORGE_BRIDGE_TOKEN ?? process.env.BRIDGE_TOKEN ?? null;
2430
- }
2431
2599
  function humanBytes(n) {
2432
2600
  if (n < 1024) return `${n}B`;
2433
2601
  if (n < 1024 * 1024) return `${(n / 1024).toFixed(1)}K`;
@@ -2445,13 +2613,14 @@ import * as fs7 from "fs";
2445
2613
  import * as path7 from "path";
2446
2614
  var ALLOWED_EXTS = /* @__PURE__ */ new Set([".mp4", ".webm", ".mov"]);
2447
2615
  async function pushHookCommand(opts) {
2448
- const token = resolveBridgeToken2(opts.token);
2616
+ loadDotEnv();
2617
+ const baseUrl = resolveBaseUrl(opts.baseUrl);
2618
+ const token = resolveBridgeTokenForCommand({ url: baseUrl, explicit: opts.token });
2449
2619
  if (!token) {
2450
2620
  throw new Error(
2451
- "push-hook requires a bridge token. Run `storyforge login`, set BRIDGE_TOKEN, or pass --token <bearer>."
2621
+ "push-hook requires a bridge token. Run `storyforge link`, set BRIDGE_TOKEN, or pass --token <bearer>."
2452
2622
  );
2453
2623
  }
2454
- const baseUrl = resolveBaseUrl(opts.baseUrl);
2455
2624
  if (opts.files.length === 0) throw new Error("no files to upload");
2456
2625
  log.info(`push-hook \xB7 slug=${opts.slug}`);
2457
2626
  log.info(`api \xB7 ${baseUrl}`);
@@ -2501,63 +2670,6 @@ async function pushHookCommand(opts) {
2501
2670
  throw new Error("all hooks failed to upload");
2502
2671
  }
2503
2672
  }
2504
- function resolveBridgeToken2(explicit) {
2505
- if (explicit) return explicit;
2506
- if (process.env.FORGE_BRIDGE_TOKEN) return process.env.FORGE_BRIDGE_TOKEN;
2507
- if (process.env.BRIDGE_TOKEN) return process.env.BRIDGE_TOKEN;
2508
- loadDotEnv();
2509
- return process.env.FORGE_BRIDGE_TOKEN ?? process.env.BRIDGE_TOKEN ?? null;
2510
- }
2511
-
2512
- // package.json
2513
- var package_default = {
2514
- name: "storyforge",
2515
- version: "0.18.0",
2516
- description: "StoryForge \u2014 local bridge for the Forge video production web app. Parallel clip-render orchestrator (Remotion 4 + Manim + HyperFrames + ffmpeg) + final video stitcher + dependency doctor.",
2517
- type: "module",
2518
- bin: {
2519
- storyforge: "./dist/index.js"
2520
- },
2521
- files: [
2522
- "dist/",
2523
- "README.md"
2524
- ],
2525
- scripts: {
2526
- build: "tsup",
2527
- lint: "tsc --noEmit",
2528
- prepublishOnly: "npm run build"
2529
- },
2530
- dependencies: {
2531
- "@ccusage/codex": "18.0.11",
2532
- "@mozilla/readability": "0.6.0",
2533
- ccusage: "18.0.11",
2534
- jsdom: "29.1.1",
2535
- "react-tweet": "3.3.0"
2536
- },
2537
- devDependencies: {
2538
- tsup: "8.5.0",
2539
- typescript: "5.8.3"
2540
- },
2541
- publishConfig: {
2542
- access: "public"
2543
- },
2544
- keywords: [
2545
- "video",
2546
- "remotion",
2547
- "storyforge",
2548
- "cli",
2549
- "forge"
2550
- ],
2551
- license: "MIT",
2552
- engines: {
2553
- node: ">=18.0.0"
2554
- },
2555
- repository: {
2556
- type: "git",
2557
- url: "git+https://github.com/backspacevenkat/forge.git",
2558
- directory: "packages/cli"
2559
- }
2560
- };
2561
2673
 
2562
2674
  // src/index.ts
2563
2675
  var VERSION = package_default.version;
@@ -2571,6 +2683,7 @@ Usage:
2571
2683
  storyforge push-hook <slug> <file> [<file>...]
2572
2684
  Upload standalone hook MP4(s) to a project on Storj
2573
2685
  storyforge login Log in to forge.algo-thinker.com
2686
+ storyforge link Pair this machine with a Forge deployment (mints a bridge worker token)
2574
2687
  storyforge doctor Probe local renderers (ffmpeg / python / manim / hyperframes)
2575
2688
  storyforge install-renderers Install missing renderers (interactive, asks per-tool)
2576
2689
  storyforge install-renderers -y Same, but auto-confirm every prompt
@@ -2636,6 +2749,14 @@ async function main() {
2636
2749
  await loginCommand();
2637
2750
  return;
2638
2751
  }
2752
+ if (firstArg === "link") {
2753
+ const opts2 = parseArgs(args.slice(1));
2754
+ await linkCommand({
2755
+ baseUrl: typeof opts2["base-url"] === "string" ? opts2["base-url"] : void 0,
2756
+ noOpen: opts2.open === false
2757
+ });
2758
+ return;
2759
+ }
2639
2760
  if (firstArg === "doctor") {
2640
2761
  await doctorCommand();
2641
2762
  return;
@@ -0,0 +1,38 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ LEGACY_SECTION_TRANSITION_ROOT,
4
+ buildFinalEncodeContract,
5
+ channelPackageProvenance,
6
+ concatWithExtras,
7
+ formatSrtTime,
8
+ normalizeClip,
9
+ renderCrossfadeFallback,
10
+ renderSectionTransition,
11
+ resolutionFor,
12
+ resolveChannelPackage,
13
+ srtFromTimings,
14
+ stitchFinalVideo
15
+ } from "./chunk-7KBSDIP5.js";
16
+ import {
17
+ FINAL_LOUDNORM_FILTER,
18
+ buildFinalConcatArgs,
19
+ buildFinalEncodeArgs
20
+ } from "./chunk-ZHECC3YI.js";
21
+ import "./chunk-NSPRIPOP.js";
22
+ export {
23
+ FINAL_LOUDNORM_FILTER,
24
+ LEGACY_SECTION_TRANSITION_ROOT,
25
+ buildFinalConcatArgs,
26
+ buildFinalEncodeArgs,
27
+ buildFinalEncodeContract,
28
+ channelPackageProvenance,
29
+ concatWithExtras,
30
+ formatSrtTime,
31
+ normalizeClip,
32
+ renderCrossfadeFallback,
33
+ renderSectionTransition,
34
+ resolutionFor,
35
+ resolveChannelPackage,
36
+ srtFromTimings,
37
+ stitchFinalVideo
38
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "storyforge",
3
- "version": "0.18.0",
3
+ "version": "0.18.1",
4
4
  "description": "StoryForge — local bridge for the Forge video production web app. Parallel clip-render orchestrator (Remotion 4 + Manim + HyperFrames + ffmpeg) + final video stitcher + dependency doctor.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -13,6 +13,7 @@
13
13
  "scripts": {
14
14
  "build": "tsup",
15
15
  "lint": "tsc --noEmit",
16
+ "test": "vitest run",
16
17
  "prepublishOnly": "npm run build"
17
18
  },
18
19
  "dependencies": {