dual-brain 0.3.28 → 0.3.29

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 +85 -7
  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`); }
@@ -2501,6 +2533,41 @@ function applyIntelligenceCommand(cmd, cwd) {
2501
2533
  return { scope: 'this session', level: cmd.level };
2502
2534
  }
2503
2535
 
2536
+ function parseProviderSwitchCommand(input) {
2537
+ const text = input.trim().toLowerCase().replace(/\s+/g, ' ');
2538
+ 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)) {
2539
+ return null;
2540
+ }
2541
+ let provider = null;
2542
+ if (/\b(gpt|codex|openai)\b/.test(text)) provider = 'codex';
2543
+ if (/\bclaude\b/.test(text)) provider = 'claude';
2544
+ return { provider };
2545
+ }
2546
+
2547
+ function getActionSession(cwd, recentSessions = []) {
2548
+ const active = readActiveConversation(cwd);
2549
+ if (active) {
2550
+ return recentSessions.find(s => s.id === active.sessionId) || {
2551
+ id: active.sessionId,
2552
+ tool: active.provider,
2553
+ smartName: active.sessionName || active.conversationId || active.sessionId,
2554
+ };
2555
+ }
2556
+ return recentSessions[0] || null;
2557
+ }
2558
+
2559
+ async function switchProviderNow(cwd, session, target = null) {
2560
+ if (!session) return false;
2561
+ const currentTool = _sessionTool(session);
2562
+ const targetTool = target || (currentTool === 'codex' ? 'claude' : 'codex');
2563
+ const brief = _sessionBrief(session, targetTool);
2564
+ setSessionResumeProvider(cwd, session, targetTool, 'head-provider-switch');
2565
+ writeHandoffConversationLease(cwd, session, currentTool, targetTool, brief);
2566
+ markSessionSuperseded(cwd, session, targetTool, 'head-provider-switch');
2567
+ await cmdSwitch([targetTool, brief]);
2568
+ return true;
2569
+ }
2570
+
2504
2571
  async function offerRuntimeReloadAfterModeChange(cwd, ask, recentSessions, label = 'runtime settings') {
2505
2572
  const cyan = '\x1b[36m';
2506
2573
  const reset = '\x1b[0m';
@@ -3330,6 +3397,11 @@ function classifyInput(input) {
3330
3397
  return { tier: 'free', command: 'intelligence', args: [], intelligenceCommand };
3331
3398
  }
3332
3399
 
3400
+ const providerSwitchCommand = parseProviderSwitchCommand(trimmed);
3401
+ if (providerSwitchCommand) {
3402
+ return { tier: 'free', command: 'provider-switch', args: [], providerSwitchCommand };
3403
+ }
3404
+
3333
3405
  // Tier 0: SKILL — slash commands (checked first, deterministic)
3334
3406
  if (trimmed.startsWith('/')) {
3335
3407
  try {
@@ -4224,6 +4296,15 @@ async function mainScreen(rl, ask) {
4224
4296
  return await offerRuntimeReloadAfterModeChange(cwd, ask, recentSessions, `Intelligence ${result.level}`);
4225
4297
  }
4226
4298
 
4299
+ if (cmd === 'provider-switch') {
4300
+ const sess = getActionSession(cwd, recentSessions);
4301
+ if (!sess) return { next: 'new-session' };
4302
+ const target = classified.providerSwitchCommand.provider;
4303
+ process.stdout.write(`\n Switching HEAD to ${target === 'claude' ? 'Claude' : target === 'codex' ? 'Codex/GPT' : 'the other provider'}...\n\n`);
4304
+ await switchProviderNow(cwd, sess, target);
4305
+ return { next: 'main' };
4306
+ }
4307
+
4227
4308
  if (cmd === 'resume' || cmd === 'r') {
4228
4309
  if (recentSessions.length === 0) return { next: 'new-session' };
4229
4310
  return { next: 'sessions' };
@@ -4548,10 +4629,7 @@ async function switchProviderScreen(rl, ask, ctx = {}) {
4548
4629
  }
4549
4630
  if (choice === 'b' || choice === 'q') return { next: 'main' };
4550
4631
 
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]);
4632
+ await switchProviderNow(cwd, session, target);
4555
4633
  return { next: 'main' };
4556
4634
  }
4557
4635
 
@@ -7756,6 +7834,9 @@ async function main() {
7756
7834
 
7757
7835
  const args = process.argv.slice(2);
7758
7836
  const cmd = args[0];
7837
+ const isInteractive = process.stdin.isTTY;
7838
+
7839
+ await maybeAutoUpdateAndRelaunch(args, { cmd, isInteractive });
7759
7840
 
7760
7841
  // Session start marker — feeds routing advisor with cross-session timing signals
7761
7842
  try {
@@ -7766,9 +7847,6 @@ async function main() {
7766
7847
  if (cmd === '--help' || cmd === '-h') { printHelp(); return; }
7767
7848
  if (cmd === '--version' || cmd === '-v') { console.log(readVersion()); return; }
7768
7849
 
7769
- // Interactive-only commands: enter screen state machine (only when TTY)
7770
- const isInteractive = process.stdin.isTTY;
7771
-
7772
7850
  if (cmd === 'menu') {
7773
7851
  if (!isInteractive) {
7774
7852
  process.stderr.write('dual-brain menu requires an interactive terminal.\n');
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dual-brain",
3
- "version": "0.3.28",
3
+ "version": "0.3.29",
4
4
  "description": "AI orchestration across Claude + OpenAI subscriptions — smart routing, budget awareness, and dual-brain collaboration",
5
5
  "type": "module",
6
6
  "bin": {