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.
- package/bin/dual-brain.mjs +100 -8
- package/package.json +1 -1
- package/src/profile.mjs +113 -0
package/bin/dual-brain.mjs
CHANGED
|
@@ -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
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
const
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
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
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
|
};
|