dual-brain 3.5.0 → 3.6.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/hooks/control-panel.mjs +56 -61
- package/hooks/profiles.mjs +3 -3
- package/install.mjs +21 -18
- package/package.json +1 -1
package/hooks/control-panel.mjs
CHANGED
|
@@ -40,9 +40,9 @@ const blue = s => e('1;38;5;33', s);
|
|
|
40
40
|
// ─── Profiles ──────────────────────────────────────────────────────────────
|
|
41
41
|
|
|
42
42
|
const PROFILES = {
|
|
43
|
-
balanced: { emoji: '⚖️', uiLabel: 'Default', desc: 'Auto-routes by complexity,
|
|
44
|
-
'cost-saver': { emoji: '
|
|
45
|
-
'quality-first': { emoji: '
|
|
43
|
+
balanced: { emoji: '⚖️', uiLabel: 'Default', desc: 'Auto-routes by complexity, uses both providers evenly' },
|
|
44
|
+
'cost-saver': { emoji: '🛡️', uiLabel: 'Conservative', desc: 'Fewer GPT dispatches, sticks to Claude for most work' },
|
|
45
|
+
'quality-first': { emoji: '🚀', uiLabel: 'Aggressive', desc: 'Maximizes both subscriptions, dual-brain for medium+ risk' },
|
|
46
46
|
};
|
|
47
47
|
|
|
48
48
|
const PROFILE_BUDGETS = {
|
|
@@ -245,14 +245,48 @@ function countRunning() {
|
|
|
245
245
|
return { claude, codex };
|
|
246
246
|
}
|
|
247
247
|
|
|
248
|
-
// ───
|
|
248
|
+
// ─── Provider Balance ─────────────────────────────────────────────────────
|
|
249
249
|
|
|
250
|
-
function
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
if (
|
|
254
|
-
|
|
255
|
-
|
|
250
|
+
function loadProviderBalance() {
|
|
251
|
+
const today = new Date().toISOString().slice(0, 10);
|
|
252
|
+
const logFile = join(__dirname, `usage-${today}.jsonl`);
|
|
253
|
+
if (!existsSync(logFile)) return { claude: 0, openai: 0, total: 0, label: 'No activity yet' };
|
|
254
|
+
|
|
255
|
+
let claude = 0, openai = 0;
|
|
256
|
+
try {
|
|
257
|
+
const lines = readFileSync(logFile, 'utf8').split('\n').filter(Boolean);
|
|
258
|
+
for (const line of lines) {
|
|
259
|
+
try {
|
|
260
|
+
const e = JSON.parse(line);
|
|
261
|
+
if (e.provider === 'claude') claude++;
|
|
262
|
+
else if (e.provider === 'openai') openai++;
|
|
263
|
+
} catch {}
|
|
264
|
+
}
|
|
265
|
+
} catch {}
|
|
266
|
+
|
|
267
|
+
const total = claude + openai;
|
|
268
|
+
if (total === 0) return { claude: 0, openai: 0, total: 0, label: 'No activity yet' };
|
|
269
|
+
|
|
270
|
+
const claudePct = Math.round((claude / total) * 100);
|
|
271
|
+
const openaiPct = 100 - claudePct;
|
|
272
|
+
|
|
273
|
+
let label;
|
|
274
|
+
if (openaiPct === 0) label = 'Claude only — GPT subscription unused';
|
|
275
|
+
else if (claudePct === 0) label = 'GPT only — Claude subscription unused';
|
|
276
|
+
else if (Math.abs(claudePct - openaiPct) <= 20) label = 'Well balanced';
|
|
277
|
+
else if (claudePct > openaiPct) label = `Claude-heavy — GPT has capacity`;
|
|
278
|
+
else label = `GPT-heavy — Claude has capacity`;
|
|
279
|
+
|
|
280
|
+
return { claude: claudePct, openai: openaiPct, total, label };
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
function balanceBar(claudePct, openaiPct, width = 20) {
|
|
284
|
+
if (claudePct === 0 && openaiPct === 0) return dim('░'.repeat(width) + ' no activity');
|
|
285
|
+
const cFill = Math.round((claudePct / 100) * width);
|
|
286
|
+
const oFill = width - cFill;
|
|
287
|
+
const cBar = noColor ? '█'.repeat(cFill) : `\x1b[38;5;208m${'█'.repeat(cFill)}\x1b[0m`;
|
|
288
|
+
const oBar = noColor ? '▓'.repeat(oFill) : `\x1b[32m${'▓'.repeat(oFill)}\x1b[0m`;
|
|
289
|
+
return `${cBar}${oBar} ${orange(claudePct + '%')} Claude · ${green(openaiPct + '%')} GPT`;
|
|
256
290
|
}
|
|
257
291
|
|
|
258
292
|
// ─── Menu Renderers ───────────────────────────────────────────────────────
|
|
@@ -314,17 +348,22 @@ function renderReturningMenu(providers, sessions) {
|
|
|
314
348
|
const profile = loadProfile();
|
|
315
349
|
const pf = PROFILES[profile.name];
|
|
316
350
|
const running = countRunning();
|
|
351
|
+
const balance = loadProviderBalance();
|
|
317
352
|
const lines = [];
|
|
318
353
|
|
|
319
354
|
lines.push('');
|
|
320
355
|
lines.push(` 🧠 ${bold(`Dual-Brain v${VERSION}`)}`);
|
|
321
356
|
lines.push('');
|
|
322
357
|
|
|
323
|
-
//
|
|
358
|
+
// Provider status
|
|
324
359
|
const cStat = providers.claude.authed ? '✅' : '⚠️';
|
|
325
360
|
const xStat = providers.codex.authed ? '✅' : providers.codex.installed ? '⚠️' : '❌';
|
|
326
361
|
lines.push(` 🟠 Claude ${cStat} 🟢 Codex ${xStat} ${pf.emoji} ${bold(pf.uiLabel)}`);
|
|
327
362
|
|
|
363
|
+
// Provider balance bar
|
|
364
|
+
lines.push(` ${balanceBar(balance.claude, balance.openai)}`);
|
|
365
|
+
if (balance.total > 0) lines.push(` ${dim(balance.label + ' · ' + balance.total + ' calls today')}`);
|
|
366
|
+
|
|
328
367
|
// Recent sessions
|
|
329
368
|
if (sessions.length > 0) {
|
|
330
369
|
lines.push('');
|
|
@@ -350,7 +389,6 @@ function renderReturningMenu(providers, sessions) {
|
|
|
350
389
|
if (sessions.length > 0) lines.push(` ${bold('[1-9]')} Resume numbered above`);
|
|
351
390
|
lines.push(` ${bold('[n]')} New session`);
|
|
352
391
|
lines.push(` ${bold('[p]')} Mode: ${dim(pf.uiLabel)}`);
|
|
353
|
-
lines.push(` ${bold('[b]')} Cost alerts: ${dim(costAlertLabel(profile))}`);
|
|
354
392
|
|
|
355
393
|
// Auth if needed
|
|
356
394
|
if (!providers.claude.authed) lines.push(` ${bold('[j]')} Sign in to Claude`);
|
|
@@ -368,8 +406,12 @@ function renderReturningMenu(providers, sessions) {
|
|
|
368
406
|
function showProfilePicker(rl) {
|
|
369
407
|
return new Promise((resolve) => {
|
|
370
408
|
const current = loadProfile();
|
|
409
|
+
const balance = loadProviderBalance();
|
|
371
410
|
console.log('');
|
|
372
|
-
console.log(` ${bold('Switch mode:')}`);
|
|
411
|
+
console.log(` ${bold('Switch routing mode:')}`);
|
|
412
|
+
if (balance.total > 0) {
|
|
413
|
+
console.log(` ${dim('Current balance: Claude ' + balance.claude + '% / GPT ' + balance.openai + '% · ' + balance.label)}`);
|
|
414
|
+
}
|
|
373
415
|
console.log('');
|
|
374
416
|
for (const [i, [name, pf]] of Object.entries(PROFILES).entries()) {
|
|
375
417
|
const active = name === current.name ? ' ✅' : '';
|
|
@@ -396,49 +438,7 @@ function showProfilePicker(rl) {
|
|
|
396
438
|
});
|
|
397
439
|
}
|
|
398
440
|
|
|
399
|
-
//
|
|
400
|
-
|
|
401
|
-
function showCostAlertEditor(rl) {
|
|
402
|
-
return new Promise((resolve) => {
|
|
403
|
-
const profile = loadProfile();
|
|
404
|
-
console.log('');
|
|
405
|
-
console.log(` ${bold('Cost alerts')}`);
|
|
406
|
-
console.log(` ${dim('Dual-brain estimates API costs from session activity.')}`);
|
|
407
|
-
console.log(` ${dim('These are alerts, not billing caps.')}`);
|
|
408
|
-
console.log('');
|
|
409
|
-
console.log(` Current: warn at $${profile.budgets.session_warn_usd}/session, $${profile.budgets.daily_warn_usd}/day`);
|
|
410
|
-
console.log(` limit at $${profile.budgets.session_limit_usd}/session, $${profile.budgets.daily_limit_usd}/day`);
|
|
411
|
-
console.log('');
|
|
412
|
-
|
|
413
|
-
rl.question(' Session alert limit ($, Enter = keep): ', (sessionStr) => {
|
|
414
|
-
if (!sessionStr.trim()) return resolve();
|
|
415
|
-
const session = parseFloat(sessionStr);
|
|
416
|
-
if (isNaN(session) || session <= 0) { console.log(' Cancelled.'); return resolve(); }
|
|
417
|
-
|
|
418
|
-
rl.question(' Daily alert limit ($, Enter = auto): ', (dailyStr) => {
|
|
419
|
-
const daily = parseFloat(dailyStr);
|
|
420
|
-
const finalDaily = (isNaN(daily) || daily <= 0) ? session * 3 : daily;
|
|
421
|
-
|
|
422
|
-
let existing = {};
|
|
423
|
-
try { existing = JSON.parse(readFileSync(PROFILE_FILE, 'utf8')); } catch {}
|
|
424
|
-
const custom = existing.custom_overrides || {};
|
|
425
|
-
custom.budgets = {
|
|
426
|
-
session_warn_usd: +(session * 0.6).toFixed(2),
|
|
427
|
-
session_limit_usd: session,
|
|
428
|
-
daily_warn_usd: +(finalDaily * 0.6).toFixed(2),
|
|
429
|
-
daily_limit_usd: finalDaily,
|
|
430
|
-
};
|
|
431
|
-
const data = { active: existing.active || 'balanced', switched_at: existing.switched_at || new Date().toISOString(), custom_overrides: custom };
|
|
432
|
-
const tmp = PROFILE_FILE + '.tmp.' + process.pid;
|
|
433
|
-
writeFileSync(tmp, JSON.stringify(data, null, 2) + '\n');
|
|
434
|
-
renameSync(tmp, PROFILE_FILE);
|
|
435
|
-
|
|
436
|
-
console.log(` ✅ Cost alerts: $${session}/session · $${finalDaily}/day`);
|
|
437
|
-
resolve();
|
|
438
|
-
});
|
|
439
|
-
});
|
|
440
|
-
});
|
|
441
|
-
}
|
|
441
|
+
// (Cost alert editor removed — replaced by provider balance + mode switching)
|
|
442
442
|
|
|
443
443
|
// ─── Session Runner ───────────────────────────────────────────────────────
|
|
444
444
|
|
|
@@ -514,11 +514,6 @@ async function mainLoop() {
|
|
|
514
514
|
continue;
|
|
515
515
|
}
|
|
516
516
|
|
|
517
|
-
if (choice === 'b') {
|
|
518
|
-
await showCostAlertEditor(rl);
|
|
519
|
-
continue;
|
|
520
|
-
}
|
|
521
|
-
|
|
522
517
|
if (choice === 'j') {
|
|
523
518
|
console.log('');
|
|
524
519
|
console.log(' Starting Claude login...');
|
package/hooks/profiles.mjs
CHANGED
|
@@ -22,7 +22,7 @@ const CONFIG_FILE = join(__dirname, '..', 'orchestrator.json');
|
|
|
22
22
|
|
|
23
23
|
const PROFILES = {
|
|
24
24
|
balanced: {
|
|
25
|
-
description: '
|
|
25
|
+
description: 'Auto-routes by complexity, uses both providers evenly',
|
|
26
26
|
routing: {
|
|
27
27
|
prefer_provider: 'auto',
|
|
28
28
|
think_threshold: 'normal',
|
|
@@ -42,7 +42,7 @@ const PROFILES = {
|
|
|
42
42
|
},
|
|
43
43
|
|
|
44
44
|
'cost-saver': {
|
|
45
|
-
description: '
|
|
45
|
+
description: 'Conservative — fewer GPT dispatches, sticks to Claude',
|
|
46
46
|
routing: {
|
|
47
47
|
prefer_provider: 'cheapest',
|
|
48
48
|
think_threshold: 'strict',
|
|
@@ -65,7 +65,7 @@ const PROFILES = {
|
|
|
65
65
|
},
|
|
66
66
|
|
|
67
67
|
'quality-first': {
|
|
68
|
-
description: '
|
|
68
|
+
description: 'Aggressive — maximizes both subscriptions, dual-brain for medium+',
|
|
69
69
|
routing: {
|
|
70
70
|
prefer_provider: 'most-capable',
|
|
71
71
|
think_threshold: 'relaxed',
|
package/install.mjs
CHANGED
|
@@ -57,10 +57,10 @@ if (flag('--help') || flag('-h')) {
|
|
|
57
57
|
--json Output detection as JSON
|
|
58
58
|
--help Show this help
|
|
59
59
|
|
|
60
|
-
🎛️
|
|
61
|
-
⚖️
|
|
62
|
-
|
|
63
|
-
|
|
60
|
+
🎛️ Routing modes:
|
|
61
|
+
⚖️ Default Auto-routes, uses both providers evenly
|
|
62
|
+
🛡️ Conservative Fewer GPT dispatches, sticks to Claude
|
|
63
|
+
🚀 Aggressive Maximizes both subscriptions, dual-brain for medium+
|
|
64
64
|
|
|
65
65
|
🚀 Examples:
|
|
66
66
|
${cmd('npx dual-brain')} # install or update
|
|
@@ -424,19 +424,19 @@ function profilePath(workspace) {
|
|
|
424
424
|
|
|
425
425
|
const PROFILES = {
|
|
426
426
|
balanced: {
|
|
427
|
-
description: '
|
|
427
|
+
description: 'Auto-routes by complexity, uses both providers evenly',
|
|
428
428
|
routing: { prefer_provider: 'auto', think_threshold: 'normal', gpt_dispatch_bias: 0 },
|
|
429
429
|
budgets: { session_warn_usd: 5, session_limit_usd: 10, daily_warn_usd: 20, daily_limit_usd: 50 },
|
|
430
430
|
quality_gate: { sensitivity_floor: 'medium', dual_brain_minimum: 'high' },
|
|
431
431
|
},
|
|
432
432
|
'cost-saver': {
|
|
433
|
-
description: '
|
|
433
|
+
description: 'Conservative — fewer GPT dispatches, sticks to Claude',
|
|
434
434
|
routing: { prefer_provider: 'cheapest', think_threshold: 'strict', gpt_dispatch_bias: -20 },
|
|
435
435
|
budgets: { session_warn_usd: 2, session_limit_usd: 5, daily_warn_usd: 8, daily_limit_usd: 20 },
|
|
436
436
|
quality_gate: { sensitivity_floor: 'high', dual_brain_minimum: 'critical' },
|
|
437
437
|
},
|
|
438
438
|
'quality-first': {
|
|
439
|
-
description: '
|
|
439
|
+
description: 'Aggressive — maximizes both subscriptions, dual-brain for medium+',
|
|
440
440
|
routing: { prefer_provider: 'most-capable', think_threshold: 'relaxed', gpt_dispatch_bias: 10 },
|
|
441
441
|
budgets: { session_warn_usd: 15, session_limit_usd: 30, daily_warn_usd: 50, daily_limit_usd: 100 },
|
|
442
442
|
quality_gate: { sensitivity_floor: 'low', dual_brain_minimum: 'medium' },
|
|
@@ -490,16 +490,18 @@ function cmdMode() {
|
|
|
490
490
|
|
|
491
491
|
if (!modeArg || modeArg === 'list') {
|
|
492
492
|
const current = loadProfile(workspace);
|
|
493
|
-
const PEMOJIS = { balanced: '⚖️ ', 'cost-saver': '
|
|
493
|
+
const PEMOJIS = { balanced: '⚖️ ', 'cost-saver': '🛡️', 'quality-first': '🚀' };
|
|
494
|
+
const UI_NAMES = { balanced: 'Default', 'cost-saver': 'Conservative', 'quality-first': 'Aggressive' };
|
|
494
495
|
console.log('');
|
|
495
|
-
console.log(' 🎛️
|
|
496
|
+
console.log(' 🎛️ Routing modes:');
|
|
496
497
|
console.log('');
|
|
497
498
|
for (const [name, p] of Object.entries(PROFILES)) {
|
|
498
499
|
const active = name === current.name ? ' ✅ active' : '';
|
|
499
|
-
|
|
500
|
+
const label = UI_NAMES[name] || name;
|
|
501
|
+
console.log(` ${PEMOJIS[name] || ' '} ${label.padEnd(15)} ${p.description}${active}`);
|
|
500
502
|
}
|
|
501
503
|
console.log('');
|
|
502
|
-
console.log(` Switch: ${cmd('npx dual-brain mode <
|
|
504
|
+
console.log(` Switch: ${cmd('npx dual-brain mode <name>')}`);
|
|
503
505
|
console.log('');
|
|
504
506
|
return;
|
|
505
507
|
}
|
|
@@ -522,9 +524,10 @@ function cmdMode() {
|
|
|
522
524
|
|
|
523
525
|
saveProfile(workspace, modeArg, customOverrides);
|
|
524
526
|
|
|
525
|
-
const PEMOJIS = { balanced: '⚖️ ', 'cost-saver': '
|
|
527
|
+
const PEMOJIS = { balanced: '⚖️ ', 'cost-saver': '🛡️', 'quality-first': '🚀' };
|
|
528
|
+
const UI_NAMES = { balanced: 'Default', 'cost-saver': 'Conservative', 'quality-first': 'Aggressive' };
|
|
526
529
|
console.log('');
|
|
527
|
-
console.log(` ✅
|
|
530
|
+
console.log(` ✅ Mode switched: ${PEMOJIS[modeArg] || ''} ${UI_NAMES[modeArg] || modeArg}`);
|
|
528
531
|
console.log(` ${profile.description}`);
|
|
529
532
|
console.log('');
|
|
530
533
|
console.log(' 🧭 Routing changes:');
|
|
@@ -547,12 +550,12 @@ function cmdBudget() {
|
|
|
547
550
|
if (sessionArg == null) {
|
|
548
551
|
const profile = loadProfile(workspace);
|
|
549
552
|
console.log('');
|
|
550
|
-
console.log('
|
|
551
|
-
console.log(` Session: ⚠️ $${profile.budgets.session_warn_usd} warn · 🛑 $${profile.budgets.session_limit_usd}
|
|
552
|
-
console.log(` Daily: ⚠️ $${profile.budgets.daily_warn_usd} warn · 🛑 $${profile.budgets.daily_limit_usd}
|
|
553
|
+
console.log(' 📊 Usage alert thresholds (estimated, not billing caps):');
|
|
554
|
+
console.log(` Session: ⚠️ $${profile.budgets.session_warn_usd} warn · 🛑 $${profile.budgets.session_limit_usd} alert`);
|
|
555
|
+
console.log(` Daily: ⚠️ $${profile.budgets.daily_warn_usd} warn · 🛑 $${profile.budgets.daily_limit_usd} alert`);
|
|
553
556
|
console.log('');
|
|
554
|
-
console.log(`
|
|
555
|
-
console.log(` Example:
|
|
557
|
+
console.log(` Adjust: ${cmd('npx dual-brain budget <session$> [daily$]')}`);
|
|
558
|
+
console.log(` Example: ${cmd('npx dual-brain budget 8 25')}`);
|
|
556
559
|
console.log('');
|
|
557
560
|
return;
|
|
558
561
|
}
|
package/package.json
CHANGED