dual-brain 7.0.0 → 7.0.1

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.
@@ -10,6 +10,7 @@ import {
10
10
  ensureProfile, loadProfile, saveProfile, runOnboarding,
11
11
  rememberPreference, forgetPreference, getActivePreferences,
12
12
  getAvailableProviders, isSoloBrain, getHeadModel,
13
+ detectAuth, detectEnvironment,
13
14
  } from '../src/profile.mjs';
14
15
 
15
16
  import { detectTask } from '../src/detect.mjs';
@@ -45,6 +46,7 @@ dual-brain <command> [options]
45
46
 
46
47
  Commands:
47
48
  init First-time setup (providers, plans, optimization)
49
+ auth Show authentication status for all providers
48
50
  install Install Claude Code hooks into the current project
49
51
  go "task description" Detect → decide → dispatch a task
50
52
  --dry-run Show routing decision without executing
@@ -64,6 +66,44 @@ Options:
64
66
  `.trim());
65
67
  }
66
68
 
69
+ // ─── Auth helpers ─────────────────────────────────────────────────────────────
70
+
71
+ /**
72
+ * Print a compact auth status table to stdout.
73
+ * @param {{ claude: object, openai: object }} auth Result from detectAuth()
74
+ */
75
+ function printAuthTable(auth) {
76
+ const W = 55; // inner width (wide enough for source labels)
77
+ const bar = '═'.repeat(W);
78
+ const pad = (s) => {
79
+ const visible = s.replace(/[̀-ͯ]/g, ''); // strip combining chars for length
80
+ return s + ' '.repeat(Math.max(0, W - visible.length));
81
+ };
82
+
83
+ const claudeLine1 = auth.claude.found
84
+ ? ` Claude: ✓ found via ${auth.claude.source}`
85
+ : ` Claude: ✗ not found`;
86
+ const claudeLine2 = auth.claude.found
87
+ ? ` ${auth.claude.masked}`
88
+ : ` run: claude auth login`;
89
+
90
+ const openaiLine1 = auth.openai.found
91
+ ? ` OpenAI: ✓ found via ${auth.openai.source}`
92
+ : ` OpenAI: ✗ not found`;
93
+ const openaiLine2 = auth.openai.found
94
+ ? ` ${auth.openai.masked}`
95
+ : ` run: codex auth OR export OPENAI_API_KEY=sk-...`;
96
+
97
+ console.log(`╔${bar}╗`);
98
+ console.log(`║${pad(' Auth Status')}║`);
99
+ console.log(`╠${bar}╣`);
100
+ console.log(`║${pad(claudeLine1)}║`);
101
+ console.log(`║${pad(claudeLine2)}║`);
102
+ console.log(`║${pad(openaiLine1)}║`);
103
+ console.log(`║${pad(openaiLine2)}║`);
104
+ console.log(`╚${bar}╝`);
105
+ }
106
+
67
107
  // ─── Card command (default) ──────────────────────────────────────────────────
68
108
 
69
109
  async function cmdCard() {
@@ -83,19 +123,70 @@ async function cmdCard() {
83
123
  const health = getHealth(cwd);
84
124
  const card = formatSessionCard(session, repo, health);
85
125
  console.log(card);
126
+
127
+ // Auth status warnings (non-blocking)
128
+ const auth = await detectAuth();
129
+ const warnings = [];
130
+ if (!auth.claude.found) warnings.push('Claude auth not found — run: claude auth login');
131
+ if (!auth.openai.found) warnings.push('OpenAI auth not found — run: codex auth OR export OPENAI_API_KEY=sk-...');
132
+ if (warnings.length > 0) {
133
+ console.log('\nAuth warnings:');
134
+ for (const w of warnings) console.log(` ⚠ ${w}`);
135
+ }
136
+
137
+ // Environment info
138
+ const env = detectEnvironment();
139
+ if (env.isReplit || env.hasReplitTools) {
140
+ const envLabel = env.hasReplitTools ? 'Replit + replit-tools' : 'Replit';
141
+ console.log(`\nRuntime: ${envLabel}`);
142
+ }
86
143
  }
87
144
 
88
145
  // ─── Commands ─────────────────────────────────────────────────────────────────
89
146
 
90
147
  async function cmdInit() {
91
- const profile = await runOnboarding({ interactive: true });
92
- saveProfile(profile, { cwd: process.cwd() });
93
- const rt = await detectRuntime();
94
- const providers = getAvailableProviders(profile);
95
- const providerSummary = providers.length
96
- ? providers.map(p => `${p.name === 'claude' ? 'Claude' : 'OpenAI'} (${p.plan})`).join(', ')
97
- : 'none';
98
- console.log(`Profile saved. Providers: ${providerSummary}. Mode: ${profile.mode}. Runtime: ${rt.runtime}`);
148
+ const cwd = process.cwd();
149
+
150
+ // --- Step 1: Auth preflight ---
151
+ const auth = await detectAuth();
152
+ printAuthTable(auth);
153
+
154
+ const noneFound = !auth.claude.found && !auth.openai.found;
155
+ if (noneFound) {
156
+ console.log('\nNo AI provider credentials found. Set up at least one before continuing:\n');
157
+ console.log(' Claude : claude auth login');
158
+ console.log(' OpenAI : codex auth OR export OPENAI_API_KEY=sk-...\n');
159
+ console.log('Re-run "dual-brain init" after authenticating.');
160
+ return;
161
+ }
162
+
163
+ // --- Step 2: Run onboarding wizard, skipping tiers for auto-detected plans ---
164
+ const profile = await runOnboarding({ interactive: true, detectedAuth: auth });
165
+ saveProfile(profile, { cwd });
166
+
167
+ // --- Step 3: Show dashboard + next step ---
168
+ console.log('');
169
+ const repo = loadRepoCache(cwd);
170
+ const session = loadSession(cwd);
171
+ const health = getHealth(cwd);
172
+ const card = formatSessionCard(session, repo, health);
173
+ console.log(card);
174
+ console.log('\nReady! Try: dual-brain go "your task here"\n');
175
+ }
176
+
177
+ async function cmdAuth() {
178
+ const auth = await detectAuth();
179
+ printAuthTable(auth);
180
+
181
+ // If anything is missing, print setup commands
182
+ if (!auth.claude.found) {
183
+ console.log('\nTo set up Claude:');
184
+ console.log(' claude auth login');
185
+ }
186
+ if (!auth.openai.found) {
187
+ console.log('\nTo set up OpenAI:');
188
+ console.log(' codex auth OR export OPENAI_API_KEY=sk-...');
189
+ }
99
190
  }
100
191
 
101
192
  async function cmdGo(args) {
@@ -410,6 +501,7 @@ async function main() {
410
501
 
411
502
  if (cmd === 'init') { await cmdInit(); return; }
412
503
  if (cmd === 'install') { await cmdInstall(); return; }
504
+ if (cmd === 'auth') { await cmdAuth(); return; }
413
505
  if (cmd === 'go') { await cmdGo(args.slice(1)); return; }
414
506
  if (cmd === 'status') { await cmdStatus(args.slice(1)); return; }
415
507
  if (cmd === 'hot') { cmdHot(args[1]); return; }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dual-brain",
3
- "version": "7.0.0",
3
+ "version": "7.0.1",
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/profile.mjs CHANGED
@@ -106,6 +106,118 @@ function syncPreferencesToMemory(profile, cwd) {
106
106
  }
107
107
  }
108
108
 
109
+ // ---------------------------------------------------------------------------
110
+ // Environment detection
111
+ // ---------------------------------------------------------------------------
112
+
113
+ /**
114
+ * Detect the runtime environment.
115
+ * Returns { isReplit, hasReplitTools, isCI }.
116
+ */
117
+ function detectEnvironment() {
118
+ const isReplit = !!(process.env.REPL_ID || process.env.REPLIT_DB_URL);
119
+ const hasReplitTools = existsSync(join(process.cwd(), '.replit-tools'));
120
+ const isCI = !!(process.env.CI || process.env.GITHUB_ACTIONS);
121
+ return { isReplit, hasReplitTools, isCI };
122
+ }
123
+
124
+ // ---------------------------------------------------------------------------
125
+ // Auth detection
126
+ // ---------------------------------------------------------------------------
127
+
128
+ /**
129
+ * Mask a credential string: show first 4 + "..." + last 4 chars.
130
+ * For short strings (< 8 chars), just returns "***".
131
+ * @param {string} str
132
+ * @returns {string}
133
+ */
134
+ function _maskCredential(str) {
135
+ if (!str || str.length < 8) return '***';
136
+ return str.slice(0, 4) + '...' + str.slice(-4);
137
+ }
138
+
139
+ /**
140
+ * Detect authentication credentials from all known sources.
141
+ * Checks in priority order: config files first, then env vars.
142
+ * Never makes network calls — validation is always null in v1.
143
+ *
144
+ * @returns {{ claude: AuthEntry, openai: AuthEntry }}
145
+ * @typedef {{ found: boolean, source: string|null, masked: string|null, validated: null }} AuthEntry
146
+ */
147
+ async function detectAuth() {
148
+ const results = {
149
+ claude: { found: false, source: null, masked: null, validated: null },
150
+ openai: { found: false, source: null, masked: null, validated: null },
151
+ };
152
+
153
+ // --- Claude: check .claude.json for oauthAccount or apiKey ---
154
+ const claudePaths = [
155
+ '/home/runner/workspace/.replit-tools/.claude-persistent/.claude.json',
156
+ join(homedir(), '.claude', '.claude.json'),
157
+ ];
158
+ for (const p of claudePaths) {
159
+ try {
160
+ const data = JSON.parse(readFileSync(p, 'utf8'));
161
+ if (data?.oauthAccount) {
162
+ // OAuth session found
163
+ results.claude.found = true;
164
+ results.claude.source = p.includes('.replit-tools') ? '.claude.json (replit-tools)' : '.claude.json';
165
+ results.claude.masked = 'oauth:configured';
166
+ break;
167
+ }
168
+ if (data?.apiKey && typeof data.apiKey === 'string') {
169
+ results.claude.found = true;
170
+ results.claude.source = p.includes('.replit-tools') ? '.claude.json (replit-tools)' : '.claude.json';
171
+ results.claude.masked = _maskCredential(data.apiKey);
172
+ break;
173
+ }
174
+ } catch { continue; }
175
+ }
176
+
177
+ // --- Claude: fallback to ANTHROPIC_API_KEY env var ---
178
+ if (!results.claude.found && process.env.ANTHROPIC_API_KEY) {
179
+ results.claude.found = true;
180
+ results.claude.source = 'env:ANTHROPIC_API_KEY';
181
+ results.claude.masked = _maskCredential(process.env.ANTHROPIC_API_KEY);
182
+ }
183
+
184
+ // --- OpenAI/Codex: check auth.json for access_token or id_token ---
185
+ const codexPaths = [
186
+ '/home/runner/workspace/.replit-tools/.codex-persistent/auth.json',
187
+ join(homedir(), '.codex', 'auth.json'),
188
+ ];
189
+ for (const p of codexPaths) {
190
+ try {
191
+ const data = JSON.parse(readFileSync(p, 'utf8'));
192
+ const accessToken = data?.tokens?.access_token || data?.access_token;
193
+ const idToken = data?.tokens?.id_token || data?.id_token;
194
+ const apiKey = data?.apiKey ?? data?.api_key ?? null;
195
+
196
+ if (accessToken || idToken) {
197
+ results.openai.found = true;
198
+ results.openai.source = p.includes('.replit-tools') ? 'codex auth.json (replit-tools)' : 'codex auth.json';
199
+ results.openai.masked = 'oauth:configured';
200
+ break;
201
+ }
202
+ if (apiKey && typeof apiKey === 'string') {
203
+ results.openai.found = true;
204
+ results.openai.source = p.includes('.replit-tools') ? 'codex auth.json (replit-tools)' : 'codex auth.json';
205
+ results.openai.masked = _maskCredential(apiKey);
206
+ break;
207
+ }
208
+ } catch { continue; }
209
+ }
210
+
211
+ // --- OpenAI: fallback to OPENAI_API_KEY env var ---
212
+ if (!results.openai.found && process.env.OPENAI_API_KEY) {
213
+ results.openai.found = true;
214
+ results.openai.source = 'env:OPENAI_API_KEY';
215
+ results.openai.masked = _maskCredential(process.env.OPENAI_API_KEY);
216
+ }
217
+
218
+ return results;
219
+ }
220
+
109
221
  // ---------------------------------------------------------------------------
110
222
  // Auto-detect subscription plans from provider config files
111
223
  // ---------------------------------------------------------------------------
@@ -469,4 +581,5 @@ export {
469
581
  rememberPreference, forgetPreference, getActivePreferences,
470
582
  getAvailableProviders, isSoloBrain, getHeadModel,
471
583
  detectPlans, syncPreferencesToMemory,
584
+ detectAuth, detectEnvironment,
472
585
  };