dual-brain 3.4.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.
@@ -1,9 +1,10 @@
1
1
  #!/usr/bin/env node
2
2
  /**
3
- * control-panel.mjs — Session manager + control panel for Dual-Brain.
3
+ * control-panel.mjs — Session launcher for Dual-Brain.
4
4
  *
5
- * Data-tools-style interactive menu: recent sessions, continue/resume/new,
6
- * profile switching, budget editing. Loops until user exits to shell.
5
+ * Progressive disclosure: first-run shows minimal menu (new/shell + auth).
6
+ * Returning users see recent sessions, profile mode, cost alert settings.
7
+ * Loops until user exits to shell.
7
8
  */
8
9
 
9
10
  import readline from 'readline';
@@ -14,6 +15,7 @@ import { spawnSync } from 'child_process';
14
15
 
15
16
  const __dirname = dirname(fileURLToPath(import.meta.url));
16
17
  const PROFILE_FILE = join(__dirname, '..', 'dual-brain.profile.json');
18
+ const LAUNCHED_MARKER = join(__dirname, '..', '.launched');
17
19
  const VERSION = (() => {
18
20
  try { return JSON.parse(readFileSync(join(__dirname, '..', 'package.json'), 'utf8')).version; } catch {}
19
21
  return '?';
@@ -32,16 +34,15 @@ const dim = s => e('2', s);
32
34
  const cyan = s => e('36', s);
33
35
  const green = s => e('32', s);
34
36
  const yellow = s => e('33', s);
35
- const magenta = s => e('95', s);
36
37
  const orange = s => e('1;38;5;208', s);
37
38
  const blue = s => e('1;38;5;33', s);
38
39
 
39
40
  // ─── Profiles ──────────────────────────────────────────────────────────────
40
41
 
41
42
  const PROFILES = {
42
- balanced: { emoji: '⚖️', label: 'Balanced', desc: 'Best model per tier, normal budgets' },
43
- 'cost-saver': { emoji: '💸', label: 'Cost-saver', desc: 'Prefer cheaper models, lower budgets' },
44
- 'quality-first': { emoji: '💎', label: 'Quality-first', desc: 'Dual-brain for medium+, strict reviews' },
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' },
45
46
  };
46
47
 
47
48
  const PROFILE_BUDGETS = {
@@ -55,9 +56,9 @@ function loadProfile() {
55
56
  const data = JSON.parse(readFileSync(PROFILE_FILE, 'utf8'));
56
57
  const name = data.active && PROFILES[data.active] ? data.active : 'balanced';
57
58
  const custom = data.custom_overrides || {};
58
- return { name, budgets: { ...PROFILE_BUDGETS[name], ...custom.budgets } };
59
+ return { name, budgets: { ...PROFILE_BUDGETS[name], ...custom.budgets }, hasCustomBudget: !!custom.budgets };
59
60
  } catch {
60
- return { name: 'balanced', budgets: PROFILE_BUDGETS.balanced };
61
+ return { name: 'balanced', budgets: PROFILE_BUDGETS.balanced, hasCustomBudget: false };
61
62
  }
62
63
  }
63
64
 
@@ -69,6 +70,25 @@ function saveProfile(name, customOverrides) {
69
70
  renameSync(tmp, PROFILE_FILE);
70
71
  }
71
72
 
73
+ // ─── First-Run Detection ──────────────────────────────────────────────────
74
+
75
+ function isFirstRun() {
76
+ if (existsSync(LAUNCHED_MARKER)) return false;
77
+ // Also check Claude history for any session in this workspace
78
+ const historyFile = join(HOME, '.claude', 'history.jsonl');
79
+ if (existsSync(historyFile)) {
80
+ try {
81
+ const content = readFileSync(historyFile, 'utf8');
82
+ if (content.includes('"sessionId"')) return false;
83
+ } catch {}
84
+ }
85
+ return true;
86
+ }
87
+
88
+ function markLaunched() {
89
+ try { writeFileSync(LAUNCHED_MARKER, new Date().toISOString() + '\n'); } catch {}
90
+ }
91
+
72
92
  // ─── Provider Detection ───────────────────────────────────────────────────
73
93
 
74
94
  function detectProviders() {
@@ -122,17 +142,13 @@ function getRecentSessions() {
122
142
  return true;
123
143
  };
124
144
 
125
- // Claude sessions
126
145
  const historyFile = join(HOME, '.claude', 'history.jsonl');
127
146
  if (existsSync(historyFile)) {
128
147
  try {
129
148
  const lines = readFileSync(historyFile, 'utf8').trim().split('\n');
130
149
  const entries = [];
131
150
  for (const line of lines) {
132
- try {
133
- const j = JSON.parse(line);
134
- if (j.sessionId && j.timestamp) entries.push(j);
135
- } catch {}
151
+ try { const j = JSON.parse(line); if (j.sessionId && j.timestamp) entries.push(j); } catch {}
136
152
  }
137
153
  entries.sort((a, b) => a.timestamp - b.timestamp);
138
154
  for (const j of entries) {
@@ -151,7 +167,6 @@ function getRecentSessions() {
151
167
  } catch {}
152
168
  }
153
169
 
154
- // Codex sessions
155
170
  const codexDir = join(HOME, '.codex', 'sessions');
156
171
  if (existsSync(codexDir)) {
157
172
  const walk = (dir) => {
@@ -230,68 +245,125 @@ function countRunning() {
230
245
  return { claude, codex };
231
246
  }
232
247
 
233
- // ─── Replit-Tools Check ───────────────────────────────────────────────────
248
+ // ─── Provider Balance ─────────────────────────────────────────────────────
249
+
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' };
234
269
 
235
- function checkReplitTools() {
236
- if (!IS_REPLIT) return true;
237
- return existsSync(join(CWD, '.replit-tools'));
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 };
238
281
  }
239
282
 
240
- // ─── Menu Renderer ────────────────────────────────────────────────────────
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`;
290
+ }
241
291
 
242
- function renderMenu() {
243
- const providers = detectProviders();
244
- const profile = loadProfile();
245
- const sessions = getRecentSessions();
246
- const running = countRunning();
247
- const pf = PROFILES[profile.name];
248
- const hasReplitTools = checkReplitTools();
292
+ // ─── Menu Renderers ───────────────────────────────────────────────────────
249
293
 
294
+ function renderFirstRunMenu(providers) {
250
295
  const lines = [];
251
296
 
252
297
  lines.push('');
253
298
  lines.push(` 🧠 ${bold(`Dual-Brain v${VERSION}`)}`);
254
299
  lines.push('');
255
300
 
256
- // Quick reference box
257
- lines.push(' ┌─────────────────────────────┐');
258
- if (IS_REPLIT) {
259
- lines.push(` ${magenta('At')} ${blue('~/workspace')}${magenta('$ prompt:')} │`);
260
- lines.push(` │ ${cyan('! npx dual-brain')} = this menu│`);
301
+ // Provider status
302
+ const cStat = providers.claude.authed ? '' : providers.claude.installed ? '⚠️' : '❌';
303
+ const xStat = providers.codex.authed ? '✅' : providers.codex.installed ? '⚠️' : '❌';
304
+ lines.push(` 🟠 Claude ${cStat} 🟢 Codex ${xStat}`);
305
+
306
+ if (providers.claude.authed && providers.codex.authed) {
307
+ lines.push(` ${green('Both providers ready — full dual-brain mode')}`);
308
+ } else if (providers.claude.authed) {
309
+ lines.push(` ${dim('Claude ready. Add Codex for dual-brain features.')}`);
310
+ } else if (!providers.claude.installed) {
311
+ lines.push(` ${yellow('Claude not found — needed to start.')}`);
261
312
  } else {
262
- lines.push(` ${magenta('At shell prompt:')} │`);
263
- lines.push(` │ ${cyan('npx dual-brain')} = this menu │`);
313
+ lines.push(` ${yellow('Claude needs login to start.')}`);
264
314
  }
265
- lines.push(` │ ${cyan('j')} = login to Claude │`);
266
- lines.push(` │ ${cyan('k')} = login to Codex │`);
267
- lines.push(' ├─────────────────────────────┤');
268
- lines.push(` │ ${orange('In Claude session:')} │`);
269
- lines.push(` │ ${green('Ctrl+C x2')} = back to menu │`);
270
- lines.push(` │ ${green('Ctrl+C x3')} = exit to shell │`);
271
- lines.push(' └─────────────────────────────┘');
272
- lines.push('');
273
315
 
274
- // Provider status line
275
- const cStat = providers.claude.authed ? '✅' : providers.claude.installed ? '⚠️' : '❌';
276
- const xStat = providers.codex.authed ? '✅' : providers.codex.installed ? '⚠️' : '❌';
277
- lines.push(` 🟠 Claude ${cStat} 🟢 Codex ${xStat} ${pf.emoji} ${bold(pf.label)} ${dim('$' + profile.budgets.session_limit_usd + '/session')}`);
316
+ lines.push('');
278
317
 
279
- // Missing provider nudge
318
+ // Auth actions if needed
280
319
  if (!providers.claude.authed || !providers.codex.authed) {
320
+ if (!providers.claude.installed) {
321
+ lines.push(` ${dim('Install Claude:')} ${cyan('curl -fsSL https://claude.ai/install.sh | sh')}`);
322
+ }
323
+ if (!providers.claude.authed && providers.claude.installed) {
324
+ lines.push(` ${bold('[j]')} Sign in to Claude`);
325
+ }
326
+ if (!providers.codex.installed) {
327
+ lines.push(` ${dim('Install Codex:')} ${cyan('npm i -g @openai/codex')}`);
328
+ } else if (!providers.codex.authed) {
329
+ lines.push(` ${bold('[k]')} Sign in to Codex ${dim('(optional — enables GPT collaboration)')}`);
330
+ }
281
331
  lines.push('');
282
- if (!providers.claude.installed) lines.push(` ${dim('└')} Install Claude: ${cyan('curl -fsSL https://claude.ai/install.sh | sh')}`);
283
- else if (!providers.claude.authed) lines.push(` ${dim('└')} Auth Claude: press ${bold('j')} below`);
284
- if (!providers.codex.installed) lines.push(` ${dim('└')} Install Codex: ${cyan('npm i -g @openai/codex')}`);
285
- else if (!providers.codex.authed) lines.push(` ${dim('└')} Auth Codex: press ${bold('k')} below`);
286
332
  }
287
333
 
288
334
  // Replit-tools check
289
- if (IS_REPLIT && !hasReplitTools) {
290
- lines.push('');
291
- lines.push(` ⚠️ ${yellow('replit-tools not found')} — recommended for Replit environments`);
292
- lines.push(` ${dim('└')} Press ${bold('t')} to install replit-tools`);
335
+ if (IS_REPLIT && !existsSync(join(CWD, '.replit-tools'))) {
336
+ lines.push(` ${bold('[t]')} Install replit-tools ${dim('(recommended for Replit)')}`);
293
337
  }
294
338
 
339
+ // Primary actions
340
+ lines.push(` ${bold('[n]')} Start new session`);
341
+ lines.push(` ${bold('[s]')} Skip — just shell`);
342
+ lines.push('');
343
+
344
+ return lines;
345
+ }
346
+
347
+ function renderReturningMenu(providers, sessions) {
348
+ const profile = loadProfile();
349
+ const pf = PROFILES[profile.name];
350
+ const running = countRunning();
351
+ const balance = loadProviderBalance();
352
+ const lines = [];
353
+
354
+ lines.push('');
355
+ lines.push(` 🧠 ${bold(`Dual-Brain v${VERSION}`)}`);
356
+ lines.push('');
357
+
358
+ // Provider status
359
+ const cStat = providers.claude.authed ? '✅' : '⚠️';
360
+ const xStat = providers.codex.authed ? '✅' : providers.codex.installed ? '⚠️' : '❌';
361
+ lines.push(` 🟠 Claude ${cStat} 🟢 Codex ${xStat} ${pf.emoji} ${bold(pf.uiLabel)}`);
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
+
295
367
  // Recent sessions
296
368
  if (sessions.length > 0) {
297
369
  lines.push('');
@@ -305,31 +377,28 @@ function renderMenu() {
305
377
  }
306
378
  }
307
379
 
308
- // Session manager box
309
380
  lines.push('');
310
- lines.push(' ┌─────────────────────────────┐');
311
- lines.push(' │ 🧠 Dual-Brain Session Mgr │');
312
- lines.push(' └─────────────────────────────┘');
313
381
 
314
382
  const runParts = [];
315
383
  if (running.claude > 0) runParts.push(`${running.claude} claude`);
316
384
  if (running.codex > 0) runParts.push(`${running.codex} codex`);
317
385
  if (runParts.length > 0) lines.push(` ${dim('(' + runParts.join(', ') + ' running)')}`);
318
- lines.push('');
319
386
 
320
387
  // Menu options
321
388
  lines.push(` ${bold('[c]')} Continue last session`);
322
389
  if (sessions.length > 0) lines.push(` ${bold('[1-9]')} Resume numbered above`);
323
390
  lines.push(` ${bold('[n]')} New session`);
324
- lines.push(` ${bold('[p]')} Profile ${dim('(' + pf.emoji + ' ' + profile.name + ')')}`);
325
- lines.push(` ${bold('[b]')} Budget ${dim('($' + profile.budgets.session_limit_usd + ' session / $' + profile.budgets.daily_limit_usd + ' daily)')}`);
326
- lines.push(` ${bold('[j]')} Login to Claude`);
327
- lines.push(` ${bold('[k]')} Login to Codex`);
328
- if (IS_REPLIT && !hasReplitTools) lines.push(` ${bold('[t]')} Install replit-tools`);
329
- lines.push(` ${bold('[s]')} Skip — just shell`);
391
+ lines.push(` ${bold('[p]')} Mode: ${dim(pf.uiLabel)}`);
392
+
393
+ // Auth if needed
394
+ if (!providers.claude.authed) lines.push(` ${bold('[j]')} Sign in to Claude`);
395
+ if (providers.codex.installed && !providers.codex.authed) lines.push(` ${bold('[k]')} Sign in to Codex`);
396
+ if (IS_REPLIT && !existsSync(join(CWD, '.replit-tools'))) lines.push(` ${bold('[t]')} Install replit-tools`);
397
+
398
+ lines.push(` ${bold('[s]')} Shell`);
330
399
  lines.push('');
331
400
 
332
- return { lines, sessions, providers };
401
+ return lines;
333
402
  }
334
403
 
335
404
  // ─── Profile Picker ───────────────────────────────────────────────────────
@@ -337,12 +406,16 @@ function renderMenu() {
337
406
  function showProfilePicker(rl) {
338
407
  return new Promise((resolve) => {
339
408
  const current = loadProfile();
409
+ const balance = loadProviderBalance();
340
410
  console.log('');
341
- console.log(` ${bold('🎛️ Switch Profile:')}`);
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
+ }
342
415
  console.log('');
343
416
  for (const [i, [name, pf]] of Object.entries(PROFILES).entries()) {
344
417
  const active = name === current.name ? ' ✅' : '';
345
- console.log(` ${bold('[' + (i + 1) + ']')} ${pf.emoji} ${name.padEnd(15)} ${dim(pf.desc)}${active}`);
418
+ console.log(` ${bold('[' + (i + 1) + ']')} ${pf.emoji} ${pf.uiLabel.padEnd(15)} ${dim(pf.desc)}${active}`);
346
419
  }
347
420
  console.log(` ${bold('[q]')} Cancel`);
348
421
  console.log('');
@@ -358,63 +431,26 @@ function showProfilePicker(rl) {
358
431
  } catch {}
359
432
  saveProfile(names[idx], customOverrides);
360
433
  const pf = PROFILES[names[idx]];
361
- console.log(` ✅ Switched to ${pf.emoji} ${pf.label}`);
434
+ console.log(` ✅ Switched to ${pf.emoji} ${pf.uiLabel}`);
362
435
  }
363
436
  resolve();
364
437
  });
365
438
  });
366
439
  }
367
440
 
368
- // ─── Budget Editor ────────────────────────────────────────────────────────
369
-
370
- function showBudgetEditor(rl) {
371
- return new Promise((resolve) => {
372
- const profile = loadProfile();
373
- console.log('');
374
- console.log(` ${bold('💵 Edit Budget')}`);
375
- console.log(` ${dim('Current: $' + profile.budgets.session_limit_usd + ' session / $' + profile.budgets.daily_limit_usd + ' daily')}`);
376
- console.log('');
377
-
378
- rl.question(' Session limit ($): ', (sessionStr) => {
379
- const session = parseFloat(sessionStr);
380
- if (isNaN(session) || session <= 0) {
381
- console.log(' Cancelled.');
382
- return resolve();
383
- }
384
- rl.question(' Daily limit ($, Enter = auto): ', (dailyStr) => {
385
- const daily = parseFloat(dailyStr);
386
- const finalDaily = (isNaN(daily) || daily <= 0) ? session * 3 : daily;
387
-
388
- let existing = {};
389
- try { existing = JSON.parse(readFileSync(PROFILE_FILE, 'utf8')); } catch {}
390
- const custom = existing.custom_overrides || {};
391
- custom.budgets = {
392
- session_warn_usd: +(session * 0.6).toFixed(2),
393
- session_limit_usd: session,
394
- daily_warn_usd: +(finalDaily * 0.6).toFixed(2),
395
- daily_limit_usd: finalDaily,
396
- };
397
- const data = { active: existing.active || 'balanced', switched_at: existing.switched_at || new Date().toISOString(), custom_overrides: custom };
398
- const tmp = PROFILE_FILE + '.tmp.' + process.pid;
399
- writeFileSync(tmp, JSON.stringify(data, null, 2) + '\n');
400
- renameSync(tmp, PROFILE_FILE);
401
-
402
- console.log(` ✅ Budget: $${session}/session · $${finalDaily}/daily`);
403
- resolve();
404
- });
405
- });
406
- });
407
- }
441
+ // (Cost alert editor removed — replaced by provider balance + mode switching)
408
442
 
409
443
  // ─── Session Runner ───────────────────────────────────────────────────────
410
444
 
411
445
  function runSession(cmd, args, label) {
412
446
  console.log('');
413
- console.log(` ${label}...`);
447
+ console.log(` ${label}`);
448
+ console.log(` ${dim('Inside Claude: press Ctrl+C twice to return here.')}`);
414
449
  console.log('');
450
+ markLaunched();
415
451
  const result = spawnSync(cmd, args, { stdio: 'inherit' });
416
452
  console.log('');
417
- console.log(' Exited. Returning to menu...');
453
+ console.log(' Returned to Dual-Brain.');
418
454
  return result.status || 0;
419
455
  }
420
456
 
@@ -422,11 +458,17 @@ function runSession(cmd, args, label) {
422
458
 
423
459
  async function mainLoop() {
424
460
  const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
425
-
426
461
  const ask = () => new Promise(resolve => rl.question(' Choice: ', resolve));
427
462
 
428
463
  while (true) {
429
- const { lines, sessions } = renderMenu();
464
+ const firstRun = isFirstRun();
465
+ const providers = detectProviders();
466
+ const sessions = firstRun ? [] : getRecentSessions();
467
+
468
+ const lines = firstRun
469
+ ? renderFirstRunMenu(providers)
470
+ : renderReturningMenu(providers, sessions);
471
+
430
472
  for (const l of lines) console.log(l);
431
473
 
432
474
  const choice = (await ask()).trim().toLowerCase();
@@ -438,16 +480,15 @@ async function mainLoop() {
438
480
  }
439
481
 
440
482
  if (choice === 'c' || choice === '') {
441
- // Continue most recent session
442
483
  if (sessions.length > 0) {
443
484
  const s = sessions[0];
444
485
  if (s.tool === 'codex') {
445
- runSession('codex', ['--dangerously-bypass-approvals-and-sandbox', 'resume', s.id], `Resuming codex session ${s.id.slice(0, 8)}`);
486
+ runSession('codex', ['--dangerously-bypass-approvals-and-sandbox', 'resume', s.id], `Resuming codex ${s.id.slice(0, 8)}...`);
446
487
  } else {
447
- runSession('claude', ['-r', s.id, '--dangerously-skip-permissions'], `Resuming session ${s.id.slice(0, 8)}`);
488
+ runSession('claude', ['-r', s.id, '--dangerously-skip-permissions'], `Resuming session ${s.id.slice(0, 8)}...`);
448
489
  }
449
490
  } else {
450
- runSession('claude', ['--dangerously-skip-permissions'], 'Starting new session');
491
+ runSession('claude', ['--dangerously-skip-permissions'], 'Starting new session...');
451
492
  }
452
493
  continue;
453
494
  }
@@ -456,15 +497,15 @@ async function mainLoop() {
456
497
  if (num >= 1 && num <= 9 && sessions[num - 1]) {
457
498
  const s = sessions[num - 1];
458
499
  if (s.tool === 'codex') {
459
- runSession('codex', ['--dangerously-bypass-approvals-and-sandbox', 'resume', s.id], `Resuming codex session ${s.id.slice(0, 8)}`);
500
+ runSession('codex', ['--dangerously-bypass-approvals-and-sandbox', 'resume', s.id], `Resuming codex ${s.id.slice(0, 8)}...`);
460
501
  } else {
461
- runSession('claude', ['-r', s.id, '--dangerously-skip-permissions'], `Resuming session ${s.id.slice(0, 8)}`);
502
+ runSession('claude', ['-r', s.id, '--dangerously-skip-permissions'], `Resuming session ${s.id.slice(0, 8)}...`);
462
503
  }
463
504
  continue;
464
505
  }
465
506
 
466
507
  if (choice === 'n') {
467
- runSession('claude', ['--dangerously-skip-permissions'], 'Starting new session');
508
+ runSession('claude', ['--dangerously-skip-permissions'], 'Starting new session...');
468
509
  continue;
469
510
  }
470
511
 
@@ -473,11 +514,6 @@ async function mainLoop() {
473
514
  continue;
474
515
  }
475
516
 
476
- if (choice === 'b') {
477
- await showBudgetEditor(rl);
478
- continue;
479
- }
480
-
481
517
  if (choice === 'j') {
482
518
  console.log('');
483
519
  console.log(' Starting Claude login...');
@@ -508,7 +544,7 @@ async function mainLoop() {
508
544
  console.log('');
509
545
  spawnSync('npx', ['-y', 'data-tools'], { stdio: 'inherit', cwd: CWD });
510
546
  console.log('');
511
- console.log(' ✅ replit-tools installed. You may need to restart your shell.');
547
+ console.log(' ✅ replit-tools installed.');
512
548
  console.log('');
513
549
  await ask();
514
550
  continue;
@@ -521,7 +557,11 @@ async function mainLoop() {
521
557
  // ─── Non-Interactive Fallback ─────────────────────────────────────────────
522
558
 
523
559
  function renderStatic() {
524
- const { lines } = renderMenu();
560
+ const providers = detectProviders();
561
+ const sessions = getRecentSessions();
562
+ const lines = sessions.length > 0
563
+ ? renderReturningMenu(providers, sessions)
564
+ : renderFirstRunMenu(providers);
525
565
  for (const l of lines) console.log(l);
526
566
  }
527
567
 
@@ -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
@@ -313,6 +313,7 @@ function generateGitignoreEntries(workspace) {
313
313
  '.claude/dual-brain.profile.json',
314
314
  '.claude/hooks/usage-summary-*.json',
315
315
  '.claude/hooks/decision-ledger.jsonl',
316
+ '.claude/.launched',
316
317
  ];
317
318
  let existing = '';
318
319
  try { existing = readFileSync(join(workspace, '.gitignore'), 'utf8'); } catch {}
@@ -423,19 +424,19 @@ function profilePath(workspace) {
423
424
 
424
425
  const PROFILES = {
425
426
  balanced: {
426
- description: 'Standard routing best model for each tier, normal budgets',
427
+ description: 'Auto-routes by complexity, uses both providers evenly',
427
428
  routing: { prefer_provider: 'auto', think_threshold: 'normal', gpt_dispatch_bias: 0 },
428
429
  budgets: { session_warn_usd: 5, session_limit_usd: 10, daily_warn_usd: 20, daily_limit_usd: 50 },
429
430
  quality_gate: { sensitivity_floor: 'medium', dual_brain_minimum: 'high' },
430
431
  },
431
432
  'cost-saver': {
432
- description: 'Minimize spend prefer cheaper models, skip GPT for low risk',
433
+ description: 'Conservativefewer GPT dispatches, sticks to Claude',
433
434
  routing: { prefer_provider: 'cheapest', think_threshold: 'strict', gpt_dispatch_bias: -20 },
434
435
  budgets: { session_warn_usd: 2, session_limit_usd: 5, daily_warn_usd: 8, daily_limit_usd: 20 },
435
436
  quality_gate: { sensitivity_floor: 'high', dual_brain_minimum: 'critical' },
436
437
  },
437
438
  'quality-first': {
438
- description: 'Maximum quality — dual-brain for medium+, stricter reviews',
439
+ description: 'Aggressivemaximizes both subscriptions, dual-brain for medium+',
439
440
  routing: { prefer_provider: 'most-capable', think_threshold: 'relaxed', gpt_dispatch_bias: 10 },
440
441
  budgets: { session_warn_usd: 15, session_limit_usd: 30, daily_warn_usd: 50, daily_limit_usd: 100 },
441
442
  quality_gate: { sensitivity_floor: 'low', dual_brain_minimum: 'medium' },
@@ -489,16 +490,18 @@ function cmdMode() {
489
490
 
490
491
  if (!modeArg || modeArg === 'list') {
491
492
  const current = loadProfile(workspace);
492
- 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' };
493
495
  console.log('');
494
- console.log(' 🎛️ Profiles:');
496
+ console.log(' 🎛️ Routing modes:');
495
497
  console.log('');
496
498
  for (const [name, p] of Object.entries(PROFILES)) {
497
499
  const active = name === current.name ? ' ✅ active' : '';
498
- 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}`);
499
502
  }
500
503
  console.log('');
501
- console.log(` Switch: ${cmd('npx dual-brain mode <profile>')}`);
504
+ console.log(` Switch: ${cmd('npx dual-brain mode <name>')}`);
502
505
  console.log('');
503
506
  return;
504
507
  }
@@ -521,9 +524,10 @@ function cmdMode() {
521
524
 
522
525
  saveProfile(workspace, modeArg, customOverrides);
523
526
 
524
- 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' };
525
529
  console.log('');
526
- console.log(` ✅ Profile switched: ${PEMOJIS[modeArg] || ''} ${modeArg}`);
530
+ console.log(` ✅ Mode switched: ${PEMOJIS[modeArg] || ''} ${UI_NAMES[modeArg] || modeArg}`);
527
531
  console.log(` ${profile.description}`);
528
532
  console.log('');
529
533
  console.log(' 🧭 Routing changes:');
@@ -546,12 +550,12 @@ function cmdBudget() {
546
550
  if (sessionArg == null) {
547
551
  const profile = loadProfile(workspace);
548
552
  console.log('');
549
- console.log(' 💵 Current budget:');
550
- console.log(` Session: ⚠️ $${profile.budgets.session_warn_usd} warn · 🛑 $${profile.budgets.session_limit_usd} limit`);
551
- 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`);
552
556
  console.log('');
553
- console.log(` Set limits: ${cmd('npx dual-brain budget <session$> [daily$]')}`);
554
- 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')}`);
555
559
  console.log('');
556
560
  return;
557
561
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dual-brain",
3
- "version": "3.4.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": {