@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.
Files changed (138) hide show
  1. package/LICENSE +190 -0
  2. package/README.md +256 -0
  3. package/dist/agent/commands.d.ts +27 -0
  4. package/dist/agent/commands.js +659 -0
  5. package/dist/agent/compact.d.ts +31 -0
  6. package/dist/agent/compact.js +366 -0
  7. package/dist/agent/context.d.ts +11 -0
  8. package/dist/agent/context.js +184 -0
  9. package/dist/agent/error-classifier.d.ts +10 -0
  10. package/dist/agent/error-classifier.js +61 -0
  11. package/dist/agent/llm.d.ts +63 -0
  12. package/dist/agent/llm.js +448 -0
  13. package/dist/agent/loop.d.ts +12 -0
  14. package/dist/agent/loop.js +346 -0
  15. package/dist/agent/optimize.d.ts +53 -0
  16. package/dist/agent/optimize.js +262 -0
  17. package/dist/agent/permissions.d.ts +39 -0
  18. package/dist/agent/permissions.js +226 -0
  19. package/dist/agent/reduce.d.ts +49 -0
  20. package/dist/agent/reduce.js +317 -0
  21. package/dist/agent/streaming-executor.d.ts +36 -0
  22. package/dist/agent/streaming-executor.js +149 -0
  23. package/dist/agent/tokens.d.ts +53 -0
  24. package/dist/agent/tokens.js +185 -0
  25. package/dist/agent/types.d.ts +125 -0
  26. package/dist/agent/types.js +5 -0
  27. package/dist/banner.d.ts +1 -0
  28. package/dist/banner.js +27 -0
  29. package/dist/commands/balance.d.ts +1 -0
  30. package/dist/commands/balance.js +40 -0
  31. package/dist/commands/config.d.ts +14 -0
  32. package/dist/commands/config.js +107 -0
  33. package/dist/commands/daemon.d.ts +3 -0
  34. package/dist/commands/daemon.js +117 -0
  35. package/dist/commands/history.d.ts +5 -0
  36. package/dist/commands/history.js +31 -0
  37. package/dist/commands/init.d.ts +3 -0
  38. package/dist/commands/init.js +92 -0
  39. package/dist/commands/logs.d.ts +5 -0
  40. package/dist/commands/logs.js +89 -0
  41. package/dist/commands/models.d.ts +1 -0
  42. package/dist/commands/models.js +56 -0
  43. package/dist/commands/plugin.d.ts +14 -0
  44. package/dist/commands/plugin.js +176 -0
  45. package/dist/commands/proxy.d.ts +13 -0
  46. package/dist/commands/proxy.js +106 -0
  47. package/dist/commands/setup.d.ts +1 -0
  48. package/dist/commands/setup.js +49 -0
  49. package/dist/commands/start.d.ts +8 -0
  50. package/dist/commands/start.js +292 -0
  51. package/dist/commands/stats.d.ts +10 -0
  52. package/dist/commands/stats.js +94 -0
  53. package/dist/commands/uninit.d.ts +1 -0
  54. package/dist/commands/uninit.js +63 -0
  55. package/dist/config.d.ts +9 -0
  56. package/dist/config.js +41 -0
  57. package/dist/index.d.ts +2 -0
  58. package/dist/index.js +179 -0
  59. package/dist/mcp/client.d.ts +44 -0
  60. package/dist/mcp/client.js +147 -0
  61. package/dist/mcp/config.d.ts +20 -0
  62. package/dist/mcp/config.js +138 -0
  63. package/dist/plugin-sdk/channel.d.ts +100 -0
  64. package/dist/plugin-sdk/channel.js +10 -0
  65. package/dist/plugin-sdk/index.d.ts +14 -0
  66. package/dist/plugin-sdk/index.js +9 -0
  67. package/dist/plugin-sdk/plugin.d.ts +87 -0
  68. package/dist/plugin-sdk/plugin.js +7 -0
  69. package/dist/plugin-sdk/search.d.ts +13 -0
  70. package/dist/plugin-sdk/search.js +4 -0
  71. package/dist/plugin-sdk/tracker.d.ts +27 -0
  72. package/dist/plugin-sdk/tracker.js +5 -0
  73. package/dist/plugin-sdk/workflow.d.ts +126 -0
  74. package/dist/plugin-sdk/workflow.js +11 -0
  75. package/dist/plugins/registry.d.ts +33 -0
  76. package/dist/plugins/registry.js +155 -0
  77. package/dist/plugins/runner.d.ts +21 -0
  78. package/dist/plugins/runner.js +453 -0
  79. package/dist/plugins-bundled/social/index.d.ts +10 -0
  80. package/dist/plugins-bundled/social/index.js +363 -0
  81. package/dist/plugins-bundled/social/plugin.json +14 -0
  82. package/dist/plugins-bundled/social/prompts.d.ts +19 -0
  83. package/dist/plugins-bundled/social/prompts.js +67 -0
  84. package/dist/plugins-bundled/social/types.d.ts +58 -0
  85. package/dist/plugins-bundled/social/types.js +16 -0
  86. package/dist/pricing.d.ts +21 -0
  87. package/dist/pricing.js +91 -0
  88. package/dist/proxy/fallback.d.ts +38 -0
  89. package/dist/proxy/fallback.js +144 -0
  90. package/dist/proxy/server.d.ts +18 -0
  91. package/dist/proxy/server.js +576 -0
  92. package/dist/proxy/sse-translator.d.ts +29 -0
  93. package/dist/proxy/sse-translator.js +270 -0
  94. package/dist/router/index.d.ts +22 -0
  95. package/dist/router/index.js +269 -0
  96. package/dist/session/search.d.ts +33 -0
  97. package/dist/session/search.js +229 -0
  98. package/dist/session/storage.d.ts +48 -0
  99. package/dist/session/storage.js +173 -0
  100. package/dist/stats/insights.d.ts +55 -0
  101. package/dist/stats/insights.js +195 -0
  102. package/dist/stats/tracker.d.ts +54 -0
  103. package/dist/stats/tracker.js +165 -0
  104. package/dist/tools/askuser.d.ts +6 -0
  105. package/dist/tools/askuser.js +76 -0
  106. package/dist/tools/bash.d.ts +5 -0
  107. package/dist/tools/bash.js +336 -0
  108. package/dist/tools/edit.d.ts +5 -0
  109. package/dist/tools/edit.js +148 -0
  110. package/dist/tools/glob.d.ts +5 -0
  111. package/dist/tools/glob.js +158 -0
  112. package/dist/tools/grep.d.ts +5 -0
  113. package/dist/tools/grep.js +194 -0
  114. package/dist/tools/imagegen.d.ts +6 -0
  115. package/dist/tools/imagegen.js +172 -0
  116. package/dist/tools/index.d.ts +17 -0
  117. package/dist/tools/index.js +30 -0
  118. package/dist/tools/read.d.ts +11 -0
  119. package/dist/tools/read.js +90 -0
  120. package/dist/tools/subagent.d.ts +5 -0
  121. package/dist/tools/subagent.js +116 -0
  122. package/dist/tools/task.d.ts +5 -0
  123. package/dist/tools/task.js +91 -0
  124. package/dist/tools/webfetch.d.ts +5 -0
  125. package/dist/tools/webfetch.js +166 -0
  126. package/dist/tools/websearch.d.ts +5 -0
  127. package/dist/tools/websearch.js +103 -0
  128. package/dist/tools/write.d.ts +5 -0
  129. package/dist/tools/write.js +114 -0
  130. package/dist/ui/app.d.ts +26 -0
  131. package/dist/ui/app.js +545 -0
  132. package/dist/ui/model-picker.d.ts +14 -0
  133. package/dist/ui/model-picker.js +161 -0
  134. package/dist/ui/terminal.d.ts +35 -0
  135. package/dist/ui/terminal.js +337 -0
  136. package/dist/wallet/manager.d.ts +10 -0
  137. package/dist/wallet/manager.js +23 -0
  138. 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
+ }
@@ -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
+ }
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
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;