dual-brain 4.5.0 → 4.6.0
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/install.mjs +554 -152
- package/package.json +1 -1
package/install.mjs
CHANGED
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
* npx dual-brain --help
|
|
10
10
|
*/
|
|
11
11
|
import { appendFileSync, cpSync, existsSync, mkdirSync, readFileSync, readdirSync, renameSync, statSync, unlinkSync, writeFileSync } from 'fs';
|
|
12
|
+
import https from 'https';
|
|
12
13
|
import { dirname, join, resolve } from 'path';
|
|
13
14
|
import { fileURLToPath } from 'url';
|
|
14
15
|
import { spawnSync } from 'child_process';
|
|
@@ -29,9 +30,14 @@ const flag = (f) => argv.includes(f);
|
|
|
29
30
|
const force = flag('--force');
|
|
30
31
|
const dryRun = flag('--dry-run');
|
|
31
32
|
const jsonOut = flag('--json');
|
|
33
|
+
const yesFlag = flag('--yes') || flag('-y');
|
|
34
|
+
const noLaunch = flag('--no-launch');
|
|
32
35
|
const positional = argv.filter(a => !a.startsWith('-'));
|
|
33
36
|
const subcommand = positional[0] || null;
|
|
34
37
|
|
|
38
|
+
const BRAND = `Data Tools — Dual Brain v${VERSION}`;
|
|
39
|
+
const BRAND_SUBTITLE = 'Built on replit-tools by Steve Moraco';
|
|
40
|
+
|
|
35
41
|
if (flag('--version') || flag('-v')) {
|
|
36
42
|
console.log(`dual-brain v${VERSION}`);
|
|
37
43
|
process.exit(0);
|
|
@@ -39,13 +45,18 @@ if (flag('--version') || flag('-v')) {
|
|
|
39
45
|
|
|
40
46
|
if (flag('--help') || flag('-h')) {
|
|
41
47
|
console.log(`
|
|
42
|
-
|
|
48
|
+
${BRAND}
|
|
49
|
+
${BRAND_SUBTITLE}
|
|
43
50
|
|
|
44
51
|
Usage: npx -y dual-brain [command] [options]
|
|
45
52
|
|
|
46
53
|
Setup:
|
|
47
|
-
(none)
|
|
54
|
+
(none) Start persistent chat (setup if needed)
|
|
48
55
|
init Alias for default install
|
|
56
|
+
start Start persistent chat
|
|
57
|
+
chat Alias for start
|
|
58
|
+
auth Check and repair provider authentication
|
|
59
|
+
setup Configure providers, hooks, and preferences
|
|
49
60
|
doctor Check system health and report issues
|
|
50
61
|
reset Clear all state files (keeps config/hooks)
|
|
51
62
|
repair Fix corrupt files, stale locks, re-register hooks
|
|
@@ -99,6 +110,7 @@ if (flag('--help') || flag('-h')) {
|
|
|
99
110
|
--force Overwrite all existing config
|
|
100
111
|
--dry-run Detect environment only
|
|
101
112
|
--json Output detection as JSON
|
|
113
|
+
--no-launch Install/setup only, do not open Claude
|
|
102
114
|
--help Show this help
|
|
103
115
|
|
|
104
116
|
Routing modes:
|
|
@@ -108,7 +120,8 @@ if (flag('--help') || flag('-h')) {
|
|
|
108
120
|
Aggressive Maximizes both subscriptions, dual-brain for medium+
|
|
109
121
|
|
|
110
122
|
Examples:
|
|
111
|
-
${cmd('npx dual-brain')} #
|
|
123
|
+
${cmd('npx dual-brain')} # quick start
|
|
124
|
+
${cmd('npx dual-brain setup')} # configure providers and hooks
|
|
112
125
|
${cmd('npx dual-brain status')} # open control panel
|
|
113
126
|
${cmd('npx dual-brain mode cost-saver')} # switch profile
|
|
114
127
|
${cmd('npx dual-brain budget 8 25')} # \$8 session / \$25 daily
|
|
@@ -124,10 +137,11 @@ if (flag('--help') || flag('-h')) {
|
|
|
124
137
|
}
|
|
125
138
|
|
|
126
139
|
const SUBCOMMANDS = [
|
|
127
|
-
'init', 'status', 'mode', 'budget', 'explain',
|
|
140
|
+
'init', 'setup', 'status', 'mode', 'budget', 'explain',
|
|
128
141
|
'review', 'think', 'health', 'report', 'gate',
|
|
129
142
|
'vibe', 'plan', 'cost', 'dispatch', 'memory',
|
|
130
143
|
'test', 'ledger', 'doctor', 'reset', 'repair',
|
|
144
|
+
'auth', 'start', 'chat',
|
|
131
145
|
'chain', 'chains',
|
|
132
146
|
'agent', 'agents',
|
|
133
147
|
'do', 'ship', 'test-run', 'diff', 'runs', 'resume',
|
|
@@ -166,6 +180,240 @@ function detectReplit() {
|
|
|
166
180
|
return { isReplit, hasReplitTools };
|
|
167
181
|
}
|
|
168
182
|
|
|
183
|
+
// ─── Session Preferences + Auth Helpers ────────────────────────────────────
|
|
184
|
+
|
|
185
|
+
const SESSION_PREFS_REL = '.claude/dual-brain.session.json';
|
|
186
|
+
const DEFAULT_SESSION_PREFS = {
|
|
187
|
+
head_model: 'sonnet',
|
|
188
|
+
effort: 'medium',
|
|
189
|
+
};
|
|
190
|
+
|
|
191
|
+
function writeAtomicJson(targetPath, data) {
|
|
192
|
+
const tmpPath = `${targetPath}.tmp.${process.pid}`;
|
|
193
|
+
writeFileSync(tmpPath, JSON.stringify(data, null, 2) + '\n');
|
|
194
|
+
renameSync(tmpPath, targetPath);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
function loadSessionPrefs(workspace) {
|
|
198
|
+
const prefsPath = join(workspace, SESSION_PREFS_REL);
|
|
199
|
+
try {
|
|
200
|
+
const raw = JSON.parse(readFileSync(prefsPath, 'utf8'));
|
|
201
|
+
return { ...DEFAULT_SESSION_PREFS, ...raw };
|
|
202
|
+
} catch {
|
|
203
|
+
return { ...DEFAULT_SESSION_PREFS };
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
function saveSessionPrefs(workspace, prefs) {
|
|
208
|
+
const claudeDir = join(workspace, '.claude');
|
|
209
|
+
mkdirSync(claudeDir, { recursive: true });
|
|
210
|
+
const prefsPath = join(workspace, SESSION_PREFS_REL);
|
|
211
|
+
writeAtomicJson(prefsPath, { ...DEFAULT_SESSION_PREFS, ...prefs });
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
function getClaudeCredentialPaths(workspace = process.cwd()) {
|
|
215
|
+
const configDir = process.env.CLAUDE_CONFIG_DIR;
|
|
216
|
+
const home = process.env.HOME || '';
|
|
217
|
+
return [
|
|
218
|
+
configDir ? join(configDir, '.credentials.json') : null,
|
|
219
|
+
join(home, '.claude', '.credentials.json'),
|
|
220
|
+
resolve(workspace, '.replit-tools', '.claude-persistent', '.credentials.json'),
|
|
221
|
+
].filter(Boolean);
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
function safeParseJson(path) {
|
|
225
|
+
try {
|
|
226
|
+
return JSON.parse(readFileSync(path, 'utf8'));
|
|
227
|
+
} catch {
|
|
228
|
+
return null;
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
function decodeJwtPayload(token) {
|
|
233
|
+
if (!token || typeof token !== 'string') return null;
|
|
234
|
+
const parts = token.split('.');
|
|
235
|
+
if (parts.length < 2) return null;
|
|
236
|
+
try {
|
|
237
|
+
return JSON.parse(Buffer.from(parts[1], 'base64url').toString());
|
|
238
|
+
} catch {
|
|
239
|
+
return null;
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
function computeExpiresInHours(expiresAtMs) {
|
|
244
|
+
if (!Number.isFinite(expiresAtMs)) return null;
|
|
245
|
+
return Math.round(((expiresAtMs - Date.now()) / 3_600_000) * 10) / 10;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
function formatExpires(expiresInHours) {
|
|
249
|
+
if (expiresInHours == null) return 'unknown';
|
|
250
|
+
if (expiresInHours <= 0) return 'expired';
|
|
251
|
+
return `${Math.round(expiresInHours)}h remaining`;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
function getClaudeAuthStatus() {
|
|
255
|
+
const defaults = {
|
|
256
|
+
valid: false,
|
|
257
|
+
expiresInHours: null,
|
|
258
|
+
hasRefreshToken: false,
|
|
259
|
+
email: null,
|
|
260
|
+
credPath: null,
|
|
261
|
+
};
|
|
262
|
+
|
|
263
|
+
for (const credPath of getClaudeCredentialPaths(process.cwd())) {
|
|
264
|
+
const cred = safeParseJson(credPath);
|
|
265
|
+
const oauth = cred?.claudeAiOauth;
|
|
266
|
+
if (!oauth) continue;
|
|
267
|
+
|
|
268
|
+
const expiresAtMs = typeof oauth.expiresAt === 'number'
|
|
269
|
+
? oauth.expiresAt
|
|
270
|
+
: oauth.expiresAt
|
|
271
|
+
? Date.parse(oauth.expiresAt)
|
|
272
|
+
: NaN;
|
|
273
|
+
const expiresInHours = computeExpiresInHours(expiresAtMs);
|
|
274
|
+
const valid = Number.isFinite(expiresAtMs) ? expiresAtMs > Date.now() : false;
|
|
275
|
+
|
|
276
|
+
let subscription = null;
|
|
277
|
+
try {
|
|
278
|
+
const authOut = run('claude', ['auth', 'status', '--json']);
|
|
279
|
+
if (authOut.status === 0) {
|
|
280
|
+
const info = JSON.parse(authOut.stdout.trim());
|
|
281
|
+
subscription = info.subscriptionType || null;
|
|
282
|
+
}
|
|
283
|
+
} catch {}
|
|
284
|
+
if (!subscription) subscription = oauth.subscriptionType || null;
|
|
285
|
+
|
|
286
|
+
return {
|
|
287
|
+
valid,
|
|
288
|
+
expiresInHours,
|
|
289
|
+
hasRefreshToken: !!oauth.refreshToken,
|
|
290
|
+
email: oauth.email || oauth.account?.email || cred.email || null,
|
|
291
|
+
subscription,
|
|
292
|
+
credPath,
|
|
293
|
+
};
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
return defaults;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
function getCodexAuthStatus() {
|
|
300
|
+
const authPath = join(process.env.HOME || '', '.codex', 'auth.json');
|
|
301
|
+
const auth = safeParseJson(authPath);
|
|
302
|
+
const tokens = auth?.tokens || auth;
|
|
303
|
+
const accessToken = tokens?.access_token;
|
|
304
|
+
const refreshToken = tokens?.refresh_token;
|
|
305
|
+
const payload = decodeJwtPayload(accessToken);
|
|
306
|
+
const expiresAtMs = payload?.exp ? payload.exp * 1000 : NaN;
|
|
307
|
+
const expiresInHours = computeExpiresInHours(expiresAtMs);
|
|
308
|
+
|
|
309
|
+
return {
|
|
310
|
+
valid: Number.isFinite(expiresAtMs) ? expiresAtMs > Date.now() : false,
|
|
311
|
+
expiresInHours,
|
|
312
|
+
hasRefreshToken: !!refreshToken,
|
|
313
|
+
email: payload?.email || auth?.email || auth?.user?.email || null,
|
|
314
|
+
};
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
function postForm(url, body) {
|
|
318
|
+
return new Promise((resolve, reject) => {
|
|
319
|
+
const payload = Buffer.from(new URLSearchParams(body).toString(), 'utf8');
|
|
320
|
+
const req = https.request(url, {
|
|
321
|
+
method: 'POST',
|
|
322
|
+
headers: {
|
|
323
|
+
'Content-Type': 'application/x-www-form-urlencoded',
|
|
324
|
+
'Content-Length': payload.length,
|
|
325
|
+
},
|
|
326
|
+
timeout: 8000,
|
|
327
|
+
}, (res) => {
|
|
328
|
+
const chunks = [];
|
|
329
|
+
res.on('data', (chunk) => chunks.push(chunk));
|
|
330
|
+
res.on('end', () => {
|
|
331
|
+
const raw = Buffer.concat(chunks).toString('utf8');
|
|
332
|
+
if (res.statusCode && res.statusCode >= 200 && res.statusCode < 300) {
|
|
333
|
+
try {
|
|
334
|
+
resolve(JSON.parse(raw));
|
|
335
|
+
} catch (err) {
|
|
336
|
+
reject(err);
|
|
337
|
+
}
|
|
338
|
+
} else {
|
|
339
|
+
reject(new Error(`HTTP ${res.statusCode || 0}`));
|
|
340
|
+
}
|
|
341
|
+
});
|
|
342
|
+
});
|
|
343
|
+
req.on('timeout', () => req.destroy(new Error('Request timeout')));
|
|
344
|
+
req.on('error', reject);
|
|
345
|
+
req.write(payload);
|
|
346
|
+
req.end();
|
|
347
|
+
});
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
async function refreshClaudeToken(credPath) {
|
|
351
|
+
try {
|
|
352
|
+
const cred = safeParseJson(credPath);
|
|
353
|
+
const oauth = cred?.claudeAiOauth;
|
|
354
|
+
if (!oauth?.refreshToken) return { success: false, expiresInHours: null };
|
|
355
|
+
|
|
356
|
+
const refreshed = await postForm('https://console.anthropic.com/v1/oauth/token', {
|
|
357
|
+
grant_type: 'refresh_token',
|
|
358
|
+
refresh_token: oauth.refreshToken,
|
|
359
|
+
client_id: '9d1c250a-e61b-44d9-88ed-5944d1962f5e',
|
|
360
|
+
});
|
|
361
|
+
|
|
362
|
+
const nextOauth = {
|
|
363
|
+
...oauth,
|
|
364
|
+
accessToken: refreshed.access_token || oauth.accessToken,
|
|
365
|
+
refreshToken: refreshed.refresh_token || oauth.refreshToken,
|
|
366
|
+
tokenType: refreshed.token_type || oauth.tokenType,
|
|
367
|
+
scopes: refreshed.scope || oauth.scopes,
|
|
368
|
+
expiresAt: Date.now() + ((refreshed.expires_in || 0) * 1000),
|
|
369
|
+
};
|
|
370
|
+
const updated = { ...cred, claudeAiOauth: nextOauth };
|
|
371
|
+
writeAtomicJson(credPath, updated);
|
|
372
|
+
|
|
373
|
+
return {
|
|
374
|
+
success: true,
|
|
375
|
+
expiresInHours: computeExpiresInHours(nextOauth.expiresAt),
|
|
376
|
+
};
|
|
377
|
+
} catch {
|
|
378
|
+
return { success: false, expiresInHours: null };
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
async function refreshCodexToken() {
|
|
383
|
+
try {
|
|
384
|
+
const authPath = join(process.env.HOME || '', '.codex', 'auth.json');
|
|
385
|
+
const auth = safeParseJson(authPath);
|
|
386
|
+
const tokens = auth?.tokens || auth;
|
|
387
|
+
const refreshToken = tokens?.refresh_token;
|
|
388
|
+
if (!refreshToken) return { success: false, expiresInHours: null };
|
|
389
|
+
|
|
390
|
+
const refreshed = await postForm('https://auth.openai.com/oauth/token', {
|
|
391
|
+
grant_type: 'refresh_token',
|
|
392
|
+
refresh_token: refreshToken,
|
|
393
|
+
client_id: 'app_EMoamEEZ73f0CkXaXp7hrann',
|
|
394
|
+
});
|
|
395
|
+
|
|
396
|
+
const updatedTokens = {
|
|
397
|
+
...tokens,
|
|
398
|
+
access_token: refreshed.access_token || tokens.access_token,
|
|
399
|
+
refresh_token: refreshed.refresh_token || tokens.refresh_token,
|
|
400
|
+
id_token: refreshed.id_token || tokens.id_token,
|
|
401
|
+
token_type: refreshed.token_type || tokens.token_type,
|
|
402
|
+
scope: refreshed.scope || tokens.scope,
|
|
403
|
+
};
|
|
404
|
+
const updated = auth?.tokens ? { ...auth, tokens: updatedTokens } : updatedTokens;
|
|
405
|
+
writeAtomicJson(authPath, updated);
|
|
406
|
+
|
|
407
|
+
const payload = decodeJwtPayload(updatedTokens.access_token);
|
|
408
|
+
return {
|
|
409
|
+
success: true,
|
|
410
|
+
expiresInHours: payload?.exp ? computeExpiresInHours(payload.exp * 1000) : null,
|
|
411
|
+
};
|
|
412
|
+
} catch {
|
|
413
|
+
return { success: false, expiresInHours: null };
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
|
|
169
417
|
function detectClaude() {
|
|
170
418
|
const result = { installed: false, version: null, authed: false };
|
|
171
419
|
|
|
@@ -261,6 +509,51 @@ function detectEnvironment() {
|
|
|
261
509
|
};
|
|
262
510
|
}
|
|
263
511
|
|
|
512
|
+
function runtimeInstalled(workspace = process.cwd()) {
|
|
513
|
+
return existsSync(resolve(workspace, '.replit-tools'));
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
function providerReadiness(env) {
|
|
517
|
+
const issues = [];
|
|
518
|
+
if (!env.claude.installed) issues.push('Claude CLI not found.');
|
|
519
|
+
else if (!env.claude.authed) issues.push('Claude CLI not authenticated.');
|
|
520
|
+
|
|
521
|
+
if (!env.codex.installed) issues.push('Codex CLI not found.');
|
|
522
|
+
else if (!env.codex.authed) issues.push('Codex CLI not authenticated.');
|
|
523
|
+
|
|
524
|
+
return { ready: issues.length === 0, issues };
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
function readInputLine(prompt) {
|
|
528
|
+
process.stdout.write(prompt);
|
|
529
|
+
return new Promise((resolve) => {
|
|
530
|
+
process.stdin.setEncoding('utf8');
|
|
531
|
+
process.stdin.once('data', (chunk) => resolve(chunk.trim()));
|
|
532
|
+
process.stdin.resume();
|
|
533
|
+
}).finally(() => {
|
|
534
|
+
process.stdin.pause();
|
|
535
|
+
});
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
function isRunningFromNpxCache() {
|
|
539
|
+
const scriptPath = fileURLToPath(import.meta.url);
|
|
540
|
+
return scriptPath.includes('/.npm/_npx/') || scriptPath.includes('\\.npm\\_npx\\');
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
function warnIfUpdateAvailable() {
|
|
544
|
+
if (!isRunningFromNpxCache()) return;
|
|
545
|
+
const pkg = JSON.parse(readFileSync(join(__dirname, 'package.json'), 'utf8'));
|
|
546
|
+
const packageName = pkg.name || 'dual-brain';
|
|
547
|
+
const latest = run('npm', ['view', packageName, 'version']);
|
|
548
|
+
if (latest.status !== 0) return;
|
|
549
|
+
const newest = latest.stdout.trim().split('\n').pop();
|
|
550
|
+
if (newest && newest !== VERSION) {
|
|
551
|
+
console.log('');
|
|
552
|
+
console.log(` Update available: ${cmd('npx -y dual-brain@latest')}`);
|
|
553
|
+
console.log('');
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
|
|
264
557
|
// ─── Mode Resolution ────────────────────────────────────────────────────────
|
|
265
558
|
|
|
266
559
|
function resolveMode(env) {
|
|
@@ -444,131 +737,17 @@ function install(workspace, env, mode) {
|
|
|
444
737
|
return actions;
|
|
445
738
|
}
|
|
446
739
|
|
|
447
|
-
// ─── Guided Auth ────────────────────────────────────────────────────────────
|
|
448
|
-
|
|
449
|
-
function printGuidedAuth(env) {
|
|
450
|
-
const claudeOk = env.claude.installed && env.claude.authed;
|
|
451
|
-
const codexOk = env.codex.installed && env.codex.authed;
|
|
452
|
-
|
|
453
|
-
if (claudeOk && codexOk) return false; // nothing to guide
|
|
454
|
-
|
|
455
|
-
const claudeMissing = !env.claude.installed || !env.claude.authed;
|
|
456
|
-
const codexMissing = !env.codex.installed || !env.codex.authed;
|
|
457
|
-
|
|
458
|
-
console.log('');
|
|
459
|
-
|
|
460
|
-
if (claudeMissing && codexMissing) {
|
|
461
|
-
console.log(' ⚠️ No AI providers detected. You need at least one:');
|
|
462
|
-
console.log('');
|
|
463
|
-
console.log(' Claude (recommended):');
|
|
464
|
-
console.log(' npm install -g @anthropic-ai/claude-code && claude login');
|
|
465
|
-
console.log('');
|
|
466
|
-
console.log(' OpenAI (optional, enables dual-brain):');
|
|
467
|
-
console.log(' npm install -g @openai/codex && codex login');
|
|
468
|
-
console.log('');
|
|
469
|
-
console.log(' Then re-run: npx dual-brain');
|
|
470
|
-
console.log('');
|
|
471
|
-
return true;
|
|
472
|
-
}
|
|
473
|
-
|
|
474
|
-
if (claudeMissing) {
|
|
475
|
-
if (!env.claude.installed) {
|
|
476
|
-
console.log(' ⚠️ Claude CLI not detected. To enable Claude routing:');
|
|
477
|
-
} else {
|
|
478
|
-
console.log(' ⚠️ Claude CLI not authenticated. To enable Claude routing:');
|
|
479
|
-
}
|
|
480
|
-
console.log('');
|
|
481
|
-
console.log(' 1. Install: npm install -g @anthropic-ai/claude-code');
|
|
482
|
-
console.log(' 2. Login: claude login');
|
|
483
|
-
console.log('');
|
|
484
|
-
console.log(' Run these commands, then re-run: npx dual-brain');
|
|
485
|
-
console.log('');
|
|
486
|
-
}
|
|
487
|
-
|
|
488
|
-
if (codexMissing) {
|
|
489
|
-
if (!env.codex.installed) {
|
|
490
|
-
console.log(' ℹ️ Codex CLI not detected. To enable GPT routing:');
|
|
491
|
-
} else {
|
|
492
|
-
console.log(' ℹ️ Codex CLI not authenticated. To enable GPT routing:');
|
|
493
|
-
}
|
|
494
|
-
console.log('');
|
|
495
|
-
console.log(' 1. Install: npm install -g @openai/codex');
|
|
496
|
-
console.log(' 2. Login: codex login');
|
|
497
|
-
console.log('');
|
|
498
|
-
if (claudeMissing) {
|
|
499
|
-
console.log(' Run these commands, then re-run: npx dual-brain');
|
|
500
|
-
} else {
|
|
501
|
-
console.log(' Run these commands, then re-run: npx dual-brain');
|
|
502
|
-
console.log(' GPT features will be disabled until Codex is configured.');
|
|
503
|
-
}
|
|
504
|
-
console.log('');
|
|
505
|
-
}
|
|
506
|
-
|
|
507
|
-
return claudeMissing; // only block/poll if Claude (primary provider) is missing
|
|
508
|
-
}
|
|
509
|
-
|
|
510
|
-
async function waitForAuth(env) {
|
|
511
|
-
if (!process.stdin.isTTY || !process.stdout.isTTY) return;
|
|
512
|
-
|
|
513
|
-
const claudeMissing = !env.claude.installed || !env.claude.authed;
|
|
514
|
-
if (!claudeMissing) return; // only wait if primary provider needs setup
|
|
515
|
-
|
|
516
|
-
process.stdout.write(' Would you like me to wait while you authenticate? [Y/n] ');
|
|
517
|
-
|
|
518
|
-
const answer = await new Promise((resolve) => {
|
|
519
|
-
process.stdin.setEncoding('utf8');
|
|
520
|
-
process.stdin.once('data', (chunk) => resolve(chunk.trim().toLowerCase()));
|
|
521
|
-
process.stdin.resume();
|
|
522
|
-
});
|
|
523
|
-
|
|
524
|
-
if (answer === 'n' || answer === 'no') {
|
|
525
|
-
process.stdin.pause();
|
|
526
|
-
console.log('');
|
|
527
|
-
console.log(' Re-run `npx dual-brain` after authenticating.');
|
|
528
|
-
console.log('');
|
|
529
|
-
process.exit(0);
|
|
530
|
-
}
|
|
531
|
-
|
|
532
|
-
console.log('');
|
|
533
|
-
console.log(' Waiting for Claude CLI to become available (checking every 5s, max 5 min)...');
|
|
534
|
-
console.log('');
|
|
535
|
-
|
|
536
|
-
const maxAttempts = 60; // 60 × 5s = 5 minutes
|
|
537
|
-
for (let i = 0; i < maxAttempts; i++) {
|
|
538
|
-
await new Promise(r => setTimeout(r, 5000));
|
|
539
|
-
process.stdout.write('.');
|
|
540
|
-
const fresh = detectClaude();
|
|
541
|
-
if (fresh.installed) {
|
|
542
|
-
console.log('');
|
|
543
|
-
console.log('');
|
|
544
|
-
console.log(' Claude CLI detected! Continuing setup...');
|
|
545
|
-
console.log('');
|
|
546
|
-
process.stdin.pause();
|
|
547
|
-
// Update env in-place
|
|
548
|
-
env.claude = fresh;
|
|
549
|
-
return;
|
|
550
|
-
}
|
|
551
|
-
}
|
|
552
|
-
|
|
553
|
-
console.log('');
|
|
554
|
-
console.log('');
|
|
555
|
-
console.log(' Timed out after 5 minutes. Re-run `npx dual-brain` after authenticating.');
|
|
556
|
-
console.log('');
|
|
557
|
-
process.stdin.pause();
|
|
558
|
-
process.exit(1);
|
|
559
|
-
}
|
|
560
|
-
|
|
561
740
|
// ─── Quick Start Block ───────────────────────────────────────────────────────
|
|
562
741
|
|
|
563
742
|
function printQuickStart() {
|
|
564
|
-
console.log(' ✓ dual-brain installed successfully');
|
|
565
743
|
console.log('');
|
|
566
|
-
console.log(
|
|
567
|
-
console.log(` npx dual-brain do "fix a bug and write tests" ← full pipeline`);
|
|
568
|
-
console.log(' npx dual-brain status ← check system');
|
|
569
|
-
console.log(' npx dual-brain doctor ← diagnose issues');
|
|
744
|
+
console.log(` ${BRAND}`);
|
|
570
745
|
console.log('');
|
|
571
|
-
console.log('
|
|
746
|
+
console.log(' Quick start:');
|
|
747
|
+
console.log(' npx dual-brain do "fix the login bug" ← one command to PR');
|
|
748
|
+
console.log(' npx dual-brain setup ← configure providers');
|
|
749
|
+
console.log(' npx dual-brain doctor ← check system health');
|
|
750
|
+
console.log(' npx dual-brain help ← full command list');
|
|
572
751
|
console.log('');
|
|
573
752
|
}
|
|
574
753
|
|
|
@@ -578,16 +757,10 @@ function printReport(env, mode, actions, isDryRun) {
|
|
|
578
757
|
const lines = [];
|
|
579
758
|
|
|
580
759
|
lines.push(br('╔', '╗'));
|
|
581
|
-
lines.push(ln(
|
|
760
|
+
lines.push(ln(BRAND));
|
|
761
|
+
lines.push(ln(BRAND_SUBTITLE));
|
|
582
762
|
lines.push(sep());
|
|
583
|
-
|
|
584
|
-
const cAuth = env.claude.authed ? '✅' : env.claude.installed ? '⚠️' : '❌';
|
|
585
|
-
const xAuth = env.codex.authed ? '✅' : env.codex.installed ? '⚠️' : '❌';
|
|
586
|
-
lines.push(ln(` 🟠 Claude ${cAuth} 🟢 Codex ${xAuth}`));
|
|
587
|
-
|
|
588
|
-
if (env.isReplit) {
|
|
589
|
-
lines.push(ln(` 🌀 Replit${env.hasReplitTools ? ' + replit-tools' : ''}`));
|
|
590
|
-
}
|
|
763
|
+
lines.push(ln(`Workspace: ${env.workspace}`));
|
|
591
764
|
|
|
592
765
|
if (actions) {
|
|
593
766
|
lines.push(sep());
|
|
@@ -1558,6 +1731,228 @@ async function checkReplitTools() {
|
|
|
1558
1731
|
}
|
|
1559
1732
|
}
|
|
1560
1733
|
|
|
1734
|
+
// ─── Auth Command ──────────────────────────────────────────────────────────
|
|
1735
|
+
|
|
1736
|
+
async function cmdAuth() {
|
|
1737
|
+
console.log('');
|
|
1738
|
+
console.log(` ${BRAND} — Auth Status`);
|
|
1739
|
+
console.log(' ──────────────────────────────────────────────────');
|
|
1740
|
+
|
|
1741
|
+
const claude = getClaudeAuthStatus();
|
|
1742
|
+
if (claude.valid) {
|
|
1743
|
+
const email = claude.email ? ` — ${claude.email}` : '';
|
|
1744
|
+
const sub = claude.subscription ? ` (${claude.subscription})` : '';
|
|
1745
|
+
console.log(` ✓ Claude: authenticated (${Math.round(claude.expiresInHours)}h remaining)${email}${sub}`);
|
|
1746
|
+
} else if (claude.hasRefreshToken) {
|
|
1747
|
+
console.log(' ⟳ Claude: expired — attempting refresh...');
|
|
1748
|
+
const r = await refreshClaudeToken(claude.credPath);
|
|
1749
|
+
console.log(r.success ? ` ✓ Claude: refreshed (${Math.round(r.expiresInHours)}h remaining)` : ' ✗ Claude: refresh failed — run: claude login');
|
|
1750
|
+
} else {
|
|
1751
|
+
console.log(` ✗ Claude: ${claude.credPath ? 'expired' : 'not configured'} — run: claude login`);
|
|
1752
|
+
}
|
|
1753
|
+
|
|
1754
|
+
const codex = getCodexAuthStatus();
|
|
1755
|
+
if (codex.valid) {
|
|
1756
|
+
const email = codex.email ? ` — ${codex.email}` : '';
|
|
1757
|
+
console.log(` ✓ Codex: authenticated (${Math.round(codex.expiresInHours)}h remaining)${email}`);
|
|
1758
|
+
} else if (codex.hasRefreshToken) {
|
|
1759
|
+
console.log(' ⟳ Codex: expired — attempting refresh...');
|
|
1760
|
+
const r = await refreshCodexToken();
|
|
1761
|
+
console.log(r.success ? ` ✓ Codex: refreshed (${Math.round(r.expiresInHours)}h remaining)` : ' ✗ Codex: refresh failed — run: codex login');
|
|
1762
|
+
} else {
|
|
1763
|
+
console.log(' ✗ Codex: not configured — run: codex login');
|
|
1764
|
+
console.log(' (GPT lane will be disabled)');
|
|
1765
|
+
}
|
|
1766
|
+
|
|
1767
|
+
if (IS_REPLIT) {
|
|
1768
|
+
console.log('');
|
|
1769
|
+
console.log(' Replit tip: If login shows localhost errors, use browser-based auth');
|
|
1770
|
+
console.log(' or run the login command from a local machine and copy credentials.');
|
|
1771
|
+
}
|
|
1772
|
+
console.log('');
|
|
1773
|
+
}
|
|
1774
|
+
|
|
1775
|
+
// ─── Preflight Auth ────────────────────────────────────────────────────────
|
|
1776
|
+
|
|
1777
|
+
async function preflightAuth() {
|
|
1778
|
+
const claude = getClaudeAuthStatus();
|
|
1779
|
+
const codex = getCodexAuthStatus();
|
|
1780
|
+
let claudeOk = claude.valid;
|
|
1781
|
+
let codexOk = codex.valid;
|
|
1782
|
+
|
|
1783
|
+
if (!claudeOk && claude.hasRefreshToken) {
|
|
1784
|
+
const r = await refreshClaudeToken(claude.credPath);
|
|
1785
|
+
claudeOk = r.success;
|
|
1786
|
+
if (r.success) console.log(` ⟳ Claude auth refreshed (${Math.round(r.expiresInHours)}h)`);
|
|
1787
|
+
}
|
|
1788
|
+
if (!codexOk && codex.hasRefreshToken) {
|
|
1789
|
+
const r = await refreshCodexToken();
|
|
1790
|
+
codexOk = r.success;
|
|
1791
|
+
if (r.success) console.log(` ⟳ Codex auth refreshed (${Math.round(r.expiresInHours)}h)`);
|
|
1792
|
+
}
|
|
1793
|
+
|
|
1794
|
+
if (!claudeOk) {
|
|
1795
|
+
console.log('');
|
|
1796
|
+
console.log(' ✗ Claude auth required. Run: claude login');
|
|
1797
|
+
if (IS_REPLIT) console.log(' (Use browser flow — localhost callbacks may not work)');
|
|
1798
|
+
console.log('');
|
|
1799
|
+
return false;
|
|
1800
|
+
}
|
|
1801
|
+
if (!codexOk) console.log(' ⚠ Codex not authenticated — GPT lane disabled (Claude-only mode)');
|
|
1802
|
+
return true;
|
|
1803
|
+
}
|
|
1804
|
+
|
|
1805
|
+
// ─── Session Wizard ────────────────────────────────────────────────────────
|
|
1806
|
+
|
|
1807
|
+
function recommendModel() {
|
|
1808
|
+
const claude = getClaudeAuthStatus();
|
|
1809
|
+
const codex = getCodexAuthStatus();
|
|
1810
|
+
if (claude.subscription === 'max' && codex.valid) return { model: 'sonnet', reason: 'Both $100 subs — sonnet for chat, opus auto-escalates for thinking' };
|
|
1811
|
+
if (claude.subscription === 'max') return { model: 'sonnet', reason: '$100 Claude Max — sonnet balances speed + quality' };
|
|
1812
|
+
if (claude.subscription === 'pro') return { model: 'haiku', reason: '$20 Claude Pro — haiku conserves limited credits' };
|
|
1813
|
+
return { model: 'sonnet', reason: 'Default — good balance of speed and capability' };
|
|
1814
|
+
}
|
|
1815
|
+
|
|
1816
|
+
async function askLine(prompt) {
|
|
1817
|
+
if (!process.stdin.isTTY || yesFlag || process.env.CI) return '';
|
|
1818
|
+
process.stdout.write(prompt);
|
|
1819
|
+
const answer = await new Promise(resolve => {
|
|
1820
|
+
process.stdin.setEncoding('utf8');
|
|
1821
|
+
process.stdin.once('data', chunk => resolve(chunk.trim().toLowerCase()));
|
|
1822
|
+
process.stdin.resume();
|
|
1823
|
+
});
|
|
1824
|
+
process.stdin.pause();
|
|
1825
|
+
return answer;
|
|
1826
|
+
}
|
|
1827
|
+
|
|
1828
|
+
async function runSessionWizard(workspace) {
|
|
1829
|
+
const rec = recommendModel();
|
|
1830
|
+
const claude = getClaudeAuthStatus();
|
|
1831
|
+
const codex = getCodexAuthStatus();
|
|
1832
|
+
|
|
1833
|
+
console.log('');
|
|
1834
|
+
console.log(` ${BRAND}`);
|
|
1835
|
+
console.log(` ${BRAND_SUBTITLE}`);
|
|
1836
|
+
console.log('');
|
|
1837
|
+
|
|
1838
|
+
const parts = [];
|
|
1839
|
+
if (claude.subscription) parts.push(`Claude ${claude.subscription}`);
|
|
1840
|
+
if (codex.valid) parts.push('Codex Pro');
|
|
1841
|
+
if (parts.length) {
|
|
1842
|
+
console.log(` Detected: ${parts.join(' + ')}`);
|
|
1843
|
+
console.log(` Recommendation: ${rec.reason}`);
|
|
1844
|
+
console.log('');
|
|
1845
|
+
}
|
|
1846
|
+
|
|
1847
|
+
console.log(' Head session model:');
|
|
1848
|
+
console.log(' [s] Sonnet — fast, cost-effective, great for most work');
|
|
1849
|
+
console.log(' [o] Opus — slower, smarter, best for complex architecture');
|
|
1850
|
+
console.log(' [h] Haiku — fastest, cheapest, good for simple tasks');
|
|
1851
|
+
const modelKey = await askLine(` Choice [${rec.model === 'haiku' ? 'h' : 's'}]: `);
|
|
1852
|
+
let model = rec.model === 'haiku' ? 'haiku' : 'sonnet';
|
|
1853
|
+
if (modelKey === 'o' || modelKey === 'opus') model = 'opus';
|
|
1854
|
+
else if (modelKey === 'h' || modelKey === 'haiku') model = 'haiku';
|
|
1855
|
+
else if (modelKey === 's' || modelKey === 'sonnet') model = 'sonnet';
|
|
1856
|
+
|
|
1857
|
+
if (model === 'opus' && process.stdin.isTTY) {
|
|
1858
|
+
console.log(' ⚠ Opus uses ~5x more credits than Sonnet.');
|
|
1859
|
+
const confirm = await askLine(' Continue with Opus? [y/N] ');
|
|
1860
|
+
if (confirm !== 'y' && confirm !== 'yes') { model = 'sonnet'; console.log(' Switched to Sonnet.'); }
|
|
1861
|
+
}
|
|
1862
|
+
|
|
1863
|
+
console.log('');
|
|
1864
|
+
console.log(' Effort level:');
|
|
1865
|
+
console.log(' [l] Low — faster responses, less thorough');
|
|
1866
|
+
console.log(' [m] Medium — balanced (default)');
|
|
1867
|
+
console.log(' [h] High — thorough, slower, more expensive');
|
|
1868
|
+
const effortKey = await askLine(' Choice [m]: ');
|
|
1869
|
+
let effort = 'medium';
|
|
1870
|
+
if (effortKey === 'l' || effortKey === 'low') effort = 'low';
|
|
1871
|
+
else if (effortKey === 'h' || effortKey === 'high') effort = 'high';
|
|
1872
|
+
|
|
1873
|
+
const prefs = { head_model: model, effort, created_at: new Date().toISOString(), updated_at: new Date().toISOString() };
|
|
1874
|
+
saveSessionPrefs(workspace, prefs);
|
|
1875
|
+
console.log('');
|
|
1876
|
+
console.log(` Saved: ${model} / ${effort} effort`);
|
|
1877
|
+
console.log(' Change anytime: npx dual-brain start --model opus --effort high');
|
|
1878
|
+
return prefs;
|
|
1879
|
+
}
|
|
1880
|
+
|
|
1881
|
+
// ─── Session Launcher ──────────────────────────────────────────────────────
|
|
1882
|
+
|
|
1883
|
+
function launchClaudeSession(workspace, prefs) {
|
|
1884
|
+
const model = prefs.head_model || 'sonnet';
|
|
1885
|
+
const args = [];
|
|
1886
|
+
const helpOut = run('claude', ['--help']);
|
|
1887
|
+
const helpText = (helpOut.stdout || '') + (helpOut.stderr || '');
|
|
1888
|
+
if (helpText.includes('--model')) args.push('--model', model);
|
|
1889
|
+
if (helpText.includes('--resume') || helpText.includes('-c')) args.push('-c');
|
|
1890
|
+
|
|
1891
|
+
console.log('');
|
|
1892
|
+
console.log(` Launching ${model} session...`);
|
|
1893
|
+
console.log(' (Work agents auto-route: haiku/sonnet/opus by task tier)');
|
|
1894
|
+
console.log('');
|
|
1895
|
+
|
|
1896
|
+
const result = spawnSync('claude', args, {
|
|
1897
|
+
stdio: 'inherit',
|
|
1898
|
+
cwd: workspace,
|
|
1899
|
+
env: { ...process.env, DUAL_BRAIN_HEAD_MODEL: model, DUAL_BRAIN_HEAD_EFFORT: prefs.effort || 'medium', DUAL_BRAIN_WORKSPACE: workspace },
|
|
1900
|
+
});
|
|
1901
|
+
process.exit(result.status ?? 0);
|
|
1902
|
+
}
|
|
1903
|
+
|
|
1904
|
+
// ─── Ensure Setup + Launch ─────────────────────────────────────────────────
|
|
1905
|
+
|
|
1906
|
+
async function ensureAndLaunch() {
|
|
1907
|
+
const workspace = resolve(process.cwd());
|
|
1908
|
+
if (!dryRun && !jsonOut) await checkReplitTools();
|
|
1909
|
+
|
|
1910
|
+
const alreadySetUp = existsSync(join(workspace, '.claude', 'hooks'))
|
|
1911
|
+
|| existsSync(join(workspace, '.claude', 'settings.json'));
|
|
1912
|
+
if (!alreadySetUp || force) {
|
|
1913
|
+
const env = detectEnvironment();
|
|
1914
|
+
const mode = resolveMode(env);
|
|
1915
|
+
install(env.workspace, env, mode);
|
|
1916
|
+
}
|
|
1917
|
+
|
|
1918
|
+
const authOk = await preflightAuth();
|
|
1919
|
+
if (!authOk) { console.log(' Fix auth first, then run: npx dual-brain'); process.exit(1); }
|
|
1920
|
+
|
|
1921
|
+
const cliModel = argv.find((a, i) => argv[i - 1] === '--model');
|
|
1922
|
+
const cliEffort = argv.find((a, i) => argv[i - 1] === '--effort');
|
|
1923
|
+
let prefs = loadSessionPrefs(workspace);
|
|
1924
|
+
|
|
1925
|
+
if (!prefs.created_at && !noLaunch) prefs = await runSessionWizard(workspace);
|
|
1926
|
+
if (cliModel) prefs.head_model = cliModel;
|
|
1927
|
+
if (cliEffort) prefs.effort = cliEffort;
|
|
1928
|
+
|
|
1929
|
+
if (noLaunch) { printQuickStart(); return; }
|
|
1930
|
+
launchClaudeSession(workspace, prefs);
|
|
1931
|
+
}
|
|
1932
|
+
|
|
1933
|
+
// ─── Doctor with Auth ──────────────────────────────────────────────────────
|
|
1934
|
+
|
|
1935
|
+
async function cmdDoctorWithAuth() {
|
|
1936
|
+
cmdDoctor();
|
|
1937
|
+
console.log(' Auth Status:');
|
|
1938
|
+
const claude = getClaudeAuthStatus();
|
|
1939
|
+
if (claude.valid) {
|
|
1940
|
+
const email = claude.email ? ` — ${claude.email}` : '';
|
|
1941
|
+
const sub = claude.subscription ? ` (${claude.subscription})` : '';
|
|
1942
|
+
console.log(` ✓ Claude: ${Math.round(claude.expiresInHours)}h remaining${email}${sub}`);
|
|
1943
|
+
} else {
|
|
1944
|
+
console.log(' ✗ Claude: not authenticated — run: npx dual-brain auth');
|
|
1945
|
+
}
|
|
1946
|
+
const codex = getCodexAuthStatus();
|
|
1947
|
+
if (codex.valid) {
|
|
1948
|
+
const email = codex.email ? ` — ${codex.email}` : '';
|
|
1949
|
+
console.log(` ✓ Codex: ${Math.round(codex.expiresInHours)}h remaining${email}`);
|
|
1950
|
+
} else {
|
|
1951
|
+
console.log(' ✗ Codex: not authenticated — run: npx dual-brain auth');
|
|
1952
|
+
}
|
|
1953
|
+
console.log('');
|
|
1954
|
+
}
|
|
1955
|
+
|
|
1561
1956
|
// ─── Main ───────────────────────────────────────────────────────────────────
|
|
1562
1957
|
|
|
1563
1958
|
async function main() {
|
|
@@ -1570,9 +1965,13 @@ async function main() {
|
|
|
1570
1965
|
if (subcommand === 'mode') { cmdMode(); return; }
|
|
1571
1966
|
if (subcommand === 'budget') { cmdBudget(); return; }
|
|
1572
1967
|
if (subcommand === 'explain') { cmdExplain(); return; }
|
|
1573
|
-
if (subcommand === 'doctor') {
|
|
1968
|
+
if (subcommand === 'doctor') { await cmdDoctorWithAuth(); return; }
|
|
1574
1969
|
if (subcommand === 'reset') { cmdReset(); return; }
|
|
1575
1970
|
if (subcommand === 'repair') { cmdRepair(); return; }
|
|
1971
|
+
if (subcommand === 'auth') { await cmdAuth(); return; }
|
|
1972
|
+
if (subcommand === 'start' || subcommand === 'chat') {
|
|
1973
|
+
await ensureAndLaunch(); return;
|
|
1974
|
+
}
|
|
1576
1975
|
|
|
1577
1976
|
// agent <template> [flags] → agent-templates.mjs --run <template> [flags]
|
|
1578
1977
|
if (subcommand === 'agent') {
|
|
@@ -1847,6 +2246,27 @@ async function main() {
|
|
|
1847
2246
|
return;
|
|
1848
2247
|
}
|
|
1849
2248
|
|
|
2249
|
+
// ── Bare invocation (no subcommand): setup if needed, then launch ──
|
|
2250
|
+
if (!subcommand) {
|
|
2251
|
+
await ensureAndLaunch();
|
|
2252
|
+
return;
|
|
2253
|
+
}
|
|
2254
|
+
|
|
2255
|
+
if (subcommand === 'setup') {
|
|
2256
|
+
if (!dryRun && !jsonOut) await checkReplitTools();
|
|
2257
|
+
const env = detectEnvironment();
|
|
2258
|
+
const mode = resolveMode(env);
|
|
2259
|
+
const needsGuidance = printGuidedAuth(env);
|
|
2260
|
+
if (needsGuidance && process.stdin.isTTY && process.stdout.isTTY && !process.env.CI) {
|
|
2261
|
+
await waitForAuth(env);
|
|
2262
|
+
Object.assign(mode, resolveMode(env));
|
|
2263
|
+
}
|
|
2264
|
+
const actions = install(env.workspace, env, mode);
|
|
2265
|
+
printReport(env, mode, actions);
|
|
2266
|
+
printQuickStart();
|
|
2267
|
+
return;
|
|
2268
|
+
}
|
|
2269
|
+
|
|
1850
2270
|
// ── replit-tools check — first thing before auth detection or install ──
|
|
1851
2271
|
if (!dryRun && !jsonOut) {
|
|
1852
2272
|
await checkReplitTools();
|
|
@@ -1869,30 +2289,12 @@ async function main() {
|
|
|
1869
2289
|
const needsGuidance = printGuidedAuth(env);
|
|
1870
2290
|
if (needsGuidance && process.stdin.isTTY && process.stdout.isTTY && !process.env.CI) {
|
|
1871
2291
|
await waitForAuth(env);
|
|
1872
|
-
// Re-resolve mode after potential auth
|
|
1873
2292
|
Object.assign(mode, resolveMode(env));
|
|
1874
2293
|
}
|
|
1875
2294
|
|
|
1876
2295
|
const actions = install(env.workspace, env, mode);
|
|
1877
2296
|
printReport(env, mode, actions);
|
|
1878
|
-
|
|
1879
|
-
// Always print quick-start block after successful install
|
|
1880
2297
|
printQuickStart();
|
|
1881
|
-
|
|
1882
|
-
// Offer to launch control panel (opt-in, interactive TTY only)
|
|
1883
|
-
if (process.stdin.isTTY && process.stdout.isTTY && !process.env.CI) {
|
|
1884
|
-
process.stdout.write(' Launch control panel? [y/N] ');
|
|
1885
|
-
const answer = await new Promise((resolve) => {
|
|
1886
|
-
process.stdin.setEncoding('utf8');
|
|
1887
|
-
process.stdin.once('data', (chunk) => resolve(chunk.trim().toLowerCase()));
|
|
1888
|
-
process.stdin.resume();
|
|
1889
|
-
});
|
|
1890
|
-
process.stdin.pause();
|
|
1891
|
-
console.log('');
|
|
1892
|
-
if (answer === 'y' || answer === 'yes') {
|
|
1893
|
-
launchPanel();
|
|
1894
|
-
}
|
|
1895
|
-
}
|
|
1896
2298
|
}
|
|
1897
2299
|
|
|
1898
2300
|
main();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dual-brain",
|
|
3
|
-
"version": "4.
|
|
3
|
+
"version": "4.6.0",
|
|
4
4
|
"description": "Data-Tools Dual-Brain — dual-provider orchestration extension for data-tools/replit-tools. Tiered routing, budget balancing, and GPT dual-brain review across Claude + OpenAI subscriptions",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|