@blockrun/franklin 3.2.3 → 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.
- package/dist/agent/commands.js +30 -1
- package/dist/agent/context.js +13 -0
- package/dist/agent/permissions.js +3 -3
- package/dist/banner.js +61 -75
- package/dist/commands/start.js +33 -2
- package/dist/events/bridge.d.ts +1 -0
- package/dist/events/bridge.js +24 -0
- package/dist/events/bus.d.ts +17 -0
- package/dist/events/bus.js +55 -0
- package/dist/events/types.d.ts +49 -0
- package/dist/events/types.js +8 -0
- package/dist/learnings/extractor.d.ts +16 -0
- package/dist/learnings/extractor.js +234 -0
- package/dist/learnings/index.d.ts +3 -0
- package/dist/learnings/index.js +2 -0
- package/dist/learnings/store.d.ts +15 -0
- package/dist/learnings/store.js +130 -0
- package/dist/learnings/types.d.ts +24 -0
- package/dist/learnings/types.js +7 -0
- package/dist/narrative/state.d.ts +30 -0
- package/dist/narrative/state.js +69 -0
- package/dist/social/browser-pool.d.ts +29 -0
- package/dist/social/browser-pool.js +57 -0
- package/dist/social/preflight.d.ts +14 -0
- package/dist/social/preflight.js +26 -0
- package/dist/social/x.d.ts +8 -0
- package/dist/social/x.js +9 -1
- package/dist/tools/index.js +7 -0
- package/dist/tools/posttox.d.ts +7 -0
- package/dist/tools/posttox.js +137 -0
- package/dist/tools/searchx.d.ts +7 -0
- package/dist/tools/searchx.js +111 -0
- package/dist/tools/trading.d.ts +3 -0
- package/dist/tools/trading.js +168 -0
- package/dist/trading/config.d.ts +23 -0
- package/dist/trading/config.js +45 -0
- package/dist/trading/data.d.ts +30 -0
- package/dist/trading/data.js +112 -0
- package/dist/trading/metrics.d.ts +29 -0
- package/dist/trading/metrics.js +105 -0
- package/package.json +1 -1
package/dist/agent/commands.js
CHANGED
|
@@ -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);
|
package/dist/agent/context.js
CHANGED
|
@@ -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
|
@@ -2,47 +2,38 @@ import chalk from 'chalk';
|
|
|
2
2
|
// ─── Ben Franklin portrait ─────────────────────────────────────────────────
|
|
3
3
|
//
|
|
4
4
|
// Generated once, at build time, from the Joseph Duplessis 1785 oil painting
|
|
5
|
-
// of Benjamin Franklin (same source
|
|
6
|
-
//
|
|
5
|
+
// of Benjamin Franklin (same source as the portrait on the US $100 bill).
|
|
6
|
+
// Public domain image from Wikimedia Commons:
|
|
7
7
|
// https://commons.wikimedia.org/wiki/File:BenFranklinDuplessis.jpg
|
|
8
8
|
//
|
|
9
9
|
// Pipeline:
|
|
10
|
-
// 1. Crop the
|
|
11
|
-
// (sips --cropToHeightWidth
|
|
12
|
-
// 2. Convert
|
|
13
|
-
//
|
|
14
|
-
//
|
|
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
|
+
// 2. Convert via chafa:
|
|
13
|
+
// chafa --size=16x8 --symbols=block --colors=full ben-face.jpg
|
|
14
|
+
// 3. Strip cursor visibility control codes (\x1b[?25l / \x1b[?25h)
|
|
15
|
+
// 4. Paste here as hex-escaped string array (readable + diff-friendly)
|
|
15
16
|
//
|
|
16
|
-
//
|
|
17
|
-
// a 34×16 braille output gives 68×64 = 4,352 effective "pixels" — 2.7× the
|
|
18
|
-
// resolution of chafa half-block mode at the same visible size. For a face,
|
|
19
|
-
// which is all about silhouette + key features, this is a massive win.
|
|
17
|
+
// Visible dimensions: ~16 characters wide × 8 rows tall.
|
|
20
18
|
//
|
|
21
|
-
//
|
|
22
|
-
//
|
|
23
|
-
// brand tinting, and it ships as a clean readable TS array.
|
|
19
|
+
// Rendered best in a 256-color or truecolor terminal. Degrades gracefully
|
|
20
|
+
// on ancient terminals — but those are long gone and we don't support them.
|
|
24
21
|
const BEN_PORTRAIT_ROWS = [
|
|
25
|
-
'
|
|
26
|
-
'
|
|
27
|
-
'
|
|
28
|
-
'
|
|
29
|
-
'
|
|
30
|
-
'
|
|
31
|
-
'
|
|
32
|
-
'
|
|
33
|
-
'⠀⠀⠀⠀⠀⠀⠀⠀⠘⣿⣿⣿⣿⣿⣿⣿⢿⣿⠿⢷⠄⠀⠙⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀',
|
|
34
|
-
'⠀⠀⠀⠀⠀⠀⠀⠀⠀⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣷⡆⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀',
|
|
35
|
-
'⠀⠀⠀⠀⠀⠀⠀⠀⢀⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣧⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀',
|
|
36
|
-
'⠀⠀⠀⠀⠀⠀⠀⠀⠘⣿⣿⣿⡿⣿⡿⣿⣏⠛⠛⠙⠁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀',
|
|
37
|
-
'⠀⠀⡴⠺⠖⢒⣂⢄⡀⣹⣿⣿⣿⣶⣙⠂⠁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀',
|
|
38
|
-
'⣶⣤⣄⡀⠈⠻⠿⡙⠗⠸⡻⣿⡻⣿⣿⣷⣦⣄⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀',
|
|
39
|
-
'⣿⡟⠻⣿⣦⡀⠀⢁⡆⠀⠹⢿⣿⣮⣟⠿⣿⠏⠀⠀⣀⣴⣶⣦⣄⠀⠀⠀⠀⠀⠀⠀⠀⠀',
|
|
40
|
-
'⣿⣷⣄⠈⠿⣷⡀⣾⣿⡀⢦⢸⣿⡹⣿⣿⡆⣤⡐⠻⡻⣿⣿⣿⣿⣦⠀⠀⠀⠀⠀⠀⠀⠀',
|
|
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',
|
|
41
30
|
];
|
|
42
31
|
// ─── FRANKLIN text banner (gold → emerald gradient) ────────────────────────
|
|
43
32
|
//
|
|
44
|
-
// Kept from v3.1.0. 6 block-letter rows
|
|
45
|
-
//
|
|
33
|
+
// Kept from v3.1.0. The text is laid out as 6 block-letter rows. Each row
|
|
34
|
+
// is tinted with a color interpolated between GOLD_START and EMERALD_END,
|
|
35
|
+
// giving the smooth vertical gradient that's been Franklin's banner since
|
|
36
|
+
// v3.1.0.
|
|
46
37
|
const FRANKLIN_ART = [
|
|
47
38
|
' ███████╗██████╗ █████╗ ███╗ ██╗██╗ ██╗██╗ ██╗███╗ ██╗',
|
|
48
39
|
' ██╔════╝██╔══██╗██╔══██╗████╗ ██║██║ ██╔╝██║ ██║████╗ ██║',
|
|
@@ -72,19 +63,23 @@ function interpolateHex(start, end, t) {
|
|
|
72
63
|
}
|
|
73
64
|
// ─── Banner layout ─────────────────────────────────────────────────────────
|
|
74
65
|
// Minimum terminal width to show the side-by-side portrait + text layout.
|
|
75
|
-
//
|
|
76
|
-
//
|
|
77
|
-
const MIN_WIDTH_FOR_PORTRAIT =
|
|
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;
|
|
78
69
|
/**
|
|
79
|
-
* Pad a line to an exact visual width
|
|
80
|
-
*
|
|
81
|
-
* codepoint count.
|
|
70
|
+
* Pad a line to an exact visual width, ignoring ANSI escape codes when
|
|
71
|
+
* measuring. Used to align the portrait's right edge before the text block.
|
|
82
72
|
*/
|
|
83
|
-
function
|
|
84
|
-
|
|
73
|
+
function padVisible(s, targetWidth) {
|
|
74
|
+
// Strip ANSI color codes to measure visible length
|
|
75
|
+
// eslint-disable-next-line no-control-regex
|
|
76
|
+
const visible = s.replace(/\x1b\[[0-9;]*m/g, '');
|
|
77
|
+
// Unicode block characters are width 1 (they're half-blocks, not double-width)
|
|
78
|
+
const current = [...visible].length;
|
|
85
79
|
if (current >= targetWidth)
|
|
86
80
|
return s;
|
|
87
|
-
|
|
81
|
+
// Append a reset + padding so background colors don't bleed into the gap
|
|
82
|
+
return s + '\x1b[0m' + ' '.repeat(targetWidth - current);
|
|
88
83
|
}
|
|
89
84
|
export function printBanner(version) {
|
|
90
85
|
const termWidth = process.stdout.columns ?? 80;
|
|
@@ -97,42 +92,30 @@ export function printBanner(version) {
|
|
|
97
92
|
}
|
|
98
93
|
}
|
|
99
94
|
/**
|
|
100
|
-
* Full layout: Ben Franklin
|
|
101
|
-
*
|
|
102
|
-
*
|
|
103
|
-
* Ben's face region (head at rows 1-4, face at rows 5-10, shoulders 11-16),
|
|
104
|
-
* giving the classic "portrait and nameplate" composition.
|
|
95
|
+
* Full layout: Ben Franklin portrait on the left, FRANKLIN text block on the
|
|
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
|
-
* row
|
|
107
|
-
* row
|
|
108
|
-
* row
|
|
109
|
-
* row
|
|
110
|
-
* row
|
|
111
|
-
* row
|
|
112
|
-
* row
|
|
113
|
-
* row
|
|
114
|
-
* row 9 ⠀⠀⠀⠀⠀⠀⠀⠀⠘⣿⣿... ██║ ██║ ██║██║ ██║...
|
|
115
|
-
* row 10 ⠀⠀⠀⠀⠀⠀⠀⠀⠀⣿⣿... ╚═╝ ╚═╝ ╚═╝╚═╝ ╚═╝...
|
|
116
|
-
* row 11 ⠀⠀⠀⠀⠀⠀⠀⠀⢀⣿⣿... blockrun.ai · The AI agent with a wallet · vX
|
|
117
|
-
* row 12 ⠀⠀⠀⠀⠀⠀⠀⠀⠘⣿⣿...
|
|
118
|
-
* row 13-16: neck, collar, body
|
|
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
|
|
119
107
|
*/
|
|
120
108
|
function printSideBySide(version) {
|
|
121
|
-
const TEXT_TOP_OFFSET =
|
|
122
|
-
const PORTRAIT_WIDTH =
|
|
123
|
-
const GAP = ' ';
|
|
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
|
|
111
|
+
const GAP = ' '; // gap between portrait and text
|
|
124
112
|
const portraitRows = BEN_PORTRAIT_ROWS;
|
|
125
113
|
const textRows = FRANKLIN_ART.length;
|
|
126
114
|
const totalRows = Math.max(portraitRows.length, TEXT_TOP_OFFSET + textRows + 2);
|
|
127
|
-
// Tint the braille portrait in dim white for a "pencil portrait" feel.
|
|
128
|
-
// Braille chars carry no colour on their own — chalk wraps them in an
|
|
129
|
-
// ANSI colour sequence at render time.
|
|
130
|
-
const portraitTint = chalk.hex('#E8E8E8');
|
|
131
115
|
for (let i = 0; i < totalRows; i++) {
|
|
132
|
-
const
|
|
133
|
-
?
|
|
116
|
+
const portraitLine = i < portraitRows.length
|
|
117
|
+
? padVisible(portraitRows[i], PORTRAIT_WIDTH)
|
|
134
118
|
: ' '.repeat(PORTRAIT_WIDTH);
|
|
135
|
-
const portraitLine = portraitTint(rawPortraitLine);
|
|
136
119
|
// Text column content
|
|
137
120
|
let textCol = '';
|
|
138
121
|
const textIdx = i - TEXT_TOP_OFFSET;
|
|
@@ -144,21 +127,24 @@ function printSideBySide(version) {
|
|
|
144
127
|
}
|
|
145
128
|
else if (textIdx === textRows) {
|
|
146
129
|
// Tagline row sits right under the FRANKLIN block.
|
|
147
|
-
// The big block-letter FRANKLIN above already says the product
|
|
148
|
-
// — the tagline uses that
|
|
149
|
-
// (blockrun.ai
|
|
150
|
-
//
|
|
130
|
+
// The big block-letter "FRANKLIN" above already says the product
|
|
131
|
+
// name — the tagline uses that real estate for the parent brand URL
|
|
132
|
+
// (blockrun.ai, which is a real live domain — unlike franklin.run
|
|
133
|
+
// which we own but haven't deployed yet, see v3.1.0 changelog).
|
|
151
134
|
textCol =
|
|
152
135
|
chalk.bold.hex(GOLD_START)(' blockrun.ai') +
|
|
153
136
|
chalk.dim(' · The AI agent with a wallet · v' + version);
|
|
154
137
|
}
|
|
155
|
-
|
|
138
|
+
// Write with a reset at the very start to prevent stray bg from the
|
|
139
|
+
// previous line bleeding into the current row's portrait column.
|
|
140
|
+
process.stdout.write('\x1b[0m' + portraitLine + GAP + textCol + '\x1b[0m\n');
|
|
156
141
|
}
|
|
142
|
+
// Trailing blank line for breathing room
|
|
157
143
|
process.stdout.write('\n');
|
|
158
144
|
}
|
|
159
145
|
/**
|
|
160
146
|
* Compact layout for narrow terminals: just the FRANKLIN text block with
|
|
161
|
-
* its gradient, no portrait.
|
|
147
|
+
* its gradient, no portrait. Matches the v3.1.0 banner exactly.
|
|
162
148
|
*/
|
|
163
149
|
function printTextOnly(version) {
|
|
164
150
|
const textRows = FRANKLIN_ART.length;
|
package/dist/commands/start.js
CHANGED
|
@@ -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
|
-
|
|
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,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>;
|