dual-brain 4.6.0 → 4.7.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.
@@ -16,7 +16,14 @@ import { spawnSync } from 'child_process';
16
16
  const __dirname = dirname(fileURLToPath(import.meta.url));
17
17
  const PROFILE_FILE = join(__dirname, '..', 'dual-brain.profile.json');
18
18
  const LAUNCHED_MARKER = join(__dirname, '..', '.launched');
19
+ const VERSION_STAMP_FILE = join(__dirname, '..', 'dual-brain.version.json');
20
+ const UPDATE_CACHE_FILE = join(__dirname, '..', 'dual-brain.update-check.json');
21
+ const UPDATE_CACHE_TTL_MS = 60 * 60 * 1000;
19
22
  const VERSION = (() => {
23
+ try {
24
+ const stamp = JSON.parse(readFileSync(VERSION_STAMP_FILE, 'utf8'));
25
+ if (stamp.version) return stamp.version;
26
+ } catch {}
20
27
  try { return JSON.parse(readFileSync(join(__dirname, '..', 'package.json'), 'utf8')).version; } catch {}
21
28
  return '?';
22
29
  })();
@@ -37,6 +44,92 @@ const yellow = s => e('33', s);
37
44
  const orange = s => e('1;38;5;208', s);
38
45
  const blue = s => e('1;38;5;33', s);
39
46
 
47
+ function readJsonFile(path) {
48
+ try {
49
+ return JSON.parse(readFileSync(path, 'utf8'));
50
+ } catch {
51
+ return null;
52
+ }
53
+ }
54
+
55
+ function writeJsonFile(path, value) {
56
+ writeFileSync(path, JSON.stringify(value, null, 2) + '\n');
57
+ }
58
+
59
+ function compareVersions(a, b) {
60
+ const aParts = String(a || '').replace(/^v/i, '').split('.').map(n => parseInt(n, 10) || 0);
61
+ const bParts = String(b || '').replace(/^v/i, '').split('.').map(n => parseInt(n, 10) || 0);
62
+ const len = Math.max(aParts.length, bParts.length);
63
+ for (let i = 0; i < len; i++) {
64
+ const diff = (aParts[i] || 0) - (bParts[i] || 0);
65
+ if (diff !== 0) return diff;
66
+ }
67
+ return 0;
68
+ }
69
+
70
+ function getInstalledVersion() {
71
+ return readJsonFile(VERSION_STAMP_FILE)?.version || VERSION;
72
+ }
73
+
74
+ function getCachedUpdateStatus() {
75
+ const cache = readJsonFile(UPDATE_CACHE_FILE);
76
+ if (!cache?.checked_at) return null;
77
+ const age = Date.now() - Date.parse(cache.checked_at);
78
+ if (!Number.isFinite(age) || age < 0 || age > UPDATE_CACHE_TTL_MS) return null;
79
+ return cache;
80
+ }
81
+
82
+ function writeUpdateStatusCache(result) {
83
+ writeJsonFile(UPDATE_CACHE_FILE, {
84
+ checked_at: new Date().toISOString(),
85
+ ...result,
86
+ });
87
+ }
88
+
89
+ function checkForUpdate({ force = false } = {}) {
90
+ const installed = getInstalledVersion();
91
+
92
+ if (!force) {
93
+ const cached = getCachedUpdateStatus();
94
+ if (cached && cached.installed === installed) {
95
+ return {
96
+ updateAvailable: !!cached.updateAvailable,
97
+ installed: cached.installed,
98
+ latest: cached.latest || installed,
99
+ checkedAt: cached.checked_at,
100
+ };
101
+ }
102
+ }
103
+
104
+ try {
105
+ const result = spawnSync('npm', ['view', 'dual-brain', 'version', '--json'], {
106
+ encoding: 'utf8',
107
+ stdio: ['pipe', 'pipe', 'pipe'],
108
+ timeout: 5000,
109
+ });
110
+ if (result.status !== 0 || !result.stdout.trim()) return null;
111
+ const latestRaw = JSON.parse(result.stdout);
112
+ const latest = Array.isArray(latestRaw) ? latestRaw[latestRaw.length - 1] : latestRaw;
113
+ if (!latest) return null;
114
+ const payload = {
115
+ updateAvailable: compareVersions(latest, installed) > 0,
116
+ installed,
117
+ latest,
118
+ };
119
+ writeUpdateStatusCache(payload);
120
+ return payload;
121
+ } catch {
122
+ return null;
123
+ }
124
+ }
125
+
126
+ function formatVersionStatus(updateInfo) {
127
+ const installed = updateInfo?.installed || getInstalledVersion();
128
+ if (updateInfo?.updateAvailable && updateInfo.latest) return `v${installed} → v${updateInfo.latest} available`;
129
+ if (updateInfo?.latest && updateInfo.latest === installed) return `v${installed} (up to date)`;
130
+ return `v${installed}`;
131
+ }
132
+
40
133
  // ─── Profiles ──────────────────────────────────────────────────────────────
41
134
 
42
135
  const PROFILES = {
@@ -122,7 +215,8 @@ function detectProviders() {
122
215
  codex.installed = true;
123
216
  const login = spawnSync(codexCheck.stdout.trim(), ['login', 'status'], { encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'], timeout: 5000 });
124
217
  const out = ((login.stdout || '') + (login.stderr || '')).toLowerCase();
125
- if (login.status === 0 || out.includes('logged in') || out.includes('authenticated')) codex.authed = true;
218
+ const ok = login.status === 0 || (out.includes('logged in') && !out.includes('not logged in'));
219
+ if (ok) codex.authed = true;
126
220
  }
127
221
 
128
222
  return { claude, codex };
@@ -291,13 +385,240 @@ function balanceBar(claudePct, openaiPct, width = 20) {
291
385
  return `${cBar}${oBar} ${orange(claudePct + '%')} Claude · ${green(openaiPct + '%')} GPT`;
292
386
  }
293
387
 
388
+ // ─── Auth Detail Helpers ──────────────────────────────────────────────────
389
+
390
+ function getClaudeAuthDetail() {
391
+ const credPaths = [
392
+ join(HOME, '.claude', '.credentials.json'),
393
+ join(HOME, '.claude', 'credentials.json'),
394
+ join(CWD, '.replit-tools', '.claude-persistent', '.credentials.json'),
395
+ ];
396
+ for (const p of credPaths) {
397
+ try {
398
+ const cred = JSON.parse(readFileSync(p, 'utf8'));
399
+ if (cred.claudeAiOauth) {
400
+ const exp = cred.claudeAiOauth.expiresAt;
401
+ let expiryText = 'n/a';
402
+ if (exp) {
403
+ const remaining = exp - Date.now();
404
+ if (remaining <= 0) expiryText = 'expired';
405
+ else {
406
+ const h = Math.floor(remaining / 3600000);
407
+ const m = Math.floor((remaining % 3600000) / 60000);
408
+ expiryText = `${h}h ${m}m remaining`;
409
+ }
410
+ }
411
+ return { method: 'subscription (OAuth)', expiry: expiryText, storage: p.replace(HOME, '~') };
412
+ }
413
+ if (cred.apiKey) return { method: 'API key', expiry: 'n/a', storage: p.replace(HOME, '~') };
414
+ } catch {}
415
+ }
416
+ return { method: 'unknown', expiry: 'n/a', storage: 'n/a' };
417
+ }
418
+
419
+ function getCodexAuthDetail() {
420
+ const authPath = join(HOME, '.codex', 'auth.json');
421
+ try {
422
+ const stat = statSync(authPath);
423
+ return {
424
+ method: 'subscription (device-auth)',
425
+ lastRefresh: timeAgo(stat.mtimeMs),
426
+ storage: '~/.codex/auth.json',
427
+ };
428
+ } catch {}
429
+ return { method: 'unknown', lastRefresh: 'n/a', storage: 'n/a' };
430
+ }
431
+
432
+ // ─── Submenu: Auth ────────────────────────────────────────────────────────
433
+
434
+ async function showAuthMenu(rl, providers) {
435
+ const ask = () => new Promise(resolve => rl.question(' Choice: ', resolve));
436
+
437
+ while (true) {
438
+ const claudeDetail = getClaudeAuthDetail();
439
+ const codexDetail = getCodexAuthDetail();
440
+ const cStat = providers.claude.authed ? green('✅ authenticated') : yellow('❌ not authenticated');
441
+ const xStat = providers.codex.authed ? green('✅ authenticated') : yellow('❌ not authenticated');
442
+
443
+ console.log('');
444
+ console.log(` ${bold('🔑 Auth Management')}`);
445
+ console.log(' ' + '─'.repeat(44));
446
+ console.log(` 🟠 Claude ${cStat}`);
447
+ if (providers.claude.authed) {
448
+ console.log(` Method: ${dim(claudeDetail.method)}`);
449
+ console.log(` Expiry: ${dim(claudeDetail.expiry)}`);
450
+ console.log(` Storage: ${dim(claudeDetail.storage)}`);
451
+ }
452
+ console.log('');
453
+ console.log(` 🟢 Codex ${xStat}`);
454
+ if (providers.codex.authed) {
455
+ console.log(` Method: ${dim(codexDetail.method)}`);
456
+ console.log(` Refresh: ${dim(codexDetail.lastRefresh)}`);
457
+ console.log(` Storage: ${dim(codexDetail.storage)}`);
458
+ }
459
+ console.log('');
460
+ if (!providers.claude.authed) console.log(` ${bold('[j]')} Sign in to Claude`);
461
+ if (providers.codex.installed && !providers.codex.authed) console.log(` ${bold('[k]')} Sign in to Codex ${dim('(ChatGPT subscription)')}`);
462
+ if (providers.claude.authed || providers.codex.authed) console.log(` ${bold('[r]')} Refresh all tokens`);
463
+ console.log(` ${bold('[q]')} Back to main menu`);
464
+ console.log('');
465
+
466
+ const choice = (await ask()).trim().toLowerCase();
467
+ if (choice === 'q' || choice === '') return;
468
+
469
+ if (choice === 'j') {
470
+ console.log('');
471
+ spawnSync('claude', ['login'], { stdio: 'inherit' });
472
+ providers.claude.authed = true;
473
+ continue;
474
+ }
475
+ if (choice === 'k' && providers.codex.installed) {
476
+ const codexPath = spawnSync('which', ['codex'], { encoding: 'utf8', stdio: 'pipe', timeout: 3000 });
477
+ if (codexPath.status === 0) {
478
+ console.log('');
479
+ console.log(` Open: ${cyan('https://auth.openai.com/codex/device')}`);
480
+ console.log('');
481
+ spawnSync(codexPath.stdout.trim(), ['login', '--device-auth'], { stdio: 'inherit' });
482
+ providers.codex.authed = true;
483
+ }
484
+ continue;
485
+ }
486
+ if (choice === 'r') {
487
+ console.log('');
488
+ const refreshScript = join(CWD, '.replit-tools', 'scripts', 'claude-auth-refresh.sh');
489
+ if (existsSync(refreshScript)) {
490
+ console.log(' Refreshing Claude token...');
491
+ const r = spawnSync('bash', [refreshScript, '--force'], { encoding: 'utf8', stdio: 'pipe', timeout: 10000 });
492
+ console.log(` ${(r.stdout || '').trim() || 'Done'}`);
493
+ }
494
+ console.log(' Codex tokens refreshed on next API call.');
495
+ console.log('');
496
+ continue;
497
+ }
498
+ }
499
+ }
500
+
501
+ // ─── Submenu: Budget ──────────────────────────────────────────────────────
502
+
503
+ async function showBudgetMenu(rl) {
504
+ const ask = () => new Promise(resolve => rl.question(' Choice: ', resolve));
505
+
506
+ while (true) {
507
+ const profile = loadProfile();
508
+ const balance = loadProviderBalance();
509
+
510
+ console.log('');
511
+ console.log(` ${bold('💵 Budget & Spend')}`);
512
+ console.log(' ' + '─'.repeat(44));
513
+ console.log(` Session: ⚠️ $${profile.budgets.session_warn_usd} warn · 🛑 $${profile.budgets.session_limit_usd} limit`);
514
+ console.log(` Daily: ⚠️ $${profile.budgets.daily_warn_usd} warn · 🛑 $${profile.budgets.daily_limit_usd} limit`);
515
+ console.log('');
516
+ console.log(` Today: ${balance.total} calls · ${balance.label}`);
517
+ console.log(` ${balanceBar(balance.claude, balance.openai)}`);
518
+ console.log('');
519
+ console.log(` ${bold('[c]')} Change budget limits`);
520
+ console.log(` ${bold('[r]')} Full cost report`);
521
+ console.log(` ${bold('[q]')} Back to main menu`);
522
+ console.log('');
523
+
524
+ const choice = (await ask()).trim().toLowerCase();
525
+ if (choice === 'q' || choice === '') return;
526
+
527
+ if (choice === 'c') {
528
+ const sessionAns = await new Promise(r => rl.question(' New session limit ($): ', r));
529
+ const sessionVal = parseFloat(sessionAns);
530
+ if (isNaN(sessionVal) || sessionVal <= 0) { console.log(' Invalid number.'); continue; }
531
+ const dailyAns = await new Promise(r => rl.question(` New daily limit ($ default ${sessionVal * 3}): `, r));
532
+ const dailyVal = dailyAns.trim() ? parseFloat(dailyAns) : sessionVal * 3;
533
+ if (isNaN(dailyVal) || dailyVal <= 0) { console.log(' Invalid number.'); continue; }
534
+
535
+ const customOverrides = {
536
+ budgets: {
537
+ session_warn_usd: +(sessionVal * 0.6).toFixed(2),
538
+ session_limit_usd: sessionVal,
539
+ daily_warn_usd: +(dailyVal * 0.6).toFixed(2),
540
+ daily_limit_usd: dailyVal,
541
+ },
542
+ };
543
+ let existing = {};
544
+ try { existing = JSON.parse(readFileSync(PROFILE_FILE, 'utf8')); } catch {}
545
+ saveProfile(existing.active || 'auto', customOverrides);
546
+ console.log(` ✅ Budget updated: $${sessionVal}/session, $${dailyVal}/day`);
547
+ continue;
548
+ }
549
+
550
+ if (choice === 'r') {
551
+ console.log('');
552
+ spawnSync(process.execPath, [join(__dirname, 'cost-report.mjs')], { stdio: 'inherit' });
553
+ console.log('');
554
+ await new Promise(r => rl.question(' Press Enter to continue...', r));
555
+ continue;
556
+ }
557
+ }
558
+ }
559
+
560
+ // ─── Submenu: Tools Dashboard ─────────────────────────────────────────────
561
+
562
+ async function showToolsMenu(rl) {
563
+ const ask = () => new Promise(resolve => rl.question(' Choice: ', resolve));
564
+
565
+ while (true) {
566
+ const updateInfo = checkForUpdate();
567
+ console.log('');
568
+ console.log(` ${bold('🛠️ Tools & Diagnostics')}`);
569
+ console.log(' ' + '─'.repeat(44));
570
+ console.log(` ${bold('[1]')} Health check`);
571
+ console.log(` ${bold('[2]')} Cost report`);
572
+ console.log(` ${bold('[3]')} Decision ledger insights`);
573
+ console.log(` ${bold('[4]')} Run test suite (40 tests)`);
574
+ console.log(` ${bold('[5]')} Session report`);
575
+ console.log(` ${bold('[u]')} Update dual-brain ${dim('(' + formatVersionStatus(updateInfo) + ')')}`);
576
+ console.log(` ${bold('[q]')} Back to main menu`);
577
+ console.log('');
578
+
579
+ const choice = (await ask()).trim().toLowerCase();
580
+ if (choice === 'q' || choice === '') return;
581
+
582
+ const tools = {
583
+ '1': 'health-check.mjs',
584
+ '2': 'cost-report.mjs',
585
+ '3': 'decision-ledger.mjs',
586
+ '4': 'test-orchestrator.mjs',
587
+ '5': 'session-report.mjs',
588
+ };
589
+
590
+ if (tools[choice]) {
591
+ console.log('');
592
+ spawnSync(process.execPath, [join(__dirname, tools[choice])], { stdio: 'inherit' });
593
+ console.log('');
594
+ await new Promise(r => rl.question(' Press Enter to continue...', r));
595
+ continue;
596
+ }
597
+
598
+ if (choice === 'u') {
599
+ console.log('');
600
+ const result = spawnSync('npx', ['-y', 'dual-brain', 'update'], { stdio: 'inherit', cwd: CWD });
601
+ console.log('');
602
+ if (result.status === 0) {
603
+ console.log(' ✅ Dual-brain hooks refreshed.');
604
+ } else {
605
+ console.log(' ⚠️ Update did not complete.');
606
+ }
607
+ console.log('');
608
+ await new Promise(r => rl.question(' Press Enter to continue...', r));
609
+ }
610
+ }
611
+ }
612
+
294
613
  // ─── Menu Renderers ───────────────────────────────────────────────────────
295
614
 
296
615
  function renderFirstRunMenu(providers) {
297
616
  const lines = [];
617
+ const updateInfo = checkForUpdate();
298
618
 
299
619
  lines.push('');
300
620
  lines.push(` 🧠 ${bold(`Dual-Brain v${VERSION}`)}`);
621
+ lines.push(` ${dim(formatVersionStatus(updateInfo))}`);
301
622
  lines.push('');
302
623
 
303
624
  // Provider status
@@ -340,6 +661,8 @@ function renderFirstRunMenu(providers) {
340
661
 
341
662
  // Primary actions
342
663
  lines.push(` ${bold('[n]')} Start new session`);
664
+ lines.push(` ${bold('[a]')} Auth management`);
665
+ lines.push(` ${bold('[d]')} Dashboard & diagnostics`);
343
666
  lines.push(` ${bold('[s]')} Skip — just shell`);
344
667
  lines.push('');
345
668
 
@@ -351,10 +674,12 @@ function renderReturningMenu(providers, sessions) {
351
674
  const pf = PROFILES[profile.name];
352
675
  const running = countRunning();
353
676
  const balance = loadProviderBalance();
677
+ const updateInfo = checkForUpdate();
354
678
  const lines = [];
355
679
 
356
680
  lines.push('');
357
681
  lines.push(` 🧠 ${bold(`Dual-Brain v${VERSION}`)}`);
682
+ lines.push(` ${dim(formatVersionStatus(updateInfo))}`);
358
683
  lines.push('');
359
684
 
360
685
  // Provider status
@@ -398,18 +723,39 @@ function renderReturningMenu(providers, sessions) {
398
723
  if (running.codex > 0) runParts.push(`${running.codex} codex`);
399
724
  if (runParts.length > 0) lines.push(` ${dim('(' + runParts.join(', ') + ' running)')}`);
400
725
 
401
- // Menu options
726
+ // ── Sessions
727
+ lines.push(` ${dim('─── Sessions')}`);
402
728
  lines.push(` ${bold('[c]')} Continue last session`);
403
729
  if (sessions.length > 0) lines.push(` ${bold('[1-9]')} Resume numbered above`);
404
730
  lines.push(` ${bold('[n]')} New session`);
731
+
732
+ // ── Settings
733
+ lines.push('');
734
+ lines.push(` ${dim('─── Settings')}`);
405
735
  lines.push(` ${bold('[p]')} Mode: ${dim(pf.uiLabel)}`);
736
+ lines.push(` ${bold('[b]')} Budget: ${dim('$' + profile.budgets.session_limit_usd + '/session, $' + profile.budgets.daily_limit_usd + '/day')}`);
737
+
738
+ // ── Auth
739
+ lines.push('');
740
+ const authSummary = providers.claude.authed && providers.codex.authed
741
+ ? green('both connected')
742
+ : providers.claude.authed ? yellow('Claude only')
743
+ : yellow('needs setup');
744
+ lines.push(` ${dim('─── Auth')}`);
745
+ lines.push(` ${bold('[a]')} Auth management ${dim('(' + authSummary + ')')}`);
746
+
747
+ // ── Tools
748
+ lines.push('');
749
+ lines.push(` ${dim('─── Tools')}`);
750
+ lines.push(` ${bold('[d]')} Dashboard & diagnostics`);
751
+ lines.push(` ${bold('[u]')} Update dual-brain ${dim('(' + formatVersionStatus(updateInfo) + ')')}`);
406
752
 
407
- // Auth if needed
408
- if (!providers.claude.authed) lines.push(` ${bold('[j]')} Sign in to Claude`);
409
- if (providers.codex.installed && !providers.codex.authed) lines.push(` ${bold('[k]')} Sign in to Codex`);
410
- if (IS_REPLIT && !existsSync(join(CWD, '.replit-tools'))) lines.push(` ${bold('[t]')} Install replit-tools`);
753
+ if (IS_REPLIT && !existsSync(join(CWD, '.replit-tools'))) {
754
+ lines.push(` ${bold('[t]')} Install replit-tools`);
755
+ }
411
756
 
412
- lines.push(` ${bold('[s]')} Shell`);
757
+ lines.push('');
758
+ lines.push(` ${bold('[s]')} Exit to shell`);
413
759
  lines.push('');
414
760
 
415
761
  return lines;
@@ -553,6 +899,29 @@ async function mainLoop() {
553
899
  continue;
554
900
  }
555
901
 
902
+ if (choice === 'a') {
903
+ await showAuthMenu(rl, providers);
904
+ continue;
905
+ }
906
+
907
+ if (choice === 'b') {
908
+ await showBudgetMenu(rl);
909
+ continue;
910
+ }
911
+
912
+ if (choice === 'd') {
913
+ await showToolsMenu(rl);
914
+ continue;
915
+ }
916
+
917
+ if (choice === 'u') {
918
+ console.log('');
919
+ console.log(' Updating dual-brain...');
920
+ console.log('');
921
+ spawnSync('npx', ['-y', 'dual-brain', 'update'], { stdio: 'inherit', cwd: CWD });
922
+ continue;
923
+ }
924
+
556
925
  if (choice === 'j') {
557
926
  console.log('');
558
927
  console.log(' Starting Claude login...');
@@ -573,7 +942,9 @@ async function mainLoop() {
573
942
  console.log('');
574
943
  console.log(' Starting Codex login...');
575
944
  console.log('');
576
- spawnSync(codexPath.stdout.trim(), ['login'], { stdio: 'inherit' });
945
+ console.log(` Open: ${cyan('https://auth.openai.com/codex/device')}`);
946
+ console.log('');
947
+ spawnSync(codexPath.stdout.trim(), ['login', '--device-auth'], { stdio: 'inherit' });
577
948
  continue;
578
949
  }
579
950
 
@@ -11,7 +11,6 @@
11
11
  import { appendFileSync, mkdirSync, readFileSync, writeFileSync } from "fs";
12
12
  import { dirname, join } from "path";
13
13
  import { fileURLToPath } from "url";
14
- import { logHookError } from './error-channel.mjs';
15
14
 
16
15
  const __dirname = dirname(fileURLToPath(import.meta.url));
17
16
  const PROFILE_FILE = join(__dirname, '..', 'dual-brain.profile.json');
@@ -167,11 +166,11 @@ async function checkBudget() {
167
166
  } catch {}
168
167
 
169
168
  // Use summary checkpoint for fast budget check (O(1) instead of full scan)
170
- let activityScore = 0;
169
+ let totalCost = 0;
171
170
  try {
172
171
  const { readSummary } = await import('./summary-checkpoint.mjs');
173
172
  const summary = readSummary();
174
- activityScore = summary.totals.activity_score || 0;
173
+ totalCost = summary.totals.cost_estimate;
175
174
  } catch {
176
175
  // Fallback: scan the log (only if summary unavailable)
177
176
  const todayFile = usageFile();
@@ -181,26 +180,15 @@ async function checkBudget() {
181
180
  try { return JSON.parse(l); } catch { return null; }
182
181
  }).filter(Boolean);
183
182
  } catch { return null; }
184
- const TIER_WEIGHTS = { search: 3, execute: 10, think: 25 };
185
- const rawActivity = records.reduce((sum, r) => {
186
- if (r.input_tokens != null && r.output_tokens != null) {
187
- return sum + (r.input_tokens * 1) + (r.output_tokens * 3);
188
- }
189
- return sum + (TIER_WEIGHTS[r.tier] || TIER_WEIGHTS.execute);
190
- }, 0);
191
- activityScore = Math.min(100, Math.round((rawActivity / 5_000_000) * 100));
183
+ const RATES = { search: 0.003, execute: 0.012, think: 0.055 };
184
+ totalCost = records.reduce((sum, r) => sum + (RATES[r.tier] || RATES.execute), 0);
192
185
  }
193
186
 
194
- // Budget thresholds use activity score (0-100) instead of dollar amounts.
195
- // Falls back to legacy daily_limit_usd / daily_warn_usd field names for compat.
196
- const activityLimit = budgets.daily_activity_limit || (budgets.daily_limit_usd ? 85 : null);
197
- const activityWarn = budgets.daily_activity_warn || (budgets.daily_warn_usd ? 65 : null);
198
-
199
187
  let msg = null;
200
- if (activityLimit && activityScore >= activityLimit) {
201
- msg = `**[Activity Alert]** Session activity score (${activityScore}/100) has reached the limit. Consider pausing non-essential work.`;
202
- } else if (activityWarn && activityScore >= activityWarn) {
203
- msg = `**[Activity Alert]** Session activity score (${activityScore}/100) has passed the warning threshold.`;
188
+ if (budgets.daily_limit_usd && totalCost >= budgets.daily_limit_usd) {
189
+ msg = `**[Budget Alert]** Daily cost estimate (~$${totalCost.toFixed(2)}) has reached the $${budgets.daily_limit_usd} limit. Consider pausing non-essential work.`;
190
+ } else if (budgets.daily_warn_usd && totalCost >= budgets.daily_warn_usd) {
191
+ msg = `**[Budget Alert]** Daily cost estimate (~$${totalCost.toFixed(2)}) has passed the $${budgets.daily_warn_usd} warning threshold.`;
204
192
  }
205
193
 
206
194
  if (msg) {
@@ -234,14 +222,6 @@ async function main() {
234
222
  }
235
223
 
236
224
  const toolName = payload?.tool_name || payload?.toolName || "unknown";
237
-
238
- // Early exit for high-frequency read-only tools — not worth logging
239
- const READ_ONLY_TOOLS = new Set(["Read", "Grep", "Glob", "LS", "ListDir"]);
240
- if (READ_ONLY_TOOLS.has(toolName)) {
241
- process.stdout.write("{}\n");
242
- process.exit(0);
243
- }
244
-
245
225
  const toolInput = payload?.tool_input || payload?.toolInput || {};
246
226
  const agentModel = payload?.model || payload?.agent_model || null;
247
227
 
@@ -273,13 +253,13 @@ async function main() {
273
253
 
274
254
  try {
275
255
  appendFileSync(usageFile(), entry + "\n", { encoding: "utf8", flag: "a" });
276
- } catch (e) { logHookError('cost-logger', 'usage log write', e); }
256
+ } catch {}
277
257
 
278
258
  // Update summary checkpoint (non-blocking, best-effort)
279
259
  try {
280
260
  const { updateSummary } = await import('./summary-checkpoint.mjs');
281
261
  updateSummary(entryObj);
282
- } catch (e) { logHookError('cost-logger', 'summary checkpoint update', e); }
262
+ } catch {}
283
263
 
284
264
  // Record failures for adaptive routing (failure-loop detection)
285
265
  if (status === 'error' && toolName === 'Agent') {
@@ -289,29 +269,7 @@ async function main() {
289
269
  recordFailure(promptHash, tier, payload?.error || 'agent_error');
290
270
  // Best-effort cleanup of stale failure entries (>24h old)
291
271
  try { pruneOldFailures(); } catch {}
292
- } catch (e) { logHookError('cost-logger', 'failure recording', e); }
293
- }
294
-
295
- // Record outcomes (success + failure) to decision ledger for routing feedback
296
- if (toolName === 'Agent') {
297
- try {
298
- const { computePromptHash } = await import('./failure-detector.mjs');
299
- const { recordDecision, recordOutcome } = await import('./decision-ledger.mjs');
300
- const promptHash = computePromptHash(toolInput);
301
- const decisionId = recordDecision({
302
- tier,
303
- provider: detectProvider(model),
304
- model,
305
- prompt_hash: promptHash,
306
- profile: loadActiveProfile(),
307
- session_id: SESSION_ID,
308
- });
309
- recordOutcome(decisionId, {
310
- success: status !== 'error',
311
- actual_input_tokens: inputTokens,
312
- actual_output_tokens: outputTokens,
313
- });
314
- } catch (e) { logHookError('cost-logger', 'decision ledger recording', e); }
272
+ } catch {}
315
273
  }
316
274
 
317
275
  const budgetMsg = await checkBudget();