@blockrun/franklin 3.3.0 → 3.3.2

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,413 @@
1
+ /**
2
+ * franklin migrate — one-click import from other AI coding agents.
3
+ *
4
+ * Detects installed tools (Claude Code, Cline, Cursor, etc.),
5
+ * shows what can be migrated, and imports with user confirmation.
6
+ */
7
+ import fs from 'node:fs';
8
+ import path from 'node:path';
9
+ import os from 'node:os';
10
+ import readline from 'node:readline';
11
+ import chalk from 'chalk';
12
+ import { BLOCKRUN_DIR } from '../config.js';
13
+ function detectSources() {
14
+ const sources = [];
15
+ const home = os.homedir();
16
+ // ── Claude Code ──
17
+ const claudeDir = path.join(home, '.claude');
18
+ if (fs.existsSync(claudeDir)) {
19
+ const items = [];
20
+ // MCP servers
21
+ const claudeMcp = path.join(claudeDir, 'mcp.json');
22
+ if (fs.existsSync(claudeMcp)) {
23
+ items.push({
24
+ label: 'MCP servers',
25
+ source: claudeMcp,
26
+ target: path.join(BLOCKRUN_DIR, 'mcp.json'),
27
+ size: fileSize(claudeMcp),
28
+ transform: () => migrateMcp(claudeMcp),
29
+ });
30
+ }
31
+ // Global instructions → learnings
32
+ const claudeMd = path.join(claudeDir, 'CLAUDE.md');
33
+ if (fs.existsSync(claudeMd)) {
34
+ items.push({
35
+ label: 'Global instructions (CLAUDE.md)',
36
+ source: claudeMd,
37
+ target: path.join(BLOCKRUN_DIR, 'learnings.jsonl'),
38
+ size: fileSize(claudeMd),
39
+ transform: () => migrateInstructions(claudeMd),
40
+ });
41
+ }
42
+ // Session history
43
+ const claudeHistory = path.join(claudeDir, 'history.jsonl');
44
+ if (fs.existsSync(claudeHistory)) {
45
+ const lines = countLines(claudeHistory);
46
+ items.push({
47
+ label: `Session history (${lines.toLocaleString()} messages)`,
48
+ source: claudeHistory,
49
+ target: path.join(BLOCKRUN_DIR, 'sessions'),
50
+ size: fileSize(claudeHistory),
51
+ transform: () => migrateSessions(claudeHistory),
52
+ });
53
+ }
54
+ // Project memory files
55
+ const projectsDir = path.join(claudeDir, 'projects');
56
+ if (fs.existsSync(projectsDir)) {
57
+ const memoryFiles = findMemoryFiles(projectsDir);
58
+ if (memoryFiles.length > 0) {
59
+ items.push({
60
+ label: `Project memories (${memoryFiles.length} files)`,
61
+ source: projectsDir,
62
+ target: path.join(BLOCKRUN_DIR, 'learnings.jsonl'),
63
+ size: `${memoryFiles.length} files`,
64
+ transform: () => migrateMemories(memoryFiles),
65
+ });
66
+ }
67
+ }
68
+ if (items.length > 0) {
69
+ sources.push({ name: 'Claude Code', dir: claudeDir, items });
70
+ }
71
+ }
72
+ // ── Cline / OpenClaw ──
73
+ const clineDir = path.join(home, 'Library', 'Application Support', 'Code', 'User', 'globalStorage', 'saoudrizwan.claude-dev');
74
+ if (fs.existsSync(clineDir)) {
75
+ const items = [];
76
+ // TODO: detect Cline data
77
+ if (items.length > 0) {
78
+ sources.push({ name: 'Cline', dir: clineDir, items });
79
+ }
80
+ }
81
+ // ── Cursor ──
82
+ const cursorDir = path.join(home, 'Library', 'Application Support', 'Cursor');
83
+ if (fs.existsSync(cursorDir)) {
84
+ const items = [];
85
+ // TODO: detect Cursor data
86
+ if (items.length > 0) {
87
+ sources.push({ name: 'Cursor', dir: cursorDir, items });
88
+ }
89
+ }
90
+ return sources;
91
+ }
92
+ // ─── Transforms ───────────────────────────────────────────────────────────
93
+ function migrateMcp(source) {
94
+ const target = path.join(BLOCKRUN_DIR, 'mcp.json');
95
+ const raw = JSON.parse(fs.readFileSync(source, 'utf-8'));
96
+ // Claude Code format: { mcpServers: { name: { command, args, env } } }
97
+ // Franklin format: { mcpServers: { name: { transport, command, args, label } } }
98
+ const servers = {};
99
+ const skipped = [];
100
+ if (raw.mcpServers) {
101
+ for (const [name, config] of Object.entries(raw.mcpServers)) {
102
+ // Skip MCP servers that require external credentials (OAuth, API keys,
103
+ // tokens) — importing them causes noisy startup errors because the
104
+ // credentials aren't available in Franklin's context. Users can add
105
+ // these manually via ~/.blockrun/mcp.json if they set up the credentials.
106
+ const configStr = JSON.stringify(config).toLowerCase();
107
+ const needsCredentials = configStr.includes('oauth') ||
108
+ configStr.includes('credential') ||
109
+ configStr.includes('api_key') ||
110
+ configStr.includes('api-key') ||
111
+ configStr.includes('token') ||
112
+ name.includes('calendar') ||
113
+ name.includes('gmail') ||
114
+ name.includes('google') ||
115
+ name.includes('slack') ||
116
+ name.includes('notion');
117
+ if (needsCredentials) {
118
+ skipped.push(name);
119
+ continue;
120
+ }
121
+ servers[name] = {
122
+ transport: config.transport || 'stdio',
123
+ command: config.command,
124
+ args: config.args || [],
125
+ label: name,
126
+ ...(config.env ? { env: config.env } : {}),
127
+ };
128
+ }
129
+ }
130
+ // Merge with existing Franklin MCP config
131
+ let existing = {};
132
+ try {
133
+ if (fs.existsSync(target)) {
134
+ existing = JSON.parse(fs.readFileSync(target, 'utf-8'));
135
+ }
136
+ }
137
+ catch { /* start fresh */ }
138
+ const merged = {
139
+ mcpServers: {
140
+ ...(existing.mcpServers || {}),
141
+ ...servers,
142
+ },
143
+ };
144
+ fs.mkdirSync(BLOCKRUN_DIR, { recursive: true });
145
+ fs.writeFileSync(target, JSON.stringify(merged, null, 2));
146
+ const importedCount = Object.keys(servers).length;
147
+ console.log(chalk.green(` ✓ ${importedCount} MCP server(s) imported`));
148
+ if (skipped.length > 0) {
149
+ console.log(chalk.dim(` · ${skipped.length} skipped (need credentials): ${skipped.join(', ')}`));
150
+ }
151
+ }
152
+ function migrateInstructions(source) {
153
+ // Read CLAUDE.md and convert key preferences to learnings
154
+ const content = fs.readFileSync(source, 'utf-8');
155
+ const learningsPath = path.join(BLOCKRUN_DIR, 'learnings.jsonl');
156
+ // Extract simple preference lines as learnings
157
+ const lines = content.split('\n');
158
+ const learnings = [];
159
+ const now = Date.now();
160
+ let count = 0;
161
+ for (const line of lines) {
162
+ const trimmed = line.trim();
163
+ // Skip empty lines, headers, and code blocks
164
+ if (!trimmed || trimmed.startsWith('#') || trimmed.startsWith('```') || trimmed.startsWith('|'))
165
+ continue;
166
+ // Skip very short or very long lines
167
+ if (trimmed.length < 15 || trimmed.length > 200)
168
+ continue;
169
+ // Skip lines that are just paths or URLs
170
+ if (trimmed.startsWith('/') || trimmed.startsWith('http'))
171
+ continue;
172
+ // Lines starting with - or * are likely preference rules
173
+ if (trimmed.startsWith('- ') || trimmed.startsWith('* ')) {
174
+ const text = trimmed.slice(2).trim();
175
+ if (text.length > 15) {
176
+ const entry = {
177
+ id: `migrate-${count++}`,
178
+ learning: text.slice(0, 200),
179
+ category: 'other',
180
+ confidence: 0.8,
181
+ source_session: 'migrate:claude-code',
182
+ created_at: now,
183
+ last_confirmed: now,
184
+ times_confirmed: 1,
185
+ };
186
+ learnings.push(JSON.stringify(entry));
187
+ }
188
+ }
189
+ }
190
+ if (learnings.length > 0) {
191
+ fs.mkdirSync(BLOCKRUN_DIR, { recursive: true });
192
+ // Append to existing learnings
193
+ fs.appendFileSync(learningsPath, learnings.join('\n') + '\n');
194
+ console.log(chalk.green(` ✓ ${learnings.length} preferences imported`));
195
+ }
196
+ else {
197
+ console.log(chalk.dim(' ○ No extractable preferences found'));
198
+ }
199
+ }
200
+ function migrateSessions(source) {
201
+ const sessionsDir = path.join(BLOCKRUN_DIR, 'sessions');
202
+ fs.mkdirSync(sessionsDir, { recursive: true });
203
+ const raw = fs.readFileSync(source, 'utf-8');
204
+ const lines = raw.split('\n').filter(l => l.trim());
205
+ // Group by conversation turns — each user+assistant pair is a chunk
206
+ // We'll create session files grouped by day
207
+ const sessions = new Map();
208
+ for (const line of lines) {
209
+ try {
210
+ const msg = JSON.parse(line);
211
+ // Use date from the line or current date as session key
212
+ const dateKey = new Date().toISOString().split('T')[0];
213
+ // Try to extract timestamp if present
214
+ const ts = msg.timestamp || msg.created_at || msg.ts;
215
+ const key = ts ? new Date(ts).toISOString().split('T')[0] : dateKey;
216
+ if (!sessions.has(key))
217
+ sessions.set(key, []);
218
+ sessions.get(key).push(line);
219
+ }
220
+ catch {
221
+ // Skip unparseable lines
222
+ }
223
+ }
224
+ let imported = 0;
225
+ for (const [dateKey, msgs] of sessions) {
226
+ const sessionId = `imported-${dateKey}`;
227
+ const sessionFile = path.join(sessionsDir, `${sessionId}.jsonl`);
228
+ // Don't overwrite existing imported sessions
229
+ if (fs.existsSync(sessionFile))
230
+ continue;
231
+ fs.writeFileSync(sessionFile, msgs.join('\n') + '\n');
232
+ // Create metadata
233
+ const meta = {
234
+ id: sessionId,
235
+ model: 'imported',
236
+ workDir: os.homedir(),
237
+ createdAt: new Date(dateKey).getTime(),
238
+ updatedAt: Date.now(),
239
+ turnCount: Math.floor(msgs.length / 2),
240
+ messageCount: msgs.length,
241
+ };
242
+ fs.writeFileSync(path.join(sessionsDir, `${sessionId}.meta.json`), JSON.stringify(meta, null, 2));
243
+ imported++;
244
+ }
245
+ console.log(chalk.green(` ✓ ${lines.length.toLocaleString()} messages → ${imported} session(s)`));
246
+ }
247
+ function migrateMemories(files) {
248
+ const learningsPath = path.join(BLOCKRUN_DIR, 'learnings.jsonl');
249
+ const now = Date.now();
250
+ let count = 0;
251
+ const entries = [];
252
+ for (const file of files) {
253
+ try {
254
+ const content = fs.readFileSync(file, 'utf-8');
255
+ const lines = content.split('\n');
256
+ for (const line of lines) {
257
+ const trimmed = line.trim();
258
+ if (!trimmed || trimmed.startsWith('#') || trimmed.startsWith('```'))
259
+ continue;
260
+ if (trimmed.startsWith('- ') && trimmed.length > 20 && trimmed.length < 200) {
261
+ const text = trimmed.slice(2).trim();
262
+ // Skip index entries (links to other files)
263
+ if (text.startsWith('[') && text.includes(']('))
264
+ continue;
265
+ entries.push(JSON.stringify({
266
+ id: `memory-${count++}`,
267
+ learning: text.slice(0, 200),
268
+ category: 'other',
269
+ confidence: 0.7,
270
+ source_session: 'migrate:project-memory',
271
+ created_at: now,
272
+ last_confirmed: now,
273
+ times_confirmed: 1,
274
+ }));
275
+ }
276
+ }
277
+ }
278
+ catch { /* skip unreadable files */ }
279
+ }
280
+ if (entries.length > 0) {
281
+ fs.mkdirSync(BLOCKRUN_DIR, { recursive: true });
282
+ fs.appendFileSync(learningsPath, entries.join('\n') + '\n');
283
+ console.log(chalk.green(` ✓ ${entries.length} memories imported`));
284
+ }
285
+ else {
286
+ console.log(chalk.dim(' ○ No extractable memories found'));
287
+ }
288
+ }
289
+ // ─── Helpers ──────────────────────────────────────────────────────────────
290
+ function fileSize(p) {
291
+ try {
292
+ const bytes = fs.statSync(p).size;
293
+ if (bytes < 1024)
294
+ return `${bytes} B`;
295
+ if (bytes < 1024 * 1024)
296
+ return `${(bytes / 1024).toFixed(1)} KB`;
297
+ return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
298
+ }
299
+ catch {
300
+ return '?';
301
+ }
302
+ }
303
+ function countLines(p) {
304
+ try {
305
+ return fs.readFileSync(p, 'utf-8').split('\n').filter(l => l.trim()).length;
306
+ }
307
+ catch {
308
+ return 0;
309
+ }
310
+ }
311
+ function findMemoryFiles(projectsDir) {
312
+ const files = [];
313
+ try {
314
+ for (const project of fs.readdirSync(projectsDir)) {
315
+ const memoryDir = path.join(projectsDir, project, 'memory');
316
+ if (!fs.existsSync(memoryDir))
317
+ continue;
318
+ for (const file of fs.readdirSync(memoryDir)) {
319
+ if (file.endsWith('.md') && file !== 'MEMORY.md') {
320
+ files.push(path.join(memoryDir, file));
321
+ }
322
+ }
323
+ }
324
+ }
325
+ catch { /* ignore */ }
326
+ return files;
327
+ }
328
+ // ─── Interactive prompt ───────────────────────────────────────────────────
329
+ function ask(question) {
330
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
331
+ return new Promise(resolve => {
332
+ rl.question(question, answer => { rl.close(); resolve(answer.trim().toLowerCase()); });
333
+ });
334
+ }
335
+ // ─── Main command ─────────────────────────────────────────────────────────
336
+ export async function migrateCommand() {
337
+ console.log(chalk.bold('\n franklin migrate\n'));
338
+ const sources = detectSources();
339
+ if (sources.length === 0) {
340
+ console.log(chalk.dim(' No other AI tools detected. Nothing to migrate.\n'));
341
+ console.log(chalk.dim(' Supported: Claude Code, Cline, Cursor\n'));
342
+ return;
343
+ }
344
+ // Show what was found
345
+ for (const source of sources) {
346
+ console.log(chalk.bold(` ${chalk.green('●')} ${source.name}`) + chalk.dim(` (${source.dir})`));
347
+ for (const item of source.items) {
348
+ console.log(chalk.dim(` ├─ ${item.label}`) + (item.size ? chalk.dim(` [${item.size}]`) : ''));
349
+ }
350
+ console.log('');
351
+ }
352
+ const total = sources.reduce((n, s) => n + s.items.length, 0);
353
+ const answer = await ask(chalk.yellow(` Import ${total} item(s) into Franklin? [Y/n] `));
354
+ if (answer && answer !== 'y' && answer !== 'yes') {
355
+ console.log(chalk.dim('\n Cancelled.\n'));
356
+ return;
357
+ }
358
+ console.log('');
359
+ // Run migrations
360
+ for (const source of sources) {
361
+ console.log(chalk.bold(` Migrating from ${source.name}...`));
362
+ for (const item of source.items) {
363
+ try {
364
+ item.transform();
365
+ }
366
+ catch (err) {
367
+ console.log(chalk.red(` ✗ ${item.label}: ${err.message}`));
368
+ }
369
+ }
370
+ console.log('');
371
+ }
372
+ console.log(chalk.green(' Done.') + chalk.dim(' Run `franklin --trust` to start.\n'));
373
+ }
374
+ // ─── First-run detection (called from start.ts) ──────────────────────────
375
+ const MIGRATED_MARKER = path.join(BLOCKRUN_DIR, '.migrated');
376
+ /**
377
+ * Check if other AI tools are installed and suggest migration.
378
+ * Only runs once — writes a marker file after first check.
379
+ * Returns true if the user chose to migrate (caller should re-run start after).
380
+ */
381
+ export async function checkAndSuggestMigration() {
382
+ // Only suggest once
383
+ if (fs.existsSync(MIGRATED_MARKER))
384
+ return false;
385
+ // Write marker immediately so we never ask again
386
+ fs.mkdirSync(BLOCKRUN_DIR, { recursive: true });
387
+ fs.writeFileSync(MIGRATED_MARKER, new Date().toISOString());
388
+ const sources = detectSources();
389
+ if (sources.length === 0)
390
+ return false;
391
+ const names = sources.map(s => s.name).join(', ');
392
+ const total = sources.reduce((n, s) => n + s.items.length, 0);
393
+ console.log(chalk.bold(`\n ${chalk.green('●')} Found ${names} — ${total} items available to import.`));
394
+ const answer = await ask(chalk.yellow(` Import into Franklin? [Y/n] `));
395
+ if (answer && answer !== 'y' && answer !== 'yes') {
396
+ console.log(chalk.dim(' Skipped. Run `franklin migrate` anytime.\n'));
397
+ return false;
398
+ }
399
+ console.log('');
400
+ for (const source of sources) {
401
+ console.log(chalk.bold(` Migrating from ${source.name}...`));
402
+ for (const item of source.items) {
403
+ try {
404
+ item.transform();
405
+ }
406
+ catch (err) {
407
+ console.log(chalk.red(` ✗ ${item.label}: ${err.message}`));
408
+ }
409
+ }
410
+ }
411
+ console.log(chalk.green('\n Done.') + ' Starting Franklin...\n');
412
+ return true;
413
+ }
@@ -0,0 +1,6 @@
1
+ /**
2
+ * franklin panel — launch the local web dashboard.
3
+ */
4
+ export declare function panelCommand(options: {
5
+ port?: string;
6
+ }): Promise<void>;
@@ -0,0 +1,29 @@
1
+ /**
2
+ * franklin panel — launch the local web dashboard.
3
+ */
4
+ import chalk from 'chalk';
5
+ import { createPanelServer } from '../panel/server.js';
6
+ export async function panelCommand(options) {
7
+ const port = parseInt(options.port || '3100', 10);
8
+ const server = createPanelServer(port);
9
+ server.listen(port, () => {
10
+ console.log('');
11
+ console.log(chalk.bold(' Franklin Panel'));
12
+ console.log(chalk.dim(` http://localhost:${port}`));
13
+ console.log('');
14
+ console.log(chalk.dim(' Press Ctrl+C to stop.'));
15
+ console.log('');
16
+ // Try to open browser
17
+ const open = process.platform === 'darwin' ? 'open' : process.platform === 'win32' ? 'start' : 'xdg-open';
18
+ import('node:child_process').then(({ exec }) => {
19
+ exec(`${open} http://localhost:${port}`);
20
+ }).catch(() => { });
21
+ });
22
+ // Graceful shutdown
23
+ const shutdown = () => {
24
+ server.close();
25
+ process.exit(0);
26
+ };
27
+ process.on('SIGINT', shutdown);
28
+ process.on('SIGTERM', shutdown);
29
+ }
@@ -50,6 +50,14 @@ export async function startCommand(options) {
50
50
  console.log(chalk.dim(' Free models work now. Fund with USDC for paid models.\n'));
51
51
  }
52
52
  }
53
+ // First-run: detect other AI tools and offer migration
54
+ if (process.stdin.isTTY) {
55
+ try {
56
+ const { checkAndSuggestMigration } = await import('./migrate.js');
57
+ await checkAndSuggestMigration();
58
+ }
59
+ catch { /* migration is optional */ }
60
+ }
53
61
  printBanner(version);
54
62
  const workDir = process.cwd();
55
63
  // Show session info immediately, fetch balance in background
@@ -65,10 +73,10 @@ export async function startCommand(options) {
65
73
  // After the user's first session, the tip fades and they go straight to the prompt.
66
74
  console.log('');
67
75
  console.log(chalk.dim(' Try something only Franklin can do:'));
68
- console.log(chalk.dim(' ') + chalk.hex('#FFD700')('"what\'s BTC looking like today?"') + chalk.dim(' live market signal'));
69
- console.log(chalk.dim(' ') + chalk.hex('#10B981')('"find X posts about ai agent"') + chalk.dim(' ← social growth'));
70
- console.log(chalk.dim(' ') + chalk.hex('#60A5FA')('"generate a hero image for my app"') + chalk.dim(' AI image gen'));
71
- console.log(chalk.dim(' Or just code — 55+ models ready, no API keys needed.'));
76
+ console.log(chalk.dim(' ') + chalk.hex('#FFD700')('"what\'s BTC looking like today?"') + chalk.dim(' ← market signal'));
77
+ console.log(chalk.dim(' ') + chalk.hex('#10B981')('"find X posts about ai agent"') + chalk.dim(' ← social growth'));
78
+ console.log(chalk.dim(' ') + chalk.hex('#60A5FA')('"generate a hero image for my app"') + chalk.dim(' ← image gen'));
79
+ console.log(chalk.dim(' Or just code — 55+ models, no API keys.'));
72
80
  console.log('');
73
81
  // Balance fetcher — used at startup and after each turn
74
82
  const fetchBalance = async () => {
package/dist/index.js CHANGED
@@ -64,6 +64,14 @@ program
64
64
  .description('Manage runcode background proxy (start|stop|status)')
65
65
  .option('-p, --port <port>', 'Proxy port', '8402')
66
66
  .action((action, options) => daemonCommand(action, options));
67
+ program
68
+ .command('panel')
69
+ .description('Open the Franklin dashboard (localhost:3100)')
70
+ .option('-p, --port <port>', 'Dashboard port', '3100')
71
+ .action(async (options) => {
72
+ const { panelCommand } = await import('./commands/panel.js');
73
+ await panelCommand(options);
74
+ });
67
75
  program
68
76
  .command('models')
69
77
  .description('List available models and pricing')
@@ -149,6 +157,13 @@ program
149
157
  });
150
158
  }
151
159
  }
160
+ program
161
+ .command('migrate')
162
+ .description('Import data from other AI tools (Claude Code, Cline, Cursor)')
163
+ .action(async () => {
164
+ const { migrateCommand } = await import('./commands/migrate.js');
165
+ await migrateCommand();
166
+ });
152
167
  program
153
168
  .command('plugins')
154
169
  .description('List installed plugins')
@@ -19,6 +19,11 @@ async function connectStdio(name, config) {
19
19
  command: config.command,
20
20
  args: config.args || [],
21
21
  env: { ...process.env, ...(config.env || {}) },
22
+ // 'ignore' discards subprocess stderr completely so a misconfigured MCP
23
+ // server (e.g. missing OAuth keys) can't dump multi-line stack traces
24
+ // into the user's terminal. 'pipe' didn't fully work because some SDK
25
+ // versions read piped stderr and re-emit it.
26
+ stderr: 'ignore',
22
27
  });
23
28
  const client = new Client({ name: `runcode-mcp-${name}`, version: '1.0.0' }, { capabilities: {} });
24
29
  try {
@@ -111,8 +116,10 @@ export async function connectMcpServers(config, debug) {
111
116
  }
112
117
  }
113
118
  catch (err) {
114
- // Graceful degradation — log and continue without this server
115
- console.error(`[runcode] MCP ${name} failed: ${err.message}`);
119
+ // Graceful degradation — one-line warning, continue without this server.
120
+ // Always visible (not debug-only) so the user knows why tools are missing.
121
+ const shortMsg = err.message?.split('\n')[0]?.slice(0, 100) || 'unknown error';
122
+ console.error(` ${name}: ${shortMsg} ${debug ? '' : '(--debug for details)'}`);
116
123
  }
117
124
  }
118
125
  return allTools;
@@ -86,6 +86,34 @@ export function loadMcpConfig(workDir) {
86
86
  catch {
87
87
  // Ignore corrupt project config
88
88
  }
89
+ // Filter out servers whose required credential files are missing.
90
+ // This prevents noisy startup errors from MCP servers that were imported
91
+ // (e.g. via `franklin migrate`) but don't have credentials configured yet.
92
+ // The server stays in the config file — it just gets auto-disabled until
93
+ // the user provides the credentials.
94
+ for (const [name, config] of Object.entries(servers)) {
95
+ if (config.disabled)
96
+ continue;
97
+ const env = (config.env || {});
98
+ const args = (config.args || []);
99
+ const configStr = JSON.stringify(config).toLowerCase();
100
+ // Check if any env var points to a file that doesn't exist
101
+ let missingFile = false;
102
+ for (const [, val] of Object.entries(env)) {
103
+ if (typeof val === 'string' && (val.endsWith('.json') || val.endsWith('.key') || val.endsWith('.pem'))) {
104
+ if (!fs.existsSync(val)) {
105
+ missingFile = true;
106
+ break;
107
+ }
108
+ }
109
+ }
110
+ // Check for common credential-dependent patterns in config
111
+ const needsAuth = missingFile ||
112
+ (configStr.includes('oauth') && !args.some(a => fs.existsSync(a)));
113
+ if (needsAuth) {
114
+ servers[name].disabled = true;
115
+ }
116
+ }
89
117
  return { mcpServers: servers };
90
118
  }
91
119
  /**
@@ -0,0 +1,5 @@
1
+ /**
2
+ * Franklin Panel — embedded HTML dashboard.
3
+ * Single page, dark theme, zero dependencies.
4
+ */
5
+ export declare function getHTML(): string;