@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,176 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Generic plugin command dispatcher.
|
|
3
|
+
*
|
|
4
|
+
* `runcode <plugin-id> <action>` works for ANY plugin that registers a workflow.
|
|
5
|
+
* Core stays plugin-agnostic — adding a new plugin requires zero changes here.
|
|
6
|
+
*/
|
|
7
|
+
import chalk from 'chalk';
|
|
8
|
+
import readline from 'node:readline';
|
|
9
|
+
import { ModelClient } from '../agent/llm.js';
|
|
10
|
+
import { loadChain, API_URLS } from '../config.js';
|
|
11
|
+
import { loadAllPlugins, getPlugin, listWorkflowPlugins } from '../plugins/registry.js';
|
|
12
|
+
import { loadWorkflowConfig, saveWorkflowConfig, runWorkflow, getStats, getByAction, formatWorkflowResult, formatWorkflowStats, } from '../plugins/runner.js';
|
|
13
|
+
import { DEFAULT_MODEL_TIERS } from '../plugin-sdk/workflow.js';
|
|
14
|
+
/** Run a plugin command. Plugin id is the first arg. */
|
|
15
|
+
export async function pluginCommand(pluginId, action, options) {
|
|
16
|
+
await loadAllPlugins();
|
|
17
|
+
const loaded = getPlugin(pluginId);
|
|
18
|
+
if (!loaded) {
|
|
19
|
+
console.log(chalk.red(`Plugin "${pluginId}" not found.`));
|
|
20
|
+
listAvailablePlugins();
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
23
|
+
// Get the workflow this plugin provides (if any)
|
|
24
|
+
const workflows = loaded.plugin.workflows || {};
|
|
25
|
+
const workflowFactory = workflows[pluginId] || workflows[Object.keys(workflows)[0] ?? ''];
|
|
26
|
+
if (!workflowFactory) {
|
|
27
|
+
console.log(chalk.red(`Plugin "${pluginId}" does not provide a workflow.`));
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
const workflow = workflowFactory();
|
|
31
|
+
const chain = loadChain();
|
|
32
|
+
const apiUrl = API_URLS[chain];
|
|
33
|
+
const client = new ModelClient({ apiUrl, chain, debug: options.debug });
|
|
34
|
+
const existingConfig = loadWorkflowConfig(workflow.id);
|
|
35
|
+
switch (action) {
|
|
36
|
+
case 'init':
|
|
37
|
+
case undefined:
|
|
38
|
+
case '': {
|
|
39
|
+
if (!existingConfig) {
|
|
40
|
+
const config = await runOnboarding(workflow, client);
|
|
41
|
+
if (config)
|
|
42
|
+
saveWorkflowConfig(workflow.id, config);
|
|
43
|
+
}
|
|
44
|
+
else if (action === 'init') {
|
|
45
|
+
console.log(chalk.yellow(`Already configured at ~/.blockrun/workflows/${workflow.id}.config.json`));
|
|
46
|
+
console.log(chalk.dim('Delete the file to reconfigure.'));
|
|
47
|
+
}
|
|
48
|
+
else {
|
|
49
|
+
// No action and already configured: show stats + dry-run hint
|
|
50
|
+
const stats = getStats(workflow.id);
|
|
51
|
+
console.log(formatWorkflowStats(workflow, stats));
|
|
52
|
+
console.log(chalk.dim(`Run "runcode ${pluginId} run --dry" to preview.\n`));
|
|
53
|
+
}
|
|
54
|
+
break;
|
|
55
|
+
}
|
|
56
|
+
case 'run': {
|
|
57
|
+
const config = existingConfig ?? await runOnboarding(workflow, client);
|
|
58
|
+
if (!config)
|
|
59
|
+
return;
|
|
60
|
+
if (!existingConfig)
|
|
61
|
+
saveWorkflowConfig(workflow.id, config);
|
|
62
|
+
const dryRun = options.dryRun ?? false;
|
|
63
|
+
console.log(chalk.dim(`\nRunning ${workflow.name}${dryRun ? ' (dry-run)' : ''}...\n`));
|
|
64
|
+
const result = await runWorkflow(workflow, config, client, { dryRun });
|
|
65
|
+
console.log(formatWorkflowResult(workflow, result));
|
|
66
|
+
break;
|
|
67
|
+
}
|
|
68
|
+
case 'stats': {
|
|
69
|
+
const stats = getStats(workflow.id);
|
|
70
|
+
console.log(formatWorkflowStats(workflow, stats));
|
|
71
|
+
break;
|
|
72
|
+
}
|
|
73
|
+
case 'leads': {
|
|
74
|
+
const leads = getByAction(workflow.id, 'lead');
|
|
75
|
+
if (leads.length === 0) {
|
|
76
|
+
console.log(chalk.dim(`\nNo leads found yet. Run "runcode ${pluginId} run" first.\n`));
|
|
77
|
+
break;
|
|
78
|
+
}
|
|
79
|
+
console.log(chalk.bold(`\n LEADS (${leads.length})\n`));
|
|
80
|
+
for (const lead of leads.slice(-20)) {
|
|
81
|
+
const m = lead.metadata;
|
|
82
|
+
const score = m.leadScore ?? 0;
|
|
83
|
+
const icon = score >= 8 ? '🔥' : score >= 6 ? '⭐' : '📋';
|
|
84
|
+
console.log(` ${icon} [${score}/10] ${m.title?.slice(0, 60) ?? ''}`);
|
|
85
|
+
console.log(chalk.dim(` ${m.url} | ${m.platform} | ${m.urgency ?? ''}`));
|
|
86
|
+
if (m.painPoints && Array.isArray(m.painPoints)) {
|
|
87
|
+
console.log(chalk.dim(` Pain: ${m.painPoints.join(', ')}`));
|
|
88
|
+
}
|
|
89
|
+
console.log();
|
|
90
|
+
}
|
|
91
|
+
break;
|
|
92
|
+
}
|
|
93
|
+
default:
|
|
94
|
+
console.log(chalk.red(`Unknown action: ${action}`));
|
|
95
|
+
console.log(chalk.dim(`
|
|
96
|
+
Usage:
|
|
97
|
+
runcode ${pluginId} # show stats / first-run setup
|
|
98
|
+
runcode ${pluginId} init # interactive setup
|
|
99
|
+
runcode ${pluginId} run # execute workflow
|
|
100
|
+
runcode ${pluginId} run --dry # dry run (no side effects)
|
|
101
|
+
runcode ${pluginId} stats # show statistics
|
|
102
|
+
runcode ${pluginId} leads # show tracked leads (if applicable)
|
|
103
|
+
`));
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
/** List all installed plugins */
|
|
107
|
+
export function listAvailablePlugins() {
|
|
108
|
+
const plugins = listWorkflowPlugins();
|
|
109
|
+
if (plugins.length === 0) {
|
|
110
|
+
console.log(chalk.dim('\nNo workflow plugins installed.\n'));
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
console.log(chalk.bold('\n Installed plugins:\n'));
|
|
114
|
+
for (const p of plugins) {
|
|
115
|
+
console.log(` ${chalk.cyan(p.manifest.id.padEnd(15))} ${p.manifest.description}`);
|
|
116
|
+
}
|
|
117
|
+
console.log();
|
|
118
|
+
}
|
|
119
|
+
// ─── Onboarding ───────────────────────────────────────────────────────────
|
|
120
|
+
async function runOnboarding(workflow, client) {
|
|
121
|
+
console.log(chalk.bold(`\n ╭─ ${workflow.name} setup ${'─'.repeat(Math.max(0, 40 - workflow.name.length))}╮`));
|
|
122
|
+
console.log(chalk.bold(' │ │'));
|
|
123
|
+
console.log(chalk.bold(` │ ${workflow.description.padEnd(48)}│`));
|
|
124
|
+
console.log(chalk.bold(' │ │'));
|
|
125
|
+
console.log(chalk.bold(' ╰──────────────────────────────────────────────────╯\n'));
|
|
126
|
+
const rl = readline.createInterface({
|
|
127
|
+
input: process.stdin,
|
|
128
|
+
output: process.stdout,
|
|
129
|
+
terminal: process.stdin.isTTY ?? false,
|
|
130
|
+
});
|
|
131
|
+
const ask = (prompt) => new Promise(resolve => rl.question(chalk.cyan(` ${prompt}\n > `), answer => resolve(answer.trim())));
|
|
132
|
+
const answers = {};
|
|
133
|
+
for (const q of workflow.onboardingQuestions) {
|
|
134
|
+
if (q.type === 'select' && q.options) {
|
|
135
|
+
console.log(chalk.cyan(` ${q.prompt}`));
|
|
136
|
+
for (let i = 0; i < q.options.length; i++) {
|
|
137
|
+
console.log(chalk.dim(` ${i + 1}. ${q.options[i]}`));
|
|
138
|
+
}
|
|
139
|
+
const choice = await ask('Pick a number');
|
|
140
|
+
const idx = parseInt(choice) - 1;
|
|
141
|
+
answers[q.id] = q.options[idx] ?? q.options[0];
|
|
142
|
+
}
|
|
143
|
+
else {
|
|
144
|
+
answers[q.id] = await ask(q.prompt);
|
|
145
|
+
}
|
|
146
|
+
console.log();
|
|
147
|
+
}
|
|
148
|
+
rl.close();
|
|
149
|
+
console.log(chalk.dim(' Building configuration...\n'));
|
|
150
|
+
// Provide an LLM helper for buildConfigFromAnswers
|
|
151
|
+
const llm = async (prompt) => {
|
|
152
|
+
const result = await client.complete({
|
|
153
|
+
model: DEFAULT_MODEL_TIERS.cheap,
|
|
154
|
+
messages: [{ role: 'user', content: prompt }],
|
|
155
|
+
max_tokens: 2048,
|
|
156
|
+
stream: true,
|
|
157
|
+
});
|
|
158
|
+
let text = '';
|
|
159
|
+
for (const part of result.content) {
|
|
160
|
+
if (part.type === 'text')
|
|
161
|
+
text += part.text;
|
|
162
|
+
}
|
|
163
|
+
return text;
|
|
164
|
+
};
|
|
165
|
+
try {
|
|
166
|
+
const config = await workflow.buildConfigFromAnswers(answers, llm);
|
|
167
|
+
console.log(chalk.green(' ✓ Configuration saved!\n'));
|
|
168
|
+
console.log(chalk.dim(` Config: ~/.blockrun/workflows/${workflow.id}.config.json\n`));
|
|
169
|
+
console.log(chalk.dim(` Run "runcode ${workflow.id} run --dry" to preview.\n`));
|
|
170
|
+
return config;
|
|
171
|
+
}
|
|
172
|
+
catch (err) {
|
|
173
|
+
console.error(chalk.red(` Setup failed: ${err.message}`));
|
|
174
|
+
return null;
|
|
175
|
+
}
|
|
176
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Proxy-only mode — runs the BlockRun payment proxy for other tools (e.g. Claude Code).
|
|
3
|
+
* The proxy translates requests and handles x402 payments so Claude Code can use any model.
|
|
4
|
+
*/
|
|
5
|
+
interface ProxyOptions {
|
|
6
|
+
port?: string;
|
|
7
|
+
model?: string;
|
|
8
|
+
fallback?: boolean;
|
|
9
|
+
debug?: boolean;
|
|
10
|
+
version?: string;
|
|
11
|
+
}
|
|
12
|
+
export declare function proxyCommand(options: ProxyOptions): Promise<void>;
|
|
13
|
+
export {};
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Proxy-only mode — runs the BlockRun payment proxy for other tools (e.g. Claude Code).
|
|
3
|
+
* The proxy translates requests and handles x402 payments so Claude Code can use any model.
|
|
4
|
+
*/
|
|
5
|
+
import chalk from 'chalk';
|
|
6
|
+
import { getOrCreateWallet, getOrCreateSolanaWallet } from '@blockrun/llm';
|
|
7
|
+
import { createProxy } from '../proxy/server.js';
|
|
8
|
+
import { loadChain, API_URLS, DEFAULT_PROXY_PORT } from '../config.js';
|
|
9
|
+
import { loadConfig } from './config.js';
|
|
10
|
+
import { printBanner } from '../banner.js';
|
|
11
|
+
export async function proxyCommand(options) {
|
|
12
|
+
const version = options.version ?? '1.0.0';
|
|
13
|
+
const chain = loadChain();
|
|
14
|
+
const apiUrl = API_URLS[chain];
|
|
15
|
+
const fallbackEnabled = options.fallback !== false;
|
|
16
|
+
const config = loadConfig();
|
|
17
|
+
const port = parseInt(options.port || String(DEFAULT_PROXY_PORT));
|
|
18
|
+
if (isNaN(port) || port < 1 || port > 65535) {
|
|
19
|
+
console.log(chalk.red(`Invalid port: ${options.port}. Must be 1-65535.`));
|
|
20
|
+
process.exit(1);
|
|
21
|
+
}
|
|
22
|
+
const model = options.model || config['default-model'];
|
|
23
|
+
if (chain === 'solana') {
|
|
24
|
+
const wallet = await getOrCreateSolanaWallet();
|
|
25
|
+
if (wallet.isNew) {
|
|
26
|
+
console.log(chalk.yellow('No Solana wallet found — created a new one.'));
|
|
27
|
+
console.log(`Address: ${chalk.cyan(wallet.address)}`);
|
|
28
|
+
console.log(`\nSend USDC on Solana to this address, then run ${chalk.bold('runcode proxy')} again.\n`);
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
printBanner(version);
|
|
32
|
+
console.log(`Mode: ${chalk.bold('proxy')}`);
|
|
33
|
+
console.log(`Chain: ${chalk.magenta('solana')}`);
|
|
34
|
+
console.log(`Wallet: ${chalk.cyan(wallet.address)}`);
|
|
35
|
+
if (model)
|
|
36
|
+
console.log(`Model: ${chalk.green(model)}`);
|
|
37
|
+
console.log(`Fallback: ${fallbackEnabled ? chalk.green('enabled') : chalk.yellow('disabled')}`);
|
|
38
|
+
console.log(`Proxy: ${chalk.cyan(`http://localhost:${port}`)}`);
|
|
39
|
+
console.log(`Backend: ${chalk.dim(apiUrl)}\n`);
|
|
40
|
+
const server = createProxy({
|
|
41
|
+
port,
|
|
42
|
+
apiUrl,
|
|
43
|
+
chain: 'solana',
|
|
44
|
+
modelOverride: model,
|
|
45
|
+
debug: options.debug,
|
|
46
|
+
fallbackEnabled,
|
|
47
|
+
});
|
|
48
|
+
launchProxy(server, port, options.debug);
|
|
49
|
+
}
|
|
50
|
+
else {
|
|
51
|
+
const wallet = getOrCreateWallet();
|
|
52
|
+
if (wallet.isNew) {
|
|
53
|
+
console.log(chalk.yellow('No wallet found — created a new one.'));
|
|
54
|
+
console.log(`Address: ${chalk.cyan(wallet.address)}`);
|
|
55
|
+
console.log(`\nSend USDC on Base to this address, then run ${chalk.bold('runcode proxy')} again.\n`);
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
printBanner(version);
|
|
59
|
+
console.log(`Mode: ${chalk.bold('proxy')}`);
|
|
60
|
+
console.log(`Chain: ${chalk.magenta('base')}`);
|
|
61
|
+
console.log(`Wallet: ${chalk.cyan(wallet.address)}`);
|
|
62
|
+
if (model)
|
|
63
|
+
console.log(`Model: ${chalk.green(model)}`);
|
|
64
|
+
console.log(`Fallback: ${fallbackEnabled ? chalk.green('enabled') : chalk.yellow('disabled')}`);
|
|
65
|
+
console.log(`Proxy: ${chalk.cyan(`http://localhost:${port}`)}`);
|
|
66
|
+
console.log(`Backend: ${chalk.dim(apiUrl)}\n`);
|
|
67
|
+
const server = createProxy({
|
|
68
|
+
port,
|
|
69
|
+
apiUrl,
|
|
70
|
+
chain: 'base',
|
|
71
|
+
modelOverride: model,
|
|
72
|
+
debug: options.debug,
|
|
73
|
+
fallbackEnabled,
|
|
74
|
+
});
|
|
75
|
+
launchProxy(server, port, options.debug);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
function launchProxy(server, port, debug) {
|
|
79
|
+
server.on('error', (err) => {
|
|
80
|
+
if (err.code === 'EADDRINUSE') {
|
|
81
|
+
console.error(chalk.red(`Port ${port} is already in use. Try a different port with --port.`));
|
|
82
|
+
}
|
|
83
|
+
else {
|
|
84
|
+
console.error(chalk.red(`Server error: ${err.message}`));
|
|
85
|
+
}
|
|
86
|
+
process.exit(1);
|
|
87
|
+
});
|
|
88
|
+
server.listen(port, () => {
|
|
89
|
+
console.log(chalk.green(`✓ Proxy running on port ${port}`));
|
|
90
|
+
console.log(chalk.dim(` Usage tracking: ~/.blockrun/runcode-stats.json`));
|
|
91
|
+
if (debug)
|
|
92
|
+
console.log(chalk.dim(` Debug log: ~/.blockrun/runcode-debug.log`));
|
|
93
|
+
console.log(chalk.dim(` Run 'runcode stats' to view statistics\n`));
|
|
94
|
+
console.log('Set this in your shell to use with Claude Code:\n');
|
|
95
|
+
console.log(chalk.bold(` export ANTHROPIC_BASE_URL=http://localhost:${port}/api`));
|
|
96
|
+
console.log(chalk.bold(` export ANTHROPIC_AUTH_TOKEN=x402-proxy-handles-auth`));
|
|
97
|
+
console.log(`\nThen run ${chalk.bold('claude')} in another terminal.`);
|
|
98
|
+
});
|
|
99
|
+
const shutdown = () => {
|
|
100
|
+
console.log('\nShutting down...');
|
|
101
|
+
server.close();
|
|
102
|
+
process.exit(0);
|
|
103
|
+
};
|
|
104
|
+
process.on('SIGINT', shutdown);
|
|
105
|
+
process.on('SIGTERM', shutdown);
|
|
106
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function setupCommand(chainArg?: string): Promise<void>;
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
import { getOrCreateWallet, scanWallets, getOrCreateSolanaWallet, scanSolanaWallets, } from '@blockrun/llm';
|
|
3
|
+
import { saveChain } from '../config.js';
|
|
4
|
+
export async function setupCommand(chainArg) {
|
|
5
|
+
const chain = chainArg === 'solana' ? 'solana' : 'base';
|
|
6
|
+
if (chain === 'solana') {
|
|
7
|
+
const wallets = scanSolanaWallets();
|
|
8
|
+
if (wallets.length > 0) {
|
|
9
|
+
console.log(chalk.yellow('Solana wallet already exists.'));
|
|
10
|
+
console.log(`Address: ${chalk.cyan(wallets[0].publicKey)}`);
|
|
11
|
+
console.log(chalk.dim('\nNext steps:'));
|
|
12
|
+
console.log(chalk.dim(' runcode start — start coding'));
|
|
13
|
+
console.log(chalk.dim(' runcode balance — check USDC balance'));
|
|
14
|
+
console.log(chalk.dim(' runcode start -m free — use free models (no USDC needed)'));
|
|
15
|
+
saveChain('solana');
|
|
16
|
+
return;
|
|
17
|
+
}
|
|
18
|
+
console.log('Creating new Solana wallet...\n');
|
|
19
|
+
const { address, isNew } = await getOrCreateSolanaWallet();
|
|
20
|
+
if (isNew) {
|
|
21
|
+
console.log(chalk.green('Solana wallet created!\n'));
|
|
22
|
+
}
|
|
23
|
+
console.log(`Address: ${chalk.cyan(address)}`);
|
|
24
|
+
console.log(`\nSend USDC on Solana to this address to fund your account.`);
|
|
25
|
+
}
|
|
26
|
+
else {
|
|
27
|
+
const wallets = scanWallets();
|
|
28
|
+
if (wallets.length > 0) {
|
|
29
|
+
console.log(chalk.yellow('Wallet already exists.'));
|
|
30
|
+
console.log(`Address: ${chalk.cyan(wallets[0].address)}`);
|
|
31
|
+
console.log(chalk.dim('\nNext steps:'));
|
|
32
|
+
console.log(chalk.dim(' runcode start — start coding'));
|
|
33
|
+
console.log(chalk.dim(' runcode balance — check USDC balance'));
|
|
34
|
+
console.log(chalk.dim(' runcode start -m free — use free models (no USDC needed)'));
|
|
35
|
+
saveChain('base');
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
console.log('Creating new wallet...\n');
|
|
39
|
+
const { address, isNew } = getOrCreateWallet();
|
|
40
|
+
if (isNew) {
|
|
41
|
+
console.log(chalk.green('Wallet created!\n'));
|
|
42
|
+
}
|
|
43
|
+
console.log(`Address: ${chalk.cyan(address)}`);
|
|
44
|
+
console.log(`\nSend USDC on Base to this address to fund your account.`);
|
|
45
|
+
}
|
|
46
|
+
saveChain(chain);
|
|
47
|
+
console.log(`Then run ${chalk.bold('runcode start')} to begin.\n`);
|
|
48
|
+
console.log(chalk.dim(`Chain: ${chain} — saved to ~/.blockrun/`));
|
|
49
|
+
}
|
|
@@ -0,0 +1,292 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
import { getOrCreateWallet, getOrCreateSolanaWallet } from '@blockrun/llm';
|
|
3
|
+
import { loadChain, API_URLS } from '../config.js';
|
|
4
|
+
import { flushStats } from '../stats/tracker.js';
|
|
5
|
+
import { loadConfig } from './config.js';
|
|
6
|
+
import { printBanner } from '../banner.js';
|
|
7
|
+
import { assembleInstructions } from '../agent/context.js';
|
|
8
|
+
import { interactiveSession } from '../agent/loop.js';
|
|
9
|
+
import { allCapabilities, createSubAgentCapability } from '../tools/index.js';
|
|
10
|
+
import { launchInkUI } from '../ui/app.js';
|
|
11
|
+
import { pickModel, resolveModel } from '../ui/model-picker.js';
|
|
12
|
+
import { loadMcpConfig } from '../mcp/config.js';
|
|
13
|
+
import { connectMcpServers, disconnectMcpServers } from '../mcp/client.js';
|
|
14
|
+
export async function startCommand(options) {
|
|
15
|
+
const version = options.version ?? '1.0.0';
|
|
16
|
+
const chain = loadChain();
|
|
17
|
+
const apiUrl = API_URLS[chain];
|
|
18
|
+
const config = loadConfig();
|
|
19
|
+
// Resolve model — default to GLM-5.1 promo if nothing specified
|
|
20
|
+
let model;
|
|
21
|
+
const configModel = config['default-model'];
|
|
22
|
+
if (options.model) {
|
|
23
|
+
model = resolveModel(options.model);
|
|
24
|
+
}
|
|
25
|
+
else if (configModel) {
|
|
26
|
+
model = configModel;
|
|
27
|
+
}
|
|
28
|
+
else {
|
|
29
|
+
// Default: GLM-5.1 promo if still active, otherwise Gemini Flash (cheap & reliable)
|
|
30
|
+
const promoExpiry = new Date('2026-04-15');
|
|
31
|
+
model = Date.now() < promoExpiry.getTime() ? 'zai/glm-5.1' : 'google/gemini-2.5-flash';
|
|
32
|
+
}
|
|
33
|
+
// Auto-create wallet if needed (no interruption — free models work without funding)
|
|
34
|
+
let walletAddress = '';
|
|
35
|
+
if (chain === 'solana') {
|
|
36
|
+
const wallet = await getOrCreateSolanaWallet();
|
|
37
|
+
walletAddress = wallet.address;
|
|
38
|
+
if (wallet.isNew) {
|
|
39
|
+
console.log(chalk.green(' Wallet created automatically.'));
|
|
40
|
+
console.log(chalk.dim(` Address: ${wallet.address}`));
|
|
41
|
+
console.log(chalk.dim(' Free models work now. Fund with USDC for paid models.\n'));
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
else {
|
|
45
|
+
const wallet = getOrCreateWallet();
|
|
46
|
+
walletAddress = wallet.address;
|
|
47
|
+
if (wallet.isNew) {
|
|
48
|
+
console.log(chalk.green(' Wallet created automatically.'));
|
|
49
|
+
console.log(chalk.dim(` Address: ${wallet.address}`));
|
|
50
|
+
console.log(chalk.dim(' Free models work now. Fund with USDC for paid models.\n'));
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
printBanner(version);
|
|
54
|
+
const workDir = process.cwd();
|
|
55
|
+
// Show session info immediately, fetch balance in background
|
|
56
|
+
console.log(chalk.dim(` Model: ${model}`));
|
|
57
|
+
console.log(chalk.dim(` Wallet: ${walletAddress || 'not set'}`));
|
|
58
|
+
console.log(chalk.dim(` Dir: ${workDir}`));
|
|
59
|
+
// First-run tip: show if no config file exists yet
|
|
60
|
+
if (!configModel && !options.model) {
|
|
61
|
+
console.log(chalk.dim(`\n Tip: /model to switch models · /compact to save tokens · /help for all commands`));
|
|
62
|
+
}
|
|
63
|
+
console.log('');
|
|
64
|
+
// Balance fetcher — used at startup and after each turn
|
|
65
|
+
const fetchBalance = async () => {
|
|
66
|
+
try {
|
|
67
|
+
let bal;
|
|
68
|
+
if (chain === 'solana') {
|
|
69
|
+
const { setupAgentSolanaWallet } = await import('@blockrun/llm');
|
|
70
|
+
const client = await setupAgentSolanaWallet({ silent: true });
|
|
71
|
+
bal = await client.getBalance();
|
|
72
|
+
}
|
|
73
|
+
else {
|
|
74
|
+
const { setupAgentWallet } = await import('@blockrun/llm');
|
|
75
|
+
const client = setupAgentWallet({ silent: true });
|
|
76
|
+
bal = await client.getBalance();
|
|
77
|
+
}
|
|
78
|
+
return `$${bal.toFixed(2)} USDC`;
|
|
79
|
+
}
|
|
80
|
+
catch {
|
|
81
|
+
return '$?.?? USDC';
|
|
82
|
+
}
|
|
83
|
+
};
|
|
84
|
+
// Fetch balance in background (don't block startup)
|
|
85
|
+
const walletInfo = {
|
|
86
|
+
address: walletAddress,
|
|
87
|
+
balance: 'checking...',
|
|
88
|
+
chain,
|
|
89
|
+
};
|
|
90
|
+
// Balance fetch callback — will update Ink UI once resolved
|
|
91
|
+
let onBalanceFetched;
|
|
92
|
+
(async () => {
|
|
93
|
+
const balStr = await fetchBalance();
|
|
94
|
+
walletInfo.balance = balStr;
|
|
95
|
+
onBalanceFetched?.(balStr);
|
|
96
|
+
})();
|
|
97
|
+
// Assemble system instructions
|
|
98
|
+
const systemInstructions = assembleInstructions(workDir);
|
|
99
|
+
// Connect MCP servers (non-blocking — add tools if servers are available)
|
|
100
|
+
const mcpConfig = loadMcpConfig(workDir);
|
|
101
|
+
let mcpTools = [];
|
|
102
|
+
const mcpServerCount = Object.keys(mcpConfig.mcpServers).filter(k => !mcpConfig.mcpServers[k].disabled).length;
|
|
103
|
+
if (mcpServerCount > 0) {
|
|
104
|
+
try {
|
|
105
|
+
mcpTools = await connectMcpServers(mcpConfig, options.debug);
|
|
106
|
+
if (mcpTools.length > 0) {
|
|
107
|
+
console.log(chalk.dim(` MCP: ${mcpTools.length} tools from ${mcpServerCount} server(s)`));
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
catch (err) {
|
|
111
|
+
if (options.debug) {
|
|
112
|
+
console.error(chalk.yellow(` MCP error: ${err.message}`));
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
// Build capabilities (built-in + MCP + sub-agent)
|
|
117
|
+
const subAgent = createSubAgentCapability(apiUrl, chain, allCapabilities);
|
|
118
|
+
const capabilities = [...allCapabilities, ...mcpTools, subAgent];
|
|
119
|
+
// Agent config
|
|
120
|
+
const agentConfig = {
|
|
121
|
+
model,
|
|
122
|
+
apiUrl,
|
|
123
|
+
chain,
|
|
124
|
+
systemInstructions,
|
|
125
|
+
capabilities,
|
|
126
|
+
maxTurns: 100,
|
|
127
|
+
workingDir: workDir,
|
|
128
|
+
// Non-TTY (piped) input = scripted mode → trust all tools automatically.
|
|
129
|
+
// Interactive TTY = default mode (prompts for Bash/Write/Edit).
|
|
130
|
+
permissionMode: (options.trust || !process.stdin.isTTY) ? 'trust' : 'default',
|
|
131
|
+
debug: options.debug,
|
|
132
|
+
};
|
|
133
|
+
// Use Ink UI if TTY, fallback to basic readline for piped input
|
|
134
|
+
if (process.stdin.isTTY) {
|
|
135
|
+
await runWithInkUI(agentConfig, model, workDir, version, walletInfo, (cb) => {
|
|
136
|
+
onBalanceFetched = cb;
|
|
137
|
+
}, fetchBalance);
|
|
138
|
+
}
|
|
139
|
+
else {
|
|
140
|
+
await runWithBasicUI(agentConfig, model, workDir);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
// ─── Ink UI (interactive terminal) ─────────────────────────────────────────
|
|
144
|
+
async function runWithInkUI(agentConfig, model, workDir, version, walletInfo, onBalanceReady, fetchBalance) {
|
|
145
|
+
const ui = launchInkUI({
|
|
146
|
+
model,
|
|
147
|
+
workDir,
|
|
148
|
+
version,
|
|
149
|
+
walletAddress: walletInfo?.address,
|
|
150
|
+
walletBalance: walletInfo?.balance,
|
|
151
|
+
chain: walletInfo?.chain,
|
|
152
|
+
onModelChange: (newModel) => {
|
|
153
|
+
agentConfig.model = newModel;
|
|
154
|
+
},
|
|
155
|
+
});
|
|
156
|
+
// Wire permission prompts through Ink UI to avoid stdin/readline conflict.
|
|
157
|
+
// Ink owns stdin in raw mode; the old readline-based askQuestion() got EOF
|
|
158
|
+
// immediately and auto-denied every permission. Now y/n/a goes through useInput.
|
|
159
|
+
agentConfig.permissionPromptFn = (toolName, description) => ui.requestPermission(toolName, description);
|
|
160
|
+
agentConfig.onAskUser = (question, options) => ui.requestAskUser(question, options);
|
|
161
|
+
agentConfig.onModelChange = (model) => ui.updateModel(model);
|
|
162
|
+
// Wire up background balance fetch to UI
|
|
163
|
+
onBalanceReady?.((bal) => ui.updateBalance(bal));
|
|
164
|
+
// Refresh balance after each completed turn so the display stays current
|
|
165
|
+
if (fetchBalance) {
|
|
166
|
+
ui.onTurnDone(() => {
|
|
167
|
+
fetchBalance().then(bal => ui.updateBalance(bal)).catch(() => { });
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
try {
|
|
171
|
+
await interactiveSession(agentConfig, async () => {
|
|
172
|
+
const input = await ui.waitForInput();
|
|
173
|
+
if (input === null)
|
|
174
|
+
return null;
|
|
175
|
+
if (input === '')
|
|
176
|
+
return '';
|
|
177
|
+
return input;
|
|
178
|
+
}, (event) => ui.handleEvent(event), (abortFn) => ui.onAbort(abortFn));
|
|
179
|
+
}
|
|
180
|
+
catch (err) {
|
|
181
|
+
if (err.name !== 'AbortError') {
|
|
182
|
+
console.error(chalk.red(`\nError: ${err.message}`));
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
ui.cleanup();
|
|
186
|
+
flushStats();
|
|
187
|
+
await disconnectMcpServers();
|
|
188
|
+
console.log(chalk.dim('\nGoodbye.\n'));
|
|
189
|
+
}
|
|
190
|
+
// ─── Basic readline UI (piped input) ───────────────────────────────────────
|
|
191
|
+
async function runWithBasicUI(agentConfig, model, workDir) {
|
|
192
|
+
const { TerminalUI } = await import('../ui/terminal.js');
|
|
193
|
+
const ui = new TerminalUI();
|
|
194
|
+
ui.printWelcome(model, workDir);
|
|
195
|
+
let lastTerminalPrompt = '';
|
|
196
|
+
try {
|
|
197
|
+
await interactiveSession(agentConfig, async () => {
|
|
198
|
+
while (true) {
|
|
199
|
+
const input = await ui.promptUser();
|
|
200
|
+
if (input === null)
|
|
201
|
+
return null;
|
|
202
|
+
if (input === '')
|
|
203
|
+
continue;
|
|
204
|
+
// Handle slash commands in terminal UI
|
|
205
|
+
if (input.startsWith('/') && ui.handleSlashCommand(input))
|
|
206
|
+
continue;
|
|
207
|
+
// Handle model switch via /model shortcut
|
|
208
|
+
if (input === '/model' || input === '/models') {
|
|
209
|
+
console.error(chalk.dim(` Current model: ${agentConfig.model}`));
|
|
210
|
+
console.error(chalk.dim(' Switch with: /model <name> (e.g. /model sonnet, /model free)'));
|
|
211
|
+
continue;
|
|
212
|
+
}
|
|
213
|
+
if (input.startsWith('/model ')) {
|
|
214
|
+
const newModel = resolveModel(input.slice(7).trim());
|
|
215
|
+
agentConfig.model = newModel;
|
|
216
|
+
console.error(chalk.green(` Model → ${newModel}`));
|
|
217
|
+
continue;
|
|
218
|
+
}
|
|
219
|
+
// /retry — resend last prompt
|
|
220
|
+
if (input === '/retry') {
|
|
221
|
+
if (!lastTerminalPrompt) {
|
|
222
|
+
console.error(chalk.yellow(' No previous prompt to retry'));
|
|
223
|
+
continue;
|
|
224
|
+
}
|
|
225
|
+
return lastTerminalPrompt;
|
|
226
|
+
}
|
|
227
|
+
// /compact passes through to loop
|
|
228
|
+
if (input === '/compact')
|
|
229
|
+
return input;
|
|
230
|
+
lastTerminalPrompt = input;
|
|
231
|
+
return input;
|
|
232
|
+
}
|
|
233
|
+
}, (event) => ui.handleEvent(event));
|
|
234
|
+
}
|
|
235
|
+
catch (err) {
|
|
236
|
+
if (err.name !== 'AbortError') {
|
|
237
|
+
console.error(chalk.red(`\nError: ${err.message}`));
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
ui.printGoodbye();
|
|
241
|
+
flushStats();
|
|
242
|
+
}
|
|
243
|
+
async function handleSlashCommand(cmd, config, ui) {
|
|
244
|
+
const parts = cmd.trim().split(/\s+/);
|
|
245
|
+
const command = parts[0].toLowerCase();
|
|
246
|
+
switch (command) {
|
|
247
|
+
case '/exit':
|
|
248
|
+
case '/quit':
|
|
249
|
+
return 'exit';
|
|
250
|
+
case '/model': {
|
|
251
|
+
const newModel = parts[1];
|
|
252
|
+
if (newModel) {
|
|
253
|
+
config.model = resolveModel(newModel);
|
|
254
|
+
console.error(chalk.green(` Model → ${config.model}`));
|
|
255
|
+
return null;
|
|
256
|
+
}
|
|
257
|
+
const picked = await pickModel(config.model);
|
|
258
|
+
if (picked) {
|
|
259
|
+
config.model = picked;
|
|
260
|
+
console.error(chalk.green(` Model → ${config.model}`));
|
|
261
|
+
}
|
|
262
|
+
return null;
|
|
263
|
+
}
|
|
264
|
+
case '/models': {
|
|
265
|
+
const picked = await pickModel(config.model);
|
|
266
|
+
if (picked) {
|
|
267
|
+
config.model = picked;
|
|
268
|
+
console.error(chalk.green(` Model → ${config.model}`));
|
|
269
|
+
}
|
|
270
|
+
return null;
|
|
271
|
+
}
|
|
272
|
+
case '/cost':
|
|
273
|
+
case '/usage': {
|
|
274
|
+
const { getStatsSummary } = await import('../stats/tracker.js');
|
|
275
|
+
const { stats, saved } = getStatsSummary();
|
|
276
|
+
console.error(chalk.dim(`\n Requests: ${stats.totalRequests} | Cost: $${stats.totalCostUsd.toFixed(4)} | Saved: $${saved.toFixed(2)} vs Opus\n`));
|
|
277
|
+
return null;
|
|
278
|
+
}
|
|
279
|
+
case '/help':
|
|
280
|
+
console.error(chalk.bold('\n Commands:'));
|
|
281
|
+
console.error(' /model [name] — switch model (picker if no name)');
|
|
282
|
+
console.error(' /models — browse available models');
|
|
283
|
+
console.error(' /cost — session cost and savings');
|
|
284
|
+
console.error(' /exit — quit');
|
|
285
|
+
console.error(' /help — this help\n');
|
|
286
|
+
console.error(chalk.dim(' Shortcuts: sonnet, opus, gpt, gemini, deepseek, flash, free, r1, o4\n'));
|
|
287
|
+
return null;
|
|
288
|
+
default:
|
|
289
|
+
console.error(chalk.yellow(` Unknown command: ${command}. Try /help`));
|
|
290
|
+
return null;
|
|
291
|
+
}
|
|
292
|
+
}
|