dual-brain 0.3.28 → 0.3.30

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.
Files changed (2) hide show
  1. package/bin/dual-brain.mjs +122 -9
  2. package/package.json +1 -1
@@ -331,6 +331,38 @@ async function checkForUpdates(currentVersion) {
331
331
  } catch {}
332
332
  return null;
333
333
  }
334
+
335
+ async function maybeAutoUpdateAndRelaunch(args, { cmd, isInteractive } = {}) {
336
+ if (!isInteractive) return false;
337
+ if (process.env.DUAL_BRAIN_NO_AUTO_UPDATE === '1') return false;
338
+ if (process.env.DUAL_BRAIN_AUTO_UPDATED === '1') return false;
339
+ const skip = new Set(['update', 'upgrade', 'install', 'uninstall', 'shell-hook', '--version', '-v', '--help', '-h']);
340
+ if (skip.has(cmd)) return false;
341
+
342
+ const current = readVersion();
343
+ const latest = await checkForUpdates(current);
344
+ if (!latest) return false;
345
+
346
+ const { spawnSync } = await import('node:child_process');
347
+ process.stdout.write(`\n Updating dual-brain ${current} -> ${latest}...\n`);
348
+ const install = spawnSync('npm', ['install', '-g', `dual-brain@${latest}`], {
349
+ stdio: 'inherit',
350
+ cwd: process.cwd(),
351
+ });
352
+ if (install.status !== 0) {
353
+ process.stdout.write(` Update failed; continuing with ${current}.\n\n`);
354
+ return false;
355
+ }
356
+
357
+ process.stdout.write(` Updated to ${latest}. Restarting dual-brain...\n\n`);
358
+ const relaunched = spawnSync('dual-brain', args, {
359
+ stdio: 'inherit',
360
+ cwd: process.cwd(),
361
+ env: { ...process.env, DUAL_BRAIN_AUTO_UPDATED: '1' },
362
+ });
363
+ process.exit(relaunched.status ?? 0);
364
+ }
365
+
334
366
  function flag(args, name) { const i = args.indexOf(name); return i !== -1 ? (args[i + 1] ?? true) : null; }
335
367
  function err(msg) { process.stderr.write(`Error: ${msg}\n`); process.exit(1); }
336
368
  function vtrace(msg) { process.stderr.write(`[verbose] ${msg}\n`); }
@@ -1532,12 +1564,38 @@ async function cmdRuntimeSwitch(args = []) {
1532
1564
  }
1533
1565
 
1534
1566
  async function cmdAuto(args = []) {
1535
- const switchArgs = ['--automode', '--confirm'];
1567
+ const switchArgs = ['--automode', '--confirm', '--apply'];
1536
1568
  const levelIdx = args.findIndex(a => a === '--level' || a === '--intelligence' || a === '--intelligence-level');
1537
1569
  if (levelIdx !== -1 && args[levelIdx + 1]) switchArgs.push('--level', args[levelIdx + 1]);
1538
1570
  await cmdRuntimeSwitch(switchArgs);
1539
1571
  }
1540
1572
 
1573
+ async function cmdSwitchover(args = []) {
1574
+ const cwd = process.cwd();
1575
+ let sessions = [];
1576
+ try { sessions = enrichSessions(importReplitSessions(cwd), cwd); } catch {}
1577
+ const active = readActiveConversation(cwd);
1578
+ const sess = active
1579
+ ? (sessions.find(s => s.id === active.sessionId) || {
1580
+ id: active.sessionId,
1581
+ tool: active.provider,
1582
+ smartName: active.sessionName || active.conversationId || active.sessionId,
1583
+ })
1584
+ : sessions[0];
1585
+ if (!sess) {
1586
+ console.log('No resumable session found.');
1587
+ return;
1588
+ }
1589
+
1590
+ let target = null;
1591
+ const joined = args.join(' ').toLowerCase();
1592
+ if (/\b(gpt|codex|openai)\b/.test(joined)) target = 'codex';
1593
+ if (/\bclaude\b/.test(joined)) target = 'claude';
1594
+ target ||= _sessionTool(sess) === 'codex' ? 'claude' : 'codex';
1595
+
1596
+ await cmdRuntimeSwitch(['--to', target, '--automode', '--confirm', '--apply', '--reason', 'head-switchover']);
1597
+ }
1598
+
1541
1599
  async function cmdUpdate() {
1542
1600
  const cwd = process.cwd();
1543
1601
  console.log(' Updating Dual Brain...');
@@ -2501,6 +2559,44 @@ function applyIntelligenceCommand(cmd, cwd) {
2501
2559
  return { scope: 'this session', level: cmd.level };
2502
2560
  }
2503
2561
 
2562
+ function parseProviderSwitchCommand(input) {
2563
+ const text = input.trim().toLowerCase().replace(/\s+/g, ' ');
2564
+ if (/\b(switch over|switchover|switch)\b.*\b(auto|automode|auto mode|smart auto)\b/.test(text)) {
2565
+ return { provider: null, automode: true };
2566
+ }
2567
+ if (!/\b(switchover|switch over|switch provider|other provider|continue in (gpt|codex|claude|other provider)|switch (to|into) (gpt|codex|claude|openai))\b/.test(text)) {
2568
+ return null;
2569
+ }
2570
+ let provider = null;
2571
+ if (/\b(gpt|codex|openai)\b/.test(text)) provider = 'codex';
2572
+ if (/\bclaude\b/.test(text)) provider = 'claude';
2573
+ return { provider };
2574
+ }
2575
+
2576
+ function getActionSession(cwd, recentSessions = []) {
2577
+ const active = readActiveConversation(cwd);
2578
+ if (active) {
2579
+ return recentSessions.find(s => s.id === active.sessionId) || {
2580
+ id: active.sessionId,
2581
+ tool: active.provider,
2582
+ smartName: active.sessionName || active.conversationId || active.sessionId,
2583
+ };
2584
+ }
2585
+ return recentSessions[0] || null;
2586
+ }
2587
+
2588
+ async function switchProviderNow(cwd, session, target = null) {
2589
+ if (!session) return false;
2590
+ const currentTool = _sessionTool(session);
2591
+ const targetTool = target || (currentTool === 'codex' ? 'claude' : 'codex');
2592
+ const brief = _sessionBrief(session, targetTool);
2593
+ setSessionResumeProvider(cwd, session, targetTool, 'head-provider-switch');
2594
+ writeHandoffConversationLease(cwd, session, currentTool, targetTool, brief);
2595
+ markSessionSuperseded(cwd, session, targetTool, 'head-provider-switch');
2596
+ await cmdSwitch([targetTool, brief]);
2597
+ return true;
2598
+ }
2599
+
2504
2600
  async function offerRuntimeReloadAfterModeChange(cwd, ask, recentSessions, label = 'runtime settings') {
2505
2601
  const cyan = '\x1b[36m';
2506
2602
  const reset = '\x1b[0m';
@@ -3330,6 +3426,11 @@ function classifyInput(input) {
3330
3426
  return { tier: 'free', command: 'intelligence', args: [], intelligenceCommand };
3331
3427
  }
3332
3428
 
3429
+ const providerSwitchCommand = parseProviderSwitchCommand(trimmed);
3430
+ if (providerSwitchCommand) {
3431
+ return { tier: 'free', command: 'provider-switch', args: [], providerSwitchCommand };
3432
+ }
3433
+
3333
3434
  // Tier 0: SKILL — slash commands (checked first, deterministic)
3334
3435
  if (trimmed.startsWith('/')) {
3335
3436
  try {
@@ -4224,6 +4325,19 @@ async function mainScreen(rl, ask) {
4224
4325
  return await offerRuntimeReloadAfterModeChange(cwd, ask, recentSessions, `Intelligence ${result.level}`);
4225
4326
  }
4226
4327
 
4328
+ if (cmd === 'provider-switch') {
4329
+ if (classified.providerSwitchCommand.automode) {
4330
+ await cmdAuto(['--level', String(_getIntelligenceLevel(loadProfile(cwd), loadSessionSettings(cwd)))]);
4331
+ return { next: 'main' };
4332
+ }
4333
+ const sess = getActionSession(cwd, recentSessions);
4334
+ if (!sess) return { next: 'new-session' };
4335
+ const target = classified.providerSwitchCommand.provider;
4336
+ process.stdout.write(`\n Switching HEAD to ${target === 'claude' ? 'Claude' : target === 'codex' ? 'Codex/GPT' : 'the other provider'}...\n\n`);
4337
+ await switchProviderNow(cwd, sess, target);
4338
+ return { next: 'main' };
4339
+ }
4340
+
4227
4341
  if (cmd === 'resume' || cmd === 'r') {
4228
4342
  if (recentSessions.length === 0) return { next: 'new-session' };
4229
4343
  return { next: 'sessions' };
@@ -4548,10 +4662,7 @@ async function switchProviderScreen(rl, ask, ctx = {}) {
4548
4662
  }
4549
4663
  if (choice === 'b' || choice === 'q') return { next: 'main' };
4550
4664
 
4551
- setSessionResumeProvider(cwd, session, target, 'user-switch-now');
4552
- writeHandoffConversationLease(cwd, session, currentTool, target, brief);
4553
- markSessionSuperseded(cwd, session, target, 'manual-provider-switch');
4554
- await cmdSwitch([target, brief]);
4665
+ await switchProviderNow(cwd, session, target);
4555
4666
  return { next: 'main' };
4556
4667
  }
4557
4668
 
@@ -7756,6 +7867,9 @@ async function main() {
7756
7867
 
7757
7868
  const args = process.argv.slice(2);
7758
7869
  const cmd = args[0];
7870
+ const isInteractive = process.stdin.isTTY;
7871
+
7872
+ await maybeAutoUpdateAndRelaunch(args, { cmd, isInteractive });
7759
7873
 
7760
7874
  // Session start marker — feeds routing advisor with cross-session timing signals
7761
7875
  try {
@@ -7766,9 +7880,6 @@ async function main() {
7766
7880
  if (cmd === '--help' || cmd === '-h') { printHelp(); return; }
7767
7881
  if (cmd === '--version' || cmd === '-v') { console.log(readVersion()); return; }
7768
7882
 
7769
- // Interactive-only commands: enter screen state machine (only when TTY)
7770
- const isInteractive = process.stdin.isTTY;
7771
-
7772
7883
  if (cmd === 'menu') {
7773
7884
  if (!isInteractive) {
7774
7885
  process.stderr.write('dual-brain menu requires an interactive terminal.\n');
@@ -8023,9 +8134,11 @@ async function main() {
8023
8134
  if (cmd === 'pr') { await cmdPR(args.slice(1)); return; }
8024
8135
  if (cmd === 'status') { await cmdStatus(args.slice(1)); return; }
8025
8136
  if (cmd === 'handoff') { await cmdHandoff(args.slice(1)); return; }
8137
+ if (cmd === 'switch' && /\b(auto|automode|auto mode|smart auto)\b/i.test(args.join(' '))) { await cmdAuto(args.slice(1)); return; }
8026
8138
  if (cmd === 'switch') { await cmdSwitch(args.slice(1)); return; }
8027
8139
  if (cmd === 'runtime-switch') { await cmdRuntimeSwitch(args.slice(1)); return; }
8028
8140
  if (cmd === 'auto' || cmd === 'automode' || cmd === 'smart-auto') { await cmdAuto(args.slice(1)); return; }
8141
+ if (cmd === 'switchover' || cmd === 'switch-over') { await cmdSwitchover(args.slice(1)); return; }
8029
8142
  if (cmd === 'update' || cmd === 'upgrade') { await cmdUpdate(); return; }
8030
8143
  if (cmd === 'hot') { cmdHot(args[1]); return; }
8031
8144
  if (cmd === 'cool') { cmdCool(args[1]); return; }
@@ -8090,7 +8203,7 @@ fi
8090
8203
  // If cmd is not a recognized subcommand, treat the entire arg list as a task.
8091
8204
  // e.g. `dual-brain fix failing tests` → same as `dual-brain go "fix failing tests"`
8092
8205
  const KNOWN_COMMANDS = new Set([
8093
- 'menu', 'init', 'install', 'uninstall', 'auth', 'go', 'do', 'plan', 'ship', 'think', 'review', 'pr', 'status', 'handoff', 'switch', 'runtime-switch', 'auto', 'automode', 'smart-auto', 'hot', 'cool',
8206
+ 'menu', 'init', 'install', 'uninstall', 'auth', 'go', 'do', 'plan', 'ship', 'think', 'review', 'pr', 'status', 'handoff', 'switch', 'switchover', 'switch-over', 'runtime-switch', 'auto', 'automode', 'smart-auto', 'hot', 'cool',
8094
8207
  'remember', 'forget', 'break-glass', 'specialists', 'search', 'shell-hook', 'watch', 'update', 'upgrade',
8095
8208
  '--help', '-h', '--version', '-v',
8096
8209
  ...Object.keys(loadSpecialistRegistry()),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dual-brain",
3
- "version": "0.3.28",
3
+ "version": "0.3.30",
4
4
  "description": "AI orchestration across Claude + OpenAI subscriptions — smart routing, budget awareness, and dual-brain collaboration",
5
5
  "type": "module",
6
6
  "bin": {