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.
@@ -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, standard alerts' },
44
- 'cost-saver': { emoji: '💸', uiLabel: 'Cost-saver', desc: 'Prefers cheaper models, tighter alerts' },
45
- 'quality-first': { emoji: '💎', uiLabel: 'Quality-first', desc: 'Uses best models, dual-brain for medium+ risk' },
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
- // ─── Cost Alert Label ─────────────────────────────────────────────────────
248
+ // ─── Provider Balance ─────────────────────────────────────────────────────
249
249
 
250
- function costAlertLabel(profile) {
251
- if (profile.hasCustomBudget) return 'Custom';
252
- if (profile.name === 'balanced') return 'Default';
253
- if (profile.name === 'cost-saver') return 'Tight';
254
- if (profile.name === 'quality-first') return 'Relaxed';
255
- return 'Default';
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
- // Compact provider + mode line
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
- // ─── Cost Alert Editor ────────────────────────────────────────────────────
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...');
@@ -22,7 +22,7 @@ const CONFIG_FILE = join(__dirname, '..', 'orchestrator.json');
22
22
 
23
23
  const PROFILES = {
24
24
  balanced: {
25
- description: 'Standard routing best model for each tier, normal budgets',
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: 'Minimize spend prefer cheaper models, skip GPT for low risk',
45
+ description: 'Conservativefewer 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: 'Maximum quality — dual-brain for medium+, stricter reviews',
68
+ description: 'Aggressivemaximizes 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
- 🎛️ Profiles:
61
- ⚖️ balanced Standard routing best model per tier
62
- 💸 cost-saver Minimize spend prefer cheaper models
63
- 💎 quality-first Maximum quality dual-brain for medium+
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: 'Standard routing best model for each tier, normal budgets',
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: 'Minimize spend prefer cheaper models, skip GPT for low risk',
433
+ description: 'Conservativefewer 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: 'Maximum quality — dual-brain for medium+, stricter reviews',
439
+ description: 'Aggressivemaximizes 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': '💸', 'quality-first': '💎' };
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(' 🎛️ Profiles:');
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
- console.log(` ${PEMOJIS[name] || ' '} ${name.padEnd(15)} ${p.description}${active}`);
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 <profile>')}`);
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': '💸', 'quality-first': '💎' };
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(` ✅ Profile switched: ${PEMOJIS[modeArg] || ''} ${modeArg}`);
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(' 💵 Current budget:');
551
- console.log(` Session: ⚠️ $${profile.budgets.session_warn_usd} warn · 🛑 $${profile.budgets.session_limit_usd} limit`);
552
- console.log(` Daily: ⚠️ $${profile.budgets.daily_warn_usd} warn · 🛑 $${profile.budgets.daily_limit_usd} limit`);
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(` Set limits: ${cmd('npx dual-brain budget <session$> [daily$]')}`);
555
- console.log(` Example: ${cmd('npx dual-brain budget 8 25')}`);
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dual-brain",
3
- "version": "3.5.0",
3
+ "version": "3.6.0",
4
4
  "description": "Dual-provider orchestration for Claude Code — tiered routing, budget balancing, and GPT dual-brain review across Claude + OpenAI subscriptions",
5
5
  "type": "module",
6
6
  "bin": {