@blockrun/franklin 3.3.3 → 3.5.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/README.md +55 -4
- package/dist/agent/commands.d.ts +1 -1
- package/dist/agent/commands.js +128 -17
- package/dist/agent/compact.d.ts +2 -2
- package/dist/agent/compact.js +148 -22
- package/dist/agent/context.d.ts +8 -3
- package/dist/agent/context.js +301 -108
- package/dist/agent/error-classifier.d.ts +11 -2
- package/dist/agent/error-classifier.js +64 -10
- package/dist/agent/llm.d.ts +8 -1
- package/dist/agent/llm.js +114 -19
- package/dist/agent/loop.d.ts +1 -2
- package/dist/agent/loop.js +509 -61
- package/dist/agent/optimize.d.ts +2 -2
- package/dist/agent/optimize.js +9 -7
- package/dist/agent/permissions.d.ts +1 -1
- package/dist/agent/permissions.js +1 -1
- package/dist/agent/planner.d.ts +42 -0
- package/dist/agent/planner.js +110 -0
- package/dist/agent/reduce.d.ts +7 -1
- package/dist/agent/reduce.js +85 -3
- package/dist/agent/streaming-executor.d.ts +6 -1
- package/dist/agent/streaming-executor.js +83 -5
- package/dist/agent/tokens.d.ts +11 -2
- package/dist/agent/tokens.js +38 -5
- package/dist/agent/tool-guard.d.ts +27 -0
- package/dist/agent/tool-guard.js +324 -0
- package/dist/agent/types.d.ts +7 -1
- package/dist/agent/types.js +1 -1
- package/dist/brain/extract.d.ts +11 -0
- package/dist/brain/extract.js +154 -0
- package/dist/brain/index.d.ts +3 -0
- package/dist/brain/index.js +2 -0
- package/dist/brain/store.d.ts +42 -0
- package/dist/brain/store.js +225 -0
- package/dist/brain/types.d.ts +45 -0
- package/dist/brain/types.js +5 -0
- package/dist/commands/daemon.js +2 -1
- package/dist/commands/start.js +16 -3
- package/dist/config.js +1 -1
- package/dist/index.js +27 -2
- package/dist/learnings/extractor.d.ts +13 -0
- package/dist/learnings/extractor.js +69 -8
- package/dist/learnings/index.d.ts +1 -1
- package/dist/learnings/index.js +1 -1
- package/dist/learnings/store.js +42 -13
- package/dist/learnings/types.d.ts +1 -1
- package/dist/mcp/client.d.ts +1 -1
- package/dist/mcp/client.js +5 -5
- package/dist/mcp/config.d.ts +1 -1
- package/dist/mcp/config.js +1 -1
- package/dist/panel/html.d.ts +2 -0
- package/dist/panel/html.js +409 -146
- package/dist/panel/server.js +19 -0
- package/dist/pricing.js +3 -2
- package/dist/proxy/fallback.d.ts +3 -1
- package/dist/proxy/fallback.js +4 -4
- package/dist/proxy/server.js +29 -11
- package/dist/proxy/sse-translator.js +1 -1
- package/dist/router/categories.d.ts +21 -0
- package/dist/router/categories.js +96 -0
- package/dist/router/index.d.ts +9 -2
- package/dist/router/index.js +106 -27
- package/dist/router/local-elo.d.ts +32 -0
- package/dist/router/local-elo.js +107 -0
- package/dist/router/selector.d.ts +46 -0
- package/dist/router/selector.js +106 -0
- package/dist/session/storage.d.ts +5 -1
- package/dist/session/storage.js +24 -2
- package/dist/social/a11y.d.ts +1 -1
- package/dist/social/a11y.js +5 -1
- package/dist/social/browser.d.ts +5 -0
- package/dist/social/browser.js +22 -0
- package/dist/social/preflight.d.ts +4 -0
- package/dist/social/preflight.js +42 -3
- package/dist/stats/failures.d.ts +20 -0
- package/dist/stats/failures.js +63 -0
- package/dist/stats/format.d.ts +6 -0
- package/dist/stats/format.js +23 -0
- package/dist/stats/insights.js +1 -21
- package/dist/stats/session-tracker.d.ts +21 -0
- package/dist/stats/session-tracker.js +28 -0
- package/dist/stats/tracker.d.ts +1 -1
- package/dist/stats/tracker.js +1 -1
- package/dist/tools/bash.d.ts +14 -1
- package/dist/tools/bash.js +132 -7
- package/dist/tools/edit.js +77 -14
- package/dist/tools/glob.js +13 -3
- package/dist/tools/grep.js +30 -12
- package/dist/tools/imagegen.js +3 -3
- package/dist/tools/index.d.ts +1 -1
- package/dist/tools/index.js +5 -1
- package/dist/tools/read.d.ts +16 -2
- package/dist/tools/read.js +36 -8
- package/dist/tools/searchx.d.ts +6 -2
- package/dist/tools/searchx.js +221 -44
- package/dist/tools/subagent.js +37 -3
- package/dist/tools/task.js +43 -7
- package/dist/tools/validate.d.ts +11 -0
- package/dist/tools/validate.js +42 -0
- package/dist/tools/webfetch.js +18 -7
- package/dist/tools/websearch.js +41 -7
- package/dist/tools/write.js +26 -6
- package/dist/ui/app.js +31 -6
- package/dist/ui/model-picker.d.ts +1 -1
- package/dist/ui/model-picker.js +1 -1
- package/dist/ui/terminal.d.ts +1 -1
- package/dist/ui/terminal.js +1 -1
- package/package.json +2 -2
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Model selector for the learned router.
|
|
3
|
+
*
|
|
4
|
+
* Scoring formula (4 factors):
|
|
5
|
+
* score = w_quality * norm_quality
|
|
6
|
+
* + w_cost * (1 - norm_cost)
|
|
7
|
+
* + w_latency * (1 - norm_latency)
|
|
8
|
+
* + w_efficiency * norm_efficiency
|
|
9
|
+
*
|
|
10
|
+
* Efficiency = how few tool calls a model needs to complete a task.
|
|
11
|
+
* A model that does it in 5 calls is better than one that loops 85 times.
|
|
12
|
+
* Measured as 1/avg_tool_calls_per_turn (higher = more efficient).
|
|
13
|
+
*
|
|
14
|
+
* Profile weights:
|
|
15
|
+
* auto — balanced: quality 0.2, cost 0.3, latency 0.25, efficiency 0.25
|
|
16
|
+
* eco — cost-first: quality 0.1, cost 0.6, latency 0.15, efficiency 0.15
|
|
17
|
+
* premium — quality-first: quality 0.4, cost 0.1, latency 0.25, efficiency 0.25
|
|
18
|
+
* free — best efficiency among free models
|
|
19
|
+
*/
|
|
20
|
+
import type { Category } from './categories.js';
|
|
21
|
+
import type { RoutingProfile } from './index.js';
|
|
22
|
+
export interface ModelScore {
|
|
23
|
+
model: string;
|
|
24
|
+
elo: number;
|
|
25
|
+
avg_cost_per_1k?: number;
|
|
26
|
+
avg_latency_ms?: number;
|
|
27
|
+
avg_tool_calls_per_turn?: number;
|
|
28
|
+
requests?: number;
|
|
29
|
+
unique_users?: number;
|
|
30
|
+
}
|
|
31
|
+
export interface LearnedWeights {
|
|
32
|
+
version: number;
|
|
33
|
+
trained_on: number;
|
|
34
|
+
trained_at: string;
|
|
35
|
+
categories: string[];
|
|
36
|
+
category_keywords?: Record<string, string[]>;
|
|
37
|
+
model_scores: Record<string, ModelScore[]>;
|
|
38
|
+
}
|
|
39
|
+
export interface SelectionResult {
|
|
40
|
+
model: string;
|
|
41
|
+
score: number;
|
|
42
|
+
expectedCost: number;
|
|
43
|
+
expectedLatency: number;
|
|
44
|
+
category: Category;
|
|
45
|
+
}
|
|
46
|
+
export declare function selectModel(category: Category, profile: RoutingProfile, weights: LearnedWeights): SelectionResult | null;
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Model selector for the learned router.
|
|
3
|
+
*
|
|
4
|
+
* Scoring formula (4 factors):
|
|
5
|
+
* score = w_quality * norm_quality
|
|
6
|
+
* + w_cost * (1 - norm_cost)
|
|
7
|
+
* + w_latency * (1 - norm_latency)
|
|
8
|
+
* + w_efficiency * norm_efficiency
|
|
9
|
+
*
|
|
10
|
+
* Efficiency = how few tool calls a model needs to complete a task.
|
|
11
|
+
* A model that does it in 5 calls is better than one that loops 85 times.
|
|
12
|
+
* Measured as 1/avg_tool_calls_per_turn (higher = more efficient).
|
|
13
|
+
*
|
|
14
|
+
* Profile weights:
|
|
15
|
+
* auto — balanced: quality 0.2, cost 0.3, latency 0.25, efficiency 0.25
|
|
16
|
+
* eco — cost-first: quality 0.1, cost 0.6, latency 0.15, efficiency 0.15
|
|
17
|
+
* premium — quality-first: quality 0.4, cost 0.1, latency 0.25, efficiency 0.25
|
|
18
|
+
* free — best efficiency among free models
|
|
19
|
+
*/
|
|
20
|
+
import { MODEL_PRICING } from '../pricing.js';
|
|
21
|
+
const PROFILE_WEIGHTS = {
|
|
22
|
+
auto: { quality: 0.20, cost: 0.30, latency: 0.25, efficiency: 0.25 },
|
|
23
|
+
eco: { quality: 0.10, cost: 0.60, latency: 0.15, efficiency: 0.15 },
|
|
24
|
+
premium: { quality: 0.40, cost: 0.10, latency: 0.25, efficiency: 0.25 },
|
|
25
|
+
};
|
|
26
|
+
export function selectModel(category, profile, weights) {
|
|
27
|
+
const candidates = weights.model_scores[category];
|
|
28
|
+
if (!candidates || candidates.length === 0)
|
|
29
|
+
return null;
|
|
30
|
+
// Enrich with pricing data and defaults
|
|
31
|
+
const enriched = candidates.map(c => {
|
|
32
|
+
const pricing = MODEL_PRICING[c.model];
|
|
33
|
+
const costPer1K = pricing
|
|
34
|
+
? (pricing.input + pricing.output) / 2 / 1000
|
|
35
|
+
: c.avg_cost_per_1k ?? 0.005;
|
|
36
|
+
const latencyMs = c.avg_latency_ms ?? 2000;
|
|
37
|
+
// Efficiency: 1/avg_tool_calls (higher = better). Default 10 calls/turn if unknown.
|
|
38
|
+
const toolCallsPerTurn = c.avg_tool_calls_per_turn ?? 10;
|
|
39
|
+
const efficiency = 1 / Math.max(1, toolCallsPerTurn);
|
|
40
|
+
return { ...c, costPer1K, latencyMs, efficiency };
|
|
41
|
+
});
|
|
42
|
+
// Filter to models we can actually route to
|
|
43
|
+
const available = enriched.filter(c => MODEL_PRICING[c.model]);
|
|
44
|
+
if (available.length === 0)
|
|
45
|
+
return null;
|
|
46
|
+
// ── Free profile: best efficiency + latency among free models ──
|
|
47
|
+
if (profile === 'free') {
|
|
48
|
+
const free = available.filter(c => c.costPer1K === 0);
|
|
49
|
+
if (free.length === 0)
|
|
50
|
+
return null;
|
|
51
|
+
// Score free models by efficiency (60%) + latency (40%)
|
|
52
|
+
const maxLat = Math.max(...free.map(c => c.latencyMs)) || 1;
|
|
53
|
+
const maxEff = Math.max(...free.map(c => c.efficiency)) || 1;
|
|
54
|
+
const selected = free.reduce((best, c) => {
|
|
55
|
+
const s = 0.6 * (c.efficiency / maxEff) + 0.4 * (1 - c.latencyMs / maxLat);
|
|
56
|
+
const bestS = 0.6 * (best.efficiency / maxEff) + 0.4 * (1 - best.latencyMs / maxLat);
|
|
57
|
+
return s > bestS ? c : best;
|
|
58
|
+
});
|
|
59
|
+
return {
|
|
60
|
+
model: selected.model,
|
|
61
|
+
score: selected.efficiency,
|
|
62
|
+
expectedCost: 0,
|
|
63
|
+
expectedLatency: selected.latencyMs,
|
|
64
|
+
category,
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
// ── Scored profiles: auto / eco / premium ──
|
|
68
|
+
const w = PROFILE_WEIGHTS[profile] ?? PROFILE_WEIGHTS.auto;
|
|
69
|
+
// Compute normalization bounds
|
|
70
|
+
const elos = available.map(c => c.elo);
|
|
71
|
+
const costs = available.map(c => c.costPer1K);
|
|
72
|
+
const latencies = available.map(c => c.latencyMs);
|
|
73
|
+
const efficiencies = available.map(c => c.efficiency);
|
|
74
|
+
const minElo = Math.min(...elos);
|
|
75
|
+
const maxElo = Math.max(...elos);
|
|
76
|
+
const maxCost = Math.max(...costs);
|
|
77
|
+
const maxLatency = Math.max(...latencies);
|
|
78
|
+
const maxEfficiency = Math.max(...efficiencies);
|
|
79
|
+
const eloRange = maxElo - minElo || 1;
|
|
80
|
+
const costRange = maxCost || 1;
|
|
81
|
+
const latencyRange = maxLatency || 1;
|
|
82
|
+
const efficiencyRange = maxEfficiency || 1;
|
|
83
|
+
let bestScore = -Infinity;
|
|
84
|
+
let selected = available[0];
|
|
85
|
+
for (const c of available) {
|
|
86
|
+
const normQuality = (c.elo - minElo) / eloRange;
|
|
87
|
+
const normCost = c.costPer1K / costRange;
|
|
88
|
+
const normLatency = c.latencyMs / latencyRange;
|
|
89
|
+
const normEfficiency = c.efficiency / efficiencyRange;
|
|
90
|
+
const score = w.quality * normQuality +
|
|
91
|
+
w.cost * (1 - normCost) +
|
|
92
|
+
w.latency * (1 - normLatency) +
|
|
93
|
+
w.efficiency * normEfficiency;
|
|
94
|
+
if (score > bestScore) {
|
|
95
|
+
bestScore = score;
|
|
96
|
+
selected = c;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
return {
|
|
100
|
+
model: selected.model,
|
|
101
|
+
score: bestScore,
|
|
102
|
+
expectedCost: selected.costPer1K,
|
|
103
|
+
expectedLatency: selected.latencyMs,
|
|
104
|
+
category,
|
|
105
|
+
};
|
|
106
|
+
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Session persistence for
|
|
2
|
+
* Session persistence for Franklin.
|
|
3
3
|
* Saves conversation history as JSONL for resume capability.
|
|
4
4
|
*/
|
|
5
5
|
import type { Dialogue } from '../agent/types.js';
|
|
@@ -11,6 +11,10 @@ export interface SessionMeta {
|
|
|
11
11
|
updatedAt: number;
|
|
12
12
|
turnCount: number;
|
|
13
13
|
messageCount: number;
|
|
14
|
+
inputTokens?: number;
|
|
15
|
+
outputTokens?: number;
|
|
16
|
+
costUsd?: number;
|
|
17
|
+
savedVsOpusUsd?: number;
|
|
14
18
|
}
|
|
15
19
|
/** Get the absolute path to a session's JSONL file (for external readers like search). */
|
|
16
20
|
export declare function getSessionFilePath(id: string): string;
|
package/dist/session/storage.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Session persistence for
|
|
2
|
+
* Session persistence for Franklin.
|
|
3
3
|
* Saves conversation history as JSONL for resume capability.
|
|
4
4
|
*/
|
|
5
5
|
import fs from 'node:fs';
|
|
@@ -88,6 +88,10 @@ export function updateSessionMeta(sessionId, meta) {
|
|
|
88
88
|
updatedAt: Date.now(),
|
|
89
89
|
turnCount: meta.turnCount ?? existing?.turnCount ?? 0,
|
|
90
90
|
messageCount: meta.messageCount ?? existing?.messageCount ?? 0,
|
|
91
|
+
inputTokens: meta.inputTokens ?? existing?.inputTokens ?? 0,
|
|
92
|
+
outputTokens: meta.outputTokens ?? existing?.outputTokens ?? 0,
|
|
93
|
+
costUsd: meta.costUsd ?? existing?.costUsd ?? 0,
|
|
94
|
+
savedVsOpusUsd: meta.savedVsOpusUsd ?? existing?.savedVsOpusUsd ?? 0,
|
|
91
95
|
};
|
|
92
96
|
fs.writeFileSync(metaPath(sessionId), JSON.stringify(updated, null, 2));
|
|
93
97
|
});
|
|
@@ -142,7 +146,9 @@ export function listSessions() {
|
|
|
142
146
|
}
|
|
143
147
|
catch { /* skip corrupted */ }
|
|
144
148
|
}
|
|
145
|
-
|
|
149
|
+
// Filter out ghost sessions (0 messages)
|
|
150
|
+
const filtered = metas.filter(m => m.messageCount > 0);
|
|
151
|
+
return filtered.sort((a, b) => b.updatedAt - a.updatedAt);
|
|
146
152
|
}
|
|
147
153
|
catch {
|
|
148
154
|
return [];
|
|
@@ -172,4 +178,20 @@ export function pruneOldSessions(activeSessionId) {
|
|
|
172
178
|
}
|
|
173
179
|
catch { /* ok */ }
|
|
174
180
|
}
|
|
181
|
+
// Also clean up ghost sessions (0 messages, older than 5 minutes)
|
|
182
|
+
const fiveMinAgo = Date.now() - 5 * 60 * 1000;
|
|
183
|
+
for (const s of sessions) {
|
|
184
|
+
if (s.id === activeSessionId)
|
|
185
|
+
continue;
|
|
186
|
+
if (s.messageCount === 0 && s.createdAt < fiveMinAgo) {
|
|
187
|
+
try {
|
|
188
|
+
fs.unlinkSync(sessionPath(s.id));
|
|
189
|
+
}
|
|
190
|
+
catch { /* ok */ }
|
|
191
|
+
try {
|
|
192
|
+
fs.unlinkSync(metaPath(s.id));
|
|
193
|
+
}
|
|
194
|
+
catch { /* ok */ }
|
|
195
|
+
}
|
|
196
|
+
}
|
|
175
197
|
}
|
package/dist/social/a11y.d.ts
CHANGED
|
@@ -51,4 +51,4 @@ export declare function extractArticleBlocks(tree: string): Array<{
|
|
|
51
51
|
* This doubles as the "this is a tweet" signal in social-bot — the only link
|
|
52
52
|
* inside an article block with this label shape is the permalink to the tweet.
|
|
53
53
|
*/
|
|
54
|
-
export declare const X_TIME_LINK_PATTERN = "(?:Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec)\\s+\\d
|
|
54
|
+
export declare const X_TIME_LINK_PATTERN = "(?:Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec)\\s+\\d+(?:,?\\s+\\d{4})?|\\d+[smhd]|\\d+\\s+(?:second|minute|hour|day|week|month|year)s?\\s+ago|just now|now|yesterday|\\d{1,2}:\\d{2}\\s*[AaPp][Mm]|\\d{4}\u5E74\\d{1,2}\u6708\\d{1,2}\u65E5";
|
package/dist/social/a11y.js
CHANGED
|
@@ -83,7 +83,11 @@ export function extractArticleBlocks(tree) {
|
|
|
83
83
|
* This doubles as the "this is a tweet" signal in social-bot — the only link
|
|
84
84
|
* inside an article block with this label shape is the permalink to the tweet.
|
|
85
85
|
*/
|
|
86
|
-
|
|
86
|
+
// Matches all known X time-link formats:
|
|
87
|
+
// "Mar 16", "Apr 12, 2026", "5h", "5m", "2d", "30s", "just now", "now"
|
|
88
|
+
// "31 seconds ago", "35 minutes ago", "4 hours ago" (full-word format)
|
|
89
|
+
// "Yesterday", "Apr 12", "12:30 AM", "2026年4月12日" (CJK)
|
|
90
|
+
export const X_TIME_LINK_PATTERN = '(?:Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec)\\s+\\d+(?:,?\\s+\\d{4})?|\\d+[smhd]|\\d+\\s+(?:second|minute|hour|day|week|month|year)s?\\s+ago|just now|now|yesterday|\\d{1,2}:\\d{2}\\s*[AaPp][Mm]|\\d{4}年\\d{1,2}月\\d{1,2}日';
|
|
87
91
|
function escapeRegex(s) {
|
|
88
92
|
return s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
89
93
|
}
|
package/dist/social/browser.d.ts
CHANGED
|
@@ -87,6 +87,11 @@ export declare class SocialBrowser {
|
|
|
87
87
|
getUrl(): Promise<string>;
|
|
88
88
|
getTitle(): Promise<string>;
|
|
89
89
|
waitForTimeout(ms: number): Promise<void>;
|
|
90
|
+
/**
|
|
91
|
+
* Resolve a ref from the last snapshot to its href attribute.
|
|
92
|
+
* Returns the href string, or null if the ref isn't a link or has no href.
|
|
93
|
+
*/
|
|
94
|
+
getHref(ref: string): Promise<string | null>;
|
|
90
95
|
/**
|
|
91
96
|
* Block until the user closes the browser tab (used by the login flow).
|
|
92
97
|
* Resolves when the context is closed.
|
package/dist/social/browser.js
CHANGED
|
@@ -201,6 +201,28 @@ export class SocialBrowser {
|
|
|
201
201
|
this.requirePage();
|
|
202
202
|
await this.page.waitForTimeout(ms);
|
|
203
203
|
}
|
|
204
|
+
/**
|
|
205
|
+
* Resolve a ref from the last snapshot to its href attribute.
|
|
206
|
+
* Returns the href string, or null if the ref isn't a link or has no href.
|
|
207
|
+
*/
|
|
208
|
+
async getHref(ref) {
|
|
209
|
+
this.requirePage();
|
|
210
|
+
const axRef = this.lastRefs.get(ref);
|
|
211
|
+
if (!axRef)
|
|
212
|
+
return null;
|
|
213
|
+
try {
|
|
214
|
+
const el = this.page.locator(axRef.selector).first();
|
|
215
|
+
// Try the element itself, then walk up to find the nearest <a>
|
|
216
|
+
const href = await el.evaluate((node) => {
|
|
217
|
+
const anchor = node.closest('a') || (node.tagName === 'A' ? node : null);
|
|
218
|
+
return anchor ? anchor.href : null;
|
|
219
|
+
});
|
|
220
|
+
return href;
|
|
221
|
+
}
|
|
222
|
+
catch {
|
|
223
|
+
return null;
|
|
224
|
+
}
|
|
225
|
+
}
|
|
204
226
|
/**
|
|
205
227
|
* Block until the user closes the browser tab (used by the login flow).
|
|
206
228
|
* Resolves when the context is closed.
|
|
@@ -6,6 +6,10 @@ import type { SocialBrowser } from './browser.js';
|
|
|
6
6
|
/**
|
|
7
7
|
* Verify that social config is ready and the user is logged in to X.
|
|
8
8
|
* Returns the browser instance on success so callers can reuse it.
|
|
9
|
+
*
|
|
10
|
+
* Login detection order:
|
|
11
|
+
* 1. Check Cookies DB for auth_token (fast, no browser needed)
|
|
12
|
+
* 2. Fallback: open x.com/home and check AX tree for login_detection string
|
|
9
13
|
*/
|
|
10
14
|
export declare function checkSocialReady(): Promise<{
|
|
11
15
|
ready: boolean;
|
package/dist/social/preflight.js
CHANGED
|
@@ -2,11 +2,38 @@
|
|
|
2
2
|
* Pre-flight checks before social tools can run.
|
|
3
3
|
* Validates config readiness and browser login state.
|
|
4
4
|
*/
|
|
5
|
+
import fs from 'node:fs';
|
|
6
|
+
import path from 'node:path';
|
|
5
7
|
import { loadConfig, isConfigReady } from './config.js';
|
|
6
8
|
import { browserPool } from './browser-pool.js';
|
|
9
|
+
import { SOCIAL_PROFILE_DIR } from './browser.js';
|
|
10
|
+
/**
|
|
11
|
+
* Quick cookie check — verify auth_token exists in the profile's Cookies DB.
|
|
12
|
+
* Much faster and more reliable than loading X in a browser and inspecting
|
|
13
|
+
* the accessibility tree for a username string.
|
|
14
|
+
*/
|
|
15
|
+
function hasSavedAuthCookie() {
|
|
16
|
+
const cookiesPath = path.join(SOCIAL_PROFILE_DIR, 'Default', 'Cookies');
|
|
17
|
+
if (!fs.existsSync(cookiesPath))
|
|
18
|
+
return false;
|
|
19
|
+
try {
|
|
20
|
+
// Read the SQLite file as binary and look for the auth_token cookie.
|
|
21
|
+
// This avoids requiring sqlite3 as a dependency — the cookie name is
|
|
22
|
+
// stored as plain text in the DB file.
|
|
23
|
+
const raw = fs.readFileSync(cookiesPath);
|
|
24
|
+
return raw.includes('auth_token');
|
|
25
|
+
}
|
|
26
|
+
catch {
|
|
27
|
+
return false;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
7
30
|
/**
|
|
8
31
|
* Verify that social config is ready and the user is logged in to X.
|
|
9
32
|
* Returns the browser instance on success so callers can reuse it.
|
|
33
|
+
*
|
|
34
|
+
* Login detection order:
|
|
35
|
+
* 1. Check Cookies DB for auth_token (fast, no browser needed)
|
|
36
|
+
* 2. Fallback: open x.com/home and check AX tree for login_detection string
|
|
10
37
|
*/
|
|
11
38
|
export async function checkSocialReady() {
|
|
12
39
|
const cfg = loadConfig();
|
|
@@ -14,13 +41,25 @@ export async function checkSocialReady() {
|
|
|
14
41
|
if (!configStatus.ready) {
|
|
15
42
|
return { ready: false, reason: configStatus.reason };
|
|
16
43
|
}
|
|
44
|
+
// Fast path: check saved cookies first
|
|
45
|
+
if (!hasSavedAuthCookie()) {
|
|
46
|
+
return { ready: false, reason: 'Not logged in to X (no auth_token cookie). Run: franklin social login x' };
|
|
47
|
+
}
|
|
48
|
+
// Cookies exist — browser login should work. Open browser for caller to use.
|
|
17
49
|
const browser = await browserPool.getBrowser();
|
|
18
50
|
await browser.open('https://x.com/home');
|
|
19
51
|
await browser.waitForTimeout(2500);
|
|
20
52
|
const tree = await browser.snapshot();
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
53
|
+
// If login_detection is set, verify it as a secondary check.
|
|
54
|
+
// But don't fail if cookies exist — X may just not show the handle in AX tree.
|
|
55
|
+
if (cfg.x.login_detection && !tree.includes(cfg.x.login_detection)) {
|
|
56
|
+
// Check if we're actually on the home feed (not redirected to login page)
|
|
57
|
+
const isLoginPage = tree.includes('Sign in') && tree.includes('Create account');
|
|
58
|
+
if (isLoginPage) {
|
|
59
|
+
browserPool.releaseBrowser();
|
|
60
|
+
return { ready: false, reason: 'Cookie expired. Run: franklin social login x' };
|
|
61
|
+
}
|
|
62
|
+
// Cookies valid, just handle not found in AX tree — proceed anyway
|
|
24
63
|
}
|
|
25
64
|
return { ready: true, browser };
|
|
26
65
|
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Structured failure logging for self-evolution analysis.
|
|
3
|
+
* Append-only JSONL at ~/.blockrun/failures.jsonl (capped 500 records).
|
|
4
|
+
*/
|
|
5
|
+
export interface FailureRecord {
|
|
6
|
+
timestamp: number;
|
|
7
|
+
model: string;
|
|
8
|
+
failureType: 'tool_error' | 'model_error' | 'permission_denied' | 'agent_loop';
|
|
9
|
+
toolName?: string;
|
|
10
|
+
errorMessage: string;
|
|
11
|
+
recoveryAction?: string;
|
|
12
|
+
}
|
|
13
|
+
export declare function recordFailure(record: FailureRecord): void;
|
|
14
|
+
export declare function loadFailures(limit?: number): FailureRecord[];
|
|
15
|
+
export declare function getFailureStats(): {
|
|
16
|
+
byTool: Map<string, number>;
|
|
17
|
+
byType: Map<string, number>;
|
|
18
|
+
total: number;
|
|
19
|
+
recentFailures: FailureRecord[];
|
|
20
|
+
};
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Structured failure logging for self-evolution analysis.
|
|
3
|
+
* Append-only JSONL at ~/.blockrun/failures.jsonl (capped 500 records).
|
|
4
|
+
*/
|
|
5
|
+
import fs from 'node:fs';
|
|
6
|
+
import path from 'node:path';
|
|
7
|
+
import { BLOCKRUN_DIR } from '../config.js';
|
|
8
|
+
const FAILURES_FILE = path.join(BLOCKRUN_DIR, 'failures.jsonl');
|
|
9
|
+
const MAX_RECORDS = 500;
|
|
10
|
+
export function recordFailure(record) {
|
|
11
|
+
try {
|
|
12
|
+
fs.mkdirSync(path.dirname(FAILURES_FILE), { recursive: true });
|
|
13
|
+
fs.appendFileSync(FAILURES_FILE, JSON.stringify(record) + '\n');
|
|
14
|
+
// Trim to MAX_RECORDS (only check periodically to avoid constant reads)
|
|
15
|
+
if (Math.random() < 0.1) {
|
|
16
|
+
trimFailures();
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
catch {
|
|
20
|
+
// Fire-and-forget — never block the critical path
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
function trimFailures() {
|
|
24
|
+
try {
|
|
25
|
+
if (!fs.existsSync(FAILURES_FILE))
|
|
26
|
+
return;
|
|
27
|
+
const lines = fs.readFileSync(FAILURES_FILE, 'utf-8').trim().split('\n');
|
|
28
|
+
if (lines.length > MAX_RECORDS) {
|
|
29
|
+
const trimmed = lines.slice(-MAX_RECORDS).join('\n') + '\n';
|
|
30
|
+
fs.writeFileSync(FAILURES_FILE, trimmed);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
catch {
|
|
34
|
+
// ignore
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
export function loadFailures(limit = 100) {
|
|
38
|
+
try {
|
|
39
|
+
if (!fs.existsSync(FAILURES_FILE))
|
|
40
|
+
return [];
|
|
41
|
+
const lines = fs.readFileSync(FAILURES_FILE, 'utf-8').trim().split('\n').filter(Boolean);
|
|
42
|
+
return lines.slice(-limit).map(l => JSON.parse(l));
|
|
43
|
+
}
|
|
44
|
+
catch {
|
|
45
|
+
return [];
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
export function getFailureStats() {
|
|
49
|
+
const records = loadFailures(500);
|
|
50
|
+
const byTool = new Map();
|
|
51
|
+
const byType = new Map();
|
|
52
|
+
for (const r of records) {
|
|
53
|
+
if (r.toolName)
|
|
54
|
+
byTool.set(r.toolName, (byTool.get(r.toolName) ?? 0) + 1);
|
|
55
|
+
byType.set(r.failureType, (byType.get(r.failureType) ?? 0) + 1);
|
|
56
|
+
}
|
|
57
|
+
return {
|
|
58
|
+
byTool,
|
|
59
|
+
byType,
|
|
60
|
+
total: records.length,
|
|
61
|
+
recentFailures: records.slice(-10),
|
|
62
|
+
};
|
|
63
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared formatting utilities for token counts, costs, and model names.
|
|
3
|
+
*/
|
|
4
|
+
export function formatTokens(n) {
|
|
5
|
+
if (n < 1000)
|
|
6
|
+
return String(n);
|
|
7
|
+
if (n < 1_000_000)
|
|
8
|
+
return `${(n / 1000).toFixed(1)}K`;
|
|
9
|
+
return `${(n / 1_000_000).toFixed(2)}M`;
|
|
10
|
+
}
|
|
11
|
+
export function formatUsd(n) {
|
|
12
|
+
if (n === 0)
|
|
13
|
+
return '$0';
|
|
14
|
+
if (n < 0.01)
|
|
15
|
+
return `$${n.toFixed(4)}`;
|
|
16
|
+
if (n < 1)
|
|
17
|
+
return `$${n.toFixed(3)}`;
|
|
18
|
+
return `$${n.toFixed(2)}`;
|
|
19
|
+
}
|
|
20
|
+
export function shortModelName(model) {
|
|
21
|
+
const idx = model.indexOf('/');
|
|
22
|
+
return idx > -1 ? model.slice(idx + 1) : model;
|
|
23
|
+
}
|
package/dist/stats/insights.js
CHANGED
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
*/
|
|
14
14
|
import { loadStats } from './tracker.js';
|
|
15
15
|
import { OPUS_PRICING, MODEL_PRICING } from '../pricing.js';
|
|
16
|
+
import { formatTokens, formatUsd, shortModelName } from './format.js';
|
|
16
17
|
// ─── Generate Report ──────────────────────────────────────────────────────
|
|
17
18
|
export function generateInsights(days = 30) {
|
|
18
19
|
const stats = loadStats();
|
|
@@ -113,27 +114,6 @@ function sparkline(values) {
|
|
|
113
114
|
const chars = ['▁', '▂', '▃', '▄', '▅', '▆', '▇', '█'];
|
|
114
115
|
return values.map(v => chars[Math.min(7, Math.floor((v / max) * 8))]).join('');
|
|
115
116
|
}
|
|
116
|
-
function formatUsd(n) {
|
|
117
|
-
if (n === 0)
|
|
118
|
-
return '$0';
|
|
119
|
-
if (n < 0.01)
|
|
120
|
-
return `$${n.toFixed(4)}`;
|
|
121
|
-
if (n < 1)
|
|
122
|
-
return `$${n.toFixed(3)}`;
|
|
123
|
-
return `$${n.toFixed(2)}`;
|
|
124
|
-
}
|
|
125
|
-
function formatTokens(n) {
|
|
126
|
-
if (n < 1000)
|
|
127
|
-
return String(n);
|
|
128
|
-
if (n < 1_000_000)
|
|
129
|
-
return `${(n / 1000).toFixed(1)}K`;
|
|
130
|
-
return `${(n / 1_000_000).toFixed(2)}M`;
|
|
131
|
-
}
|
|
132
|
-
function shortModelName(model) {
|
|
133
|
-
// Strip provider prefix for display
|
|
134
|
-
const idx = model.indexOf('/');
|
|
135
|
-
return idx > -1 ? model.slice(idx + 1) : model;
|
|
136
|
-
}
|
|
137
117
|
export function formatInsights(report, days) {
|
|
138
118
|
const sep = '─'.repeat(60);
|
|
139
119
|
const lines = [];
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Session-scoped per-model usage tracking.
|
|
3
|
+
* In-memory only — resets on new session. Used by /cost and UI footer.
|
|
4
|
+
*/
|
|
5
|
+
export interface SessionModelUsage {
|
|
6
|
+
requests: number;
|
|
7
|
+
inputTokens: number;
|
|
8
|
+
outputTokens: number;
|
|
9
|
+
costUsd: number;
|
|
10
|
+
lastTier?: string;
|
|
11
|
+
}
|
|
12
|
+
export declare function recordSessionUsage(model: string, inputTokens: number, outputTokens: number, costUsd: number, tier?: string): void;
|
|
13
|
+
export declare function getSessionModelBreakdown(): Array<{
|
|
14
|
+
model: string;
|
|
15
|
+
requests: number;
|
|
16
|
+
inputTokens: number;
|
|
17
|
+
outputTokens: number;
|
|
18
|
+
costUsd: number;
|
|
19
|
+
lastTier?: string;
|
|
20
|
+
}>;
|
|
21
|
+
export declare function resetSession(): void;
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Session-scoped per-model usage tracking.
|
|
3
|
+
* In-memory only — resets on new session. Used by /cost and UI footer.
|
|
4
|
+
*/
|
|
5
|
+
const sessionModels = new Map();
|
|
6
|
+
export function recordSessionUsage(model, inputTokens, outputTokens, costUsd, tier) {
|
|
7
|
+
const existing = sessionModels.get(model) ?? {
|
|
8
|
+
requests: 0,
|
|
9
|
+
inputTokens: 0,
|
|
10
|
+
outputTokens: 0,
|
|
11
|
+
costUsd: 0,
|
|
12
|
+
};
|
|
13
|
+
existing.requests++;
|
|
14
|
+
existing.inputTokens += inputTokens;
|
|
15
|
+
existing.outputTokens += outputTokens;
|
|
16
|
+
existing.costUsd += costUsd;
|
|
17
|
+
if (tier)
|
|
18
|
+
existing.lastTier = tier;
|
|
19
|
+
sessionModels.set(model, existing);
|
|
20
|
+
}
|
|
21
|
+
export function getSessionModelBreakdown() {
|
|
22
|
+
return Array.from(sessionModels.entries())
|
|
23
|
+
.map(([model, usage]) => ({ model, ...usage }))
|
|
24
|
+
.sort((a, b) => b.costUsd - a.costUsd);
|
|
25
|
+
}
|
|
26
|
+
export function resetSession() {
|
|
27
|
+
sessionModels.clear();
|
|
28
|
+
}
|
package/dist/stats/tracker.d.ts
CHANGED
package/dist/stats/tracker.js
CHANGED
package/dist/tools/bash.d.ts
CHANGED
|
@@ -1,5 +1,18 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Bash capability — execute shell commands with timeout and output capture.
|
|
3
3
|
*/
|
|
4
|
-
import type { CapabilityHandler } from '../agent/types.js';
|
|
4
|
+
import type { CapabilityHandler, CapabilityResult } from '../agent/types.js';
|
|
5
|
+
interface BackgroundTask {
|
|
6
|
+
id: string;
|
|
7
|
+
command: string;
|
|
8
|
+
description: string;
|
|
9
|
+
startedAt: number;
|
|
10
|
+
status: 'running' | 'completed' | 'failed';
|
|
11
|
+
result?: CapabilityResult;
|
|
12
|
+
}
|
|
13
|
+
/** Get a background task's result (called by the agent to check status). */
|
|
14
|
+
export declare function getBackgroundTask(id: string): BackgroundTask | undefined;
|
|
15
|
+
/** List all background tasks. */
|
|
16
|
+
export declare function listBackgroundTasks(): BackgroundTask[];
|
|
5
17
|
export declare const bashCapability: CapabilityHandler;
|
|
18
|
+
export {};
|