@blockrun/franklin 3.7.0 → 3.7.2
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/context.js +1 -0
- package/dist/agent/loop.js +13 -0
- package/dist/commands/start.js +78 -17
- package/dist/panel/html.js +80 -0
- package/dist/panel/server.js +39 -2
- package/dist/proxy/server.js +21 -0
- package/dist/stats/audit.d.ts +33 -0
- package/dist/stats/audit.js +86 -0
- package/package.json +1 -1
package/dist/agent/context.js
CHANGED
|
@@ -38,6 +38,7 @@ You are an interactive agent — not a chatbot. Use the tools available to you t
|
|
|
38
38
|
- **Search strategy**: Glob/Grep for directed searches (known file/symbol). Use Agent for open-ended exploration that may require multiple rounds.
|
|
39
39
|
- **Batch bash**: chain sequential shell commands with && in a single call. Only split when you need intermediate output.
|
|
40
40
|
- **AskUser discipline**: Only use AskUser when you need explicit confirmation for a destructive action (deleting files, dropping databases). NEVER use AskUser to ask what the user wants — just answer their message directly. If the request is vague, make a reasonable assumption and proceed.
|
|
41
|
+
- **Greetings**: When the user sends only a greeting or filler ("hi", "hello", "hey", "ok", "thanks", "yo"), reply with ONE short plain-text sentence (e.g. "Hi — what do you want to work on?"). Do NOT call AskUser. Do NOT assume a marketing/trading/coding task. Do NOT invoke any tools.
|
|
41
42
|
- Never write to /etc, /usr, ~/.ssh, ~/.aws. Don't commit secrets.`;
|
|
42
43
|
}
|
|
43
44
|
function getCodeStyleSection() {
|
package/dist/agent/loop.js
CHANGED
|
@@ -14,6 +14,7 @@ import { classifyAgentError } from './error-classifier.js';
|
|
|
14
14
|
import { SessionToolGuard } from './tool-guard.js';
|
|
15
15
|
import { recordUsage } from '../stats/tracker.js';
|
|
16
16
|
import { recordSessionUsage } from '../stats/session-tracker.js';
|
|
17
|
+
import { appendAudit, extractLastUserPrompt } from '../stats/audit.js';
|
|
17
18
|
import { estimateCost, OPUS_PRICING } from '../pricing.js';
|
|
18
19
|
import { maybeMidSessionExtract } from '../learnings/extractor.js';
|
|
19
20
|
import { routeRequest, parseRoutingProfile } from '../router/index.js';
|
|
@@ -656,6 +657,18 @@ export async function interactiveSession(config, getUserInput, onEvent, onAbortR
|
|
|
656
657
|
const costEstimate = estimateCost(resolvedModel, inputTokens, usage.outputTokens, 1);
|
|
657
658
|
recordUsage(resolvedModel, inputTokens, usage.outputTokens, costEstimate, 0);
|
|
658
659
|
recordSessionUsage(resolvedModel, inputTokens, usage.outputTokens, costEstimate, routingTier);
|
|
660
|
+
appendAudit({
|
|
661
|
+
ts: Date.now(),
|
|
662
|
+
sessionId,
|
|
663
|
+
model: resolvedModel,
|
|
664
|
+
inputTokens,
|
|
665
|
+
outputTokens: usage.outputTokens,
|
|
666
|
+
costUsd: costEstimate,
|
|
667
|
+
source: 'agent',
|
|
668
|
+
workDir,
|
|
669
|
+
prompt: extractLastUserPrompt(history),
|
|
670
|
+
routingTier,
|
|
671
|
+
});
|
|
659
672
|
// Accumulate session-level totals for session meta
|
|
660
673
|
sessionInputTokens += inputTokens;
|
|
661
674
|
sessionOutputTokens += usage.outputTokens;
|
package/dist/commands/start.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import chalk from 'chalk';
|
|
2
2
|
import { getOrCreateWallet, getOrCreateSolanaWallet } from '@blockrun/llm';
|
|
3
3
|
import { loadChain, API_URLS } from '../config.js';
|
|
4
|
-
import { flushStats } from '../stats/tracker.js';
|
|
4
|
+
import { flushStats, loadStats } from '../stats/tracker.js';
|
|
5
|
+
import { OPUS_PRICING } from '../pricing.js';
|
|
5
6
|
import { loadConfig } from './config.js';
|
|
6
7
|
import { printBanner } from '../banner.js';
|
|
7
8
|
import { assembleInstructions } from '../agent/context.js';
|
|
@@ -114,11 +115,17 @@ export async function startCommand(options) {
|
|
|
114
115
|
}
|
|
115
116
|
printBanner(version);
|
|
116
117
|
const workDir = process.cwd();
|
|
118
|
+
// Auto-start panel in background unless explicitly disabled.
|
|
119
|
+
// Binds loopback-only (wallet secrets on /api/wallet/secret — never expose on LAN).
|
|
120
|
+
let panelUrl;
|
|
121
|
+
if (process.env.FRANKLIN_PANEL_AUTOSTART !== '0') {
|
|
122
|
+
panelUrl = await startPanelBackground(3100);
|
|
123
|
+
}
|
|
117
124
|
// Session info — aligned, minimal. Model + balance live in the input bar below.
|
|
118
125
|
// Full wallet address is shown so the user can copy-paste it to fund the wallet.
|
|
119
126
|
console.log(chalk.dim(' Wallet: ') + (walletAddress || chalk.yellow('not set')));
|
|
120
127
|
console.log(chalk.dim(' Dir: ') + workDir);
|
|
121
|
-
console.log(chalk.dim(' Dashboard: ') + chalk.cyan('franklin panel') + chalk.dim(' → http://localhost:3100'));
|
|
128
|
+
console.log(chalk.dim(' Dashboard: ') + (panelUrl ? chalk.cyan(panelUrl) : chalk.cyan('franklin panel') + chalk.dim(' → http://localhost:3100')));
|
|
122
129
|
console.log(chalk.dim(' Help: ') + chalk.cyan('/help'));
|
|
123
130
|
console.log('');
|
|
124
131
|
// Balance fetcher — used at startup and after each turn
|
|
@@ -257,6 +264,7 @@ export async function startCommand(options) {
|
|
|
257
264
|
}
|
|
258
265
|
// ─── Ink UI (interactive terminal) ─────────────────────────────────────────
|
|
259
266
|
async function runWithInkUI(agentConfig, model, workDir, version, walletInfo, onBalanceReady, fetchBalance) {
|
|
267
|
+
const startSnapshot = snapshotStats();
|
|
260
268
|
const ui = launchInkUI({
|
|
261
269
|
model,
|
|
262
270
|
workDir,
|
|
@@ -324,15 +332,17 @@ async function runWithInkUI(agentConfig, model, workDir, version, walletInfo, on
|
|
|
324
332
|
catch { /* extraction is best-effort */ }
|
|
325
333
|
}
|
|
326
334
|
await disconnectMcpServers();
|
|
327
|
-
// Session summary —
|
|
335
|
+
// Session summary — delta vs. snapshot at session start
|
|
328
336
|
try {
|
|
329
|
-
const
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
const
|
|
333
|
-
const
|
|
334
|
-
|
|
335
|
-
|
|
337
|
+
const delta = statsDelta(startSnapshot);
|
|
338
|
+
if (delta.requests > 0) {
|
|
339
|
+
const cost = delta.cost.toFixed(4);
|
|
340
|
+
const savedStr = delta.saved > 0.001 ? ` · saved $${delta.saved.toFixed(2)} vs Opus` : '';
|
|
341
|
+
const tokens = `${(delta.inputTokens / 1000).toFixed(0)}k in / ${(delta.outputTokens / 1000).toFixed(0)}k out`;
|
|
342
|
+
console.log(chalk.dim(`\n Session: ${delta.requests} requests · $${cost} USDC${savedStr} · ${tokens}`));
|
|
343
|
+
}
|
|
344
|
+
else {
|
|
345
|
+
console.log(chalk.dim('\n Session: 0 requests · no spend'));
|
|
336
346
|
}
|
|
337
347
|
}
|
|
338
348
|
catch { /* stats unavailable */ }
|
|
@@ -343,6 +353,7 @@ async function runWithBasicUI(agentConfig, model, workDir) {
|
|
|
343
353
|
const { TerminalUI } = await import('../ui/terminal.js');
|
|
344
354
|
const ui = new TerminalUI();
|
|
345
355
|
ui.printWelcome(model, workDir);
|
|
356
|
+
const startSnapshot = snapshotStats();
|
|
346
357
|
let lastTerminalPrompt = '';
|
|
347
358
|
try {
|
|
348
359
|
await interactiveSession(agentConfig, async () => {
|
|
@@ -390,19 +401,69 @@ async function runWithBasicUI(agentConfig, model, workDir) {
|
|
|
390
401
|
}
|
|
391
402
|
// Session summary for piped mode
|
|
392
403
|
try {
|
|
393
|
-
const
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
const
|
|
397
|
-
const
|
|
398
|
-
|
|
399
|
-
console.error(`Session: ${stats.totalRequests} requests · $${cost} USDC${savedStr} · ${tokens}`);
|
|
404
|
+
const delta = statsDelta(startSnapshot);
|
|
405
|
+
if (delta.requests > 0) {
|
|
406
|
+
const cost = delta.cost.toFixed(4);
|
|
407
|
+
const savedStr = delta.saved > 0.001 ? ` · saved $${delta.saved.toFixed(2)} vs Opus` : '';
|
|
408
|
+
const tokens = `${(delta.inputTokens / 1000).toFixed(0)}k in / ${(delta.outputTokens / 1000).toFixed(0)}k out`;
|
|
409
|
+
console.error(`Session: ${delta.requests} requests · $${cost} USDC${savedStr} · ${tokens}`);
|
|
400
410
|
}
|
|
401
411
|
}
|
|
402
412
|
catch { /* stats unavailable */ }
|
|
403
413
|
ui.printGoodbye();
|
|
404
414
|
flushStats();
|
|
405
415
|
}
|
|
416
|
+
// ─── Panel auto-start ──────────────────────────────────────────────────────
|
|
417
|
+
async function startPanelBackground(startPort) {
|
|
418
|
+
const MAX_ATTEMPTS = 20;
|
|
419
|
+
try {
|
|
420
|
+
const { createPanelServer } = await import('../panel/server.js');
|
|
421
|
+
return await new Promise((resolve) => {
|
|
422
|
+
const tryListen = (port, attempt) => {
|
|
423
|
+
const server = createPanelServer(port);
|
|
424
|
+
server.on('error', (err) => {
|
|
425
|
+
if (err.code === 'EADDRINUSE' && attempt < MAX_ATTEMPTS) {
|
|
426
|
+
tryListen(port + 1, attempt + 1);
|
|
427
|
+
return;
|
|
428
|
+
}
|
|
429
|
+
resolve(undefined);
|
|
430
|
+
});
|
|
431
|
+
server.listen(port, '127.0.0.1', () => {
|
|
432
|
+
server.unref?.();
|
|
433
|
+
resolve(`http://localhost:${port}`);
|
|
434
|
+
});
|
|
435
|
+
};
|
|
436
|
+
tryListen(startPort, 0);
|
|
437
|
+
});
|
|
438
|
+
}
|
|
439
|
+
catch {
|
|
440
|
+
return undefined;
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
function snapshotStats() {
|
|
444
|
+
try {
|
|
445
|
+
const s = loadStats();
|
|
446
|
+
return {
|
|
447
|
+
requests: s.totalRequests,
|
|
448
|
+
cost: s.totalCostUsd,
|
|
449
|
+
inputTokens: s.totalInputTokens,
|
|
450
|
+
outputTokens: s.totalOutputTokens,
|
|
451
|
+
};
|
|
452
|
+
}
|
|
453
|
+
catch {
|
|
454
|
+
return { requests: 0, cost: 0, inputTokens: 0, outputTokens: 0 };
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
function statsDelta(before) {
|
|
458
|
+
const now = loadStats();
|
|
459
|
+
const requests = Math.max(0, now.totalRequests - before.requests);
|
|
460
|
+
const cost = Math.max(0, now.totalCostUsd - before.cost);
|
|
461
|
+
const inputTokens = Math.max(0, now.totalInputTokens - before.inputTokens);
|
|
462
|
+
const outputTokens = Math.max(0, now.totalOutputTokens - before.outputTokens);
|
|
463
|
+
const opusCost = (inputTokens / 1_000_000) * OPUS_PRICING.input +
|
|
464
|
+
(outputTokens / 1_000_000) * OPUS_PRICING.output;
|
|
465
|
+
return { requests, cost, inputTokens, outputTokens, saved: Math.max(0, opusCost - cost) };
|
|
466
|
+
}
|
|
406
467
|
async function handleSlashCommand(cmd, config, ui) {
|
|
407
468
|
const parts = cmd.trim().split(/\s+/);
|
|
408
469
|
const command = parts[0].toLowerCase();
|
package/dist/panel/html.js
CHANGED
|
@@ -383,6 +383,10 @@ a:hover { text-decoration:underline; }
|
|
|
383
383
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M2 3h6a4 4 0 0 1 4 4v14a3 3 0 0 0-3-3H2z"/><path d="M22 3h-6a4 4 0 0 0-4 4v14a3 3 0 0 1 3-3h7z"/></svg>
|
|
384
384
|
Learnings
|
|
385
385
|
</button>
|
|
386
|
+
<button class="nav-item" data-tab="audit">
|
|
387
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><circle cx="11" cy="11" r="7"/><path d="M21 21l-4.3-4.3"/><path d="M11 8v3l2 1"/></svg>
|
|
388
|
+
Audit Log
|
|
389
|
+
</button>
|
|
386
390
|
</div>
|
|
387
391
|
|
|
388
392
|
<div class="sidebar-footer">
|
|
@@ -559,6 +563,31 @@ a:hover { text-decoration:underline; }
|
|
|
559
563
|
</div>
|
|
560
564
|
<div id="learnings-list"></div>
|
|
561
565
|
</div>
|
|
566
|
+
|
|
567
|
+
<!-- Audit Log -->
|
|
568
|
+
<div class="tab" id="tab-audit">
|
|
569
|
+
<div class="content-header">
|
|
570
|
+
<h2>Audit Log</h2>
|
|
571
|
+
<p>Every LLM call: prompt, model, tokens, cost. Where the money actually went.</p>
|
|
572
|
+
</div>
|
|
573
|
+
<div style="display:flex;gap:8px;align-items:center;margin-bottom:16px;flex-wrap:wrap;">
|
|
574
|
+
<label style="display:flex;align-items:center;gap:6px;font-size:13px;color:var(--text-dim);cursor:pointer;">
|
|
575
|
+
<input type="checkbox" id="audit-paid-only" style="margin:0;" /> Paid only
|
|
576
|
+
</label>
|
|
577
|
+
<select id="audit-since" style="padding:4px 8px;background:var(--bg-card);color:var(--text);border:1px solid var(--border);border-radius:6px;font-size:13px;">
|
|
578
|
+
<option value="0">All time</option>
|
|
579
|
+
<option value="3600000">Last hour</option>
|
|
580
|
+
<option value="86400000" selected>Last 24h</option>
|
|
581
|
+
<option value="604800000">Last 7 days</option>
|
|
582
|
+
<option value="2592000000">Last 30 days</option>
|
|
583
|
+
</select>
|
|
584
|
+
<input id="audit-model" placeholder="Filter by model…" style="padding:4px 8px;background:var(--bg-card);color:var(--text);border:1px solid var(--border);border-radius:6px;font-size:13px;width:180px;" />
|
|
585
|
+
<input id="audit-session" placeholder="Filter by session prefix…" style="padding:4px 8px;background:var(--bg-card);color:var(--text);border:1px solid var(--border);border-radius:6px;font-size:13px;width:180px;" />
|
|
586
|
+
<button id="audit-refresh" style="padding:4px 10px;background:var(--bg-card);color:var(--text);border:1px solid var(--border);border-radius:6px;font-size:13px;cursor:pointer;">Refresh</button>
|
|
587
|
+
<span id="audit-summary" style="margin-left:auto;font-size:13px;color:var(--text-dim);"></span>
|
|
588
|
+
</div>
|
|
589
|
+
<div id="audit-list" style="font-family:ui-monospace,SFMono-Regular,Consolas,monospace;font-size:12px;"></div>
|
|
590
|
+
</div>
|
|
562
591
|
</div>
|
|
563
592
|
|
|
564
593
|
<script>
|
|
@@ -827,6 +856,57 @@ es.onmessage = (e) => {
|
|
|
827
856
|
try { if (JSON.parse(e.data).type === 'stats.updated') loadOverview(); } catch {}
|
|
828
857
|
};
|
|
829
858
|
|
|
859
|
+
async function loadAudit() {
|
|
860
|
+
const list = document.getElementById('audit-list');
|
|
861
|
+
const summary = document.getElementById('audit-summary');
|
|
862
|
+
if (!list) return;
|
|
863
|
+
const params = new URLSearchParams({ limit: '300' });
|
|
864
|
+
if (document.getElementById('audit-paid-only').checked) params.set('paidOnly', '1');
|
|
865
|
+
const sinceMs = parseInt(document.getElementById('audit-since').value, 10);
|
|
866
|
+
if (sinceMs > 0) params.set('since', String(Date.now() - sinceMs));
|
|
867
|
+
const model = document.getElementById('audit-model').value.trim();
|
|
868
|
+
if (model) params.set('model', model);
|
|
869
|
+
const session = document.getElementById('audit-session').value.trim();
|
|
870
|
+
if (session) params.set('session', session);
|
|
871
|
+
|
|
872
|
+
list.innerHTML = '<div style="color:var(--text-dim);padding:12px;">Loading…</div>';
|
|
873
|
+
const data = await fetch('/api/audit?' + params.toString()).then(r => r.json()).catch(() => null);
|
|
874
|
+
if (!data) { list.innerHTML = '<div style="color:var(--text-dim);padding:12px;">API offline</div>'; return; }
|
|
875
|
+
if (!data.entries.length) {
|
|
876
|
+
list.innerHTML = '<div style="color:var(--text-dim);padding:12px;">No audit entries match these filters. Run franklin and make a request.</div>';
|
|
877
|
+
summary.textContent = '0 calls';
|
|
878
|
+
return;
|
|
879
|
+
}
|
|
880
|
+
summary.textContent = data.returned + ' / ' + data.total + ' calls · $' + data.totalCostUsd.toFixed(4) + ' · ' +
|
|
881
|
+
(data.totalInputTokens/1000).toFixed(1) + 'k in / ' + (data.totalOutputTokens/1000).toFixed(1) + 'k out';
|
|
882
|
+
|
|
883
|
+
list.innerHTML = data.entries.map(e => {
|
|
884
|
+
const ts = new Date(e.ts).toLocaleString('en-US', { hour12: false });
|
|
885
|
+
const cost = e.costUsd > 0
|
|
886
|
+
? '<span style="color:#fbbf24;">$' + e.costUsd.toFixed(4) + '</span>'
|
|
887
|
+
: '<span style="color:#10b981;">FREE</span>';
|
|
888
|
+
const fb = e.fallback ? ' <span style="color:#f97316;">·fb</span>' : '';
|
|
889
|
+
const sid = e.sessionId ? ' <span style="color:var(--text-dim);">' + esc(e.sessionId.slice(0,8)) + '</span>' : '';
|
|
890
|
+
const prompt = e.prompt
|
|
891
|
+
? '<div style="color:var(--text-dim);padding:2px 0 4px 16px;white-space:pre-wrap;word-break:break-word;">"' + esc(e.prompt) + '"</div>'
|
|
892
|
+
: '';
|
|
893
|
+
const dir = e.workDir ? '<div style="color:var(--text-dim);padding:0 0 0 16px;font-size:11px;">📁 ' + esc(e.workDir) + '</div>' : '';
|
|
894
|
+
return '<div style="padding:8px 12px;border-bottom:1px solid var(--border);">' +
|
|
895
|
+
'<div><span style="color:var(--text-dim);">' + ts + '</span> ' + cost + ' <span style="color:#60a5fa;">' + esc(e.model) + '</span> ' +
|
|
896
|
+
'<span style="color:var(--text-dim);">in=' + e.inputTokens + ' out=' + e.outputTokens + '</span> ' +
|
|
897
|
+
'<span style="color:var(--text-dim);">[' + esc(e.source) + ']' + fb + '</span>' + sid + '</div>' +
|
|
898
|
+
prompt + dir +
|
|
899
|
+
'</div>';
|
|
900
|
+
}).join('');
|
|
901
|
+
}
|
|
902
|
+
|
|
903
|
+
['audit-paid-only','audit-since','audit-model','audit-session'].forEach(id => {
|
|
904
|
+
const el = document.getElementById(id);
|
|
905
|
+
if (el) el.addEventListener(el.tagName === 'INPUT' && el.type === 'text' ? 'input' : 'change', () => loadAudit());
|
|
906
|
+
});
|
|
907
|
+
document.getElementById('audit-refresh')?.addEventListener('click', loadAudit);
|
|
908
|
+
document.querySelector('[data-tab="audit"]')?.addEventListener('click', loadAudit);
|
|
909
|
+
|
|
830
910
|
loadOverview();
|
|
831
911
|
loadSessions();
|
|
832
912
|
loadSocial();
|
package/dist/panel/server.js
CHANGED
|
@@ -12,6 +12,7 @@ import { generateInsights } from '../stats/insights.js';
|
|
|
12
12
|
import { listSessions, loadSessionHistory } from '../session/storage.js';
|
|
13
13
|
import { searchSessions } from '../session/search.js';
|
|
14
14
|
import { loadLearnings } from '../learnings/store.js';
|
|
15
|
+
import { readAudit } from '../stats/audit.js';
|
|
15
16
|
import { getStats as getSocialStats } from '../social/db.js';
|
|
16
17
|
import { getHTML } from './html.js';
|
|
17
18
|
const sseClients = new Set();
|
|
@@ -129,6 +130,37 @@ export function createPanelServer(port) {
|
|
|
129
130
|
json(res, report);
|
|
130
131
|
return;
|
|
131
132
|
}
|
|
133
|
+
if (p === '/api/audit') {
|
|
134
|
+
// Per-call LLM audit log — prompt, model, tokens, cost per call.
|
|
135
|
+
// Supports ?limit=N&paidOnly=1&since=<ms>&session=<prefix>&model=<substr>
|
|
136
|
+
const limit = Math.min(parseInt(url.searchParams.get('limit') || '200', 10), 2000);
|
|
137
|
+
const paidOnly = url.searchParams.get('paidOnly') === '1';
|
|
138
|
+
const since = parseInt(url.searchParams.get('since') || '0', 10);
|
|
139
|
+
const sessionFilter = url.searchParams.get('session') || '';
|
|
140
|
+
const modelFilter = url.searchParams.get('model') || '';
|
|
141
|
+
let entries = readAudit();
|
|
142
|
+
if (since > 0)
|
|
143
|
+
entries = entries.filter(e => e.ts >= since);
|
|
144
|
+
if (paidOnly)
|
|
145
|
+
entries = entries.filter(e => e.costUsd > 0);
|
|
146
|
+
if (sessionFilter)
|
|
147
|
+
entries = entries.filter(e => e.sessionId?.startsWith(sessionFilter));
|
|
148
|
+
if (modelFilter)
|
|
149
|
+
entries = entries.filter(e => e.model.includes(modelFilter));
|
|
150
|
+
const recent = entries.slice(-limit).reverse(); // newest first
|
|
151
|
+
const totalCost = entries.reduce((s, e) => s + e.costUsd, 0);
|
|
152
|
+
const totalIn = entries.reduce((s, e) => s + e.inputTokens, 0);
|
|
153
|
+
const totalOut = entries.reduce((s, e) => s + e.outputTokens, 0);
|
|
154
|
+
json(res, {
|
|
155
|
+
total: entries.length,
|
|
156
|
+
returned: recent.length,
|
|
157
|
+
totalCostUsd: totalCost,
|
|
158
|
+
totalInputTokens: totalIn,
|
|
159
|
+
totalOutputTokens: totalOut,
|
|
160
|
+
entries: recent,
|
|
161
|
+
});
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
132
164
|
if (p === '/api/sessions') {
|
|
133
165
|
const sessions = listSessions();
|
|
134
166
|
json(res, sessions);
|
|
@@ -310,13 +342,18 @@ export function createPanelServer(port) {
|
|
|
310
342
|
console.error('[panel] request error:', err.message);
|
|
311
343
|
}
|
|
312
344
|
});
|
|
313
|
-
// Swallow socket errors (client disconnects, etc.) so they don't crash the process
|
|
345
|
+
// Swallow socket errors (client disconnects, etc.) so they don't crash the process.
|
|
346
|
+
// ECONNRESET / EPIPE happen every time a browser tab closes an SSE stream — pure noise.
|
|
314
347
|
server.on('clientError', (err, socket) => {
|
|
315
348
|
try {
|
|
316
349
|
socket.destroy();
|
|
317
350
|
}
|
|
318
351
|
catch { /* already closed */ }
|
|
319
|
-
|
|
352
|
+
if (err.code === 'ECONNRESET' || err.code === 'EPIPE')
|
|
353
|
+
return;
|
|
354
|
+
if (process.env.FRANKLIN_PANEL_DEBUG) {
|
|
355
|
+
console.error('[panel] client error:', err.message);
|
|
356
|
+
}
|
|
320
357
|
});
|
|
321
358
|
// Watch stats file for changes → push to SSE clients
|
|
322
359
|
const statsFile = fs.existsSync(path.join(BLOCKRUN_DIR, 'franklin-stats.json'))
|
package/dist/proxy/server.js
CHANGED
|
@@ -4,6 +4,7 @@ import path from 'node:path';
|
|
|
4
4
|
import os from 'node:os';
|
|
5
5
|
import { getOrCreateWallet, getOrCreateSolanaWallet, createPaymentPayload, createSolanaPaymentPayload, parsePaymentRequired, extractPaymentDetails, solanaKeyToBytes, SOLANA_NETWORK, } from '@blockrun/llm';
|
|
6
6
|
import { recordUsage } from '../stats/tracker.js';
|
|
7
|
+
import { appendAudit } from '../stats/audit.js';
|
|
7
8
|
import { fetchWithFallback, buildFallbackChain, DEFAULT_FALLBACK_CONFIG, ROUTING_PROFILES, } from './fallback.js';
|
|
8
9
|
import { routeRequest, parseRoutingProfile, } from '../router/index.js';
|
|
9
10
|
import { estimateCost } from '../pricing.js';
|
|
@@ -481,6 +482,16 @@ export function createProxy(options) {
|
|
|
481
482
|
const latencyMs = Date.now() - requestStartTime;
|
|
482
483
|
const cost = estimateCost(finalModel, inputTokens, outputTokens);
|
|
483
484
|
recordUsage(finalModel, inputTokens, outputTokens, cost, latencyMs, usedFallback);
|
|
485
|
+
appendAudit({
|
|
486
|
+
ts: Date.now(),
|
|
487
|
+
model: finalModel,
|
|
488
|
+
inputTokens,
|
|
489
|
+
outputTokens,
|
|
490
|
+
costUsd: cost,
|
|
491
|
+
latencyMs,
|
|
492
|
+
fallback: usedFallback,
|
|
493
|
+
source: 'proxy',
|
|
494
|
+
});
|
|
484
495
|
debug(options, `recorded: model=${finalModel} in=${inputTokens} out=${outputTokens} cost=$${cost.toFixed(4)} fallback=${usedFallback}`);
|
|
485
496
|
}
|
|
486
497
|
}
|
|
@@ -510,6 +521,16 @@ export function createProxy(options) {
|
|
|
510
521
|
const latencyMs = Date.now() - requestStartTime;
|
|
511
522
|
const cost = estimateCost(finalModel, inputTokens, outputTokens);
|
|
512
523
|
recordUsage(finalModel, inputTokens, outputTokens, cost, latencyMs, usedFallback);
|
|
524
|
+
appendAudit({
|
|
525
|
+
ts: Date.now(),
|
|
526
|
+
model: finalModel,
|
|
527
|
+
inputTokens,
|
|
528
|
+
outputTokens,
|
|
529
|
+
costUsd: cost,
|
|
530
|
+
latencyMs,
|
|
531
|
+
fallback: usedFallback,
|
|
532
|
+
source: 'proxy',
|
|
533
|
+
});
|
|
513
534
|
debug(options, `recorded: model=${finalModel} in=${inputTokens} out=${outputTokens} cost=$${cost.toFixed(4)} fallback=${usedFallback}`);
|
|
514
535
|
}
|
|
515
536
|
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Audit log — append-only forensic record of every LLM call.
|
|
3
|
+
*
|
|
4
|
+
* Lives at ~/.blockrun/franklin-audit.jsonl. One line per call, JSONL.
|
|
5
|
+
* Unlike franklin-stats.json (aggregates), this file lets you answer
|
|
6
|
+
* "what was I actually doing when $1.50 disappeared on Apr 12?".
|
|
7
|
+
*
|
|
8
|
+
* Fields kept intentionally small (truncated prompt, no tool args) so the
|
|
9
|
+
* file stays readable and doesn't leak large tool outputs to disk.
|
|
10
|
+
*/
|
|
11
|
+
export interface AuditEntry {
|
|
12
|
+
ts: number;
|
|
13
|
+
sessionId?: string;
|
|
14
|
+
model: string;
|
|
15
|
+
inputTokens: number;
|
|
16
|
+
outputTokens: number;
|
|
17
|
+
costUsd: number;
|
|
18
|
+
latencyMs?: number;
|
|
19
|
+
fallback?: boolean;
|
|
20
|
+
source: 'agent' | 'proxy' | 'subagent' | 'moa' | 'plugin';
|
|
21
|
+
workDir?: string;
|
|
22
|
+
prompt?: string;
|
|
23
|
+
toolCalls?: string[];
|
|
24
|
+
routingTier?: string;
|
|
25
|
+
}
|
|
26
|
+
export declare function appendAudit(entry: AuditEntry): void;
|
|
27
|
+
export declare function getAuditFilePath(): string;
|
|
28
|
+
export declare function readAudit(): AuditEntry[];
|
|
29
|
+
/** Pull the last user message from a Dialogue history, flatten, and strip newlines. */
|
|
30
|
+
export declare function extractLastUserPrompt(history: Array<{
|
|
31
|
+
role: string;
|
|
32
|
+
content: unknown;
|
|
33
|
+
}>): string | undefined;
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Audit log — append-only forensic record of every LLM call.
|
|
3
|
+
*
|
|
4
|
+
* Lives at ~/.blockrun/franklin-audit.jsonl. One line per call, JSONL.
|
|
5
|
+
* Unlike franklin-stats.json (aggregates), this file lets you answer
|
|
6
|
+
* "what was I actually doing when $1.50 disappeared on Apr 12?".
|
|
7
|
+
*
|
|
8
|
+
* Fields kept intentionally small (truncated prompt, no tool args) so the
|
|
9
|
+
* file stays readable and doesn't leak large tool outputs to disk.
|
|
10
|
+
*/
|
|
11
|
+
import fs from 'node:fs';
|
|
12
|
+
import path from 'node:path';
|
|
13
|
+
import { BLOCKRUN_DIR } from '../config.js';
|
|
14
|
+
const AUDIT_FILE = path.join(BLOCKRUN_DIR, 'franklin-audit.jsonl');
|
|
15
|
+
const PROMPT_PREVIEW_CHARS = 240;
|
|
16
|
+
export function appendAudit(entry) {
|
|
17
|
+
try {
|
|
18
|
+
fs.mkdirSync(BLOCKRUN_DIR, { recursive: true });
|
|
19
|
+
const safe = {
|
|
20
|
+
...entry,
|
|
21
|
+
prompt: entry.prompt ? truncate(entry.prompt, PROMPT_PREVIEW_CHARS) : undefined,
|
|
22
|
+
};
|
|
23
|
+
fs.appendFileSync(AUDIT_FILE, JSON.stringify(safe) + '\n');
|
|
24
|
+
}
|
|
25
|
+
catch {
|
|
26
|
+
/* best-effort — never break the agent loop on audit-write failure */
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
export function getAuditFilePath() {
|
|
30
|
+
return AUDIT_FILE;
|
|
31
|
+
}
|
|
32
|
+
export function readAudit() {
|
|
33
|
+
try {
|
|
34
|
+
if (!fs.existsSync(AUDIT_FILE))
|
|
35
|
+
return [];
|
|
36
|
+
const lines = fs.readFileSync(AUDIT_FILE, 'utf-8').split('\n');
|
|
37
|
+
const out = [];
|
|
38
|
+
for (const line of lines) {
|
|
39
|
+
if (!line.trim())
|
|
40
|
+
continue;
|
|
41
|
+
try {
|
|
42
|
+
out.push(JSON.parse(line));
|
|
43
|
+
}
|
|
44
|
+
catch { /* skip malformed line */ }
|
|
45
|
+
}
|
|
46
|
+
return out;
|
|
47
|
+
}
|
|
48
|
+
catch {
|
|
49
|
+
return [];
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
/** Pull the last user message from a Dialogue history, flatten, and strip newlines. */
|
|
53
|
+
export function extractLastUserPrompt(history) {
|
|
54
|
+
for (let i = history.length - 1; i >= 0; i--) {
|
|
55
|
+
const msg = history[i];
|
|
56
|
+
if (msg.role !== 'user')
|
|
57
|
+
continue;
|
|
58
|
+
const text = flattenContent(msg.content);
|
|
59
|
+
if (!text)
|
|
60
|
+
continue;
|
|
61
|
+
return text.replace(/\s+/g, ' ').trim();
|
|
62
|
+
}
|
|
63
|
+
return undefined;
|
|
64
|
+
}
|
|
65
|
+
function flattenContent(content) {
|
|
66
|
+
if (typeof content === 'string')
|
|
67
|
+
return content;
|
|
68
|
+
if (!Array.isArray(content))
|
|
69
|
+
return '';
|
|
70
|
+
const parts = [];
|
|
71
|
+
for (const block of content) {
|
|
72
|
+
if (typeof block === 'string') {
|
|
73
|
+
parts.push(block);
|
|
74
|
+
}
|
|
75
|
+
else if (block && typeof block === 'object') {
|
|
76
|
+
const b = block;
|
|
77
|
+
// Skip tool_result blocks — they're tool output, not user intent
|
|
78
|
+
if (b.type === 'text' && typeof b.text === 'string')
|
|
79
|
+
parts.push(b.text);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
return parts.join(' ');
|
|
83
|
+
}
|
|
84
|
+
function truncate(s, n) {
|
|
85
|
+
return s.length > n ? s.slice(0, n) + '…' : s;
|
|
86
|
+
}
|
package/package.json
CHANGED