@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.
- package/README.md +65 -25
- package/dist/agent/commands.d.ts +1 -1
- package/dist/agent/commands.js +128 -17
- package/dist/agent/compact.d.ts +2 -2
- package/dist/agent/compact.js +148 -22
- package/dist/agent/context.d.ts +8 -3
- package/dist/agent/context.js +301 -108
- package/dist/agent/error-classifier.d.ts +11 -2
- package/dist/agent/error-classifier.js +64 -10
- package/dist/agent/llm.d.ts +8 -1
- package/dist/agent/llm.js +114 -19
- package/dist/agent/loop.d.ts +1 -2
- package/dist/agent/loop.js +509 -61
- package/dist/agent/optimize.d.ts +2 -2
- package/dist/agent/optimize.js +9 -7
- package/dist/agent/permissions.d.ts +1 -1
- package/dist/agent/permissions.js +1 -1
- package/dist/agent/planner.d.ts +42 -0
- package/dist/agent/planner.js +110 -0
- package/dist/agent/reduce.d.ts +7 -1
- package/dist/agent/reduce.js +85 -3
- package/dist/agent/streaming-executor.d.ts +6 -1
- package/dist/agent/streaming-executor.js +83 -5
- package/dist/agent/tokens.d.ts +11 -2
- package/dist/agent/tokens.js +38 -5
- package/dist/agent/tool-guard.d.ts +27 -0
- package/dist/agent/tool-guard.js +324 -0
- package/dist/agent/types.d.ts +7 -1
- package/dist/agent/types.js +1 -1
- package/dist/brain/extract.d.ts +11 -0
- package/dist/brain/extract.js +154 -0
- package/dist/brain/index.d.ts +3 -0
- package/dist/brain/index.js +2 -0
- package/dist/brain/store.d.ts +42 -0
- package/dist/brain/store.js +225 -0
- package/dist/brain/types.d.ts +45 -0
- package/dist/brain/types.js +5 -0
- package/dist/commands/daemon.js +2 -1
- package/dist/commands/start.js +19 -7
- package/dist/config.js +1 -1
- package/dist/index.js +27 -2
- package/dist/learnings/extractor.d.ts +13 -0
- package/dist/learnings/extractor.js +69 -8
- package/dist/learnings/index.d.ts +1 -1
- package/dist/learnings/index.js +1 -1
- package/dist/learnings/store.js +42 -13
- package/dist/learnings/types.d.ts +1 -1
- package/dist/mcp/client.d.ts +1 -1
- package/dist/mcp/client.js +5 -5
- package/dist/mcp/config.d.ts +1 -1
- package/dist/mcp/config.js +1 -1
- package/dist/panel/html.d.ts +2 -0
- package/dist/panel/html.js +409 -146
- package/dist/panel/server.js +19 -0
- package/dist/pricing.js +3 -2
- package/dist/proxy/fallback.d.ts +3 -1
- package/dist/proxy/fallback.js +4 -4
- package/dist/proxy/server.js +29 -11
- package/dist/proxy/sse-translator.js +1 -1
- package/dist/router/categories.d.ts +21 -0
- package/dist/router/categories.js +96 -0
- package/dist/router/index.d.ts +9 -2
- package/dist/router/index.js +106 -27
- package/dist/router/local-elo.d.ts +32 -0
- package/dist/router/local-elo.js +107 -0
- package/dist/router/selector.d.ts +46 -0
- package/dist/router/selector.js +106 -0
- package/dist/session/storage.d.ts +5 -1
- package/dist/session/storage.js +24 -2
- package/dist/social/a11y.d.ts +1 -1
- package/dist/social/a11y.js +5 -1
- package/dist/social/browser.d.ts +5 -0
- package/dist/social/browser.js +22 -0
- package/dist/social/preflight.d.ts +4 -0
- package/dist/social/preflight.js +42 -3
- package/dist/stats/failures.d.ts +20 -0
- package/dist/stats/failures.js +63 -0
- package/dist/stats/format.d.ts +6 -0
- package/dist/stats/format.js +23 -0
- package/dist/stats/insights.js +1 -21
- package/dist/stats/session-tracker.d.ts +21 -0
- package/dist/stats/session-tracker.js +28 -0
- package/dist/stats/tracker.d.ts +1 -1
- package/dist/stats/tracker.js +1 -1
- package/dist/tools/bash.d.ts +14 -1
- package/dist/tools/bash.js +132 -7
- package/dist/tools/edit.js +77 -14
- package/dist/tools/glob.js +13 -3
- package/dist/tools/grep.js +30 -12
- package/dist/tools/imagegen.js +5 -5
- package/dist/tools/index.d.ts +1 -1
- package/dist/tools/index.js +5 -1
- package/dist/tools/read.d.ts +16 -2
- package/dist/tools/read.js +36 -8
- package/dist/tools/searchx.d.ts +6 -2
- package/dist/tools/searchx.js +221 -44
- package/dist/tools/subagent.js +37 -3
- package/dist/tools/task.js +43 -7
- package/dist/tools/validate.d.ts +11 -0
- package/dist/tools/validate.js +42 -0
- package/dist/tools/webfetch.js +18 -7
- package/dist/tools/websearch.js +41 -7
- package/dist/tools/write.js +26 -6
- package/dist/ui/app.js +31 -6
- package/dist/ui/model-picker.d.ts +1 -1
- package/dist/ui/model-picker.js +1 -1
- package/dist/ui/terminal.d.ts +1 -1
- package/dist/ui/terminal.js +1 -1
- package/package.json +2 -2
package/dist/tools/websearch.js
CHANGED
|
@@ -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),
|
|
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': `
|
|
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
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
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:
|
|
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: {
|
package/dist/tools/write.js
CHANGED
|
@@ -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: `${
|
|
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:
|
|
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: '
|
|
109
|
-
content: { type: 'string', description: '
|
|
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
|
|
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
|
|
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, {
|
|
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;
|
package/dist/ui/model-picker.js
CHANGED
package/dist/ui/terminal.d.ts
CHANGED
package/dist/ui/terminal.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@blockrun/franklin",
|
|
3
|
-
"version": "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://
|
|
57
|
+
"homepage": "https://Franklin.run",
|
|
58
58
|
"engines": {
|
|
59
59
|
"node": ">=20"
|
|
60
60
|
},
|