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.
- package/CLAUDE.md +35 -130
- package/README.md +34 -179
- package/hooks/control-panel.mjs +379 -8
- package/hooks/cost-logger.mjs +11 -53
- package/hooks/cost-report.mjs +126 -65
- package/hooks/decision-ledger.mjs +3 -53
- package/hooks/dual-brain-review.mjs +25 -261
- package/hooks/dual-brain-think.mjs +37 -300
- package/hooks/enforce-tier.mjs +93 -265
- package/hooks/failure-detector.mjs +1 -3
- package/hooks/gpt-work-dispatcher.mjs +153 -12
- package/hooks/health-check.mjs +25 -17
- package/hooks/quality-gate.mjs +11 -6
- package/hooks/risk-classifier.mjs +2 -135
- package/hooks/session-report.mjs +71 -41
- package/hooks/summary-checkpoint.mjs +8 -35
- package/hooks/test-orchestrator.mjs +31 -2080
- package/install.mjs +628 -1557
- package/orchestrator.json +96 -73
- package/package.json +2 -7
- package/hooks/agent-chains.mjs +0 -369
- package/hooks/agent-templates.mjs +0 -441
- package/hooks/atomic-write.mjs +0 -109
- package/hooks/config-validator.mjs +0 -156
- package/hooks/confirmation-policy.mjs +0 -167
- package/hooks/error-channel.mjs +0 -68
- package/hooks/ship-captain.mjs +0 -1176
- package/hooks/ship-gate.mjs +0 -971
package/hooks/control-panel.mjs
CHANGED
|
@@ -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
|
-
|
|
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
|
-
//
|
|
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
|
-
|
|
408
|
-
|
|
409
|
-
|
|
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(
|
|
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
|
-
|
|
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
|
|
package/hooks/cost-logger.mjs
CHANGED
|
@@ -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
|
|
169
|
+
let totalCost = 0;
|
|
171
170
|
try {
|
|
172
171
|
const { readSummary } = await import('./summary-checkpoint.mjs');
|
|
173
172
|
const summary = readSummary();
|
|
174
|
-
|
|
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
|
|
185
|
-
|
|
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 (
|
|
201
|
-
msg = `**[
|
|
202
|
-
} else if (
|
|
203
|
-
msg = `**[
|
|
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
|
|
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
|
|
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
|
|
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();
|