dual-brain 7.1.10 → 7.1.11

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 +177 -47
  2. package/package.json +1 -1
@@ -126,7 +126,7 @@ function printSubscriptionTable(auth, profile) {
126
126
 
127
127
  const claudeLine1 = auth.claude.found
128
128
  ? ` Claude: logged in (${auth.claude.source})`
129
- : ` Claude: not logged in — run: claude login`;
129
+ : ` Claude: not logged in — run: claude auth login`;
130
130
  const claudeLine2 = ` plan: ${claudePlanLabel}${claudeLabel}`;
131
131
 
132
132
  const openaiLine1 = auth.openai.found
@@ -156,7 +156,7 @@ async function cmdInit(rl) {
156
156
  const noneFound = !auth.claude.found && !auth.openai.found;
157
157
  if (noneFound) {
158
158
  console.log('\nNo AI provider found. Log in first:');
159
- console.log(' Claude: claude login');
159
+ console.log(' Claude: claude auth login');
160
160
  console.log(' OpenAI: codex login\n');
161
161
  console.log('Then re-run: dual-brain init');
162
162
  return;
@@ -189,7 +189,7 @@ async function cmdAuth(subArgs = []) {
189
189
 
190
190
  if (!auth.claude.found || !auth.openai.found) {
191
191
  console.log('');
192
- if (!auth.claude.found) console.log(' Claude not logged in. Run: claude login');
192
+ if (!auth.claude.found) console.log(' Claude not logged in. Run: claude auth login');
193
193
  if (!auth.openai.found) console.log(' OpenAI not logged in. Run: codex login');
194
194
  }
195
195
  }
@@ -525,6 +525,35 @@ function cmdForget(text) {
525
525
 
526
526
  // ─── Screen helpers ───────────────────────────────────────────────────────────
527
527
 
528
+ /**
529
+ * Render the data-tools-style rounded header box for the main screen.
530
+ * Inner width is 39 chars. Lines are padded with spaces to fill the box.
531
+ */
532
+ function renderHeader(version, providerLines) {
533
+ const W = 39; // inner width
534
+ const pad = (s) => {
535
+ // Strip ANSI codes for length calculation
536
+ const visible = s.replace(/\x1b\[[0-9;]*m/g, '');
537
+ return s + ' '.repeat(Math.max(0, W - visible.length));
538
+ };
539
+ const top = ` ┌${'─'.repeat(W)}┐`;
540
+ const sep = ` ├${'─'.repeat(W)}┤`;
541
+ const bottom = ` └${'─'.repeat(W)}┘`;
542
+
543
+ const title = `DATA Tools - Dual Brain v${version}`;
544
+ const credit = `by Steve Moraco + dual-brain`;
545
+
546
+ const lines = [top];
547
+ lines.push(` │ ${pad(title)}│`);
548
+ lines.push(` │ ${pad(credit)}│`);
549
+ lines.push(sep);
550
+ for (const pl of providerLines) {
551
+ lines.push(` │ ${pad(pl)}│`);
552
+ }
553
+ lines.push(bottom);
554
+ return lines.join('\n');
555
+ }
556
+
528
557
  function profileExists(cwd) {
529
558
  const dir = cwd || process.cwd();
530
559
  const globalPath = join(process.env.HOME || '/root', '.config', 'dual-brain', 'profile.json');
@@ -609,8 +638,8 @@ async function welcomeScreen(rl, ask) {
609
638
 
610
639
  if (!claudeReady && !openaiReady) {
611
640
  console.log('No CLI login found. Log in first:');
612
- console.log(' claude login — for Claude');
613
- console.log(' codex login — for OpenAI/Codex\n');
641
+ console.log(' claude auth login — for Claude');
642
+ console.log(' codex login — for OpenAI/Codex\n');
614
643
  console.log('Then re-run: dual-brain init');
615
644
  return { next: 'exit' };
616
645
  }
@@ -777,7 +806,13 @@ async function mainScreen(rl, ask) {
777
806
 
778
807
  function subLine(name, plan, found, expired, days, sub) {
779
808
  const label = sub?.label ? ` [${sub.label}]` : '';
780
- if (!found) return `⚠️ ${name}: not logged in — run: ${name === 'Claude' ? 'claude login' : 'codex login'}`;
809
+ if (!found) return `⚠️ ${name}: not logged in — run: ${name === 'Claude' ? 'claude auth login' : 'codex login'}`;
810
+ // Multi-sub: show aggregated counts when more than one sub exists
811
+ const subs = sub?.subs;
812
+ if (subs && subs.length > 1) {
813
+ const aggregate = aggregatePlans(subs);
814
+ return `✅ ${name}: ${aggregate} [${subs.length} subs]`;
815
+ }
781
816
  if (expired) return `🔴 ${name}: ${plan} expired${label} — will re-auth`;
782
817
  const daysNote = (days !== null && days <= 7) ? ` (${days}d left)` : '';
783
818
  return `✅ ${name}: ${plan}${label}${daysNote}`;
@@ -789,7 +824,7 @@ async function mainScreen(rl, ask) {
789
824
  ];
790
825
 
791
826
  console.log('');
792
- console.log(box(`🧠 dual-brain v${version}`, headerLines));
827
+ console.log(renderHeader(version, headerLines));
793
828
 
794
829
  // Auto-refresh expired subscriptions
795
830
  if (claudeExpired || openaiExpired) {
@@ -799,7 +834,7 @@ async function mainScreen(rl, ask) {
799
834
  if (openaiExpired) expired.push('OpenAI');
800
835
  console.log(`\n ${expired.join(' & ')} subscription expired. Re-authenticating...`);
801
836
  if (claudeExpired) {
802
- const r = spawnSync('claude', ['login'], { stdio: 'inherit', timeout: 30000 });
837
+ const r = spawnSync('claude', ['auth', 'login'], { stdio: 'inherit', timeout: 30000 });
803
838
  if (r.status === 0) {
804
839
  claudeSub.expiresAt = null;
805
840
  saveProfile(profile, { cwd });
@@ -820,7 +855,7 @@ async function mainScreen(rl, ask) {
820
855
  const recentSessions = enrichSessions(importReplitSessions(cwd), cwd).slice(0, 7);
821
856
 
822
857
  if (recentSessions.length > 0) {
823
- console.log(separator('Recent Sessions'));
858
+ console.log(' Recent Sessions:');
824
859
  recentSessions.forEach((sess, i) => {
825
860
  const pin = sess.pinned ? '📌 ' : ' ';
826
861
  const active = sess.isActive ? ' ●' : '';
@@ -830,18 +865,17 @@ async function mainScreen(rl, ask) {
830
865
  console.log('');
831
866
  }
832
867
 
833
- const menuOpts = [];
834
- menuOpts.push({ key: 'c', label: 'Continue last session', section: 'Sessions' });
835
- menuOpts.push({ key: 'n', label: 'New session', section: 'Sessions' });
868
+ console.log(' [c] Continue last session');
869
+ console.log(' [n] New session');
836
870
  if (recentSessions.length > 0) {
837
- menuOpts.push({ key: '1-9', label: 'Resume numbered above', section: 'Sessions' });
838
- }
839
- menuOpts.push({ key: 'e', label: 'Manage sessions', section: 'Sessions' });
840
- menuOpts.push({ key: 'd', label: 'Switch to data-tools', section: 'Tools' });
841
- menuOpts.push({ key: 'm', label: 'Manage subscriptions', section: 'Subscriptions' });
842
- menuOpts.push({ key: 's', label: 'Settings', section: '' });
843
- menuOpts.push({ key: 'q', label: 'Exit', section: '' });
844
- console.log(menu(menuOpts));
871
+ console.log(' [1-9] Resume numbered above');
872
+ }
873
+ console.log(' [e] Manage sessions');
874
+ console.log(' [i] Import from replit-tools');
875
+ console.log(' [m] Manage subscriptions');
876
+ console.log(' [d] Switch to data-tools');
877
+ console.log(' [s] Settings');
878
+ console.log(' [q] Exit');
845
879
  console.log('');
846
880
 
847
881
  const choice = (await ask(' Choice: ')).trim().toLowerCase();
@@ -872,6 +906,18 @@ async function mainScreen(rl, ask) {
872
906
 
873
907
  if (choice === 'e') { return { next: 'sessions' }; }
874
908
 
909
+ if (choice === 'i') {
910
+ const sessions = importReplitSessions(cwd);
911
+ if (sessions.length === 0) {
912
+ console.log('\n No replit-tools sessions found to import.\n');
913
+ } else {
914
+ console.log(`\n ✅ Found ${sessions.length} sessions from replit-tools.`);
915
+ console.log(' Sessions are automatically available in the list above.\n');
916
+ }
917
+ await ask(' Press Enter to continue...');
918
+ return { next: 'main' };
919
+ }
920
+
875
921
  if (choice === 'd') {
876
922
  const { spawnSync } = await import('node:child_process');
877
923
  const which = spawnSync('which', ['claude-menu'], { encoding: 'utf8' });
@@ -1000,6 +1046,26 @@ async function settingsScreen(rl, ask) {
1000
1046
  return { next: 'settings' };
1001
1047
  }
1002
1048
 
1049
+ // ─── Helper: aggregatePlans ───────────────────────────────────────────────────
1050
+
1051
+ const PLAN_PRICES = {
1052
+ pro: '$20', max5: '$100', max20: '$200',
1053
+ plus: '$20', pro100: '$100', pro200: '$200',
1054
+ };
1055
+
1056
+ function aggregatePlans(subs) {
1057
+ if (!subs || subs.length === 0) return '';
1058
+ const counts = {};
1059
+ for (const s of subs) {
1060
+ const price = PLAN_PRICES[s.plan] || s.plan;
1061
+ counts[price] = (counts[price] || 0) + 1;
1062
+ }
1063
+ return Object.entries(counts)
1064
+ .sort((a, b) => parseInt(b[0].slice(1)) - parseInt(a[0].slice(1)))
1065
+ .map(([price, count]) => `${price}×${count}`)
1066
+ .join(' ');
1067
+ }
1068
+
1003
1069
  // ─── Screen: subscriptionsScreen ─────────────────────────────────────────────
1004
1070
 
1005
1071
  async function subscriptionsScreen(rl, ask) {
@@ -1007,37 +1073,49 @@ async function subscriptionsScreen(rl, ask) {
1007
1073
  const cwd = process.cwd();
1008
1074
  const profile = loadProfile(cwd);
1009
1075
  const auth = await detectAuth();
1010
- const plans = detectPlans();
1011
1076
 
1012
- // Build status lines
1013
- const lines = [];
1014
- if (auth.claude.found) {
1015
- const plan = plans.claude?.label || plans.claude?.plan || 'unknown plan';
1016
- const sub = profile?.providers?.claude;
1017
- const label = sub?.label ? ` [${sub.label}]` : '';
1018
- const d = sub?.expiresAt ? daysUntil(sub.expiresAt) : null;
1019
- const expiry = d !== null ? ` (${d < 0 ? 'expired' : d === 0 ? 'today' : `${d}d left`})` : '';
1020
- lines.push(` ✅ Claude: ${plan}${label}${expiry}`);
1021
- } else {
1022
- lines.push(` ⚠️ Claude: not linked`);
1077
+ // Backward compat: migrate old single-sub format to subs array
1078
+ for (const prov of ['claude', 'openai']) {
1079
+ const p = profile?.providers?.[prov];
1080
+ if (p && !p.subs && p.plan) {
1081
+ p.subs = [{ plan: p.plan, label: p.label || null, expiresAt: p.expiresAt || null }];
1082
+ }
1023
1083
  }
1024
- if (auth.openai.found) {
1025
- const plan = plans.openai?.label || plans.openai?.plan || 'unknown plan';
1026
- const sub = profile?.providers?.openai;
1027
- const label = sub?.label ? ` [${sub.label}]` : '';
1028
- const d = sub?.expiresAt ? daysUntil(sub.expiresAt) : null;
1029
- const expiry = d !== null ? ` (${d < 0 ? 'expired' : d === 0 ? 'today' : `${d}d left`})` : '';
1030
- lines.push(` ✅ OpenAI: ${plan}${label}${expiry}`);
1031
- } else {
1032
- lines.push(` ⚠️ OpenAI: not linked`);
1084
+
1085
+ // Build status lines roster format
1086
+ const lines = [];
1087
+
1088
+ function buildProviderLines(provKey, displayName, authFound) {
1089
+ const sub = profile?.providers?.[provKey];
1090
+ const subs = sub?.subs || [];
1091
+ if (!authFound && subs.length === 0) {
1092
+ lines.push(` ⚠️ ${displayName}: not linked`);
1093
+ return;
1094
+ }
1095
+ const aggregate = aggregatePlans(subs);
1096
+ const prefix = authFound ? '✅' : '⚠️ ';
1097
+ lines.push(` ${prefix} ${displayName}:${aggregate ? ' ' + aggregate : ' (no subs)'}`);
1098
+ subs.forEach((s, i) => {
1099
+ const planLabels = provKey === 'claude' ? CLAUDE_PLAN_LABELS : OPENAI_PLAN_LABELS;
1100
+ const planLabel = planLabels[s.plan] ?? s.plan ?? 'unknown';
1101
+ const nameStr = (s.label || '(no label)').padEnd(22);
1102
+ const d = s.expiresAt ? daysUntil(s.expiresAt) : null;
1103
+ const expiry = d === null ? '' : d < 0 ? ' (expired)' : d === 0 ? ' (today)' : ` (${d}d left)`;
1104
+ lines.push(` ${i + 1}. ${nameStr} ${planLabel}${expiry}`);
1105
+ });
1033
1106
  }
1034
1107
 
1108
+ buildProviderLines('claude', 'Claude', auth.claude.found);
1109
+ lines.push('');
1110
+ buildProviderLines('openai', 'OpenAI', auth.openai.found);
1111
+
1035
1112
  console.log(box('Subscriptions', lines));
1036
1113
  console.log('');
1037
1114
 
1038
1115
  const menuOpts = [
1039
1116
  { key: '1', label: 'Add Claude sub', section: 'Link' },
1040
1117
  { key: '2', label: 'Add Codex sub', section: 'Link' },
1118
+ { key: 'r', label: 'Remove a sub', section: 'Link' },
1041
1119
  { key: 'b', label: 'Back to home', section: '' },
1042
1120
  ];
1043
1121
  console.log(menu(menuOpts));
@@ -1049,7 +1127,7 @@ async function subscriptionsScreen(rl, ask) {
1049
1127
  console.log('\n Linking Claude subscription...');
1050
1128
  console.log(' A browser window will open — paste the code below when prompted.\n');
1051
1129
  const { spawnSync } = await import('node:child_process');
1052
- const r = spawnSync('claude', ['login'], { stdio: 'inherit', timeout: 60000 });
1130
+ const r = spawnSync('claude', ['auth', 'login'], { stdio: 'inherit', timeout: 60000 });
1053
1131
  if (r.status === 0) {
1054
1132
  console.log('\n ✅ Claude linked successfully!\n');
1055
1133
  const label = (await ask(" Label (e.g. \"Josh's $100 sub\", or Enter to skip): ")).trim();
@@ -1060,8 +1138,9 @@ async function subscriptionsScreen(rl, ask) {
1060
1138
  if (!profile.providers.claude) profile.providers.claude = { enabled: true };
1061
1139
  profile.providers.claude.plan = plan;
1062
1140
  profile.providers.claude.enabled = true;
1063
- if (label) profile.providers.claude.label = label;
1064
- if (expiry) profile.providers.claude.expiresAt = expiry;
1141
+ // Push to subs array instead of overwriting
1142
+ if (!profile.providers.claude.subs) profile.providers.claude.subs = [];
1143
+ profile.providers.claude.subs.push({ plan, label: label || null, expiresAt: expiry || null });
1065
1144
  saveProfile(profile, { cwd });
1066
1145
  console.log(' ✓ Saved\n');
1067
1146
  await ask(' Press Enter to continue...');
@@ -1087,8 +1166,9 @@ async function subscriptionsScreen(rl, ask) {
1087
1166
  if (!profile.providers.openai) profile.providers.openai = { enabled: true };
1088
1167
  profile.providers.openai.plan = plan;
1089
1168
  profile.providers.openai.enabled = true;
1090
- if (label) profile.providers.openai.label = label;
1091
- if (expiry) profile.providers.openai.expiresAt = expiry;
1169
+ // Push to subs array instead of overwriting
1170
+ if (!profile.providers.openai.subs) profile.providers.openai.subs = [];
1171
+ profile.providers.openai.subs.push({ plan, label: label || null, expiresAt: expiry || null });
1092
1172
  saveProfile(profile, { cwd });
1093
1173
  console.log(' ✓ Saved\n');
1094
1174
  await ask(' Press Enter to continue...');
@@ -1099,6 +1179,56 @@ async function subscriptionsScreen(rl, ask) {
1099
1179
  return { next: 'subscriptions' };
1100
1180
  }
1101
1181
 
1182
+ if (choice === 'r') {
1183
+ // Build a flat numbered list of all subs across both providers
1184
+ const allSubs = [];
1185
+ for (const [provKey, displayName] of [['claude', 'Claude'], ['openai', 'OpenAI']]) {
1186
+ const subs = profile?.providers?.[provKey]?.subs || [];
1187
+ for (const s of subs) {
1188
+ allSubs.push({ provKey, displayName, sub: s });
1189
+ }
1190
+ }
1191
+
1192
+ if (allSubs.length === 0) {
1193
+ console.log('\n No subscriptions to remove.\n');
1194
+ await ask(' Press Enter to continue...');
1195
+ return { next: 'subscriptions' };
1196
+ }
1197
+
1198
+ console.log('\n Remove a subscription:\n');
1199
+ allSubs.forEach(({ displayName, sub }, i) => {
1200
+ const planLabels = displayName === 'Claude' ? CLAUDE_PLAN_LABELS : OPENAI_PLAN_LABELS;
1201
+ const planLabel = planLabels[sub.plan] ?? sub.plan ?? 'unknown';
1202
+ const labelStr = sub.label ? ` [${sub.label}]` : '';
1203
+ console.log(` (${i + 1}) ${displayName}: ${planLabel}${labelStr}`);
1204
+ });
1205
+ console.log(' (Enter) Cancel\n');
1206
+
1207
+ const numStr = (await ask(' Remove #: ')).trim();
1208
+ const numChoice = parseInt(numStr, 10);
1209
+ if (!isNaN(numChoice) && numChoice >= 1 && numChoice <= allSubs.length) {
1210
+ const { provKey, sub } = allSubs[numChoice - 1];
1211
+ const confirm = (await ask(` Remove "${sub.label || sub.plan}" from ${provKey}? (y/N): `)).trim().toLowerCase();
1212
+ if (confirm === 'y') {
1213
+ const subs = profile.providers[provKey].subs;
1214
+ const idx = subs.indexOf(sub);
1215
+ if (idx !== -1) subs.splice(idx, 1);
1216
+ // Update top-level plan to first remaining sub (or keep as-is)
1217
+ if (subs.length > 0) {
1218
+ profile.providers[provKey].plan = subs[0].plan;
1219
+ }
1220
+ saveProfile(profile, { cwd });
1221
+ console.log(' ✓ Removed\n');
1222
+ } else {
1223
+ console.log(' Cancelled.\n');
1224
+ }
1225
+ } else {
1226
+ console.log(' Cancelled.\n');
1227
+ }
1228
+ await ask(' Press Enter to continue...');
1229
+ return { next: 'subscriptions' };
1230
+ }
1231
+
1102
1232
  return { next: 'main' };
1103
1233
  }
1104
1234
 
@@ -1128,7 +1258,7 @@ async function authScreen(rl, ask) {
1128
1258
  'Claude:',
1129
1259
  auth.claude.found
1130
1260
  ? ` logged in via ${auth.claude.source}`
1131
- : ` not logged in — run: claude login`,
1261
+ : ` not logged in — run: claude auth login`,
1132
1262
  ` plan: ${claudePlanLabel}${claudeSub?.label ? ` [${claudeSub.label}]` : ''}`,
1133
1263
  '',
1134
1264
  'OpenAI:',
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dual-brain",
3
- "version": "7.1.10",
3
+ "version": "7.1.11",
4
4
  "description": "AI orchestration across Claude + OpenAI subscriptions — smart routing, budget awareness, and dual-brain collaboration",
5
5
  "type": "module",
6
6
  "bin": {