dual-brain 7.1.8 → 7.1.10

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.
@@ -838,8 +838,7 @@ async function mainScreen(rl, ask) {
838
838
  }
839
839
  menuOpts.push({ key: 'e', label: 'Manage sessions', section: 'Sessions' });
840
840
  menuOpts.push({ key: 'd', label: 'Switch to data-tools', section: 'Tools' });
841
- menuOpts.push({ key: 'j', label: 'Login to Claude', section: 'Auth' });
842
- menuOpts.push({ key: 'k', label: 'Login to Codex', section: 'Auth' });
841
+ menuOpts.push({ key: 'm', label: 'Manage subscriptions', section: 'Subscriptions' });
843
842
  menuOpts.push({ key: 's', label: 'Settings', section: '' });
844
843
  menuOpts.push({ key: 'q', label: 'Exit', section: '' });
845
844
  console.log(menu(menuOpts));
@@ -885,17 +884,7 @@ async function mainScreen(rl, ask) {
885
884
  return { next: 'main' };
886
885
  }
887
886
 
888
- if (choice === 'j') {
889
- const { spawnSync } = await import('node:child_process');
890
- spawnSync('claude', ['login'], { stdio: 'inherit' });
891
- return { next: 'main' };
892
- }
893
-
894
- if (choice === 'k') {
895
- const { spawnSync } = await import('node:child_process');
896
- spawnSync('codex', ['login'], { stdio: 'inherit' });
897
- return { next: 'main' };
898
- }
887
+ if (choice === 'm') { return { next: 'subscriptions' }; }
899
888
 
900
889
  if (choice === 's') { return { next: 'settings' }; }
901
890
  if (choice === 'q' || choice === 'exit') { return { next: 'exit' }; }
@@ -1014,116 +1003,103 @@ async function settingsScreen(rl, ask) {
1014
1003
  // ─── Screen: subscriptionsScreen ─────────────────────────────────────────────
1015
1004
 
1016
1005
  async function subscriptionsScreen(rl, ask) {
1006
+ console.clear();
1017
1007
  const cwd = process.cwd();
1018
1008
  const profile = loadProfile(cwd);
1019
1009
  const auth = await detectAuth();
1020
1010
  const plans = detectPlans();
1021
1011
 
1022
- const claudeSub = profile?.providers?.claude;
1023
- const openaiSub = profile?.providers?.openai;
1024
-
1025
- const claudePlanLabel = claudeSub?.enabled
1026
- ? (CLAUDE_PLAN_LABELS[claudeSub.plan] ?? claudeSub.plan ?? 'n/a')
1027
- : 'disabled';
1028
- const openaiPlanLabel = openaiSub?.enabled
1029
- ? (OPENAI_PLAN_LABELS[openaiSub.plan] ?? openaiSub.plan ?? 'n/a')
1030
- : 'disabled';
1031
-
1032
- const subLines = [
1033
- `Claude: ${auth.claude.found ? 'logged in' : 'not logged in'} — ${claudePlanLabel}`,
1034
- claudeSub?.label ? ` label: ${claudeSub.label}` : '',
1035
- claudeSub?.expiresAt ? ` expires: ${claudeSub.expiresAt.slice(0, 10)}` : '',
1036
- '',
1037
- `OpenAI: ${auth.openai.found ? 'logged in' : 'not logged in'} — ${openaiPlanLabel}`,
1038
- openaiSub?.label ? ` label: ${openaiSub.label}` : '',
1039
- openaiSub?.expiresAt ? ` expires: ${openaiSub.expiresAt.slice(0, 10)}` : '',
1040
- ].filter(line => line !== '');
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`);
1023
+ }
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`);
1033
+ }
1041
1034
 
1035
+ console.log(box('Subscriptions', lines));
1042
1036
  console.log('');
1043
- console.log(box('Subscriptions', subLines));
1044
- console.log('');
1045
- console.log(menu([
1046
- { key: 'd', label: 'Re-detect from CLI', section: '' },
1047
- { key: 'c', label: 'Set Claude plan tier', section: '' },
1048
- { key: 'o', label: 'Set OpenAI plan tier', section: '' },
1049
- { key: 't', label: 'Set team label/expiry',section: '' },
1050
- { key: 'b', label: 'Back to settings', section: '' },
1051
- ]));
1037
+
1038
+ const menuOpts = [
1039
+ { key: '1', label: 'Add Claude sub', section: 'Link' },
1040
+ { key: '2', label: 'Add Codex sub', section: 'Link' },
1041
+ { key: 'b', label: 'Back to home', section: '' },
1042
+ ];
1043
+ console.log(menu(menuOpts));
1052
1044
  console.log('');
1053
1045
 
1054
1046
  const choice = (await ask(' Choice: ')).trim().toLowerCase();
1055
1047
 
1056
- if (choice === 'd') {
1057
- // Re-detect from CLI config files
1058
- if (plans.claude && claudeSub) {
1059
- profile.providers.claude.plan = plans.claude;
1060
- console.log(` Detected Claude: ${CLAUDE_PLAN_LABELS[plans.claude] ?? plans.claude}`);
1061
- }
1062
- if (plans.openai && openaiSub) {
1063
- profile.providers.openai.plan = plans.openai;
1064
- console.log(` Detected OpenAI: ${OPENAI_PLAN_LABELS[plans.openai] ?? plans.openai}`);
1065
- }
1066
- saveProfile(profile, { cwd });
1067
- return { next: 'subscriptions' };
1068
- }
1069
-
1070
- if (choice === 'c') {
1071
- console.log('');
1072
- console.log(' Claude plan:');
1073
- console.log(' (1) Pro ($20/mo)');
1074
- console.log(' (2) Max x5 ($100/mo)');
1075
- console.log(' (3) Max x20 ($200/mo)');
1076
- const c = (await ask(' > ')).trim();
1077
- const planMap = { '1': 'pro', '2': 'max5', '3': 'max20' };
1078
- if (planMap[c]) {
1048
+ if (choice === '1') {
1049
+ console.log('\n Linking Claude subscription...');
1050
+ console.log(' A browser window will open — paste the code below when prompted.\n');
1051
+ const { spawnSync } = await import('node:child_process');
1052
+ const r = spawnSync('claude', ['login'], { stdio: 'inherit', timeout: 60000 });
1053
+ if (r.status === 0) {
1054
+ console.log('\n ✅ Claude linked successfully!\n');
1055
+ const label = (await ask(" Label (e.g. \"Josh's $100 sub\", or Enter to skip): ")).trim();
1056
+ const expiry = await askExpiry(ask, 'Claude');
1057
+ const newPlans = detectPlans();
1058
+ const plan = newPlans.claude?.plan || 'pro';
1059
+ if (!profile.providers) profile.providers = {};
1079
1060
  if (!profile.providers.claude) profile.providers.claude = { enabled: true };
1080
- profile.providers.claude.plan = planMap[c];
1061
+ profile.providers.claude.plan = plan;
1081
1062
  profile.providers.claude.enabled = true;
1063
+ if (label) profile.providers.claude.label = label;
1064
+ if (expiry) profile.providers.claude.expiresAt = expiry;
1082
1065
  saveProfile(profile, { cwd });
1083
- console.log(` Claude plan set to: ${CLAUDE_PLAN_LABELS[planMap[c]]}`);
1066
+ console.log(' Saved\n');
1067
+ await ask(' Press Enter to continue...');
1068
+ } else {
1069
+ console.log('\n ❌ Claude login failed or was cancelled.\n');
1070
+ await ask(' Press Enter to continue...');
1084
1071
  }
1085
1072
  return { next: 'subscriptions' };
1086
1073
  }
1087
1074
 
1088
- if (choice === 'o') {
1089
- console.log('');
1090
- console.log(' OpenAI plan:');
1091
- console.log(' (1) Plus ($20/mo)');
1092
- console.log(' (2) Pro ($100/mo)');
1093
- console.log(' (3) Pro ($200/mo higher limits)');
1094
- const c = (await ask(' > ')).trim();
1095
- const planMap = { '1': 'plus', '2': 'pro', '3': 'pro200' };
1096
- if (planMap[c]) {
1075
+ if (choice === '2') {
1076
+ console.log('\n Linking Codex subscription...');
1077
+ console.log(' A browser window will open — paste the code below when prompted.\n');
1078
+ const { spawnSync } = await import('node:child_process');
1079
+ const r = spawnSync('codex', ['login'], { stdio: 'inherit', timeout: 60000 });
1080
+ if (r.status === 0) {
1081
+ console.log('\n Codex linked successfully!\n');
1082
+ const label = (await ask(' Label (e.g. "Team Codex Pro", or Enter to skip): ')).trim();
1083
+ const expiry = await askExpiry(ask, 'Codex');
1084
+ const newPlans = detectPlans();
1085
+ const plan = newPlans.openai?.plan || 'plus';
1086
+ if (!profile.providers) profile.providers = {};
1097
1087
  if (!profile.providers.openai) profile.providers.openai = { enabled: true };
1098
- profile.providers.openai.plan = planMap[c];
1088
+ profile.providers.openai.plan = plan;
1099
1089
  profile.providers.openai.enabled = true;
1090
+ if (label) profile.providers.openai.label = label;
1091
+ if (expiry) profile.providers.openai.expiresAt = expiry;
1100
1092
  saveProfile(profile, { cwd });
1101
- console.log(` OpenAI plan set to: ${OPENAI_PLAN_LABELS[planMap[c]]}`);
1102
- }
1103
- return { next: 'subscriptions' };
1104
- }
1105
-
1106
- if (choice === 't') {
1107
- // Team label/expiry for each provider
1108
- for (const provider of ['claude', 'openai']) {
1109
- const prov = profile.providers[provider];
1110
- if (!prov?.enabled) continue;
1111
- const provLabel = provider === 'claude' ? 'Claude' : 'OpenAI';
1112
- const currentLabel = prov.label || '';
1113
- const label = (await ask(` ${provLabel} label [${currentLabel || 'none'}]: `)).trim();
1114
- if (label === '-') { delete prov.label; }
1115
- else if (label) { prov.label = label; }
1116
- const expiry = await askExpiry(ask, provLabel);
1117
- if (expiry) { prov.expiresAt = expiry; }
1093
+ console.log(' Saved\n');
1094
+ await ask(' Press Enter to continue...');
1095
+ } else {
1096
+ console.log('\n ❌ Codex login failed or was cancelled.\n');
1097
+ await ask(' Press Enter to continue...');
1118
1098
  }
1119
- saveProfile(profile, { cwd });
1120
- console.log(' Team config saved.');
1121
1099
  return { next: 'subscriptions' };
1122
1100
  }
1123
1101
 
1124
- if (choice === 'b' || choice === 'back') { return { next: 'settings' }; }
1125
-
1126
- return { next: 'subscriptions' };
1102
+ return { next: 'main' };
1127
1103
  }
1128
1104
 
1129
1105
  // ─── Screen: dashboardScreen (kept for internal reference, unreachable) ───────
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dual-brain",
3
- "version": "7.1.8",
3
+ "version": "7.1.10",
4
4
  "description": "AI orchestration across Claude + OpenAI subscriptions — smart routing, budget awareness, and dual-brain collaboration",
5
5
  "type": "module",
6
6
  "bin": {
package/src/decide.mjs CHANGED
@@ -213,7 +213,7 @@ function getHealthScores(tier, cwd) {
213
213
  * @returns {boolean}
214
214
  */
215
215
  export function shouldDualBrain(detection, profile) {
216
- const { intent = '', risk = 'low', complexity = 'simple' } = detection;
216
+ const { intent = '', risk = 'low', complexity = 'simple', designImpact = false } = detection;
217
217
  const dualEnabled = profile?.dual_brain_enabled !== false;
218
218
  const hasBothProviders = !!(
219
219
  profile?.providers?.claude?.enabled &&
@@ -227,7 +227,7 @@ export function shouldDualBrain(detection, profile) {
227
227
  const archOrSecurity = ['architecture', 'security'].includes(intent);
228
228
  const complexHighRisk = complexity === 'complex' && risk === 'high';
229
229
 
230
- return criticalRisk || archOrSecurity || complexHighRisk;
230
+ return criticalRisk || archOrSecurity || complexHighRisk || designImpact;
231
231
  }
232
232
 
233
233
  // ─── Internal: select model for provider ─────────────────────────────────────
package/src/detect.mjs CHANGED
@@ -43,6 +43,13 @@ const RISK_KEYWORDS = [
43
43
  { level: 'low', regex: /\b(readme|docs?|comment|format|lint|changelog|typo|whitespace)\b/i },
44
44
  ];
45
45
 
46
+ const DESIGN_IMPACT_PATTERNS = [
47
+ /\bbin\/dual-brain\.mjs\b/,
48
+ /\bsrc\/(?:tui|profile|detect|decide|dispatch|session|health|index)\.mjs\b/,
49
+ /\bhooks\/(?:head-guard|enforce-tier|budget-balancer|dual-brain-think|dual-brain-review|wave-orchestrator)\.mjs\b/,
50
+ /\bVISION\.md\b/,
51
+ ];
52
+
46
53
  const LEVEL_ORDER = { critical: 3, high: 2, medium: 1, low: 0 };
47
54
 
48
55
  // ─── Helpers / Exported functions ─────────────────────────────────────────────
@@ -152,6 +159,7 @@ function detectTask(input) {
152
159
  const extractedPaths = extractPaths(prompt);
153
160
  const allPaths = [...files, ...extractedPaths];
154
161
  const { level: pathRiskLevel, riskyFiles } = classifyRisk(allPaths);
162
+ const designImpact = allPaths.some(p => DESIGN_IMPACT_PATTERNS.some(re => re.test(p)));
155
163
 
156
164
  // 3. Keyword risk from description
157
165
  let keywordRisk = 'low';
@@ -199,6 +207,7 @@ function detectTask(input) {
199
207
  tier,
200
208
  fileCount,
201
209
  riskyFiles,
210
+ designImpact,
202
211
  requiresWrite: requiresWrite(intent),
203
212
  explanation,
204
213
  };