@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,161 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Interactive model picker for runcode.
|
|
3
|
+
* Shows categorized model list, supports shortcuts and arrow-key selection.
|
|
4
|
+
*/
|
|
5
|
+
import readline from 'node:readline';
|
|
6
|
+
import chalk from 'chalk';
|
|
7
|
+
// ─── Model Shortcuts (same as proxy) ───────────────────────────────────────
|
|
8
|
+
export const MODEL_SHORTCUTS = {
|
|
9
|
+
// Routing profiles
|
|
10
|
+
auto: 'blockrun/auto',
|
|
11
|
+
smart: 'blockrun/auto',
|
|
12
|
+
eco: 'blockrun/eco',
|
|
13
|
+
premium: 'blockrun/premium',
|
|
14
|
+
// Anthropic
|
|
15
|
+
sonnet: 'anthropic/claude-sonnet-4.6',
|
|
16
|
+
claude: 'anthropic/claude-sonnet-4.6',
|
|
17
|
+
opus: 'anthropic/claude-opus-4.6',
|
|
18
|
+
haiku: 'anthropic/claude-haiku-4.5-20251001',
|
|
19
|
+
// OpenAI
|
|
20
|
+
gpt: 'openai/gpt-5.4',
|
|
21
|
+
gpt5: 'openai/gpt-5.4',
|
|
22
|
+
'gpt-5': 'openai/gpt-5.4',
|
|
23
|
+
'gpt-5.4': 'openai/gpt-5.4',
|
|
24
|
+
'gpt-5.4-pro': 'openai/gpt-5.4-pro',
|
|
25
|
+
'gpt-5.3': 'openai/gpt-5.3',
|
|
26
|
+
'gpt-5.2': 'openai/gpt-5.2',
|
|
27
|
+
'gpt-5.2-pro': 'openai/gpt-5.2-pro',
|
|
28
|
+
'gpt-4.1': 'openai/gpt-4.1',
|
|
29
|
+
codex: 'openai/gpt-5.3-codex',
|
|
30
|
+
nano: 'openai/gpt-5-nano',
|
|
31
|
+
mini: 'openai/gpt-5-mini',
|
|
32
|
+
o3: 'openai/o3',
|
|
33
|
+
o4: 'openai/o4-mini',
|
|
34
|
+
'o4-mini': 'openai/o4-mini',
|
|
35
|
+
o1: 'openai/o1',
|
|
36
|
+
// Google
|
|
37
|
+
gemini: 'google/gemini-2.5-pro',
|
|
38
|
+
flash: 'google/gemini-2.5-flash',
|
|
39
|
+
'gemini-3': 'google/gemini-3.1-pro',
|
|
40
|
+
// xAI
|
|
41
|
+
grok: 'xai/grok-3',
|
|
42
|
+
'grok-4': 'xai/grok-4-0709',
|
|
43
|
+
'grok-fast': 'xai/grok-4-1-fast-reasoning',
|
|
44
|
+
// DeepSeek
|
|
45
|
+
deepseek: 'deepseek/deepseek-chat',
|
|
46
|
+
r1: 'deepseek/deepseek-reasoner',
|
|
47
|
+
// Free
|
|
48
|
+
free: 'nvidia/nemotron-ultra-253b',
|
|
49
|
+
nemotron: 'nvidia/nemotron-ultra-253b',
|
|
50
|
+
'deepseek-free': 'nvidia/deepseek-v3.2',
|
|
51
|
+
devstral: 'nvidia/devstral-2-123b',
|
|
52
|
+
'qwen-coder': 'nvidia/qwen3-coder-480b',
|
|
53
|
+
maverick: 'nvidia/llama-4-maverick',
|
|
54
|
+
// Others
|
|
55
|
+
minimax: 'minimax/minimax-m2.7',
|
|
56
|
+
glm: 'zai/glm-5.1',
|
|
57
|
+
'glm-turbo': 'zai/glm-5.1-turbo',
|
|
58
|
+
'glm5': 'zai/glm-5.1',
|
|
59
|
+
kimi: 'moonshot/kimi-k2.5',
|
|
60
|
+
};
|
|
61
|
+
/**
|
|
62
|
+
* Resolve a model name — supports shortcuts.
|
|
63
|
+
*/
|
|
64
|
+
export function resolveModel(input) {
|
|
65
|
+
const lower = input.trim().toLowerCase();
|
|
66
|
+
return MODEL_SHORTCUTS[lower] || input.trim();
|
|
67
|
+
}
|
|
68
|
+
const PICKER_MODELS = [
|
|
69
|
+
{
|
|
70
|
+
category: 'Popular',
|
|
71
|
+
models: [
|
|
72
|
+
{ id: 'anthropic/claude-sonnet-4.6', shortcut: 'sonnet', label: 'Claude Sonnet 4.6', price: '$3/$15' },
|
|
73
|
+
{ id: 'anthropic/claude-opus-4.6', shortcut: 'opus', label: 'Claude Opus 4.6', price: '$5/$25' },
|
|
74
|
+
{ id: 'openai/gpt-5.4', shortcut: 'gpt', label: 'GPT-5.4', price: '$2.5/$15' },
|
|
75
|
+
{ id: 'google/gemini-2.5-pro', shortcut: 'gemini', label: 'Gemini 2.5 Pro', price: '$1.25/$10' },
|
|
76
|
+
{ id: 'deepseek/deepseek-chat', shortcut: 'deepseek', label: 'DeepSeek V3', price: '$0.28/$0.42' },
|
|
77
|
+
],
|
|
78
|
+
},
|
|
79
|
+
{
|
|
80
|
+
category: 'Budget',
|
|
81
|
+
models: [
|
|
82
|
+
{ id: 'google/gemini-2.5-flash', shortcut: 'flash', label: 'Gemini 2.5 Flash', price: '$0.15/$0.6' },
|
|
83
|
+
{ id: 'openai/gpt-5-mini', shortcut: 'mini', label: 'GPT-5 Mini', price: '$0.25/$2' },
|
|
84
|
+
{ id: 'anthropic/claude-haiku-4.5-20251001', shortcut: 'haiku', label: 'Claude Haiku 4.5', price: '$1/$5' },
|
|
85
|
+
{ id: 'openai/gpt-5-nano', shortcut: 'nano', label: 'GPT-5 Nano', price: '$0.05/$0.4' },
|
|
86
|
+
],
|
|
87
|
+
},
|
|
88
|
+
{
|
|
89
|
+
category: 'Reasoning',
|
|
90
|
+
models: [
|
|
91
|
+
{ id: 'deepseek/deepseek-reasoner', shortcut: 'r1', label: 'DeepSeek R1', price: '$0.28/$0.42' },
|
|
92
|
+
{ id: 'openai/o4-mini', shortcut: 'o4', label: 'O4 Mini', price: '$1.1/$4.4' },
|
|
93
|
+
{ id: 'openai/o3', shortcut: 'o3', label: 'O3', price: '$2/$8' },
|
|
94
|
+
],
|
|
95
|
+
},
|
|
96
|
+
{
|
|
97
|
+
category: 'Free (no USDC needed)',
|
|
98
|
+
models: [
|
|
99
|
+
{ id: 'nvidia/nemotron-ultra-253b', shortcut: 'free', label: 'Nemotron Ultra 253B', price: 'FREE' },
|
|
100
|
+
{ id: 'nvidia/qwen3-coder-480b', shortcut: 'qwen-coder', label: 'Qwen3 Coder 480B', price: 'FREE' },
|
|
101
|
+
{ id: 'nvidia/devstral-2-123b', shortcut: 'devstral', label: 'Devstral 2 123B', price: 'FREE' },
|
|
102
|
+
{ id: 'nvidia/llama-4-maverick', shortcut: 'maverick', label: 'Llama 4 Maverick', price: 'FREE' },
|
|
103
|
+
],
|
|
104
|
+
},
|
|
105
|
+
];
|
|
106
|
+
/**
|
|
107
|
+
* Show interactive model picker. Returns the selected model ID.
|
|
108
|
+
* Falls back to text input if terminal doesn't support raw mode.
|
|
109
|
+
*/
|
|
110
|
+
export async function pickModel(currentModel) {
|
|
111
|
+
// Flatten for numbering
|
|
112
|
+
const allModels = [];
|
|
113
|
+
for (const cat of PICKER_MODELS) {
|
|
114
|
+
allModels.push(...cat.models);
|
|
115
|
+
}
|
|
116
|
+
// Display
|
|
117
|
+
console.error('');
|
|
118
|
+
console.error(chalk.bold(' Select a model:\n'));
|
|
119
|
+
let idx = 1;
|
|
120
|
+
for (const cat of PICKER_MODELS) {
|
|
121
|
+
console.error(chalk.dim(` ── ${cat.category} ──`));
|
|
122
|
+
for (const m of cat.models) {
|
|
123
|
+
const current = m.id === currentModel ? chalk.green(' ←') : '';
|
|
124
|
+
const priceStr = m.price === 'FREE' ? chalk.green(m.price) : chalk.dim(m.price);
|
|
125
|
+
console.error(` ${chalk.cyan(String(idx).padStart(2))}. ${m.label.padEnd(24)} ${chalk.dim(m.shortcut.padEnd(12))} ${priceStr}${current}`);
|
|
126
|
+
idx++;
|
|
127
|
+
}
|
|
128
|
+
console.error('');
|
|
129
|
+
}
|
|
130
|
+
console.error(chalk.dim(' Enter number, shortcut, or full model ID:'));
|
|
131
|
+
// Read input
|
|
132
|
+
const rl = readline.createInterface({
|
|
133
|
+
input: process.stdin,
|
|
134
|
+
output: process.stderr,
|
|
135
|
+
terminal: process.stdin.isTTY ?? false,
|
|
136
|
+
});
|
|
137
|
+
return new Promise((resolve) => {
|
|
138
|
+
let answered = false;
|
|
139
|
+
rl.question(chalk.bold(' model> '), (answer) => {
|
|
140
|
+
answered = true;
|
|
141
|
+
rl.close();
|
|
142
|
+
const trimmed = answer.trim();
|
|
143
|
+
if (!trimmed) {
|
|
144
|
+
resolve(null); // Keep current
|
|
145
|
+
return;
|
|
146
|
+
}
|
|
147
|
+
// Try number
|
|
148
|
+
const num = parseInt(trimmed, 10);
|
|
149
|
+
if (!isNaN(num) && num >= 1 && num <= allModels.length) {
|
|
150
|
+
resolve(allModels[num - 1].id);
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
// Try shortcut or full ID
|
|
154
|
+
resolve(resolveModel(trimmed));
|
|
155
|
+
});
|
|
156
|
+
rl.on('close', () => {
|
|
157
|
+
if (!answered)
|
|
158
|
+
resolve(null);
|
|
159
|
+
});
|
|
160
|
+
});
|
|
161
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Terminal UI for runcode
|
|
3
|
+
* Raw terminal input/output with markdown rendering and diff display.
|
|
4
|
+
* No heavy dependencies — just chalk and readline.
|
|
5
|
+
*/
|
|
6
|
+
import type { StreamEvent } from '../agent/types.js';
|
|
7
|
+
export declare class TerminalUI {
|
|
8
|
+
private spinner;
|
|
9
|
+
private activeCapabilities;
|
|
10
|
+
private totalInputTokens;
|
|
11
|
+
private totalOutputTokens;
|
|
12
|
+
private sessionModel;
|
|
13
|
+
private mdRenderer;
|
|
14
|
+
private lineQueue;
|
|
15
|
+
private lineWaiters;
|
|
16
|
+
private stdinEOF;
|
|
17
|
+
constructor();
|
|
18
|
+
/**
|
|
19
|
+
* Prompt the user for input. Returns null on EOF/exit.
|
|
20
|
+
* Uses a line-queue approach so piped input works across multiple calls.
|
|
21
|
+
*/
|
|
22
|
+
promptUser(promptText?: string): Promise<string | null>;
|
|
23
|
+
private nextLine;
|
|
24
|
+
/** No-op kept for API compatibility — readline closes when stdin EOF. */
|
|
25
|
+
closeInput(): void;
|
|
26
|
+
/**
|
|
27
|
+
* Handle a stream event from the agent loop.
|
|
28
|
+
*/
|
|
29
|
+
handleEvent(event: StreamEvent): void;
|
|
30
|
+
/** Check if input is a slash command. Returns true if handled locally (don't pass to agent). */
|
|
31
|
+
handleSlashCommand(input: string): boolean;
|
|
32
|
+
printWelcome(model: string, workDir: string): void;
|
|
33
|
+
printUsageSummary(): void;
|
|
34
|
+
printGoodbye(): void;
|
|
35
|
+
}
|
|
@@ -0,0 +1,337 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Terminal UI for runcode
|
|
3
|
+
* Raw terminal input/output with markdown rendering and diff display.
|
|
4
|
+
* No heavy dependencies — just chalk and readline.
|
|
5
|
+
*/
|
|
6
|
+
import readline from 'node:readline';
|
|
7
|
+
import chalk from 'chalk';
|
|
8
|
+
import { estimateCost } from '../pricing.js';
|
|
9
|
+
// ─── Spinner ───────────────────────────────────────────────────────────────
|
|
10
|
+
const SPINNER_FRAMES = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
|
|
11
|
+
class Spinner {
|
|
12
|
+
interval = null;
|
|
13
|
+
frameIdx = 0;
|
|
14
|
+
label = '';
|
|
15
|
+
start(label) {
|
|
16
|
+
this.stop();
|
|
17
|
+
this.label = label;
|
|
18
|
+
this.frameIdx = 0;
|
|
19
|
+
this.interval = setInterval(() => {
|
|
20
|
+
const frame = SPINNER_FRAMES[this.frameIdx % SPINNER_FRAMES.length];
|
|
21
|
+
process.stderr.write(`\r${chalk.cyan(frame)} ${chalk.dim(this.label)} `);
|
|
22
|
+
this.frameIdx++;
|
|
23
|
+
}, 80);
|
|
24
|
+
}
|
|
25
|
+
stop() {
|
|
26
|
+
if (this.interval) {
|
|
27
|
+
clearInterval(this.interval);
|
|
28
|
+
this.interval = null;
|
|
29
|
+
process.stderr.write('\r' + ' '.repeat(this.label.length + 10) + '\r');
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
// ─── Markdown Renderer ─────────────────────────────────────────────────────
|
|
34
|
+
/**
|
|
35
|
+
* Simple streaming markdown renderer.
|
|
36
|
+
* Buffers content and renders when complete blocks are available.
|
|
37
|
+
*/
|
|
38
|
+
class MarkdownRenderer {
|
|
39
|
+
buffer = '';
|
|
40
|
+
inCodeBlock = false;
|
|
41
|
+
codeBlockLang = '';
|
|
42
|
+
/**
|
|
43
|
+
* Feed text delta and return rendered ANSI output.
|
|
44
|
+
*/
|
|
45
|
+
feed(text) {
|
|
46
|
+
this.buffer += text;
|
|
47
|
+
let output = '';
|
|
48
|
+
// Process complete lines
|
|
49
|
+
while (this.buffer.includes('\n')) {
|
|
50
|
+
const nlIdx = this.buffer.indexOf('\n');
|
|
51
|
+
const line = this.buffer.slice(0, nlIdx);
|
|
52
|
+
this.buffer = this.buffer.slice(nlIdx + 1);
|
|
53
|
+
output += this.renderLine(line) + '\n';
|
|
54
|
+
}
|
|
55
|
+
return output;
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Flush remaining buffer.
|
|
59
|
+
*/
|
|
60
|
+
flush() {
|
|
61
|
+
if (this.buffer.length === 0)
|
|
62
|
+
return '';
|
|
63
|
+
const result = this.renderLine(this.buffer);
|
|
64
|
+
this.buffer = '';
|
|
65
|
+
return result;
|
|
66
|
+
}
|
|
67
|
+
renderLine(line) {
|
|
68
|
+
// Code block toggle
|
|
69
|
+
if (line.startsWith('```')) {
|
|
70
|
+
if (this.inCodeBlock) {
|
|
71
|
+
this.inCodeBlock = false;
|
|
72
|
+
this.codeBlockLang = '';
|
|
73
|
+
return chalk.dim('```');
|
|
74
|
+
}
|
|
75
|
+
else {
|
|
76
|
+
this.inCodeBlock = true;
|
|
77
|
+
this.codeBlockLang = line.slice(3).trim();
|
|
78
|
+
return chalk.dim('```' + this.codeBlockLang);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
// Inside code block — render dim
|
|
82
|
+
if (this.inCodeBlock) {
|
|
83
|
+
return chalk.cyan(line);
|
|
84
|
+
}
|
|
85
|
+
// Headers
|
|
86
|
+
if (line.startsWith('### '))
|
|
87
|
+
return chalk.bold(line.slice(4));
|
|
88
|
+
if (line.startsWith('## '))
|
|
89
|
+
return chalk.bold.underline(line.slice(3));
|
|
90
|
+
if (line.startsWith('# '))
|
|
91
|
+
return chalk.bold.underline(line.slice(2));
|
|
92
|
+
// Horizontal rule
|
|
93
|
+
if (/^[-=]{3,}$/.test(line.trim()))
|
|
94
|
+
return chalk.dim('─'.repeat(40));
|
|
95
|
+
// Bullet points
|
|
96
|
+
if (line.match(/^(\s*)[-*] /)) {
|
|
97
|
+
return line.replace(/^(\s*)[-*] /, '$1• ');
|
|
98
|
+
}
|
|
99
|
+
// Numbered lists
|
|
100
|
+
if (/^\s*\d+\.\s/.test(line)) {
|
|
101
|
+
return this.renderInline(line);
|
|
102
|
+
}
|
|
103
|
+
// Blockquotes
|
|
104
|
+
if (line.startsWith('> ')) {
|
|
105
|
+
return chalk.dim('│ ') + chalk.italic(this.renderInline(line.slice(2)));
|
|
106
|
+
}
|
|
107
|
+
// Tables — leave as-is (chalk doesn't help much)
|
|
108
|
+
// Inline formatting
|
|
109
|
+
return this.renderInline(line);
|
|
110
|
+
}
|
|
111
|
+
renderInline(text) {
|
|
112
|
+
// Process in order: code first (to protect from other formatting), then bold, italic, links
|
|
113
|
+
return text
|
|
114
|
+
// Inline code (process first to protect contents)
|
|
115
|
+
.replace(/`([^`]+)`/g, (_, t) => `\x00CODE${chalk.cyan(t)}\x00END`)
|
|
116
|
+
// Bold (before italic to avoid ** being consumed by *)
|
|
117
|
+
.replace(/\*\*([^*]+)\*\*/g, (_, t) => chalk.bold(t))
|
|
118
|
+
// Italic (only single * not preceded/followed by *)
|
|
119
|
+
.replace(/(?<!\*)\*([^*]+)\*(?!\*)/g, (_, t) => chalk.italic(t))
|
|
120
|
+
// Strikethrough
|
|
121
|
+
.replace(/~~([^~]+)~~/g, (_, t) => chalk.strikethrough(t))
|
|
122
|
+
// Links
|
|
123
|
+
.replace(/\[([^\]]+)\]\(([^)]+)\)/g, (_, label, url) => chalk.blue.underline(label) + chalk.dim(` (${url})`))
|
|
124
|
+
// Restore code markers
|
|
125
|
+
.replace(/\x00CODE/g, '').replace(/\x00END/g, '');
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
// ─── Terminal UI ───────────────────────────────────────────────────────────
|
|
129
|
+
export class TerminalUI {
|
|
130
|
+
spinner = new Spinner();
|
|
131
|
+
activeCapabilities = new Map();
|
|
132
|
+
totalInputTokens = 0;
|
|
133
|
+
totalOutputTokens = 0;
|
|
134
|
+
sessionModel = '';
|
|
135
|
+
mdRenderer = new MarkdownRenderer();
|
|
136
|
+
// Line queue for piped (non-TTY) input — buffers all stdin lines eagerly
|
|
137
|
+
lineQueue = [];
|
|
138
|
+
lineWaiters = [];
|
|
139
|
+
stdinEOF = false;
|
|
140
|
+
constructor() {
|
|
141
|
+
const rl = readline.createInterface({
|
|
142
|
+
input: process.stdin,
|
|
143
|
+
output: process.stderr,
|
|
144
|
+
terminal: false, // Always treat as non-TTY so line events fire for piped input
|
|
145
|
+
});
|
|
146
|
+
rl.on('line', (line) => {
|
|
147
|
+
if (this.lineWaiters.length > 0) {
|
|
148
|
+
// Someone is already waiting — deliver immediately
|
|
149
|
+
const waiter = this.lineWaiters.shift();
|
|
150
|
+
waiter(line);
|
|
151
|
+
}
|
|
152
|
+
else {
|
|
153
|
+
// Buffer the line for the next promptUser() call
|
|
154
|
+
this.lineQueue.push(line);
|
|
155
|
+
}
|
|
156
|
+
});
|
|
157
|
+
rl.on('close', () => {
|
|
158
|
+
this.stdinEOF = true;
|
|
159
|
+
// Keep lineQueue intact — buffered lines should still drain before signaling EOF.
|
|
160
|
+
// If there are active waiters, queue is already empty (nextLine checks queue first),
|
|
161
|
+
// so it's safe to resolve them with null now.
|
|
162
|
+
for (const waiter of this.lineWaiters)
|
|
163
|
+
waiter(null);
|
|
164
|
+
this.lineWaiters = [];
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
/**
|
|
168
|
+
* Prompt the user for input. Returns null on EOF/exit.
|
|
169
|
+
* Uses a line-queue approach so piped input works across multiple calls.
|
|
170
|
+
*/
|
|
171
|
+
async promptUser(promptText) {
|
|
172
|
+
const prompt = promptText ?? chalk.bold.green('> ');
|
|
173
|
+
process.stderr.write(prompt);
|
|
174
|
+
const raw = await this.nextLine();
|
|
175
|
+
if (raw === null)
|
|
176
|
+
return null;
|
|
177
|
+
const trimmed = raw.trim();
|
|
178
|
+
if (trimmed === '/exit' || trimmed === '/quit')
|
|
179
|
+
return null;
|
|
180
|
+
return trimmed;
|
|
181
|
+
}
|
|
182
|
+
nextLine() {
|
|
183
|
+
if (this.lineQueue.length > 0) {
|
|
184
|
+
return Promise.resolve(this.lineQueue.shift());
|
|
185
|
+
}
|
|
186
|
+
if (this.stdinEOF) {
|
|
187
|
+
return Promise.resolve(null);
|
|
188
|
+
}
|
|
189
|
+
return new Promise((resolve) => {
|
|
190
|
+
this.lineWaiters.push(resolve);
|
|
191
|
+
});
|
|
192
|
+
}
|
|
193
|
+
/** No-op kept for API compatibility — readline closes when stdin EOF. */
|
|
194
|
+
closeInput() {
|
|
195
|
+
// Nothing to do — readline closes itself on stdin EOF
|
|
196
|
+
}
|
|
197
|
+
/**
|
|
198
|
+
* Handle a stream event from the agent loop.
|
|
199
|
+
*/
|
|
200
|
+
handleEvent(event) {
|
|
201
|
+
switch (event.kind) {
|
|
202
|
+
case 'text_delta': {
|
|
203
|
+
this.spinner.stop();
|
|
204
|
+
// Render markdown
|
|
205
|
+
const rendered = this.mdRenderer.feed(event.text);
|
|
206
|
+
if (rendered)
|
|
207
|
+
process.stdout.write(rendered);
|
|
208
|
+
break;
|
|
209
|
+
}
|
|
210
|
+
case 'thinking_delta':
|
|
211
|
+
this.spinner.stop();
|
|
212
|
+
process.stderr.write(chalk.dim(event.text));
|
|
213
|
+
break;
|
|
214
|
+
case 'capability_start': {
|
|
215
|
+
// Flush any pending markdown text before showing tool status
|
|
216
|
+
this.spinner.stop();
|
|
217
|
+
const flushed = this.mdRenderer.flush();
|
|
218
|
+
if (flushed)
|
|
219
|
+
process.stdout.write(flushed + '\n');
|
|
220
|
+
this.activeCapabilities.set(event.id, {
|
|
221
|
+
name: event.name,
|
|
222
|
+
startTime: Date.now(),
|
|
223
|
+
});
|
|
224
|
+
this.spinner.start(`${event.name}...`);
|
|
225
|
+
break;
|
|
226
|
+
}
|
|
227
|
+
case 'capability_input_delta':
|
|
228
|
+
break;
|
|
229
|
+
case 'capability_done': {
|
|
230
|
+
this.spinner.stop();
|
|
231
|
+
const cap = this.activeCapabilities.get(event.id);
|
|
232
|
+
const capName = cap?.name || 'unknown';
|
|
233
|
+
const elapsed = cap ? Date.now() - cap.startTime : 0;
|
|
234
|
+
this.activeCapabilities.delete(event.id);
|
|
235
|
+
const timeStr = elapsed > 100 ? chalk.dim(` ${elapsed}ms`) : '';
|
|
236
|
+
if (event.result.isError) {
|
|
237
|
+
console.error(chalk.red(` ✗ ${capName}`) +
|
|
238
|
+
timeStr +
|
|
239
|
+
chalk.red(`: ${truncateOutput(event.result.output, 200)}`));
|
|
240
|
+
}
|
|
241
|
+
else {
|
|
242
|
+
// Show diff-like output for Edit tool
|
|
243
|
+
const output = event.result.output;
|
|
244
|
+
if (capName === 'Edit' && output.includes('replacement')) {
|
|
245
|
+
console.error(chalk.green(` ✓ ${capName}`) + timeStr + chalk.dim(` — ${output}`));
|
|
246
|
+
}
|
|
247
|
+
else if (capName === 'Write') {
|
|
248
|
+
console.error(chalk.green(` ✓ ${capName}`) + timeStr + chalk.dim(` — ${output}`));
|
|
249
|
+
}
|
|
250
|
+
else if (capName === 'Bash') {
|
|
251
|
+
// Show command output preview
|
|
252
|
+
const preview = truncateOutput(output, 120);
|
|
253
|
+
console.error(chalk.green(` ✓ ${capName}`) + timeStr);
|
|
254
|
+
if (preview && preview !== '(no output)') {
|
|
255
|
+
const lines = output.split('\n').slice(0, 5);
|
|
256
|
+
for (const line of lines) {
|
|
257
|
+
console.error(chalk.dim(` │ ${line.slice(0, 100)}`));
|
|
258
|
+
}
|
|
259
|
+
if (output.split('\n').length > 5) {
|
|
260
|
+
console.error(chalk.dim(` │ ... (${output.split('\n').length - 5} more lines)`));
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
else {
|
|
265
|
+
const preview = truncateOutput(output, 120);
|
|
266
|
+
console.error(chalk.green(` ✓ ${capName}`) + timeStr + chalk.dim(` — ${preview}`));
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
break;
|
|
270
|
+
}
|
|
271
|
+
case 'usage':
|
|
272
|
+
this.totalInputTokens += event.inputTokens;
|
|
273
|
+
this.totalOutputTokens += event.outputTokens;
|
|
274
|
+
if (event.model)
|
|
275
|
+
this.sessionModel = event.model;
|
|
276
|
+
break;
|
|
277
|
+
case 'turn_done': {
|
|
278
|
+
this.spinner.stop();
|
|
279
|
+
// Flush any remaining markdown
|
|
280
|
+
const remaining = this.mdRenderer.flush();
|
|
281
|
+
if (remaining)
|
|
282
|
+
process.stdout.write(remaining);
|
|
283
|
+
process.stdout.write('\n');
|
|
284
|
+
if (event.reason === 'error') {
|
|
285
|
+
console.error(chalk.red(`\nAgent error: ${event.error}`));
|
|
286
|
+
}
|
|
287
|
+
else if (event.reason === 'max_turns') {
|
|
288
|
+
console.error(chalk.yellow('\nMax turns reached.'));
|
|
289
|
+
}
|
|
290
|
+
// Reset renderer for next turn
|
|
291
|
+
this.mdRenderer = new MarkdownRenderer();
|
|
292
|
+
break;
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
/** Check if input is a slash command. Returns true if handled locally (don't pass to agent). */
|
|
297
|
+
handleSlashCommand(input) {
|
|
298
|
+
const parts = input.trim().split(/\s+/);
|
|
299
|
+
const cmd = parts[0].toLowerCase();
|
|
300
|
+
switch (cmd) {
|
|
301
|
+
case '/cost':
|
|
302
|
+
case '/usage': {
|
|
303
|
+
const cost = this.sessionModel
|
|
304
|
+
? estimateCost(this.sessionModel, this.totalInputTokens, this.totalOutputTokens)
|
|
305
|
+
: 0;
|
|
306
|
+
const costStr = cost > 0 ? ` · $${cost.toFixed(4)} USDC` : '';
|
|
307
|
+
console.error(chalk.dim(`\n Tokens: ${this.totalInputTokens.toLocaleString()} in / ${this.totalOutputTokens.toLocaleString()} out${costStr}\n`));
|
|
308
|
+
return true;
|
|
309
|
+
}
|
|
310
|
+
default:
|
|
311
|
+
// All other slash commands pass through to the agent loop (commands.ts handles them)
|
|
312
|
+
return false;
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
printWelcome(model, workDir) {
|
|
316
|
+
console.error(chalk.dim(`Model: ${model}`));
|
|
317
|
+
console.error(chalk.dim(`Dir: ${workDir}`));
|
|
318
|
+
console.error(chalk.dim(`Type /exit to quit, /help for commands.\n`));
|
|
319
|
+
}
|
|
320
|
+
printUsageSummary() {
|
|
321
|
+
if (this.totalInputTokens > 0 || this.totalOutputTokens > 0) {
|
|
322
|
+
console.error(chalk.dim(`\nTokens: ${this.totalInputTokens.toLocaleString()} in / ${this.totalOutputTokens.toLocaleString()} out`));
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
printGoodbye() {
|
|
326
|
+
this.closeInput();
|
|
327
|
+
this.printUsageSummary();
|
|
328
|
+
console.error(chalk.dim('\nGoodbye.\n'));
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
// ─── Helpers ───────────────────────────────────────────────────────────────
|
|
332
|
+
function truncateOutput(text, maxLen) {
|
|
333
|
+
const oneLine = text.replace(/\n/g, ' ').trim();
|
|
334
|
+
if (oneLine.length <= maxLen)
|
|
335
|
+
return oneLine;
|
|
336
|
+
return oneLine.slice(0, maxLen - 3) + '...';
|
|
337
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export declare function walletExists(): boolean;
|
|
2
|
+
export declare function setupWallet(): {
|
|
3
|
+
address: string;
|
|
4
|
+
isNew: boolean;
|
|
5
|
+
};
|
|
6
|
+
export declare function setupSolanaWallet(): Promise<{
|
|
7
|
+
address: string;
|
|
8
|
+
isNew: boolean;
|
|
9
|
+
}>;
|
|
10
|
+
export declare function getAddress(): string;
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { getOrCreateWallet, scanWallets, getWalletAddress, getOrCreateSolanaWallet, scanSolanaWallets, } from '@blockrun/llm';
|
|
2
|
+
import { loadChain } from '../config.js';
|
|
3
|
+
export function walletExists() {
|
|
4
|
+
const chain = loadChain();
|
|
5
|
+
if (chain === 'solana') {
|
|
6
|
+
return scanSolanaWallets().length > 0;
|
|
7
|
+
}
|
|
8
|
+
return scanWallets().length > 0;
|
|
9
|
+
}
|
|
10
|
+
export function setupWallet() {
|
|
11
|
+
const { address, isNew } = getOrCreateWallet();
|
|
12
|
+
return { address, isNew };
|
|
13
|
+
}
|
|
14
|
+
export async function setupSolanaWallet() {
|
|
15
|
+
const { address, isNew } = await getOrCreateSolanaWallet();
|
|
16
|
+
return { address, isNew };
|
|
17
|
+
}
|
|
18
|
+
export function getAddress() {
|
|
19
|
+
const addr = getWalletAddress();
|
|
20
|
+
if (!addr)
|
|
21
|
+
throw new Error('No wallet found. Run `runcode setup` first.');
|
|
22
|
+
return addr;
|
|
23
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@blockrun/franklin",
|
|
3
|
+
"version": "3.0.0",
|
|
4
|
+
"description": "Franklin — The AI agent with a wallet. Marketing at franklin.run, trading at franklin.bet. Pay per action in USDC.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"exports": {
|
|
7
|
+
".": {
|
|
8
|
+
"types": "./dist/index.d.ts",
|
|
9
|
+
"default": "./dist/index.js"
|
|
10
|
+
},
|
|
11
|
+
"./plugin-sdk": {
|
|
12
|
+
"types": "./dist/plugin-sdk/index.d.ts",
|
|
13
|
+
"default": "./dist/plugin-sdk/index.js"
|
|
14
|
+
},
|
|
15
|
+
"./package.json": "./package.json"
|
|
16
|
+
},
|
|
17
|
+
"bin": {
|
|
18
|
+
"franklin": "./dist/index.js",
|
|
19
|
+
"runcode": "./dist/index.js"
|
|
20
|
+
},
|
|
21
|
+
"files": [
|
|
22
|
+
"dist",
|
|
23
|
+
"README.md",
|
|
24
|
+
"LICENSE"
|
|
25
|
+
],
|
|
26
|
+
"scripts": {
|
|
27
|
+
"build": "tsc && node scripts/copy-plugin-assets.mjs",
|
|
28
|
+
"dev": "tsc --watch",
|
|
29
|
+
"start": "node dist/index.js",
|
|
30
|
+
"test": "npm run build && node --test --test-reporter=spec test/local.mjs",
|
|
31
|
+
"test:e2e": "npm run build && node --test --test-reporter=spec test/e2e.mjs",
|
|
32
|
+
"test:all": "npm run test && npm run test:e2e",
|
|
33
|
+
"test:watch": "node --test --watch test/local.mjs",
|
|
34
|
+
"test:e2e:watch": "node --test --watch test/e2e.mjs"
|
|
35
|
+
},
|
|
36
|
+
"keywords": [
|
|
37
|
+
"franklin",
|
|
38
|
+
"autonomous-economic-agent",
|
|
39
|
+
"ai-agent",
|
|
40
|
+
"wallet-native",
|
|
41
|
+
"agent-with-wallet",
|
|
42
|
+
"x402",
|
|
43
|
+
"usdc",
|
|
44
|
+
"pay-per-action",
|
|
45
|
+
"multi-model",
|
|
46
|
+
"ai-marketing",
|
|
47
|
+
"ai-trading",
|
|
48
|
+
"crypto-native",
|
|
49
|
+
"blockrun",
|
|
50
|
+
"runcode"
|
|
51
|
+
],
|
|
52
|
+
"license": "Apache-2.0",
|
|
53
|
+
"repository": {
|
|
54
|
+
"type": "git",
|
|
55
|
+
"url": "https://github.com/BlockRunAI/runcode"
|
|
56
|
+
},
|
|
57
|
+
"homepage": "https://franklin.run",
|
|
58
|
+
"engines": {
|
|
59
|
+
"node": ">=20"
|
|
60
|
+
},
|
|
61
|
+
"dependencies": {
|
|
62
|
+
"@blockrun/llm": "^1.4.2",
|
|
63
|
+
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
64
|
+
"@solana/spl-token": "^0.4.14",
|
|
65
|
+
"@solana/web3.js": "^1.98.4",
|
|
66
|
+
"@types/react": "^19.2.14",
|
|
67
|
+
"bs58": "^6.0.0",
|
|
68
|
+
"chalk": "^5.4.0",
|
|
69
|
+
"commander": "^13.0.0",
|
|
70
|
+
"ink": "^6.8.0",
|
|
71
|
+
"ink-spinner": "^5.0.0",
|
|
72
|
+
"ink-text-input": "^6.0.0",
|
|
73
|
+
"react": "^19.2.4"
|
|
74
|
+
},
|
|
75
|
+
"devDependencies": {
|
|
76
|
+
"@types/node": "^22.0.0",
|
|
77
|
+
"typescript": "^5.7.0"
|
|
78
|
+
}
|
|
79
|
+
}
|