@blockrun/franklin 3.0.0
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/LICENSE +190 -0
- package/README.md +256 -0
- package/dist/agent/commands.d.ts +27 -0
- package/dist/agent/commands.js +659 -0
- package/dist/agent/compact.d.ts +31 -0
- package/dist/agent/compact.js +366 -0
- package/dist/agent/context.d.ts +11 -0
- package/dist/agent/context.js +184 -0
- package/dist/agent/error-classifier.d.ts +10 -0
- package/dist/agent/error-classifier.js +61 -0
- package/dist/agent/llm.d.ts +63 -0
- package/dist/agent/llm.js +448 -0
- package/dist/agent/loop.d.ts +12 -0
- package/dist/agent/loop.js +346 -0
- package/dist/agent/optimize.d.ts +53 -0
- package/dist/agent/optimize.js +262 -0
- package/dist/agent/permissions.d.ts +39 -0
- package/dist/agent/permissions.js +226 -0
- package/dist/agent/reduce.d.ts +49 -0
- package/dist/agent/reduce.js +317 -0
- package/dist/agent/streaming-executor.d.ts +36 -0
- package/dist/agent/streaming-executor.js +149 -0
- package/dist/agent/tokens.d.ts +53 -0
- package/dist/agent/tokens.js +185 -0
- package/dist/agent/types.d.ts +125 -0
- package/dist/agent/types.js +5 -0
- package/dist/banner.d.ts +1 -0
- package/dist/banner.js +27 -0
- package/dist/commands/balance.d.ts +1 -0
- package/dist/commands/balance.js +40 -0
- package/dist/commands/config.d.ts +14 -0
- package/dist/commands/config.js +107 -0
- package/dist/commands/daemon.d.ts +3 -0
- package/dist/commands/daemon.js +117 -0
- package/dist/commands/history.d.ts +5 -0
- package/dist/commands/history.js +31 -0
- package/dist/commands/init.d.ts +3 -0
- package/dist/commands/init.js +92 -0
- package/dist/commands/logs.d.ts +5 -0
- package/dist/commands/logs.js +89 -0
- package/dist/commands/models.d.ts +1 -0
- package/dist/commands/models.js +56 -0
- package/dist/commands/plugin.d.ts +14 -0
- package/dist/commands/plugin.js +176 -0
- package/dist/commands/proxy.d.ts +13 -0
- package/dist/commands/proxy.js +106 -0
- package/dist/commands/setup.d.ts +1 -0
- package/dist/commands/setup.js +49 -0
- package/dist/commands/start.d.ts +8 -0
- package/dist/commands/start.js +292 -0
- package/dist/commands/stats.d.ts +10 -0
- package/dist/commands/stats.js +94 -0
- package/dist/commands/uninit.d.ts +1 -0
- package/dist/commands/uninit.js +63 -0
- package/dist/config.d.ts +9 -0
- package/dist/config.js +41 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +179 -0
- package/dist/mcp/client.d.ts +44 -0
- package/dist/mcp/client.js +147 -0
- package/dist/mcp/config.d.ts +20 -0
- package/dist/mcp/config.js +138 -0
- package/dist/plugin-sdk/channel.d.ts +100 -0
- package/dist/plugin-sdk/channel.js +10 -0
- package/dist/plugin-sdk/index.d.ts +14 -0
- package/dist/plugin-sdk/index.js +9 -0
- package/dist/plugin-sdk/plugin.d.ts +87 -0
- package/dist/plugin-sdk/plugin.js +7 -0
- package/dist/plugin-sdk/search.d.ts +13 -0
- package/dist/plugin-sdk/search.js +4 -0
- package/dist/plugin-sdk/tracker.d.ts +27 -0
- package/dist/plugin-sdk/tracker.js +5 -0
- package/dist/plugin-sdk/workflow.d.ts +126 -0
- package/dist/plugin-sdk/workflow.js +11 -0
- package/dist/plugins/registry.d.ts +33 -0
- package/dist/plugins/registry.js +155 -0
- package/dist/plugins/runner.d.ts +21 -0
- package/dist/plugins/runner.js +453 -0
- package/dist/plugins-bundled/social/index.d.ts +10 -0
- package/dist/plugins-bundled/social/index.js +363 -0
- package/dist/plugins-bundled/social/plugin.json +14 -0
- package/dist/plugins-bundled/social/prompts.d.ts +19 -0
- package/dist/plugins-bundled/social/prompts.js +67 -0
- package/dist/plugins-bundled/social/types.d.ts +58 -0
- package/dist/plugins-bundled/social/types.js +16 -0
- package/dist/pricing.d.ts +21 -0
- package/dist/pricing.js +91 -0
- package/dist/proxy/fallback.d.ts +38 -0
- package/dist/proxy/fallback.js +144 -0
- package/dist/proxy/server.d.ts +18 -0
- package/dist/proxy/server.js +576 -0
- package/dist/proxy/sse-translator.d.ts +29 -0
- package/dist/proxy/sse-translator.js +270 -0
- package/dist/router/index.d.ts +22 -0
- package/dist/router/index.js +269 -0
- package/dist/session/search.d.ts +33 -0
- package/dist/session/search.js +229 -0
- package/dist/session/storage.d.ts +48 -0
- package/dist/session/storage.js +173 -0
- package/dist/stats/insights.d.ts +55 -0
- package/dist/stats/insights.js +195 -0
- package/dist/stats/tracker.d.ts +54 -0
- package/dist/stats/tracker.js +165 -0
- package/dist/tools/askuser.d.ts +6 -0
- package/dist/tools/askuser.js +76 -0
- package/dist/tools/bash.d.ts +5 -0
- package/dist/tools/bash.js +336 -0
- package/dist/tools/edit.d.ts +5 -0
- package/dist/tools/edit.js +148 -0
- package/dist/tools/glob.d.ts +5 -0
- package/dist/tools/glob.js +158 -0
- package/dist/tools/grep.d.ts +5 -0
- package/dist/tools/grep.js +194 -0
- package/dist/tools/imagegen.d.ts +6 -0
- package/dist/tools/imagegen.js +172 -0
- package/dist/tools/index.d.ts +17 -0
- package/dist/tools/index.js +30 -0
- package/dist/tools/read.d.ts +11 -0
- package/dist/tools/read.js +90 -0
- package/dist/tools/subagent.d.ts +5 -0
- package/dist/tools/subagent.js +116 -0
- package/dist/tools/task.d.ts +5 -0
- package/dist/tools/task.js +91 -0
- package/dist/tools/webfetch.d.ts +5 -0
- package/dist/tools/webfetch.js +166 -0
- package/dist/tools/websearch.d.ts +5 -0
- package/dist/tools/websearch.js +103 -0
- package/dist/tools/write.d.ts +5 -0
- package/dist/tools/write.js +114 -0
- package/dist/ui/app.d.ts +26 -0
- package/dist/ui/app.js +545 -0
- package/dist/ui/model-picker.d.ts +14 -0
- package/dist/ui/model-picker.js +161 -0
- package/dist/ui/terminal.d.ts +35 -0
- package/dist/ui/terminal.js +337 -0
- package/dist/wallet/manager.d.ts +10 -0
- package/dist/wallet/manager.js +23 -0
- package/package.json +79 -0
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* runcode stats command
|
|
3
|
+
* Display usage statistics and cost savings
|
|
4
|
+
*/
|
|
5
|
+
import chalk from 'chalk';
|
|
6
|
+
import { clearStats, getStatsSummary } from '../stats/tracker.js';
|
|
7
|
+
export function statsCommand(options) {
|
|
8
|
+
if (options.clear) {
|
|
9
|
+
clearStats();
|
|
10
|
+
console.log(chalk.green('✓ Statistics cleared'));
|
|
11
|
+
return;
|
|
12
|
+
}
|
|
13
|
+
const { stats, opusCost, saved, savedPct, avgCostPerRequest, period } = getStatsSummary();
|
|
14
|
+
// JSON output for programmatic access
|
|
15
|
+
if (options.json) {
|
|
16
|
+
console.log(JSON.stringify({
|
|
17
|
+
...stats,
|
|
18
|
+
computed: {
|
|
19
|
+
opusCost,
|
|
20
|
+
saved,
|
|
21
|
+
savedPct,
|
|
22
|
+
avgCostPerRequest,
|
|
23
|
+
period,
|
|
24
|
+
},
|
|
25
|
+
}, null, 2));
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
// Pretty output
|
|
29
|
+
console.log(chalk.bold('\n📊 runcode Usage Statistics\n'));
|
|
30
|
+
console.log('─'.repeat(55));
|
|
31
|
+
if (stats.totalRequests === 0) {
|
|
32
|
+
console.log(chalk.gray('\n No requests recorded yet. Start using runcode!\n'));
|
|
33
|
+
console.log('─'.repeat(55) + '\n');
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
// Overview
|
|
37
|
+
console.log(chalk.bold('\n Overview') + chalk.gray(` (${period})\n`));
|
|
38
|
+
console.log(` Requests: ${chalk.cyan(stats.totalRequests.toLocaleString())}`);
|
|
39
|
+
console.log(` Total Cost: ${chalk.green('$' + stats.totalCostUsd.toFixed(4))}`);
|
|
40
|
+
console.log(` Avg per Request: ${chalk.gray('$' + avgCostPerRequest.toFixed(6))}`);
|
|
41
|
+
console.log(` Input Tokens: ${stats.totalInputTokens.toLocaleString()}`);
|
|
42
|
+
console.log(` Output Tokens: ${stats.totalOutputTokens.toLocaleString()}`);
|
|
43
|
+
if (stats.totalFallbacks > 0) {
|
|
44
|
+
const fallbackPct = ((stats.totalFallbacks / stats.totalRequests) *
|
|
45
|
+
100).toFixed(1);
|
|
46
|
+
console.log(` Fallbacks: ${chalk.yellow(stats.totalFallbacks.toString())} (${fallbackPct}%)`);
|
|
47
|
+
}
|
|
48
|
+
// Per-model breakdown
|
|
49
|
+
const modelEntries = Object.entries(stats.byModel);
|
|
50
|
+
if (modelEntries.length > 0) {
|
|
51
|
+
console.log(chalk.bold('\n By Model\n'));
|
|
52
|
+
// Sort by cost (descending)
|
|
53
|
+
const sorted = modelEntries.sort((a, b) => b[1].costUsd - a[1].costUsd);
|
|
54
|
+
for (const [model, data] of sorted) {
|
|
55
|
+
const pct = stats.totalCostUsd > 0
|
|
56
|
+
? ((data.costUsd / stats.totalCostUsd) * 100).toFixed(1)
|
|
57
|
+
: '0';
|
|
58
|
+
const avgLatency = Math.round(data.avgLatencyMs);
|
|
59
|
+
// Shorten model name if too long
|
|
60
|
+
const displayModel = model.length > 35 ? model.slice(0, 32) + '...' : model;
|
|
61
|
+
console.log(` ${chalk.cyan(displayModel)}`);
|
|
62
|
+
console.log(chalk.gray(` ${data.requests} req · $${data.costUsd.toFixed(4)} (${pct}%) · ${avgLatency}ms avg`));
|
|
63
|
+
if (data.fallbackCount > 0) {
|
|
64
|
+
console.log(chalk.yellow(` ↳ ${data.fallbackCount} fallback recoveries`));
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
// Savings comparison
|
|
69
|
+
console.log(chalk.bold('\n 💰 Savings vs Claude Opus\n'));
|
|
70
|
+
if (opusCost > 0) {
|
|
71
|
+
console.log(` Opus equivalent: ${chalk.gray('$' + opusCost.toFixed(2))}`);
|
|
72
|
+
console.log(` Your actual cost:${chalk.green(' $' + stats.totalCostUsd.toFixed(2))}`);
|
|
73
|
+
console.log(` ${chalk.green.bold(`Saved: $${saved.toFixed(2)} (${savedPct.toFixed(1)}%)`)}`);
|
|
74
|
+
}
|
|
75
|
+
else {
|
|
76
|
+
console.log(chalk.gray(' Not enough data to calculate savings'));
|
|
77
|
+
}
|
|
78
|
+
// Recent activity (last 5 requests)
|
|
79
|
+
if (stats.history.length > 0) {
|
|
80
|
+
console.log(chalk.bold('\n Recent Activity\n'));
|
|
81
|
+
const recent = stats.history.slice(-5).reverse();
|
|
82
|
+
for (const record of recent) {
|
|
83
|
+
const time = new Date(record.timestamp).toLocaleTimeString();
|
|
84
|
+
const model = record.model.split('/').pop() || record.model;
|
|
85
|
+
const cost = '$' + record.costUsd.toFixed(4);
|
|
86
|
+
const fallbackMark = record.fallback ? chalk.yellow(' ↺') : '';
|
|
87
|
+
console.log(chalk.gray(` ${time}`) +
|
|
88
|
+
` ${model}${fallbackMark} ` +
|
|
89
|
+
chalk.green(cost));
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
console.log('\n' + '─'.repeat(55));
|
|
93
|
+
console.log(chalk.gray(' Run `runcode stats --clear` to reset statistics\n'));
|
|
94
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function uninitCommand(): Promise<void>;
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import os from 'node:os';
|
|
4
|
+
import chalk from 'chalk';
|
|
5
|
+
const CLAUDE_SETTINGS_FILE = path.join(os.homedir(), '.claude', 'settings.json');
|
|
6
|
+
const LAUNCH_AGENT_PLIST = path.join(os.homedir(), 'Library', 'LaunchAgents', 'ai.blockrun.runcode.plist');
|
|
7
|
+
export async function uninitCommand() {
|
|
8
|
+
let changed = false;
|
|
9
|
+
// ── 1. Remove env section from ~/.claude/settings.json ──────────────────
|
|
10
|
+
try {
|
|
11
|
+
if (fs.existsSync(CLAUDE_SETTINGS_FILE)) {
|
|
12
|
+
const settings = JSON.parse(fs.readFileSync(CLAUDE_SETTINGS_FILE, 'utf-8'));
|
|
13
|
+
const env = settings.env;
|
|
14
|
+
if (env) {
|
|
15
|
+
const proxyKeys = [
|
|
16
|
+
'ANTHROPIC_BASE_URL',
|
|
17
|
+
'ANTHROPIC_AUTH_TOKEN',
|
|
18
|
+
'ANTHROPIC_MODEL',
|
|
19
|
+
'ANTHROPIC_DEFAULT_SONNET_MODEL',
|
|
20
|
+
'ANTHROPIC_DEFAULT_OPUS_MODEL',
|
|
21
|
+
'ANTHROPIC_DEFAULT_HAIKU_MODEL',
|
|
22
|
+
];
|
|
23
|
+
let removed = false;
|
|
24
|
+
for (const k of proxyKeys) {
|
|
25
|
+
if (k in env) {
|
|
26
|
+
delete env[k];
|
|
27
|
+
removed = true;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
if (Object.keys(env).length === 0)
|
|
31
|
+
delete settings.env;
|
|
32
|
+
if (removed) {
|
|
33
|
+
fs.writeFileSync(CLAUDE_SETTINGS_FILE, JSON.stringify(settings, null, 2));
|
|
34
|
+
console.log(chalk.green(`✓ Removed runcode env from ${CLAUDE_SETTINGS_FILE}`));
|
|
35
|
+
changed = true;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
catch (e) {
|
|
41
|
+
console.log(chalk.yellow(`Could not update settings.json: ${e.message}`));
|
|
42
|
+
}
|
|
43
|
+
// ── 2. Unload and remove LaunchAgent ────────────────────────────────────
|
|
44
|
+
if (process.platform === 'darwin' && fs.existsSync(LAUNCH_AGENT_PLIST)) {
|
|
45
|
+
try {
|
|
46
|
+
const { execSync } = await import('node:child_process');
|
|
47
|
+
execSync(`launchctl unload -w "${LAUNCH_AGENT_PLIST}"`, { stdio: 'pipe' });
|
|
48
|
+
}
|
|
49
|
+
catch { /* already unloaded */ }
|
|
50
|
+
fs.unlinkSync(LAUNCH_AGENT_PLIST);
|
|
51
|
+
console.log(chalk.green(`✓ Removed LaunchAgent`));
|
|
52
|
+
changed = true;
|
|
53
|
+
}
|
|
54
|
+
if (!changed) {
|
|
55
|
+
console.log(chalk.dim('Nothing to uninit — runcode was not initialized.'));
|
|
56
|
+
}
|
|
57
|
+
else {
|
|
58
|
+
console.log('');
|
|
59
|
+
console.log(chalk.bold('runcode uninitialized.'));
|
|
60
|
+
console.log(`Claude Code will use its default Anthropic API settings again.`);
|
|
61
|
+
console.log(`Run ${chalk.bold('runcode daemon stop')} to stop any running proxy.`);
|
|
62
|
+
}
|
|
63
|
+
}
|
package/dist/config.d.ts
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export declare const VERSION: string;
|
|
2
|
+
export declare const USER_AGENT: string;
|
|
3
|
+
export type Chain = 'base' | 'solana';
|
|
4
|
+
export declare const BLOCKRUN_DIR: string;
|
|
5
|
+
export declare const CHAIN_FILE: string;
|
|
6
|
+
export declare const API_URLS: Record<Chain, string>;
|
|
7
|
+
export declare const DEFAULT_PROXY_PORT = 8402;
|
|
8
|
+
export declare function saveChain(chain: Chain): void;
|
|
9
|
+
export declare function loadChain(): Chain;
|
package/dist/config.js
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import os from 'node:os';
|
|
3
|
+
import fs from 'node:fs';
|
|
4
|
+
import { fileURLToPath } from 'node:url';
|
|
5
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
6
|
+
let _version = '2.0.0';
|
|
7
|
+
try {
|
|
8
|
+
const pkg = JSON.parse(fs.readFileSync(path.resolve(__dirname, '../package.json'), 'utf-8'));
|
|
9
|
+
_version = pkg.version || _version;
|
|
10
|
+
}
|
|
11
|
+
catch { /* use default */ }
|
|
12
|
+
export const VERSION = _version;
|
|
13
|
+
// Shared User-Agent string for all outbound HTTP requests
|
|
14
|
+
export const USER_AGENT = `runcode/${_version} (node/${process.versions.node}; ${process.platform}; ${process.arch})`;
|
|
15
|
+
export const BLOCKRUN_DIR = path.join(os.homedir(), '.blockrun');
|
|
16
|
+
export const CHAIN_FILE = path.join(BLOCKRUN_DIR, 'payment-chain');
|
|
17
|
+
export const API_URLS = {
|
|
18
|
+
base: 'https://blockrun.ai/api',
|
|
19
|
+
solana: 'https://sol.blockrun.ai/api',
|
|
20
|
+
};
|
|
21
|
+
export const DEFAULT_PROXY_PORT = 8402;
|
|
22
|
+
export function saveChain(chain) {
|
|
23
|
+
fs.mkdirSync(BLOCKRUN_DIR, { recursive: true });
|
|
24
|
+
fs.writeFileSync(CHAIN_FILE, chain + '\n', { mode: 0o600 });
|
|
25
|
+
}
|
|
26
|
+
export function loadChain() {
|
|
27
|
+
const envChain = process.env.RUNCODE_CHAIN;
|
|
28
|
+
if (envChain === 'solana')
|
|
29
|
+
return 'solana';
|
|
30
|
+
if (envChain === 'base')
|
|
31
|
+
return 'base';
|
|
32
|
+
try {
|
|
33
|
+
const content = fs.readFileSync(CHAIN_FILE, 'utf-8').trim();
|
|
34
|
+
if (content === 'solana')
|
|
35
|
+
return 'solana';
|
|
36
|
+
return 'base';
|
|
37
|
+
}
|
|
38
|
+
catch {
|
|
39
|
+
return 'base';
|
|
40
|
+
}
|
|
41
|
+
}
|
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Global error handlers — catch unhandled rejections/exceptions before they crash silently
|
|
3
|
+
process.on('unhandledRejection', (reason) => {
|
|
4
|
+
console.error(`\x1b[31mUnhandled error: ${reason instanceof Error ? reason.message : String(reason)}\x1b[0m`);
|
|
5
|
+
process.exit(1);
|
|
6
|
+
});
|
|
7
|
+
process.on('uncaughtException', (err) => {
|
|
8
|
+
console.error(`\x1b[31mFatal error: ${err.message}\x1b[0m`);
|
|
9
|
+
process.exit(1);
|
|
10
|
+
});
|
|
11
|
+
import { Command } from 'commander';
|
|
12
|
+
import { flushStats } from './stats/tracker.js';
|
|
13
|
+
// Ensure stats are flushed on any exit
|
|
14
|
+
process.on('exit', () => flushStats());
|
|
15
|
+
import { setupCommand } from './commands/setup.js';
|
|
16
|
+
import { startCommand } from './commands/start.js';
|
|
17
|
+
import { balanceCommand } from './commands/balance.js';
|
|
18
|
+
import { modelsCommand } from './commands/models.js';
|
|
19
|
+
import { configCommand } from './commands/config.js';
|
|
20
|
+
import { statsCommand } from './commands/stats.js';
|
|
21
|
+
import { logsCommand } from './commands/logs.js';
|
|
22
|
+
import { daemonCommand } from './commands/daemon.js';
|
|
23
|
+
import { initCommand } from './commands/init.js';
|
|
24
|
+
import { uninitCommand } from './commands/uninit.js';
|
|
25
|
+
import { proxyCommand } from './commands/proxy.js';
|
|
26
|
+
import { VERSION as version } from './config.js';
|
|
27
|
+
const program = new Command();
|
|
28
|
+
program
|
|
29
|
+
.name('franklin')
|
|
30
|
+
.description('Franklin — The AI agent with a wallet.\n\n' +
|
|
31
|
+
'While others chat, Franklin spends — turning your USDC into real work.\n\n' +
|
|
32
|
+
' Marketing workflows: franklin.run\n' +
|
|
33
|
+
' Trading workflows: franklin.bet\n\n' +
|
|
34
|
+
'Pay per action in USDC on Base or Solana. No subscriptions. No accounts.')
|
|
35
|
+
.version(version);
|
|
36
|
+
program
|
|
37
|
+
.command('setup [chain]')
|
|
38
|
+
.description('Create a new wallet for payments (base or solana)')
|
|
39
|
+
.action((chain) => setupCommand(chain));
|
|
40
|
+
program
|
|
41
|
+
.command('start')
|
|
42
|
+
.description('Start the runcode agent')
|
|
43
|
+
.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')
|
|
44
|
+
.option('--debug', 'Enable debug logging')
|
|
45
|
+
.option('--trust', 'Trust mode — skip permission prompts for all tools')
|
|
46
|
+
.action((options) => startCommand({ ...options, version }));
|
|
47
|
+
program
|
|
48
|
+
.command('proxy')
|
|
49
|
+
.description('Run payment proxy for Claude Code or other tools')
|
|
50
|
+
.option('-p, --port <port>', 'Proxy port', '8402')
|
|
51
|
+
.option('-m, --model <model>', 'Default model for proxied requests')
|
|
52
|
+
.option('--no-fallback', 'Disable automatic fallback to backup models')
|
|
53
|
+
.option('--debug', 'Enable debug logging')
|
|
54
|
+
.action((options) => proxyCommand({ ...options, version }));
|
|
55
|
+
program
|
|
56
|
+
.command('init')
|
|
57
|
+
.description('Configure runcode auto-start (writes ~/.claude/settings.json + installs LaunchAgent on macOS)')
|
|
58
|
+
.option('-p, --port <port>', 'Proxy port', '8402')
|
|
59
|
+
.action((options) => initCommand(options));
|
|
60
|
+
program
|
|
61
|
+
.command('uninit')
|
|
62
|
+
.description('Remove runcode configuration and uninstall LaunchAgent')
|
|
63
|
+
.action(() => uninitCommand());
|
|
64
|
+
program
|
|
65
|
+
.command('daemon <action>')
|
|
66
|
+
.description('Manage runcode background proxy (start|stop|status)')
|
|
67
|
+
.option('-p, --port <port>', 'Proxy port', '8402')
|
|
68
|
+
.action((action, options) => daemonCommand(action, options));
|
|
69
|
+
program
|
|
70
|
+
.command('models')
|
|
71
|
+
.description('List available models and pricing')
|
|
72
|
+
.action(modelsCommand);
|
|
73
|
+
program
|
|
74
|
+
.command('balance')
|
|
75
|
+
.description('Check wallet USDC balance')
|
|
76
|
+
.action(balanceCommand);
|
|
77
|
+
program
|
|
78
|
+
.command('config <action> [key] [value]')
|
|
79
|
+
.description('Manage runcode config (set, get, unset, list)\n' +
|
|
80
|
+
'Keys: default-model, sonnet-model, opus-model, haiku-model, smart-routing')
|
|
81
|
+
.action(configCommand);
|
|
82
|
+
program
|
|
83
|
+
.command('stats')
|
|
84
|
+
.description('Show usage statistics and cost savings')
|
|
85
|
+
.option('--clear', 'Clear all statistics')
|
|
86
|
+
.option('--json', 'Output in JSON format')
|
|
87
|
+
.action(statsCommand);
|
|
88
|
+
program
|
|
89
|
+
.command('logs')
|
|
90
|
+
.description('View debug logs (start with --debug to enable logging)')
|
|
91
|
+
.option('-f, --follow', 'Follow log output in real time')
|
|
92
|
+
.option('-n, --lines <count>', 'Number of lines to show (default: 50)')
|
|
93
|
+
.option('--clear', 'Delete log file')
|
|
94
|
+
.action(logsCommand);
|
|
95
|
+
program
|
|
96
|
+
.command('insights')
|
|
97
|
+
.description('Show rich usage insights — cost breakdown, trends, projections')
|
|
98
|
+
.option('-d, --days <n>', 'Window size in days (default: 30)', '30')
|
|
99
|
+
.action(async (opts) => {
|
|
100
|
+
const { generateInsights, formatInsights } = await import('./stats/insights.js');
|
|
101
|
+
const days = parseInt(opts.days ?? '30', 10) || 30;
|
|
102
|
+
const report = generateInsights(days);
|
|
103
|
+
process.stdout.write(formatInsights(report, days));
|
|
104
|
+
});
|
|
105
|
+
program
|
|
106
|
+
.command('search <query>')
|
|
107
|
+
.description('Search past sessions by keyword (use quotes for phrases)')
|
|
108
|
+
.option('-l, --limit <n>', 'Max results to show (default: 10)', '10')
|
|
109
|
+
.option('-m, --model <substring>', 'Filter by model name substring')
|
|
110
|
+
.action(async (query, opts) => {
|
|
111
|
+
const { searchSessions, formatSearchResults } = await import('./session/search.js');
|
|
112
|
+
const limit = parseInt(opts.limit ?? '10', 10) || 10;
|
|
113
|
+
const matches = searchSessions(query, { limit, model: opts.model });
|
|
114
|
+
process.stdout.write(formatSearchResults(matches, query));
|
|
115
|
+
});
|
|
116
|
+
// Plugin commands — dynamically registered from discovered plugins.
|
|
117
|
+
// Core stays plugin-agnostic: this loop adds a command for each installed plugin.
|
|
118
|
+
{
|
|
119
|
+
const { loadAllPlugins, listWorkflowPlugins } = await import('./plugins/registry.js');
|
|
120
|
+
await loadAllPlugins();
|
|
121
|
+
for (const lp of listWorkflowPlugins()) {
|
|
122
|
+
const { manifest } = lp;
|
|
123
|
+
program
|
|
124
|
+
.command(`${manifest.id} [action]`)
|
|
125
|
+
.description(manifest.description)
|
|
126
|
+
.option('--dry', 'Dry run — preview without side effects')
|
|
127
|
+
.option('--debug', 'Enable debug logging')
|
|
128
|
+
.action(async (action, opts) => {
|
|
129
|
+
const { pluginCommand } = await import('./commands/plugin.js');
|
|
130
|
+
await pluginCommand(manifest.id, action, { dryRun: opts.dry, debug: opts.debug });
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
program
|
|
135
|
+
.command('plugins')
|
|
136
|
+
.description('List installed plugins')
|
|
137
|
+
.action(async () => {
|
|
138
|
+
const { listAvailablePlugins } = await import('./commands/plugin.js');
|
|
139
|
+
listAvailablePlugins();
|
|
140
|
+
});
|
|
141
|
+
// Default action: if no subcommand given, run 'start'
|
|
142
|
+
const args = process.argv.slice(2);
|
|
143
|
+
const knownCommands = program.commands.map(c => c.name());
|
|
144
|
+
const firstArg = args[0];
|
|
145
|
+
// Handle chain shortcuts: `runcode solana` or `runcode base`
|
|
146
|
+
if (firstArg === 'solana' || firstArg === 'base') {
|
|
147
|
+
const { saveChain } = await import('./config.js');
|
|
148
|
+
saveChain(firstArg);
|
|
149
|
+
const startOpts = { version };
|
|
150
|
+
for (let i = 1; i < args.length; i++) {
|
|
151
|
+
if (args[i] === '--trust')
|
|
152
|
+
startOpts.trust = true;
|
|
153
|
+
else if (args[i] === '--debug')
|
|
154
|
+
startOpts.debug = true;
|
|
155
|
+
else if ((args[i] === '-m' || args[i] === '--model') && args[i + 1]) {
|
|
156
|
+
startOpts.model = args[++i];
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
await startCommand(startOpts);
|
|
160
|
+
process.exit(0);
|
|
161
|
+
}
|
|
162
|
+
else if (!firstArg || (firstArg.startsWith('-') && !['-h', '--help', '-V', '--version'].includes(firstArg))) {
|
|
163
|
+
// No subcommand or only flags — treat as 'start' with flags
|
|
164
|
+
const startOpts = { version };
|
|
165
|
+
for (let i = 0; i < args.length; i++) {
|
|
166
|
+
if (args[i] === '--trust')
|
|
167
|
+
startOpts.trust = true;
|
|
168
|
+
else if (args[i] === '--debug')
|
|
169
|
+
startOpts.debug = true;
|
|
170
|
+
else if ((args[i] === '-m' || args[i] === '--model') && args[i + 1]) {
|
|
171
|
+
startOpts.model = args[++i];
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
await startCommand(startOpts);
|
|
175
|
+
process.exit(0);
|
|
176
|
+
}
|
|
177
|
+
else {
|
|
178
|
+
program.parse();
|
|
179
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MCP Client for runcode.
|
|
3
|
+
* Connects to MCP servers, discovers tools, and wraps them as CapabilityHandlers.
|
|
4
|
+
* Supports stdio and HTTP (SSE) transports.
|
|
5
|
+
*/
|
|
6
|
+
import type { CapabilityHandler } from '../agent/types.js';
|
|
7
|
+
export interface McpServerConfig {
|
|
8
|
+
/** Transport type */
|
|
9
|
+
transport: 'stdio' | 'http';
|
|
10
|
+
/** For stdio: command to run */
|
|
11
|
+
command?: string;
|
|
12
|
+
/** For stdio: arguments */
|
|
13
|
+
args?: string[];
|
|
14
|
+
/** For stdio: environment variables */
|
|
15
|
+
env?: Record<string, string>;
|
|
16
|
+
/** For http: server URL */
|
|
17
|
+
url?: string;
|
|
18
|
+
/** For http: headers */
|
|
19
|
+
headers?: Record<string, string>;
|
|
20
|
+
/** Human-readable label */
|
|
21
|
+
label?: string;
|
|
22
|
+
/** Disable this server */
|
|
23
|
+
disabled?: boolean;
|
|
24
|
+
}
|
|
25
|
+
export interface McpConfig {
|
|
26
|
+
mcpServers: Record<string, McpServerConfig>;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Connect to all configured MCP servers and return discovered tools.
|
|
30
|
+
* Each connection has a 5s timeout to avoid blocking startup.
|
|
31
|
+
*/
|
|
32
|
+
export declare function connectMcpServers(config: McpConfig, debug?: boolean): Promise<CapabilityHandler[]>;
|
|
33
|
+
/**
|
|
34
|
+
* Disconnect all MCP servers.
|
|
35
|
+
*/
|
|
36
|
+
export declare function disconnectMcpServers(): Promise<void>;
|
|
37
|
+
/**
|
|
38
|
+
* List connected MCP servers and their tools.
|
|
39
|
+
*/
|
|
40
|
+
export declare function listMcpServers(): Array<{
|
|
41
|
+
name: string;
|
|
42
|
+
toolCount: number;
|
|
43
|
+
tools: string[];
|
|
44
|
+
}>;
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MCP Client for runcode.
|
|
3
|
+
* Connects to MCP servers, discovers tools, and wraps them as CapabilityHandlers.
|
|
4
|
+
* Supports stdio and HTTP (SSE) transports.
|
|
5
|
+
*/
|
|
6
|
+
import { Client } from '@modelcontextprotocol/sdk/client/index.js';
|
|
7
|
+
import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js';
|
|
8
|
+
// ─── Connection Management ────────────────────────────────────────────────
|
|
9
|
+
const connections = new Map();
|
|
10
|
+
/**
|
|
11
|
+
* Connect to an MCP server via stdio transport.
|
|
12
|
+
* Discovers tools and returns them as CapabilityHandlers.
|
|
13
|
+
*/
|
|
14
|
+
async function connectStdio(name, config) {
|
|
15
|
+
if (!config.command) {
|
|
16
|
+
throw new Error(`MCP server "${name}" missing command`);
|
|
17
|
+
}
|
|
18
|
+
const transport = new StdioClientTransport({
|
|
19
|
+
command: config.command,
|
|
20
|
+
args: config.args || [],
|
|
21
|
+
env: { ...process.env, ...(config.env || {}) },
|
|
22
|
+
});
|
|
23
|
+
const client = new Client({ name: `runcode-mcp-${name}`, version: '1.0.0' }, { capabilities: {} });
|
|
24
|
+
try {
|
|
25
|
+
await client.connect(transport);
|
|
26
|
+
}
|
|
27
|
+
catch (err) {
|
|
28
|
+
// Clean up transport if connect fails to prevent resource leak
|
|
29
|
+
try {
|
|
30
|
+
await transport.close();
|
|
31
|
+
}
|
|
32
|
+
catch { /* ignore */ }
|
|
33
|
+
throw err;
|
|
34
|
+
}
|
|
35
|
+
// Discover tools
|
|
36
|
+
const { tools: mcpTools } = await client.listTools();
|
|
37
|
+
const capabilities = [];
|
|
38
|
+
for (const tool of mcpTools) {
|
|
39
|
+
const toolName = `mcp__${name}__${tool.name}`;
|
|
40
|
+
const toolDescription = (tool.description || '').slice(0, 2048);
|
|
41
|
+
capabilities.push({
|
|
42
|
+
spec: {
|
|
43
|
+
name: toolName,
|
|
44
|
+
description: toolDescription || `MCP tool from ${name}`,
|
|
45
|
+
input_schema: tool.inputSchema || {
|
|
46
|
+
type: 'object',
|
|
47
|
+
properties: {},
|
|
48
|
+
},
|
|
49
|
+
},
|
|
50
|
+
execute: async (input, _ctx) => {
|
|
51
|
+
const MCP_TOOL_TIMEOUT = 30_000;
|
|
52
|
+
try {
|
|
53
|
+
// Timeout protection: if tool hangs, don't block the agent forever
|
|
54
|
+
const callPromise = client.callTool({ name: tool.name, arguments: input });
|
|
55
|
+
const timeoutPromise = new Promise((_, reject) => setTimeout(() => reject(new Error(`MCP tool timeout after ${MCP_TOOL_TIMEOUT / 1000}s`)), MCP_TOOL_TIMEOUT));
|
|
56
|
+
const result = await Promise.race([callPromise, timeoutPromise]);
|
|
57
|
+
// Extract text content from MCP response
|
|
58
|
+
const output = result.content
|
|
59
|
+
?.filter(c => c.type === 'text')
|
|
60
|
+
?.map(c => c.text)
|
|
61
|
+
?.join('\n') || JSON.stringify(result.content);
|
|
62
|
+
return {
|
|
63
|
+
output,
|
|
64
|
+
isError: result.isError === true,
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
catch (err) {
|
|
68
|
+
return {
|
|
69
|
+
output: `MCP tool error (${name}/${tool.name}): ${err.message}`,
|
|
70
|
+
isError: true,
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
},
|
|
74
|
+
concurrent: true, // MCP tools are safe to run concurrently
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
const connected = { name, client, transport, tools: capabilities };
|
|
78
|
+
connections.set(name, connected);
|
|
79
|
+
return connected;
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Connect to all configured MCP servers and return discovered tools.
|
|
83
|
+
*/
|
|
84
|
+
const MCP_CONNECT_TIMEOUT = 5_000; // 5s per server connection
|
|
85
|
+
/**
|
|
86
|
+
* Connect to all configured MCP servers and return discovered tools.
|
|
87
|
+
* Each connection has a 5s timeout to avoid blocking startup.
|
|
88
|
+
*/
|
|
89
|
+
export async function connectMcpServers(config, debug) {
|
|
90
|
+
const allTools = [];
|
|
91
|
+
for (const [name, serverConfig] of Object.entries(config.mcpServers)) {
|
|
92
|
+
if (serverConfig.disabled)
|
|
93
|
+
continue;
|
|
94
|
+
try {
|
|
95
|
+
if (debug) {
|
|
96
|
+
console.error(`[runcode] Connecting to MCP server: ${name}...`);
|
|
97
|
+
}
|
|
98
|
+
if (serverConfig.transport !== 'stdio') {
|
|
99
|
+
if (debug) {
|
|
100
|
+
console.error(`[runcode] MCP HTTP transport not yet supported for ${name}`);
|
|
101
|
+
}
|
|
102
|
+
continue;
|
|
103
|
+
}
|
|
104
|
+
// Timeout: don't let a slow server block startup
|
|
105
|
+
const connectPromise = connectStdio(name, serverConfig);
|
|
106
|
+
const timeoutPromise = new Promise((_, reject) => setTimeout(() => reject(new Error('connection timeout (5s)')), MCP_CONNECT_TIMEOUT));
|
|
107
|
+
const connected = await Promise.race([connectPromise, timeoutPromise]);
|
|
108
|
+
allTools.push(...connected.tools);
|
|
109
|
+
if (debug) {
|
|
110
|
+
console.error(`[runcode] MCP ${name}: ${connected.tools.length} tools discovered`);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
catch (err) {
|
|
114
|
+
// Graceful degradation — log and continue without this server
|
|
115
|
+
console.error(`[runcode] MCP ${name} failed: ${err.message}`);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
return allTools;
|
|
119
|
+
}
|
|
120
|
+
/**
|
|
121
|
+
* Disconnect all MCP servers.
|
|
122
|
+
*/
|
|
123
|
+
export async function disconnectMcpServers() {
|
|
124
|
+
for (const [name, conn] of connections) {
|
|
125
|
+
try {
|
|
126
|
+
await conn.client.close();
|
|
127
|
+
}
|
|
128
|
+
catch {
|
|
129
|
+
// Ignore cleanup errors
|
|
130
|
+
}
|
|
131
|
+
connections.delete(name);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
/**
|
|
135
|
+
* List connected MCP servers and their tools.
|
|
136
|
+
*/
|
|
137
|
+
export function listMcpServers() {
|
|
138
|
+
const result = [];
|
|
139
|
+
for (const [name, conn] of connections) {
|
|
140
|
+
result.push({
|
|
141
|
+
name,
|
|
142
|
+
toolCount: conn.tools.length,
|
|
143
|
+
tools: conn.tools.map(t => t.spec.name),
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
return result;
|
|
147
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MCP configuration management for runcode.
|
|
3
|
+
* Loads MCP server configs from:
|
|
4
|
+
* 1. Global: ~/.blockrun/mcp.json
|
|
5
|
+
* 2. Project: .mcp.json in working directory
|
|
6
|
+
*/
|
|
7
|
+
import type { McpConfig, McpServerConfig } from './client.js';
|
|
8
|
+
export declare function loadMcpConfig(workDir: string): McpConfig;
|
|
9
|
+
/**
|
|
10
|
+
* Save a server config to the global MCP config.
|
|
11
|
+
*/
|
|
12
|
+
export declare function saveMcpServer(name: string, config: McpServerConfig): void;
|
|
13
|
+
/**
|
|
14
|
+
* Remove a server from the global MCP config.
|
|
15
|
+
*/
|
|
16
|
+
export declare function removeMcpServer(name: string): boolean;
|
|
17
|
+
/**
|
|
18
|
+
* Trust a project directory to load its .mcp.json.
|
|
19
|
+
*/
|
|
20
|
+
export declare function trustProjectDir(workDir: string): void;
|