@blockrun/franklin 3.6.16 → 3.6.18
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/init.js +32 -12
- package/dist/commands/models.js +1 -1
- package/dist/commands/panel.js +44 -21
- package/dist/commands/proxy.js +2 -2
- package/dist/commands/stats.js +2 -2
- package/dist/commands/uninit.js +21 -14
- package/dist/index.js +5 -5
- package/dist/panel/server.js +128 -107
- package/dist/plugin-sdk/workflow.js +1 -1
- package/dist/pricing.js +4 -1
- package/dist/session/search.js +1 -1
- package/dist/tools/moa.js +4 -7
- package/dist/wallet/manager.js +1 -1
- package/package.json +1 -1
package/dist/commands/init.js
CHANGED
|
@@ -5,7 +5,8 @@ import chalk from 'chalk';
|
|
|
5
5
|
import { DEFAULT_PROXY_PORT } from '../config.js';
|
|
6
6
|
const CLAUDE_SETTINGS_FILE = path.join(os.homedir(), '.claude', 'settings.json');
|
|
7
7
|
const LAUNCH_AGENT_DIR = path.join(os.homedir(), 'Library', 'LaunchAgents');
|
|
8
|
-
const LAUNCH_AGENT_PLIST = path.join(LAUNCH_AGENT_DIR, 'ai.blockrun.
|
|
8
|
+
const LAUNCH_AGENT_PLIST = path.join(LAUNCH_AGENT_DIR, 'ai.blockrun.franklin.plist');
|
|
9
|
+
const LEGACY_LAUNCH_AGENT_PLIST = path.join(LAUNCH_AGENT_DIR, 'ai.blockrun.runcode.plist');
|
|
9
10
|
export async function initCommand(options) {
|
|
10
11
|
const port = parseInt(options.port || String(DEFAULT_PROXY_PORT));
|
|
11
12
|
if (isNaN(port) || port < 1 || port > 65535) {
|
|
@@ -36,24 +37,43 @@ export async function initCommand(options) {
|
|
|
36
37
|
console.log(chalk.green(`✓ Configured ${CLAUDE_SETTINGS_FILE}`));
|
|
37
38
|
// ── 2. Install macOS LaunchAgent (auto-start on login) ─────────────────
|
|
38
39
|
if (process.platform === 'darwin') {
|
|
39
|
-
|
|
40
|
+
// Clean up legacy runcode LaunchAgent if present
|
|
41
|
+
if (fs.existsSync(LEGACY_LAUNCH_AGENT_PLIST)) {
|
|
42
|
+
try {
|
|
43
|
+
const { execSync } = await import('node:child_process');
|
|
44
|
+
execSync(`launchctl unload -w "${LEGACY_LAUNCH_AGENT_PLIST}"`, { stdio: 'pipe' });
|
|
45
|
+
}
|
|
46
|
+
catch { /* may not be loaded */ }
|
|
47
|
+
try {
|
|
48
|
+
fs.unlinkSync(LEGACY_LAUNCH_AGENT_PLIST);
|
|
49
|
+
}
|
|
50
|
+
catch { /* best effort */ }
|
|
51
|
+
}
|
|
52
|
+
let franklinBin = '';
|
|
40
53
|
try {
|
|
41
54
|
const { execSync } = await import('node:child_process');
|
|
42
|
-
|
|
55
|
+
franklinBin = execSync('which franklin', { encoding: 'utf-8' }).trim();
|
|
43
56
|
}
|
|
44
57
|
catch {
|
|
45
|
-
|
|
58
|
+
// Fall back to legacy binary name
|
|
59
|
+
try {
|
|
60
|
+
const { execSync } = await import('node:child_process');
|
|
61
|
+
franklinBin = execSync('which runcode', { encoding: 'utf-8' }).trim();
|
|
62
|
+
}
|
|
63
|
+
catch {
|
|
64
|
+
console.log(chalk.yellow(' Warning: franklin not found in PATH — LaunchAgent not installed.'));
|
|
65
|
+
}
|
|
46
66
|
}
|
|
47
|
-
if (
|
|
67
|
+
if (franklinBin) {
|
|
48
68
|
const plist = `<?xml version="1.0" encoding="UTF-8"?>
|
|
49
69
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
50
70
|
<plist version="1.0">
|
|
51
71
|
<dict>
|
|
52
72
|
<key>Label</key>
|
|
53
|
-
<string>ai.blockrun.
|
|
73
|
+
<string>ai.blockrun.franklin</string>
|
|
54
74
|
<key>ProgramArguments</key>
|
|
55
75
|
<array>
|
|
56
|
-
<string>${
|
|
76
|
+
<string>${franklinBin}</string>
|
|
57
77
|
<string>proxy</string>
|
|
58
78
|
<string>--port</string>
|
|
59
79
|
<string>${port}</string>
|
|
@@ -73,7 +93,7 @@ export async function initCommand(options) {
|
|
|
73
93
|
try {
|
|
74
94
|
const { execSync } = await import('node:child_process');
|
|
75
95
|
execSync(`launchctl load -w "${LAUNCH_AGENT_PLIST}"`, { stdio: 'pipe' });
|
|
76
|
-
console.log(chalk.green(`✓ LaunchAgent installed —
|
|
96
|
+
console.log(chalk.green(`✓ LaunchAgent installed — franklin proxy starts automatically on login`));
|
|
77
97
|
}
|
|
78
98
|
catch {
|
|
79
99
|
console.log(chalk.dim(` LaunchAgent written to ${LAUNCH_AGENT_PLIST}`));
|
|
@@ -83,10 +103,10 @@ export async function initCommand(options) {
|
|
|
83
103
|
}
|
|
84
104
|
// ── 3. Start daemon now ──────────────────────────────────────────────────
|
|
85
105
|
console.log('');
|
|
86
|
-
console.log(chalk.bold('
|
|
87
|
-
console.log(`Run ${chalk.bold('
|
|
88
|
-
console.log(`Then just run ${chalk.bold('claude')} —
|
|
106
|
+
console.log(chalk.bold('franklin initialized (proxy mode for Claude Code).'));
|
|
107
|
+
console.log(`Run ${chalk.bold('franklin daemon start')} to start the background proxy now.`);
|
|
108
|
+
console.log(`Then just run ${chalk.bold('claude')} — franklin proxy handles payments automatically.`);
|
|
89
109
|
console.log('');
|
|
90
|
-
console.log(chalk.dim('Or use
|
|
110
|
+
console.log(chalk.dim('Or use franklin directly: franklin start'));
|
|
91
111
|
console.log(chalk.dim('Note: Claude Code will ask you to trust the proxy URL once.'));
|
|
92
112
|
}
|
package/dist/commands/models.js
CHANGED
|
@@ -41,7 +41,7 @@ export async function modelsCommand() {
|
|
|
41
41
|
const ctx = '';
|
|
42
42
|
console.log(` ${chalk.cyan(m.id.padEnd(35))} ${input.padEnd(12)} ${output.padEnd(12)} ${ctx}`);
|
|
43
43
|
}
|
|
44
|
-
console.log(`\n${chalk.dim(`${models.length} models available. Use:`)} ${chalk.bold('
|
|
44
|
+
console.log(`\n${chalk.dim(`${models.length} models available. Use:`)} ${chalk.bold('franklin start --model <model-id>')}`);
|
|
45
45
|
}
|
|
46
46
|
catch (err) {
|
|
47
47
|
const msg = err instanceof Error ? err.message : 'unknown error';
|
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/proxy.js
CHANGED
|
@@ -25,7 +25,7 @@ export async function proxyCommand(options) {
|
|
|
25
25
|
if (wallet.isNew) {
|
|
26
26
|
console.log(chalk.yellow('No Solana wallet found — created a new one.'));
|
|
27
27
|
console.log(`Address: ${chalk.cyan(wallet.address)}`);
|
|
28
|
-
console.log(`\nSend USDC on Solana to this address, then run ${chalk.bold('
|
|
28
|
+
console.log(`\nSend USDC on Solana to this address, then run ${chalk.bold('franklin proxy')} again.\n`);
|
|
29
29
|
return;
|
|
30
30
|
}
|
|
31
31
|
printBanner(version);
|
|
@@ -52,7 +52,7 @@ export async function proxyCommand(options) {
|
|
|
52
52
|
if (wallet.isNew) {
|
|
53
53
|
console.log(chalk.yellow('No wallet found — created a new one.'));
|
|
54
54
|
console.log(`Address: ${chalk.cyan(wallet.address)}`);
|
|
55
|
-
console.log(`\nSend USDC on Base to this address, then run ${chalk.bold('
|
|
55
|
+
console.log(`\nSend USDC on Base to this address, then run ${chalk.bold('franklin proxy')} again.\n`);
|
|
56
56
|
return;
|
|
57
57
|
}
|
|
58
58
|
printBanner(version);
|
package/dist/commands/stats.js
CHANGED
|
@@ -26,10 +26,10 @@ export function statsCommand(options) {
|
|
|
26
26
|
return;
|
|
27
27
|
}
|
|
28
28
|
// Pretty output
|
|
29
|
-
console.log(chalk.bold('\n📊
|
|
29
|
+
console.log(chalk.bold('\n📊 Franklin Usage Statistics\n'));
|
|
30
30
|
console.log('─'.repeat(55));
|
|
31
31
|
if (stats.totalRequests === 0) {
|
|
32
|
-
console.log(chalk.gray('\n No requests recorded yet. Start using
|
|
32
|
+
console.log(chalk.gray('\n No requests recorded yet. Start using franklin!\n'));
|
|
33
33
|
console.log('─'.repeat(55) + '\n');
|
|
34
34
|
return;
|
|
35
35
|
}
|
package/dist/commands/uninit.js
CHANGED
|
@@ -3,7 +3,10 @@ import path from 'node:path';
|
|
|
3
3
|
import os from 'node:os';
|
|
4
4
|
import chalk from 'chalk';
|
|
5
5
|
const CLAUDE_SETTINGS_FILE = path.join(os.homedir(), '.claude', 'settings.json');
|
|
6
|
-
const
|
|
6
|
+
const LAUNCH_AGENT_PLISTS = [
|
|
7
|
+
path.join(os.homedir(), 'Library', 'LaunchAgents', 'ai.blockrun.franklin.plist'),
|
|
8
|
+
path.join(os.homedir(), 'Library', 'LaunchAgents', 'ai.blockrun.runcode.plist'), // legacy
|
|
9
|
+
];
|
|
7
10
|
export async function uninitCommand() {
|
|
8
11
|
let changed = false;
|
|
9
12
|
// ── 1. Remove env section from ~/.claude/settings.json ──────────────────
|
|
@@ -31,7 +34,7 @@ export async function uninitCommand() {
|
|
|
31
34
|
delete settings.env;
|
|
32
35
|
if (removed) {
|
|
33
36
|
fs.writeFileSync(CLAUDE_SETTINGS_FILE, JSON.stringify(settings, null, 2));
|
|
34
|
-
console.log(chalk.green(`✓ Removed
|
|
37
|
+
console.log(chalk.green(`✓ Removed franklin env from ${CLAUDE_SETTINGS_FILE}`));
|
|
35
38
|
changed = true;
|
|
36
39
|
}
|
|
37
40
|
}
|
|
@@ -40,24 +43,28 @@ export async function uninitCommand() {
|
|
|
40
43
|
catch (e) {
|
|
41
44
|
console.log(chalk.yellow(`Could not update settings.json: ${e.message}`));
|
|
42
45
|
}
|
|
43
|
-
// ── 2. Unload and remove LaunchAgent
|
|
44
|
-
if (process.platform === 'darwin'
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
46
|
+
// ── 2. Unload and remove LaunchAgent(s) — new + legacy ─────────────────
|
|
47
|
+
if (process.platform === 'darwin') {
|
|
48
|
+
for (const plist of LAUNCH_AGENT_PLISTS) {
|
|
49
|
+
if (fs.existsSync(plist)) {
|
|
50
|
+
try {
|
|
51
|
+
const { execSync } = await import('node:child_process');
|
|
52
|
+
execSync(`launchctl unload -w "${plist}"`, { stdio: 'pipe' });
|
|
53
|
+
}
|
|
54
|
+
catch { /* already unloaded */ }
|
|
55
|
+
fs.unlinkSync(plist);
|
|
56
|
+
console.log(chalk.green(`✓ Removed LaunchAgent: ${path.basename(plist)}`));
|
|
57
|
+
changed = true;
|
|
58
|
+
}
|
|
48
59
|
}
|
|
49
|
-
catch { /* already unloaded */ }
|
|
50
|
-
fs.unlinkSync(LAUNCH_AGENT_PLIST);
|
|
51
|
-
console.log(chalk.green(`✓ Removed LaunchAgent`));
|
|
52
|
-
changed = true;
|
|
53
60
|
}
|
|
54
61
|
if (!changed) {
|
|
55
|
-
console.log(chalk.dim('Nothing to uninit —
|
|
62
|
+
console.log(chalk.dim('Nothing to uninit — franklin was not initialized.'));
|
|
56
63
|
}
|
|
57
64
|
else {
|
|
58
65
|
console.log('');
|
|
59
|
-
console.log(chalk.bold('
|
|
66
|
+
console.log(chalk.bold('franklin uninitialized.'));
|
|
60
67
|
console.log(`Claude Code will use its default Anthropic API settings again.`);
|
|
61
|
-
console.log(`Run ${chalk.bold('
|
|
68
|
+
console.log(`Run ${chalk.bold('franklin daemon stop')} to stop any running proxy.`);
|
|
62
69
|
}
|
|
63
70
|
}
|
package/dist/index.js
CHANGED
|
@@ -37,7 +37,7 @@ program
|
|
|
37
37
|
.action((chain) => setupCommand(chain));
|
|
38
38
|
program
|
|
39
39
|
.command('start')
|
|
40
|
-
.description('Start the
|
|
40
|
+
.description('Start the franklin agent')
|
|
41
41
|
.option('-m, --model <model>', 'Model to use (e.g. openai/gpt-5.4, anthropic/claude-sonnet-4.6). Default from config or claude-sonnet-4.6')
|
|
42
42
|
.option('--debug', 'Enable debug logging')
|
|
43
43
|
.option('--trust', 'Trust mode — skip permission prompts for all tools')
|
|
@@ -52,16 +52,16 @@ program
|
|
|
52
52
|
.action((options) => proxyCommand({ ...options, version }));
|
|
53
53
|
program
|
|
54
54
|
.command('init')
|
|
55
|
-
.description('Configure
|
|
55
|
+
.description('Configure franklin auto-start (writes ~/.claude/settings.json + installs LaunchAgent on macOS)')
|
|
56
56
|
.option('-p, --port <port>', 'Proxy port', '8402')
|
|
57
57
|
.action((options) => initCommand(options));
|
|
58
58
|
program
|
|
59
59
|
.command('uninit')
|
|
60
|
-
.description('Remove
|
|
60
|
+
.description('Remove franklin configuration and uninstall LaunchAgent')
|
|
61
61
|
.action(() => uninitCommand());
|
|
62
62
|
program
|
|
63
63
|
.command('daemon <action>')
|
|
64
|
-
.description('Manage
|
|
64
|
+
.description('Manage franklin background proxy (start|stop|status)')
|
|
65
65
|
.option('-p, --port <port>', 'Proxy port', '8402')
|
|
66
66
|
.action((action, options) => daemonCommand(action, options));
|
|
67
67
|
program
|
|
@@ -82,7 +82,7 @@ program
|
|
|
82
82
|
.action(balanceCommand);
|
|
83
83
|
program
|
|
84
84
|
.command('config <action> [key] [value]')
|
|
85
|
-
.description('Manage
|
|
85
|
+
.description('Manage franklin config (set, get, unset, list)\n' +
|
|
86
86
|
'Keys: default-model, sonnet-model, opus-model, haiku-model, smart-routing')
|
|
87
87
|
.action(configCommand);
|
|
88
88
|
program
|
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/dist/pricing.js
CHANGED
|
@@ -83,7 +83,10 @@ export const OPUS_PRICING = MODEL_PRICING['anthropic/claude-opus-4.6'];
|
|
|
83
83
|
* For per-call models (perCall > 0), uses flat per-call pricing instead of per-token.
|
|
84
84
|
*/
|
|
85
85
|
export function estimateCost(model, inputTokens, outputTokens, calls = 1) {
|
|
86
|
-
|
|
86
|
+
// Unknown models: assume free (0). Prevents false cost accumulation in the UI
|
|
87
|
+
// for models not yet listed — better to under-estimate than scare users with
|
|
88
|
+
// fake charges. Real on-chain charges are tracked separately in cost_log.jsonl.
|
|
89
|
+
const pricing = MODEL_PRICING[model] || { input: 0, output: 0 };
|
|
87
90
|
if (pricing.perCall) {
|
|
88
91
|
return pricing.perCall * calls;
|
|
89
92
|
}
|
package/dist/session/search.js
CHANGED
|
@@ -224,6 +224,6 @@ export function formatSearchResults(matches, query) {
|
|
|
224
224
|
lines.push(` [${m.matchedRole}] ${m.snippet}`);
|
|
225
225
|
lines.push('');
|
|
226
226
|
}
|
|
227
|
-
lines.push(` Resume:
|
|
227
|
+
lines.push(` Resume: franklin (then /resume <session-id>)\n`);
|
|
228
228
|
return lines.join('\n');
|
|
229
229
|
}
|
package/dist/tools/moa.js
CHANGED
|
@@ -19,8 +19,8 @@ const REFERENCE_MODELS = [
|
|
|
19
19
|
'google/gemini-2.5-flash', // Fast, cheap
|
|
20
20
|
'deepseek/deepseek-chat', // Cheap, good reasoning
|
|
21
21
|
];
|
|
22
|
-
/** Aggregator model —
|
|
23
|
-
const AGGREGATOR_MODEL = '
|
|
22
|
+
/** Aggregator model — free by default. Users explicitly pass `aggregator` to upgrade. */
|
|
23
|
+
const AGGREGATOR_MODEL = 'nvidia/nemotron-ultra-253b';
|
|
24
24
|
/** Max tokens per reference response. */
|
|
25
25
|
const REFERENCE_MAX_TOKENS = 4096;
|
|
26
26
|
/** Max tokens for aggregator. */
|
|
@@ -38,11 +38,8 @@ async function execute(input, ctx) {
|
|
|
38
38
|
return { output: 'Error: prompt is required', isError: true };
|
|
39
39
|
}
|
|
40
40
|
const referenceModels = models || REFERENCE_MODELS;
|
|
41
|
-
//
|
|
42
|
-
|
|
43
|
-
const parentIsFree = registeredParentModel.startsWith('nvidia/') ||
|
|
44
|
-
registeredParentModel === 'blockrun/free';
|
|
45
|
-
const aggregatorModel = aggregator || (parentIsFree ? 'nvidia/nemotron-ultra-253b' : AGGREGATOR_MODEL);
|
|
41
|
+
// Aggregator defaults to free. Pass `aggregator: 'sonnet'` to explicitly upgrade.
|
|
42
|
+
const aggregatorModel = aggregator || AGGREGATOR_MODEL;
|
|
46
43
|
const client = new ModelClient({
|
|
47
44
|
apiUrl: registeredApiUrl,
|
|
48
45
|
chain: registeredChain,
|
package/dist/wallet/manager.js
CHANGED
|
@@ -18,6 +18,6 @@ export async function setupSolanaWallet() {
|
|
|
18
18
|
export function getAddress() {
|
|
19
19
|
const addr = getWalletAddress();
|
|
20
20
|
if (!addr)
|
|
21
|
-
throw new Error('No wallet found. Run `
|
|
21
|
+
throw new Error('No wallet found. Run `franklin setup` first.');
|
|
22
22
|
return addr;
|
|
23
23
|
}
|
package/package.json
CHANGED