@amg555/frameforge-mcp 2.3.1 → 2.3.3

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.
Files changed (52) hide show
  1. package/.mcp.json +2 -4
  2. package/CHANGELOG.md +20 -1
  3. package/CLAUDE.md +154 -18
  4. package/README.md +16 -12
  5. package/SECURITY.md +1 -3
  6. package/install.sh +1 -1
  7. package/package.json +7 -3
  8. package/plugins/video-pipeline/.claude-plugin/plugin.json +19 -0
  9. package/plugins/video-pipeline/skills/html-renderer/SKILL.md +292 -0
  10. package/plugins/video-pipeline/skills/video-pipeline/SKILL.md +308 -0
  11. package/plugins/video-pipeline/skills/video-pipeline/assets/project/ANIMATION_GUIDE.md +257 -0
  12. package/plugins/video-pipeline/skills/video-pipeline/assets/project/STORYBOARD.md +77 -0
  13. package/plugins/video-pipeline/skills/video-pipeline/assets/project/index.html +80 -0
  14. package/plugins/video-pipeline/skills/video-pipeline/assets/project/pipeline/briefings/diagram-author.md +43 -0
  15. package/plugins/video-pipeline/skills/video-pipeline/assets/project/pipeline/briefings/orchestrator.md +56 -0
  16. package/plugins/video-pipeline/skills/video-pipeline/assets/project/pipeline/briefings/scene-author.md +42 -0
  17. package/plugins/video-pipeline/skills/video-pipeline/assets/project/reel/engine.css +140 -0
  18. package/plugins/video-pipeline/skills/video-pipeline/assets/project/reel/engine.js +870 -0
  19. package/plugins/video-pipeline/skills/video-pipeline/assets/project/reel/virtual-clock.js +138 -0
  20. package/plugins/video-pipeline/skills/video-pipeline/references/mcp-tools.md +167 -0
  21. package/plugins/video-pipeline/skills/video-pipeline/references/providers.md +163 -0
  22. package/scripts/db-migrate.js +112 -0
  23. package/scripts/deep-validate.js +326 -0
  24. package/scripts/generate-initial-assets.js +342 -0
  25. package/scripts/html-renderer/new-project.mjs +408 -0
  26. package/scripts/html-renderer/transcribe.py +294 -0
  27. package/src/asset-library/README.md +2 -2
  28. package/src/asset-library/asset-library.js +1 -1
  29. package/src/cli/doctor.js +31 -1
  30. package/src/cli/make.js +52 -71
  31. package/src/cli/setup-keys.js +24 -19
  32. package/src/fact-checker/review-gate.js +5 -1
  33. package/src/harness/bin/harness.mjs +324 -0
  34. package/src/harness/lib/browser.mjs +137 -0
  35. package/src/harness/lib/capture.mjs +161 -0
  36. package/src/harness/lib/encode.mjs +89 -0
  37. package/src/harness/lib/jobs.mjs +135 -0
  38. package/src/harness/lib/pixels.mjs +137 -0
  39. package/src/harness/lib/sheets.mjs +164 -0
  40. package/src/harness/lib/verify-purity.mjs +119 -0
  41. package/src/harness/test/fixtures/broken-scene.html +27 -0
  42. package/src/harness/test/fixtures/reference-scene.html +23 -0
  43. package/src/harness/test/fixtures/sample-script.json +18 -0
  44. package/src/mcp-server/server.js +318 -66
  45. package/src/pipeline/external-adapter.js +55 -15
  46. package/src/pipeline/providers.js +46 -0
  47. package/src/pipeline/telemetry.js +1 -0
  48. package/src/utils/log-redaction.js +5 -3
  49. package/src/video-renderer/renderer.js +12 -4
  50. package/src/worker/server-render-free.js +2 -3
  51. package/src/workflows/assets.js +20 -43
  52. package/src/workflows/ingest.js +5 -5
@@ -0,0 +1,138 @@
1
+ /**
2
+ * virtual-clock.js — FrameForge Deterministic Virtual Clock Shim
3
+ *
4
+ * Intercepts browser timing primitives to enable frame-exact, deterministic
5
+ * time control for headless video rendering.
6
+ *
7
+ * Laws enforced:
8
+ * 1. Read time, not frames (seek(t) is absolute time in seconds).
9
+ * 2. Seed everything (Math.random replaced with seeded PRNG).
10
+ * 3. No integrating physics (timers scheduled along virtual timeline).
11
+ * 4. Await document.fonts.ready before resolving seek().
12
+ */
13
+ (() => {
14
+ let virtualTime = 0;
15
+ const rafCallbacks = new Map();
16
+ let rafIdCounter = 0;
17
+ const timeoutQueue = [];
18
+ let timeoutIdCounter = 0;
19
+ const startDate = Date.now();
20
+
21
+ window.__playwright = true;
22
+
23
+ // 1. Hijack performance.now & Date
24
+ window.performance.now = () => virtualTime;
25
+ const OriginalDate = window.Date;
26
+ class VirtualDate extends OriginalDate {
27
+ constructor(...args) {
28
+ if (args.length === 0) {
29
+ super(startDate + virtualTime);
30
+ } else {
31
+ super(...args);
32
+ }
33
+ }
34
+ static now() {
35
+ return Math.floor(startDate + virtualTime);
36
+ }
37
+ }
38
+ window.Date = VirtualDate;
39
+
40
+ // 2. Hijack requestAnimationFrame & cancelAnimationFrame
41
+ window.requestAnimationFrame = (callback) => {
42
+ const id = ++rafIdCounter;
43
+ rafCallbacks.set(id, callback);
44
+ return id;
45
+ };
46
+ window.cancelAnimationFrame = (id) => {
47
+ rafCallbacks.delete(id);
48
+ };
49
+
50
+ // 3. Hijack setTimeout / clearTimeout / setInterval / clearInterval
51
+ window.setTimeout = (callback, delay = 0, ...args) => {
52
+ const id = ++timeoutIdCounter;
53
+ timeoutQueue.push({ id, runAt: virtualTime + Math.max(0, delay), callback, args, repeat: 0 });
54
+ timeoutQueue.sort((a, b) => a.runAt - b.runAt);
55
+ return id;
56
+ };
57
+ window.clearTimeout = (id) => {
58
+ const idx = timeoutQueue.findIndex(item => item.id === id);
59
+ if (idx !== -1) timeoutQueue.splice(idx, 1);
60
+ };
61
+ window.setInterval = (callback, interval = 0, ...args) => {
62
+ const id = ++timeoutIdCounter;
63
+ const step = Math.max(16, interval);
64
+ timeoutQueue.push({ id, runAt: virtualTime + step, callback, args, repeat: step });
65
+ timeoutQueue.sort((a, b) => a.runAt - b.runAt);
66
+ return id;
67
+ };
68
+ window.clearInterval = (id) => {
69
+ const idx = timeoutQueue.findIndex(item => item.id === id);
70
+ if (idx !== -1) timeoutQueue.splice(idx, 1);
71
+ };
72
+
73
+ // 4. Seeded PRNG replacement for Math.random()
74
+ let seed = 0x2F6E2B1;
75
+ window.Math.random = () => {
76
+ seed = (seed + 0x6D2B79F5) | 0;
77
+ let t = Math.imul(seed ^ (seed >>> 15), 1 | seed);
78
+ t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t;
79
+ return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
80
+ };
81
+
82
+ // 5. Global seek contract
83
+ window.seek = async (tSeconds) => {
84
+ virtualTime = tSeconds * 1000;
85
+
86
+ // Flush due timeouts
87
+ while (timeoutQueue.length > 0 && timeoutQueue[0].runAt <= virtualTime) {
88
+ const item = timeoutQueue.shift();
89
+ try {
90
+ item.callback(...item.args);
91
+ } catch (err) {
92
+ console.error("VirtualClock Timeout Error:", err);
93
+ }
94
+ if (item.repeat > 0) {
95
+ item.runAt = virtualTime + item.repeat;
96
+ timeoutQueue.push(item);
97
+ timeoutQueue.sort((a, b) => a.runAt - b.runAt);
98
+ }
99
+ }
100
+
101
+ // Flush current RAF callbacks
102
+ const currentRafs = Array.from(rafCallbacks.values());
103
+ rafCallbacks.clear();
104
+ for (const cb of currentRafs) {
105
+ try {
106
+ cb(virtualTime);
107
+ } catch (err) {
108
+ console.error("VirtualClock RAF Error:", err);
109
+ }
110
+ }
111
+
112
+ // Pause & seek CSS animations
113
+ try {
114
+ if (document.getAnimations) {
115
+ document.getAnimations().forEach(a => {
116
+ if (a.playState !== 'paused') a.pause();
117
+ a.currentTime = virtualTime;
118
+ });
119
+ }
120
+ } catch (_) {}
121
+
122
+ // Custom scene render hook if exposed
123
+ if (typeof window.renderFrame === "function") {
124
+ await window.renderFrame(tSeconds);
125
+ }
126
+
127
+ // Await font readiness
128
+ if (document.fonts && document.fonts.ready) {
129
+ await document.fonts.ready;
130
+ }
131
+ };
132
+
133
+ // Bridge for renderer
134
+ window.__seek = window.seek;
135
+
136
+ window.__VIRTUAL_CLOCK_INSTALLED__ = true;
137
+ })();
138
+
@@ -0,0 +1,167 @@
1
+ # MCP Tool Reference
2
+
3
+ FrameForge exposes 13 tools on the HTTP MCP server (`http://localhost:3210`)
4
+ and via the Cloudflare Remote MCP proxy. Any MCP-compatible client works:
5
+ IBM Bob, Claude Code, Codex, opencode, Cursor, Windsurf.
6
+
7
+ ---
8
+
9
+ ## Agent-first path (recommended)
10
+
11
+ The IDE writes the scenes array; FrameForge renders it.
12
+
13
+ ### create_video_from_script
14
+
15
+ ```json
16
+ {
17
+ "scenes": [
18
+ {
19
+ "narration": "WiFi sends data as invisible radio waves.",
20
+ "caption": "RADIO WAVES",
21
+ "visual_description": "router emitting waves, flat illustration",
22
+ "camera_hint": "slow_zoom_in",
23
+ "music_mood": "calm"
24
+ }
25
+ ],
26
+ "title": "How WiFi Works",
27
+ "skipImages": false,
28
+ "withMusic": true
29
+ }
30
+ ```
31
+
32
+ Returns: `{ productionId, masterPath, passed, sizeBytes, sceneCount }`
33
+
34
+ ---
35
+
36
+ ## Full pipeline path
37
+
38
+ ### research_topic
39
+
40
+ ```json
41
+ { "topic": "How does GPS work?", "claimCount": 10, "style": "educational" }
42
+ ```
43
+
44
+ ### write_script
45
+
46
+ ```json
47
+ {
48
+ "productionId": "prod-001",
49
+ "claims": ["..."],
50
+ "topic": "How does GPS work?",
51
+ "persona": "tech explainer",
52
+ "sceneCount": 15
53
+ }
54
+ ```
55
+
56
+ Returns: `{ productionId, scenes, title, description, tags, sceneCount }`
57
+
58
+ ### render_from_scenes
59
+
60
+ ```json
61
+ { "productionId": "prod-001", "skipImages": false, "withMusic": true }
62
+ ```
63
+
64
+ ### publish_video
65
+
66
+ ```json
67
+ {
68
+ "productionId": "prod-001",
69
+ "masterPath": "/path/to/final_video.mp4",
70
+ "channels": ["file"],
71
+ "outputDir": "/path/to/output"
72
+ }
73
+ ```
74
+
75
+ Channels:
76
+ - `file` — copy master + shorts to `outputDir` (or `N8N_FILES_DIR/published/`)
77
+ - `webhook` — POST JSON summary to `PUBLISH_WEBHOOK_URL`
78
+
79
+ ---
80
+
81
+ ## Edit path
82
+
83
+ ### ingest_media
84
+
85
+ ```json
86
+ { "files": ["/path/to/clip1.mp4", "/path/to/clip2.mp4"], "title": "My Video" }
87
+ ```
88
+
89
+ ### edit_scenes
90
+
91
+ ```json
92
+ {
93
+ "productionId": "prod-001",
94
+ "edits": [{ "sceneIndex": 0, "field": "caption", "value": "NEW CAPTION" }]
95
+ }
96
+ ```
97
+
98
+ ---
99
+
100
+ ## TTS
101
+
102
+ ### multi_voice_tts
103
+
104
+ ```json
105
+ {
106
+ "text": "The text to speak aloud.",
107
+ "outputPath": "/files-dir/voice_0.m4a",
108
+ "platform": "elevenlabs",
109
+ "voice": "your_cloned_voice_id",
110
+ "apiKey": "sk_..."
111
+ }
112
+ ```
113
+
114
+ ---
115
+
116
+ ## Async job control
117
+
118
+ ### queue_full_pipeline
119
+
120
+ ```json
121
+ {
122
+ "topic": "Why planes avoid straight lines",
123
+ "style": "educational",
124
+ "channels": ["file"]
125
+ }
126
+ ```
127
+
128
+ Returns `{ jobId, status }` immediately. Poll `get_job_status` for completion.
129
+
130
+ ### get_job_status
131
+
132
+ ```json
133
+ { "jobId": "job_1234567890_abc123" }
134
+ ```
135
+
136
+ Returns `{ status: "RUNNING"|"SUCCEEDED"|"FAILED", result, error }`
137
+
138
+ ---
139
+
140
+ ## External platform
141
+
142
+ ### external_platform_call
143
+
144
+ ```json
145
+ { "action": "ingest", "files": ["/path/to/clip.mp4"] }
146
+ ```
147
+
148
+ Actions: `ingest` | `render` | `status` | `download`.
149
+ Configure with `EXTERNAL_PLATFORM_URL` + `EXTERNAL_PLATFORM_KEY` in `.env`.
150
+
151
+ ---
152
+
153
+ ## Calling from any IDE
154
+
155
+ Any IDE that supports MCP calls these tools via JSON-RPC:
156
+
157
+ ```json
158
+ POST http://localhost:3210
159
+ {
160
+ "method": "tools/call",
161
+ "params": {
162
+ "name": "create_video_from_script",
163
+ "arguments": { "scenes": [...] }
164
+ },
165
+ "id": 1
166
+ }
167
+ ```
@@ -0,0 +1,163 @@
1
+ # Provider Environment Variable Reference
2
+
3
+ All secrets come from environment variables. Never hardcode them.
4
+ Copy `docs/env.template` → `.env` to get started.
5
+
6
+ ---
7
+
8
+ ## LLM Providers (`PROVIDER_LLM`)
9
+
10
+ ### groq (default)
11
+ ```bash
12
+ PROVIDER_LLM=groq
13
+ GROQ_API_KEY=gsk_...
14
+ # Optional overrides:
15
+ LLM_MODEL=llama-3.3-70b-versatile # default
16
+ LLM_BASE_URL=https://api.groq.com/openai/v1
17
+ ```
18
+
19
+ ### watsonx (IBM — enterprise / data-residency)
20
+ ```bash
21
+ PROVIDER_LLM=watsonx
22
+ WATSONX_API_KEY=... # IBM Cloud API key
23
+ WATSONX_PROJECT_ID=... # from IBM Cloud → watsonx.ai → Projects
24
+ LLM_BASE_URL=https://us-south.ml.cloud.ibm.com/ml/v1 # or eu-de / jp-tok
25
+ LLM_MODEL=ibm/granite-3-3-8b-instruct # or granite-3-3-2b-instruct
26
+ ```
27
+ Granite 3.3 supports function calling and 128 K context. All inference stays
28
+ in your IBM Cloud region — no data leaves your residency boundary.
29
+
30
+ ### openai
31
+ ```bash
32
+ PROVIDER_LLM=openai
33
+ OPENAI_API_KEY=sk-...
34
+ LLM_MODEL=gpt-4o # default
35
+ ```
36
+
37
+ ### ollama (local, no key)
38
+ ```bash
39
+ PROVIDER_LLM=ollama
40
+ LLM_BASE_URL=http://localhost:11434/api
41
+ LLM_MODEL=llama3.2 # whatever you have pulled
42
+ ```
43
+
44
+ ---
45
+
46
+ ## TTS Providers (`PROVIDER_TTS`)
47
+
48
+ ### edge-tts (default — free, no key, 300+ voices, offline)
49
+ ```bash
50
+ PROVIDER_TTS=edge-tts
51
+ # No key required. Needs: npm install -g edge-tts
52
+ # Voice list: edge-tts --list-voices
53
+ ```
54
+
55
+ ### elevenlabs (voice cloning)
56
+ ```bash
57
+ PROVIDER_TTS=elevenlabs
58
+ ELEVENLABS_API_KEY=sk_...
59
+ # Optional:
60
+ ELEVENLABS_VOICE_ID=21m00Tcm4TlvDq8ikWAM # default: Rachel
61
+ ```
62
+
63
+ ### azure (enterprise)
64
+ ```bash
65
+ PROVIDER_TTS=azure
66
+ AZURE_SPEECH_KEY=...
67
+ AZURE_SPEECH_REGION=eastus # or westeurope, etc.
68
+ ```
69
+
70
+ ### openai
71
+ ```bash
72
+ PROVIDER_TTS=openai
73
+ OPENAI_API_KEY=sk-...
74
+ # Uses tts-1-hd model, alloy voice by default
75
+ ```
76
+
77
+ ---
78
+
79
+ ## Visuals & Media
80
+
81
+ FrameForge is **code-driven first**. By default, scenes render using vector animations, kinetic typography, and user-provided media (`image_path` / `video_path`). Diffusion image generation is optional and disabled by default.
82
+
83
+ ### Code-driven animation (default)
84
+ ```bash
85
+ # Default mode — zero keys needed, broadcast vector quality
86
+ # Automatically creates animated cards, syntax blocks, terminals, and typography
87
+ ```
88
+
89
+ ### Optional Diffusion Image Providers (`ENABLE_AI_IMAGES=true`)
90
+
91
+ ### pollinations (default — free, no key)
92
+ ```bash
93
+ PROVIDER_IMAGE=pollinations
94
+ # No key required. Uses flux model.
95
+ ```
96
+
97
+ ### fal (fast, FLUX-based — recommended for production)
98
+ ```bash
99
+ PROVIDER_IMAGE=fal
100
+ FAL_API_KEY=...
101
+ # Also accepted as: FALAI_API_KEY (legacy alias)
102
+ ```
103
+
104
+ ### replicate
105
+ ```bash
106
+ PROVIDER_IMAGE=replicate
107
+ REPLICATE_API_TOKEN=r8_...
108
+ ```
109
+
110
+ ### dalle (DALL-E 3)
111
+ ```bash
112
+ PROVIDER_IMAGE=dalle
113
+ OPENAI_API_KEY=sk-...
114
+ ```
115
+
116
+ ---
117
+
118
+ ## Delivery
119
+
120
+ ### Webhook
121
+ ```bash
122
+ PUBLISH_WEBHOOK_URL=https://your-receiver.example.com/webhook
123
+ REVIEW_NOTIFY_URL=https://your-receiver.example.com/review
124
+ ```
125
+
126
+ ### Local File (default)
127
+ ```bash
128
+ # Copies master and shorts to output folder
129
+ # No external credentials needed
130
+ ```
131
+
132
+ ---
133
+
134
+ ## Telemetry / state (optional PostgreSQL)
135
+
136
+ ```bash
137
+ DATABASE_URL=postgresql://user:pass@localhost:5432/vidpipeline
138
+ ```
139
+ Without this, state is stored to `.n8n-files/productions/` (JSON files)
140
+ and telemetry is written to `logs/usage.ndjson`.
141
+
142
+ ---
143
+
144
+ ## IBM Bob specific
145
+
146
+ Bob discovers MCP servers from `~/.bob/mcp.json`:
147
+ ```json
148
+ {
149
+ "mcpServers": {
150
+ "video-pipeline": {
151
+ "command": "node",
152
+ "args": ["/absolute/path/to/src/mcp-server/server.js"],
153
+ "env": {
154
+ "PROVIDER_LLM": "watsonx",
155
+ "WATSONX_API_KEY": "${WATSONX_API_KEY}",
156
+ "WATSONX_PROJECT_ID": "${WATSONX_PROJECT_ID}",
157
+ "PROVIDER_TTS": "edge-tts"
158
+ }
159
+ }
160
+ }
161
+ }
162
+ ```
163
+ Restart Bob after adding. The 6 tools appear in Bob's tool palette.
@@ -0,0 +1,112 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * db-migrate.js — Apply FrameForge PostgreSQL schema.
4
+ *
5
+ * Reads DATABASE_URL from environment and runs the production state-machine
6
+ * schema (CREATE TABLE IF NOT EXISTS productions). Safe to run multiple
7
+ * times — all statements are idempotent.
8
+ *
9
+ * Usage:
10
+ * DATABASE_URL=postgresql://user:pass@host:5432/frameforge npm run db:migrate
11
+ *
12
+ * Or directly:
13
+ * DATABASE_URL=... node scripts/db-migrate.js
14
+ *
15
+ * Required: pg (automatically available when DATABASE_URL is set on Render,
16
+ * Railway, Supabase, Neon, or any managed PostgreSQL host)
17
+ *
18
+ * The script uses only Node.js built-in pg calls — no ORM, no migration
19
+ * framework, no extra dependencies beyond the 'pg' package.
20
+ */
21
+
22
+ 'use strict';
23
+
24
+ const DATABASE_URL = process.env.DATABASE_URL;
25
+
26
+ if (!DATABASE_URL) {
27
+ console.error('[db:migrate] ERROR: DATABASE_URL environment variable is not set.');
28
+ console.error(' Set it in .env or export it before running:');
29
+ console.error(' DATABASE_URL=postgresql://user:pass@localhost:5432/frameforge npm run db:migrate');
30
+ process.exit(1);
31
+ }
32
+
33
+ // ── Schema SQL ────────────────────────────────────────────────────────────────
34
+ // Mirrors the inline CREATE TABLE in src/pipeline/state-machine.js _pgInit().
35
+ // Keep in sync if the state-machine schema changes.
36
+
37
+ const MIGRATIONS = [
38
+ {
39
+ name: '001_create_productions',
40
+ sql: `
41
+ CREATE TABLE IF NOT EXISTS productions (
42
+ production_id VARCHAR(64) PRIMARY KEY,
43
+ status VARCHAR(20) NOT NULL DEFAULT 'QUEUED',
44
+ pipeline_version VARCHAR(20),
45
+ schema_version VARCHAR(20),
46
+ topic TEXT,
47
+ created_at TIMESTAMP DEFAULT NOW(),
48
+ started_at TIMESTAMP,
49
+ completed_at TIMESTAMP,
50
+ config_hash VARCHAR(32),
51
+ stages JSONB DEFAULT '{}',
52
+ metadata JSONB DEFAULT '{}'
53
+ );
54
+ `,
55
+ },
56
+ {
57
+ name: '002_productions_indexes',
58
+ sql: `
59
+ CREATE INDEX IF NOT EXISTS productions_status_idx
60
+ ON productions (status);
61
+ CREATE INDEX IF NOT EXISTS productions_created_at_idx
62
+ ON productions (created_at DESC);
63
+ `,
64
+ },
65
+ ];
66
+
67
+ // ── Run migrations ────────────────────────────────────────────────────────────
68
+
69
+ async function run() {
70
+ let pg;
71
+ try {
72
+ pg = require('pg');
73
+ } catch {
74
+ console.error('[db:migrate] ERROR: "pg" package not found.');
75
+ console.error(' Install it: npm install pg');
76
+ console.error(' (Only required when DATABASE_URL is set — not needed for file-mode operation.)');
77
+ process.exit(1);
78
+ }
79
+
80
+ const { Pool } = pg;
81
+ const pool = new Pool({ connectionString: DATABASE_URL, ssl: DATABASE_URL.includes('sslmode=disable') ? false : { rejectUnauthorized: false } });
82
+
83
+ try {
84
+ await pool.query('SELECT 1'); // connectivity check
85
+ console.log('[db:migrate] Connected to PostgreSQL.');
86
+ } catch (err) {
87
+ console.error(`[db:migrate] ERROR: could not connect — ${err.message}`);
88
+ console.error(' Check DATABASE_URL and that the database server is reachable.');
89
+ process.exit(1);
90
+ }
91
+
92
+ for (const migration of MIGRATIONS) {
93
+ try {
94
+ await pool.query(migration.sql);
95
+ console.log(`[db:migrate] ✔ ${migration.name}`);
96
+ } catch (err) {
97
+ console.error(`[db:migrate] FAILED: ${migration.name} — ${err.message}`);
98
+ await pool.end();
99
+ process.exit(1);
100
+ }
101
+ }
102
+
103
+ await pool.end();
104
+ console.log('[db:migrate] All migrations applied. FrameForge PostgreSQL schema is ready.');
105
+ console.log('');
106
+ console.log(' Next steps:');
107
+ console.log(' 1. Set RENDER_MODE=api_only in your deployment environment variables.');
108
+ console.log(' 2. The StateMachine will switch to pg mode automatically when DATABASE_URL is set.');
109
+ console.log(' 3. Run `npm run doctor` to verify the full stack.');
110
+ }
111
+
112
+ run();