@blockrun/franklin 3.6.15 → 3.6.17
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/dist/commands/panel.js +44 -21
- package/dist/commands/start.js +2 -2
- package/dist/panel/server.js +128 -107
- package/package.json +1 -1
package/dist/commands/panel.js
CHANGED
|
@@ -4,26 +4,49 @@
|
|
|
4
4
|
import chalk from 'chalk';
|
|
5
5
|
import { createPanelServer } from '../panel/server.js';
|
|
6
6
|
export async function panelCommand(options) {
|
|
7
|
-
const
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
server.
|
|
25
|
-
|
|
7
|
+
const requestedPort = parseInt(options.port || '3100', 10);
|
|
8
|
+
// Handle port-in-use by trying up to 10 subsequent ports.
|
|
9
|
+
const tryListen = (port, attempt) => {
|
|
10
|
+
const server = createPanelServer(port);
|
|
11
|
+
server.on('error', (err) => {
|
|
12
|
+
if (err.code === 'EADDRINUSE' && attempt < 10) {
|
|
13
|
+
console.log(chalk.yellow(` Port ${port} busy — trying ${port + 1}...`));
|
|
14
|
+
tryListen(port + 1, attempt + 1);
|
|
15
|
+
return;
|
|
16
|
+
}
|
|
17
|
+
console.error(chalk.red(`\n Panel failed to start: ${err.message}`));
|
|
18
|
+
if (err.code === 'EADDRINUSE') {
|
|
19
|
+
console.error(chalk.dim(` All ports from ${requestedPort} to ${requestedPort + 9} are busy.`));
|
|
20
|
+
console.error(chalk.dim(` Try: franklin panel --port 4000`));
|
|
21
|
+
}
|
|
22
|
+
process.exit(1);
|
|
23
|
+
});
|
|
24
|
+
server.listen(port, () => {
|
|
25
|
+
console.log('');
|
|
26
|
+
console.log(chalk.bold(' Franklin Panel'));
|
|
27
|
+
console.log(chalk.dim(` http://localhost:${port}`));
|
|
28
|
+
console.log('');
|
|
29
|
+
console.log(chalk.dim(' Press Ctrl+C to stop.'));
|
|
30
|
+
console.log('');
|
|
31
|
+
// Try to open browser
|
|
32
|
+
const open = process.platform === 'darwin' ? 'open' : process.platform === 'win32' ? 'start' : 'xdg-open';
|
|
33
|
+
import('node:child_process').then(({ exec }) => {
|
|
34
|
+
exec(`${open} http://localhost:${port}`);
|
|
35
|
+
}).catch(() => { });
|
|
36
|
+
});
|
|
37
|
+
// Graceful shutdown
|
|
38
|
+
const shutdown = () => {
|
|
39
|
+
server.close();
|
|
40
|
+
process.exit(0);
|
|
41
|
+
};
|
|
42
|
+
process.on('SIGINT', shutdown);
|
|
43
|
+
process.on('SIGTERM', shutdown);
|
|
26
44
|
};
|
|
27
|
-
|
|
28
|
-
process.on('
|
|
45
|
+
// Catch unexpected crashes with a useful message rather than a stack trace
|
|
46
|
+
process.on('uncaughtException', (err) => {
|
|
47
|
+
console.error(chalk.red(`\n Panel crashed: ${err.message}`));
|
|
48
|
+
console.error(chalk.dim(' Open an issue: https://github.com/BlockRunAI/Franklin/issues'));
|
|
49
|
+
process.exit(1);
|
|
50
|
+
});
|
|
51
|
+
tryListen(requestedPort, 0);
|
|
29
52
|
}
|
package/dist/commands/start.js
CHANGED
|
@@ -73,8 +73,8 @@ export async function startCommand(options) {
|
|
|
73
73
|
printBanner(version);
|
|
74
74
|
const workDir = process.cwd();
|
|
75
75
|
// Session info — aligned, minimal. Model + balance live in the input bar below.
|
|
76
|
-
|
|
77
|
-
console.log(chalk.dim(' Wallet: ') + (walletAddress
|
|
76
|
+
// Full wallet address is shown so the user can copy-paste it to fund the wallet.
|
|
77
|
+
console.log(chalk.dim(' Wallet: ') + (walletAddress || chalk.yellow('not set')));
|
|
78
78
|
console.log(chalk.dim(' Dir: ') + workDir);
|
|
79
79
|
console.log(chalk.dim(' Dashboard: ') + chalk.cyan('franklin panel') + chalk.dim(' → http://localhost:3100'));
|
|
80
80
|
console.log(chalk.dim(' Help: ') + chalk.cyan('/help'));
|
package/dist/panel/server.js
CHANGED
|
@@ -36,130 +36,151 @@ function broadcast(data) {
|
|
|
36
36
|
export function createPanelServer(port) {
|
|
37
37
|
const html = getHTML();
|
|
38
38
|
const server = http.createServer(async (req, res) => {
|
|
39
|
-
const url = new URL(req.url || '/', `http://localhost:${port}`);
|
|
40
|
-
const p = url.pathname;
|
|
41
|
-
// ─── HTML ──
|
|
42
|
-
if (p === '/') {
|
|
43
|
-
res.writeHead(200, {
|
|
44
|
-
'Content-Type': 'text/html; charset=utf-8',
|
|
45
|
-
'Cache-Control': 'no-store, no-cache, must-revalidate',
|
|
46
|
-
'Pragma': 'no-cache',
|
|
47
|
-
});
|
|
48
|
-
res.end(html);
|
|
49
|
-
return;
|
|
50
|
-
}
|
|
51
|
-
// ─── Static assets ──
|
|
52
|
-
if (p.startsWith('/assets/') && p.endsWith('.jpg')) {
|
|
53
|
-
const filename = path.basename(p);
|
|
54
|
-
const assetsDir = path.join(path.dirname(path.dirname(new URL(import.meta.url).pathname)), '..', 'assets');
|
|
55
|
-
const imgPath = path.join(assetsDir, filename);
|
|
56
|
-
try {
|
|
57
|
-
const img = fs.readFileSync(imgPath);
|
|
58
|
-
res.writeHead(200, {
|
|
59
|
-
'Content-Type': 'image/jpeg',
|
|
60
|
-
'Cache-Control': 'public, max-age=86400',
|
|
61
|
-
});
|
|
62
|
-
res.end(img);
|
|
63
|
-
}
|
|
64
|
-
catch {
|
|
65
|
-
res.writeHead(404);
|
|
66
|
-
res.end('Not found');
|
|
67
|
-
}
|
|
68
|
-
return;
|
|
69
|
-
}
|
|
70
|
-
// ─── SSE ──
|
|
71
|
-
if (p === '/api/events') {
|
|
72
|
-
res.writeHead(200, {
|
|
73
|
-
'Content-Type': 'text/event-stream',
|
|
74
|
-
'Cache-Control': 'no-cache',
|
|
75
|
-
'Connection': 'keep-alive',
|
|
76
|
-
'Access-Control-Allow-Origin': '*',
|
|
77
|
-
});
|
|
78
|
-
res.write('data: {"type":"connected"}\n\n');
|
|
79
|
-
sseClients.add(res);
|
|
80
|
-
req.on('close', () => sseClients.delete(res));
|
|
81
|
-
return;
|
|
82
|
-
}
|
|
83
|
-
// ─── API ──
|
|
84
39
|
try {
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
avgCostPerRequest: summary.avgCostPerRequest,
|
|
94
|
-
period: summary.period,
|
|
95
|
-
byModel: summary.stats.byModel,
|
|
40
|
+
const url = new URL(req.url || '/', `http://localhost:${port}`);
|
|
41
|
+
const p = url.pathname;
|
|
42
|
+
// ─── HTML ──
|
|
43
|
+
if (p === '/') {
|
|
44
|
+
res.writeHead(200, {
|
|
45
|
+
'Content-Type': 'text/html; charset=utf-8',
|
|
46
|
+
'Cache-Control': 'no-store, no-cache, must-revalidate',
|
|
47
|
+
'Pragma': 'no-cache',
|
|
96
48
|
});
|
|
49
|
+
res.end(html);
|
|
97
50
|
return;
|
|
98
51
|
}
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
const
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
52
|
+
// ─── Static assets ──
|
|
53
|
+
if (p.startsWith('/assets/') && p.endsWith('.jpg')) {
|
|
54
|
+
const filename = path.basename(p);
|
|
55
|
+
const assetsDir = path.join(path.dirname(path.dirname(new URL(import.meta.url).pathname)), '..', 'assets');
|
|
56
|
+
const imgPath = path.join(assetsDir, filename);
|
|
57
|
+
try {
|
|
58
|
+
const img = fs.readFileSync(imgPath);
|
|
59
|
+
res.writeHead(200, {
|
|
60
|
+
'Content-Type': 'image/jpeg',
|
|
61
|
+
'Cache-Control': 'public, max-age=86400',
|
|
62
|
+
});
|
|
63
|
+
res.end(img);
|
|
64
|
+
}
|
|
65
|
+
catch {
|
|
66
|
+
res.writeHead(404);
|
|
67
|
+
res.end('Not found');
|
|
68
|
+
}
|
|
115
69
|
return;
|
|
116
70
|
}
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
71
|
+
// ─── SSE ──
|
|
72
|
+
if (p === '/api/events') {
|
|
73
|
+
res.writeHead(200, {
|
|
74
|
+
'Content-Type': 'text/event-stream',
|
|
75
|
+
'Cache-Control': 'no-cache',
|
|
76
|
+
'Connection': 'keep-alive',
|
|
77
|
+
'Access-Control-Allow-Origin': '*',
|
|
78
|
+
});
|
|
79
|
+
res.write('data: {"type":"connected"}\n\n');
|
|
80
|
+
sseClients.add(res);
|
|
81
|
+
req.on('close', () => sseClients.delete(res));
|
|
121
82
|
return;
|
|
122
83
|
}
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
84
|
+
// ─── API ──
|
|
85
|
+
try {
|
|
86
|
+
if (p === '/api/stats') {
|
|
87
|
+
const summary = getStatsSummary();
|
|
88
|
+
json(res, {
|
|
89
|
+
totalRequests: summary.stats.totalRequests,
|
|
90
|
+
totalCostUsd: summary.stats.totalCostUsd,
|
|
91
|
+
opusCost: summary.opusCost,
|
|
92
|
+
saved: summary.saved,
|
|
93
|
+
savedPct: summary.savedPct,
|
|
94
|
+
avgCostPerRequest: summary.avgCostPerRequest,
|
|
95
|
+
period: summary.period,
|
|
96
|
+
byModel: summary.stats.byModel,
|
|
97
|
+
});
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
if (p === '/api/insights') {
|
|
101
|
+
const days = parseInt(url.searchParams.get('days') || '30', 10);
|
|
102
|
+
const report = generateInsights(days);
|
|
103
|
+
json(res, report);
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
if (p === '/api/sessions') {
|
|
107
|
+
const sessions = listSessions();
|
|
108
|
+
json(res, sessions);
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
if (p.startsWith('/api/sessions/search')) {
|
|
112
|
+
const q = url.searchParams.get('q') || '';
|
|
113
|
+
const limit = parseInt(url.searchParams.get('limit') || '20', 10);
|
|
114
|
+
const results = searchSessions(q, { limit });
|
|
115
|
+
json(res, results);
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
if (p.startsWith('/api/sessions/')) {
|
|
119
|
+
const id = decodeURIComponent(p.slice('/api/sessions/'.length));
|
|
120
|
+
const history = loadSessionHistory(id);
|
|
121
|
+
json(res, history);
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
if (p === '/api/wallet') {
|
|
125
|
+
try {
|
|
126
|
+
const chain = loadChain();
|
|
127
|
+
let address = '', balance = 0;
|
|
128
|
+
if (chain === 'solana') {
|
|
129
|
+
const { setupAgentSolanaWallet } = await import('@blockrun/llm');
|
|
130
|
+
const client = await setupAgentSolanaWallet({ silent: true });
|
|
131
|
+
address = await client.getWalletAddress();
|
|
132
|
+
balance = await client.getBalance();
|
|
133
|
+
}
|
|
134
|
+
else {
|
|
135
|
+
const { setupAgentWallet } = await import('@blockrun/llm');
|
|
136
|
+
const client = setupAgentWallet({ silent: true });
|
|
137
|
+
address = client.getWalletAddress();
|
|
138
|
+
balance = await client.getBalance();
|
|
139
|
+
}
|
|
140
|
+
json(res, { address, balance, chain });
|
|
132
141
|
}
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
const client = setupAgentWallet({ silent: true });
|
|
136
|
-
address = client.getWalletAddress();
|
|
137
|
-
balance = await client.getBalance();
|
|
142
|
+
catch {
|
|
143
|
+
json(res, { address: 'not set', balance: 0, chain: loadChain() });
|
|
138
144
|
}
|
|
139
|
-
|
|
145
|
+
return;
|
|
140
146
|
}
|
|
141
|
-
|
|
142
|
-
|
|
147
|
+
if (p === '/api/social') {
|
|
148
|
+
const stats = getSocialStats();
|
|
149
|
+
json(res, stats);
|
|
150
|
+
return;
|
|
143
151
|
}
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
152
|
+
if (p === '/api/learnings') {
|
|
153
|
+
const learnings = loadLearnings();
|
|
154
|
+
json(res, learnings);
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
157
|
+
// 404
|
|
158
|
+
res.writeHead(404);
|
|
159
|
+
res.end('Not found');
|
|
150
160
|
}
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
json(res, learnings);
|
|
154
|
-
return;
|
|
161
|
+
catch (err) {
|
|
162
|
+
json(res, { error: err.message }, 500);
|
|
155
163
|
}
|
|
156
|
-
// 404
|
|
157
|
-
res.writeHead(404);
|
|
158
|
-
res.end('Not found');
|
|
159
164
|
}
|
|
160
165
|
catch (err) {
|
|
161
|
-
|
|
166
|
+
// Outer safety net — logs but never crashes the server
|
|
167
|
+
try {
|
|
168
|
+
if (!res.headersSent)
|
|
169
|
+
json(res, { error: err.message }, 500);
|
|
170
|
+
else
|
|
171
|
+
res.end();
|
|
172
|
+
}
|
|
173
|
+
catch { /* socket already gone */ }
|
|
174
|
+
console.error('[panel] request error:', err.message);
|
|
175
|
+
}
|
|
176
|
+
});
|
|
177
|
+
// Swallow socket errors (client disconnects, etc.) so they don't crash the process
|
|
178
|
+
server.on('clientError', (err, socket) => {
|
|
179
|
+
try {
|
|
180
|
+
socket.destroy();
|
|
162
181
|
}
|
|
182
|
+
catch { /* already closed */ }
|
|
183
|
+
console.error('[panel] client error:', err.message);
|
|
163
184
|
});
|
|
164
185
|
// Watch stats file for changes → push to SSE clients
|
|
165
186
|
const statsFile = fs.existsSync(path.join(BLOCKRUN_DIR, 'franklin-stats.json'))
|
package/package.json
CHANGED