@blockrun/franklin 3.3.3 → 3.5.1

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 (109) hide show
  1. package/README.md +65 -25
  2. package/dist/agent/commands.d.ts +1 -1
  3. package/dist/agent/commands.js +128 -17
  4. package/dist/agent/compact.d.ts +2 -2
  5. package/dist/agent/compact.js +148 -22
  6. package/dist/agent/context.d.ts +8 -3
  7. package/dist/agent/context.js +301 -108
  8. package/dist/agent/error-classifier.d.ts +11 -2
  9. package/dist/agent/error-classifier.js +64 -10
  10. package/dist/agent/llm.d.ts +8 -1
  11. package/dist/agent/llm.js +114 -19
  12. package/dist/agent/loop.d.ts +1 -2
  13. package/dist/agent/loop.js +509 -61
  14. package/dist/agent/optimize.d.ts +2 -2
  15. package/dist/agent/optimize.js +9 -7
  16. package/dist/agent/permissions.d.ts +1 -1
  17. package/dist/agent/permissions.js +1 -1
  18. package/dist/agent/planner.d.ts +42 -0
  19. package/dist/agent/planner.js +110 -0
  20. package/dist/agent/reduce.d.ts +7 -1
  21. package/dist/agent/reduce.js +85 -3
  22. package/dist/agent/streaming-executor.d.ts +6 -1
  23. package/dist/agent/streaming-executor.js +83 -5
  24. package/dist/agent/tokens.d.ts +11 -2
  25. package/dist/agent/tokens.js +38 -5
  26. package/dist/agent/tool-guard.d.ts +27 -0
  27. package/dist/agent/tool-guard.js +324 -0
  28. package/dist/agent/types.d.ts +7 -1
  29. package/dist/agent/types.js +1 -1
  30. package/dist/brain/extract.d.ts +11 -0
  31. package/dist/brain/extract.js +154 -0
  32. package/dist/brain/index.d.ts +3 -0
  33. package/dist/brain/index.js +2 -0
  34. package/dist/brain/store.d.ts +42 -0
  35. package/dist/brain/store.js +225 -0
  36. package/dist/brain/types.d.ts +45 -0
  37. package/dist/brain/types.js +5 -0
  38. package/dist/commands/daemon.js +2 -1
  39. package/dist/commands/start.js +19 -7
  40. package/dist/config.js +1 -1
  41. package/dist/index.js +27 -2
  42. package/dist/learnings/extractor.d.ts +13 -0
  43. package/dist/learnings/extractor.js +69 -8
  44. package/dist/learnings/index.d.ts +1 -1
  45. package/dist/learnings/index.js +1 -1
  46. package/dist/learnings/store.js +42 -13
  47. package/dist/learnings/types.d.ts +1 -1
  48. package/dist/mcp/client.d.ts +1 -1
  49. package/dist/mcp/client.js +5 -5
  50. package/dist/mcp/config.d.ts +1 -1
  51. package/dist/mcp/config.js +1 -1
  52. package/dist/panel/html.d.ts +2 -0
  53. package/dist/panel/html.js +409 -146
  54. package/dist/panel/server.js +19 -0
  55. package/dist/pricing.js +3 -2
  56. package/dist/proxy/fallback.d.ts +3 -1
  57. package/dist/proxy/fallback.js +4 -4
  58. package/dist/proxy/server.js +29 -11
  59. package/dist/proxy/sse-translator.js +1 -1
  60. package/dist/router/categories.d.ts +21 -0
  61. package/dist/router/categories.js +96 -0
  62. package/dist/router/index.d.ts +9 -2
  63. package/dist/router/index.js +106 -27
  64. package/dist/router/local-elo.d.ts +32 -0
  65. package/dist/router/local-elo.js +107 -0
  66. package/dist/router/selector.d.ts +46 -0
  67. package/dist/router/selector.js +106 -0
  68. package/dist/session/storage.d.ts +5 -1
  69. package/dist/session/storage.js +24 -2
  70. package/dist/social/a11y.d.ts +1 -1
  71. package/dist/social/a11y.js +5 -1
  72. package/dist/social/browser.d.ts +5 -0
  73. package/dist/social/browser.js +22 -0
  74. package/dist/social/preflight.d.ts +4 -0
  75. package/dist/social/preflight.js +42 -3
  76. package/dist/stats/failures.d.ts +20 -0
  77. package/dist/stats/failures.js +63 -0
  78. package/dist/stats/format.d.ts +6 -0
  79. package/dist/stats/format.js +23 -0
  80. package/dist/stats/insights.js +1 -21
  81. package/dist/stats/session-tracker.d.ts +21 -0
  82. package/dist/stats/session-tracker.js +28 -0
  83. package/dist/stats/tracker.d.ts +1 -1
  84. package/dist/stats/tracker.js +1 -1
  85. package/dist/tools/bash.d.ts +14 -1
  86. package/dist/tools/bash.js +132 -7
  87. package/dist/tools/edit.js +77 -14
  88. package/dist/tools/glob.js +13 -3
  89. package/dist/tools/grep.js +30 -12
  90. package/dist/tools/imagegen.js +5 -5
  91. package/dist/tools/index.d.ts +1 -1
  92. package/dist/tools/index.js +5 -1
  93. package/dist/tools/read.d.ts +16 -2
  94. package/dist/tools/read.js +36 -8
  95. package/dist/tools/searchx.d.ts +6 -2
  96. package/dist/tools/searchx.js +221 -44
  97. package/dist/tools/subagent.js +37 -3
  98. package/dist/tools/task.js +43 -7
  99. package/dist/tools/validate.d.ts +11 -0
  100. package/dist/tools/validate.js +42 -0
  101. package/dist/tools/webfetch.js +18 -7
  102. package/dist/tools/websearch.js +41 -7
  103. package/dist/tools/write.js +26 -6
  104. package/dist/ui/app.js +31 -6
  105. package/dist/ui/model-picker.d.ts +1 -1
  106. package/dist/ui/model-picker.js +1 -1
  107. package/dist/ui/terminal.d.ts +1 -1
  108. package/dist/ui/terminal.js +1 -1
  109. package/package.json +2 -2
@@ -2,12 +2,15 @@
2
2
  * WebSearch capability — search the web via BlockRun API or DuckDuckGo fallback.
3
3
  */
4
4
  import { VERSION } from '../config.js';
5
+ const MAX_RESULTS_CAP = 8;
6
+ const MAX_SNIPPET_CHARS = 220;
7
+ const MAX_OUTPUT_CHARS = 3_200;
5
8
  async function execute(input, _ctx) {
6
9
  const { query, max_results } = input;
7
10
  if (!query) {
8
11
  return { output: 'Error: query is required', isError: true };
9
12
  }
10
- const maxResults = Math.min(Math.max(max_results ?? 5, 1), 20);
13
+ const maxResults = Math.min(Math.max(max_results ?? 5, 1), MAX_RESULTS_CAP);
11
14
  // Try DuckDuckGo HTML search (no API key needed)
12
15
  try {
13
16
  const encoded = encodeURIComponent(query);
@@ -17,7 +20,7 @@ async function execute(input, _ctx) {
17
20
  const response = await fetch(url, {
18
21
  signal: controller.signal,
19
22
  headers: {
20
- 'User-Agent': `runcode/${VERSION} (coding-agent)`,
23
+ 'User-Agent': `franklin/${VERSION} (coding-agent)`,
21
24
  },
22
25
  });
23
26
  clearTimeout(timeout);
@@ -29,10 +32,22 @@ async function execute(input, _ctx) {
29
32
  if (results.length === 0) {
30
33
  return { output: `No results found for: ${query}` };
31
34
  }
32
- const formatted = results
33
- .map((r, i) => `${i + 1}. ${r.title}\n ${r.url}\n ${r.snippet}`)
34
- .join('\n\n');
35
- return { output: `Search results for "${query}":\n\n${formatted}` };
35
+ const lines = [];
36
+ let totalChars = `Search results for "${query}":\n\n`.length;
37
+ for (let i = 0; i < results.length; i++) {
38
+ const r = results[i];
39
+ const snippet = r.snippet.length > MAX_SNIPPET_CHARS
40
+ ? r.snippet.slice(0, MAX_SNIPPET_CHARS - 3) + '...'
41
+ : r.snippet;
42
+ const block = `${i + 1}. ${r.title}\n ${r.url}\n ${snippet}`;
43
+ if (lines.length > 0 && totalChars + block.length + 2 > MAX_OUTPUT_CHARS) {
44
+ lines.push(`... (${results.length - i} more results omitted)`);
45
+ break;
46
+ }
47
+ lines.push(block);
48
+ totalChars += block.length + 2;
49
+ }
50
+ return { output: `Search results for "${query}":\n\n${lines.join('\n\n')}` };
36
51
  }
37
52
  catch (err) {
38
53
  const msg = err instanceof Error ? err.message : String(err);
@@ -44,6 +59,7 @@ async function execute(input, _ctx) {
44
59
  }
45
60
  function parseDuckDuckGoResults(html, maxResults) {
46
61
  const results = [];
62
+ const seenUrls = new Set();
47
63
  // Primary parser: match result blocks by class names
48
64
  const linkRegex = /<a[^>]*class="result__a"[^>]*href="([^"]*)"[^>]*>([\s\S]*?)<\/a>/gi;
49
65
  const snippetRegex = /<a[^>]*class="result__snippet"[^>]*>([\s\S]*?)<\/a>/gi;
@@ -66,6 +82,9 @@ function parseDuckDuckGoResults(html, maxResults) {
66
82
  // Skip internal DDG links
67
83
  if (url.startsWith('/') || url.includes('duckduckgo.com'))
68
84
  continue;
85
+ if (seenUrls.has(url))
86
+ continue;
87
+ seenUrls.add(url);
69
88
  results.push({
70
89
  title: stripTags(link[2] || '').trim(),
71
90
  url,
@@ -88,7 +107,22 @@ function stripTags(html) {
88
107
  export const webSearchCapability = {
89
108
  spec: {
90
109
  name: 'WebSearch',
91
- description: 'Search the web and return results with titles, URLs, and snippets.',
110
+ description: `Search the web and use the results to inform responses. Returns titles, URLs, and snippets.
111
+
112
+ Usage:
113
+ - Provides up-to-date information beyond training data cutoff
114
+ - Cannot access X.com content (use SearchX for X posts)
115
+ - Do NOT rephrase and retry the same search — if results are empty, stop. Max 3-5 searches per topic.
116
+
117
+ CRITICAL REQUIREMENT — After answering, you MUST include a "Sources:" section at the end of your response listing all relevant URLs as markdown hyperlinks:
118
+
119
+ Sources:
120
+ - [Source Title 1](https://example.com/1)
121
+ - [Source Title 2](https://example.com/2)
122
+
123
+ This is MANDATORY — never skip including sources when using web search results.
124
+
125
+ IMPORTANT — The current date is ${new Date().toISOString().slice(0, 7)} (${new Date().toLocaleString('en-US', { month: 'long', year: 'numeric' })}). Use the current year when searching for recent information, documentation, or current events.`,
92
126
  input_schema: {
93
127
  type: 'object',
94
128
  properties: {
@@ -4,7 +4,7 @@
4
4
  import fs from 'node:fs';
5
5
  import path from 'node:path';
6
6
  import os from 'node:os';
7
- import { partiallyReadFiles } from './read.js';
7
+ import { partiallyReadFiles, fileReadTracker } from './read.js';
8
8
  function withTrailingSep(value) {
9
9
  return value.endsWith(path.sep) ? value : value + path.sep;
10
10
  }
@@ -79,18 +79,28 @@ async function execute(input, ctx) {
79
79
  }
80
80
  }
81
81
  catch { /* file doesn't exist yet, ok */ }
82
+ // Enforce read-before-overwrite for existing files
83
+ const fileExists = fs.existsSync(resolved);
84
+ if (fileExists && !fileReadTracker.has(resolved)) {
85
+ return {
86
+ output: `Error: this file already exists. You MUST use Read first to understand its current content before overwriting.\nFile: ${resolved}`,
87
+ isError: true,
88
+ };
89
+ }
82
90
  try {
83
91
  // Ensure parent directory exists
84
92
  const parentDir = path.dirname(resolved);
85
93
  fs.mkdirSync(parentDir, { recursive: true });
86
- const existed = fs.existsSync(resolved);
87
94
  fs.writeFileSync(resolved, content, 'utf-8');
88
95
  partiallyReadFiles.delete(resolved);
96
+ // Update read tracker so subsequent edits don't trigger stale detection
97
+ const newStat = fs.statSync(resolved);
98
+ fileReadTracker.set(resolved, { mtimeMs: newStat.mtimeMs, readAt: Date.now() });
89
99
  const lineCount = content.split('\n').length;
90
100
  const byteCount = Buffer.byteLength(content, 'utf-8');
91
101
  const sizeStr = byteCount >= 1024 ? `${(byteCount / 1024).toFixed(1)}KB` : `${byteCount}B`;
92
102
  return {
93
- output: `${existed ? 'Updated' : 'Created'} ${resolved} (${lineCount} lines, ${sizeStr})`,
103
+ output: `${fileExists ? 'Updated' : 'Created'} ${resolved} (${lineCount} lines, ${sizeStr})`,
94
104
  };
95
105
  }
96
106
  catch (err) {
@@ -101,12 +111,22 @@ async function execute(input, ctx) {
101
111
  export const writeCapability = {
102
112
  spec: {
103
113
  name: 'Write',
104
- description: 'Create or overwrite a file.',
114
+ description: `Write a file to the local filesystem.
115
+
116
+ Usage:
117
+ - This tool will overwrite the existing file if there is one at the provided path.
118
+ - If this is an existing file, you MUST use Read first to read the file's contents. This tool will fail if you did not read an existing file first.
119
+ - Prefer the Edit tool for modifying existing files — it only sends the diff. Only use this tool to create new files or for complete rewrites.
120
+ - NEVER create documentation files (*.md) or README files unless explicitly requested by the user.
121
+ - Only use emojis if the user explicitly requests it. Avoid writing emojis to files unless asked.
122
+ - Do not create files unless they're absolutely necessary for achieving your goal. Generally prefer editing an existing file to creating a new one, as this prevents file bloat and builds on existing work.
123
+
124
+ IMPORTANT: Always use Write instead of echo/heredoc/cat redirection via Bash.`,
105
125
  input_schema: {
106
126
  type: 'object',
107
127
  properties: {
108
- file_path: { type: 'string', description: 'Absolute path' },
109
- content: { type: 'string', description: 'File content' },
128
+ file_path: { type: 'string', description: 'The absolute path to the file to write (must be absolute, not relative)' },
129
+ content: { type: 'string', description: 'The content to write to the file' },
110
130
  },
111
131
  required: ['file_path', 'content'],
112
132
  },
package/dist/ui/app.js CHANGED
@@ -11,8 +11,9 @@ import TextInput from 'ink-text-input';
11
11
  import { renderMarkdown } from './markdown.js';
12
12
  import { resolveModel, PICKER_CATEGORIES, PICKER_MODELS_FLAT, } from './model-picker.js';
13
13
  import { estimateCost } from '../pricing.js';
14
+ import { formatTokens, shortModelName } from '../stats/format.js';
14
15
  // ─── Full-width input box ──────────────────────────────────────────────────
15
- function InputBox({ input, setInput, onSubmit, model, balance, sessionCost, queued, queuedCount, focused, busy }) {
16
+ function InputBox({ input, setInput, onSubmit, model, balance, sessionCost, queued, queuedCount, focused, busy, contextPct }) {
16
17
  const { stdout } = useStdout();
17
18
  const cols = stdout?.columns ?? 80;
18
19
  const innerWidth = Math.min(Math.max(30, cols - 4), cols - 2);
@@ -21,7 +22,7 @@ function InputBox({ input, setInput, onSubmit, model, balance, sessionCost, queu
21
22
  ? `⏎ ${queuedCount ?? 1} queued: ${queued.slice(0, 40)}`
22
23
  : 'Working...')
23
24
  : 'Type a message...';
24
- return (_jsxs(Box, { flexDirection: "column", marginTop: 1, children: [_jsx(Text, { dimColor: true, children: '╭' + '─'.repeat(cols - 2) + '╮' }), _jsxs(Box, { children: [_jsx(Text, { dimColor: true, children: "\u2502 " }), busy && !input ? _jsxs(Text, { color: "yellow", children: [_jsx(Spinner, { type: "dots" }), " "] }) : null, _jsx(Box, { width: busy && !input ? innerWidth - 4 : innerWidth, children: _jsx(TextInput, { value: input, onChange: setInput, onSubmit: onSubmit, placeholder: placeholder, focus: focused !== false }) }), _jsxs(Text, { dimColor: true, children: [' '.repeat(Math.max(0, cols - innerWidth - 4)), "\u2502"] })] }), _jsx(Text, { dimColor: true, children: '╰' + '─'.repeat(cols - 2) + '╯' }), _jsx(Box, { marginLeft: 1, children: _jsxs(Text, { dimColor: true, children: [busy ? _jsx(Text, { color: "yellow", children: _jsx(Spinner, { type: "dots" }) }) : null, busy ? ' ' : '', model, " \u00B7 ", balance, sessionCost > 0.00001 ? _jsxs(Text, { color: "yellow", children: [" -$", sessionCost.toFixed(4)] }) : '', (queuedCount ?? 0) > 0 ? _jsxs(Text, { color: "cyan", children: [" \u00B7 ", queuedCount, " queued"] }) : null, ' · esc to abort/quit'] }) })] }));
25
+ return (_jsxs(Box, { flexDirection: "column", marginTop: 1, children: [_jsx(Text, { dimColor: true, children: '╭' + '─'.repeat(cols - 2) + '╮' }), _jsxs(Box, { children: [_jsx(Text, { dimColor: true, children: "\u2502 " }), busy && !input ? _jsxs(Text, { color: "yellow", children: [_jsx(Spinner, { type: "dots" }), " "] }) : null, _jsx(Box, { width: busy && !input ? innerWidth - 4 : innerWidth, children: _jsx(TextInput, { value: input, onChange: setInput, onSubmit: onSubmit, placeholder: placeholder, focus: focused !== false }) }), _jsxs(Text, { dimColor: true, children: [' '.repeat(Math.max(0, cols - innerWidth - 4)), "\u2502"] })] }), _jsx(Text, { dimColor: true, children: '╰' + '─'.repeat(cols - 2) + '╯' }), _jsx(Box, { marginLeft: 1, children: _jsxs(Text, { dimColor: true, children: [busy ? _jsx(Text, { color: "yellow", children: _jsx(Spinner, { type: "dots" }) }) : null, busy ? ' ' : '', model, " \u00B7 ", balance, sessionCost > 0.00001 ? _jsxs(Text, { color: "yellow", children: [" -$", sessionCost.toFixed(4)] }) : '', contextPct !== undefined && contextPct > 0 ? (_jsxs(Text, { color: contextPct > 85 ? 'red' : contextPct > 70 ? 'yellow' : undefined, children: [' · ctx ', contextPct, '%'] })) : null, (queuedCount ?? 0) > 0 ? _jsxs(Text, { color: "cyan", children: [" \u00B7 ", queuedCount, " queued"] }) : null, ' · esc'] }) })] }));
25
26
  }
26
27
  function RunCodeApp({ initialModel, workDir, walletAddress, walletBalance, chain, startWithPicker, onSubmit, onModelChange, onAbort, onExit, }) {
27
28
  const { exit } = useApp();
@@ -43,6 +44,7 @@ function RunCodeApp({ initialModel, workDir, walletAddress, walletBalance, chain
43
44
  const [statusMsg, setStatusMsg] = useState('');
44
45
  const [statusTone, setStatusTone] = useState('success');
45
46
  const [turnTokens, setTurnTokens] = useState({ input: 0, output: 0, calls: 0 });
47
+ const [contextPct, setContextPct] = useState(0);
46
48
  const [totalCost, setTotalCost] = useState(0);
47
49
  const [showHelp, setShowHelp] = useState(false);
48
50
  const [showWallet, setShowWallet] = useState(false);
@@ -72,6 +74,9 @@ function RunCodeApp({ initialModel, workDir, walletAddress, walletBalance, chain
72
74
  const turnTokensRef = useRef({ input: 0, output: 0, calls: 0 });
73
75
  const totalCostRef = useRef(0);
74
76
  const turnCostRef = useRef(0); // per-turn cost (reset each turn)
77
+ const turnModelRef = useRef(undefined);
78
+ const turnTierRef = useRef(undefined);
79
+ const turnSavingsRef = useRef(undefined);
75
80
  const queuedInputsRef = useRef([]);
76
81
  // Keep refs in sync so memoized event handlers can read current values
77
82
  streamTextRef.current = streamText;
@@ -99,6 +104,9 @@ function RunCodeApp({ initialModel, workDir, walletAddress, walletBalance, chain
99
104
  text,
100
105
  tokens,
101
106
  cost,
107
+ model: turnModelRef.current,
108
+ tier: turnTierRef.current,
109
+ savings: turnSavingsRef.current,
102
110
  }]);
103
111
  const allLines = text.split('\n');
104
112
  if (allLines.length > 20) {
@@ -253,6 +261,9 @@ function RunCodeApp({ initialModel, workDir, walletAddress, walletBalance, chain
253
261
  setTools(new Map());
254
262
  setTurnTokens({ input: 0, output: 0, calls: 0 });
255
263
  turnCostRef.current = 0;
264
+ turnModelRef.current = undefined;
265
+ turnTierRef.current = undefined;
266
+ turnSavingsRef.current = undefined;
256
267
  setWaiting(true);
257
268
  setReady(false);
258
269
  // Pass through to agent loop to clear the actual conversation history
@@ -271,6 +282,9 @@ function RunCodeApp({ initialModel, workDir, walletAddress, walletBalance, chain
271
282
  setWaiting(true);
272
283
  setTurnTokens({ input: 0, output: 0, calls: 0 });
273
284
  turnCostRef.current = 0;
285
+ turnModelRef.current = undefined;
286
+ turnTierRef.current = undefined;
287
+ turnSavingsRef.current = undefined;
274
288
  onSubmit(lastPrompt);
275
289
  return;
276
290
  default:
@@ -310,6 +324,9 @@ function RunCodeApp({ initialModel, workDir, walletAddress, walletBalance, chain
310
324
  setShowWallet(false);
311
325
  setTurnTokens({ input: 0, output: 0, calls: 0 });
312
326
  turnCostRef.current = 0;
327
+ turnModelRef.current = undefined;
328
+ turnTierRef.current = undefined;
329
+ turnSavingsRef.current = undefined;
313
330
  onSubmit(trimmed);
314
331
  }, [ready, currentModel, totalCost, onSubmit, onModelChange, onAbort, onExit, exit, lastPrompt, inputHistory, showStatus]);
315
332
  // Expose event handler, balance updater, and permission bridge
@@ -416,6 +433,14 @@ function RunCodeApp({ initialModel, workDir, walletAddress, walletBalance, chain
416
433
  const turnCallCost = estimateCost(event.model, event.inputTokens, event.outputTokens, event.calls ?? 1);
417
434
  turnCostRef.current += turnCallCost;
418
435
  setTotalCost(prev => prev + turnCallCost);
436
+ // Capture routing metadata for this turn
437
+ turnModelRef.current = event.model;
438
+ if (event.tier)
439
+ turnTierRef.current = event.tier;
440
+ if (event.savings !== undefined)
441
+ turnSavingsRef.current = event.savings;
442
+ if (event.contextPct !== undefined)
443
+ setContextPct(event.contextPct);
419
444
  break;
420
445
  }
421
446
  case 'turn_done': {
@@ -479,15 +504,15 @@ function RunCodeApp({ initialModel, workDir, walletAddress, walletBalance, chain
479
504
  const inPicker = mode === 'model-picker';
480
505
  return (_jsxs(Box, { flexDirection: "column", children: [statusMsg && (_jsx(Box, { marginLeft: 2, children: _jsx(Text, { color: statusTone === 'error' ? 'red' : statusTone === 'warning' ? 'yellow' : 'green', children: statusMsg }) })), showHelp && (_jsxs(Box, { flexDirection: "column", marginLeft: 2, marginTop: 1, marginBottom: 1, children: [_jsx(Text, { bold: true, children: "Commands" }), _jsx(Text, { children: " " }), _jsxs(Text, { children: [" ", _jsx(Text, { color: "cyan", children: "/model" }), " [name] Switch model (picker if no name)"] }), _jsxs(Text, { children: [" ", _jsx(Text, { color: "cyan", children: "/wallet" }), " Show wallet address & balance"] }), _jsxs(Text, { children: [" ", _jsx(Text, { color: "cyan", children: "/cost" }), " Session cost & savings"] }), _jsxs(Text, { children: [" ", _jsx(Text, { color: "cyan", children: "/retry" }), " Retry the last prompt"] }), _jsxs(Text, { children: [" ", _jsx(Text, { color: "cyan", children: "/compact" }), " Compress conversation history"] }), _jsx(Text, { dimColor: true, children: " \u2500\u2500 Coding \u2500\u2500" }), _jsxs(Text, { children: [" ", _jsx(Text, { color: "cyan", children: "/test" }), " Run tests"] }), _jsxs(Text, { children: [" ", _jsx(Text, { color: "cyan", children: "/fix" }), " Fix last error"] }), _jsxs(Text, { children: [" ", _jsx(Text, { color: "cyan", children: "/review" }), " Code review"] }), _jsxs(Text, { children: [" ", _jsx(Text, { color: "cyan", children: "/explain" }), " file Explain code"] }), _jsxs(Text, { children: [" ", _jsx(Text, { color: "cyan", children: "/search" }), " query Search codebase"] }), _jsxs(Text, { children: [" ", _jsx(Text, { color: "cyan", children: "/session-search" }), " q Search past sessions"] }), _jsxs(Text, { children: [" ", _jsx(Text, { color: "cyan", children: "/refactor" }), " desc Refactor code"] }), _jsxs(Text, { children: [" ", _jsx(Text, { color: "cyan", children: "/scaffold" }), " desc Generate boilerplate"] }), _jsx(Text, { dimColor: true, children: " \u2500\u2500 Git \u2500\u2500" }), _jsxs(Text, { children: [" ", _jsx(Text, { color: "cyan", children: "/commit" }), " Commit changes"] }), _jsxs(Text, { children: [" ", _jsx(Text, { color: "cyan", children: "/push" }), " Push to remote"] }), _jsxs(Text, { children: [" ", _jsx(Text, { color: "cyan", children: "/pr" }), " Create pull request"] }), _jsxs(Text, { children: [" ", _jsx(Text, { color: "cyan", children: "/status" }), " Git status"] }), _jsxs(Text, { children: [" ", _jsx(Text, { color: "cyan", children: "/diff" }), " Git diff"] }), _jsxs(Text, { children: [" ", _jsx(Text, { color: "cyan", children: "/log" }), " Git log"] }), _jsxs(Text, { children: [" ", _jsx(Text, { color: "cyan", children: "/branch" }), " [name] Branches"] }), _jsxs(Text, { children: [" ", _jsx(Text, { color: "cyan", children: "/stash" }), " Stash changes"] }), _jsxs(Text, { children: [" ", _jsx(Text, { color: "cyan", children: "/undo" }), " Undo last commit"] }), _jsx(Text, { dimColor: true, children: " \u2500\u2500 Analysis \u2500\u2500" }), _jsxs(Text, { children: [" ", _jsx(Text, { color: "cyan", children: "/security" }), " Security audit"] }), _jsxs(Text, { children: [" ", _jsx(Text, { color: "cyan", children: "/lint" }), " Quality check"] }), _jsxs(Text, { children: [" ", _jsx(Text, { color: "cyan", children: "/optimize" }), " Performance check"] }), _jsxs(Text, { children: [" ", _jsx(Text, { color: "cyan", children: "/todo" }), " Find TODOs"] }), _jsxs(Text, { children: [" ", _jsx(Text, { color: "cyan", children: "/deps" }), " Dependencies"] }), _jsxs(Text, { children: [" ", _jsx(Text, { color: "cyan", children: "/clean" }), " Dead code removal"] }), _jsxs(Text, { children: [" ", _jsx(Text, { color: "cyan", children: "/context" }), " Session info (model, tokens, mode)"] }), _jsxs(Text, { children: [" ", _jsx(Text, { color: "cyan", children: "/plan" }), " Enter plan mode (read-only tools)"] }), _jsxs(Text, { children: [" ", _jsx(Text, { color: "cyan", children: "/execute" }), " Exit plan mode (enable all tools)"] }), _jsxs(Text, { children: [" ", _jsx(Text, { color: "cyan", children: "/sessions" }), " List saved sessions"] }), _jsxs(Text, { children: [" ", _jsx(Text, { color: "cyan", children: "/resume" }), " id Resume a saved session"] }), _jsxs(Text, { children: [" ", _jsx(Text, { color: "cyan", children: "/clear" }), " Clear conversation history"] }), _jsxs(Text, { children: [" ", _jsx(Text, { color: "cyan", children: "/doctor" }), " Diagnose setup issues"] }), _jsxs(Text, { children: [" ", _jsx(Text, { color: "cyan", children: "/help" }), " This help"] }), _jsxs(Text, { children: [" ", _jsx(Text, { color: "cyan", children: "/exit" }), " Quit"] }), _jsx(Text, { children: " " }), _jsx(Text, { dimColor: true, children: " Shortcuts: sonnet, opus, gpt, gemini, deepseek, flash, free, r1, o4, nano, mini, haiku" })] })), showWallet && (_jsxs(Box, { flexDirection: "column", marginLeft: 2, marginTop: 1, marginBottom: 1, children: [_jsx(Text, { bold: true, children: "Wallet" }), _jsx(Text, { children: " " }), _jsxs(Text, { children: [" Chain: ", _jsx(Text, { color: "magenta", children: chain })] }), _jsxs(Text, { children: [" Address: ", _jsx(Text, { color: "cyan", children: walletAddress })] }), _jsxs(Text, { children: [" Balance: ", _jsx(Text, { color: "green", children: balance })] })] })), _jsx(Static, { items: completedTools, children: (tool) => (_jsx(Box, { marginLeft: 1, children: tool.error
481
506
  ? _jsxs(Text, { color: "red", children: [" \u2717 ", tool.name, " ", _jsxs(Text, { dimColor: true, children: [tool.elapsed, "ms", tool.preview ? ` — ${tool.preview}` : ''] })] })
482
- : _jsxs(Text, { color: "green", children: [" \u2713 ", tool.name, " ", _jsxs(Text, { dimColor: true, children: [tool.elapsed, "ms", tool.preview ? ` — ${tool.preview}` : ''] })] }) }, tool.key)) }), _jsx(Static, { items: committedResponses, children: (r) => (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { wrap: "wrap", children: renderMarkdown(r.text) }), (r.tokens.input > 0 || r.tokens.output > 0) && (_jsx(Box, { marginLeft: 1, children: _jsxs(Text, { dimColor: true, children: [r.tokens.calls > 0 && r.tokens.input === 0
507
+ : _jsxs(Text, { color: "green", children: [" \u2713 ", tool.name, " ", _jsxs(Text, { dimColor: true, children: [tool.elapsed, "ms", tool.preview ? ` — ${tool.preview}` : ''] })] }) }, tool.key)) }), _jsx(Static, { items: committedResponses, children: (r) => (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { wrap: "wrap", children: renderMarkdown(r.text) }), (r.tokens.input > 0 || r.tokens.output > 0) && (_jsx(Box, { marginLeft: 1, children: _jsxs(Text, { dimColor: true, children: [r.tier && _jsxs(Text, { color: "cyan", children: [r.tier, " "] }), r.model ? shortModelName(r.model) : '', r.model ? ' · ' : '', r.tokens.calls > 0 && r.tokens.input === 0
483
508
  ? `${r.tokens.calls} calls`
484
- : `${r.tokens.input.toLocaleString()} in / ${r.tokens.output.toLocaleString()} out${r.tokens.calls > 0 ? ` / ${r.tokens.calls} calls` : ''}`, r.cost > 0 ? ` · $${r.cost.toFixed(4)}` : ''] }) }))] }, r.key)) }), permissionRequest && (_jsxs(Box, { flexDirection: "column", marginTop: 1, marginLeft: 1, children: [_jsx(Text, { color: "yellow", children: " \u256D\u2500 Permission required \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500" }), _jsxs(Text, { color: "yellow", children: [" \u2502 ", _jsx(Text, { bold: true, children: permissionRequest.toolName })] }), permissionRequest.description.split('\n').map((line, i) => (_jsxs(Text, { dimColor: true, children: [" \u2502 ", line] }, i))), _jsx(Text, { color: "yellow", children: " \u2570\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500" }), _jsx(Box, { marginLeft: 3, children: _jsxs(Text, { children: [_jsx(Text, { bold: true, color: "green", children: "[y]" }), _jsx(Text, { dimColor: true, children: " yes " }), _jsx(Text, { bold: true, color: "cyan", children: "[a]" }), _jsx(Text, { dimColor: true, children: " always " }), _jsx(Text, { bold: true, color: "red", children: "[n]" }), _jsx(Text, { dimColor: true, children: " no" })] }) })] })), askUserRequest && (_jsxs(Box, { flexDirection: "column", marginTop: 1, marginLeft: 1, children: [_jsx(Text, { color: "cyan", children: " \u256D\u2500 Question \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500" }), _jsxs(Text, { color: "cyan", children: [" \u2502 ", _jsx(Text, { bold: true, children: askUserRequest.question })] }), askUserRequest.options && askUserRequest.options.length > 0 && (askUserRequest.options.map((opt, i) => (_jsxs(Text, { dimColor: true, children: [" \u2502 ", i + 1, ". ", opt] }, i)))), _jsx(Text, { color: "cyan", children: " \u2570\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500" }), _jsxs(Box, { marginLeft: 3, children: [_jsx(Text, { bold: true, children: "answer> " }), _jsx(TextInput, { value: askUserInput, onChange: setAskUserInput, onSubmit: (val) => {
509
+ : `${formatTokens(r.tokens.input)} in / ${formatTokens(r.tokens.output)} out`, r.cost > 0 ? ` · $${r.cost.toFixed(4)}` : '', r.savings !== undefined && r.savings > 0 ? _jsxs(Text, { color: "green", children: [" saved ", Math.round(r.savings * 100), "%"] }) : ''] }) }))] }, r.key)) }), permissionRequest && (_jsxs(Box, { flexDirection: "column", marginTop: 1, marginLeft: 1, children: [_jsx(Text, { color: "yellow", children: " \u256D\u2500 Permission required \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500" }), _jsxs(Text, { color: "yellow", children: [" \u2502 ", _jsx(Text, { bold: true, children: permissionRequest.toolName })] }), permissionRequest.description.split('\n').map((line, i) => (_jsxs(Text, { dimColor: true, children: [" \u2502 ", line] }, i))), _jsx(Text, { color: "yellow", children: " \u2570\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500" }), _jsx(Box, { marginLeft: 3, children: _jsxs(Text, { children: [_jsx(Text, { bold: true, color: "green", children: "[y]" }), _jsx(Text, { dimColor: true, children: " yes " }), _jsx(Text, { bold: true, color: "cyan", children: "[a]" }), _jsx(Text, { dimColor: true, children: " always " }), _jsx(Text, { bold: true, color: "red", children: "[n]" }), _jsx(Text, { dimColor: true, children: " no" })] }) })] })), askUserRequest && (_jsxs(Box, { flexDirection: "column", marginTop: 1, marginLeft: 1, children: [_jsx(Text, { color: "cyan", children: " \u256D\u2500 Question \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500" }), _jsxs(Text, { color: "cyan", children: [" \u2502 ", _jsx(Text, { bold: true, children: askUserRequest.question })] }), askUserRequest.options && askUserRequest.options.length > 0 && (askUserRequest.options.map((opt, i) => (_jsxs(Text, { dimColor: true, children: [" \u2502 ", i + 1, ". ", opt] }, i)))), _jsx(Text, { color: "cyan", children: " \u2570\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500" }), _jsxs(Box, { marginLeft: 3, children: [_jsx(Text, { bold: true, children: "answer> " }), _jsx(TextInput, { value: askUserInput, onChange: setAskUserInput, onSubmit: (val) => {
485
510
  const answer = val.trim() || '(no response)';
486
511
  const r = askUserRequest.resolve;
487
512
  setAskUserRequest(null);
488
513
  setAskUserInput('');
489
514
  r(answer);
490
- }, focus: true })] })] })), Array.from(tools.entries()).map(([id, tool]) => (_jsxs(Box, { flexDirection: "column", marginLeft: 1, children: [_jsxs(Text, { color: "cyan", children: [' ', _jsx(Spinner, { type: "dots" }), ' ', tool.name, tool.preview ? _jsxs(Text, { dimColor: true, children: [": ", tool.preview] }) : null, _jsx(Text, { dimColor: true, children: (() => { const s = Math.round((Date.now() - tool.startTime) / 1000); return s > 0 ? ` ${s}s` : ''; })() })] }), tool.liveOutput ? (_jsxs(Text, { dimColor: true, children: [" \u2514 ", tool.liveOutput] })) : null] }, id))), thinking && (_jsxs(Box, { flexDirection: "column", marginLeft: 1, children: [_jsxs(Text, { color: "magenta", children: [" ", _jsx(Spinner, { type: "dots" }), " thinking..."] }), thinkingText && (_jsxs(Text, { dimColor: true, wrap: "truncate-end", children: [" ", thinkingText.split('\n').pop()?.slice(0, 80)] }))] })), waiting && !thinking && tools.size === 0 && (_jsx(Box, { marginLeft: 1, children: _jsxs(Text, { color: "yellow", children: [" ", _jsx(Spinner, { type: "dots" }), " ", _jsx(Text, { dimColor: true, children: currentModel })] }) })), streamText && (_jsx(Box, { marginTop: 0, marginBottom: 0, children: _jsx(Text, { wrap: "wrap", children: renderMarkdown(streamText) }) })), responsePreview && !streamText && (_jsx(Box, { flexDirection: "column", marginBottom: 0, children: _jsx(Text, { wrap: "wrap", children: renderMarkdown(responsePreview) }) })), inPicker && (() => {
515
+ }, focus: true })] })] })), Array.from(tools.entries()).map(([id, tool]) => (_jsxs(Box, { flexDirection: "column", marginLeft: 1, children: [_jsxs(Text, { color: "cyan", children: [' ', _jsx(Spinner, { type: "dots" }), ' ', tool.name, tool.preview ? _jsxs(Text, { dimColor: true, children: [": ", tool.preview.slice(0, 60)] }) : null, _jsx(Text, { dimColor: true, children: (() => { const s = Math.round((Date.now() - tool.startTime) / 1000); return s > 0 ? ` ${s}s` : ''; })() })] }), tool.liveOutput ? (_jsxs(Text, { color: "yellow", children: [' ', tool.liveOutput.slice(0, 100)] })) : null] }, id))), thinking && (_jsxs(Box, { flexDirection: "column", marginLeft: 1, children: [_jsxs(Text, { color: "magenta", children: [" ", _jsx(Spinner, { type: "dots" }), " thinking", completedTools.length > 0 ? _jsxs(Text, { dimColor: true, children: [' ', "(step ", completedTools.length + 1, ")"] }) : null] }), thinkingText && (_jsxs(Text, { dimColor: true, wrap: "truncate-end", children: [' ', thinkingText.split('\n').pop()?.slice(0, 100)] }))] })), waiting && !thinking && tools.size === 0 && (_jsx(Box, { marginLeft: 1, children: _jsxs(Text, { color: "yellow", children: [" ", _jsx(Spinner, { type: "dots" }), " ", _jsxs(Text, { dimColor: true, children: [currentModel, completedTools.length > 0 ? ` · step ${completedTools.length + 1}` : ''] })] }) })), streamText && (_jsx(Box, { marginTop: 0, marginBottom: 0, children: _jsx(Text, { wrap: "wrap", children: renderMarkdown(streamText) }) })), responsePreview && !streamText && (_jsx(Box, { flexDirection: "column", marginBottom: 0, children: _jsx(Text, { wrap: "wrap", children: renderMarkdown(responsePreview) }) })), inPicker && (() => {
491
516
  let flatIdx = 0;
492
517
  return (_jsxs(Box, { flexDirection: "column", marginTop: 1, children: [_jsxs(Box, { marginLeft: 2, children: [_jsx(Text, { bold: true, children: "Select a model " }), _jsx(Text, { dimColor: true, children: "(\u2191\u2193 navigate, Enter select, Esc cancel)" })] }), PICKER_CATEGORIES.map((cat) => (_jsxs(Box, { flexDirection: "column", marginTop: 1, children: [_jsx(Box, { marginLeft: 2, children: _jsxs(Text, { dimColor: true, children: ["\u2500\u2500 ", cat.category, " \u2500\u2500"] }) }), cat.models.map((m) => {
493
518
  const myIdx = flatIdx++;
@@ -496,7 +521,7 @@ function RunCodeApp({ initialModel, workDir, walletAddress, walletBalance, chain
496
521
  const isHighlight = m.highlight === true;
497
522
  return (_jsxs(Box, { marginLeft: 2, children: [_jsxs(Text, { inverse: isSelected, color: isSelected ? 'cyan' : isHighlight ? 'yellow' : undefined, bold: isSelected || isHighlight, children: [' ', m.label.padEnd(26), ' '] }), _jsxs(Text, { dimColor: true, children: [" ", m.shortcut.padEnd(14)] }), _jsx(Text, { color: m.price === 'FREE' ? 'green' : isHighlight ? 'yellow' : undefined, dimColor: !isHighlight && m.price !== 'FREE', children: m.price }), isCurrent && _jsx(Text, { color: "green", children: " \u2190" })] }, m.id));
498
523
  })] }, cat.category))), _jsx(Box, { marginTop: 1, marginLeft: 2, children: _jsx(Text, { dimColor: true, children: "Your conversation stays above \u2014 picking a model keeps all history intact." }) })] }));
499
- })(), !inPicker && (_jsx(InputBox, { input: (permissionRequest || askUserRequest) ? '' : input, setInput: (permissionRequest || askUserRequest) ? () => { } : setInput, onSubmit: (permissionRequest || askUserRequest) ? () => { } : handleSubmit, model: currentModel, balance: liveBalance, sessionCost: totalCost, queued: queuedInputs[0] || undefined, queuedCount: queuedInputs.length, focused: !permissionRequest && !askUserRequest, busy: !askUserRequest && (waiting || thinking || tools.size > 0) }))] }));
524
+ })(), !inPicker && (_jsx(InputBox, { input: (permissionRequest || askUserRequest) ? '' : input, setInput: (permissionRequest || askUserRequest) ? () => { } : setInput, onSubmit: (permissionRequest || askUserRequest) ? () => { } : handleSubmit, model: currentModel, balance: liveBalance, sessionCost: totalCost, queued: queuedInputs[0] || undefined, queuedCount: queuedInputs.length, focused: !permissionRequest && !askUserRequest, busy: !askUserRequest && (waiting || thinking || tools.size > 0), contextPct: contextPct }))] }));
500
525
  }
501
526
  export function launchInkUI(opts) {
502
527
  let resolveInput = null;
@@ -1,5 +1,5 @@
1
1
  /**
2
- * Interactive model picker for runcode.
2
+ * Interactive model picker for Franklin.
3
3
  * Shows categorized model list, supports shortcuts and arrow-key selection.
4
4
  */
5
5
  export declare const MODEL_SHORTCUTS: Record<string, string>;
@@ -1,5 +1,5 @@
1
1
  /**
2
- * Interactive model picker for runcode.
2
+ * Interactive model picker for Franklin.
3
3
  * Shows categorized model list, supports shortcuts and arrow-key selection.
4
4
  */
5
5
  import readline from 'node:readline';
@@ -1,5 +1,5 @@
1
1
  /**
2
- * Terminal UI for runcode
2
+ * Terminal UI for Franklin
3
3
  * Raw terminal input/output with markdown rendering and diff display.
4
4
  * No heavy dependencies — just chalk and readline.
5
5
  */
@@ -1,5 +1,5 @@
1
1
  /**
2
- * Terminal UI for runcode
2
+ * Terminal UI for Franklin
3
3
  * Raw terminal input/output with markdown rendering and diff display.
4
4
  * No heavy dependencies — just chalk and readline.
5
5
  */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@blockrun/franklin",
3
- "version": "3.3.3",
3
+ "version": "3.5.1",
4
4
  "description": "Franklin — The AI agent with a wallet. Spends USDC autonomously to get real work done. Pay per action, no subscriptions.",
5
5
  "type": "module",
6
6
  "exports": {
@@ -54,7 +54,7 @@
54
54
  "type": "git",
55
55
  "url": "https://github.com/BlockRunAI/franklin"
56
56
  },
57
- "homepage": "https://github.com/BlockRunAI/franklin",
57
+ "homepage": "https://Franklin.run",
58
58
  "engines": {
59
59
  "node": ">=20"
60
60
  },