@blockrun/franklin 3.2.4 → 3.3.0

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 (41) hide show
  1. package/dist/agent/commands.js +30 -1
  2. package/dist/agent/context.js +13 -0
  3. package/dist/agent/permissions.js +3 -3
  4. package/dist/banner.js +27 -40
  5. package/dist/commands/start.js +33 -2
  6. package/dist/events/bridge.d.ts +1 -0
  7. package/dist/events/bridge.js +24 -0
  8. package/dist/events/bus.d.ts +17 -0
  9. package/dist/events/bus.js +55 -0
  10. package/dist/events/types.d.ts +49 -0
  11. package/dist/events/types.js +8 -0
  12. package/dist/learnings/extractor.d.ts +16 -0
  13. package/dist/learnings/extractor.js +234 -0
  14. package/dist/learnings/index.d.ts +3 -0
  15. package/dist/learnings/index.js +2 -0
  16. package/dist/learnings/store.d.ts +15 -0
  17. package/dist/learnings/store.js +130 -0
  18. package/dist/learnings/types.d.ts +24 -0
  19. package/dist/learnings/types.js +7 -0
  20. package/dist/narrative/state.d.ts +30 -0
  21. package/dist/narrative/state.js +69 -0
  22. package/dist/social/browser-pool.d.ts +29 -0
  23. package/dist/social/browser-pool.js +57 -0
  24. package/dist/social/preflight.d.ts +14 -0
  25. package/dist/social/preflight.js +26 -0
  26. package/dist/social/x.d.ts +8 -0
  27. package/dist/social/x.js +9 -1
  28. package/dist/tools/index.js +7 -0
  29. package/dist/tools/posttox.d.ts +7 -0
  30. package/dist/tools/posttox.js +137 -0
  31. package/dist/tools/searchx.d.ts +7 -0
  32. package/dist/tools/searchx.js +111 -0
  33. package/dist/tools/trading.d.ts +3 -0
  34. package/dist/tools/trading.js +168 -0
  35. package/dist/trading/config.d.ts +23 -0
  36. package/dist/trading/config.js +45 -0
  37. package/dist/trading/data.d.ts +30 -0
  38. package/dist/trading/data.js +112 -0
  39. package/dist/trading/metrics.d.ts +29 -0
  40. package/dist/trading/metrics.js +105 -0
  41. package/package.json +1 -1
@@ -198,7 +198,7 @@ const DIRECT_COMMANDS = {
198
198
  ` **Analysis:** /security /lint /optimize /todo /deps /clean /migrate /doc\n` +
199
199
  ` **Session:** /plan /ultraplan /execute /compact /retry /sessions /resume /context /tasks\n` +
200
200
  ` **Power:** /ultrathink [query] /ultraplan /dump\n` +
201
- ` **Info:** /model /wallet /cost /tokens /mcp /doctor /version /bug /help\n` +
201
+ ` **Info:** /model /wallet /cost /tokens /learnings /mcp /doctor /version /bug /help\n` +
202
202
  ` **UI:** /clear /exit\n` +
203
203
  (ultrathinkOn ? `\n Ultrathink: ON\n` : '')
204
204
  });
@@ -513,6 +513,34 @@ export async function handleSlashCommand(input, ctx) {
513
513
  emitDone(ctx);
514
514
  return { handled: true };
515
515
  }
516
+ // /learnings — view or clear per-user learnings
517
+ if (input === '/learnings' || input.startsWith('/learnings ')) {
518
+ const { loadLearnings, decayLearnings, saveLearnings } = await import('../learnings/store.js');
519
+ const arg = input.slice('/learnings'.length).trim();
520
+ if (arg === 'clear') {
521
+ saveLearnings([]);
522
+ ctx.onEvent({ kind: 'text_delta', text: 'All learnings cleared.\n' });
523
+ }
524
+ else {
525
+ let learnings = loadLearnings();
526
+ if (learnings.length === 0) {
527
+ ctx.onEvent({ kind: 'text_delta', text: 'No learnings yet. Franklin learns your preferences over time.\n' });
528
+ }
529
+ else {
530
+ learnings = decayLearnings(learnings);
531
+ const sorted = [...learnings].sort((a, b) => (b.confidence * b.times_confirmed) - (a.confidence * a.times_confirmed));
532
+ let text = `**Personal Learnings** (${sorted.length})\n\n`;
533
+ for (const l of sorted) {
534
+ const conf = l.confidence >= 0.8 ? 'high' : l.confidence >= 0.5 ? 'mid' : 'low';
535
+ text += ` [${conf}] ${l.learning} (×${l.times_confirmed})\n`;
536
+ }
537
+ text += '\nUse `/learnings clear` to reset.\n';
538
+ ctx.onEvent({ kind: 'text_delta', text });
539
+ }
540
+ }
541
+ emitDone(ctx);
542
+ return { handled: true };
543
+ }
516
544
  // /model — show current model or switch with /model <name>
517
545
  if (input === '/model' || input.startsWith('/model ')) {
518
546
  if (input === '/model') {
@@ -523,6 +551,7 @@ export async function handleSlashCommand(input, ctx) {
523
551
  else {
524
552
  const newModel = resolveModel(input.slice(7).trim());
525
553
  ctx.config.model = newModel;
554
+ ctx.config.onModelChange?.(newModel);
526
555
  ctx.onEvent({ kind: 'text_delta', text: `Model → **${newModel}**\n` });
527
556
  }
528
557
  emitDone(ctx);
@@ -5,6 +5,7 @@
5
5
  import fs from 'node:fs';
6
6
  import path from 'node:path';
7
7
  import { execSync } from 'node:child_process';
8
+ import { loadLearnings, decayLearnings, saveLearnings, formatForPrompt } from '../learnings/store.js';
8
9
  // ─── System Instructions Assembly ──────────────────────────────────────────
9
10
  const BASE_INSTRUCTIONS = `You are runcode, an AI coding agent that helps users with software engineering tasks.
10
11
  You have access to tools for reading, writing, editing files, running shell commands, searching codebases, web browsing, and more.
@@ -57,6 +58,18 @@ export function assembleInstructions(workingDir) {
57
58
  if (gitInfo) {
58
59
  parts.push(`# Git Context\n\n${gitInfo}`);
59
60
  }
61
+ // Inject per-user learnings from self-evolution system
62
+ try {
63
+ let learnings = loadLearnings();
64
+ if (learnings.length > 0) {
65
+ learnings = decayLearnings(learnings);
66
+ saveLearnings(learnings);
67
+ const personalContext = formatForPrompt(learnings);
68
+ if (personalContext)
69
+ parts.push(personalContext);
70
+ }
71
+ }
72
+ catch { /* learnings are optional — never block startup */ }
60
73
  _instructionCache.set(workingDir, parts);
61
74
  return parts;
62
75
  }
@@ -8,12 +8,12 @@ import readline from 'node:readline';
8
8
  import chalk from 'chalk';
9
9
  import { BLOCKRUN_DIR } from '../config.js';
10
10
  // ─── Default Rules ─────────────────────────────────────────────────────────
11
- const READ_ONLY_TOOLS = new Set(['Read', 'Glob', 'Grep', 'WebSearch', 'Task', 'AskUser', 'ImageGen']);
11
+ const READ_ONLY_TOOLS = new Set(['Read', 'Glob', 'Grep', 'WebSearch', 'Task', 'AskUser', 'ImageGen', 'TradingSignal', 'TradingMarket', 'SearchX']);
12
12
  const DESTRUCTIVE_TOOLS = new Set(['Write', 'Edit', 'Bash']);
13
13
  const DEFAULT_RULES = {
14
- allow: ['Read', 'Glob', 'Grep', 'WebSearch', 'WebFetch', 'Task', 'AskUser', 'ImageGen'],
14
+ allow: ['Read', 'Glob', 'Grep', 'WebSearch', 'WebFetch', 'Task', 'AskUser', 'ImageGen', 'TradingSignal', 'TradingMarket', 'SearchX'],
15
15
  deny: [],
16
- ask: ['Write', 'Edit', 'Bash', 'Agent'],
16
+ ask: ['Write', 'Edit', 'Bash', 'Agent', 'PostToX'],
17
17
  };
18
18
  // ─── Permission Manager ────────────────────────────────────────────────────
19
19
  export class PermissionManager {
package/dist/banner.js CHANGED
@@ -7,32 +7,26 @@ import chalk from 'chalk';
7
7
  // https://commons.wikimedia.org/wiki/File:BenFranklinDuplessis.jpg
8
8
  //
9
9
  // Pipeline:
10
- // 1. Crop the 2403×2971 original to a 1400×1400 square centred on the face
11
- // (sips --cropToHeightWidth 1400 1400 --cropOffset 400 500)
10
+ // 1. Crop the 800×989 thumb to a 500×500 square centred on the face
11
+ // (sips --cropToHeightWidth 500 500 --cropOffset 140 150)
12
12
  // 2. Convert via chafa:
13
- // chafa --size=30x14 --symbols=block --colors=256 ben-face.jpg
13
+ // chafa --size=16x8 --symbols=block --colors=full ben-face.jpg
14
14
  // 3. Strip cursor visibility control codes (\x1b[?25l / \x1b[?25h)
15
15
  // 4. Paste here as hex-escaped string array (readable + diff-friendly)
16
16
  //
17
- // Visible dimensions: ~28 characters wide × 14 rows tall.
17
+ // Visible dimensions: ~16 characters wide × 8 rows tall.
18
18
  //
19
19
  // Rendered best in a 256-color or truecolor terminal. Degrades gracefully
20
20
  // on ancient terminals — but those are long gone and we don't support them.
21
21
  const BEN_PORTRAIT_ROWS = [
22
- '\x1b[0m\x1b[38;5;16;48;5;16m \x1b[38;5;232m▁\x1b[38;5;235;48;5;232m▂\x1b[38;5;58;48;5;233m▄\x1b[38;5;95;48;5;234m▆\x1b[38;5;137;48;5;58m▄\x1b[38;5;173m▅\x1b[48;5;94m▅\x1b[48;5;58m▆▅\x1b[48;5;237m▄\x1b[38;5;137;48;5;234m▃\x1b[38;5;235;48;5;233m▂ \x1b[38;5;233;48;5;232m▂▅\x1b[48;5;233m \x1b[0m',
23
- '\x1b[38;5;16;48;5;16m \x1b[38;5;235;48;5;232m▗\x1b[38;5;233;48;5;236m▘\x1b[38;5;8;48;5;239m▌\x1b[38;5;95;48;5;137m▋\x1b[38;5;137;48;5;179m▘ \x1b[38;5;179;48;5;173m▃\x1b[48;5;179m \x1b[48;5;173m▊\x1b[38;5;58;48;5;137m▝\x1b[38;5;94;48;5;235m▖\x1b[38;5;234;48;5;233m▅▄▂ ▂▗▄▃\x1b[0m',
24
- '\x1b[38;5;16;48;5;16m \x1b[38;5;235;48;5;232m▗\x1b[38;5;236;48;5;237m▍ \x1b[38;5;58;48;5;94m▋\x1b[38;5;95;48;5;173m▌\x1b[48;5;179m \x1b[38;5;179;48;5;215m▍\x1b[48;5;221m▔\x1b[38;5;222;48;5;180m▍\x1b[48;5;179m \x1b[38;5;173m▕\x1b[38;5;179;48;5;173m▅\x1b[38;5;137m▕\x1b[38;5;95;48;5;58m▍\x1b[38;5;58;48;5;235m▖\x1b[38;5;235;48;5;234m▖▃▄ \x1b[0m',
25
- '\x1b[38;5;16;48;5;16m \x1b[38;5;233m▗\x1b[48;5;235m▏\x1b[38;5;237;48;5;238m▊\x1b[38;5;238;48;5;236m▌\x1b[38;5;236;48;5;58m▖\x1b[38;5;95;48;5;179m▌ \x1b[38;5;137m▗\x1b[38;5;94m▄\x1b[38;5;58m▄\x1b[38;5;94m▄\x1b[38;5;137m▖\x1b[38;5;173m▗\x1b[38;5;131m▗\x1b[38;5;58;48;5;137m▃\x1b[38;5;131;48;5;58m▘\x1b[38;5;234m▕\x1b[48;5;236m▖\x1b[38;5;236;48;5;235m▃ \x1b[38;5;234m▝\x1b[38;5;235;48;5;234m▃\x1b[0m',
26
- '\x1b[38;5;16;48;5;16m \x1b[38;5;235;48;5;232m▂\x1b[38;5;236;48;5;234m▄\x1b[38;5;237;48;5;236m▗\x1b[38;5;8;48;5;239m▖\x1b[38;5;240;48;5;8m▎\x1b[38;5;94;48;5;236m▕\x1b[38;5;137;48;5;179m▍ \x1b[38;5;94;48;5;137m▝\x1b[38;5;173;48;5;94m▂\x1b[38;5;137;48;5;58m▂\x1b[48;5;94m▃\x1b[48;5;179m▘\x1b[38;5;173m▝\x1b[38;5;137;48;5;235m▍\x1b[38;5;94;48;5;236m▝\x1b[38;5;235;48;5;94m▖\x1b[38;5;52;48;5;58m▖\x1b[38;5;235;48;5;233m▝\x1b[48;5;236m▁\x1b[48;5;235m \x1b[0m',
27
- '\x1b[38;5;232;48;5;16m▗\x1b[38;5;233;48;5;236m▌\x1b[38;5;95;48;5;239m▅\x1b[48;5;240m▃\x1b[38;5;94;48;5;238m▖\x1b[38;5;240;48;5;8m▝\x1b[38;5;95;48;5;236m▘\x1b[38;5;236;48;5;95m▘\x1b[38;5;173;48;5;179m▏ \x1b[38;5;215m▄ \x1b[38;5;179;48;5;137m▅\x1b[38;5;137;48;5;179m▘\x1b[38;5;216m▘\x1b[38;5;179;48;5;216m▃\x1b[48;5;94m▌\x1b[38;5;94;48;5;131m▘\x1b[38;5;95;48;5;94m▋\x1b[38;5;94;48;5;52m▃\x1b[38;5;52;48;5;233m▎\x1b[38;5;233;48;5;235m▅\x1b[38;5;234m▂ \x1b[0m',
28
- '\x1b[38;5;233;48;5;232m▕\x1b[38;5;234;48;5;236m▘\x1b[38;5;8;48;5;95m▌ \x1b[38;5;236m▃\x1b[38;5;58;48;5;234m▘\x1b[38;5;94m▝\x1b[48;5;137m▎\x1b[38;5;179;48;5;173m▍\x1b[38;5;173;48;5;179m▌▆▖▃▞\x1b[38;5;94;48;5;173m▗\x1b[48;5;179m▄\x1b[38;5;179;48;5;58m▘\x1b[38;5;94;48;5;52m▝\x1b[38;5;130;48;5;131m▃\x1b[38;5;94;48;5;58m▍\x1b[38;5;52;48;5;232m▎\x1b[38;5;232;48;5;233m▌\x1b[38;5;233;48;5;234m▏\x1b[38;5;234;48;5;235m▎\x1b[38;5;236m▌▅▄ \x1b[0m',
29
- '\x1b[38;5;232;48;5;235m▋\x1b[38;5;58;48;5;236m▝\x1b[48;5;58m \x1b[38;5;239;48;5;94m▅\x1b[38;5;237;48;5;235m▂\x1b[38;5;235;48;5;233m▂\x1b[38;5;234;48;5;94m▄\x1b[38;5;94;48;5;137m▖\x1b[48;5;173m \x1b[38;5;173;48;5;179m▃ \x1b[38;5;137m▂▃▂\x1b[38;5;131;48;5;137m▃\x1b[38;5;58;48;5;131m▝\x1b[38;5;94;48;5;52m▅\x1b[48;5;94m \x1b[48;5;58m▍\x1b[38;5;235;48;5;232m▎\x1b[38;5;232;48;5;233m▋\x1b[38;5;233;48;5;234m▍\x1b[38;5;235;48;5;236m▏ \x1b[38;5;236;48;5;235m▎ \x1b[0m',
30
- '\x1b[38;5;234;48;5;235m▏\x1b[38;5;236;48;5;237m▋\x1b[38;5;237;48;5;8m▃\x1b[38;5;235;48;5;238m▗\x1b[38;5;237m▖\x1b[38;5;58;48;5;234m▌\x1b[38;5;234;48;5;233m▎\x1b[38;5;236;48;5;137m▎\x1b[38;5;137;48;5;173m▄ \x1b[38;5;173;48;5;179m▄▃ \x1b[38;5;179;48;5;215m▅\x1b[38;5;173;48;5;179m▄\x1b[38;5;179;48;5;137m▘\x1b[38;5;137;48;5;131m▌\x1b[48;5;94m \x1b[38;5;58m▗\x1b[38;5;233;48;5;58m▗\x1b[48;5;233m \x1b[38;5;234;48;5;236m▘ \x1b[38;5;236;48;5;235m▃▞\x1b[38;5;235;48;5;236m▄\x1b[48;5;235m \x1b[0m',
31
- '\x1b[38;5;234;48;5;235m▏▆\x1b[38;5;235;48;5;237m▌\x1b[38;5;236m▝\x1b[38;5;237;48;5;234m▍\x1b[38;5;234;48;5;233m▖\x1b[38;5;240;48;5;234m▗\x1b[38;5;101;48;5;186m▌\x1b[38;5;137m▝\x1b[48;5;137m \x1b[48;5;173m▆▄▃\x1b[38;5;131m▂\x1b[38;5;130;48;5;137m▂\x1b[38;5;58;48;5;94m▃\x1b[48;5;58m \x1b[38;5;234;48;5;233m▏\x1b[38;5;235;48;5;234m▅\x1b[48;5;236m▌ ▝ \x1b[48;5;235m \x1b[0m',
32
- '\x1b[38;5;234;48;5;233m▕\x1b[38;5;239;48;5;235m▂\x1b[38;5;95m▃\x1b[48;5;237m▄\x1b[48;5;236m▄\x1b[48;5;235m▄\x1b[38;5;236;48;5;240m▘\x1b[38;5;101;48;5;95m▕\x1b[48;5;186m▖\x1b[38;5;179;48;5;229m▝\x1b[38;5;223;48;5;137m▃\x1b[38;5;137;48;5;131m▁\x1b[38;5;95m▅\x1b[38;5;94m▂\x1b[48;5;94m \x1b[38;5;58m▗\x1b[38;5;94;48;5;58m▔\x1b[38;5;236m▁ \x1b[48;5;235m▆\x1b[38;5;235;48;5;236m▍\x1b[38;5;236;48;5;235m▆\x1b[48;5;236m \x1b[38;5;235m▅\x1b[48;5;235m \x1b[0m',
33
- '\x1b[38;5;237;48;5;95m▔ \x1b[38;5;137;48;5;101m▝\x1b[48;5;187m▅\x1b[38;5;180;48;5;229m▂\x1b[38;5;143;48;5;222m▔\x1b[38;5;186;48;5;58m▅\x1b[38;5;179m▂\x1b[38;5;95m▁\x1b[38;5;235m▂\x1b[38;5;236m▄\x1b[48;5;233m▌\x1b[38;5;235m▔\x1b[38;5;233;48;5;236m▅\x1b[38;5;234m▃\x1b[38;5;235m▁ ▔\x1b[48;5;235m \x1b[0m',
34
- '\x1b[38;5;101;48;5;137m▔\x1b[38;5;95;48;5;101m▄▔\x1b[38;5;101;48;5;95m▄ ▗ \x1b[38;5;240m▖\x1b[38;5;95;48;5;101m▘\x1b[38;5;137m▔\x1b[48;5;222m▅\x1b[48;5;186m▃\x1b[48;5;179m▂\x1b[38;5;101;48;5;95m▌\x1b[48;5;58m \x1b[38;5;238;48;5;236m▁\x1b[38;5;180;48;5;234m▃\x1b[48;5;235m▄\x1b[38;5;179;48;5;234m▃\x1b[38;5;95m▁\x1b[38;5;234;48;5;235m▊\x1b[48;5;236m▆\x1b[38;5;235m▃\x1b[38;5;234m▂\x1b[38;5;235m▁ \x1b[38;5;236;48;5;235m▎\x1b[0m',
35
- '\x1b[38;5;137;48;5;137m \x1b[48;5;95m▄ \x1b[38;5;95;48;5;101m▖\x1b[48;5;137m▝\x1b[48;5;95m \x1b[38;5;101m▅\x1b[48;5;239m▋\x1b[48;5;95m \x1b[38;5;95;48;5;137m▋\x1b[38;5;101;48;5;95m▍\x1b[38;5;95;48;5;101m▖\x1b[38;5;101;48;5;95m▆\x1b[38;5;239m▗\x1b[38;5;101m▄ \x1b[38;5;95;48;5;137m▅\x1b[38;5;137;48;5;180m▅\x1b[38;5;180;48;5;186m▃\x1b[48;5;143m▆\x1b[38;5;95m▔\x1b[38;5;143;48;5;235m▖\x1b[48;5;234m \x1b[38;5;235m▆\x1b[38;5;234;48;5;235m▝\x1b[38;5;235;48;5;234m▞\x1b[38;5;234;48;5;235m▄ \x1b[0m',
22
+ '\x1b[0m\x1b[38;2;7;0;0;48;2;8;0;0m▔ \x1b[38;2;9;1;0m▂\x1b[38;2;56;36;15;48;2;11;2;0m▗\x1b[38;2;100;73;36;48;2;31;16;6m▅\x1b[38;2;189;141;75;48;2;117;87;43m▅\x1b[38;2;217;162;85;48;2;152;111;51m▆\x1b[38;2;164;122;64;48;2;215;158;85m▔\x1b[38;2;124;90;46;48;2;217;160;93m▔\x1b[38;2;185;136;75;48;2;77;48;20m▅\x1b[38;2;100;61;24;48;2;39;18;4m▖\x1b[38;2;48;26;9;48;2;32;13;3m▃\x1b[38;2;39;18;4;48;2;30;11;2m▄\x1b[38;2;38;17;4;48;2;32;13;3m▄\x1b[38;2;40;20;5;48;2;35;15;2m▃\x1b[38;2;41;21;5;48;2;36;16;3m▂\x1b[0m',
23
+ '\x1b[7m\x1b[38;2;8;0;0m \x1b[0m\x1b[38;2;0;0;0;48;2;8;0;0m \x1b[38;2;13;2;1;48;2;45;26;10m▊\x1b[38;2;61;40;17;48;2;87;63;31m▎\x1b[38;2;88;61;29;48;2;134;94;42m▋\x1b[38;2;182;132;66;48;2;223;172;93m▏\x1b[38;2;140;91;38;48;2;233;193;106m▂\x1b[38;2;135;82;35;48;2;229;178;106m▂\x1b[38;2;201;145;78;48;2;223;166;95m▂\x1b[38;2;133;88;46;48;2;198;148;86m▁\x1b[38;2;144;96;47;48;2;96;57;21m▍\x1b[38;2;66;42;15;48;2;58;33;11m▗\x1b[38;2;59;36;13;48;2;47;25;9m▆\x1b[38;2;57;35;11;48;2;46;24;7m▅\x1b[38;2;58;36;11;48;2;50;29;8m▖\x1b[38;2;53;32;8;48;2;48;26;7m▃\x1b[0m',
24
+ '\x1b[38;2;12;3;3;48;2;9;0;0m▁\x1b[38;2;102;76;40;48;2;19;8;4m▗\x1b[38;2;110;83;45;48;2;56;35;15m▄\x1b[38;2;91;67;37;48;2;105;79;45m▌\x1b[38;2;96;64;31;48;2;186;135;70m▊\x1b[38;2;226;169;101;48;2;217;162;91m▗\x1b[38;2;216;159;89;48;2;144;93;44m▅\x1b[38;2;195;145;83;48;2;112;62;24m▅\x1b[38;2;233;178;110;48;2;206;151;81m▆\x1b[38;2;207;155;92;48;2;105;61;30m▎\x1b[38;2;145;94;46;48;2;94;50;19m▖\x1b[38;2;90;48;17;48;2;52;26;8m▎\x1b[38;2;59;33;9;48;2;64;40;14m▖\x1b[38;2;63;39;13;48;2;65;41;13m▊\x1b[38;2;58;36;11;48;2;64;40;14m▝\x1b[38;2;60;38;13;48;2;57;35;10m▍\x1b[0m',
25
+ '\x1b[38;2;37;22;12;48;2;11;2;2m▕\x1b[38;2;52;32;16;48;2;94;67;32m▘\x1b[38;2;77;53;21;48;2;125;96;52m▗\x1b[38;2;44;15;6;48;2;83;48;21m▞\x1b[38;2;122;73;33;48;2;195;138;72m▍\x1b[38;2;209;149;77;48;2;223;160;89m▋\x1b[38;2;228;157;84;48;2;234;173;98m▆\x1b[38;2;207;140;80;48;2;225;167;96m▝\x1b[38;2;213;151;88;48;2;193;135;79m▏\x1b[38;2;164;111;60;48;2;104;54;21m▍\x1b[38;2;175;110;52;48;2;136;78;32m▘\x1b[38;2;93;47;15;48;2;26;5;2m▎\x1b[38;2;39;13;4;48;2;54;28;8m▍\x1b[38;2;63;40;13;48;2;67;44;16m▔\x1b[38;2;68;44;15;48;2;65;41;16m▊\x1b[38;2;60;36;11;48;2;63;39;14m▝\x1b[0m',
26
+ '\x1b[38;2;12;1;0;48;2;55;33;13m▌\x1b[38;2;92;63;32;48;2;68;43;17m▝\x1b[38;2;75;51;24;48;2;93;65;34m▗\x1b[38;2;88;61;30;48;2;42;18;8m▘\x1b[38;2;62;35;18;48;2;191;150;83m▍\x1b[38;2;186;140;75;48;2;194;138;63m▁\x1b[38;2;189;130;61;48;2;219;157;79m▄\x1b[38;2;191;132;70;48;2;217;159;87m▂\x1b[38;2;179;105;60;48;2;207;146;83m▔\x1b[38;2;171;106;51;48;2;135;79;32m▋\x1b[38;2;64;30;8;48;2;120;69;27m▗\x1b[38;2;56;26;8;48;2;39;13;5m▂\x1b[38;2;44;18;7;48;2;72;44;16m▘\x1b[38;2;72;47;18;48;2;69;44;14m▖\x1b[38;2;70;46;14;48;2;68;44;14m▁\x1b[38;2;65;41;12;48;2;65;41;14m▘\x1b[0m',
27
+ '\x1b[38;2;77;56;35;48;2;22;8;3m▂\x1b[38;2;126;100;69;48;2;59;36;15m▃\x1b[38;2;131;105;70;48;2;80;54;27m▄\x1b[38;2;128;103;68;48;2;57;33;14m▄\x1b[38;2;191;174;117;48;2;125;103;69m▝\x1b[38;2;191;164;108;48;2;236;227;160m▞\x1b[38;2;220;202;137;48;2;173;123;63m▃\x1b[38;2;130;85;43;48;2;164;111;58m▄\x1b[38;2;117;68;26;48;2;185;116;58m▆\x1b[38;2;135;80;33;48;2;94;52;15m▘\x1b[38;2;51;28;9;48;2;80;50;16m▂\x1b[38;2;62;33;9;48;2;76;46;14m▘\x1b[38;2;75;50;16;48;2;74;47;15m▗\x1b[38;2;71;46;14;48;2;72;47;15m▝\x1b[38;2;73;48;16;48;2;69;44;14m▏\x1b[38;2;65;41;11;48;2;66;41;15m▆\x1b[0m',
28
+ '\x1b[38;2;125;101;70;48;2;159;129;87m▔\x1b[38;2;145;114;71;48;2;124;100;70m▆\x1b[38;2;152;123;81;48;2;121;100;69m▃\x1b[38;2;117;95;60;48;2;129;106;70m▖\x1b[38;2;115;91;61;48;2;131;105;69m▗\x1b[38;2;166;145;103;48;2;140;113;71m▔\x1b[38;2;162;135;87;48;2;231;217;147m▅\x1b[38;2;133;107;71;48;2;199;171;110m▂\x1b[38;2;131;100;59;48;2;107;75;37m▍\x1b[38;2;166;139;88;48;2;67;40;14m▃\x1b[38;2;204;179;121;48;2;39;19;8m▄\x1b[38;2;137;112;73;48;2;52;28;10m▖\x1b[38;2;54;32;10;48;2;76;49;16m▅\x1b[38;2;56;33;9;48;2;74;48;15m▃\x1b[38;2;60;37;10;48;2;70;47;14m▁\x1b[38;2;66;43;12;48;2;64;40;11m▅\x1b[0m',
29
+ '\x1b[38;2;157;128;85;48;2;167;138;98m▝\x1b[38;2;141;111;71;48;2;166;136;98m▝\x1b[38;2;149;119;83;48;2;126;96;60m▞\x1b[38;2;157;129;93;48;2;139;113;81m▅\x1b[38;2;144;117;79;48;2;117;92;58m▋\x1b[38;2;130;102;62;48;2;169;138;87m▋\x1b[38;2;171;141;87;48;2;143;117;77m▖\x1b[38;2;144;117;79;48;2;122;96;63m▊\x1b[38;2;132;105;68;48;2;144;117;82m▖\x1b[38;2;153;127;92;48;2;140;115;83m▞\x1b[38;2;134;108;71;48;2;217;193;135m▅\x1b[38;2;176;150;98;48;2;129;105;66m▋\x1b[38;2;118;94;61;48;2;54;32;14m▂\x1b[38;2;44;23;8;48;2;59;37;13m▃\x1b[38;2;62;41;16;48;2;48;26;9m▖\x1b[38;2;46;24;6;48;2;66;42;15m▖\x1b[0m',
36
30
  ];
37
31
  // ─── FRANKLIN text banner (gold → emerald gradient) ────────────────────────
38
32
  //
@@ -69,9 +63,9 @@ function interpolateHex(start, end, t) {
69
63
  }
70
64
  // ─── Banner layout ─────────────────────────────────────────────────────────
71
65
  // Minimum terminal width to show the side-by-side portrait + text layout.
72
- // The portrait is ~28 chars, the FRANKLIN text is ~65 chars, plus a 3-char
73
- // gap = 96 chars. We add a small margin so 100 cols is the threshold.
74
- const MIN_WIDTH_FOR_PORTRAIT = 100;
66
+ // The portrait is ~16 chars, the FRANKLIN text is ~65 chars, plus a 3-char
67
+ // gap = 84 chars. We round up to 85 cols as the threshold.
68
+ const MIN_WIDTH_FOR_PORTRAIT = 85;
75
69
  /**
76
70
  * Pad a line to an exact visual width, ignoring ANSI escape codes when
77
71
  * measuring. Used to align the portrait's right edge before the text block.
@@ -99,28 +93,21 @@ export function printBanner(version) {
99
93
  }
100
94
  /**
101
95
  * Full layout: Ben Franklin portrait on the left, FRANKLIN text block on the
102
- * right. Portrait is 14 rows × ~28 chars, text is 6 rows — text is vertically
103
- * centred inside the portrait with 4 rows of padding above and 4 below,
104
- * tagline sitting right under the FRANKLIN block.
96
+ * right. Portrait is 8 rows × ~16 chars, text is 6 rows — text is vertically
97
+ * centred inside the portrait with 1 row of padding above.
105
98
  *
106
- * [portrait row 1] (empty)
107
- * [portrait row 2] (empty)
108
- * [portrait row 3] (empty)
109
- * [portrait row 4] (empty)
110
- * [portrait row 5] ███████╗██████╗ █████╗ ...
111
- * [portrait row 6] ██╔════╝██╔══██╗██╔══██╗...
112
- * [portrait row 7] █████╗ ██████╔╝███████║...
113
- * [portrait row 8] ██╔══╝ ██╔══██╗██╔══██║...
114
- * [portrait row 9] ██║ ██║ ██║██║ ██║...
115
- * [portrait row 10] ╚═╝ ╚═╝ ╚═╝╚═╝ ╚═╝...
116
- * [portrait row 11] blockrun.ai · The AI agent with a wallet · vX
117
- * [portrait row 12] (empty)
118
- * [portrait row 13] (empty)
119
- * [portrait row 14] (empty)
99
+ * [portrait row 1] (empty)
100
+ * [portrait row 2] ███████╗██████╗ █████╗ ...
101
+ * [portrait row 3] ██╔════╝██╔══██╗██╔══██╗...
102
+ * [portrait row 4] █████╗ ██████╔╝███████║...
103
+ * [portrait row 5] ██╔══╝ ██╔══██╗██╔══██║...
104
+ * [portrait row 6] ██║ ██║ ██║██║ ██║...
105
+ * [portrait row 7] ╚═╝ ╚═╝ ╚═╝╚═╝ ╚═╝...
106
+ * [portrait row 8] blockrun.ai · The AI agent with a wallet · vX
120
107
  */
121
108
  function printSideBySide(version) {
122
- const TEXT_TOP_OFFSET = 4; // rows of portrait above the text
123
- const PORTRAIT_WIDTH = 29; // columns (char width) of the portrait + 1 pad
109
+ const TEXT_TOP_OFFSET = 1; // rows of portrait above the text
110
+ const PORTRAIT_WIDTH = 17; // columns (char width) of the portrait + 1 pad
124
111
  const GAP = ' '; // gap between portrait and text
125
112
  const portraitRows = BEN_PORTRAIT_ROWS;
126
113
  const textRows = FRANKLIN_ART.length;
@@ -53,13 +53,22 @@ export async function startCommand(options) {
53
53
  printBanner(version);
54
54
  const workDir = process.cwd();
55
55
  // Show session info immediately, fetch balance in background
56
- console.log(chalk.dim(` Model: ${model}`));
56
+ // Model is shown in the live status bar — no static line needed.
57
57
  console.log(chalk.dim(` Wallet: ${walletAddress || 'not set'}`));
58
58
  console.log(chalk.dim(` Dir: ${workDir}`));
59
59
  // First-run tip: show if no config file exists yet
60
60
  if (!configModel && !options.model) {
61
61
  console.log(chalk.dim(`\n Tip: /model to switch models · /compact to save tokens · /help for all commands`));
62
62
  }
63
+ // Welcome message — show things Hermes/OpenClaw can't do.
64
+ // Only on first run or when no model is configured (new user indicator).
65
+ // After the user's first session, the tip fades and they go straight to the prompt.
66
+ console.log('');
67
+ console.log(chalk.dim(' Try something only Franklin can do:'));
68
+ console.log(chalk.dim(' ') + chalk.hex('#FFD700')('"what\'s BTC looking like today?"') + chalk.dim(' ← live market signal'));
69
+ console.log(chalk.dim(' ') + chalk.hex('#10B981')('"find X posts about ai agent"') + chalk.dim(' ← social growth'));
70
+ console.log(chalk.dim(' ') + chalk.hex('#60A5FA')('"generate a hero image for my app"') + chalk.dim(' ← AI image gen'));
71
+ console.log(chalk.dim(' Or just code — 55+ models ready, no API keys needed.'));
63
72
  console.log('');
64
73
  // Balance fetcher — used at startup and after each turn
65
74
  const fetchBalance = async () => {
@@ -130,6 +139,14 @@ export async function startCommand(options) {
130
139
  permissionMode: (options.trust || !process.stdin.isTTY) ? 'trust' : 'default',
131
140
  debug: options.debug,
132
141
  };
142
+ // Bootstrap learnings from Claude Code config on first run (async, non-blocking)
143
+ Promise.all([
144
+ import('../learnings/extractor.js'),
145
+ import('../agent/llm.js'),
146
+ ]).then(([{ bootstrapFromClaudeConfig }, { ModelClient }]) => {
147
+ const client = new ModelClient({ apiUrl, chain });
148
+ bootstrapFromClaudeConfig(client).catch(() => { });
149
+ }).catch(() => { });
133
150
  // Use Ink UI if TTY, fallback to basic readline for piped input
134
151
  if (process.stdin.isTTY) {
135
152
  await runWithInkUI(agentConfig, model, workDir, version, walletInfo, (cb) => {
@@ -167,8 +184,9 @@ async function runWithInkUI(agentConfig, model, workDir, version, walletInfo, on
167
184
  fetchBalance().then(bal => ui.updateBalance(bal)).catch(() => { });
168
185
  });
169
186
  }
187
+ let sessionHistory;
170
188
  try {
171
- await interactiveSession(agentConfig, async () => {
189
+ sessionHistory = await interactiveSession(agentConfig, async () => {
172
190
  const input = await ui.waitForInput();
173
191
  if (input === null)
174
192
  return null;
@@ -184,6 +202,19 @@ async function runWithInkUI(agentConfig, model, workDir, version, walletInfo, on
184
202
  }
185
203
  ui.cleanup();
186
204
  flushStats();
205
+ // Extract learnings from the session (async, 10s timeout, never blocks exit)
206
+ if (sessionHistory && sessionHistory.length >= 4) {
207
+ try {
208
+ const { extractLearnings } = await import('../learnings/extractor.js');
209
+ const { ModelClient } = await import('../agent/llm.js');
210
+ const client = new ModelClient({ apiUrl: agentConfig.apiUrl, chain: agentConfig.chain });
211
+ await Promise.race([
212
+ extractLearnings(sessionHistory, `session-${new Date().toISOString()}`, client),
213
+ new Promise(resolve => setTimeout(resolve, 10_000)),
214
+ ]);
215
+ }
216
+ catch { /* extraction is best-effort */ }
217
+ }
187
218
  await disconnectMcpServers();
188
219
  console.log(chalk.dim('\nGoodbye.\n'));
189
220
  }
@@ -0,0 +1 @@
1
+ export declare function initBridge(): void;
@@ -0,0 +1,24 @@
1
+ import { bus } from './bus.js';
2
+ import { addSignal, addPost } from '../narrative/state.js';
3
+ export function initBridge() {
4
+ bus.on('signal.detected', (event) => {
5
+ const e = event;
6
+ addSignal({
7
+ asset: e.data.asset,
8
+ direction: e.data.direction,
9
+ confidence: e.data.confidence,
10
+ summary: e.data.summary,
11
+ ts: e.ts,
12
+ });
13
+ });
14
+ bus.on('post.published', (event) => {
15
+ const e = event;
16
+ addPost({
17
+ platform: e.data.platform,
18
+ url: e.data.url,
19
+ text: e.data.text,
20
+ referencesAssets: e.data.referencesAssets,
21
+ ts: e.ts,
22
+ });
23
+ });
24
+ }
@@ -0,0 +1,17 @@
1
+ import type { FranklinEvent } from './types.js';
2
+ type Handler = (event: FranklinEvent) => void | Promise<void>;
3
+ export declare class EventBus {
4
+ private handlers;
5
+ private logEnabled;
6
+ private logPath;
7
+ constructor(opts?: {
8
+ log?: boolean;
9
+ });
10
+ on(type: FranklinEvent['type'], handler: Handler): void;
11
+ off(type: FranklinEvent['type'], handler: Handler): void;
12
+ emit(event: FranklinEvent): Promise<void>;
13
+ clear(): void;
14
+ private appendLog;
15
+ }
16
+ export declare const bus: EventBus;
17
+ export {};
@@ -0,0 +1,55 @@
1
+ import os from 'node:os';
2
+ import path from 'node:path';
3
+ import fs from 'node:fs';
4
+ export class EventBus {
5
+ handlers = new Map();
6
+ logEnabled;
7
+ logPath;
8
+ constructor(opts = {}) {
9
+ this.logEnabled = opts.log ?? false;
10
+ this.logPath = path.join(os.homedir(), '.blockrun', 'events.jsonl');
11
+ }
12
+ on(type, handler) {
13
+ let set = this.handlers.get(type);
14
+ if (!set) {
15
+ set = new Set();
16
+ this.handlers.set(type, set);
17
+ }
18
+ set.add(handler);
19
+ }
20
+ off(type, handler) {
21
+ this.handlers.get(type)?.delete(handler);
22
+ }
23
+ async emit(event) {
24
+ if (this.logEnabled) {
25
+ this.appendLog(event);
26
+ }
27
+ const set = this.handlers.get(event.type);
28
+ if (!set)
29
+ return;
30
+ const promises = [];
31
+ for (const handler of set) {
32
+ const result = handler(event);
33
+ if (result)
34
+ promises.push(result);
35
+ }
36
+ if (promises.length)
37
+ await Promise.all(promises);
38
+ }
39
+ clear() {
40
+ this.handlers.clear();
41
+ }
42
+ appendLog(event) {
43
+ try {
44
+ const dir = path.dirname(this.logPath);
45
+ if (!fs.existsSync(dir)) {
46
+ fs.mkdirSync(dir, { recursive: true });
47
+ }
48
+ fs.appendFileSync(this.logPath, JSON.stringify(event) + '\n');
49
+ }
50
+ catch {
51
+ // best-effort logging — don't crash the agent
52
+ }
53
+ }
54
+ }
55
+ export const bus = new EventBus();
@@ -0,0 +1,49 @@
1
+ export interface BaseEvent {
2
+ id: string;
3
+ type: string;
4
+ ts: string;
5
+ source: 'trading' | 'social' | 'core';
6
+ costUsd?: number;
7
+ correlationId?: string;
8
+ }
9
+ export interface SignalDetectedEvent extends BaseEvent {
10
+ type: 'signal.detected';
11
+ data: {
12
+ asset: string;
13
+ direction: 'bullish' | 'bearish' | 'neutral';
14
+ confidence: number;
15
+ indicators: Record<string, number>;
16
+ summary: string;
17
+ };
18
+ }
19
+ export interface PostPublishedEvent extends BaseEvent {
20
+ type: 'post.published';
21
+ data: {
22
+ platform: 'x' | 'reddit' | (string & {});
23
+ url: string;
24
+ text: string;
25
+ referencesAssets?: string[];
26
+ };
27
+ }
28
+ export interface MentionReceivedEvent extends BaseEvent {
29
+ type: 'mention.received';
30
+ data: {
31
+ platform: string;
32
+ url: string;
33
+ text: string;
34
+ author: string;
35
+ sentiment?: 'positive' | 'negative' | 'neutral';
36
+ mentionsAsset?: string;
37
+ };
38
+ }
39
+ export interface BudgetExceededEvent extends BaseEvent {
40
+ type: 'budget.exceeded';
41
+ data: {
42
+ category: 'llm' | 'data' | 'gas';
43
+ spent: number;
44
+ cap: number;
45
+ blockedAction: string;
46
+ };
47
+ }
48
+ export type FranklinEvent = SignalDetectedEvent | PostPublishedEvent | MentionReceivedEvent | BudgetExceededEvent;
49
+ export declare function makeEvent<T extends FranklinEvent>(props: Omit<T, 'id' | 'ts'>): T;
@@ -0,0 +1,8 @@
1
+ import crypto from 'node:crypto';
2
+ export function makeEvent(props) {
3
+ return {
4
+ id: crypto.randomUUID(),
5
+ ts: new Date().toISOString(),
6
+ ...props,
7
+ };
8
+ }
@@ -0,0 +1,16 @@
1
+ /**
2
+ * Extract user preferences from a completed session trace.
3
+ * Uses a cheap model to analyze the conversation and produce learnings.
4
+ */
5
+ import { ModelClient } from '../agent/llm.js';
6
+ import type { Dialogue } from '../agent/types.js';
7
+ /**
8
+ * Scan for Claude Code configuration and bootstrap learnings from it.
9
+ * Only runs once — skips if learnings already exist.
10
+ */
11
+ export declare function bootstrapFromClaudeConfig(client: ModelClient): Promise<number>;
12
+ /**
13
+ * Extract learnings from a completed session.
14
+ * Runs asynchronously — caller should fire-and-forget.
15
+ */
16
+ export declare function extractLearnings(history: Dialogue[], sessionId: string, client: ModelClient): Promise<void>;