@blockrun/franklin 3.2.4 → 3.3.1
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/README.md +216 -233
- package/dist/agent/commands.js +54 -13
- package/dist/agent/context.js +31 -1
- package/dist/agent/loop.js +48 -19
- package/dist/agent/permissions.js +3 -3
- package/dist/commands/migrate.d.ts +13 -0
- package/dist/commands/migrate.js +389 -0
- package/dist/commands/panel.d.ts +6 -0
- package/dist/commands/panel.js +29 -0
- package/dist/commands/start.js +41 -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/index.js +15 -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/mcp/client.js +9 -2
- package/dist/narrative/state.d.ts +30 -0
- package/dist/narrative/state.js +69 -0
- package/dist/panel/html.d.ts +5 -0
- package/dist/panel/html.js +341 -0
- package/dist/panel/server.d.ts +7 -0
- package/dist/panel/server.js +152 -0
- package/dist/session/storage.js +4 -2
- 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/stats/tracker.d.ts +1 -0
- package/dist/stats/tracker.js +59 -13
- package/dist/tools/bash.js +6 -1
- package/dist/tools/index.js +3 -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/tools/webfetch.js +19 -9
- package/dist/tools/write.js +2 -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/dist/ui/app.js +73 -44
- package/dist/ui/markdown.d.ts +9 -0
- package/dist/ui/markdown.js +86 -0
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -64,6 +64,14 @@ program
|
|
|
64
64
|
.description('Manage runcode background proxy (start|stop|status)')
|
|
65
65
|
.option('-p, --port <port>', 'Proxy port', '8402')
|
|
66
66
|
.action((action, options) => daemonCommand(action, options));
|
|
67
|
+
program
|
|
68
|
+
.command('panel')
|
|
69
|
+
.description('Open the Franklin dashboard (localhost:3100)')
|
|
70
|
+
.option('-p, --port <port>', 'Dashboard port', '3100')
|
|
71
|
+
.action(async (options) => {
|
|
72
|
+
const { panelCommand } = await import('./commands/panel.js');
|
|
73
|
+
await panelCommand(options);
|
|
74
|
+
});
|
|
67
75
|
program
|
|
68
76
|
.command('models')
|
|
69
77
|
.description('List available models and pricing')
|
|
@@ -149,6 +157,13 @@ program
|
|
|
149
157
|
});
|
|
150
158
|
}
|
|
151
159
|
}
|
|
160
|
+
program
|
|
161
|
+
.command('migrate')
|
|
162
|
+
.description('Import data from other AI tools (Claude Code, Cline, Cursor)')
|
|
163
|
+
.action(async () => {
|
|
164
|
+
const { migrateCommand } = await import('./commands/migrate.js');
|
|
165
|
+
await migrateCommand();
|
|
166
|
+
});
|
|
152
167
|
program
|
|
153
168
|
.command('plugins')
|
|
154
169
|
.description('List installed plugins')
|
|
@@ -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>;
|
|
@@ -0,0 +1,234 @@
|
|
|
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 fs from 'node:fs';
|
|
6
|
+
import path from 'node:path';
|
|
7
|
+
import os from 'node:os';
|
|
8
|
+
import { loadLearnings, mergeLearning, saveLearnings } from './store.js';
|
|
9
|
+
// Cheapest models that reliably output structured JSON
|
|
10
|
+
const EXTRACTION_MODELS = [
|
|
11
|
+
'google/gemini-2.5-flash-lite',
|
|
12
|
+
'google/gemini-2.5-flash',
|
|
13
|
+
'nvidia/nemotron-super-49b',
|
|
14
|
+
];
|
|
15
|
+
const VALID_CATEGORIES = new Set([
|
|
16
|
+
'language', 'model_preference', 'tool_pattern', 'coding_style',
|
|
17
|
+
'communication', 'domain', 'correction', 'workflow', 'other',
|
|
18
|
+
]);
|
|
19
|
+
const EXTRACTION_PROMPT = `You are analyzing a conversation between a user and an AI coding agent. Extract user preferences and behavioral patterns that would help personalize future interactions.
|
|
20
|
+
|
|
21
|
+
Analyze for:
|
|
22
|
+
1. Language — what language does the user write in? (English, Chinese, mixed?)
|
|
23
|
+
2. Model preferences — did they switch models or express a preference?
|
|
24
|
+
3. Coding style — did they correct the agent's code style? (naming, formatting, conventions)
|
|
25
|
+
4. Communication — are they terse or verbose? Do they want explanations or just code?
|
|
26
|
+
5. Domain — what tech stack, frameworks, or project type?
|
|
27
|
+
6. Corrections — did they repeatedly correct the same agent behavior?
|
|
28
|
+
7. Workflow — do they prefer short tasks or long planning sessions?
|
|
29
|
+
|
|
30
|
+
Rules:
|
|
31
|
+
- ONLY extract signals clearly supported by evidence in the conversation.
|
|
32
|
+
- Do NOT speculate. If evidence is weak, set confidence below 0.5.
|
|
33
|
+
- If the conversation is too short or generic, return an empty array.
|
|
34
|
+
- Each learning should be one clear, actionable sentence.
|
|
35
|
+
|
|
36
|
+
Respond with ONLY a JSON object (no markdown fences, no commentary):
|
|
37
|
+
{"learnings":[{"learning":"...","category":"language|model_preference|tool_pattern|coding_style|communication|domain|correction|workflow|other","confidence":0.5}]}`;
|
|
38
|
+
/**
|
|
39
|
+
* Condense session history into a compact text for extraction.
|
|
40
|
+
* Only includes user messages and assistant text — skips tool calls/results.
|
|
41
|
+
*/
|
|
42
|
+
function condenseHistory(history) {
|
|
43
|
+
const parts = [];
|
|
44
|
+
let chars = 0;
|
|
45
|
+
const CAP = 4000;
|
|
46
|
+
for (const msg of history) {
|
|
47
|
+
if (chars >= CAP)
|
|
48
|
+
break;
|
|
49
|
+
const role = msg.role === 'user' ? 'User' : 'Assistant';
|
|
50
|
+
let text = '';
|
|
51
|
+
if (typeof msg.content === 'string') {
|
|
52
|
+
text = msg.content;
|
|
53
|
+
}
|
|
54
|
+
else if (Array.isArray(msg.content)) {
|
|
55
|
+
text = msg.content
|
|
56
|
+
.filter(p => p.type === 'text')
|
|
57
|
+
.map(p => p.text)
|
|
58
|
+
.join('\n');
|
|
59
|
+
}
|
|
60
|
+
if (!text.trim())
|
|
61
|
+
continue;
|
|
62
|
+
// Truncate long messages
|
|
63
|
+
if (text.length > 500)
|
|
64
|
+
text = text.slice(0, 500) + '…';
|
|
65
|
+
const line = `${role}: ${text}`;
|
|
66
|
+
parts.push(line);
|
|
67
|
+
chars += line.length;
|
|
68
|
+
}
|
|
69
|
+
return parts.join('\n\n');
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Parse JSON from LLM response, handling common quirks
|
|
73
|
+
* (markdown fences, trailing commas, commentary).
|
|
74
|
+
*/
|
|
75
|
+
function parseExtraction(raw) {
|
|
76
|
+
// Strip markdown fences
|
|
77
|
+
let cleaned = raw.replace(/```json\s*/gi, '').replace(/```\s*/g, '').trim();
|
|
78
|
+
// Find the JSON object
|
|
79
|
+
const start = cleaned.indexOf('{');
|
|
80
|
+
const end = cleaned.lastIndexOf('}');
|
|
81
|
+
if (start === -1 || end === -1)
|
|
82
|
+
return { learnings: [] };
|
|
83
|
+
cleaned = cleaned.slice(start, end + 1);
|
|
84
|
+
const parsed = JSON.parse(cleaned);
|
|
85
|
+
if (!Array.isArray(parsed.learnings))
|
|
86
|
+
return { learnings: [] };
|
|
87
|
+
// Validate and sanitize each entry
|
|
88
|
+
return {
|
|
89
|
+
learnings: parsed.learnings
|
|
90
|
+
.filter((l) => typeof l.learning === 'string' &&
|
|
91
|
+
typeof l.category === 'string' &&
|
|
92
|
+
VALID_CATEGORIES.has(l.category) &&
|
|
93
|
+
typeof l.confidence === 'number' &&
|
|
94
|
+
l.confidence >= 0.1 && l.confidence <= 1.0 &&
|
|
95
|
+
l.learning.length > 5)
|
|
96
|
+
.map((l) => ({
|
|
97
|
+
learning: l.learning.slice(0, 200),
|
|
98
|
+
category: l.category,
|
|
99
|
+
confidence: Math.round(l.confidence * 100) / 100,
|
|
100
|
+
})),
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
// ─── Onboarding: bootstrap from Claude Code config ───────────────────────
|
|
104
|
+
const BOOTSTRAP_PROMPT = `You are analyzing a user's AI coding agent configuration file (CLAUDE.md). Extract user preferences that would help personalize a different AI agent's behavior.
|
|
105
|
+
|
|
106
|
+
Analyze for:
|
|
107
|
+
1. Language — what language do they communicate in?
|
|
108
|
+
2. Coding style — naming conventions, formatting, lint rules, type annotations?
|
|
109
|
+
3. Communication — how do they want the agent to behave? (terse? formal? call them something?)
|
|
110
|
+
4. Domain — what tech stack, frameworks, languages do they work with?
|
|
111
|
+
5. Workflow — any specific git, commit, or deployment preferences?
|
|
112
|
+
6. Corrections — any explicit "do NOT" rules or anti-patterns?
|
|
113
|
+
7. Other — any other clear preferences?
|
|
114
|
+
|
|
115
|
+
Rules:
|
|
116
|
+
- Extract EVERY explicit preference. These are user-written rules, so confidence is high (0.8-1.0).
|
|
117
|
+
- Each learning should be one clear, actionable sentence.
|
|
118
|
+
- Do NOT include project-specific paths or secrets.
|
|
119
|
+
- Do NOT include things that are tool-specific to Claude Code and wouldn't apply to another agent.
|
|
120
|
+
|
|
121
|
+
Respond with ONLY a JSON object (no markdown fences, no commentary):
|
|
122
|
+
{"learnings":[{"learning":"...","category":"language|model_preference|tool_pattern|coding_style|communication|domain|correction|workflow|other","confidence":0.9}]}`;
|
|
123
|
+
/**
|
|
124
|
+
* Scan for Claude Code configuration and bootstrap learnings from it.
|
|
125
|
+
* Only runs once — skips if learnings already exist.
|
|
126
|
+
*/
|
|
127
|
+
export async function bootstrapFromClaudeConfig(client) {
|
|
128
|
+
// Only bootstrap if no learnings exist yet (first run)
|
|
129
|
+
const existing = loadLearnings();
|
|
130
|
+
if (existing.length > 0)
|
|
131
|
+
return 0;
|
|
132
|
+
// Scan for Claude Code config files
|
|
133
|
+
const configPaths = [
|
|
134
|
+
path.join(os.homedir(), '.claude', 'CLAUDE.md'),
|
|
135
|
+
path.join(process.cwd(), 'CLAUDE.md'),
|
|
136
|
+
path.join(process.cwd(), '.claude', 'CLAUDE.md'),
|
|
137
|
+
];
|
|
138
|
+
const contents = [];
|
|
139
|
+
for (const p of configPaths) {
|
|
140
|
+
try {
|
|
141
|
+
const text = fs.readFileSync(p, 'utf-8').trim();
|
|
142
|
+
if (text && text.length > 20) {
|
|
143
|
+
contents.push(`--- ${p} ---\n${text}`);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
catch { /* file doesn't exist */ }
|
|
147
|
+
}
|
|
148
|
+
if (contents.length === 0)
|
|
149
|
+
return 0;
|
|
150
|
+
// Cap total content
|
|
151
|
+
let combined = contents.join('\n\n');
|
|
152
|
+
if (combined.length > 6000)
|
|
153
|
+
combined = combined.slice(0, 6000) + '\n…(truncated)';
|
|
154
|
+
// Extract learnings
|
|
155
|
+
let result = null;
|
|
156
|
+
for (const model of EXTRACTION_MODELS) {
|
|
157
|
+
try {
|
|
158
|
+
const response = await client.complete({
|
|
159
|
+
model,
|
|
160
|
+
messages: [{ role: 'user', content: combined }],
|
|
161
|
+
system: BOOTSTRAP_PROMPT,
|
|
162
|
+
max_tokens: 1500,
|
|
163
|
+
temperature: 0.2,
|
|
164
|
+
});
|
|
165
|
+
const text = response.content
|
|
166
|
+
.filter((p) => p.type === 'text')
|
|
167
|
+
.map((p) => p.text)
|
|
168
|
+
.join('');
|
|
169
|
+
result = parseExtraction(text);
|
|
170
|
+
break;
|
|
171
|
+
}
|
|
172
|
+
catch {
|
|
173
|
+
continue;
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
if (!result || result.learnings.length === 0)
|
|
177
|
+
return 0;
|
|
178
|
+
// Save all bootstrapped learnings
|
|
179
|
+
let learnings = loadLearnings();
|
|
180
|
+
for (const entry of result.learnings) {
|
|
181
|
+
learnings = mergeLearning(learnings, {
|
|
182
|
+
...entry,
|
|
183
|
+
source_session: 'bootstrap:claude-config',
|
|
184
|
+
});
|
|
185
|
+
}
|
|
186
|
+
saveLearnings(learnings);
|
|
187
|
+
return result.learnings.length;
|
|
188
|
+
}
|
|
189
|
+
// ─── Session extraction ──────────────────────────────────────────────────
|
|
190
|
+
/**
|
|
191
|
+
* Extract learnings from a completed session.
|
|
192
|
+
* Runs asynchronously — caller should fire-and-forget.
|
|
193
|
+
*/
|
|
194
|
+
export async function extractLearnings(history, sessionId, client) {
|
|
195
|
+
// Skip very short sessions
|
|
196
|
+
if (history.length < 4)
|
|
197
|
+
return;
|
|
198
|
+
const condensed = condenseHistory(history);
|
|
199
|
+
if (condensed.length < 100)
|
|
200
|
+
return; // Too little content
|
|
201
|
+
// Try each model until one succeeds
|
|
202
|
+
let result = null;
|
|
203
|
+
for (const model of EXTRACTION_MODELS) {
|
|
204
|
+
try {
|
|
205
|
+
const response = await client.complete({
|
|
206
|
+
model,
|
|
207
|
+
messages: [{ role: 'user', content: condensed }],
|
|
208
|
+
system: EXTRACTION_PROMPT,
|
|
209
|
+
max_tokens: 1000,
|
|
210
|
+
temperature: 0.3,
|
|
211
|
+
});
|
|
212
|
+
const text = response.content
|
|
213
|
+
.filter((p) => p.type === 'text')
|
|
214
|
+
.map((p) => p.text)
|
|
215
|
+
.join('');
|
|
216
|
+
result = parseExtraction(text);
|
|
217
|
+
break;
|
|
218
|
+
}
|
|
219
|
+
catch {
|
|
220
|
+
continue; // Try next model
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
if (!result || result.learnings.length === 0)
|
|
224
|
+
return;
|
|
225
|
+
// Merge with existing learnings
|
|
226
|
+
let existing = loadLearnings();
|
|
227
|
+
for (const entry of result.learnings) {
|
|
228
|
+
existing = mergeLearning(existing, {
|
|
229
|
+
...entry,
|
|
230
|
+
source_session: sessionId,
|
|
231
|
+
});
|
|
232
|
+
}
|
|
233
|
+
saveLearnings(existing);
|
|
234
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Persistence layer for per-user learnings.
|
|
3
|
+
* Stored as JSONL at ~/.blockrun/learnings.jsonl.
|
|
4
|
+
*/
|
|
5
|
+
import type { Learning, LearningCategory } from './types.js';
|
|
6
|
+
export declare function loadLearnings(): Learning[];
|
|
7
|
+
export declare function saveLearnings(learnings: Learning[]): void;
|
|
8
|
+
export declare function mergeLearning(existing: Learning[], newEntry: {
|
|
9
|
+
learning: string;
|
|
10
|
+
category: LearningCategory;
|
|
11
|
+
confidence: number;
|
|
12
|
+
source_session: string;
|
|
13
|
+
}): Learning[];
|
|
14
|
+
export declare function decayLearnings(learnings: Learning[]): Learning[];
|
|
15
|
+
export declare function formatForPrompt(learnings: Learning[]): string;
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Persistence layer for per-user learnings.
|
|
3
|
+
* Stored as JSONL at ~/.blockrun/learnings.jsonl.
|
|
4
|
+
*/
|
|
5
|
+
import fs from 'node:fs';
|
|
6
|
+
import path from 'node:path';
|
|
7
|
+
import crypto from 'node:crypto';
|
|
8
|
+
import { BLOCKRUN_DIR } from '../config.js';
|
|
9
|
+
const LEARNINGS_PATH = path.join(BLOCKRUN_DIR, 'learnings.jsonl');
|
|
10
|
+
const MAX_LEARNINGS = 50;
|
|
11
|
+
const DECAY_AFTER_DAYS = 30;
|
|
12
|
+
const DECAY_AMOUNT = 0.15;
|
|
13
|
+
const PRUNE_THRESHOLD = 0.2;
|
|
14
|
+
const MERGE_SIMILARITY = 0.6;
|
|
15
|
+
// ─── Load / Save ──────────────────────────────────────────────────────────
|
|
16
|
+
export function loadLearnings() {
|
|
17
|
+
try {
|
|
18
|
+
const raw = fs.readFileSync(LEARNINGS_PATH, 'utf-8');
|
|
19
|
+
const results = [];
|
|
20
|
+
for (const line of raw.split('\n')) {
|
|
21
|
+
if (!line.trim())
|
|
22
|
+
continue;
|
|
23
|
+
try {
|
|
24
|
+
results.push(JSON.parse(line));
|
|
25
|
+
}
|
|
26
|
+
catch { /* skip corrupted lines */ }
|
|
27
|
+
}
|
|
28
|
+
return results;
|
|
29
|
+
}
|
|
30
|
+
catch {
|
|
31
|
+
return [];
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
export function saveLearnings(learnings) {
|
|
35
|
+
fs.mkdirSync(BLOCKRUN_DIR, { recursive: true });
|
|
36
|
+
const tmpPath = LEARNINGS_PATH + '.tmp';
|
|
37
|
+
const content = learnings.map(l => JSON.stringify(l)).join('\n') + '\n';
|
|
38
|
+
fs.writeFileSync(tmpPath, content);
|
|
39
|
+
fs.renameSync(tmpPath, LEARNINGS_PATH);
|
|
40
|
+
}
|
|
41
|
+
// ─── Merge / Dedup ────────────────────────────────────────────────────────
|
|
42
|
+
function tokenize(text) {
|
|
43
|
+
return new Set(text.toLowerCase().replace(/[^\w\s]/g, '').split(/\s+/).filter(w => w.length > 2));
|
|
44
|
+
}
|
|
45
|
+
function jaccardSimilarity(a, b) {
|
|
46
|
+
if (a.size === 0 && b.size === 0)
|
|
47
|
+
return 1;
|
|
48
|
+
let intersection = 0;
|
|
49
|
+
for (const w of a)
|
|
50
|
+
if (b.has(w))
|
|
51
|
+
intersection++;
|
|
52
|
+
return intersection / (a.size + b.size - intersection);
|
|
53
|
+
}
|
|
54
|
+
export function mergeLearning(existing, newEntry) {
|
|
55
|
+
const now = Date.now();
|
|
56
|
+
const newTokens = tokenize(newEntry.learning);
|
|
57
|
+
// Find similar existing learning in same category
|
|
58
|
+
for (const entry of existing) {
|
|
59
|
+
if (entry.category !== newEntry.category)
|
|
60
|
+
continue;
|
|
61
|
+
const similarity = jaccardSimilarity(tokenize(entry.learning), newTokens);
|
|
62
|
+
if (similarity >= MERGE_SIMILARITY) {
|
|
63
|
+
// Merge: boost confidence, update timestamp
|
|
64
|
+
entry.times_confirmed++;
|
|
65
|
+
entry.last_confirmed = now;
|
|
66
|
+
entry.confidence = Math.min(entry.confidence + 0.1, 1.0);
|
|
67
|
+
// Prefer more specific wording
|
|
68
|
+
if (newEntry.learning.length > entry.learning.length) {
|
|
69
|
+
entry.learning = newEntry.learning;
|
|
70
|
+
}
|
|
71
|
+
return existing;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
// No match — insert new
|
|
75
|
+
existing.push({
|
|
76
|
+
id: crypto.randomBytes(8).toString('hex'),
|
|
77
|
+
learning: newEntry.learning,
|
|
78
|
+
category: newEntry.category,
|
|
79
|
+
confidence: newEntry.confidence,
|
|
80
|
+
source_session: newEntry.source_session,
|
|
81
|
+
created_at: now,
|
|
82
|
+
last_confirmed: now,
|
|
83
|
+
times_confirmed: 1,
|
|
84
|
+
});
|
|
85
|
+
// Cap at MAX_LEARNINGS — drop lowest-scoring
|
|
86
|
+
if (existing.length > MAX_LEARNINGS) {
|
|
87
|
+
existing.sort((a, b) => score(b) - score(a));
|
|
88
|
+
existing.length = MAX_LEARNINGS;
|
|
89
|
+
}
|
|
90
|
+
return existing;
|
|
91
|
+
}
|
|
92
|
+
function score(l) {
|
|
93
|
+
return l.confidence * Math.log2(l.times_confirmed + 1);
|
|
94
|
+
}
|
|
95
|
+
// ─── Decay ────────────────────────────────────────────────────────────────
|
|
96
|
+
export function decayLearnings(learnings) {
|
|
97
|
+
const now = Date.now();
|
|
98
|
+
const cutoff = DECAY_AFTER_DAYS * 24 * 60 * 60 * 1000;
|
|
99
|
+
return learnings.filter(l => {
|
|
100
|
+
if (l.times_confirmed >= 3)
|
|
101
|
+
return true; // Immune to time decay
|
|
102
|
+
if (now - l.last_confirmed > cutoff) {
|
|
103
|
+
l.confidence -= DECAY_AMOUNT;
|
|
104
|
+
return l.confidence >= PRUNE_THRESHOLD;
|
|
105
|
+
}
|
|
106
|
+
return true;
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
// ─── Format for System Prompt ─────────────────────────────────────────────
|
|
110
|
+
const MAX_PROMPT_CHARS = 2000; // ~500 tokens
|
|
111
|
+
export function formatForPrompt(learnings) {
|
|
112
|
+
if (learnings.length === 0)
|
|
113
|
+
return '';
|
|
114
|
+
const sorted = [...learnings].sort((a, b) => score(b) - score(a));
|
|
115
|
+
const lines = [];
|
|
116
|
+
let chars = 0;
|
|
117
|
+
const header = '# Personal Context\nPreferences learned from previous sessions:\n';
|
|
118
|
+
chars += header.length;
|
|
119
|
+
for (const l of sorted) {
|
|
120
|
+
const conf = l.confidence >= 0.8 ? '●' : l.confidence >= 0.5 ? '◐' : '○';
|
|
121
|
+
const line = `- ${conf} ${l.learning}`;
|
|
122
|
+
if (chars + line.length + 1 > MAX_PROMPT_CHARS)
|
|
123
|
+
break;
|
|
124
|
+
lines.push(line);
|
|
125
|
+
chars += line.length + 1;
|
|
126
|
+
}
|
|
127
|
+
if (lines.length === 0)
|
|
128
|
+
return '';
|
|
129
|
+
return header + lines.join('\n');
|
|
130
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Types for Franklin's per-user self-evolution system.
|
|
3
|
+
*
|
|
4
|
+
* Each user's Franklin learns preferences from session traces and
|
|
5
|
+
* injects them into the system prompt on next startup.
|
|
6
|
+
*/
|
|
7
|
+
export interface Learning {
|
|
8
|
+
id: string;
|
|
9
|
+
learning: string;
|
|
10
|
+
category: LearningCategory;
|
|
11
|
+
confidence: number;
|
|
12
|
+
source_session: string;
|
|
13
|
+
created_at: number;
|
|
14
|
+
last_confirmed: number;
|
|
15
|
+
times_confirmed: number;
|
|
16
|
+
}
|
|
17
|
+
export type LearningCategory = 'language' | 'model_preference' | 'tool_pattern' | 'coding_style' | 'communication' | 'domain' | 'correction' | 'workflow' | 'other';
|
|
18
|
+
export interface ExtractionResult {
|
|
19
|
+
learnings: Array<{
|
|
20
|
+
learning: string;
|
|
21
|
+
category: LearningCategory;
|
|
22
|
+
confidence: number;
|
|
23
|
+
}>;
|
|
24
|
+
}
|
package/dist/mcp/client.js
CHANGED
|
@@ -19,6 +19,11 @@ async function connectStdio(name, config) {
|
|
|
19
19
|
command: config.command,
|
|
20
20
|
args: config.args || [],
|
|
21
21
|
env: { ...process.env, ...(config.env || {}) },
|
|
22
|
+
// 'ignore' discards subprocess stderr completely so a misconfigured MCP
|
|
23
|
+
// server (e.g. missing OAuth keys) can't dump multi-line stack traces
|
|
24
|
+
// into the user's terminal. 'pipe' didn't fully work because some SDK
|
|
25
|
+
// versions read piped stderr and re-emit it.
|
|
26
|
+
stderr: 'ignore',
|
|
22
27
|
});
|
|
23
28
|
const client = new Client({ name: `runcode-mcp-${name}`, version: '1.0.0' }, { capabilities: {} });
|
|
24
29
|
try {
|
|
@@ -111,8 +116,10 @@ export async function connectMcpServers(config, debug) {
|
|
|
111
116
|
}
|
|
112
117
|
}
|
|
113
118
|
catch (err) {
|
|
114
|
-
// Graceful degradation —
|
|
115
|
-
|
|
119
|
+
// Graceful degradation — one-line warning, continue without this server.
|
|
120
|
+
// Always visible (not debug-only) so the user knows why tools are missing.
|
|
121
|
+
const shortMsg = err.message?.split('\n')[0]?.slice(0, 100) || 'unknown error';
|
|
122
|
+
console.error(` ${name}: ${shortMsg} ${debug ? '' : '(--debug for details)'}`);
|
|
116
123
|
}
|
|
117
124
|
}
|
|
118
125
|
return allTools;
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
export interface SignalRecord {
|
|
2
|
+
asset: string;
|
|
3
|
+
direction: 'bullish' | 'bearish' | 'neutral';
|
|
4
|
+
confidence: number;
|
|
5
|
+
summary: string;
|
|
6
|
+
ts: string;
|
|
7
|
+
}
|
|
8
|
+
export interface PostRecord {
|
|
9
|
+
platform: string;
|
|
10
|
+
url: string;
|
|
11
|
+
text: string;
|
|
12
|
+
referencesAssets?: string[];
|
|
13
|
+
ts: string;
|
|
14
|
+
}
|
|
15
|
+
export interface BudgetEnvelope {
|
|
16
|
+
dailyCapUsd: number;
|
|
17
|
+
spentTodayUsd: number;
|
|
18
|
+
date: string;
|
|
19
|
+
}
|
|
20
|
+
export interface NarrativeState {
|
|
21
|
+
watchlist: string[];
|
|
22
|
+
recentSignals: SignalRecord[];
|
|
23
|
+
recentPosts: PostRecord[];
|
|
24
|
+
budget: BudgetEnvelope;
|
|
25
|
+
}
|
|
26
|
+
export declare function loadNarrative(): NarrativeState;
|
|
27
|
+
export declare function saveNarrative(s: NarrativeState): void;
|
|
28
|
+
export declare function updateNarrative(patch: Partial<NarrativeState>): NarrativeState;
|
|
29
|
+
export declare function addSignal(signal: SignalRecord): void;
|
|
30
|
+
export declare function addPost(post: PostRecord): void;
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import fs from 'node:fs';
|
|
3
|
+
import os from 'node:os';
|
|
4
|
+
const STORE_DIR = path.join(os.homedir(), '.blockrun');
|
|
5
|
+
const STATE_PATH = path.join(STORE_DIR, 'narrative.json');
|
|
6
|
+
const MAX_ENTRIES = 50;
|
|
7
|
+
let loaded = false;
|
|
8
|
+
let state;
|
|
9
|
+
function today() {
|
|
10
|
+
return new Date().toISOString().slice(0, 10);
|
|
11
|
+
}
|
|
12
|
+
function defaults() {
|
|
13
|
+
return {
|
|
14
|
+
watchlist: [],
|
|
15
|
+
recentSignals: [],
|
|
16
|
+
recentPosts: [],
|
|
17
|
+
budget: { dailyCapUsd: 10, spentTodayUsd: 0, date: today() },
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
export function loadNarrative() {
|
|
21
|
+
if (loaded)
|
|
22
|
+
return state;
|
|
23
|
+
fs.mkdirSync(STORE_DIR, { recursive: true });
|
|
24
|
+
if (fs.existsSync(STATE_PATH)) {
|
|
25
|
+
try {
|
|
26
|
+
state = JSON.parse(fs.readFileSync(STATE_PATH, 'utf8'));
|
|
27
|
+
}
|
|
28
|
+
catch {
|
|
29
|
+
state = defaults();
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
else {
|
|
33
|
+
state = defaults();
|
|
34
|
+
}
|
|
35
|
+
if (state.budget.date !== today()) {
|
|
36
|
+
state.budget.spentTodayUsd = 0;
|
|
37
|
+
state.budget.date = today();
|
|
38
|
+
}
|
|
39
|
+
loaded = true;
|
|
40
|
+
return state;
|
|
41
|
+
}
|
|
42
|
+
export function saveNarrative(s) {
|
|
43
|
+
fs.mkdirSync(STORE_DIR, { recursive: true });
|
|
44
|
+
fs.writeFileSync(STATE_PATH, JSON.stringify(s, null, 2) + '\n');
|
|
45
|
+
state = s;
|
|
46
|
+
loaded = true;
|
|
47
|
+
}
|
|
48
|
+
export function updateNarrative(patch) {
|
|
49
|
+
const cur = loadNarrative();
|
|
50
|
+
const merged = { ...cur, ...patch };
|
|
51
|
+
if (patch.recentSignals) {
|
|
52
|
+
merged.recentSignals = [...patch.recentSignals, ...cur.recentSignals].slice(0, MAX_ENTRIES);
|
|
53
|
+
}
|
|
54
|
+
if (patch.recentPosts) {
|
|
55
|
+
merged.recentPosts = [...patch.recentPosts, ...cur.recentPosts].slice(0, MAX_ENTRIES);
|
|
56
|
+
}
|
|
57
|
+
saveNarrative(merged);
|
|
58
|
+
return merged;
|
|
59
|
+
}
|
|
60
|
+
export function addSignal(signal) {
|
|
61
|
+
const cur = loadNarrative();
|
|
62
|
+
cur.recentSignals = [signal, ...cur.recentSignals].slice(0, MAX_ENTRIES);
|
|
63
|
+
saveNarrative(cur);
|
|
64
|
+
}
|
|
65
|
+
export function addPost(post) {
|
|
66
|
+
const cur = loadNarrative();
|
|
67
|
+
cur.recentPosts = [post, ...cur.recentPosts].slice(0, MAX_ENTRIES);
|
|
68
|
+
saveNarrative(cur);
|
|
69
|
+
}
|