@blockrun/franklin 3.8.8 → 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 (45) hide show
  1. package/dist/agent/loop.js +61 -2
  2. package/dist/agent/permissions.js +2 -2
  3. package/dist/agent/types.d.ts +7 -0
  4. package/dist/commands/start.d.ts +4 -0
  5. package/dist/commands/start.js +72 -2
  6. package/dist/index.js +9 -1
  7. package/dist/panel/html.js +111 -21
  8. package/dist/panel/server.js +15 -4
  9. package/dist/tools/activate.d.ts +29 -0
  10. package/dist/tools/activate.js +96 -0
  11. package/dist/tools/index.js +2 -0
  12. package/dist/tools/tool-categories.d.ts +22 -0
  13. package/dist/tools/tool-categories.js +44 -0
  14. package/dist/tools/trading-execute.d.ts +11 -21
  15. package/dist/tools/trading-execute.js +43 -130
  16. package/dist/tools/trading-views.d.ts +64 -0
  17. package/dist/tools/trading-views.js +115 -0
  18. package/dist/tools/trading.js +86 -7
  19. package/dist/tools/webhook.d.ts +18 -0
  20. package/dist/tools/webhook.js +185 -0
  21. package/dist/trading/data.d.ts +24 -1
  22. package/dist/trading/data.js +67 -102
  23. package/dist/trading/providers/blockrun/client.d.ts +48 -0
  24. package/dist/trading/providers/blockrun/client.js +253 -0
  25. package/dist/trading/providers/blockrun/price.d.ts +24 -0
  26. package/dist/trading/providers/blockrun/price.js +110 -0
  27. package/dist/trading/providers/coingecko/client.d.ts +20 -0
  28. package/dist/trading/providers/coingecko/client.js +87 -0
  29. package/dist/trading/providers/coingecko/markets.d.ts +3 -0
  30. package/dist/trading/providers/coingecko/markets.js +25 -0
  31. package/dist/trading/providers/coingecko/ohlcv.d.ts +3 -0
  32. package/dist/trading/providers/coingecko/ohlcv.js +29 -0
  33. package/dist/trading/providers/coingecko/price.d.ts +11 -0
  34. package/dist/trading/providers/coingecko/price.js +41 -0
  35. package/dist/trading/providers/coingecko/trending.d.ts +3 -0
  36. package/dist/trading/providers/coingecko/trending.js +22 -0
  37. package/dist/trading/providers/fetcher.d.ts +43 -0
  38. package/dist/trading/providers/fetcher.js +45 -0
  39. package/dist/trading/providers/registry.d.ts +45 -0
  40. package/dist/trading/providers/registry.js +82 -0
  41. package/dist/trading/providers/standard-models.d.ts +94 -0
  42. package/dist/trading/providers/standard-models.js +21 -0
  43. package/dist/trading/providers/telemetry.d.ts +51 -0
  44. package/dist/trading/providers/telemetry.js +115 -0
  45. package/package.json +1 -1
@@ -13,6 +13,8 @@ import { optimizeHistory, CAPPED_MAX_TOKENS, ESCALATED_MAX_TOKENS, getMaxOutputT
13
13
  import { classifyAgentError } from './error-classifier.js';
14
14
  import { SessionToolGuard } from './tool-guard.js';
15
15
  import { resetToolSessionState } from '../tools/index.js';
16
+ import { CORE_TOOL_NAMES, dynamicToolsEnabled } from '../tools/tool-categories.js';
17
+ import { createActivateToolCapability } from '../tools/activate.js';
16
18
  import { recordUsage } from '../stats/tracker.js';
17
19
  import { recordSessionUsage } from '../stats/session-tracker.js';
18
20
  import { appendAudit, extractLastUserPrompt } from '../stats/audit.js';
@@ -317,11 +319,33 @@ export async function interactiveSession(config, getUserInput, onEvent, onAbortR
317
319
  chain: config.chain,
318
320
  debug: config.debug,
319
321
  });
322
+ // ── Dynamic tool visibility ──
323
+ // Register ActivateTool before building the capability map so the agent
324
+ // can always reach the meta-tool. When FRANKLIN_DYNAMIC_TOOLS=0 is set,
325
+ // `activeTools` is seeded with every registered name — behaves as the
326
+ // pre-3.8.9 static registry.
320
327
  const capabilityMap = new Map();
321
328
  for (const cap of config.capabilities) {
322
329
  capabilityMap.set(cap.spec.name, cap);
323
330
  }
324
- const toolDefs = config.capabilities.map((c) => c.spec);
331
+ const activeTools = new Set();
332
+ const dynamicTools = dynamicToolsEnabled();
333
+ if (dynamicTools) {
334
+ for (const name of CORE_TOOL_NAMES) {
335
+ if (capabilityMap.has(name))
336
+ activeTools.add(name);
337
+ }
338
+ }
339
+ else {
340
+ for (const cap of config.capabilities)
341
+ activeTools.add(cap.spec.name);
342
+ }
343
+ const activateToolCap = createActivateToolCapability({ activeTools, allTools: capabilityMap });
344
+ capabilityMap.set(activateToolCap.spec.name, activateToolCap);
345
+ if (dynamicTools)
346
+ activeTools.add(activateToolCap.spec.name);
347
+ const allToolDefs = [...capabilityMap.values()].map(c => c.spec);
348
+ const buildCallToolDefs = () => dynamicTools ? allToolDefs.filter(t => activeTools.has(t.name)) : allToolDefs;
325
349
  const maxTurns = config.maxTurns ?? 15;
326
350
  const workDir = config.workingDir ?? process.cwd();
327
351
  const permissions = new PermissionManager(config.permissionMode ?? 'default', config.permissionPromptFn);
@@ -576,6 +600,20 @@ export async function interactiveSession(config, getUserInput, onEvent, onAbortR
576
600
  '4. Think step by step — show your reasoning explicitly when it adds value\n' +
577
601
  'Prioritize correctness and thoroughness over speed.');
578
602
  }
603
+ // ── Dynamic tool visibility hint ──
604
+ // When the core/on-demand split is active, tell every model up front
605
+ // that its tool list is intentionally small and that extras can be
606
+ // pulled via ActivateTool. Kept byte-stable across turns (no tool
607
+ // names inlined) so the prompt cache still holds.
608
+ if (dynamicTools && allToolDefs.length > activeTools.size) {
609
+ systemParts.push('# Tool Inventory\n' +
610
+ 'Your current tool list is intentionally minimal. Additional tools ' +
611
+ '(web search, image/video/music generation, trading, content, brain ' +
612
+ 'recall, etc.) are available on demand. Call `ActivateTool()` with ' +
613
+ 'no arguments to see what is available, then call `ActivateTool({ ' +
614
+ '"names": ["<name>"] })` to enable the ones you need. Activated ' +
615
+ 'tools become visible on the next turn.');
616
+ }
579
617
  // ── Context awareness injection ──
580
618
  // Tell the model how full its context window is so it can self-regulate.
581
619
  // At high usage, nudge it to be concise and avoid unnecessary tool calls.
@@ -669,7 +707,10 @@ export async function interactiveSession(config, getUserInput, onEvent, onAbortR
669
707
  }
670
708
  // Build per-call tool defs, max_tokens, and system prompt
671
709
  // (planning calls get no tools + short output + planning prompt)
672
- let callToolDefs = toolDefs;
710
+ // Dynamic visibility: `buildCallToolDefs()` returns only the active set
711
+ // (core + any the agent pulled via ActivateTool). Re-evaluated every
712
+ // turn so newly activated tools take effect immediately.
713
+ let callToolDefs = buildCallToolDefs();
673
714
  let callMaxTokens = maxTokens;
674
715
  let callSystemPrompt = systemPrompt;
675
716
  if (planActive && loopCount === 1) {
@@ -918,6 +959,24 @@ export async function interactiveSession(config, getUserInput, onEvent, onAbortR
918
959
  const opusCost = (inputTokens / 1_000_000) * OPUS_PRICING.input
919
960
  + (usage.outputTokens / 1_000_000) * OPUS_PRICING.output;
920
961
  sessionSavedVsOpus += Math.max(0, opusCost - costEstimate);
962
+ // ── Max-spend guard ──
963
+ // Session-level cost ceiling. Cron/daily drivers pass this to bound a
964
+ // single run ("spend at most $0.50 for today's digest"); interactive
965
+ // users can pass it to feel safe walking away. Hits as soon as accumulated
966
+ // cost crosses the cap — the last call that tipped us over still runs,
967
+ // but no further API calls are made.
968
+ const maxSpend = config.maxSpendUsd;
969
+ if (typeof maxSpend === 'number' && Number.isFinite(maxSpend) && maxSpend > 0 &&
970
+ sessionCostUsd >= maxSpend) {
971
+ onEvent({
972
+ kind: 'text_delta',
973
+ text: `\n\n_Max-spend reached: $${sessionCostUsd.toFixed(4)} ≥ cap $${maxSpend.toFixed(2)}. ` +
974
+ `Stopping session — further calls would exceed the budget._\n`,
975
+ });
976
+ persistSessionMeta();
977
+ onEvent({ kind: 'turn_done', reason: 'budget' });
978
+ return history;
979
+ }
921
980
  // ── Max output tokens recovery ──
922
981
  if (stopReason === 'max_tokens' && recoveryAttempts < MAX_RECOVERY_ATTEMPTS) {
923
982
  recoveryAttempts++;
@@ -33,10 +33,10 @@ function isCommonDevCommand(cmd) {
33
33
  return COMMON_DEV_PATTERNS.some(p => p.test(trimmed));
34
34
  }
35
35
  // ─── Default Rules ─────────────────────────────────────────────────────────
36
- const READ_ONLY_TOOLS = new Set(['Read', 'Glob', 'Grep', 'WebSearch', 'Task', 'AskUser', 'ImageGen', 'TradingSignal', 'TradingMarket', 'SearchX']);
36
+ const READ_ONLY_TOOLS = new Set(['Read', 'Glob', 'Grep', 'WebSearch', 'Task', 'AskUser', 'ActivateTool', 'ImageGen', 'TradingSignal', 'TradingMarket', 'SearchX']);
37
37
  const DESTRUCTIVE_TOOLS = new Set(['Write', 'Edit', 'Bash']);
38
38
  const DEFAULT_RULES = {
39
- allow: ['Read', 'Glob', 'Grep', 'WebSearch', 'WebFetch', 'Task', 'AskUser', 'ImageGen', 'TradingSignal', 'TradingMarket', 'SearchX'],
39
+ allow: ['Read', 'Glob', 'Grep', 'WebSearch', 'WebFetch', 'Task', 'AskUser', 'ActivateTool', 'ImageGen', 'TradingSignal', 'TradingMarket', 'SearchX'],
40
40
  deny: [],
41
41
  ask: ['Write', 'Edit', 'Bash', 'Agent', 'PostToX'],
42
42
  };
@@ -155,4 +155,11 @@ export interface AgentConfig {
155
155
  * unset. Format: "<driver>:<owner-or-chat-id>", e.g. "telegram:12345".
156
156
  */
157
157
  sessionChannel?: string;
158
+ /**
159
+ * Hard cap on total USD spend for this session. When accumulated API cost
160
+ * crosses the cap, the loop stops with `reason: 'budget'`. Zero/negative
161
+ * values disable the cap. Primary use case: cron/daily drivers that must
162
+ * bound a single run to keep autonomous execution inside a known envelope.
163
+ */
164
+ maxSpendUsd?: number;
158
165
  }
@@ -7,6 +7,10 @@ interface StartOptions {
7
7
  resume?: string | boolean | 'picker';
8
8
  /** Continue: resume most recent session matching the current working directory */
9
9
  continue?: boolean;
10
+ /** Hard USD cap on total session spend. Stops the loop when exceeded. */
11
+ maxSpend?: string | number;
12
+ /** Run a single prompt non-interactively, then exit. For cron/scripted use. */
13
+ prompt?: string;
10
14
  }
11
15
  export declare function startCommand(options: StartOptions): Promise<void>;
12
16
  export {};
@@ -74,6 +74,49 @@ export async function startCommand(options) {
74
74
  // Default: free NVIDIA model — zero wallet charges until user explicitly switches
75
75
  model = 'nvidia/nemotron-ultra-253b';
76
76
  }
77
+ const workDir = process.cwd();
78
+ // --prompt batch mode: skip all interactive startup UI/side effects so
79
+ // stdout stays clean for cron/scripts. Keep the capability surface to the
80
+ // built-ins only — no panel, no MCP autoconnect, no wallet/banner chatter.
81
+ if (options.prompt) {
82
+ if (options.resume === true || options.resume === 'picker') {
83
+ console.error(chalk.red('`--prompt` requires `--resume` to include an explicit session id.'));
84
+ process.exitCode = 1;
85
+ return;
86
+ }
87
+ const systemInstructions = assembleInstructions(workDir, model);
88
+ const subAgent = createSubAgentCapability(apiUrl, chain, allCapabilities, model);
89
+ const { registerMoAConfig } = await import('../tools/moa.js');
90
+ registerMoAConfig(apiUrl, chain, model);
91
+ const capabilities = [...allCapabilities, subAgent];
92
+ if (options.debug) {
93
+ const issues = validateToolDescriptions(capabilities);
94
+ for (const issue of issues) {
95
+ console.error(`[validate] ${issue.severity}: ${issue.toolName} — ${issue.issue}`);
96
+ }
97
+ }
98
+ const agentConfig = {
99
+ model,
100
+ apiUrl,
101
+ chain,
102
+ systemInstructions,
103
+ capabilities,
104
+ maxTurns: 100,
105
+ workingDir: workDir,
106
+ permissionMode: 'trust',
107
+ debug: options.debug,
108
+ resumeSessionId: (typeof options.resume === 'string' && options.resume !== 'picker')
109
+ ? options.resume
110
+ : continueResolvedId,
111
+ ...(options.maxSpend != null
112
+ ? { maxSpendUsd: Number(options.maxSpend) }
113
+ : {}),
114
+ };
115
+ const exitCode = await runOneShot(agentConfig, options.prompt);
116
+ flushStats();
117
+ process.exitCode = exitCode;
118
+ return;
119
+ }
77
120
  // Warn when a paid model is active so users know they'll be charged
78
121
  const FREE_MODELS = new Set([
79
122
  'nvidia/nemotron-ultra-253b',
@@ -114,7 +157,6 @@ export async function startCommand(options) {
114
157
  catch { /* migration is optional */ }
115
158
  }
116
159
  printBanner(version);
117
- const workDir = process.cwd();
118
160
  // Auto-start panel in background unless explicitly disabled.
119
161
  // Binds loopback-only (wallet secrets on /api/wallet/secret — never expose on LAN).
120
162
  let panelUrl;
@@ -240,9 +282,13 @@ export async function startCommand(options) {
240
282
  workingDir: workDir,
241
283
  // Non-TTY (piped) input = scripted mode → trust all tools automatically.
242
284
  // Interactive TTY = default mode (prompts for Bash/Write/Edit).
243
- permissionMode: (options.trust || !process.stdin.isTTY) ? 'trust' : 'default',
285
+ // --prompt is also scripted; the cron driver never sees a TTY.
286
+ permissionMode: (options.trust || options.prompt || !process.stdin.isTTY) ? 'trust' : 'default',
244
287
  debug: options.debug,
245
288
  resumeSessionId,
289
+ ...(options.maxSpend != null
290
+ ? { maxSpendUsd: Number(options.maxSpend) }
291
+ : {}),
246
292
  };
247
293
  // Bootstrap learnings from existing CLAUDE.md on first run (async, non-blocking)
248
294
  Promise.all([
@@ -262,6 +308,30 @@ export async function startCommand(options) {
262
308
  await runWithBasicUI(agentConfig, model, workDir);
263
309
  }
264
310
  }
311
+ // ─── One-shot mode (franklin --prompt "...") ──────────────────────────────
312
+ // Used by cron drivers. Non-interactive, prints text deltas to stdout as
313
+ // they stream, honors --max-spend, exits 0 on completion / 1 on error.
314
+ async function runOneShot(agentConfig, prompt) {
315
+ let delivered = false;
316
+ let exitCode = 0;
317
+ const getInput = async () => {
318
+ if (delivered)
319
+ return null;
320
+ delivered = true;
321
+ return prompt;
322
+ };
323
+ await interactiveSession(agentConfig, getInput, (event) => {
324
+ if (event.kind === 'text_delta') {
325
+ process.stdout.write(event.text);
326
+ }
327
+ else if (event.kind === 'turn_done') {
328
+ if (event.reason === 'error')
329
+ exitCode = 1;
330
+ process.stdout.write('\n');
331
+ }
332
+ });
333
+ return exitCode;
334
+ }
265
335
  // ─── Ink UI (interactive terminal) ─────────────────────────────────────────
266
336
  async function runWithInkUI(agentConfig, model, workDir, version, walletInfo, onBalanceReady, fetchBalance) {
267
337
  const startSnapshot = snapshotStats();
package/dist/index.js CHANGED
@@ -43,6 +43,8 @@ program
43
43
  .option('--trust', 'Trust mode — skip permission prompts for all tools')
44
44
  .option('-r, --resume [sessionId]', 'Resume a session by ID (or show picker if omitted)')
45
45
  .option('-c, --continue', 'Continue the most recent session in this directory')
46
+ .option('--max-spend <usd>', 'Hard USD cap on total session API spend — session stops when exceeded')
47
+ .option('-p, --prompt <text>', 'Run a single prompt non-interactively (for cron/scripted use)')
46
48
  .action((options) => startCommand({ ...options, version }));
47
49
  program
48
50
  .command('resume [sessionId]')
@@ -209,7 +211,7 @@ const args = process.argv.slice(2);
209
211
  const firstArg = args[0];
210
212
  const HELP_FLAGS = new Set(['-h', '--help']);
211
213
  const VERSION_FLAGS = new Set(['-V', '--version']);
212
- const START_ONLY_FLAGS = new Set(['--trust', '--debug', '-m', '--model', '-r', '--resume', '-c', '--continue']);
214
+ const START_ONLY_FLAGS = new Set(['--trust', '--debug', '-m', '--model', '-r', '--resume', '-c', '--continue', '-p', '--prompt', '--max-spend']);
213
215
  function hasAnyFlag(argv, flags) {
214
216
  return argv.some(arg => flags.has(arg));
215
217
  }
@@ -227,6 +229,12 @@ function parseStartFlags(argv, startIdx = 0) {
227
229
  else if ((arg === '-m' || arg === '--model') && argv[i + 1]) {
228
230
  opts.model = argv[++i];
229
231
  }
232
+ else if ((arg === '-p' || arg === '--prompt') && argv[i + 1]) {
233
+ opts.prompt = argv[++i];
234
+ }
235
+ else if (arg === '--max-spend' && argv[i + 1]) {
236
+ opts.maxSpend = argv[++i];
237
+ }
230
238
  else if (arg === '-c' || arg === '--continue') {
231
239
  opts.continue = true;
232
240
  }
@@ -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';