@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,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
+ }