@blockrun/franklin 3.8.8 → 3.8.10

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 (50) hide show
  1. package/dist/agent/error-classifier.js +1 -0
  2. package/dist/agent/llm.d.ts +7 -0
  3. package/dist/agent/llm.js +48 -7
  4. package/dist/agent/loop.js +66 -3
  5. package/dist/agent/permissions.js +2 -2
  6. package/dist/agent/types.d.ts +7 -0
  7. package/dist/banner.js +15 -0
  8. package/dist/commands/start.d.ts +4 -0
  9. package/dist/commands/start.js +72 -2
  10. package/dist/index.js +11 -3
  11. package/dist/panel/html.js +111 -21
  12. package/dist/panel/server.js +15 -4
  13. package/dist/tools/activate.d.ts +29 -0
  14. package/dist/tools/activate.js +96 -0
  15. package/dist/tools/index.js +2 -0
  16. package/dist/tools/tool-categories.d.ts +22 -0
  17. package/dist/tools/tool-categories.js +44 -0
  18. package/dist/tools/trading-execute.d.ts +11 -21
  19. package/dist/tools/trading-execute.js +43 -130
  20. package/dist/tools/trading-views.d.ts +64 -0
  21. package/dist/tools/trading-views.js +115 -0
  22. package/dist/tools/trading.js +86 -7
  23. package/dist/tools/webhook.d.ts +18 -0
  24. package/dist/tools/webhook.js +185 -0
  25. package/dist/trading/data.d.ts +24 -1
  26. package/dist/trading/data.js +67 -102
  27. package/dist/trading/providers/blockrun/client.d.ts +48 -0
  28. package/dist/trading/providers/blockrun/client.js +253 -0
  29. package/dist/trading/providers/blockrun/price.d.ts +24 -0
  30. package/dist/trading/providers/blockrun/price.js +110 -0
  31. package/dist/trading/providers/coingecko/client.d.ts +20 -0
  32. package/dist/trading/providers/coingecko/client.js +87 -0
  33. package/dist/trading/providers/coingecko/markets.d.ts +3 -0
  34. package/dist/trading/providers/coingecko/markets.js +25 -0
  35. package/dist/trading/providers/coingecko/ohlcv.d.ts +3 -0
  36. package/dist/trading/providers/coingecko/ohlcv.js +29 -0
  37. package/dist/trading/providers/coingecko/price.d.ts +11 -0
  38. package/dist/trading/providers/coingecko/price.js +41 -0
  39. package/dist/trading/providers/coingecko/trending.d.ts +3 -0
  40. package/dist/trading/providers/coingecko/trending.js +22 -0
  41. package/dist/trading/providers/fetcher.d.ts +43 -0
  42. package/dist/trading/providers/fetcher.js +45 -0
  43. package/dist/trading/providers/registry.d.ts +45 -0
  44. package/dist/trading/providers/registry.js +82 -0
  45. package/dist/trading/providers/standard-models.d.ts +94 -0
  46. package/dist/trading/providers/standard-models.js +21 -0
  47. package/dist/trading/providers/telemetry.d.ts +51 -0
  48. package/dist/trading/providers/telemetry.js +115 -0
  49. package/dist/ui/app.js +28 -2
  50. package/package.json +1 -1
@@ -0,0 +1,115 @@
1
+ /**
2
+ * Provider-layer telemetry.
3
+ *
4
+ * Records every fetcher call so the Panel Markets page can show live health
5
+ * ("• CoinGecko OK", "• BlockRun OK"), today's call count, today's spend,
6
+ * and a p50 latency estimate. Entirely in-memory — dies with the process
7
+ * and re-hydrates from the on-disk wallet/stats files if we ever care to
8
+ * persist (we don't yet).
9
+ *
10
+ * Tiny by design: zero deps, no background timers, no DB. Callers push a
11
+ * single record per fetch; the Panel pulls a snapshot on demand.
12
+ */
13
+ const LATENCY_RING = 64;
14
+ const PAID_ROW_RING = 32;
15
+ function newRoll() {
16
+ return {
17
+ calls: 0,
18
+ ok: 0,
19
+ failures: 0,
20
+ lastOkAt: null,
21
+ lastErrorAt: null,
22
+ spendUsdToday: 0,
23
+ spendResetAt: startOfUtcDay(Date.now()),
24
+ latencies: [],
25
+ };
26
+ }
27
+ function startOfUtcDay(ts) {
28
+ const d = new Date(ts);
29
+ return Date.UTC(d.getUTCFullYear(), d.getUTCMonth(), d.getUTCDate());
30
+ }
31
+ const rolls = {
32
+ coingecko: newRoll(),
33
+ blockrun: newRoll(),
34
+ };
35
+ const paidRecent = [];
36
+ export function recordFetch(evt) {
37
+ const roll = rolls[evt.provider];
38
+ const now = Date.now();
39
+ // Daily rollover for spend.
40
+ const dayNow = startOfUtcDay(now);
41
+ if (dayNow !== roll.spendResetAt) {
42
+ roll.spendUsdToday = 0;
43
+ roll.spendResetAt = dayNow;
44
+ }
45
+ roll.calls++;
46
+ if (evt.ok) {
47
+ roll.ok++;
48
+ roll.lastOkAt = now;
49
+ }
50
+ else {
51
+ roll.failures++;
52
+ roll.lastErrorAt = now;
53
+ }
54
+ if (evt.latencyMs >= 0) {
55
+ roll.latencies.push(evt.latencyMs);
56
+ if (roll.latencies.length > LATENCY_RING)
57
+ roll.latencies.shift();
58
+ }
59
+ if (evt.costUsd && evt.costUsd > 0) {
60
+ roll.spendUsdToday += evt.costUsd;
61
+ paidRecent.push({ endpoint: evt.endpoint, costUsd: evt.costUsd, ts: now });
62
+ if (paidRecent.length > PAID_ROW_RING)
63
+ paidRecent.shift();
64
+ }
65
+ }
66
+ function p50(values) {
67
+ if (values.length === 0)
68
+ return null;
69
+ const sorted = [...values].sort((a, b) => a - b);
70
+ return sorted[Math.floor(sorted.length / 2)];
71
+ }
72
+ function snapshotProvider(name) {
73
+ const r = rolls[name];
74
+ const p = p50(r.latencies);
75
+ const now = Date.now();
76
+ let status = 'cold';
77
+ if (r.calls > 0) {
78
+ const freshError = r.lastErrorAt && now - r.lastErrorAt < 60_000;
79
+ const freshOk = r.lastOkAt && now - r.lastOkAt < 5 * 60_000;
80
+ if (freshError && !freshOk)
81
+ status = 'degraded';
82
+ else
83
+ status = 'ok';
84
+ }
85
+ return {
86
+ name,
87
+ calls: r.calls,
88
+ ok: r.ok,
89
+ failures: r.failures,
90
+ p50LatencyMs: p,
91
+ lastOkAt: r.lastOkAt,
92
+ lastErrorAt: r.lastErrorAt,
93
+ spendUsdToday: r.spendUsdToday,
94
+ status,
95
+ };
96
+ }
97
+ export function snapshot() {
98
+ const providers = [snapshotProvider('coingecko'), snapshotProvider('blockrun')];
99
+ const allLatencies = [...rolls.coingecko.latencies, ...rolls.blockrun.latencies];
100
+ return {
101
+ providers,
102
+ totals: {
103
+ callsToday: providers.reduce((s, p) => s + p.calls, 0),
104
+ spendUsdToday: providers.reduce((s, p) => s + p.spendUsdToday, 0),
105
+ p50LatencyMs: p50(allLatencies),
106
+ },
107
+ recentPaidCalls: [...paidRecent].reverse(),
108
+ };
109
+ }
110
+ /** Test helper: reset all counters. Do not call in production code paths. */
111
+ export function resetTelemetry() {
112
+ rolls.coingecko = newRoll();
113
+ rolls.blockrun = newRoll();
114
+ paidRecent.length = 0;
115
+ }
package/dist/ui/app.js CHANGED
@@ -39,12 +39,16 @@ function useTerminalSize() {
39
39
  }
40
40
  function InputBox({ input, setInput, onSubmit, model, balance, sessionCost, queued, queuedCount, focused, busy, contextPct, vimMode, onVimModeChange }) {
41
41
  const { cols } = useTerminalSize();
42
+ // Avoid drawing right up to the terminal edge. Several terminals auto-wrap
43
+ // a full-width border glyph onto the next row, which leaves "ghost" top
44
+ // borders behind on re-render after errors / status changes.
45
+ const boxWidth = Math.max(20, cols - 2);
42
46
  const placeholder = busy
43
47
  ? (queued
44
48
  ? `⏎ ${queuedCount ?? 1} queued: ${queued.slice(0, 40)}`
45
49
  : 'Working...')
46
50
  : 'Type a message...';
47
- return (_jsxs(Box, { flexDirection: "column", marginTop: 1, children: [_jsxs(Box, { borderStyle: "round", borderDimColor: true, paddingX: 1, width: cols, children: [busy && !input ? _jsxs(Text, { color: "yellow", children: [_jsx(Spinner, { type: "dots" }), " "] }) : null, _jsx(Box, { flexGrow: 1, children: vimMode ? (_jsx(VimInput, { value: input, onChange: setInput, onSubmit: onSubmit, placeholder: placeholder, focus: focused !== false, showMode: true, onModeChange: onVimModeChange })) : (_jsx(TextInput, { value: input, onChange: setInput, onSubmit: onSubmit, placeholder: placeholder, focus: focused !== false })) })] }), _jsx(Box, { marginLeft: 1, children: _jsxs(Text, { dimColor: true, children: [busy ? _jsx(Text, { color: "yellow", children: _jsx(Spinner, { type: "dots" }) }) : null, busy ? ' ' : '', shortModelName(model), " \u00B7 ", balance, sessionCost > 0.00001 ? _jsxs(Text, { color: "yellow", children: [" -$", sessionCost.toFixed(4)] }) : '', contextPct !== undefined && contextPct > 0 ? (() => {
51
+ return (_jsxs(Box, { flexDirection: "column", marginTop: 1, children: [_jsxs(Box, { borderStyle: "round", borderDimColor: true, paddingX: 1, width: boxWidth, children: [busy && !input ? _jsxs(Text, { color: "yellow", children: [_jsx(Spinner, { type: "dots" }), " "] }) : null, _jsx(Box, { flexGrow: 1, children: vimMode ? (_jsx(VimInput, { value: input, onChange: setInput, onSubmit: onSubmit, placeholder: placeholder, focus: focused !== false, showMode: true, onModeChange: onVimModeChange })) : (_jsx(TextInput, { value: input, onChange: setInput, onSubmit: onSubmit, placeholder: placeholder, focus: focused !== false })) })] }), _jsx(Box, { marginLeft: 1, children: _jsxs(Text, { dimColor: true, children: [busy ? _jsx(Text, { color: "yellow", children: _jsx(Spinner, { type: "dots" }) }) : null, busy ? ' ' : '', shortModelName(model), " \u00B7 ", balance, sessionCost > 0.00001 ? _jsxs(Text, { color: "yellow", children: [" -$", sessionCost.toFixed(4)] }) : '', contextPct !== undefined && contextPct > 0 ? (() => {
48
52
  // Visual context bar: ▓▓▓▓▓▓░░░░ 75%
49
53
  const filled = Math.round(contextPct / 10);
50
54
  const empty = 10 - filled;
@@ -52,6 +56,28 @@ function InputBox({ input, setInput, onSubmit, model, balance, sessionCost, queu
52
56
  return (_jsxs(Text, { children: [' ', _jsx(Text, { color: barColor, children: '▓'.repeat(filled) }), _jsx(Text, { dimColor: true, children: '░'.repeat(empty) }), _jsxs(Text, { color: barColor, children: [' ', contextPct, "%"] })] }));
53
57
  })() : null, (queuedCount ?? 0) > 0 ? _jsxs(Text, { color: "cyan", children: [" \u00B7 ", queuedCount, " queued"] }) : null, ' · esc'] }) })] }));
54
58
  }
59
+ function formatAgentErrorForDisplay(error) {
60
+ const lines = error.split('\n').map((line) => line.trim()).filter(Boolean);
61
+ const tipIndex = lines.findIndex((line) => /^tip:/i.test(line));
62
+ const mainLines = tipIndex >= 0 ? lines.slice(0, tipIndex) : lines;
63
+ const tipLines = tipIndex >= 0 ? lines.slice(tipIndex) : [];
64
+ let main = mainLines.join(' ').replace(/\s+/g, ' ').trim();
65
+ let tip = tipLines.join(' ').replace(/\s+/g, ' ').trim();
66
+ const labelMatch = /^\[([^\]]+)\]\s*/.exec(main);
67
+ const label = labelMatch?.[1];
68
+ if (labelMatch)
69
+ main = main.slice(labelMatch[0].length).trim();
70
+ if (tip)
71
+ tip = tip.replace(/^tip:\s*/i, '');
72
+ const out = ['**Request failed**'];
73
+ if (label)
74
+ out.push(`- Type: ${label}`);
75
+ if (main)
76
+ out.push(`- Message: ${main}`);
77
+ if (tip)
78
+ out.push(`- Tip: ${tip}`);
79
+ return out.join('\n');
80
+ }
55
81
  function RunCodeApp({ initialModel, workDir, walletAddress, walletBalance, chain, startWithPicker, onSubmit, onModelChange, onAbort, onExit, }) {
56
82
  const { exit } = useApp();
57
83
  const [input, setInput] = useState('');
@@ -635,7 +661,7 @@ function RunCodeApp({ initialModel, workDir, walletAddress, walletBalance, chain
635
661
  setStreamText('');
636
662
  }
637
663
  if (event.reason === 'error' && event.error) {
638
- commitResponse(`Error: ${event.error}`, turnTokensRef.current, turnCostRef.current);
664
+ commitResponse(formatAgentErrorForDisplay(event.error), turnTokensRef.current, turnCostRef.current);
639
665
  showStatus('Turn failed', 'error', 5000);
640
666
  }
641
667
  else if (event.reason === 'aborted') {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@blockrun/franklin",
3
- "version": "3.8.8",
3
+ "version": "3.8.10",
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": {