@blockrun/franklin 3.3.0 → 3.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/README.md +216 -233
- package/dist/agent/commands.js +24 -12
- package/dist/agent/context.js +18 -1
- package/dist/agent/loop.js +48 -19
- package/dist/banner.js +40 -27
- package/dist/commands/migrate.d.ts +13 -0
- package/dist/commands/migrate.js +389 -0
- package/dist/commands/panel.d.ts +6 -0
- package/dist/commands/panel.js +29 -0
- package/dist/commands/start.js +12 -4
- package/dist/index.js +15 -0
- package/dist/mcp/client.js +9 -2
- package/dist/panel/html.d.ts +5 -0
- package/dist/panel/html.js +341 -0
- package/dist/panel/server.d.ts +7 -0
- package/dist/panel/server.js +152 -0
- package/dist/session/storage.js +4 -2
- package/dist/stats/tracker.d.ts +1 -0
- package/dist/stats/tracker.js +59 -13
- package/dist/tools/bash.js +6 -1
- package/dist/tools/index.js +0 -4
- package/dist/tools/webfetch.js +19 -9
- package/dist/tools/write.js +2 -0
- package/dist/ui/app.js +73 -44
- package/dist/ui/markdown.d.ts +9 -0
- package/dist/ui/markdown.js +86 -0
- package/package.json +1 -1
|
@@ -0,0 +1,389 @@
|
|
|
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
|
+
if (raw.mcpServers) {
|
|
100
|
+
for (const [name, config] of Object.entries(raw.mcpServers)) {
|
|
101
|
+
servers[name] = {
|
|
102
|
+
transport: config.transport || 'stdio',
|
|
103
|
+
command: config.command,
|
|
104
|
+
args: config.args || [],
|
|
105
|
+
label: name,
|
|
106
|
+
...(config.env ? { env: config.env } : {}),
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
// Merge with existing Franklin MCP config
|
|
111
|
+
let existing = {};
|
|
112
|
+
try {
|
|
113
|
+
if (fs.existsSync(target)) {
|
|
114
|
+
existing = JSON.parse(fs.readFileSync(target, 'utf-8'));
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
catch { /* start fresh */ }
|
|
118
|
+
const merged = {
|
|
119
|
+
mcpServers: {
|
|
120
|
+
...(existing.mcpServers || {}),
|
|
121
|
+
...servers,
|
|
122
|
+
},
|
|
123
|
+
};
|
|
124
|
+
fs.mkdirSync(BLOCKRUN_DIR, { recursive: true });
|
|
125
|
+
fs.writeFileSync(target, JSON.stringify(merged, null, 2));
|
|
126
|
+
console.log(chalk.green(` ✓ ${Object.keys(servers).length} MCP server(s) imported`));
|
|
127
|
+
}
|
|
128
|
+
function migrateInstructions(source) {
|
|
129
|
+
// Read CLAUDE.md and convert key preferences to learnings
|
|
130
|
+
const content = fs.readFileSync(source, 'utf-8');
|
|
131
|
+
const learningsPath = path.join(BLOCKRUN_DIR, 'learnings.jsonl');
|
|
132
|
+
// Extract simple preference lines as learnings
|
|
133
|
+
const lines = content.split('\n');
|
|
134
|
+
const learnings = [];
|
|
135
|
+
const now = Date.now();
|
|
136
|
+
let count = 0;
|
|
137
|
+
for (const line of lines) {
|
|
138
|
+
const trimmed = line.trim();
|
|
139
|
+
// Skip empty lines, headers, and code blocks
|
|
140
|
+
if (!trimmed || trimmed.startsWith('#') || trimmed.startsWith('```') || trimmed.startsWith('|'))
|
|
141
|
+
continue;
|
|
142
|
+
// Skip very short or very long lines
|
|
143
|
+
if (trimmed.length < 15 || trimmed.length > 200)
|
|
144
|
+
continue;
|
|
145
|
+
// Skip lines that are just paths or URLs
|
|
146
|
+
if (trimmed.startsWith('/') || trimmed.startsWith('http'))
|
|
147
|
+
continue;
|
|
148
|
+
// Lines starting with - or * are likely preference rules
|
|
149
|
+
if (trimmed.startsWith('- ') || trimmed.startsWith('* ')) {
|
|
150
|
+
const text = trimmed.slice(2).trim();
|
|
151
|
+
if (text.length > 15) {
|
|
152
|
+
const entry = {
|
|
153
|
+
id: `migrate-${count++}`,
|
|
154
|
+
learning: text.slice(0, 200),
|
|
155
|
+
category: 'other',
|
|
156
|
+
confidence: 0.8,
|
|
157
|
+
source_session: 'migrate:claude-code',
|
|
158
|
+
created_at: now,
|
|
159
|
+
last_confirmed: now,
|
|
160
|
+
times_confirmed: 1,
|
|
161
|
+
};
|
|
162
|
+
learnings.push(JSON.stringify(entry));
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
if (learnings.length > 0) {
|
|
167
|
+
fs.mkdirSync(BLOCKRUN_DIR, { recursive: true });
|
|
168
|
+
// Append to existing learnings
|
|
169
|
+
fs.appendFileSync(learningsPath, learnings.join('\n') + '\n');
|
|
170
|
+
console.log(chalk.green(` ✓ ${learnings.length} preferences imported`));
|
|
171
|
+
}
|
|
172
|
+
else {
|
|
173
|
+
console.log(chalk.dim(' ○ No extractable preferences found'));
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
function migrateSessions(source) {
|
|
177
|
+
const sessionsDir = path.join(BLOCKRUN_DIR, 'sessions');
|
|
178
|
+
fs.mkdirSync(sessionsDir, { recursive: true });
|
|
179
|
+
const raw = fs.readFileSync(source, 'utf-8');
|
|
180
|
+
const lines = raw.split('\n').filter(l => l.trim());
|
|
181
|
+
// Group by conversation turns — each user+assistant pair is a chunk
|
|
182
|
+
// We'll create session files grouped by day
|
|
183
|
+
const sessions = new Map();
|
|
184
|
+
for (const line of lines) {
|
|
185
|
+
try {
|
|
186
|
+
const msg = JSON.parse(line);
|
|
187
|
+
// Use date from the line or current date as session key
|
|
188
|
+
const dateKey = new Date().toISOString().split('T')[0];
|
|
189
|
+
// Try to extract timestamp if present
|
|
190
|
+
const ts = msg.timestamp || msg.created_at || msg.ts;
|
|
191
|
+
const key = ts ? new Date(ts).toISOString().split('T')[0] : dateKey;
|
|
192
|
+
if (!sessions.has(key))
|
|
193
|
+
sessions.set(key, []);
|
|
194
|
+
sessions.get(key).push(line);
|
|
195
|
+
}
|
|
196
|
+
catch {
|
|
197
|
+
// Skip unparseable lines
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
let imported = 0;
|
|
201
|
+
for (const [dateKey, msgs] of sessions) {
|
|
202
|
+
const sessionId = `imported-${dateKey}`;
|
|
203
|
+
const sessionFile = path.join(sessionsDir, `${sessionId}.jsonl`);
|
|
204
|
+
// Don't overwrite existing imported sessions
|
|
205
|
+
if (fs.existsSync(sessionFile))
|
|
206
|
+
continue;
|
|
207
|
+
fs.writeFileSync(sessionFile, msgs.join('\n') + '\n');
|
|
208
|
+
// Create metadata
|
|
209
|
+
const meta = {
|
|
210
|
+
id: sessionId,
|
|
211
|
+
model: 'imported',
|
|
212
|
+
workDir: os.homedir(),
|
|
213
|
+
createdAt: new Date(dateKey).getTime(),
|
|
214
|
+
updatedAt: Date.now(),
|
|
215
|
+
turnCount: Math.floor(msgs.length / 2),
|
|
216
|
+
messageCount: msgs.length,
|
|
217
|
+
};
|
|
218
|
+
fs.writeFileSync(path.join(sessionsDir, `${sessionId}.meta.json`), JSON.stringify(meta, null, 2));
|
|
219
|
+
imported++;
|
|
220
|
+
}
|
|
221
|
+
console.log(chalk.green(` ✓ ${lines.length.toLocaleString()} messages → ${imported} session(s)`));
|
|
222
|
+
}
|
|
223
|
+
function migrateMemories(files) {
|
|
224
|
+
const learningsPath = path.join(BLOCKRUN_DIR, 'learnings.jsonl');
|
|
225
|
+
const now = Date.now();
|
|
226
|
+
let count = 0;
|
|
227
|
+
const entries = [];
|
|
228
|
+
for (const file of files) {
|
|
229
|
+
try {
|
|
230
|
+
const content = fs.readFileSync(file, 'utf-8');
|
|
231
|
+
const lines = content.split('\n');
|
|
232
|
+
for (const line of lines) {
|
|
233
|
+
const trimmed = line.trim();
|
|
234
|
+
if (!trimmed || trimmed.startsWith('#') || trimmed.startsWith('```'))
|
|
235
|
+
continue;
|
|
236
|
+
if (trimmed.startsWith('- ') && trimmed.length > 20 && trimmed.length < 200) {
|
|
237
|
+
const text = trimmed.slice(2).trim();
|
|
238
|
+
// Skip index entries (links to other files)
|
|
239
|
+
if (text.startsWith('[') && text.includes(']('))
|
|
240
|
+
continue;
|
|
241
|
+
entries.push(JSON.stringify({
|
|
242
|
+
id: `memory-${count++}`,
|
|
243
|
+
learning: text.slice(0, 200),
|
|
244
|
+
category: 'other',
|
|
245
|
+
confidence: 0.7,
|
|
246
|
+
source_session: 'migrate:project-memory',
|
|
247
|
+
created_at: now,
|
|
248
|
+
last_confirmed: now,
|
|
249
|
+
times_confirmed: 1,
|
|
250
|
+
}));
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
catch { /* skip unreadable files */ }
|
|
255
|
+
}
|
|
256
|
+
if (entries.length > 0) {
|
|
257
|
+
fs.mkdirSync(BLOCKRUN_DIR, { recursive: true });
|
|
258
|
+
fs.appendFileSync(learningsPath, entries.join('\n') + '\n');
|
|
259
|
+
console.log(chalk.green(` ✓ ${entries.length} memories imported`));
|
|
260
|
+
}
|
|
261
|
+
else {
|
|
262
|
+
console.log(chalk.dim(' ○ No extractable memories found'));
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
// ─── Helpers ──────────────────────────────────────────────────────────────
|
|
266
|
+
function fileSize(p) {
|
|
267
|
+
try {
|
|
268
|
+
const bytes = fs.statSync(p).size;
|
|
269
|
+
if (bytes < 1024)
|
|
270
|
+
return `${bytes} B`;
|
|
271
|
+
if (bytes < 1024 * 1024)
|
|
272
|
+
return `${(bytes / 1024).toFixed(1)} KB`;
|
|
273
|
+
return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
|
|
274
|
+
}
|
|
275
|
+
catch {
|
|
276
|
+
return '?';
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
function countLines(p) {
|
|
280
|
+
try {
|
|
281
|
+
return fs.readFileSync(p, 'utf-8').split('\n').filter(l => l.trim()).length;
|
|
282
|
+
}
|
|
283
|
+
catch {
|
|
284
|
+
return 0;
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
function findMemoryFiles(projectsDir) {
|
|
288
|
+
const files = [];
|
|
289
|
+
try {
|
|
290
|
+
for (const project of fs.readdirSync(projectsDir)) {
|
|
291
|
+
const memoryDir = path.join(projectsDir, project, 'memory');
|
|
292
|
+
if (!fs.existsSync(memoryDir))
|
|
293
|
+
continue;
|
|
294
|
+
for (const file of fs.readdirSync(memoryDir)) {
|
|
295
|
+
if (file.endsWith('.md') && file !== 'MEMORY.md') {
|
|
296
|
+
files.push(path.join(memoryDir, file));
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
catch { /* ignore */ }
|
|
302
|
+
return files;
|
|
303
|
+
}
|
|
304
|
+
// ─── Interactive prompt ───────────────────────────────────────────────────
|
|
305
|
+
function ask(question) {
|
|
306
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
307
|
+
return new Promise(resolve => {
|
|
308
|
+
rl.question(question, answer => { rl.close(); resolve(answer.trim().toLowerCase()); });
|
|
309
|
+
});
|
|
310
|
+
}
|
|
311
|
+
// ─── Main command ─────────────────────────────────────────────────────────
|
|
312
|
+
export async function migrateCommand() {
|
|
313
|
+
console.log(chalk.bold('\n franklin migrate\n'));
|
|
314
|
+
const sources = detectSources();
|
|
315
|
+
if (sources.length === 0) {
|
|
316
|
+
console.log(chalk.dim(' No other AI tools detected. Nothing to migrate.\n'));
|
|
317
|
+
console.log(chalk.dim(' Supported: Claude Code, Cline, Cursor\n'));
|
|
318
|
+
return;
|
|
319
|
+
}
|
|
320
|
+
// Show what was found
|
|
321
|
+
for (const source of sources) {
|
|
322
|
+
console.log(chalk.bold(` ${chalk.green('●')} ${source.name}`) + chalk.dim(` (${source.dir})`));
|
|
323
|
+
for (const item of source.items) {
|
|
324
|
+
console.log(chalk.dim(` ├─ ${item.label}`) + (item.size ? chalk.dim(` [${item.size}]`) : ''));
|
|
325
|
+
}
|
|
326
|
+
console.log('');
|
|
327
|
+
}
|
|
328
|
+
const total = sources.reduce((n, s) => n + s.items.length, 0);
|
|
329
|
+
const answer = await ask(chalk.yellow(` Import ${total} item(s) into Franklin? [Y/n] `));
|
|
330
|
+
if (answer && answer !== 'y' && answer !== 'yes') {
|
|
331
|
+
console.log(chalk.dim('\n Cancelled.\n'));
|
|
332
|
+
return;
|
|
333
|
+
}
|
|
334
|
+
console.log('');
|
|
335
|
+
// Run migrations
|
|
336
|
+
for (const source of sources) {
|
|
337
|
+
console.log(chalk.bold(` Migrating from ${source.name}...`));
|
|
338
|
+
for (const item of source.items) {
|
|
339
|
+
try {
|
|
340
|
+
item.transform();
|
|
341
|
+
}
|
|
342
|
+
catch (err) {
|
|
343
|
+
console.log(chalk.red(` ✗ ${item.label}: ${err.message}`));
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
console.log('');
|
|
347
|
+
}
|
|
348
|
+
console.log(chalk.green(' Done.') + chalk.dim(' Run `franklin --trust` to start.\n'));
|
|
349
|
+
}
|
|
350
|
+
// ─── First-run detection (called from start.ts) ──────────────────────────
|
|
351
|
+
const MIGRATED_MARKER = path.join(BLOCKRUN_DIR, '.migrated');
|
|
352
|
+
/**
|
|
353
|
+
* Check if other AI tools are installed and suggest migration.
|
|
354
|
+
* Only runs once — writes a marker file after first check.
|
|
355
|
+
* Returns true if the user chose to migrate (caller should re-run start after).
|
|
356
|
+
*/
|
|
357
|
+
export async function checkAndSuggestMigration() {
|
|
358
|
+
// Only suggest once
|
|
359
|
+
if (fs.existsSync(MIGRATED_MARKER))
|
|
360
|
+
return false;
|
|
361
|
+
// Write marker immediately so we never ask again
|
|
362
|
+
fs.mkdirSync(BLOCKRUN_DIR, { recursive: true });
|
|
363
|
+
fs.writeFileSync(MIGRATED_MARKER, new Date().toISOString());
|
|
364
|
+
const sources = detectSources();
|
|
365
|
+
if (sources.length === 0)
|
|
366
|
+
return false;
|
|
367
|
+
const names = sources.map(s => s.name).join(', ');
|
|
368
|
+
const total = sources.reduce((n, s) => n + s.items.length, 0);
|
|
369
|
+
console.log(chalk.bold(`\n ${chalk.green('●')} Found ${names} — ${total} items available to import.`));
|
|
370
|
+
const answer = await ask(chalk.yellow(` Import into Franklin? [Y/n] `));
|
|
371
|
+
if (answer && answer !== 'y' && answer !== 'yes') {
|
|
372
|
+
console.log(chalk.dim(' Skipped. Run `franklin migrate` anytime.\n'));
|
|
373
|
+
return false;
|
|
374
|
+
}
|
|
375
|
+
console.log('');
|
|
376
|
+
for (const source of sources) {
|
|
377
|
+
console.log(chalk.bold(` Migrating from ${source.name}...`));
|
|
378
|
+
for (const item of source.items) {
|
|
379
|
+
try {
|
|
380
|
+
item.transform();
|
|
381
|
+
}
|
|
382
|
+
catch (err) {
|
|
383
|
+
console.log(chalk.red(` ✗ ${item.label}: ${err.message}`));
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
console.log(chalk.green('\n Done.') + ' Starting Franklin...\n');
|
|
388
|
+
return true;
|
|
389
|
+
}
|
|
@@ -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
|
+
}
|
package/dist/commands/start.js
CHANGED
|
@@ -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('
|
|
69
|
-
console.log(chalk.dim(' ') + chalk.hex('#10B981')('"find X posts about ai agent"') + chalk.dim('
|
|
70
|
-
console.log(chalk.dim(' ') + chalk.hex('#60A5FA')('"generate a hero image for my app"') + chalk.dim('
|
|
71
|
-
console.log(chalk.dim(' Or just code — 55+ models
|
|
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')
|
package/dist/mcp/client.js
CHANGED
|
@@ -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 —
|
|
115
|
-
|
|
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;
|