dual-brain 3.4.0 → 3.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/hooks/control-panel.mjs +168 -128
- package/hooks/profiles.mjs +3 -3
- package/install.mjs +22 -18
- package/package.json +1 -1
package/hooks/control-panel.mjs
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
/**
|
|
3
|
-
* control-panel.mjs — Session
|
|
3
|
+
* control-panel.mjs — Session launcher for Dual-Brain.
|
|
4
4
|
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
5
|
+
* Progressive disclosure: first-run shows minimal menu (new/shell + auth).
|
|
6
|
+
* Returning users see recent sessions, profile mode, cost alert settings.
|
|
7
|
+
* Loops until user exits to shell.
|
|
7
8
|
*/
|
|
8
9
|
|
|
9
10
|
import readline from 'readline';
|
|
@@ -14,6 +15,7 @@ import { spawnSync } from 'child_process';
|
|
|
14
15
|
|
|
15
16
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
16
17
|
const PROFILE_FILE = join(__dirname, '..', 'dual-brain.profile.json');
|
|
18
|
+
const LAUNCHED_MARKER = join(__dirname, '..', '.launched');
|
|
17
19
|
const VERSION = (() => {
|
|
18
20
|
try { return JSON.parse(readFileSync(join(__dirname, '..', 'package.json'), 'utf8')).version; } catch {}
|
|
19
21
|
return '?';
|
|
@@ -32,16 +34,15 @@ const dim = s => e('2', s);
|
|
|
32
34
|
const cyan = s => e('36', s);
|
|
33
35
|
const green = s => e('32', s);
|
|
34
36
|
const yellow = s => e('33', s);
|
|
35
|
-
const magenta = s => e('95', s);
|
|
36
37
|
const orange = s => e('1;38;5;208', s);
|
|
37
38
|
const blue = s => e('1;38;5;33', s);
|
|
38
39
|
|
|
39
40
|
// ─── Profiles ──────────────────────────────────────────────────────────────
|
|
40
41
|
|
|
41
42
|
const PROFILES = {
|
|
42
|
-
balanced: { emoji: '⚖️',
|
|
43
|
-
'cost-saver': { emoji: '
|
|
44
|
-
'quality-first': { emoji: '
|
|
43
|
+
balanced: { emoji: '⚖️', uiLabel: 'Default', desc: 'Auto-routes by complexity, uses both providers evenly' },
|
|
44
|
+
'cost-saver': { emoji: '🛡️', uiLabel: 'Conservative', desc: 'Fewer GPT dispatches, sticks to Claude for most work' },
|
|
45
|
+
'quality-first': { emoji: '🚀', uiLabel: 'Aggressive', desc: 'Maximizes both subscriptions, dual-brain for medium+ risk' },
|
|
45
46
|
};
|
|
46
47
|
|
|
47
48
|
const PROFILE_BUDGETS = {
|
|
@@ -55,9 +56,9 @@ function loadProfile() {
|
|
|
55
56
|
const data = JSON.parse(readFileSync(PROFILE_FILE, 'utf8'));
|
|
56
57
|
const name = data.active && PROFILES[data.active] ? data.active : 'balanced';
|
|
57
58
|
const custom = data.custom_overrides || {};
|
|
58
|
-
return { name, budgets: { ...PROFILE_BUDGETS[name], ...custom.budgets } };
|
|
59
|
+
return { name, budgets: { ...PROFILE_BUDGETS[name], ...custom.budgets }, hasCustomBudget: !!custom.budgets };
|
|
59
60
|
} catch {
|
|
60
|
-
return { name: 'balanced', budgets: PROFILE_BUDGETS.balanced };
|
|
61
|
+
return { name: 'balanced', budgets: PROFILE_BUDGETS.balanced, hasCustomBudget: false };
|
|
61
62
|
}
|
|
62
63
|
}
|
|
63
64
|
|
|
@@ -69,6 +70,25 @@ function saveProfile(name, customOverrides) {
|
|
|
69
70
|
renameSync(tmp, PROFILE_FILE);
|
|
70
71
|
}
|
|
71
72
|
|
|
73
|
+
// ─── First-Run Detection ──────────────────────────────────────────────────
|
|
74
|
+
|
|
75
|
+
function isFirstRun() {
|
|
76
|
+
if (existsSync(LAUNCHED_MARKER)) return false;
|
|
77
|
+
// Also check Claude history for any session in this workspace
|
|
78
|
+
const historyFile = join(HOME, '.claude', 'history.jsonl');
|
|
79
|
+
if (existsSync(historyFile)) {
|
|
80
|
+
try {
|
|
81
|
+
const content = readFileSync(historyFile, 'utf8');
|
|
82
|
+
if (content.includes('"sessionId"')) return false;
|
|
83
|
+
} catch {}
|
|
84
|
+
}
|
|
85
|
+
return true;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function markLaunched() {
|
|
89
|
+
try { writeFileSync(LAUNCHED_MARKER, new Date().toISOString() + '\n'); } catch {}
|
|
90
|
+
}
|
|
91
|
+
|
|
72
92
|
// ─── Provider Detection ───────────────────────────────────────────────────
|
|
73
93
|
|
|
74
94
|
function detectProviders() {
|
|
@@ -122,17 +142,13 @@ function getRecentSessions() {
|
|
|
122
142
|
return true;
|
|
123
143
|
};
|
|
124
144
|
|
|
125
|
-
// Claude sessions
|
|
126
145
|
const historyFile = join(HOME, '.claude', 'history.jsonl');
|
|
127
146
|
if (existsSync(historyFile)) {
|
|
128
147
|
try {
|
|
129
148
|
const lines = readFileSync(historyFile, 'utf8').trim().split('\n');
|
|
130
149
|
const entries = [];
|
|
131
150
|
for (const line of lines) {
|
|
132
|
-
try {
|
|
133
|
-
const j = JSON.parse(line);
|
|
134
|
-
if (j.sessionId && j.timestamp) entries.push(j);
|
|
135
|
-
} catch {}
|
|
151
|
+
try { const j = JSON.parse(line); if (j.sessionId && j.timestamp) entries.push(j); } catch {}
|
|
136
152
|
}
|
|
137
153
|
entries.sort((a, b) => a.timestamp - b.timestamp);
|
|
138
154
|
for (const j of entries) {
|
|
@@ -151,7 +167,6 @@ function getRecentSessions() {
|
|
|
151
167
|
} catch {}
|
|
152
168
|
}
|
|
153
169
|
|
|
154
|
-
// Codex sessions
|
|
155
170
|
const codexDir = join(HOME, '.codex', 'sessions');
|
|
156
171
|
if (existsSync(codexDir)) {
|
|
157
172
|
const walk = (dir) => {
|
|
@@ -230,68 +245,125 @@ function countRunning() {
|
|
|
230
245
|
return { claude, codex };
|
|
231
246
|
}
|
|
232
247
|
|
|
233
|
-
// ───
|
|
248
|
+
// ─── Provider Balance ─────────────────────────────────────────────────────
|
|
249
|
+
|
|
250
|
+
function loadProviderBalance() {
|
|
251
|
+
const today = new Date().toISOString().slice(0, 10);
|
|
252
|
+
const logFile = join(__dirname, `usage-${today}.jsonl`);
|
|
253
|
+
if (!existsSync(logFile)) return { claude: 0, openai: 0, total: 0, label: 'No activity yet' };
|
|
254
|
+
|
|
255
|
+
let claude = 0, openai = 0;
|
|
256
|
+
try {
|
|
257
|
+
const lines = readFileSync(logFile, 'utf8').split('\n').filter(Boolean);
|
|
258
|
+
for (const line of lines) {
|
|
259
|
+
try {
|
|
260
|
+
const e = JSON.parse(line);
|
|
261
|
+
if (e.provider === 'claude') claude++;
|
|
262
|
+
else if (e.provider === 'openai') openai++;
|
|
263
|
+
} catch {}
|
|
264
|
+
}
|
|
265
|
+
} catch {}
|
|
266
|
+
|
|
267
|
+
const total = claude + openai;
|
|
268
|
+
if (total === 0) return { claude: 0, openai: 0, total: 0, label: 'No activity yet' };
|
|
234
269
|
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
270
|
+
const claudePct = Math.round((claude / total) * 100);
|
|
271
|
+
const openaiPct = 100 - claudePct;
|
|
272
|
+
|
|
273
|
+
let label;
|
|
274
|
+
if (openaiPct === 0) label = 'Claude only — GPT subscription unused';
|
|
275
|
+
else if (claudePct === 0) label = 'GPT only — Claude subscription unused';
|
|
276
|
+
else if (Math.abs(claudePct - openaiPct) <= 20) label = 'Well balanced';
|
|
277
|
+
else if (claudePct > openaiPct) label = `Claude-heavy — GPT has capacity`;
|
|
278
|
+
else label = `GPT-heavy — Claude has capacity`;
|
|
279
|
+
|
|
280
|
+
return { claude: claudePct, openai: openaiPct, total, label };
|
|
238
281
|
}
|
|
239
282
|
|
|
240
|
-
|
|
283
|
+
function balanceBar(claudePct, openaiPct, width = 20) {
|
|
284
|
+
if (claudePct === 0 && openaiPct === 0) return dim('░'.repeat(width) + ' no activity');
|
|
285
|
+
const cFill = Math.round((claudePct / 100) * width);
|
|
286
|
+
const oFill = width - cFill;
|
|
287
|
+
const cBar = noColor ? '█'.repeat(cFill) : `\x1b[38;5;208m${'█'.repeat(cFill)}\x1b[0m`;
|
|
288
|
+
const oBar = noColor ? '▓'.repeat(oFill) : `\x1b[32m${'▓'.repeat(oFill)}\x1b[0m`;
|
|
289
|
+
return `${cBar}${oBar} ${orange(claudePct + '%')} Claude · ${green(openaiPct + '%')} GPT`;
|
|
290
|
+
}
|
|
241
291
|
|
|
242
|
-
|
|
243
|
-
const providers = detectProviders();
|
|
244
|
-
const profile = loadProfile();
|
|
245
|
-
const sessions = getRecentSessions();
|
|
246
|
-
const running = countRunning();
|
|
247
|
-
const pf = PROFILES[profile.name];
|
|
248
|
-
const hasReplitTools = checkReplitTools();
|
|
292
|
+
// ─── Menu Renderers ───────────────────────────────────────────────────────
|
|
249
293
|
|
|
294
|
+
function renderFirstRunMenu(providers) {
|
|
250
295
|
const lines = [];
|
|
251
296
|
|
|
252
297
|
lines.push('');
|
|
253
298
|
lines.push(` 🧠 ${bold(`Dual-Brain v${VERSION}`)}`);
|
|
254
299
|
lines.push('');
|
|
255
300
|
|
|
256
|
-
//
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
301
|
+
// Provider status
|
|
302
|
+
const cStat = providers.claude.authed ? '✅' : providers.claude.installed ? '⚠️' : '❌';
|
|
303
|
+
const xStat = providers.codex.authed ? '✅' : providers.codex.installed ? '⚠️' : '❌';
|
|
304
|
+
lines.push(` 🟠 Claude ${cStat} 🟢 Codex ${xStat}`);
|
|
305
|
+
|
|
306
|
+
if (providers.claude.authed && providers.codex.authed) {
|
|
307
|
+
lines.push(` ${green('Both providers ready — full dual-brain mode')}`);
|
|
308
|
+
} else if (providers.claude.authed) {
|
|
309
|
+
lines.push(` ${dim('Claude ready. Add Codex for dual-brain features.')}`);
|
|
310
|
+
} else if (!providers.claude.installed) {
|
|
311
|
+
lines.push(` ${yellow('Claude not found — needed to start.')}`);
|
|
261
312
|
} else {
|
|
262
|
-
lines.push(`
|
|
263
|
-
lines.push(` │ ${cyan('npx dual-brain')} = this menu │`);
|
|
313
|
+
lines.push(` ${yellow('Claude needs login to start.')}`);
|
|
264
314
|
}
|
|
265
|
-
lines.push(` │ ${cyan('j')} = login to Claude │`);
|
|
266
|
-
lines.push(` │ ${cyan('k')} = login to Codex │`);
|
|
267
|
-
lines.push(' ├─────────────────────────────┤');
|
|
268
|
-
lines.push(` │ ${orange('In Claude session:')} │`);
|
|
269
|
-
lines.push(` │ ${green('Ctrl+C x2')} = back to menu │`);
|
|
270
|
-
lines.push(` │ ${green('Ctrl+C x3')} = exit to shell │`);
|
|
271
|
-
lines.push(' └─────────────────────────────┘');
|
|
272
|
-
lines.push('');
|
|
273
315
|
|
|
274
|
-
|
|
275
|
-
const cStat = providers.claude.authed ? '✅' : providers.claude.installed ? '⚠️' : '❌';
|
|
276
|
-
const xStat = providers.codex.authed ? '✅' : providers.codex.installed ? '⚠️' : '❌';
|
|
277
|
-
lines.push(` 🟠 Claude ${cStat} 🟢 Codex ${xStat} ${pf.emoji} ${bold(pf.label)} ${dim('$' + profile.budgets.session_limit_usd + '/session')}`);
|
|
316
|
+
lines.push('');
|
|
278
317
|
|
|
279
|
-
//
|
|
318
|
+
// Auth actions if needed
|
|
280
319
|
if (!providers.claude.authed || !providers.codex.authed) {
|
|
320
|
+
if (!providers.claude.installed) {
|
|
321
|
+
lines.push(` ${dim('Install Claude:')} ${cyan('curl -fsSL https://claude.ai/install.sh | sh')}`);
|
|
322
|
+
}
|
|
323
|
+
if (!providers.claude.authed && providers.claude.installed) {
|
|
324
|
+
lines.push(` ${bold('[j]')} Sign in to Claude`);
|
|
325
|
+
}
|
|
326
|
+
if (!providers.codex.installed) {
|
|
327
|
+
lines.push(` ${dim('Install Codex:')} ${cyan('npm i -g @openai/codex')}`);
|
|
328
|
+
} else if (!providers.codex.authed) {
|
|
329
|
+
lines.push(` ${bold('[k]')} Sign in to Codex ${dim('(optional — enables GPT collaboration)')}`);
|
|
330
|
+
}
|
|
281
331
|
lines.push('');
|
|
282
|
-
if (!providers.claude.installed) lines.push(` ${dim('└')} Install Claude: ${cyan('curl -fsSL https://claude.ai/install.sh | sh')}`);
|
|
283
|
-
else if (!providers.claude.authed) lines.push(` ${dim('└')} Auth Claude: press ${bold('j')} below`);
|
|
284
|
-
if (!providers.codex.installed) lines.push(` ${dim('└')} Install Codex: ${cyan('npm i -g @openai/codex')}`);
|
|
285
|
-
else if (!providers.codex.authed) lines.push(` ${dim('└')} Auth Codex: press ${bold('k')} below`);
|
|
286
332
|
}
|
|
287
333
|
|
|
288
334
|
// Replit-tools check
|
|
289
|
-
if (IS_REPLIT && !
|
|
290
|
-
lines.push('');
|
|
291
|
-
lines.push(` ⚠️ ${yellow('replit-tools not found')} — recommended for Replit environments`);
|
|
292
|
-
lines.push(` ${dim('└')} Press ${bold('t')} to install replit-tools`);
|
|
335
|
+
if (IS_REPLIT && !existsSync(join(CWD, '.replit-tools'))) {
|
|
336
|
+
lines.push(` ${bold('[t]')} Install replit-tools ${dim('(recommended for Replit)')}`);
|
|
293
337
|
}
|
|
294
338
|
|
|
339
|
+
// Primary actions
|
|
340
|
+
lines.push(` ${bold('[n]')} Start new session`);
|
|
341
|
+
lines.push(` ${bold('[s]')} Skip — just shell`);
|
|
342
|
+
lines.push('');
|
|
343
|
+
|
|
344
|
+
return lines;
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
function renderReturningMenu(providers, sessions) {
|
|
348
|
+
const profile = loadProfile();
|
|
349
|
+
const pf = PROFILES[profile.name];
|
|
350
|
+
const running = countRunning();
|
|
351
|
+
const balance = loadProviderBalance();
|
|
352
|
+
const lines = [];
|
|
353
|
+
|
|
354
|
+
lines.push('');
|
|
355
|
+
lines.push(` 🧠 ${bold(`Dual-Brain v${VERSION}`)}`);
|
|
356
|
+
lines.push('');
|
|
357
|
+
|
|
358
|
+
// Provider status
|
|
359
|
+
const cStat = providers.claude.authed ? '✅' : '⚠️';
|
|
360
|
+
const xStat = providers.codex.authed ? '✅' : providers.codex.installed ? '⚠️' : '❌';
|
|
361
|
+
lines.push(` 🟠 Claude ${cStat} 🟢 Codex ${xStat} ${pf.emoji} ${bold(pf.uiLabel)}`);
|
|
362
|
+
|
|
363
|
+
// Provider balance bar
|
|
364
|
+
lines.push(` ${balanceBar(balance.claude, balance.openai)}`);
|
|
365
|
+
if (balance.total > 0) lines.push(` ${dim(balance.label + ' · ' + balance.total + ' calls today')}`);
|
|
366
|
+
|
|
295
367
|
// Recent sessions
|
|
296
368
|
if (sessions.length > 0) {
|
|
297
369
|
lines.push('');
|
|
@@ -305,31 +377,28 @@ function renderMenu() {
|
|
|
305
377
|
}
|
|
306
378
|
}
|
|
307
379
|
|
|
308
|
-
// Session manager box
|
|
309
380
|
lines.push('');
|
|
310
|
-
lines.push(' ┌─────────────────────────────┐');
|
|
311
|
-
lines.push(' │ 🧠 Dual-Brain Session Mgr │');
|
|
312
|
-
lines.push(' └─────────────────────────────┘');
|
|
313
381
|
|
|
314
382
|
const runParts = [];
|
|
315
383
|
if (running.claude > 0) runParts.push(`${running.claude} claude`);
|
|
316
384
|
if (running.codex > 0) runParts.push(`${running.codex} codex`);
|
|
317
385
|
if (runParts.length > 0) lines.push(` ${dim('(' + runParts.join(', ') + ' running)')}`);
|
|
318
|
-
lines.push('');
|
|
319
386
|
|
|
320
387
|
// Menu options
|
|
321
388
|
lines.push(` ${bold('[c]')} Continue last session`);
|
|
322
389
|
if (sessions.length > 0) lines.push(` ${bold('[1-9]')} Resume numbered above`);
|
|
323
390
|
lines.push(` ${bold('[n]')} New session`);
|
|
324
|
-
lines.push(` ${bold('[p]')}
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
lines.push(` ${bold('[
|
|
328
|
-
if (
|
|
329
|
-
lines.push(` ${bold('[
|
|
391
|
+
lines.push(` ${bold('[p]')} Mode: ${dim(pf.uiLabel)}`);
|
|
392
|
+
|
|
393
|
+
// Auth if needed
|
|
394
|
+
if (!providers.claude.authed) lines.push(` ${bold('[j]')} Sign in to Claude`);
|
|
395
|
+
if (providers.codex.installed && !providers.codex.authed) lines.push(` ${bold('[k]')} Sign in to Codex`);
|
|
396
|
+
if (IS_REPLIT && !existsSync(join(CWD, '.replit-tools'))) lines.push(` ${bold('[t]')} Install replit-tools`);
|
|
397
|
+
|
|
398
|
+
lines.push(` ${bold('[s]')} Shell`);
|
|
330
399
|
lines.push('');
|
|
331
400
|
|
|
332
|
-
return
|
|
401
|
+
return lines;
|
|
333
402
|
}
|
|
334
403
|
|
|
335
404
|
// ─── Profile Picker ───────────────────────────────────────────────────────
|
|
@@ -337,12 +406,16 @@ function renderMenu() {
|
|
|
337
406
|
function showProfilePicker(rl) {
|
|
338
407
|
return new Promise((resolve) => {
|
|
339
408
|
const current = loadProfile();
|
|
409
|
+
const balance = loadProviderBalance();
|
|
340
410
|
console.log('');
|
|
341
|
-
console.log(` ${bold('
|
|
411
|
+
console.log(` ${bold('Switch routing mode:')}`);
|
|
412
|
+
if (balance.total > 0) {
|
|
413
|
+
console.log(` ${dim('Current balance: Claude ' + balance.claude + '% / GPT ' + balance.openai + '% · ' + balance.label)}`);
|
|
414
|
+
}
|
|
342
415
|
console.log('');
|
|
343
416
|
for (const [i, [name, pf]] of Object.entries(PROFILES).entries()) {
|
|
344
417
|
const active = name === current.name ? ' ✅' : '';
|
|
345
|
-
console.log(` ${bold('[' + (i + 1) + ']')} ${pf.emoji} ${
|
|
418
|
+
console.log(` ${bold('[' + (i + 1) + ']')} ${pf.emoji} ${pf.uiLabel.padEnd(15)} ${dim(pf.desc)}${active}`);
|
|
346
419
|
}
|
|
347
420
|
console.log(` ${bold('[q]')} Cancel`);
|
|
348
421
|
console.log('');
|
|
@@ -358,63 +431,26 @@ function showProfilePicker(rl) {
|
|
|
358
431
|
} catch {}
|
|
359
432
|
saveProfile(names[idx], customOverrides);
|
|
360
433
|
const pf = PROFILES[names[idx]];
|
|
361
|
-
console.log(` ✅ Switched to ${pf.emoji} ${pf.
|
|
434
|
+
console.log(` ✅ Switched to ${pf.emoji} ${pf.uiLabel}`);
|
|
362
435
|
}
|
|
363
436
|
resolve();
|
|
364
437
|
});
|
|
365
438
|
});
|
|
366
439
|
}
|
|
367
440
|
|
|
368
|
-
//
|
|
369
|
-
|
|
370
|
-
function showBudgetEditor(rl) {
|
|
371
|
-
return new Promise((resolve) => {
|
|
372
|
-
const profile = loadProfile();
|
|
373
|
-
console.log('');
|
|
374
|
-
console.log(` ${bold('💵 Edit Budget')}`);
|
|
375
|
-
console.log(` ${dim('Current: $' + profile.budgets.session_limit_usd + ' session / $' + profile.budgets.daily_limit_usd + ' daily')}`);
|
|
376
|
-
console.log('');
|
|
377
|
-
|
|
378
|
-
rl.question(' Session limit ($): ', (sessionStr) => {
|
|
379
|
-
const session = parseFloat(sessionStr);
|
|
380
|
-
if (isNaN(session) || session <= 0) {
|
|
381
|
-
console.log(' Cancelled.');
|
|
382
|
-
return resolve();
|
|
383
|
-
}
|
|
384
|
-
rl.question(' Daily limit ($, Enter = auto): ', (dailyStr) => {
|
|
385
|
-
const daily = parseFloat(dailyStr);
|
|
386
|
-
const finalDaily = (isNaN(daily) || daily <= 0) ? session * 3 : daily;
|
|
387
|
-
|
|
388
|
-
let existing = {};
|
|
389
|
-
try { existing = JSON.parse(readFileSync(PROFILE_FILE, 'utf8')); } catch {}
|
|
390
|
-
const custom = existing.custom_overrides || {};
|
|
391
|
-
custom.budgets = {
|
|
392
|
-
session_warn_usd: +(session * 0.6).toFixed(2),
|
|
393
|
-
session_limit_usd: session,
|
|
394
|
-
daily_warn_usd: +(finalDaily * 0.6).toFixed(2),
|
|
395
|
-
daily_limit_usd: finalDaily,
|
|
396
|
-
};
|
|
397
|
-
const data = { active: existing.active || 'balanced', switched_at: existing.switched_at || new Date().toISOString(), custom_overrides: custom };
|
|
398
|
-
const tmp = PROFILE_FILE + '.tmp.' + process.pid;
|
|
399
|
-
writeFileSync(tmp, JSON.stringify(data, null, 2) + '\n');
|
|
400
|
-
renameSync(tmp, PROFILE_FILE);
|
|
401
|
-
|
|
402
|
-
console.log(` ✅ Budget: $${session}/session · $${finalDaily}/daily`);
|
|
403
|
-
resolve();
|
|
404
|
-
});
|
|
405
|
-
});
|
|
406
|
-
});
|
|
407
|
-
}
|
|
441
|
+
// (Cost alert editor removed — replaced by provider balance + mode switching)
|
|
408
442
|
|
|
409
443
|
// ─── Session Runner ───────────────────────────────────────────────────────
|
|
410
444
|
|
|
411
445
|
function runSession(cmd, args, label) {
|
|
412
446
|
console.log('');
|
|
413
|
-
console.log(` ${label}
|
|
447
|
+
console.log(` ${label}`);
|
|
448
|
+
console.log(` ${dim('Inside Claude: press Ctrl+C twice to return here.')}`);
|
|
414
449
|
console.log('');
|
|
450
|
+
markLaunched();
|
|
415
451
|
const result = spawnSync(cmd, args, { stdio: 'inherit' });
|
|
416
452
|
console.log('');
|
|
417
|
-
console.log('
|
|
453
|
+
console.log(' Returned to Dual-Brain.');
|
|
418
454
|
return result.status || 0;
|
|
419
455
|
}
|
|
420
456
|
|
|
@@ -422,11 +458,17 @@ function runSession(cmd, args, label) {
|
|
|
422
458
|
|
|
423
459
|
async function mainLoop() {
|
|
424
460
|
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
425
|
-
|
|
426
461
|
const ask = () => new Promise(resolve => rl.question(' Choice: ', resolve));
|
|
427
462
|
|
|
428
463
|
while (true) {
|
|
429
|
-
const
|
|
464
|
+
const firstRun = isFirstRun();
|
|
465
|
+
const providers = detectProviders();
|
|
466
|
+
const sessions = firstRun ? [] : getRecentSessions();
|
|
467
|
+
|
|
468
|
+
const lines = firstRun
|
|
469
|
+
? renderFirstRunMenu(providers)
|
|
470
|
+
: renderReturningMenu(providers, sessions);
|
|
471
|
+
|
|
430
472
|
for (const l of lines) console.log(l);
|
|
431
473
|
|
|
432
474
|
const choice = (await ask()).trim().toLowerCase();
|
|
@@ -438,16 +480,15 @@ async function mainLoop() {
|
|
|
438
480
|
}
|
|
439
481
|
|
|
440
482
|
if (choice === 'c' || choice === '') {
|
|
441
|
-
// Continue most recent session
|
|
442
483
|
if (sessions.length > 0) {
|
|
443
484
|
const s = sessions[0];
|
|
444
485
|
if (s.tool === 'codex') {
|
|
445
|
-
runSession('codex', ['--dangerously-bypass-approvals-and-sandbox', 'resume', s.id], `Resuming codex
|
|
486
|
+
runSession('codex', ['--dangerously-bypass-approvals-and-sandbox', 'resume', s.id], `Resuming codex ${s.id.slice(0, 8)}...`);
|
|
446
487
|
} else {
|
|
447
|
-
runSession('claude', ['-r', s.id, '--dangerously-skip-permissions'], `Resuming session ${s.id.slice(0, 8)}
|
|
488
|
+
runSession('claude', ['-r', s.id, '--dangerously-skip-permissions'], `Resuming session ${s.id.slice(0, 8)}...`);
|
|
448
489
|
}
|
|
449
490
|
} else {
|
|
450
|
-
runSession('claude', ['--dangerously-skip-permissions'], 'Starting new session');
|
|
491
|
+
runSession('claude', ['--dangerously-skip-permissions'], 'Starting new session...');
|
|
451
492
|
}
|
|
452
493
|
continue;
|
|
453
494
|
}
|
|
@@ -456,15 +497,15 @@ async function mainLoop() {
|
|
|
456
497
|
if (num >= 1 && num <= 9 && sessions[num - 1]) {
|
|
457
498
|
const s = sessions[num - 1];
|
|
458
499
|
if (s.tool === 'codex') {
|
|
459
|
-
runSession('codex', ['--dangerously-bypass-approvals-and-sandbox', 'resume', s.id], `Resuming codex
|
|
500
|
+
runSession('codex', ['--dangerously-bypass-approvals-and-sandbox', 'resume', s.id], `Resuming codex ${s.id.slice(0, 8)}...`);
|
|
460
501
|
} else {
|
|
461
|
-
runSession('claude', ['-r', s.id, '--dangerously-skip-permissions'], `Resuming session ${s.id.slice(0, 8)}
|
|
502
|
+
runSession('claude', ['-r', s.id, '--dangerously-skip-permissions'], `Resuming session ${s.id.slice(0, 8)}...`);
|
|
462
503
|
}
|
|
463
504
|
continue;
|
|
464
505
|
}
|
|
465
506
|
|
|
466
507
|
if (choice === 'n') {
|
|
467
|
-
runSession('claude', ['--dangerously-skip-permissions'], 'Starting new session');
|
|
508
|
+
runSession('claude', ['--dangerously-skip-permissions'], 'Starting new session...');
|
|
468
509
|
continue;
|
|
469
510
|
}
|
|
470
511
|
|
|
@@ -473,11 +514,6 @@ async function mainLoop() {
|
|
|
473
514
|
continue;
|
|
474
515
|
}
|
|
475
516
|
|
|
476
|
-
if (choice === 'b') {
|
|
477
|
-
await showBudgetEditor(rl);
|
|
478
|
-
continue;
|
|
479
|
-
}
|
|
480
|
-
|
|
481
517
|
if (choice === 'j') {
|
|
482
518
|
console.log('');
|
|
483
519
|
console.log(' Starting Claude login...');
|
|
@@ -508,7 +544,7 @@ async function mainLoop() {
|
|
|
508
544
|
console.log('');
|
|
509
545
|
spawnSync('npx', ['-y', 'data-tools'], { stdio: 'inherit', cwd: CWD });
|
|
510
546
|
console.log('');
|
|
511
|
-
console.log(' ✅ replit-tools installed.
|
|
547
|
+
console.log(' ✅ replit-tools installed.');
|
|
512
548
|
console.log('');
|
|
513
549
|
await ask();
|
|
514
550
|
continue;
|
|
@@ -521,7 +557,11 @@ async function mainLoop() {
|
|
|
521
557
|
// ─── Non-Interactive Fallback ─────────────────────────────────────────────
|
|
522
558
|
|
|
523
559
|
function renderStatic() {
|
|
524
|
-
const
|
|
560
|
+
const providers = detectProviders();
|
|
561
|
+
const sessions = getRecentSessions();
|
|
562
|
+
const lines = sessions.length > 0
|
|
563
|
+
? renderReturningMenu(providers, sessions)
|
|
564
|
+
: renderFirstRunMenu(providers);
|
|
525
565
|
for (const l of lines) console.log(l);
|
|
526
566
|
}
|
|
527
567
|
|
package/hooks/profiles.mjs
CHANGED
|
@@ -22,7 +22,7 @@ const CONFIG_FILE = join(__dirname, '..', 'orchestrator.json');
|
|
|
22
22
|
|
|
23
23
|
const PROFILES = {
|
|
24
24
|
balanced: {
|
|
25
|
-
description: '
|
|
25
|
+
description: 'Auto-routes by complexity, uses both providers evenly',
|
|
26
26
|
routing: {
|
|
27
27
|
prefer_provider: 'auto',
|
|
28
28
|
think_threshold: 'normal',
|
|
@@ -42,7 +42,7 @@ const PROFILES = {
|
|
|
42
42
|
},
|
|
43
43
|
|
|
44
44
|
'cost-saver': {
|
|
45
|
-
description: '
|
|
45
|
+
description: 'Conservative — fewer GPT dispatches, sticks to Claude',
|
|
46
46
|
routing: {
|
|
47
47
|
prefer_provider: 'cheapest',
|
|
48
48
|
think_threshold: 'strict',
|
|
@@ -65,7 +65,7 @@ const PROFILES = {
|
|
|
65
65
|
},
|
|
66
66
|
|
|
67
67
|
'quality-first': {
|
|
68
|
-
description: '
|
|
68
|
+
description: 'Aggressive — maximizes both subscriptions, dual-brain for medium+',
|
|
69
69
|
routing: {
|
|
70
70
|
prefer_provider: 'most-capable',
|
|
71
71
|
think_threshold: 'relaxed',
|
package/install.mjs
CHANGED
|
@@ -57,10 +57,10 @@ if (flag('--help') || flag('-h')) {
|
|
|
57
57
|
--json Output detection as JSON
|
|
58
58
|
--help Show this help
|
|
59
59
|
|
|
60
|
-
🎛️
|
|
61
|
-
⚖️
|
|
62
|
-
|
|
63
|
-
|
|
60
|
+
🎛️ Routing modes:
|
|
61
|
+
⚖️ Default Auto-routes, uses both providers evenly
|
|
62
|
+
🛡️ Conservative Fewer GPT dispatches, sticks to Claude
|
|
63
|
+
🚀 Aggressive Maximizes both subscriptions, dual-brain for medium+
|
|
64
64
|
|
|
65
65
|
🚀 Examples:
|
|
66
66
|
${cmd('npx dual-brain')} # install or update
|
|
@@ -313,6 +313,7 @@ function generateGitignoreEntries(workspace) {
|
|
|
313
313
|
'.claude/dual-brain.profile.json',
|
|
314
314
|
'.claude/hooks/usage-summary-*.json',
|
|
315
315
|
'.claude/hooks/decision-ledger.jsonl',
|
|
316
|
+
'.claude/.launched',
|
|
316
317
|
];
|
|
317
318
|
let existing = '';
|
|
318
319
|
try { existing = readFileSync(join(workspace, '.gitignore'), 'utf8'); } catch {}
|
|
@@ -423,19 +424,19 @@ function profilePath(workspace) {
|
|
|
423
424
|
|
|
424
425
|
const PROFILES = {
|
|
425
426
|
balanced: {
|
|
426
|
-
description: '
|
|
427
|
+
description: 'Auto-routes by complexity, uses both providers evenly',
|
|
427
428
|
routing: { prefer_provider: 'auto', think_threshold: 'normal', gpt_dispatch_bias: 0 },
|
|
428
429
|
budgets: { session_warn_usd: 5, session_limit_usd: 10, daily_warn_usd: 20, daily_limit_usd: 50 },
|
|
429
430
|
quality_gate: { sensitivity_floor: 'medium', dual_brain_minimum: 'high' },
|
|
430
431
|
},
|
|
431
432
|
'cost-saver': {
|
|
432
|
-
description: '
|
|
433
|
+
description: 'Conservative — fewer GPT dispatches, sticks to Claude',
|
|
433
434
|
routing: { prefer_provider: 'cheapest', think_threshold: 'strict', gpt_dispatch_bias: -20 },
|
|
434
435
|
budgets: { session_warn_usd: 2, session_limit_usd: 5, daily_warn_usd: 8, daily_limit_usd: 20 },
|
|
435
436
|
quality_gate: { sensitivity_floor: 'high', dual_brain_minimum: 'critical' },
|
|
436
437
|
},
|
|
437
438
|
'quality-first': {
|
|
438
|
-
description: '
|
|
439
|
+
description: 'Aggressive — maximizes both subscriptions, dual-brain for medium+',
|
|
439
440
|
routing: { prefer_provider: 'most-capable', think_threshold: 'relaxed', gpt_dispatch_bias: 10 },
|
|
440
441
|
budgets: { session_warn_usd: 15, session_limit_usd: 30, daily_warn_usd: 50, daily_limit_usd: 100 },
|
|
441
442
|
quality_gate: { sensitivity_floor: 'low', dual_brain_minimum: 'medium' },
|
|
@@ -489,16 +490,18 @@ function cmdMode() {
|
|
|
489
490
|
|
|
490
491
|
if (!modeArg || modeArg === 'list') {
|
|
491
492
|
const current = loadProfile(workspace);
|
|
492
|
-
const PEMOJIS = { balanced: '⚖️ ', 'cost-saver': '
|
|
493
|
+
const PEMOJIS = { balanced: '⚖️ ', 'cost-saver': '🛡️', 'quality-first': '🚀' };
|
|
494
|
+
const UI_NAMES = { balanced: 'Default', 'cost-saver': 'Conservative', 'quality-first': 'Aggressive' };
|
|
493
495
|
console.log('');
|
|
494
|
-
console.log(' 🎛️
|
|
496
|
+
console.log(' 🎛️ Routing modes:');
|
|
495
497
|
console.log('');
|
|
496
498
|
for (const [name, p] of Object.entries(PROFILES)) {
|
|
497
499
|
const active = name === current.name ? ' ✅ active' : '';
|
|
498
|
-
|
|
500
|
+
const label = UI_NAMES[name] || name;
|
|
501
|
+
console.log(` ${PEMOJIS[name] || ' '} ${label.padEnd(15)} ${p.description}${active}`);
|
|
499
502
|
}
|
|
500
503
|
console.log('');
|
|
501
|
-
console.log(` Switch: ${cmd('npx dual-brain mode <
|
|
504
|
+
console.log(` Switch: ${cmd('npx dual-brain mode <name>')}`);
|
|
502
505
|
console.log('');
|
|
503
506
|
return;
|
|
504
507
|
}
|
|
@@ -521,9 +524,10 @@ function cmdMode() {
|
|
|
521
524
|
|
|
522
525
|
saveProfile(workspace, modeArg, customOverrides);
|
|
523
526
|
|
|
524
|
-
const PEMOJIS = { balanced: '⚖️ ', 'cost-saver': '
|
|
527
|
+
const PEMOJIS = { balanced: '⚖️ ', 'cost-saver': '🛡️', 'quality-first': '🚀' };
|
|
528
|
+
const UI_NAMES = { balanced: 'Default', 'cost-saver': 'Conservative', 'quality-first': 'Aggressive' };
|
|
525
529
|
console.log('');
|
|
526
|
-
console.log(` ✅
|
|
530
|
+
console.log(` ✅ Mode switched: ${PEMOJIS[modeArg] || ''} ${UI_NAMES[modeArg] || modeArg}`);
|
|
527
531
|
console.log(` ${profile.description}`);
|
|
528
532
|
console.log('');
|
|
529
533
|
console.log(' 🧭 Routing changes:');
|
|
@@ -546,12 +550,12 @@ function cmdBudget() {
|
|
|
546
550
|
if (sessionArg == null) {
|
|
547
551
|
const profile = loadProfile(workspace);
|
|
548
552
|
console.log('');
|
|
549
|
-
console.log('
|
|
550
|
-
console.log(` Session: ⚠️ $${profile.budgets.session_warn_usd} warn · 🛑 $${profile.budgets.session_limit_usd}
|
|
551
|
-
console.log(` Daily: ⚠️ $${profile.budgets.daily_warn_usd} warn · 🛑 $${profile.budgets.daily_limit_usd}
|
|
553
|
+
console.log(' 📊 Usage alert thresholds (estimated, not billing caps):');
|
|
554
|
+
console.log(` Session: ⚠️ $${profile.budgets.session_warn_usd} warn · 🛑 $${profile.budgets.session_limit_usd} alert`);
|
|
555
|
+
console.log(` Daily: ⚠️ $${profile.budgets.daily_warn_usd} warn · 🛑 $${profile.budgets.daily_limit_usd} alert`);
|
|
552
556
|
console.log('');
|
|
553
|
-
console.log(`
|
|
554
|
-
console.log(` Example:
|
|
557
|
+
console.log(` Adjust: ${cmd('npx dual-brain budget <session$> [daily$]')}`);
|
|
558
|
+
console.log(` Example: ${cmd('npx dual-brain budget 8 25')}`);
|
|
555
559
|
console.log('');
|
|
556
560
|
return;
|
|
557
561
|
}
|
package/package.json
CHANGED