@amg555/frameforge-mcp 0.0.0-stage → 2.3.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/.bob/artifacts/enterprise-ai-video-agent-platform-agnostic-implementation-guide.html +464 -0
- package/.claude-plugin/marketplace.json +45 -0
- package/.gitattributes +19 -0
- package/.mcp.json +45 -0
- package/CHANGELOG.md +447 -0
- package/CLAUDE.md +320 -0
- package/CONTRIBUTING.md +112 -0
- package/LICENSE +21 -0
- package/Procfile +1 -0
- package/README.md +377 -2
- package/SECURITY.md +71 -0
- package/START_HERE.md +200 -0
- package/assets/metadata/assets.json +5 -0
- package/assets/metadata/embeddings.json +5 -0
- package/install.sh +64 -0
- package/package.json +58 -4
- package/src/animation/animation-system.js +229 -0
- package/src/asset-library/README.md +188 -0
- package/src/asset-library/asset-library.js +639 -0
- package/src/audio-mixer/mixer.js +187 -0
- package/src/cli/doctor.js +173 -0
- package/src/cli/make.js +655 -0
- package/src/cli/setup-keys.js +392 -0
- package/src/config/load-config.js +72 -0
- package/src/fact-checker/checker.js +384 -0
- package/src/fact-checker/review-gate.js +306 -0
- package/src/fact-checker/validation-set.js +1078 -0
- package/src/mcp-server/server.js +1299 -0
- package/src/mcp-server/stdio.js +87 -0
- package/src/pipeline/artifact-manifest.js +323 -0
- package/src/pipeline/artifact-retention.js +189 -0
- package/src/pipeline/external-adapter.js +366 -0
- package/src/pipeline/providers.js +853 -0
- package/src/pipeline/retry-policy.js +134 -0
- package/src/pipeline/state-machine.js +297 -0
- package/src/pipeline/telemetry.js +162 -0
- package/src/quality-gate/gate.js +229 -0
- package/src/schema/scene.schema.json +101 -0
- package/src/shorts-extractor/extractor.js +343 -0
- package/src/utils/log-redaction.js +274 -0
- package/src/video-renderer/renderer.js +364 -0
- package/src/worker/server-render-free.js +420 -0
- package/src/worker/server.js +328 -0
- package/src/workflows/assets.js +131 -0
- package/src/workflows/ingest.js +299 -0
- package/src/workflows/packaging.js +156 -0
- package/src/workflows/publishing.js +206 -0
- package/src/workflows/render.js +102 -0
- package/src/workflows/research.js +151 -0
- package/src/workflows/script.js +210 -0
- package/src/workflows/shorts.js +113 -0
|
@@ -0,0 +1,392 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* FrameForge Key Setup Wizard
|
|
4
|
+
*
|
|
5
|
+
* Interactive CLI that walks through every provider and writes a .env file.
|
|
6
|
+
* Skips sections you don't need (e.g. skip ElevenLabs if using edge-tts).
|
|
7
|
+
*
|
|
8
|
+
* Usage:
|
|
9
|
+
* node src/cli/setup-keys.js
|
|
10
|
+
* node src/cli/setup-keys.js --out .env.production
|
|
11
|
+
* node src/cli/setup-keys.js --check (validate existing .env)
|
|
12
|
+
*
|
|
13
|
+
* Provider options explained:
|
|
14
|
+
*
|
|
15
|
+
* LLM (script + research):
|
|
16
|
+
* groq — Free, fast (llama-3.3-70b). Get key: console.groq.com
|
|
17
|
+
* cerebras — Fastest inference (2000+ tok/s). Get key: cloud.cerebras.ai
|
|
18
|
+
* openrouter — All models in one key. Get key: openrouter.ai
|
|
19
|
+
* anthropic — Claude Sonnet 4.5. Get key: console.anthropic.com
|
|
20
|
+
* gemini — Gemini 2.0 Flash. Get key: aistudio.google.com
|
|
21
|
+
* mistral — Mistral Large. Get key: console.mistral.ai
|
|
22
|
+
* together — Llama 3.3-70B Turbo. Get key: api.together.ai
|
|
23
|
+
* openai — GPT-4o. Get key: platform.openai.com
|
|
24
|
+
* ollama — Fully local, no key needed. Install: ollama.com
|
|
25
|
+
* webhook — Any OpenAI-compatible endpoint. Set WEBHOOK_LLM_URL.
|
|
26
|
+
*
|
|
27
|
+
* TTS (voice generation):
|
|
28
|
+
* edge-tts — Free, offline. 400+ voices. No key needed.
|
|
29
|
+
* elevenlabs — Best quality + voice cloning. Get key: elevenlabs.io
|
|
30
|
+
* azure — Microsoft Cognitive. Get key: portal.azure.com
|
|
31
|
+
* openai — OpenAI TTS. Uses OPENAI_API_KEY.
|
|
32
|
+
* webhook — Any TTS endpoint. Set WEBHOOK_TTS_URL.
|
|
33
|
+
*
|
|
34
|
+
* Image (scene visuals):
|
|
35
|
+
* pollinations — Free, no key. Best default.
|
|
36
|
+
* falai — High quality, fast. Get key: fal.ai
|
|
37
|
+
* replicate — FLUX models. Get key: replicate.com
|
|
38
|
+
* dalle — DALL-E 3. Uses OPENAI_API_KEY.
|
|
39
|
+
* webhook — Any image endpoint. Set WEBHOOK_IMAGE_URL.
|
|
40
|
+
*/
|
|
41
|
+
|
|
42
|
+
'use strict';
|
|
43
|
+
|
|
44
|
+
const fs = require('fs');
|
|
45
|
+
const path = require('path');
|
|
46
|
+
const os = require('os');
|
|
47
|
+
const readline = require('readline');
|
|
48
|
+
|
|
49
|
+
const { CONFIG_DIR, CONFIG_FILE } = require('../config/load-config');
|
|
50
|
+
|
|
51
|
+
// ── Parse args ────────────────────────────────────────────────────────────────
|
|
52
|
+
|
|
53
|
+
const argv = process.argv.slice(2);
|
|
54
|
+
const outArg = argv.findIndex(a => a === '--out');
|
|
55
|
+
const outFile = outArg >= 0 ? argv[outArg + 1] : '.env';
|
|
56
|
+
const checkMode = argv.includes('--check');
|
|
57
|
+
|
|
58
|
+
// ── Readline helper ───────────────────────────────────────────────────────────
|
|
59
|
+
|
|
60
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
61
|
+
|
|
62
|
+
function ask(question, defaultVal = '') {
|
|
63
|
+
return new Promise(resolve => {
|
|
64
|
+
const prompt = defaultVal ? `${question} [${defaultVal}]: ` : `${question}: `;
|
|
65
|
+
rl.question(prompt, answer => {
|
|
66
|
+
const trimmed = answer.trim();
|
|
67
|
+
resolve(trimmed || defaultVal);
|
|
68
|
+
});
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function askSecret(question) {
|
|
73
|
+
return new Promise(resolve => {
|
|
74
|
+
process.stdout.write(`${question} (hidden): `);
|
|
75
|
+
// readline can't hide input; we just note it's sensitive
|
|
76
|
+
rl.question('', answer => resolve(answer.trim()));
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function menu(question, options, defaultKey) {
|
|
81
|
+
const lines = [question];
|
|
82
|
+
options.forEach(([key, label]) => {
|
|
83
|
+
const marker = key === defaultKey ? ' (default)' : '';
|
|
84
|
+
lines.push(` ${key}) ${label}${marker}`);
|
|
85
|
+
});
|
|
86
|
+
return ask(lines.join('\n'), defaultKey);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function section(title) {
|
|
90
|
+
const bar = '─'.repeat(60);
|
|
91
|
+
console.log(`\n${bar}`);
|
|
92
|
+
console.log(` ${title}`);
|
|
93
|
+
console.log(bar);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// ── Check mode ────────────────────────────────────────────────────────────────
|
|
97
|
+
|
|
98
|
+
function checkEnv(envPath) {
|
|
99
|
+
const fullPath = path.resolve(process.cwd(), envPath);
|
|
100
|
+
if (!fs.existsSync(fullPath)) {
|
|
101
|
+
console.log(`\n❌ No .env file found at: ${fullPath}`);
|
|
102
|
+
console.log(` Run: node src/cli/setup-keys.js to create one\n`);
|
|
103
|
+
process.exit(1);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const lines = fs.readFileSync(fullPath, 'utf8').split('\n');
|
|
107
|
+
const env = {};
|
|
108
|
+
lines.forEach(line => {
|
|
109
|
+
const stripped = line.trim();
|
|
110
|
+
if (!stripped || stripped.startsWith('#')) return;
|
|
111
|
+
const eq = stripped.indexOf('=');
|
|
112
|
+
if (eq < 1) return;
|
|
113
|
+
env[stripped.slice(0, eq).trim()] = stripped.slice(eq + 1).trim().replace(/^["']|["']$/g, '');
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
console.log(`\n🔍 Checking .env at: ${fullPath}\n`);
|
|
117
|
+
|
|
118
|
+
const checks = [
|
|
119
|
+
{ key: 'PROVIDER_LLM', label: 'LLM provider', optional: false },
|
|
120
|
+
{ key: 'GROQ_API_KEY', label: 'Groq key', optional: true, needs: env.PROVIDER_LLM === 'groq' || !env.PROVIDER_LLM },
|
|
121
|
+
{ key: 'OPENAI_API_KEY', label: 'OpenAI key', optional: true, needs: ['openai','dalle'].includes(env.PROVIDER_LLM) },
|
|
122
|
+
{ key: 'CEREBRAS_API_KEY', label: 'Cerebras key', optional: true, needs: env.PROVIDER_LLM === 'cerebras' },
|
|
123
|
+
{ key: 'OPENROUTER_API_KEY',label: 'OpenRouter key', optional: true, needs: env.PROVIDER_LLM === 'openrouter' },
|
|
124
|
+
{ key: 'ANTHROPIC_API_KEY', label: 'Anthropic key', optional: true, needs: env.PROVIDER_LLM === 'anthropic' },
|
|
125
|
+
{ key: 'GEMINI_API_KEY', label: 'Gemini key', optional: true, needs: env.PROVIDER_LLM === 'gemini' },
|
|
126
|
+
{ key: 'MISTRAL_API_KEY', label: 'Mistral key', optional: true, needs: env.PROVIDER_LLM === 'mistral' },
|
|
127
|
+
{ key: 'TOGETHER_API_KEY', label: 'Together key', optional: true, needs: env.PROVIDER_LLM === 'together' },
|
|
128
|
+
{ key: 'PROVIDER_TTS', label: 'TTS provider', optional: false },
|
|
129
|
+
{ key: 'ELEVENLABS_API_KEY', label: 'ElevenLabs key', optional: true, needs: env.PROVIDER_TTS === 'elevenlabs' },
|
|
130
|
+
{ key: 'AZURE_SPEECH_KEY', label: 'Azure key', optional: true, needs: env.PROVIDER_TTS === 'azure' },
|
|
131
|
+
{ key: 'PROVIDER_IMAGE', label: 'Image provider', optional: false },
|
|
132
|
+
{ key: 'FALAI_API_KEY', label: 'fal.ai key', optional: true, needs: env.PROVIDER_IMAGE === 'falai' },
|
|
133
|
+
{ key: 'REPLICATE_API_KEY', label: 'Replicate key', optional: true, needs: env.PROVIDER_IMAGE === 'replicate' },
|
|
134
|
+
{ key: 'PUBLISH_WEBHOOK_URL', label: 'Publish webhook URL', optional: true },
|
|
135
|
+
];
|
|
136
|
+
|
|
137
|
+
let issues = 0;
|
|
138
|
+
checks.forEach(({ key, label, optional, needs }) => {
|
|
139
|
+
if (needs === false) return; // skip — not needed for current config
|
|
140
|
+
const val = env[key];
|
|
141
|
+
if (!val) {
|
|
142
|
+
if (!optional || needs) {
|
|
143
|
+
console.log(` ❌ MISSING ${key.padEnd(30)} (${label})`);
|
|
144
|
+
issues++;
|
|
145
|
+
} else {
|
|
146
|
+
console.log(` ⚪ optional ${key.padEnd(30)} (${label})`);
|
|
147
|
+
}
|
|
148
|
+
} else {
|
|
149
|
+
const display = val.length > 8 ? val.slice(0, 4) + '…' + val.slice(-4) : '***';
|
|
150
|
+
console.log(` ✅ OK ${key.padEnd(30)} = ${display}`);
|
|
151
|
+
}
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
console.log(`\n${issues === 0 ? '✅ All required keys present.' : `❌ ${issues} issue(s) found. Run: node src/cli/setup-keys.js`}\n`);
|
|
155
|
+
process.exit(issues > 0 ? 1 : 0);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// ── Main wizard ───────────────────────────────────────────────────────────────
|
|
159
|
+
|
|
160
|
+
async function main() {
|
|
161
|
+
if (checkMode) {
|
|
162
|
+
checkEnv(outFile);
|
|
163
|
+
return;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
console.log(`
|
|
167
|
+
╔══════════════════════════════════════════════════════════╗
|
|
168
|
+
║ FrameForge — Provider Key Setup Wizard ║
|
|
169
|
+
╚══════════════════════════════════════════════════════════╝
|
|
170
|
+
|
|
171
|
+
This wizard writes your provider API keys to: ${path.resolve(outFile)}
|
|
172
|
+
Keys are never sent anywhere — they stay on your machine.
|
|
173
|
+
Press Enter to accept a default. Ctrl+C to cancel.
|
|
174
|
+
|
|
175
|
+
✅ No keys are required to make videos.
|
|
176
|
+
Your IDE writes the scenes — FrameForge uses free providers by default:
|
|
177
|
+
• edge-tts — free voice (no account)
|
|
178
|
+
• pollinations — free images (no account)
|
|
179
|
+
• FFmpeg — free local render (already on your machine)
|
|
180
|
+
|
|
181
|
+
You only need keys for the auto-research pipeline (FrameForge writes the
|
|
182
|
+
script itself). Skip those sections if you plan to write scenes yourself.
|
|
183
|
+
`);
|
|
184
|
+
|
|
185
|
+
const cfg = {};
|
|
186
|
+
|
|
187
|
+
// ── LLM ──────────────────────────────────────────────────────────────────
|
|
188
|
+
section('1 / 5 — LLM Provider (research + script generation)');
|
|
189
|
+
console.log(' Used for: researching topics, writing scripts, fact-checking.');
|
|
190
|
+
console.log(' ⚡ SKIP THIS if your IDE agent writes the scenes (most common path).');
|
|
191
|
+
console.log(' Free option: Groq (no credit card needed for free tier).\n');
|
|
192
|
+
|
|
193
|
+
cfg.PROVIDER_LLM = await menu('Choose LLM provider', [
|
|
194
|
+
['skip', '⏭ Skip — I write my own scenes (most common, no key needed)'],
|
|
195
|
+
['groq', 'Groq — free, fast (llama-3.3-70b). Get key: console.groq.com'],
|
|
196
|
+
['cerebras', 'Cerebras — fastest inference (2000+ tok/s). Get key: cloud.cerebras.ai'],
|
|
197
|
+
['gemini', 'Google Gemini 2.0 Flash — free tier. Get key: aistudio.google.com'],
|
|
198
|
+
['openrouter', 'OpenRouter — all models with one key. Get key: openrouter.ai'],
|
|
199
|
+
['anthropic', 'Anthropic Claude Sonnet 4.5. Get key: console.anthropic.com'],
|
|
200
|
+
['mistral', 'Mistral Large. Get key: console.mistral.ai'],
|
|
201
|
+
['together', 'Together AI Llama-3.3-70B-Turbo. Get key: api.together.ai'],
|
|
202
|
+
['openai', 'OpenAI GPT-4o. Get key: platform.openai.com'],
|
|
203
|
+
['ollama', 'Ollama — fully local, no key needed. Install: ollama.com'],
|
|
204
|
+
['webhook', 'Custom/VoxForge/any OpenAI-compatible endpoint'],
|
|
205
|
+
], 'skip');
|
|
206
|
+
|
|
207
|
+
if (cfg.PROVIDER_LLM === 'skip') {
|
|
208
|
+
console.log(' ✅ LLM skipped — your IDE will write scenes via create_video_from_script.\n');
|
|
209
|
+
cfg.PROVIDER_LLM = undefined; // don't write to config
|
|
210
|
+
} else if (cfg.PROVIDER_LLM === 'groq') {
|
|
211
|
+
cfg.GROQ_API_KEY = await askSecret(' Groq API key (starts with gsk_)');
|
|
212
|
+
} else if (cfg.PROVIDER_LLM === 'cerebras') {
|
|
213
|
+
cfg.CEREBRAS_API_KEY = await askSecret(' Cerebras API key');
|
|
214
|
+
} else if (cfg.PROVIDER_LLM === 'openrouter') {
|
|
215
|
+
cfg.OPENROUTER_API_KEY = await askSecret(' OpenRouter API key (starts with sk-or-)');
|
|
216
|
+
cfg.LLM_MODEL = await ask(' Model to use (leave blank for claude-sonnet-4-5)', '');
|
|
217
|
+
} else if (cfg.PROVIDER_LLM === 'anthropic') {
|
|
218
|
+
cfg.ANTHROPIC_API_KEY = await askSecret(' Anthropic API key (starts with sk-ant-)');
|
|
219
|
+
} else if (cfg.PROVIDER_LLM === 'gemini') {
|
|
220
|
+
cfg.GEMINI_API_KEY = await askSecret(' Gemini API key');
|
|
221
|
+
} else if (cfg.PROVIDER_LLM === 'mistral') {
|
|
222
|
+
cfg.MISTRAL_API_KEY = await askSecret(' Mistral API key');
|
|
223
|
+
} else if (cfg.PROVIDER_LLM === 'together') {
|
|
224
|
+
cfg.TOGETHER_API_KEY = await askSecret(' Together AI API key');
|
|
225
|
+
} else if (cfg.PROVIDER_LLM === 'openai') {
|
|
226
|
+
cfg.OPENAI_API_KEY = await askSecret(' OpenAI API key (starts with sk-)');
|
|
227
|
+
} else if (cfg.PROVIDER_LLM === 'ollama') {
|
|
228
|
+
cfg.OLLAMA_BASE_URL = await ask(' Ollama base URL', 'http://localhost:11434/api');
|
|
229
|
+
cfg.OLLAMA_MODEL = await ask(' Ollama model name', 'llama3.3');
|
|
230
|
+
} else if (cfg.PROVIDER_LLM === 'webhook') {
|
|
231
|
+
cfg.WEBHOOK_LLM_URL = await ask(' Webhook URL (e.g. https://voxforge.example.com/v1/chat)');
|
|
232
|
+
cfg.WEBHOOK_LLM_HEADERS = await ask(' Extra headers JSON (or leave blank)', '{}');
|
|
233
|
+
cfg.WEBHOOK_LLM_RESPONSE_PATH = await ask(' JSON response path', 'choices.0.message.content');
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
// ── TTS ───────────────────────────────────────────────────────────────────
|
|
237
|
+
section('2 / 5 — TTS Provider (voice generation)');
|
|
238
|
+
console.log(' Used for: converting narration text to audio.');
|
|
239
|
+
console.log(' Free option: edge-tts (400+ voices, no account needed).\n');
|
|
240
|
+
|
|
241
|
+
cfg.PROVIDER_TTS = await menu('Choose TTS provider', [
|
|
242
|
+
['edge-tts', 'edge-tts — free, offline, 400+ voices. No key needed.'],
|
|
243
|
+
['elevenlabs', 'ElevenLabs — best quality + voice cloning. elevenlabs.io'],
|
|
244
|
+
['azure', 'Azure Cognitive Speech. portal.azure.com'],
|
|
245
|
+
['openai', 'OpenAI TTS (uses OPENAI_API_KEY above)'],
|
|
246
|
+
['webhook', 'Custom/VoxForge/any TTS endpoint'],
|
|
247
|
+
], 'edge-tts');
|
|
248
|
+
|
|
249
|
+
if (cfg.PROVIDER_TTS === 'elevenlabs') {
|
|
250
|
+
cfg.ELEVENLABS_API_KEY = await askSecret(' ElevenLabs API key');
|
|
251
|
+
cfg.ELEVENLABS_VOICE_ID = await ask(' Voice ID (leave blank for default Adam)', 'pNInz6obpgDQGcFmaJgB');
|
|
252
|
+
console.log(' ℹ️ For voice cloning: set ELEVENLABS_VOICE_ID to your cloned voice ID.');
|
|
253
|
+
} else if (cfg.PROVIDER_TTS === 'azure') {
|
|
254
|
+
cfg.AZURE_SPEECH_KEY = await askSecret(' Azure Speech key');
|
|
255
|
+
cfg.AZURE_SPEECH_REGION = await ask(' Azure region', 'eastus');
|
|
256
|
+
} else if (cfg.PROVIDER_TTS === 'webhook') {
|
|
257
|
+
cfg.WEBHOOK_TTS_URL = await ask(' Webhook TTS URL');
|
|
258
|
+
cfg.WEBHOOK_TTS_RESPONSE = await ask(' Response type: binary | json_url', 'binary');
|
|
259
|
+
cfg.WEBHOOK_TTS_RESPONSE_PATH = await ask(' JSON path to audio URL (if json_url)', 'audio_url');
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
// ── Image ─────────────────────────────────────────────────────────────────
|
|
263
|
+
section('3 / 5 — Image Generation Provider');
|
|
264
|
+
console.log(' Used for: generating AI visuals for each scene.');
|
|
265
|
+
console.log(' Skip this if you supply your own images (--no-images).');
|
|
266
|
+
console.log(' Free option: Pollinations (no key needed).\n');
|
|
267
|
+
|
|
268
|
+
cfg.PROVIDER_IMAGE = await menu('Choose image provider', [
|
|
269
|
+
['pollinations', 'Pollinations — free, no key, FLUX model. Best default.'],
|
|
270
|
+
['falai', 'fal.ai — high quality, fast. Get key: fal.ai'],
|
|
271
|
+
['replicate', 'Replicate FLUX-schnell. Get key: replicate.com'],
|
|
272
|
+
['dalle', 'DALL-E 3 (uses OPENAI_API_KEY above)'],
|
|
273
|
+
['webhook', 'Custom/external image API endpoint'],
|
|
274
|
+
], 'pollinations');
|
|
275
|
+
|
|
276
|
+
if (cfg.PROVIDER_IMAGE === 'falai') {
|
|
277
|
+
cfg.FALAI_API_KEY = await askSecret(' fal.ai API key');
|
|
278
|
+
} else if (cfg.PROVIDER_IMAGE === 'replicate') {
|
|
279
|
+
cfg.REPLICATE_API_KEY = await askSecret(' Replicate API key');
|
|
280
|
+
} else if (cfg.PROVIDER_IMAGE === 'webhook') {
|
|
281
|
+
cfg.WEBHOOK_IMAGE_URL = await ask(' Webhook image URL');
|
|
282
|
+
cfg.WEBHOOK_IMAGE_RESPONSE = await ask(' Response type: binary | json_url', 'json_url');
|
|
283
|
+
cfg.WEBHOOK_IMAGE_RESPONSE_PATH = await ask(' JSON path to image URL', 'url');
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
// ── Publish webhook ────────────────────────────────────────────────────────
|
|
287
|
+
section('4 / 4 — Publish Webhook (optional)');
|
|
288
|
+
console.log(' Used for: receiving a JSON summary after each video is published.');
|
|
289
|
+
console.log(' POST is sent to PUBLISH_WEBHOOK_URL with productionId, masterPath, title, etc.');
|
|
290
|
+
console.log(' Works with n8n, Make, Zapier, or any HTTP endpoint.\n');
|
|
291
|
+
|
|
292
|
+
const wantWebhook = await ask(' Enable publish webhook? (y/n)', 'n');
|
|
293
|
+
if (wantWebhook.toLowerCase() === 'y') {
|
|
294
|
+
cfg.PUBLISH_WEBHOOK_URL = await ask(' Webhook URL');
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
const wantReviewNotify = await ask(' Enable fact-check review notifications? (y/n)', 'n');
|
|
298
|
+
if (wantReviewNotify.toLowerCase() === 'y') {
|
|
299
|
+
cfg.REVIEW_NOTIFY_URL = await ask(' Review notify URL');
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
// ── External adapter (VoxForge / amg555 / any platform) ────────────────────
|
|
303
|
+
section('Bonus — External Platform Adapter (optional)');
|
|
304
|
+
console.log(' Connect any external platform (e.g. VoxForge, amg555, Remotion, Runway).');
|
|
305
|
+
console.log(' Frames can be sent to / received from any HTTP endpoint.\n');
|
|
306
|
+
|
|
307
|
+
const wantExt = await ask(' Connect an external platform? (y/n)', 'n');
|
|
308
|
+
if (wantExt.toLowerCase() === 'y') {
|
|
309
|
+
cfg.EXTERNAL_PLATFORM_NAME = await ask(' Platform name (label only)', 'voxforge');
|
|
310
|
+
cfg.EXTERNAL_PLATFORM_URL = await ask(' Platform base URL (e.g. https://api.voxforge.io)');
|
|
311
|
+
cfg.EXTERNAL_PLATFORM_KEY = await askSecret(' API key (leave blank if none)');
|
|
312
|
+
cfg.EXTERNAL_PLATFORM_INGEST_PATH = await ask(' Media ingest path', '/v1/ingest');
|
|
313
|
+
cfg.EXTERNAL_PLATFORM_RENDER_PATH = await ask(' Render/export path', '/v1/render');
|
|
314
|
+
cfg.EXTERNAL_PLATFORM_STATUS_PATH = await ask(' Status poll path', '/v1/status');
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
// ── Write .env ─────────────────────────────────────────────────────────────
|
|
318
|
+
const outPath = path.resolve(process.cwd(), outFile);
|
|
319
|
+
const lines = [
|
|
320
|
+
'# FrameForge environment — generated by setup-keys wizard',
|
|
321
|
+
`# Generated: ${new Date().toISOString()}`,
|
|
322
|
+
'# DO NOT commit this file. It is already in .gitignore.',
|
|
323
|
+
'',
|
|
324
|
+
'# ── LLM ─────────────────────────────────────────────────────────',
|
|
325
|
+
];
|
|
326
|
+
|
|
327
|
+
const groups = {
|
|
328
|
+
'# ── LLM ─────────────────────────────────────────────────────────': ['PROVIDER_LLM','GROQ_API_KEY','CEREBRAS_API_KEY','OPENROUTER_API_KEY','ANTHROPIC_API_KEY','GEMINI_API_KEY','MISTRAL_API_KEY','TOGETHER_API_KEY','OPENAI_API_KEY','OLLAMA_BASE_URL','OLLAMA_MODEL','WEBHOOK_LLM_URL','WEBHOOK_LLM_HEADERS','WEBHOOK_LLM_RESPONSE_PATH','LLM_MODEL'],
|
|
329
|
+
'# ── TTS ─────────────────────────────────────────────────────────': ['PROVIDER_TTS','ELEVENLABS_API_KEY','ELEVENLABS_VOICE_ID','AZURE_SPEECH_KEY','AZURE_SPEECH_REGION','WEBHOOK_TTS_URL','WEBHOOK_TTS_RESPONSE','WEBHOOK_TTS_RESPONSE_PATH','TTS_VOICE','TTS_RATE'],
|
|
330
|
+
'# ── Image generation ─────────────────────────────────────────────': ['PROVIDER_IMAGE','FALAI_API_KEY','REPLICATE_API_KEY','WEBHOOK_IMAGE_URL','WEBHOOK_IMAGE_RESPONSE','WEBHOOK_IMAGE_RESPONSE_PATH','POLLINATIONS_MODEL'],
|
|
331
|
+
'# ── Delivery webhooks ────────────────────────────────────────────': ['PUBLISH_WEBHOOK_URL','REVIEW_NOTIFY_URL'],
|
|
332
|
+
'# ── External platform adapter ────────────────────────────────────': ['EXTERNAL_PLATFORM_NAME','EXTERNAL_PLATFORM_URL','EXTERNAL_PLATFORM_KEY','EXTERNAL_PLATFORM_INGEST_PATH','EXTERNAL_PLATFORM_RENDER_PATH','EXTERNAL_PLATFORM_STATUS_PATH'],
|
|
333
|
+
};
|
|
334
|
+
|
|
335
|
+
const envLines = ['# FrameForge environment — generated by setup-keys wizard', `# Generated: ${new Date().toISOString()}`, '# DO NOT commit this file. It is already in .gitignore.', ''];
|
|
336
|
+
for (const [header, keys] of Object.entries(groups)) {
|
|
337
|
+
const relevant = keys.filter(k => cfg[k] !== undefined);
|
|
338
|
+
if (relevant.length === 0) continue;
|
|
339
|
+
envLines.push(header);
|
|
340
|
+
for (const k of relevant) {
|
|
341
|
+
if (cfg[k]) envLines.push(`${k}=${cfg[k]}`);
|
|
342
|
+
}
|
|
343
|
+
envLines.push('');
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
fs.writeFileSync(outPath, envLines.join('\n'), 'utf8');
|
|
347
|
+
|
|
348
|
+
// Also write to ~/.frameforge/config.json so `npx frameforge-mcp` picks up
|
|
349
|
+
// the keys from any directory without a .env file in the project root.
|
|
350
|
+
try {
|
|
351
|
+
fs.mkdirSync(CONFIG_DIR, { recursive: true });
|
|
352
|
+
// Merge into existing config (preserves keys not set in this wizard run)
|
|
353
|
+
let existing = {};
|
|
354
|
+
if (fs.existsSync(CONFIG_FILE)) {
|
|
355
|
+
try { existing = JSON.parse(fs.readFileSync(CONFIG_FILE, 'utf8')); } catch { existing = {}; }
|
|
356
|
+
}
|
|
357
|
+
const merged = { ...existing, ...cfg };
|
|
358
|
+
// Omit blank values
|
|
359
|
+
for (const k of Object.keys(merged)) {
|
|
360
|
+
if (!merged[k]) delete merged[k];
|
|
361
|
+
}
|
|
362
|
+
fs.writeFileSync(CONFIG_FILE, JSON.stringify(merged, null, 2) + '\n', { encoding: 'utf8', mode: 0o600 });
|
|
363
|
+
console.log(`\n✅ Keys saved to: ${outPath}`);
|
|
364
|
+
console.log(` Keys also saved to: ${CONFIG_FILE} (used by npx frameforge-mcp)`);
|
|
365
|
+
} catch (cfgErr) {
|
|
366
|
+
console.log(`\n✅ .env written to: ${outPath}`);
|
|
367
|
+
console.warn(` (Could not write global config: ${cfgErr.message})`);
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
// Detect whether running via npx (package in npm cache) or from a cloned repo.
|
|
371
|
+
// When run via npx the file lives inside npm's temp cache, not under a project path.
|
|
372
|
+
const isNpx = __filename.includes('_npx') || __filename.includes(path.join('npm', 'cache'));
|
|
373
|
+
console.log(`\n Next steps:`);
|
|
374
|
+
if (isNpx) {
|
|
375
|
+
console.log(` 1. Validate keys: npx frameforge-doctor`);
|
|
376
|
+
console.log(` 2. Add to your IDE: see https://github.com/AMG555/FrameForge#connect-your-ide`);
|
|
377
|
+
console.log(` 3. Generate a video: npx frameforge --topic "Your topic"`);
|
|
378
|
+
} else {
|
|
379
|
+
console.log(` 1. Review the file: cat ${outFile}`);
|
|
380
|
+
console.log(` 2. Validate keys: npm run doctor`);
|
|
381
|
+
console.log(` 3. Generate a video: npm run make -- --topic "Your topic"`);
|
|
382
|
+
}
|
|
383
|
+
console.log('');
|
|
384
|
+
|
|
385
|
+
rl.close();
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
main().catch(err => {
|
|
389
|
+
console.error(`\n❌ Setup wizard failed: ${err.message}\n`);
|
|
390
|
+
rl.close();
|
|
391
|
+
process.exit(1);
|
|
392
|
+
});
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* FrameForge Config Loader
|
|
3
|
+
*
|
|
4
|
+
* Loads API keys and provider settings from ~/.frameforge/config.json
|
|
5
|
+
* into process.env — used by `npx frameforge-mcp` so keys set once via
|
|
6
|
+
* `npx frameforge-setup` work from any project directory without a .env file.
|
|
7
|
+
*
|
|
8
|
+
* Priority (highest to lowest):
|
|
9
|
+
* 1. process.env already set (shell export, .env loaded by caller, etc.)
|
|
10
|
+
* 2. ~/.frameforge/config.json (written by `npx frameforge-setup`)
|
|
11
|
+
* 3. .env in cwd (legacy / project-scoped override)
|
|
12
|
+
*
|
|
13
|
+
* Safe to call multiple times — idempotent.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
'use strict';
|
|
17
|
+
|
|
18
|
+
const fs = require('fs');
|
|
19
|
+
const path = require('path');
|
|
20
|
+
const os = require('os');
|
|
21
|
+
|
|
22
|
+
const CONFIG_DIR = path.join(os.homedir(), '.frameforge');
|
|
23
|
+
const CONFIG_FILE = path.join(CONFIG_DIR, 'config.json');
|
|
24
|
+
const CWD_ENV = path.join(process.cwd(), '.env');
|
|
25
|
+
|
|
26
|
+
let _loaded = false;
|
|
27
|
+
|
|
28
|
+
function loadConfig() {
|
|
29
|
+
if (_loaded) return;
|
|
30
|
+
_loaded = true;
|
|
31
|
+
|
|
32
|
+
// 1. Load .env from cwd first (lowest priority — user project overrides)
|
|
33
|
+
_loadDotEnv(CWD_ENV);
|
|
34
|
+
|
|
35
|
+
// 2. Load ~/.frameforge/config.json (higher priority — global npx config)
|
|
36
|
+
_loadJsonConfig(CONFIG_FILE);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** Parse a .env file and inject missing keys into process.env */
|
|
40
|
+
function _loadDotEnv(envPath) {
|
|
41
|
+
if (!fs.existsSync(envPath)) return;
|
|
42
|
+
try {
|
|
43
|
+
const lines = fs.readFileSync(envPath, 'utf8').split('\n');
|
|
44
|
+
for (const line of lines) {
|
|
45
|
+
const stripped = line.trim();
|
|
46
|
+
if (!stripped || stripped.startsWith('#')) continue;
|
|
47
|
+
const eq = stripped.indexOf('=');
|
|
48
|
+
if (eq < 1) continue;
|
|
49
|
+
const key = stripped.slice(0, eq).trim();
|
|
50
|
+
const val = stripped.slice(eq + 1).trim().replace(/^["']|["']$/g, '');
|
|
51
|
+
if (key && val && process.env[key] === undefined) {
|
|
52
|
+
process.env[key] = val;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
} catch { /* silently skip unreadable .env */ }
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** Parse ~/.frameforge/config.json and inject missing keys into process.env */
|
|
59
|
+
function _loadJsonConfig(configPath) {
|
|
60
|
+
if (!fs.existsSync(configPath)) return;
|
|
61
|
+
try {
|
|
62
|
+
const raw = fs.readFileSync(configPath, 'utf8');
|
|
63
|
+
const cfg = JSON.parse(raw);
|
|
64
|
+
for (const [key, val] of Object.entries(cfg)) {
|
|
65
|
+
if (key && val !== null && val !== undefined && process.env[key] === undefined) {
|
|
66
|
+
process.env[key] = String(val);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
} catch { /* silently skip corrupt config */ }
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
module.exports = { loadConfig, CONFIG_DIR, CONFIG_FILE };
|