@blockrun/franklin 3.2.4 → 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 +54 -13
- package/dist/agent/context.js +31 -1
- package/dist/agent/loop.js +48 -19
- package/dist/agent/permissions.js +3 -3
- 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 +41 -2
- package/dist/events/bridge.d.ts +1 -0
- package/dist/events/bridge.js +24 -0
- package/dist/events/bus.d.ts +17 -0
- package/dist/events/bus.js +55 -0
- package/dist/events/types.d.ts +49 -0
- package/dist/events/types.js +8 -0
- package/dist/index.js +15 -0
- package/dist/learnings/extractor.d.ts +16 -0
- package/dist/learnings/extractor.js +234 -0
- package/dist/learnings/index.d.ts +3 -0
- package/dist/learnings/index.js +2 -0
- package/dist/learnings/store.d.ts +15 -0
- package/dist/learnings/store.js +130 -0
- package/dist/learnings/types.d.ts +24 -0
- package/dist/learnings/types.js +7 -0
- package/dist/mcp/client.js +9 -2
- package/dist/narrative/state.d.ts +30 -0
- package/dist/narrative/state.js +69 -0
- 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/social/browser-pool.d.ts +29 -0
- package/dist/social/browser-pool.js +57 -0
- package/dist/social/preflight.d.ts +14 -0
- package/dist/social/preflight.js +26 -0
- package/dist/social/x.d.ts +8 -0
- package/dist/social/x.js +9 -1
- 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 +3 -0
- package/dist/tools/posttox.d.ts +7 -0
- package/dist/tools/posttox.js +137 -0
- package/dist/tools/searchx.d.ts +7 -0
- package/dist/tools/searchx.js +111 -0
- package/dist/tools/trading.d.ts +3 -0
- package/dist/tools/trading.js +168 -0
- package/dist/tools/webfetch.js +19 -9
- package/dist/tools/write.js +2 -0
- package/dist/trading/config.d.ts +23 -0
- package/dist/trading/config.js +45 -0
- package/dist/trading/data.d.ts +30 -0
- package/dist/trading/data.js +112 -0
- package/dist/trading/metrics.d.ts +29 -0
- package/dist/trading/metrics.js +105 -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,16 +50,33 @@ 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
|
|
56
|
-
|
|
64
|
+
// Model is shown in the live status bar — no static line needed.
|
|
57
65
|
console.log(chalk.dim(` Wallet: ${walletAddress || 'not set'}`));
|
|
58
66
|
console.log(chalk.dim(` Dir: ${workDir}`));
|
|
59
67
|
// First-run tip: show if no config file exists yet
|
|
60
68
|
if (!configModel && !options.model) {
|
|
61
69
|
console.log(chalk.dim(`\n Tip: /model to switch models · /compact to save tokens · /help for all commands`));
|
|
62
70
|
}
|
|
71
|
+
// Welcome message — show things Hermes/OpenClaw can't do.
|
|
72
|
+
// Only on first run or when no model is configured (new user indicator).
|
|
73
|
+
// After the user's first session, the tip fades and they go straight to the prompt.
|
|
74
|
+
console.log('');
|
|
75
|
+
console.log(chalk.dim(' Try something only Franklin can do:'));
|
|
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.'));
|
|
63
80
|
console.log('');
|
|
64
81
|
// Balance fetcher — used at startup and after each turn
|
|
65
82
|
const fetchBalance = async () => {
|
|
@@ -130,6 +147,14 @@ export async function startCommand(options) {
|
|
|
130
147
|
permissionMode: (options.trust || !process.stdin.isTTY) ? 'trust' : 'default',
|
|
131
148
|
debug: options.debug,
|
|
132
149
|
};
|
|
150
|
+
// Bootstrap learnings from Claude Code config on first run (async, non-blocking)
|
|
151
|
+
Promise.all([
|
|
152
|
+
import('../learnings/extractor.js'),
|
|
153
|
+
import('../agent/llm.js'),
|
|
154
|
+
]).then(([{ bootstrapFromClaudeConfig }, { ModelClient }]) => {
|
|
155
|
+
const client = new ModelClient({ apiUrl, chain });
|
|
156
|
+
bootstrapFromClaudeConfig(client).catch(() => { });
|
|
157
|
+
}).catch(() => { });
|
|
133
158
|
// Use Ink UI if TTY, fallback to basic readline for piped input
|
|
134
159
|
if (process.stdin.isTTY) {
|
|
135
160
|
await runWithInkUI(agentConfig, model, workDir, version, walletInfo, (cb) => {
|
|
@@ -167,8 +192,9 @@ async function runWithInkUI(agentConfig, model, workDir, version, walletInfo, on
|
|
|
167
192
|
fetchBalance().then(bal => ui.updateBalance(bal)).catch(() => { });
|
|
168
193
|
});
|
|
169
194
|
}
|
|
195
|
+
let sessionHistory;
|
|
170
196
|
try {
|
|
171
|
-
await interactiveSession(agentConfig, async () => {
|
|
197
|
+
sessionHistory = await interactiveSession(agentConfig, async () => {
|
|
172
198
|
const input = await ui.waitForInput();
|
|
173
199
|
if (input === null)
|
|
174
200
|
return null;
|
|
@@ -184,6 +210,19 @@ async function runWithInkUI(agentConfig, model, workDir, version, walletInfo, on
|
|
|
184
210
|
}
|
|
185
211
|
ui.cleanup();
|
|
186
212
|
flushStats();
|
|
213
|
+
// Extract learnings from the session (async, 10s timeout, never blocks exit)
|
|
214
|
+
if (sessionHistory && sessionHistory.length >= 4) {
|
|
215
|
+
try {
|
|
216
|
+
const { extractLearnings } = await import('../learnings/extractor.js');
|
|
217
|
+
const { ModelClient } = await import('../agent/llm.js');
|
|
218
|
+
const client = new ModelClient({ apiUrl: agentConfig.apiUrl, chain: agentConfig.chain });
|
|
219
|
+
await Promise.race([
|
|
220
|
+
extractLearnings(sessionHistory, `session-${new Date().toISOString()}`, client),
|
|
221
|
+
new Promise(resolve => setTimeout(resolve, 10_000)),
|
|
222
|
+
]);
|
|
223
|
+
}
|
|
224
|
+
catch { /* extraction is best-effort */ }
|
|
225
|
+
}
|
|
187
226
|
await disconnectMcpServers();
|
|
188
227
|
console.log(chalk.dim('\nGoodbye.\n'));
|
|
189
228
|
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function initBridge(): void;
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { bus } from './bus.js';
|
|
2
|
+
import { addSignal, addPost } from '../narrative/state.js';
|
|
3
|
+
export function initBridge() {
|
|
4
|
+
bus.on('signal.detected', (event) => {
|
|
5
|
+
const e = event;
|
|
6
|
+
addSignal({
|
|
7
|
+
asset: e.data.asset,
|
|
8
|
+
direction: e.data.direction,
|
|
9
|
+
confidence: e.data.confidence,
|
|
10
|
+
summary: e.data.summary,
|
|
11
|
+
ts: e.ts,
|
|
12
|
+
});
|
|
13
|
+
});
|
|
14
|
+
bus.on('post.published', (event) => {
|
|
15
|
+
const e = event;
|
|
16
|
+
addPost({
|
|
17
|
+
platform: e.data.platform,
|
|
18
|
+
url: e.data.url,
|
|
19
|
+
text: e.data.text,
|
|
20
|
+
referencesAssets: e.data.referencesAssets,
|
|
21
|
+
ts: e.ts,
|
|
22
|
+
});
|
|
23
|
+
});
|
|
24
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { FranklinEvent } from './types.js';
|
|
2
|
+
type Handler = (event: FranklinEvent) => void | Promise<void>;
|
|
3
|
+
export declare class EventBus {
|
|
4
|
+
private handlers;
|
|
5
|
+
private logEnabled;
|
|
6
|
+
private logPath;
|
|
7
|
+
constructor(opts?: {
|
|
8
|
+
log?: boolean;
|
|
9
|
+
});
|
|
10
|
+
on(type: FranklinEvent['type'], handler: Handler): void;
|
|
11
|
+
off(type: FranklinEvent['type'], handler: Handler): void;
|
|
12
|
+
emit(event: FranklinEvent): Promise<void>;
|
|
13
|
+
clear(): void;
|
|
14
|
+
private appendLog;
|
|
15
|
+
}
|
|
16
|
+
export declare const bus: EventBus;
|
|
17
|
+
export {};
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import os from 'node:os';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import fs from 'node:fs';
|
|
4
|
+
export class EventBus {
|
|
5
|
+
handlers = new Map();
|
|
6
|
+
logEnabled;
|
|
7
|
+
logPath;
|
|
8
|
+
constructor(opts = {}) {
|
|
9
|
+
this.logEnabled = opts.log ?? false;
|
|
10
|
+
this.logPath = path.join(os.homedir(), '.blockrun', 'events.jsonl');
|
|
11
|
+
}
|
|
12
|
+
on(type, handler) {
|
|
13
|
+
let set = this.handlers.get(type);
|
|
14
|
+
if (!set) {
|
|
15
|
+
set = new Set();
|
|
16
|
+
this.handlers.set(type, set);
|
|
17
|
+
}
|
|
18
|
+
set.add(handler);
|
|
19
|
+
}
|
|
20
|
+
off(type, handler) {
|
|
21
|
+
this.handlers.get(type)?.delete(handler);
|
|
22
|
+
}
|
|
23
|
+
async emit(event) {
|
|
24
|
+
if (this.logEnabled) {
|
|
25
|
+
this.appendLog(event);
|
|
26
|
+
}
|
|
27
|
+
const set = this.handlers.get(event.type);
|
|
28
|
+
if (!set)
|
|
29
|
+
return;
|
|
30
|
+
const promises = [];
|
|
31
|
+
for (const handler of set) {
|
|
32
|
+
const result = handler(event);
|
|
33
|
+
if (result)
|
|
34
|
+
promises.push(result);
|
|
35
|
+
}
|
|
36
|
+
if (promises.length)
|
|
37
|
+
await Promise.all(promises);
|
|
38
|
+
}
|
|
39
|
+
clear() {
|
|
40
|
+
this.handlers.clear();
|
|
41
|
+
}
|
|
42
|
+
appendLog(event) {
|
|
43
|
+
try {
|
|
44
|
+
const dir = path.dirname(this.logPath);
|
|
45
|
+
if (!fs.existsSync(dir)) {
|
|
46
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
47
|
+
}
|
|
48
|
+
fs.appendFileSync(this.logPath, JSON.stringify(event) + '\n');
|
|
49
|
+
}
|
|
50
|
+
catch {
|
|
51
|
+
// best-effort logging — don't crash the agent
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
export const bus = new EventBus();
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
export interface BaseEvent {
|
|
2
|
+
id: string;
|
|
3
|
+
type: string;
|
|
4
|
+
ts: string;
|
|
5
|
+
source: 'trading' | 'social' | 'core';
|
|
6
|
+
costUsd?: number;
|
|
7
|
+
correlationId?: string;
|
|
8
|
+
}
|
|
9
|
+
export interface SignalDetectedEvent extends BaseEvent {
|
|
10
|
+
type: 'signal.detected';
|
|
11
|
+
data: {
|
|
12
|
+
asset: string;
|
|
13
|
+
direction: 'bullish' | 'bearish' | 'neutral';
|
|
14
|
+
confidence: number;
|
|
15
|
+
indicators: Record<string, number>;
|
|
16
|
+
summary: string;
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
export interface PostPublishedEvent extends BaseEvent {
|
|
20
|
+
type: 'post.published';
|
|
21
|
+
data: {
|
|
22
|
+
platform: 'x' | 'reddit' | (string & {});
|
|
23
|
+
url: string;
|
|
24
|
+
text: string;
|
|
25
|
+
referencesAssets?: string[];
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
export interface MentionReceivedEvent extends BaseEvent {
|
|
29
|
+
type: 'mention.received';
|
|
30
|
+
data: {
|
|
31
|
+
platform: string;
|
|
32
|
+
url: string;
|
|
33
|
+
text: string;
|
|
34
|
+
author: string;
|
|
35
|
+
sentiment?: 'positive' | 'negative' | 'neutral';
|
|
36
|
+
mentionsAsset?: string;
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
export interface BudgetExceededEvent extends BaseEvent {
|
|
40
|
+
type: 'budget.exceeded';
|
|
41
|
+
data: {
|
|
42
|
+
category: 'llm' | 'data' | 'gas';
|
|
43
|
+
spent: number;
|
|
44
|
+
cap: number;
|
|
45
|
+
blockedAction: string;
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
export type FranklinEvent = SignalDetectedEvent | PostPublishedEvent | MentionReceivedEvent | BudgetExceededEvent;
|
|
49
|
+
export declare function makeEvent<T extends FranklinEvent>(props: Omit<T, 'id' | 'ts'>): T;
|