dual-brain 3.4.0 → 3.5.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, 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' },
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,86 @@ function countRunning() {
230
245
  return { claude, codex };
231
246
  }
232
247
 
233
- // ─── Replit-Tools Check ───────────────────────────────────────────────────
248
+ // ─── Cost Alert Label ─────────────────────────────────────────────────────
234
249
 
235
- function checkReplitTools() {
236
- if (!IS_REPLIT) return true;
237
- return existsSync(join(CWD, '.replit-tools'));
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';
238
256
  }
239
257
 
240
- // ─── Menu Renderer ────────────────────────────────────────────────────────
241
-
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();
258
+ // ─── Menu Renderers ───────────────────────────────────────────────────────
249
259
 
260
+ function renderFirstRunMenu(providers) {
250
261
  const lines = [];
251
262
 
252
263
  lines.push('');
253
264
  lines.push(` 🧠 ${bold(`Dual-Brain v${VERSION}`)}`);
254
265
  lines.push('');
255
266
 
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│`);
267
+ // Provider status
268
+ const cStat = providers.claude.authed ? '' : providers.claude.installed ? '⚠️' : '❌';
269
+ const xStat = providers.codex.authed ? '✅' : providers.codex.installed ? '⚠️' : '❌';
270
+ lines.push(` 🟠 Claude ${cStat} 🟢 Codex ${xStat}`);
271
+
272
+ if (providers.claude.authed && providers.codex.authed) {
273
+ lines.push(` ${green('Both providers ready — full dual-brain mode')}`);
274
+ } else if (providers.claude.authed) {
275
+ lines.push(` ${dim('Claude ready. Add Codex for dual-brain features.')}`);
276
+ } else if (!providers.claude.installed) {
277
+ lines.push(` ${yellow('Claude not found — needed to start.')}`);
261
278
  } else {
262
- lines.push(` ${magenta('At shell prompt:')} │`);
263
- lines.push(` │ ${cyan('npx dual-brain')} = this menu │`);
279
+ lines.push(` ${yellow('Claude needs login to start.')}`);
264
280
  }
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
281
 
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')}`);
282
+ lines.push('');
278
283
 
279
- // Missing provider nudge
284
+ // Auth actions if needed
280
285
  if (!providers.claude.authed || !providers.codex.authed) {
286
+ if (!providers.claude.installed) {
287
+ lines.push(` ${dim('Install Claude:')} ${cyan('curl -fsSL https://claude.ai/install.sh | sh')}`);
288
+ }
289
+ if (!providers.claude.authed && providers.claude.installed) {
290
+ lines.push(` ${bold('[j]')} Sign in to Claude`);
291
+ }
292
+ if (!providers.codex.installed) {
293
+ lines.push(` ${dim('Install Codex:')} ${cyan('npm i -g @openai/codex')}`);
294
+ } else if (!providers.codex.authed) {
295
+ lines.push(` ${bold('[k]')} Sign in to Codex ${dim('(optional — enables GPT collaboration)')}`);
296
+ }
281
297
  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
298
  }
287
299
 
288
300
  // 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`);
301
+ if (IS_REPLIT && !existsSync(join(CWD, '.replit-tools'))) {
302
+ lines.push(` ${bold('[t]')} Install replit-tools ${dim('(recommended for Replit)')}`);
293
303
  }
294
304
 
305
+ // Primary actions
306
+ lines.push(` ${bold('[n]')} Start new session`);
307
+ lines.push(` ${bold('[s]')} Skip — just shell`);
308
+ lines.push('');
309
+
310
+ return lines;
311
+ }
312
+
313
+ function renderReturningMenu(providers, sessions) {
314
+ const profile = loadProfile();
315
+ const pf = PROFILES[profile.name];
316
+ const running = countRunning();
317
+ const lines = [];
318
+
319
+ lines.push('');
320
+ lines.push(` 🧠 ${bold(`Dual-Brain v${VERSION}`)}`);
321
+ lines.push('');
322
+
323
+ // Compact provider + mode line
324
+ const cStat = providers.claude.authed ? '✅' : '⚠️';
325
+ const xStat = providers.codex.authed ? '✅' : providers.codex.installed ? '⚠️' : '❌';
326
+ lines.push(` 🟠 Claude ${cStat} 🟢 Codex ${xStat} ${pf.emoji} ${bold(pf.uiLabel)}`);
327
+
295
328
  // Recent sessions
296
329
  if (sessions.length > 0) {
297
330
  lines.push('');
@@ -305,31 +338,29 @@ function renderMenu() {
305
338
  }
306
339
  }
307
340
 
308
- // Session manager box
309
341
  lines.push('');
310
- lines.push(' ┌─────────────────────────────┐');
311
- lines.push(' │ 🧠 Dual-Brain Session Mgr │');
312
- lines.push(' └─────────────────────────────┘');
313
342
 
314
343
  const runParts = [];
315
344
  if (running.claude > 0) runParts.push(`${running.claude} claude`);
316
345
  if (running.codex > 0) runParts.push(`${running.codex} codex`);
317
346
  if (runParts.length > 0) lines.push(` ${dim('(' + runParts.join(', ') + ' running)')}`);
318
- lines.push('');
319
347
 
320
348
  // Menu options
321
349
  lines.push(` ${bold('[c]')} Continue last session`);
322
350
  if (sessions.length > 0) lines.push(` ${bold('[1-9]')} Resume numbered above`);
323
351
  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`);
352
+ lines.push(` ${bold('[p]')} Mode: ${dim(pf.uiLabel)}`);
353
+ lines.push(` ${bold('[b]')} Cost alerts: ${dim(costAlertLabel(profile))}`);
354
+
355
+ // Auth if needed
356
+ if (!providers.claude.authed) lines.push(` ${bold('[j]')} Sign in to Claude`);
357
+ if (providers.codex.installed && !providers.codex.authed) lines.push(` ${bold('[k]')} Sign in to Codex`);
358
+ if (IS_REPLIT && !existsSync(join(CWD, '.replit-tools'))) lines.push(` ${bold('[t]')} Install replit-tools`);
359
+
360
+ lines.push(` ${bold('[s]')} Shell`);
330
361
  lines.push('');
331
362
 
332
- return { lines, sessions, providers };
363
+ return lines;
333
364
  }
334
365
 
335
366
  // ─── Profile Picker ───────────────────────────────────────────────────────
@@ -338,11 +369,11 @@ function showProfilePicker(rl) {
338
369
  return new Promise((resolve) => {
339
370
  const current = loadProfile();
340
371
  console.log('');
341
- console.log(` ${bold('🎛️ Switch Profile:')}`);
372
+ console.log(` ${bold('Switch mode:')}`);
342
373
  console.log('');
343
374
  for (const [i, [name, pf]] of Object.entries(PROFILES).entries()) {
344
375
  const active = name === current.name ? ' ✅' : '';
345
- console.log(` ${bold('[' + (i + 1) + ']')} ${pf.emoji} ${name.padEnd(15)} ${dim(pf.desc)}${active}`);
376
+ console.log(` ${bold('[' + (i + 1) + ']')} ${pf.emoji} ${pf.uiLabel.padEnd(15)} ${dim(pf.desc)}${active}`);
346
377
  }
347
378
  console.log(` ${bold('[q]')} Cancel`);
348
379
  console.log('');
@@ -358,30 +389,33 @@ function showProfilePicker(rl) {
358
389
  } catch {}
359
390
  saveProfile(names[idx], customOverrides);
360
391
  const pf = PROFILES[names[idx]];
361
- console.log(` ✅ Switched to ${pf.emoji} ${pf.label}`);
392
+ console.log(` ✅ Switched to ${pf.emoji} ${pf.uiLabel}`);
362
393
  }
363
394
  resolve();
364
395
  });
365
396
  });
366
397
  }
367
398
 
368
- // ─── Budget Editor ────────────────────────────────────────────────────────
399
+ // ─── Cost Alert Editor ────────────────────────────────────────────────────
369
400
 
370
- function showBudgetEditor(rl) {
401
+ function showCostAlertEditor(rl) {
371
402
  return new Promise((resolve) => {
372
403
  const profile = loadProfile();
373
404
  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')}`);
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`);
376
411
  console.log('');
377
412
 
378
- rl.question(' Session limit ($): ', (sessionStr) => {
413
+ rl.question(' Session alert limit ($, Enter = keep): ', (sessionStr) => {
414
+ if (!sessionStr.trim()) return resolve();
379
415
  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) => {
416
+ if (isNaN(session) || session <= 0) { console.log(' Cancelled.'); return resolve(); }
417
+
418
+ rl.question(' Daily alert limit ($, Enter = auto): ', (dailyStr) => {
385
419
  const daily = parseFloat(dailyStr);
386
420
  const finalDaily = (isNaN(daily) || daily <= 0) ? session * 3 : daily;
387
421
 
@@ -399,7 +433,7 @@ function showBudgetEditor(rl) {
399
433
  writeFileSync(tmp, JSON.stringify(data, null, 2) + '\n');
400
434
  renameSync(tmp, PROFILE_FILE);
401
435
 
402
- console.log(` ✅ Budget: $${session}/session · $${finalDaily}/daily`);
436
+ console.log(` ✅ Cost alerts: $${session}/session · $${finalDaily}/day`);
403
437
  resolve();
404
438
  });
405
439
  });
@@ -410,11 +444,13 @@ function showBudgetEditor(rl) {
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
 
@@ -474,7 +515,7 @@ async function mainLoop() {
474
515
  }
475
516
 
476
517
  if (choice === 'b') {
477
- await showBudgetEditor(rl);
518
+ await showCostAlertEditor(rl);
478
519
  continue;
479
520
  }
480
521
 
@@ -508,7 +549,7 @@ async function mainLoop() {
508
549
  console.log('');
509
550
  spawnSync('npx', ['-y', 'data-tools'], { stdio: 'inherit', cwd: CWD });
510
551
  console.log('');
511
- console.log(' ✅ replit-tools installed. You may need to restart your shell.');
552
+ console.log(' ✅ replit-tools installed.');
512
553
  console.log('');
513
554
  await ask();
514
555
  continue;
@@ -521,7 +562,11 @@ async function mainLoop() {
521
562
  // ─── Non-Interactive Fallback ─────────────────────────────────────────────
522
563
 
523
564
  function renderStatic() {
524
- const { lines } = renderMenu();
565
+ const providers = detectProviders();
566
+ const sessions = getRecentSessions();
567
+ const lines = sessions.length > 0
568
+ ? renderReturningMenu(providers, sessions)
569
+ : renderFirstRunMenu(providers);
525
570
  for (const l of lines) console.log(l);
526
571
  }
527
572
 
package/install.mjs CHANGED
@@ -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 {}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dual-brain",
3
- "version": "3.4.0",
3
+ "version": "3.5.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": {