@blockrun/franklin 3.8.7 → 3.8.9

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/bash-guard.js +29 -0
  2. package/dist/agent/loop.js +61 -2
  3. package/dist/agent/permissions.js +2 -2
  4. package/dist/agent/types.d.ts +7 -0
  5. package/dist/commands/doctor.d.ts +15 -0
  6. package/dist/commands/doctor.js +251 -0
  7. package/dist/commands/start.d.ts +4 -0
  8. package/dist/commands/start.js +72 -2
  9. package/dist/index.js +17 -1
  10. package/dist/panel/html.js +111 -21
  11. package/dist/panel/server.js +15 -4
  12. package/dist/tools/activate.d.ts +29 -0
  13. package/dist/tools/activate.js +96 -0
  14. package/dist/tools/index.js +2 -0
  15. package/dist/tools/read.js +20 -1
  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/tools/write.js +20 -0
  26. package/dist/trading/data.d.ts +24 -1
  27. package/dist/trading/data.js +67 -102
  28. package/dist/trading/providers/blockrun/client.d.ts +48 -0
  29. package/dist/trading/providers/blockrun/client.js +253 -0
  30. package/dist/trading/providers/blockrun/price.d.ts +24 -0
  31. package/dist/trading/providers/blockrun/price.js +110 -0
  32. package/dist/trading/providers/coingecko/client.d.ts +20 -0
  33. package/dist/trading/providers/coingecko/client.js +87 -0
  34. package/dist/trading/providers/coingecko/markets.d.ts +3 -0
  35. package/dist/trading/providers/coingecko/markets.js +25 -0
  36. package/dist/trading/providers/coingecko/ohlcv.d.ts +3 -0
  37. package/dist/trading/providers/coingecko/ohlcv.js +29 -0
  38. package/dist/trading/providers/coingecko/price.d.ts +11 -0
  39. package/dist/trading/providers/coingecko/price.js +41 -0
  40. package/dist/trading/providers/coingecko/trending.d.ts +3 -0
  41. package/dist/trading/providers/coingecko/trending.js +22 -0
  42. package/dist/trading/providers/fetcher.d.ts +43 -0
  43. package/dist/trading/providers/fetcher.js +45 -0
  44. package/dist/trading/providers/registry.d.ts +45 -0
  45. package/dist/trading/providers/registry.js +82 -0
  46. package/dist/trading/providers/standard-models.d.ts +94 -0
  47. package/dist/trading/providers/standard-models.js +21 -0
  48. package/dist/trading/providers/telemetry.d.ts +51 -0
  49. package/dist/trading/providers/telemetry.js +115 -0
  50. package/package.json +1 -1
@@ -371,14 +371,14 @@ a:hover { text-decoration:underline; }
371
371
  <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M21 12V7H5a2 2 0 0 1 0-4h14v4"/><path d="M3 5v14a2 2 0 0 0 2 2h16v-5"/><path d="M18 12a2 2 0 0 0 0 4h4v-4z"/></svg>
372
372
  Wallet
373
373
  </button>
374
+ <button class="nav-item" data-tab="markets">
375
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M3 3v18h18"/><path d="M7 14l4-4 4 4 5-5"/></svg>
376
+ Markets
377
+ </button>
374
378
  <button class="nav-item" data-tab="sessions">
375
379
  <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"/></svg>
376
380
  Sessions
377
381
  </button>
378
- <button class="nav-item" data-tab="social">
379
- <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M4 4l11.733 16h4.267l-11.733-16z"/><path d="M4 20l6.768-6.768M15.232 11.232L20 4"/></svg>
380
- Social
381
- </button>
382
382
  <button class="nav-item" data-tab="learnings">
383
383
  <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M2 3h6a4 4 0 0 1 4 4v14a3 3 0 0 0-3-3H2z"/><path d="M22 3h-6a4 4 0 0 0-4 4v14a3 3 0 0 1 3-3h7z"/></svg>
384
384
  Learnings
@@ -542,16 +542,34 @@ a:hover { text-decoration:underline; }
542
542
  <div class="session-detail" id="session-detail" style="display:none"></div>
543
543
  </div>
544
544
 
545
- <!-- Social -->
546
- <div class="tab" id="tab-social">
545
+ <!-- Markets -->
546
+ <div class="tab" id="tab-markets">
547
547
  <div class="content-header">
548
- <h2>Social</h2>
549
- <p>X/Twitter engagement stats</p>
548
+ <h2>Markets</h2>
549
+ <p>How Franklin gets trading data — and what it costs.</p>
550
550
  </div>
551
- <div class="grid grid-4" id="social-stats"></div>
552
- <div class="card" style="margin-top:12px">
553
- <h3>Recent Activity</h3>
554
- <div id="social-feed" class="empty">No social activity yet</div>
551
+
552
+ <div class="grid grid-4">
553
+ <div class="card"><h3>Calls today</h3><div class="metric" id="mk-calls">&mdash;</div></div>
554
+ <div class="card"><h3>Spend today</h3><div class="metric gold" id="mk-spend">&mdash;</div></div>
555
+ <div class="card"><h3>p50 latency</h3><div class="metric" id="mk-p50">&mdash;</div></div>
556
+ <div class="card"><h3>Payment chain</h3><div class="metric" id="mk-chain">&mdash;</div></div>
557
+ </div>
558
+
559
+ <div style="display:grid;grid-template-columns:1.1fr 1fr;gap:14px;margin-top:14px">
560
+ <div class="card">
561
+ <h3>Data pipeline</h3>
562
+ <p style="color:var(--text-dim);font-size:12px;margin:4px 0 14px">
563
+ Each asset class routes through the provider registry to the active upstream.
564
+ </p>
565
+ <div id="mk-pipeline" style="font-family:var(--mono);font-size:12px;line-height:1.75"></div>
566
+ </div>
567
+ <div class="card">
568
+ <h3>Providers</h3>
569
+ <div id="mk-providers" style="margin-top:6px"></div>
570
+ <h3 style="margin-top:18px">Recent paid calls</h3>
571
+ <div id="mk-paid" class="empty" style="margin-top:6px">No paid calls yet</div>
572
+ </div>
555
573
  </div>
556
574
  </div>
557
575
 
@@ -588,6 +606,7 @@ a:hover { text-decoration:underline; }
588
606
  </div>
589
607
  <div id="audit-list" style="font-family:ui-monospace,SFMono-Regular,Consolas,monospace;font-size:12px;"></div>
590
608
  </div>
609
+
591
610
  </div>
592
611
 
593
612
  <script>
@@ -716,14 +735,84 @@ document.getElementById('session-search').addEventListener('input', (e) => {
716
735
  }, 300);
717
736
  });
718
737
 
719
- async function loadSocial() {
720
- const social = await api('social');
721
- if (!social) return;
722
- document.getElementById('social-stats').innerHTML =
723
- '<div class="card"><h3>Posted</h3><div class="metric success">' + (social.posted || 0) + '</div></div>' +
724
- '<div class="card"><h3>Drafted</h3><div class="metric">' + (social.drafted || 0) + '</div></div>' +
725
- '<div class="card"><h3>Skipped</h3><div class="metric">' + (social.skipped || 0) + '</div></div>' +
726
- '<div class="card"><h3>Social Cost</h3><div class="metric gold">' + usd(social.totalCost || 0) + '</div></div>';
738
+ async function loadMarkets() {
739
+ const data = await api('markets');
740
+ if (!data) return;
741
+
742
+ const calls = (data.totals && data.totals.callsToday) || 0;
743
+ const spend = (data.totals && data.totals.spendUsdToday) || 0;
744
+ const p50 = data.totals && data.totals.p50LatencyMs;
745
+ document.getElementById('mk-calls').textContent = String(calls);
746
+ document.getElementById('mk-spend').textContent = usd(spend);
747
+ document.getElementById('mk-p50').textContent = (p50 == null) ? '—' : (p50 + ' ms');
748
+ document.getElementById('mk-chain').textContent = (data.chain || 'base').toUpperCase();
749
+
750
+ // Pipeline: Franklin → registry → per-asset-class provider → endpoint
751
+ const rows = (data.wiring || []).filter(function(r){ return r.kind === 'price'; });
752
+ const singletonRows = (data.wiring || []).filter(function(r){ return r.kind !== 'price'; });
753
+ const providerLabel = function(name) {
754
+ if (name === 'coingecko') return '<span style="color:var(--success)">CoinGecko</span>';
755
+ if (name === 'blockrun') return '<span style="color:var(--gold)">BlockRun Gateway</span>';
756
+ return esc(name);
757
+ };
758
+ const pipeLines = [
759
+ '<div>Franklin agent</div>',
760
+ '<div style="color:var(--text-dim);padding-left:8px">↓</div>',
761
+ '<div>Provider registry</div>',
762
+ '<div style="color:var(--text-dim);padding-left:8px">↓</div>',
763
+ ];
764
+ rows.forEach(function(r, i){
765
+ const last = i === rows.length - 1;
766
+ const branch = last ? '└' : '├';
767
+ const paid = r.paid ? ' <span style="color:var(--gold);font-size:10px">◆ x402</span>' : '';
768
+ pipeLines.push(
769
+ '<div>&nbsp;' + branch + '─ ' + esc(r.assetClass).padEnd(9, ' ') +
770
+ ' → ' + providerLabel(r.provider) + paid + '</div>'
771
+ );
772
+ });
773
+ pipeLines.push('<div style="margin-top:10px;color:var(--text-dim);font-size:11px">Other singleton kinds:</div>');
774
+ singletonRows.forEach(function(r){
775
+ pipeLines.push(
776
+ '<div style="color:var(--text-dim);font-size:11px">&nbsp;&nbsp;' +
777
+ esc(r.kind) + ' → ' + providerLabel(r.provider) + '</div>'
778
+ );
779
+ });
780
+ document.getElementById('mk-pipeline').innerHTML = pipeLines.join('');
781
+
782
+ // Providers health
783
+ const statusChip = function(s){
784
+ if (s === 'ok') return '<span class="dot on"></span> <span style="color:var(--success)">OK</span>';
785
+ if (s === 'degraded') return '<span class="dot off"></span> <span style="color:var(--danger)">degraded</span>';
786
+ return '<span class="dot" style="background:var(--text-dim)"></span> <span style="color:var(--text-dim)">cold</span>';
787
+ };
788
+ const providers = data.providers || [];
789
+ document.getElementById('mk-providers').innerHTML = providers.length === 0 ? '<div class="empty">No calls recorded yet.</div>' : providers.map(function(p){
790
+ const since = p.lastOkAt ? Math.round((Date.now() - p.lastOkAt) / 1000) + 's ago' : '—';
791
+ return '<div style="display:flex;justify-content:space-between;align-items:center;padding:7px 0;border-bottom:1px solid var(--border);font-size:12px">' +
792
+ '<span>' + statusChip(p.status) + ' &nbsp;<strong>' + esc(p.name) + '</strong></span>' +
793
+ '<span style="color:var(--text-dim);font-family:var(--mono);font-size:11px">' +
794
+ p.calls + ' calls · p50 ' + (p.p50LatencyMs == null ? '—' : p.p50LatencyMs + 'ms') + ' · last ' + since +
795
+ '</span>' +
796
+ '</div>';
797
+ }).join('');
798
+
799
+ // Recent paid calls
800
+ const paid = data.recentPaidCalls || [];
801
+ const paidBox = document.getElementById('mk-paid');
802
+ if (paid.length === 0) {
803
+ paidBox.className = 'empty';
804
+ paidBox.textContent = 'No paid calls yet — stocks ship in the next release.';
805
+ } else {
806
+ paidBox.className = '';
807
+ paidBox.innerHTML = paid.map(function(r){
808
+ const age = Math.round((Date.now() - r.ts) / 1000) + 's ago';
809
+ return '<div style="display:flex;justify-content:space-between;padding:4px 0;font-family:var(--mono);font-size:12px">' +
810
+ '<span>' + esc(r.endpoint) + '</span>' +
811
+ '<span class="gold">' + usd(r.costUsd) + '</span>' +
812
+ '<span style="color:var(--text-dim)">' + age + '</span>' +
813
+ '</div>';
814
+ }).join('');
815
+ }
727
816
  }
728
817
 
729
818
  async function loadLearnings() {
@@ -909,9 +998,10 @@ document.querySelector('[data-tab="audit"]')?.addEventListener('click', loadAudi
909
998
 
910
999
  loadOverview();
911
1000
  loadSessions();
912
- loadSocial();
1001
+ loadMarkets();
913
1002
  loadLearnings();
914
1003
  loadWallet();
1004
+ document.querySelector('[data-tab="markets"]')?.addEventListener('click', loadMarkets);
915
1005
  setInterval(() => api('wallet').then(w => {
916
1006
  if (w) {
917
1007
  document.getElementById('balance').textContent = usdBig(w.balance) + ' USDC';
@@ -13,7 +13,8 @@ import { listSessions, loadSessionHistory } from '../session/storage.js';
13
13
  import { searchSessions } from '../session/search.js';
14
14
  import { loadLearnings } from '../learnings/store.js';
15
15
  import { readAudit } from '../stats/audit.js';
16
- import { getStats as getSocialStats } from '../social/db.js';
16
+ import { snapshot as marketsSnapshot } from '../trading/providers/telemetry.js';
17
+ import { describeWiring } from '../trading/providers/registry.js';
17
18
  import { getHTML } from './html.js';
18
19
  const sseClients = new Set();
19
20
  function json(res, data, status = 200) {
@@ -312,9 +313,19 @@ export function createPanelServer(port) {
312
313
  }
313
314
  return;
314
315
  }
315
- if (p === '/api/social') {
316
- const stats = getSocialStats();
317
- json(res, stats);
316
+ if (p === '/api/markets') {
317
+ // Snapshot of every active data provider for the Markets panel:
318
+ // pipeline wiring (which endpoint serves which asset class), live
319
+ // health + latency per provider, and today's paid-call ledger.
320
+ const snap = marketsSnapshot();
321
+ const wiring = describeWiring();
322
+ json(res, {
323
+ chain: loadChain(),
324
+ wiring,
325
+ providers: snap.providers,
326
+ totals: snap.totals,
327
+ recentPaidCalls: snap.recentPaidCalls,
328
+ });
318
329
  return;
319
330
  }
320
331
  if (p === '/api/learnings') {
@@ -0,0 +1,29 @@
1
+ /**
2
+ * ActivateTool — meta-capability that lets the agent pull on-demand tools
3
+ * into the active toolset per session.
4
+ *
5
+ * Pattern borrowed from OpenBB MCP server's per-session tool visibility:
6
+ * a weak model confronted with 25+ tool definitions starts inventing names
7
+ * or emits role-play "[TOOLCALL]" fragments. Register only the core file/
8
+ * shell tools by default and let the model explicitly opt in to the rest.
9
+ *
10
+ * Contract:
11
+ * - `ActivateTool()` with no args → lists every inactive tool with a
12
+ * one-line description so the model knows what's available.
13
+ * - `ActivateTool({ names: ["ExaSearch", "ExaReadUrls"] })` → adds the
14
+ * named tools to the session's active set; subsequent turns include
15
+ * their full schemas. Returns a concise confirmation.
16
+ *
17
+ * The factory captures the shared `activeTools` Set that the loop filters
18
+ * against and the full `allTools` map used for name resolution. Both live
19
+ * in the session — activation is not durable across restarts on purpose,
20
+ * since the model can always re-activate on the next turn if it needs to.
21
+ */
22
+ import type { CapabilityHandler } from '../agent/types.js';
23
+ export interface ActivateToolDeps {
24
+ /** Mutable set of tool names currently visible to the model. */
25
+ activeTools: Set<string>;
26
+ /** Map of every registered capability, keyed by name. */
27
+ allTools: Map<string, CapabilityHandler>;
28
+ }
29
+ export declare function createActivateToolCapability(deps: ActivateToolDeps): CapabilityHandler;
@@ -0,0 +1,96 @@
1
+ /**
2
+ * ActivateTool — meta-capability that lets the agent pull on-demand tools
3
+ * into the active toolset per session.
4
+ *
5
+ * Pattern borrowed from OpenBB MCP server's per-session tool visibility:
6
+ * a weak model confronted with 25+ tool definitions starts inventing names
7
+ * or emits role-play "[TOOLCALL]" fragments. Register only the core file/
8
+ * shell tools by default and let the model explicitly opt in to the rest.
9
+ *
10
+ * Contract:
11
+ * - `ActivateTool()` with no args → lists every inactive tool with a
12
+ * one-line description so the model knows what's available.
13
+ * - `ActivateTool({ names: ["ExaSearch", "ExaReadUrls"] })` → adds the
14
+ * named tools to the session's active set; subsequent turns include
15
+ * their full schemas. Returns a concise confirmation.
16
+ *
17
+ * The factory captures the shared `activeTools` Set that the loop filters
18
+ * against and the full `allTools` map used for name resolution. Both live
19
+ * in the session — activation is not durable across restarts on purpose,
20
+ * since the model can always re-activate on the next turn if it needs to.
21
+ */
22
+ function shortDesc(desc) {
23
+ // First sentence or first 120 chars, whichever is shorter.
24
+ const firstSentence = desc.split(/[.\n]/)[0]?.trim() ?? '';
25
+ if (firstSentence && firstSentence.length <= 120)
26
+ return firstSentence;
27
+ const trimmed = desc.replace(/\s+/g, ' ').trim();
28
+ return trimmed.length <= 120 ? trimmed : trimmed.slice(0, 117) + '...';
29
+ }
30
+ export function createActivateToolCapability(deps) {
31
+ const { activeTools, allTools } = deps;
32
+ return {
33
+ spec: {
34
+ name: 'ActivateTool',
35
+ description: 'Activate additional tools for this session. Most tools are hidden by default to keep your tool inventory small. ' +
36
+ 'Call with no arguments to see what is available. Call with { "names": ["ToolA", "ToolB"] } to enable specific tools — ' +
37
+ 'they become visible in your tool list on the next turn. Activate only what you need; extra tools crowd the inventory.',
38
+ input_schema: {
39
+ type: 'object',
40
+ properties: {
41
+ names: {
42
+ type: 'array',
43
+ items: { type: 'string' },
44
+ description: 'List of tool names to activate. Omit to list what is available.',
45
+ },
46
+ },
47
+ },
48
+ },
49
+ concurrent: false,
50
+ async execute(input) {
51
+ const raw = input.names;
52
+ const names = Array.isArray(raw) ? raw.filter((n) => typeof n === 'string') : undefined;
53
+ // No args → catalog the inactive tools so the model knows what's there.
54
+ if (!names || names.length === 0) {
55
+ const inactive = [...allTools.values()]
56
+ .filter(t => !activeTools.has(t.spec.name))
57
+ .sort((a, b) => a.spec.name.localeCompare(b.spec.name));
58
+ if (inactive.length === 0) {
59
+ return { output: 'All registered tools are already active.' };
60
+ }
61
+ const lines = inactive.map(t => `- ${t.spec.name}: ${shortDesc(t.spec.description)}`);
62
+ return {
63
+ output: `Available on-demand tools (${inactive.length}). Activate with ` +
64
+ `ActivateTool({ "names": ["<name>", ...] }):\n` +
65
+ lines.join('\n'),
66
+ };
67
+ }
68
+ // Activate each named tool.
69
+ const activated = [];
70
+ const alreadyActive = [];
71
+ const unknown = [];
72
+ for (const name of names) {
73
+ if (!allTools.has(name)) {
74
+ unknown.push(name);
75
+ }
76
+ else if (activeTools.has(name)) {
77
+ alreadyActive.push(name);
78
+ }
79
+ else {
80
+ activeTools.add(name);
81
+ activated.push(name);
82
+ }
83
+ }
84
+ const parts = [];
85
+ if (activated.length)
86
+ parts.push(`Activated: ${activated.join(', ')}`);
87
+ if (alreadyActive.length)
88
+ parts.push(`Already active: ${alreadyActive.join(', ')}`);
89
+ if (unknown.length)
90
+ parts.push(`Unknown (not registered): ${unknown.join(', ')}`);
91
+ const output = parts.length ? parts.join('. ') + '.' : 'No change.';
92
+ const isError = activated.length === 0 && unknown.length > 0;
93
+ return { output, isError };
94
+ },
95
+ };
96
+ }
@@ -22,6 +22,7 @@ import { tradingSignalCapability, tradingMarketCapability } from './trading.js';
22
22
  import { searchXCapability } from './searchx.js';
23
23
  import { postToXCapability } from './posttox.js';
24
24
  import { moaCapability } from './moa.js';
25
+ import { webhookPostCapability } from './webhook.js';
25
26
  import { createTradingCapabilities } from './trading-execute.js';
26
27
  import { Portfolio } from '../trading/portfolio.js';
27
28
  import { RiskEngine } from '../trading/risk.js';
@@ -138,6 +139,7 @@ export const allCapabilities = [
138
139
  searchXCapability,
139
140
  postToXCapability,
140
141
  moaCapability,
142
+ webhookPostCapability,
141
143
  ];
142
144
  export { readCapability, writeCapability, editCapability, bashCapability, globCapability, grepCapability, webFetchCapability, webSearchCapability, taskCapability, };
143
145
  export { createSubAgentCapability } from './subagent.js';
@@ -80,13 +80,32 @@ async function execute(input, ctx) {
80
80
  if (stat.size > maxBytes) {
81
81
  return { output: `Error: file is too large (${(stat.size / 1024 / 1024).toFixed(1)}MB). Use offset/limit to read a portion.`, isError: true };
82
82
  }
83
- // Detect binary files
83
+ // Detect binary files — first by extension, then by content
84
+ // (some binaries have no extension: `.env.enc`, `.data`, compiled tools
85
+ // without suffixes, etc. Content sniff catches those.)
84
86
  const ext = path.extname(resolved).toLowerCase();
85
87
  const binaryExts = new Set(['.png', '.jpg', '.jpeg', '.gif', '.webp', '.ico', '.bmp', '.pdf', '.zip', '.tar', '.gz', '.woff', '.woff2', '.ttf', '.eot', '.mp3', '.mp4', '.wav', '.avi', '.mov', '.exe', '.dll', '.so', '.dylib']);
86
88
  if (binaryExts.has(ext)) {
87
89
  const sizeStr = stat.size >= 1024 ? `${(stat.size / 1024).toFixed(1)}KB` : `${stat.size}B`;
88
90
  return { output: `Binary file: ${resolved} (${ext}, ${sizeStr}). Cannot display contents.` };
89
91
  }
92
+ // NUL-byte content sniff — read up to 8KB as a Buffer, scan for 0x00.
93
+ // Text files effectively never contain NUL; binary files almost always
94
+ // do within the first few KB.
95
+ try {
96
+ const SNIFF_BYTES = Math.min(stat.size, 8192);
97
+ if (SNIFF_BYTES > 0) {
98
+ const fd = fs.openSync(resolved, 'r');
99
+ const buf = Buffer.alloc(SNIFF_BYTES);
100
+ fs.readSync(fd, buf, 0, SNIFF_BYTES, 0);
101
+ fs.closeSync(fd);
102
+ if (buf.includes(0)) {
103
+ const sizeStr = stat.size >= 1024 ? `${(stat.size / 1024).toFixed(1)}KB` : `${stat.size}B`;
104
+ return { output: `Binary file: ${resolved} (no text extension but NUL bytes detected, ${sizeStr}). Cannot display contents.` };
105
+ }
106
+ }
107
+ }
108
+ catch { /* best-effort sniff — fall through to text read */ }
90
109
  const raw = fs.readFileSync(resolved, 'utf-8');
91
110
  const allLines = raw.split('\n');
92
111
  const startLine = Math.max(0, (Math.max(1, offset ?? 1)) - 1);
@@ -0,0 +1,22 @@
1
+ /**
2
+ * Tool visibility categories.
3
+ *
4
+ * Franklin ships with ~27 capabilities. Exposing all of them to the model on
5
+ * every turn makes the tool inventory large enough that weak models start
6
+ * hallucinating tool names or emitting role-play "[TOOLCALL]" fragments.
7
+ * The fix: keep a minimal always-on core (file ops, shell, ask) and gate the
8
+ * rest behind an `ActivateTool` meta-tool that the agent pulls on demand —
9
+ * the same per-session visibility pattern that OpenBB's MCP server uses.
10
+ *
11
+ * `CORE_TOOL_NAMES` is the per-session initial active set. Everything else
12
+ * becomes visible only after the agent calls ActivateTool with its name.
13
+ */
14
+ export declare const CORE_TOOL_NAMES: ReadonlySet<string>;
15
+ /** True if this tool is always available without activation. */
16
+ export declare function isCoreTool(name: string): boolean;
17
+ /**
18
+ * Env opt-out: setting `FRANKLIN_DYNAMIC_TOOLS=0` disables the core/on-demand
19
+ * split and exposes every registered tool on every turn (pre-3.8.9 behavior).
20
+ * Kept as a safety valve for users whose workflows depend on the full surface.
21
+ */
22
+ export declare function dynamicToolsEnabled(): boolean;
@@ -0,0 +1,44 @@
1
+ /**
2
+ * Tool visibility categories.
3
+ *
4
+ * Franklin ships with ~27 capabilities. Exposing all of them to the model on
5
+ * every turn makes the tool inventory large enough that weak models start
6
+ * hallucinating tool names or emitting role-play "[TOOLCALL]" fragments.
7
+ * The fix: keep a minimal always-on core (file ops, shell, ask) and gate the
8
+ * rest behind an `ActivateTool` meta-tool that the agent pulls on demand —
9
+ * the same per-session visibility pattern that OpenBB's MCP server uses.
10
+ *
11
+ * `CORE_TOOL_NAMES` is the per-session initial active set. Everything else
12
+ * becomes visible only after the agent calls ActivateTool with its name.
13
+ */
14
+ export const CORE_TOOL_NAMES = new Set([
15
+ // File operations — nothing else works without these.
16
+ 'Read',
17
+ 'Write',
18
+ 'Edit',
19
+ // Shell execution — needed for running tests, builds, scripts.
20
+ 'Bash',
21
+ // Search — code exploration is table stakes.
22
+ 'Grep',
23
+ 'Glob',
24
+ // User dialogue — the agent must be able to ask for clarification.
25
+ 'AskUser',
26
+ // Sub-agent delegation — the sub-agent has its own tool resolution,
27
+ // so keeping this in the core doesn't leak the full inventory.
28
+ 'Task',
29
+ // The meta-tool itself — must always be callable so the agent can
30
+ // discover and activate the rest.
31
+ 'ActivateTool',
32
+ ]);
33
+ /** True if this tool is always available without activation. */
34
+ export function isCoreTool(name) {
35
+ return CORE_TOOL_NAMES.has(name);
36
+ }
37
+ /**
38
+ * Env opt-out: setting `FRANKLIN_DYNAMIC_TOOLS=0` disables the core/on-demand
39
+ * split and exposes every registered tool on every turn (pre-3.8.9 behavior).
40
+ * Kept as a safety valve for users whose workflows depend on the full surface.
41
+ */
42
+ export function dynamicToolsEnabled() {
43
+ return process.env.FRANKLIN_DYNAMIC_TOOLS !== '0';
44
+ }
@@ -1,19 +1,13 @@
1
1
  /**
2
- * Trading execution capabilities. Exposes Franklin's Portfolio + RiskEngine
3
- * + Exchange stack to the agent as three tools: TradingPortfolio (read),
4
- * TradingOpenPosition (buy side), TradingClosePosition (sell side).
2
+ * Trading execution capabilities the three-to-four tools that let the
3
+ * agent inspect its portfolio, open/close paper positions, and (when a
4
+ * persistent trade log is attached) query cross-session history.
5
5
  *
6
- * This is the surface that differentiates Franklin from generic coding
7
- * agents stateless tools can't hold a wallet, track positions across
8
- * sessions, or reason about P&L. Every output here is deliberately
9
- * information-rich so the agent has the numbers it needs to make the next
10
- * economic decision (cash left, risk utilization, unrealized vs realized
11
- * P&L, fill detail) without a follow-up tool call.
12
- *
13
- * Factory-style construction (createTradingCapabilities) keeps testing
14
- * clean: production code calls it with a default disk-backed engine;
15
- * tests inject a MockExchange-backed engine and assert behavior without
16
- * touching disk.
6
+ * This file is now the "router" layer only: it binds the engine to tool
7
+ * handlers and delegates rendering to `trading-views.ts`. The portfolio
8
+ * math, risk math, and exchange simulation all live in `../trading/*`.
9
+ * The split mirrors OpenBB's router/engine/view layering and keeps every
10
+ * layer testable in isolation.
17
11
  */
18
12
  import type { CapabilityHandler } from '../agent/types.js';
19
13
  import type { TradingEngine } from '../trading/engine.js';
@@ -21,15 +15,11 @@ import type { RiskConfig } from '../trading/risk.js';
21
15
  import type { TradeLog } from '../trading/trade-log.js';
22
16
  export interface TradingCapabilitiesDeps {
23
17
  engine: TradingEngine;
24
- /** Risk config used to report "you're using X% of your position cap". */
18
+ /** Risk config used for "you're at X% of your exposure cap" readout. */
25
19
  riskConfig?: RiskConfig;
26
- /** Optional hook run after every state-changing call (e.g., persist to disk). */
20
+ /** Hook run after state-changing calls typically persists to disk. */
27
21
  onStateChange?: () => void | Promise<void>;
28
- /**
29
- * Optional persistent trade log. When provided, opens and closes are
30
- * appended to it and the TradingHistory capability is registered so the
31
- * agent can query cross-session P&L.
32
- */
22
+ /** Persistent trade log; when provided, TradingHistory is registered. */
33
23
  tradeLog?: TradeLog;
34
24
  }
35
25
  export declare function createTradingCapabilities(deps: TradingCapabilitiesDeps): CapabilityHandler[];