dual-brain 4.5.1 → 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 +473 -12
- 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';
|
|
@@ -30,6 +31,7 @@ const force = flag('--force');
|
|
|
30
31
|
const dryRun = flag('--dry-run');
|
|
31
32
|
const jsonOut = flag('--json');
|
|
32
33
|
const yesFlag = flag('--yes') || flag('-y');
|
|
34
|
+
const noLaunch = flag('--no-launch');
|
|
33
35
|
const positional = argv.filter(a => !a.startsWith('-'));
|
|
34
36
|
const subcommand = positional[0] || null;
|
|
35
37
|
|
|
@@ -49,9 +51,12 @@ if (flag('--help') || flag('-h')) {
|
|
|
49
51
|
Usage: npx -y dual-brain [command] [options]
|
|
50
52
|
|
|
51
53
|
Setup:
|
|
52
|
-
(none)
|
|
54
|
+
(none) Start persistent chat (setup if needed)
|
|
53
55
|
init Alias for default install
|
|
54
|
-
|
|
56
|
+
start Start persistent chat
|
|
57
|
+
chat Alias for start
|
|
58
|
+
auth Check and repair provider authentication
|
|
59
|
+
setup Configure providers, hooks, and preferences
|
|
55
60
|
doctor Check system health and report issues
|
|
56
61
|
reset Clear all state files (keeps config/hooks)
|
|
57
62
|
repair Fix corrupt files, stale locks, re-register hooks
|
|
@@ -105,6 +110,7 @@ if (flag('--help') || flag('-h')) {
|
|
|
105
110
|
--force Overwrite all existing config
|
|
106
111
|
--dry-run Detect environment only
|
|
107
112
|
--json Output detection as JSON
|
|
113
|
+
--no-launch Install/setup only, do not open Claude
|
|
108
114
|
--help Show this help
|
|
109
115
|
|
|
110
116
|
Routing modes:
|
|
@@ -135,6 +141,7 @@ const SUBCOMMANDS = [
|
|
|
135
141
|
'review', 'think', 'health', 'report', 'gate',
|
|
136
142
|
'vibe', 'plan', 'cost', 'dispatch', 'memory',
|
|
137
143
|
'test', 'ledger', 'doctor', 'reset', 'repair',
|
|
144
|
+
'auth', 'start', 'chat',
|
|
138
145
|
'chain', 'chains',
|
|
139
146
|
'agent', 'agents',
|
|
140
147
|
'do', 'ship', 'test-run', 'diff', 'runs', 'resume',
|
|
@@ -173,6 +180,240 @@ function detectReplit() {
|
|
|
173
180
|
return { isReplit, hasReplitTools };
|
|
174
181
|
}
|
|
175
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
|
+
|
|
176
417
|
function detectClaude() {
|
|
177
418
|
const result = { installed: false, version: null, authed: false };
|
|
178
419
|
|
|
@@ -1490,6 +1731,228 @@ async function checkReplitTools() {
|
|
|
1490
1731
|
}
|
|
1491
1732
|
}
|
|
1492
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
|
+
|
|
1493
1956
|
// ─── Main ───────────────────────────────────────────────────────────────────
|
|
1494
1957
|
|
|
1495
1958
|
async function main() {
|
|
@@ -1502,9 +1965,13 @@ async function main() {
|
|
|
1502
1965
|
if (subcommand === 'mode') { cmdMode(); return; }
|
|
1503
1966
|
if (subcommand === 'budget') { cmdBudget(); return; }
|
|
1504
1967
|
if (subcommand === 'explain') { cmdExplain(); return; }
|
|
1505
|
-
if (subcommand === 'doctor') {
|
|
1968
|
+
if (subcommand === 'doctor') { await cmdDoctorWithAuth(); return; }
|
|
1506
1969
|
if (subcommand === 'reset') { cmdReset(); return; }
|
|
1507
1970
|
if (subcommand === 'repair') { cmdRepair(); return; }
|
|
1971
|
+
if (subcommand === 'auth') { await cmdAuth(); return; }
|
|
1972
|
+
if (subcommand === 'start' || subcommand === 'chat') {
|
|
1973
|
+
await ensureAndLaunch(); return;
|
|
1974
|
+
}
|
|
1508
1975
|
|
|
1509
1976
|
// agent <template> [flags] → agent-templates.mjs --run <template> [flags]
|
|
1510
1977
|
if (subcommand === 'agent') {
|
|
@@ -1779,16 +2246,10 @@ async function main() {
|
|
|
1779
2246
|
return;
|
|
1780
2247
|
}
|
|
1781
2248
|
|
|
1782
|
-
// ── Bare invocation (no subcommand):
|
|
2249
|
+
// ── Bare invocation (no subcommand): setup if needed, then launch ──
|
|
1783
2250
|
if (!subcommand) {
|
|
1784
|
-
|
|
1785
|
-
|
|
1786
|
-
|| existsSync(join(workspace, '.claude', 'settings.json'));
|
|
1787
|
-
if (alreadySetUp && !flag('--force')) {
|
|
1788
|
-
printQuickStart();
|
|
1789
|
-
return;
|
|
1790
|
-
}
|
|
1791
|
-
// Not set up yet — fall through to full install/setup wizard
|
|
2251
|
+
await ensureAndLaunch();
|
|
2252
|
+
return;
|
|
1792
2253
|
}
|
|
1793
2254
|
|
|
1794
2255
|
if (subcommand === 'setup') {
|
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": {
|