dual-brain 0.2.19 → 0.2.21

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.
@@ -2755,18 +2755,23 @@ async function mainScreen(rl, ask) {
2755
2755
  const recentWorkItems = [];
2756
2756
  // Add awareness observations as recent work if meaningful
2757
2757
  if (awarenessLine1 && !awarenessLine1.includes('Ready to work')) {
2758
- const plainAware1 = awarenessLine1.replace(/\x1b\[[0-9;]*m/g, '').replace(/[︀-️]/g, '').trim();
2759
- if (plainAware1) recentWorkItems.push({ ok: !plainAware1.startsWith('⚠') && !plainAware1.startsWith('🔴'), text: plainAware1.replace(/^[🔴🟡💡]\s*/, '') });
2758
+ const plainAware1 = awarenessLine1.replace(/\x1b\[[0-9;]*m/g, '').replace(/[︀-️‍]/g, '').trim();
2759
+ if (plainAware1) {
2760
+ const isWarning = /uncommitted|stale|failure|expired|old|⚠/.test(plainAware1);
2761
+ recentWorkItems.push({ ok: !isWarning, text: plainAware1.replace(/^[🔴🟡💡⚠]\s*/, '') });
2762
+ }
2760
2763
  }
2761
2764
  // Add last commit as a recent work item
2762
2765
  if (gitLastMsg) {
2763
- recentWorkItems.push({ ok: true, text: `${gitLastMsg} (${gitLastAgo})` });
2766
+ const isStale = /\d+d ago|\d{2,}h ago/.test(gitLastAgo);
2767
+ recentWorkItems.push({ ok: !isStale, text: `${gitLastMsg} (${gitLastAgo})` });
2764
2768
  }
2765
2769
  // Fill from sessions if still room
2766
2770
  if (recentWorkItems.length < 3 && recentSessions.length > 0) {
2767
2771
  const sess = recentSessions[0];
2768
2772
  let rawName = sess.name || '';
2769
- if (/^Session [0-9a-f]{8,}$/i.test(rawName)) rawName = sess.id.slice(0, 8);
2773
+ if (/^Session [0-9a-f]{8,}$/i.test(rawName)) rawName = '';
2774
+ if (/^[0-9a-f]{6,}$/i.test(rawName)) rawName = '';
2770
2775
  if (rawName) recentWorkItems.push({ ok: true, text: rawName.slice(0, 50) });
2771
2776
  }
2772
2777
 
@@ -2921,17 +2926,22 @@ async function mainScreen(rl, ask) {
2921
2926
  }
2922
2927
 
2923
2928
  // Shortcut bar — always visible so the user never has to guess
2924
- const autoLabel = profile.automode ? `\x1b[32m⚡auto\x1b[0m` : `${DIM}auto${RST}`;
2925
- const shortcutItems = [
2926
- `${CYAN}Enter${RST} resume`,
2927
- `${CYAN}n${RST} new`,
2928
- `${CYAN}/${RST} search`,
2929
- `${CYAN}s${RST} settings`,
2930
- `${CYAN}d${RST} doctor`,
2931
- autoLabel,
2932
- `${CYAN}q${RST} quit`,
2929
+ const shortcuts = [
2930
+ [`Enter`, isReturning ? 'resume last session' : 'start working'],
2931
+ [`n`, 'new session'],
2932
+ [`/`, 'search sessions'],
2933
+ [`s`, 'settings & profiles'],
2934
+ [`d`, 'doctor (diagnose issues)'],
2935
+ [`a`, profile.automode ? 'auto mode ⚡ on' : 'auto mode'],
2936
+ [`q`, 'quit'],
2933
2937
  ];
2934
- process.stdout.write(` ${DIM}${shortcutItems.join(' ')}${RST}\n\n`);
2938
+ process.stdout.write('\n');
2939
+ for (const [key, label] of shortcuts) {
2940
+ const keyStr = key === 'Enter' ? `${CYAN}Enter${RST}` : ` ${CYAN}${key}${RST} `;
2941
+ const padded = key === 'Enter' ? ' ' : ' ';
2942
+ process.stdout.write(` ${keyStr}${padded}${DIM}${label}${RST}\n`);
2943
+ }
2944
+ process.stdout.write('\n');
2935
2945
 
2936
2946
  // Input bar — rendered below shortcut bar
2937
2947
  const inputLeft = tuiPrompt('task or command...');
package/install.mjs CHANGED
@@ -15,6 +15,8 @@ import { dirname, join, resolve } from 'path';
15
15
  import { fileURLToPath } from 'url';
16
16
  import { spawnSync } from 'child_process';
17
17
  import { createHash } from 'crypto';
18
+ import { spinner, success as fxSuccess, warn as fxWarn, error as fxError, info as fxInfo, banner, celebrate, colors, sleep, nl, getMode } from './src/fx.mjs';
19
+ import { panel, signalLine, headerBar } from './src/tui.mjs';
18
20
 
19
21
  // Skip hook installation during global npm install — hooks are installed
20
22
  // when the user runs 'dual-brain install' in their project directory.
@@ -103,14 +105,14 @@ if (subcommand && !SUBCOMMANDS.includes(subcommand)) {
103
105
  process.exit(1);
104
106
  }
105
107
 
106
- // ─── Box Drawing ────────────────────────────────────────────────────────────
108
+ // ─── Box Drawing (legacy compat — prefer panel() from tui.mjs) ─────────────
107
109
 
108
110
  const W = 54;
109
- const pad = (s, len = W - 2) => {
111
+ const pad_legacy = (s, len = W - 2) => {
110
112
  s = String(s);
111
113
  return s.length >= len ? s.slice(0, len) : s + ' '.repeat(len - s.length);
112
114
  };
113
- const ln = (s) => `║ ${pad(s)} ║`;
115
+ const ln = (s) => `║ ${pad_legacy(s)} ║`;
114
116
  const br = (l, r) => l + '═'.repeat(W) + r;
115
117
  const sep = () => '╠' + '═'.repeat(W) + '╣';
116
118
 
@@ -421,10 +423,8 @@ async function authGuidance(env) {
421
423
  if (env.claude.authed && env.codex.authed) return env;
422
424
  if (!process.stdin.isTTY || !process.stdout.isTTY) return env;
423
425
 
424
- console.log('');
425
- console.log(' ┌────────────────────────────────────────────┐');
426
- console.log(' │ 🔑 Auth Setup │');
427
- console.log(' └────────────────────────────────────────────┘');
426
+ nl();
427
+ console.log(panel('Auth Setup', ['Checking provider authentication...'], { width: 50 }));
428
428
 
429
429
  if (!env.claude.authed) {
430
430
  console.log('');
@@ -654,27 +654,24 @@ function getAuthState() {
654
654
  }
655
655
 
656
656
  function printAuthStatusBox(state) {
657
- const c = state.claude;
657
+ const cl = state.claude;
658
658
  const x = state.codex;
659
- const cIcon = c.authed ? '✅' : c.installed ? '⚠️' : '❌';
660
- const xIcon = x.authed ? '✅' : x.installed ? '⚠️' : '❌';
661
659
 
662
- console.log('');
663
- console.log(` ${br('', '')}`);
664
- console.log(` ${ln(`🔑 Auth Status`)}`);
665
- console.log(` ${sep()}`);
666
- console.log(` ${ln(`🟠 Claude ${cIcon} ${c.authed ? 'authenticated' : c.installed ? 'not authenticated' : 'not installed'}`)}`);
667
- console.log(` ${ln(` Method: ${c.method}`)}`);
668
- console.log(` ${ln(` Expiry: ${c.expiryText}`)}`);
669
- console.log(` ${ln(` Storage: ${c.storageText}`)}`);
670
- console.log(` ${sep()}`);
671
- console.log(` ${ln(`🟢 Codex ${xIcon} ${x.authed ? 'authenticated' : x.installed ? 'not authenticated' : 'not installed'}`)}`);
672
- console.log(` ${ln(` Method: ${x.method}`)}`);
673
- console.log(` ${ln(` Expiry: ${x.expiryText}`)}`);
674
- console.log(` ${ln(` Storage: ${x.storageText}`)}`);
675
- if (x.lastRefresh) console.log(` ${ln(` Refreshed:${x.lastRefreshText}`)}`);
676
- console.log(` ${br('╚', '╝')}`);
677
- console.log('');
660
+ const lines = [];
661
+ lines.push(signalLine(cl.authed ? 'success' : 'warning', `Claude ${cl.authed ? 'authenticated' : cl.installed ? 'not authenticated' : 'not installed'}`));
662
+ lines.push(` Method: ${cl.method}`);
663
+ lines.push(` Expiry: ${cl.expiryText}`);
664
+ lines.push(` Storage: ${cl.storageText}`);
665
+ lines.push('');
666
+ lines.push(signalLine(x.authed ? 'success' : 'warning', `Codex ${x.authed ? 'authenticated' : x.installed ? 'not authenticated' : 'not installed'}`));
667
+ lines.push(` Method: ${x.method}`);
668
+ lines.push(` Expiry: ${x.expiryText}`);
669
+ lines.push(` Storage: ${x.storageText}`);
670
+ if (x.lastRefresh) lines.push(` Refreshed:${x.lastRefreshText}`);
671
+
672
+ nl();
673
+ console.log(panel('Auth Status', lines, { width: 60 }));
674
+ nl();
678
675
  }
679
676
 
680
677
  function runCodexDeviceAuth(codexPath) {
@@ -981,36 +978,38 @@ function install(workspace, env, mode) {
981
978
 
982
979
  // ─── Status Report ──────────────────────────────────────────────────────────
983
980
 
984
- function printReport(env, mode, actions, isDryRun) {
985
- const lines = [];
981
+ function printReport(env, mode, actions, isDryRun, { skipBanner = false } = {}) {
982
+ const m = getMode();
983
+ nl();
986
984
 
987
- lines.push(br('╔', '╗'));
988
- lines.push(ln(`🧠 Data Tools — Dual Brain v${VERSION}`));
989
- lines.push(sep());
985
+ // Gradient banner (skip if already shown in animated detection)
986
+ if (!skipBanner) banner(`v${VERSION}`);
990
987
 
991
- const cAuth = env.claude.authed ? '✅' : env.claude.installed ? '⚠️' : '❌';
992
- const xAuth = env.codex.authed ? '' : env.codex.installed ? '⚠️' : '';
993
- lines.push(ln(` 🟠 Claude ${cAuth} 🟢 Codex ${xAuth}`));
988
+ // Provider status
989
+ const cAuth = env.claude.authed ? 'authenticated' : env.claude.installed ? 'installed · not authed' : 'not installed';
990
+ const xAuth = env.codex.authed ? 'authenticated' : env.codex.installed ? 'installed · not authed' : 'not installed';
991
+ const cHealthy = env.claude.authed;
992
+ const xHealthy = env.codex.authed;
994
993
 
994
+ const statusLines = [];
995
+ statusLines.push(signalLine(cHealthy ? 'success' : 'warning', `Claude ${cAuth}`));
996
+ statusLines.push(signalLine(xHealthy ? 'success' : 'warning', `Codex ${xAuth}`));
995
997
  if (env.isReplit) {
996
- lines.push(ln(` 🌀 Replit${env.hasReplitTools ? ' + replit-tools' : ''}`));
998
+ statusLines.push(signalLine('info', `Replit${env.hasReplitTools ? ' + replit-tools' : ''}`));
997
999
  }
998
1000
 
999
1001
  if (actions) {
1000
- lines.push(sep());
1001
- for (const a of actions) lines.push(ln(` ${a}`));
1002
- lines.push(sep());
1003
- lines.push(ln(' Installed — launching session manager...'));
1002
+ statusLines.push('');
1003
+ for (const a of actions) statusLines.push(` ${a}`);
1004
+ statusLines.push('');
1005
+ statusLines.push(signalLine('success', 'Installed — launching session manager...'));
1004
1006
  } else if (isDryRun) {
1005
- lines.push(sep());
1006
- lines.push(ln('Dry run — no files written'));
1007
+ statusLines.push('');
1008
+ statusLines.push(signalLine('info', 'Dry run — no files written'));
1007
1009
  }
1008
1010
 
1009
- lines.push(br('', '╝'));
1010
-
1011
- console.log('');
1012
- for (const l of lines) console.log(` ${l}`);
1013
- console.log('');
1011
+ console.log(panel('dual-brain status', statusLines, { width: 64 }));
1012
+ nl();
1014
1013
  }
1015
1014
 
1016
1015
  // ─── Profile System ────────────────────────────────────────────────────────
@@ -1223,17 +1222,19 @@ function cmdMode() {
1223
1222
  const current = loadProfile(workspace);
1224
1223
  const PEMOJIS = { auto: '🤖', balanced: '⚖️ ', 'cost-saver': '🛡️', 'quality-first': '🚀' };
1225
1224
  const UI_NAMES = { auto: 'Auto (default)', balanced: 'Balanced', 'cost-saver': 'Conservative', 'quality-first': 'Aggressive' };
1226
- console.log('');
1227
- console.log(' 🎛️ Routing modes:');
1228
- console.log('');
1225
+ nl();
1226
+ console.log(` ${colors.bold}${colors.cyan}Routing modes:${colors.reset}`);
1227
+ nl();
1229
1228
  for (const [name, p] of Object.entries(PROFILES)) {
1230
- const active = name === current.name ? ' ✅ active' : '';
1229
+ const isActive = name === current.name;
1231
1230
  const label = UI_NAMES[name] || name;
1232
- console.log(` ${PEMOJIS[name] || ' '} ${label.padEnd(15)} ${p.description}${active}`);
1231
+ const activeMarker = isActive ? ` ${colors.green} active${colors.reset}` : '';
1232
+ const style = isActive ? `${colors.cyan}${colors.bold}` : colors.dim;
1233
+ console.log(` ${PEMOJIS[name] || ' '} ${style}${label.padEnd(15)}${colors.reset} ${p.description}${activeMarker}`);
1233
1234
  }
1234
- console.log('');
1235
+ nl();
1235
1236
  console.log(` Switch: ${cmd('npx dual-brain mode <name>')}`);
1236
- console.log('');
1237
+ nl();
1237
1238
  return;
1238
1239
  }
1239
1240
 
@@ -1434,7 +1435,85 @@ async function main() {
1434
1435
  if (subcommand === 'budget') { cmdBudget(); return; }
1435
1436
  if (subcommand === 'explain') { cmdExplain(); return; }
1436
1437
 
1437
- let env = detectEnvironment();
1438
+ const mode_fx = getMode();
1439
+ const animate = mode_fx === 'full' || mode_fx === 'subtle';
1440
+
1441
+ // Animated detection phase
1442
+ nl();
1443
+ banner(`v${VERSION}`);
1444
+
1445
+ let env;
1446
+ if (animate) {
1447
+ const sp1 = spinner('Detecting environment...').start();
1448
+ await sleep(300);
1449
+ env = detectEnvironment();
1450
+
1451
+ // Workspace detection
1452
+ if (env.isReplit) {
1453
+ sp1.succeed(`Replit workspace detected${env.hasReplitTools ? ' + replit-tools' : ''}`);
1454
+ } else {
1455
+ sp1.succeed('Local workspace detected');
1456
+ }
1457
+
1458
+ // Node version
1459
+ const sp2 = spinner('Checking Node.js...').start();
1460
+ await sleep(200);
1461
+ sp2.succeed(`Node ${process.version} found`);
1462
+
1463
+ // Git repo
1464
+ const sp3 = spinner('Checking git...').start();
1465
+ await sleep(200);
1466
+ const gitResult = run('git', ['rev-parse', '--show-toplevel']);
1467
+ const gitBranch = run('git', ['branch', '--show-current']);
1468
+ if (gitResult.status === 0) {
1469
+ const repoName = gitResult.stdout.trim().split('/').pop();
1470
+ const branch = gitBranch.stdout?.trim() || 'unknown';
1471
+ sp3.succeed(`Git repository: ${repoName} (${branch})`);
1472
+ } else {
1473
+ sp3.warn('Not a git repository');
1474
+ }
1475
+
1476
+ // Claude CLI
1477
+ const sp4 = spinner('Checking Claude CLI...').start();
1478
+ await sleep(300);
1479
+ if (env.claude.installed) {
1480
+ const authLabel = env.claude.authed ? 'CLI OAuth' : 'not authenticated';
1481
+ sp4.succeed(`Claude CLI found · ${authLabel}`);
1482
+ } else {
1483
+ sp4.warn('Claude CLI not found');
1484
+ }
1485
+
1486
+ // Codex CLI
1487
+ const sp5 = spinner('Checking Codex CLI...').start();
1488
+ await sleep(300);
1489
+ if (env.codex.installed) {
1490
+ const authLabel = env.codex.authed ? 'authenticated' : 'not authenticated';
1491
+ sp5.succeed(`OpenAI Codex CLI found · ${authLabel}`);
1492
+ } else {
1493
+ sp5.warn('Codex CLI not found');
1494
+ }
1495
+
1496
+ // Sessions
1497
+ const sp6 = spinner('Checking sessions...').start();
1498
+ await sleep(200);
1499
+ const sessionsDir = resolve(process.cwd(), '.replit-tools', '.claude-persistent');
1500
+ if (existsSync(sessionsDir)) {
1501
+ sp6.succeed('Session persistence via replit-tools');
1502
+ } else {
1503
+ sp6.succeed('Standard session management');
1504
+ }
1505
+
1506
+ nl();
1507
+ } else {
1508
+ // Non-animated fallback for CI/plain
1509
+ env = detectEnvironment();
1510
+ fxInfo(`Workspace: ${env.isReplit ? 'Replit' : 'local'}`);
1511
+ fxInfo(`Node ${process.version}`);
1512
+ fxInfo(`Claude: ${env.claude.installed ? (env.claude.authed ? 'authed' : 'installed') : 'missing'}`);
1513
+ fxInfo(`Codex: ${env.codex.installed ? (env.codex.authed ? 'authed' : 'installed') : 'missing'}`);
1514
+ nl();
1515
+ }
1516
+
1438
1517
  const startupUpdateInfo = (subcommand === 'update' || dryRun || jsonOut)
1439
1518
  ? null
1440
1519
  : checkForUpdate(env.workspace);
@@ -1445,9 +1524,9 @@ async function main() {
1445
1524
  process.stdout.isTTY &&
1446
1525
  !process.env.CI
1447
1526
  ) {
1448
- console.log('');
1527
+ nl();
1449
1528
  const shouldUpdate = await promptForUpdate(startupUpdateInfo);
1450
- console.log('');
1529
+ nl();
1451
1530
  if (shouldUpdate) {
1452
1531
  env = healClaudeAuth(env);
1453
1532
  env = healCodexAuth(env);
@@ -1477,29 +1556,38 @@ async function main() {
1477
1556
  if (jsonOut) {
1478
1557
  console.log(JSON.stringify({ version: VERSION, env, mode }, null, 2));
1479
1558
  } else {
1480
- printReport(env, mode, null, true);
1559
+ printReport(env, mode, null, true, { skipBanner: true });
1481
1560
  }
1482
1561
  process.exit(0);
1483
1562
  }
1484
1563
 
1485
1564
  if (subcommand === 'update') {
1486
1565
  const actions = performUpdate(env.workspace, env, mode);
1487
- printReport(env, mode, actions);
1566
+ printReport(env, mode, actions, false, { skipBanner: true });
1488
1567
  process.exit(0);
1489
1568
  }
1490
1569
 
1491
1570
  // Check for replit-tools on Replit
1492
1571
  if (env.isReplit && !env.hasReplitTools) {
1493
- console.log('');
1494
- console.log(' ⚠️ replit-tools not found — recommended for Replit environments.');
1495
- console.log(' Dual-brain works best alongside replit-tools for persistent auth,');
1496
- console.log(' session management, and shell integration.');
1497
- console.log('');
1498
- console.log(` Install: ${cmd('npx -y data-tools')}`);
1499
- console.log('');
1572
+ nl();
1573
+ fxWarn('replit-tools not found — recommended for Replit environments.');
1574
+ fxInfo('Dual-brain works best alongside replit-tools for persistent auth,');
1575
+ fxInfo('session management, and shell integration.');
1576
+ nl();
1577
+ fxInfo(`Install: ${cmd('npx -y data-tools')}`);
1578
+ nl();
1500
1579
  }
1501
1580
 
1502
- const actions = install(env.workspace, env, mode);
1581
+ // Install hooks and config
1582
+ let actions;
1583
+ if (animate) {
1584
+ const spInstall = spinner('Installing dual-brain hooks...').start();
1585
+ await sleep(400);
1586
+ actions = install(env.workspace, env, mode);
1587
+ spInstall.succeed(`Installed ${actions.length} components`);
1588
+ } else {
1589
+ actions = install(env.workspace, env, mode);
1590
+ }
1503
1591
 
1504
1592
  // Write a standalone shell-hook.sh so users can source it from .bashrc.
1505
1593
  // Non-interactive installs (npm postinstall) just print the hint; interactive
@@ -1530,7 +1618,10 @@ async function main() {
1530
1618
  }
1531
1619
  }
1532
1620
 
1533
- printReport(env, mode, actions);
1621
+ nl();
1622
+ await celebrate('Setup complete');
1623
+ nl();
1624
+ printReport(env, mode, actions, false, { skipBanner: true });
1534
1625
 
1535
1626
  // After install, launch the session manager (interactive TTY only)
1536
1627
  if (process.stdin.isTTY && process.stdout.isTTY && !process.env.CI) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dual-brain",
3
- "version": "0.2.19",
3
+ "version": "0.2.21",
4
4
  "description": "AI orchestration across Claude + OpenAI subscriptions — smart routing, budget awareness, and dual-brain collaboration",
5
5
  "type": "module",
6
6
  "bin": {