dual-brain 3.3.0 → 3.4.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 +396 -351
- package/install.mjs +37 -154
- package/package.json +1 -1
package/hooks/control-panel.mjs
CHANGED
|
@@ -1,48 +1,47 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
/**
|
|
3
|
-
* control-panel.mjs —
|
|
3
|
+
* control-panel.mjs — Session manager + control panel for Dual-Brain.
|
|
4
4
|
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
* Falls back to static emoji output when not in a TTY.
|
|
5
|
+
* Data-tools-style interactive menu: recent sessions, continue/resume/new,
|
|
6
|
+
* profile switching, budget editing. Loops until user exits to shell.
|
|
9
7
|
*/
|
|
10
8
|
|
|
11
9
|
import readline from 'readline';
|
|
12
|
-
import { existsSync, readFileSync, renameSync, writeFileSync } from 'fs';
|
|
10
|
+
import { existsSync, readFileSync, readdirSync, statSync, renameSync, writeFileSync } from 'fs';
|
|
13
11
|
import { dirname, join } from 'path';
|
|
14
12
|
import { fileURLToPath } from 'url';
|
|
15
13
|
import { spawnSync } from 'child_process';
|
|
16
14
|
|
|
17
15
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
18
|
-
const CONFIG_FILE = join(__dirname, '..', 'orchestrator.json');
|
|
19
16
|
const PROFILE_FILE = join(__dirname, '..', 'dual-brain.profile.json');
|
|
20
17
|
const VERSION = (() => {
|
|
21
|
-
try { return JSON.parse(readFileSync(join(__dirname, '..', '..', 'dual-brain', 'package.json'), 'utf8')).version; } catch {}
|
|
22
18
|
try { return JSON.parse(readFileSync(join(__dirname, '..', 'package.json'), 'utf8')).version; } catch {}
|
|
23
19
|
return '?';
|
|
24
20
|
})();
|
|
25
21
|
|
|
22
|
+
const IS_REPLIT = !!(process.env.REPL_ID || process.env.REPL_SLUG);
|
|
23
|
+
const HOME = process.env.HOME || process.env.USERPROFILE || '';
|
|
24
|
+
const CWD = process.cwd();
|
|
25
|
+
|
|
26
26
|
// ─── ANSI ──────────────────────────────────────────────────────────────────
|
|
27
27
|
|
|
28
|
-
const
|
|
29
|
-
const
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
const c = (code, s) => color ? `${code}${s}${A.reset}` : s;
|
|
28
|
+
const noColor = !!process.env.NO_COLOR;
|
|
29
|
+
const e = (code, s) => noColor ? s : `\x1b[${code}m${s}\x1b[0m`;
|
|
30
|
+
const bold = s => e('1', s);
|
|
31
|
+
const dim = s => e('2', s);
|
|
32
|
+
const cyan = s => e('36', s);
|
|
33
|
+
const green = s => e('32', s);
|
|
34
|
+
const yellow = s => e('33', s);
|
|
35
|
+
const magenta = s => e('95', s);
|
|
36
|
+
const orange = s => e('1;38;5;208', s);
|
|
37
|
+
const blue = s => e('1;38;5;33', s);
|
|
39
38
|
|
|
40
39
|
// ─── Profiles ──────────────────────────────────────────────────────────────
|
|
41
40
|
|
|
42
41
|
const PROFILES = {
|
|
43
|
-
balanced: { emoji: '⚖️', label: 'Balanced', desc: '
|
|
44
|
-
'cost-saver': { emoji: '💸', label: 'Cost-saver', desc: '
|
|
45
|
-
'quality-first': { emoji: '💎', label: 'Quality-first', desc: '
|
|
42
|
+
balanced: { emoji: '⚖️', label: 'Balanced', desc: 'Best model per tier, normal budgets' },
|
|
43
|
+
'cost-saver': { emoji: '💸', label: 'Cost-saver', desc: 'Prefer cheaper models, lower budgets' },
|
|
44
|
+
'quality-first': { emoji: '💎', label: 'Quality-first', desc: 'Dual-brain for medium+, strict reviews' },
|
|
46
45
|
};
|
|
47
46
|
|
|
48
47
|
const PROFILE_BUDGETS = {
|
|
@@ -51,31 +50,14 @@ const PROFILE_BUDGETS = {
|
|
|
51
50
|
'quality-first': { session_warn_usd: 15, session_limit_usd: 30, daily_warn_usd: 50, daily_limit_usd: 100 },
|
|
52
51
|
};
|
|
53
52
|
|
|
54
|
-
const PROFILE_GATE = {
|
|
55
|
-
balanced: { sensitivity_floor: 'medium', dual_brain_minimum: 'high' },
|
|
56
|
-
'cost-saver': { sensitivity_floor: 'high', dual_brain_minimum: 'critical' },
|
|
57
|
-
'quality-first': { sensitivity_floor: 'low', dual_brain_minimum: 'medium' },
|
|
58
|
-
};
|
|
59
|
-
|
|
60
|
-
// ─── Data Loaders ──────────────────────────────────────────────────────────
|
|
61
|
-
|
|
62
|
-
function loadConfig() {
|
|
63
|
-
try { return JSON.parse(readFileSync(CONFIG_FILE, 'utf8')); } catch { return {}; }
|
|
64
|
-
}
|
|
65
|
-
|
|
66
53
|
function loadProfile() {
|
|
67
54
|
try {
|
|
68
55
|
const data = JSON.parse(readFileSync(PROFILE_FILE, 'utf8'));
|
|
69
56
|
const name = data.active && PROFILES[data.active] ? data.active : 'balanced';
|
|
70
57
|
const custom = data.custom_overrides || {};
|
|
71
|
-
return {
|
|
72
|
-
name,
|
|
73
|
-
budgets: { ...PROFILE_BUDGETS[name], ...custom.budgets },
|
|
74
|
-
gate: PROFILE_GATE[name],
|
|
75
|
-
switched_at: data.switched_at || null,
|
|
76
|
-
};
|
|
58
|
+
return { name, budgets: { ...PROFILE_BUDGETS[name], ...custom.budgets } };
|
|
77
59
|
} catch {
|
|
78
|
-
return { name: 'balanced', budgets: PROFILE_BUDGETS.balanced
|
|
60
|
+
return { name: 'balanced', budgets: PROFILE_BUDGETS.balanced };
|
|
79
61
|
}
|
|
80
62
|
}
|
|
81
63
|
|
|
@@ -87,29 +69,19 @@ function saveProfile(name, customOverrides) {
|
|
|
87
69
|
renameSync(tmp, PROFILE_FILE);
|
|
88
70
|
}
|
|
89
71
|
|
|
90
|
-
|
|
91
|
-
let existing = {};
|
|
92
|
-
try { existing = JSON.parse(readFileSync(PROFILE_FILE, 'utf8')); } catch {}
|
|
93
|
-
const custom = existing.custom_overrides || {};
|
|
94
|
-
custom.budgets = {
|
|
95
|
-
session_warn_usd: +(sessionLimit * 0.6).toFixed(2),
|
|
96
|
-
session_limit_usd: sessionLimit,
|
|
97
|
-
daily_warn_usd: +(dailyLimit * 0.6).toFixed(2),
|
|
98
|
-
daily_limit_usd: dailyLimit,
|
|
99
|
-
};
|
|
100
|
-
const data = { active: existing.active || 'balanced', switched_at: existing.switched_at || new Date().toISOString(), custom_overrides: custom };
|
|
101
|
-
const tmp = PROFILE_FILE + '.tmp.' + process.pid;
|
|
102
|
-
writeFileSync(tmp, JSON.stringify(data, null, 2) + '\n');
|
|
103
|
-
renameSync(tmp, PROFILE_FILE);
|
|
104
|
-
}
|
|
72
|
+
// ─── Provider Detection ───────────────────────────────────────────────────
|
|
105
73
|
|
|
106
74
|
function detectProviders() {
|
|
107
|
-
const claude = {
|
|
108
|
-
const codex = {
|
|
75
|
+
const claude = { installed: false, authed: false };
|
|
76
|
+
const codex = { installed: false, authed: false };
|
|
77
|
+
|
|
78
|
+
const claudeCheck = spawnSync('which', ['claude'], { encoding: 'utf8', stdio: 'pipe', timeout: 3000 });
|
|
79
|
+
claude.installed = claudeCheck.status === 0 && !!claudeCheck.stdout.trim();
|
|
109
80
|
|
|
110
81
|
const credPaths = [
|
|
111
|
-
join(
|
|
112
|
-
join(
|
|
82
|
+
join(HOME, '.claude', '.credentials.json'),
|
|
83
|
+
join(HOME, '.claude', 'credentials.json'),
|
|
84
|
+
join(CWD, '.replit-tools', '.claude-persistent', '.credentials.json'),
|
|
113
85
|
];
|
|
114
86
|
for (const p of credPaths) {
|
|
115
87
|
try {
|
|
@@ -117,16 +89,16 @@ function detectProviders() {
|
|
|
117
89
|
if (cred.claudeAiOauth || cred.apiKey || cred.oauth_token) { claude.authed = true; break; }
|
|
118
90
|
} catch {}
|
|
119
91
|
}
|
|
120
|
-
if (!claude.authed) {
|
|
92
|
+
if (!claude.authed && claude.installed) {
|
|
121
93
|
const r = spawnSync('claude', ['auth', 'status'], { encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'], timeout: 5000 });
|
|
122
94
|
const out = ((r.stdout || '') + (r.stderr || '')).toLowerCase();
|
|
123
95
|
if (out.includes('logged in') || out.includes('authenticated')) claude.authed = true;
|
|
124
96
|
}
|
|
125
97
|
|
|
126
|
-
const
|
|
127
|
-
if (
|
|
98
|
+
const codexCheck = spawnSync('which', ['codex'], { encoding: 'utf8', stdio: 'pipe', timeout: 3000 });
|
|
99
|
+
if (codexCheck.status === 0 && codexCheck.stdout.trim()) {
|
|
128
100
|
codex.installed = true;
|
|
129
|
-
const login = spawnSync(
|
|
101
|
+
const login = spawnSync(codexCheck.stdout.trim(), ['login', 'status'], { encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'], timeout: 5000 });
|
|
130
102
|
const out = ((login.stdout || '') + (login.stderr || '')).toLowerCase();
|
|
131
103
|
if (login.status === 0 || out.includes('logged in') || out.includes('authenticated')) codex.authed = true;
|
|
132
104
|
}
|
|
@@ -134,356 +106,429 @@ function detectProviders() {
|
|
|
134
106
|
return { claude, codex };
|
|
135
107
|
}
|
|
136
108
|
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
109
|
+
// ─── Session Discovery ────────────────────────────────────────────────────
|
|
110
|
+
|
|
111
|
+
function getRecentSessions() {
|
|
112
|
+
const cutoff = Date.now() - 24 * 60 * 60 * 1000;
|
|
113
|
+
const sessions = new Map();
|
|
114
|
+
|
|
115
|
+
const isRealPrompt = (txt) => {
|
|
116
|
+
if (!txt) return false;
|
|
117
|
+
const t = txt.trim();
|
|
118
|
+
if (!t) return false;
|
|
119
|
+
if (/^[✅❌📦🔗⚠️🚀🎉🔧📝]/.test(t)) return false;
|
|
120
|
+
if (/Claude (history|binary|versions) symlink/.test(t)) return false;
|
|
121
|
+
if (t.startsWith('# AGENTS.md')) return false;
|
|
122
|
+
return true;
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
// Claude sessions
|
|
126
|
+
const historyFile = join(HOME, '.claude', 'history.jsonl');
|
|
127
|
+
if (existsSync(historyFile)) {
|
|
128
|
+
try {
|
|
129
|
+
const lines = readFileSync(historyFile, 'utf8').trim().split('\n');
|
|
130
|
+
const entries = [];
|
|
131
|
+
for (const line of lines) {
|
|
132
|
+
try {
|
|
133
|
+
const j = JSON.parse(line);
|
|
134
|
+
if (j.sessionId && j.timestamp) entries.push(j);
|
|
135
|
+
} catch {}
|
|
152
136
|
}
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
137
|
+
entries.sort((a, b) => a.timestamp - b.timestamp);
|
|
138
|
+
for (const j of entries) {
|
|
139
|
+
const key = 'claude:' + j.sessionId;
|
|
140
|
+
if (!sessions.has(key)) {
|
|
141
|
+
sessions.set(key, { tool: 'claude', id: j.sessionId, firstSeen: j.timestamp, lastSeen: j.timestamp, firstPrompt: '' });
|
|
142
|
+
}
|
|
143
|
+
const s = sessions.get(key);
|
|
144
|
+
if (j.timestamp < s.firstSeen) s.firstSeen = j.timestamp;
|
|
145
|
+
if (j.timestamp > s.lastSeen) s.lastSeen = j.timestamp;
|
|
146
|
+
if (!s.firstPrompt && isRealPrompt(j.display)) s.firstPrompt = j.display;
|
|
147
|
+
}
|
|
148
|
+
for (const [key, s] of sessions) {
|
|
149
|
+
if (s.tool === 'claude' && !s.firstPrompt) sessions.delete(key);
|
|
150
|
+
}
|
|
151
|
+
} catch {}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// Codex sessions
|
|
155
|
+
const codexDir = join(HOME, '.codex', 'sessions');
|
|
156
|
+
if (existsSync(codexDir)) {
|
|
157
|
+
const walk = (dir) => {
|
|
158
|
+
let results = [];
|
|
159
|
+
try {
|
|
160
|
+
for (const entry of readdirSync(dir, { withFileTypes: true })) {
|
|
161
|
+
const full = join(dir, entry.name);
|
|
162
|
+
if (entry.isDirectory()) results = results.concat(walk(full));
|
|
163
|
+
else if (entry.isFile() && entry.name.endsWith('.jsonl')) results.push(full);
|
|
164
|
+
}
|
|
165
|
+
} catch {}
|
|
166
|
+
return results;
|
|
159
167
|
};
|
|
168
|
+
for (const f of walk(codexDir)) {
|
|
169
|
+
try {
|
|
170
|
+
const stat = statSync(f);
|
|
171
|
+
if (stat.mtimeMs < cutoff) continue;
|
|
172
|
+
const content = readFileSync(f, 'utf8');
|
|
173
|
+
const lns = content.trim().split('\n');
|
|
174
|
+
if (!lns.length) continue;
|
|
175
|
+
const meta = JSON.parse(lns[0]);
|
|
176
|
+
if (meta.type !== 'session_meta' || !meta.payload) continue;
|
|
177
|
+
if (meta.payload.cwd !== CWD) continue;
|
|
178
|
+
const id = meta.payload.id;
|
|
179
|
+
const firstTs = Date.parse(meta.payload.timestamp || meta.timestamp);
|
|
180
|
+
let lastTs = firstTs;
|
|
181
|
+
let firstPrompt = '';
|
|
182
|
+
let realMsgCount = 0;
|
|
183
|
+
for (const ln of lns) {
|
|
184
|
+
try {
|
|
185
|
+
const j = JSON.parse(ln);
|
|
186
|
+
if (j.timestamp) lastTs = Math.max(lastTs, Date.parse(j.timestamp));
|
|
187
|
+
if (j.type === 'event_msg' && j.payload?.type === 'user_message') {
|
|
188
|
+
const text = (j.payload.message || '').trim();
|
|
189
|
+
if (text) { if (!firstPrompt) firstPrompt = text; realMsgCount++; }
|
|
190
|
+
}
|
|
191
|
+
} catch {}
|
|
192
|
+
}
|
|
193
|
+
if (realMsgCount === 0 || !firstPrompt) continue;
|
|
194
|
+
if (/^(you are |you're |\*\*role\*\*|<role>|## role)/i.test(firstPrompt)) continue;
|
|
195
|
+
if (realMsgCount === 1 && firstPrompt.length > 500) continue;
|
|
196
|
+
sessions.set('codex:' + id, { tool: 'codex', id, firstSeen: firstTs, lastSeen: lastTs, firstPrompt });
|
|
197
|
+
} catch {}
|
|
198
|
+
}
|
|
160
199
|
}
|
|
200
|
+
|
|
201
|
+
return Array.from(sessions.values())
|
|
202
|
+
.filter(s => (s.lastSeen || 0) >= cutoff)
|
|
203
|
+
.sort((a, b) => (b.lastSeen || 0) - (a.lastSeen || 0))
|
|
204
|
+
.slice(0, 9);
|
|
161
205
|
}
|
|
162
206
|
|
|
163
|
-
function
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
207
|
+
function timeAgo(ts) {
|
|
208
|
+
const mins = Math.round((Date.now() - ts) / 60000);
|
|
209
|
+
if (mins < 1) return 'just now';
|
|
210
|
+
if (mins < 60) return mins + 'm ago';
|
|
211
|
+
const h = Math.round(mins / 60);
|
|
212
|
+
return h + 'h ago';
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
function snippet(s, n = 15) {
|
|
216
|
+
const clean = (s || '').replace(/\s+/g, ' ').trim();
|
|
217
|
+
return clean.length > n ? clean.slice(0, n - 1) + '…' : clean;
|
|
169
218
|
}
|
|
170
219
|
|
|
171
|
-
function
|
|
172
|
-
|
|
173
|
-
const logFile = join(__dirname, `usage-${today}.jsonl`);
|
|
174
|
-
if (!existsSync(logFile)) return null;
|
|
220
|
+
function countRunning() {
|
|
221
|
+
let claude = 0, codex = 0;
|
|
175
222
|
try {
|
|
176
|
-
const
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
}
|
|
223
|
+
const r = spawnSync('pgrep', ['-x', 'claude'], { encoding: 'utf8', stdio: 'pipe', timeout: 2000 });
|
|
224
|
+
claude = (r.stdout || '').trim().split('\n').filter(Boolean).length;
|
|
225
|
+
} catch {}
|
|
226
|
+
try {
|
|
227
|
+
const r = spawnSync('pgrep', ['-x', 'codex'], { encoding: 'utf8', stdio: 'pipe', timeout: 2000 });
|
|
228
|
+
codex = (r.stdout || '').trim().split('\n').filter(Boolean).length;
|
|
183
229
|
} catch {}
|
|
184
|
-
return
|
|
230
|
+
return { claude, codex };
|
|
185
231
|
}
|
|
186
232
|
|
|
187
|
-
// ───
|
|
188
|
-
|
|
189
|
-
function
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
const pct = String(Math.round(p * 100)).padStart(3) + '%';
|
|
193
|
-
let stateEmoji, stateLabel;
|
|
194
|
-
if (p >= 0.95) { stateEmoji = '🛑'; stateLabel = c(A.red + A.bold, 'throttled'); }
|
|
195
|
-
else if (p >= 0.82) { stateEmoji = '🔥'; stateLabel = c(A.red, 'hot'); }
|
|
196
|
-
else if (p >= 0.65) { stateEmoji = '🟡'; stateLabel = c(A.yellow, 'warm'); }
|
|
197
|
-
else { stateEmoji = '🟢'; stateLabel = c(A.green, 'healthy'); }
|
|
198
|
-
const barColored = p >= 0.82 ? c(A.red, bar) : p >= 0.65 ? c(A.yellow, bar) : c(A.green, bar);
|
|
199
|
-
return `${barColored} ${pct} ${stateEmoji} ${stateLabel}`;
|
|
233
|
+
// ─── Replit-Tools Check ───────────────────────────────────────────────────
|
|
234
|
+
|
|
235
|
+
function checkReplitTools() {
|
|
236
|
+
if (!IS_REPLIT) return true;
|
|
237
|
+
return existsSync(join(CWD, '.replit-tools'));
|
|
200
238
|
}
|
|
201
239
|
|
|
202
|
-
|
|
203
|
-
|
|
240
|
+
// ─── Menu Renderer ────────────────────────────────────────────────────────
|
|
241
|
+
|
|
242
|
+
function renderMenu() {
|
|
243
|
+
const providers = detectProviders();
|
|
244
|
+
const profile = loadProfile();
|
|
245
|
+
const sessions = getRecentSessions();
|
|
246
|
+
const running = countRunning();
|
|
204
247
|
const pf = PROFILES[profile.name];
|
|
205
|
-
const
|
|
206
|
-
const mode = (providers.claude.authed && providers.codex.authed) ? '🧠 Dual brain active' :
|
|
207
|
-
providers.claude.authed ? '🟠 Claude only' :
|
|
208
|
-
providers.codex.authed ? '🟢 OpenAI only' : '🔎 No providers';
|
|
248
|
+
const hasReplitTools = checkReplitTools();
|
|
209
249
|
|
|
210
250
|
const lines = [];
|
|
211
|
-
lines.push('');
|
|
212
|
-
lines.push(c(A.bold, ` 🧠 Dual-Brain Control Panel v${VERSION}`) + ` ${c(A.green, '🟢 Live')} ${c(A.dim, time)}`);
|
|
213
|
-
lines.push('');
|
|
214
|
-
lines.push(` ${mode}`);
|
|
215
|
-
lines.push(` 🎛️ Profile ${pf.emoji} ${c(A.bold, pf.label)} ${c(A.dim, pf.desc)}`);
|
|
216
|
-
lines.push(` 💵 Budget Session $${cost.toFixed(2)} / $${profile.budgets.session_limit_usd} Daily / $${profile.budgets.daily_limit_usd}`);
|
|
217
|
-
lines.push(` 🛡️ Gate Reviews ${profile.gate.sensitivity_floor}+ Dual-brain ${profile.gate.dual_brain_minimum}+`);
|
|
218
|
-
lines.push('');
|
|
219
251
|
|
|
220
|
-
lines.push(` 🔌 ${c(A.bold, 'Providers')}`);
|
|
221
|
-
const cStatus = providers.claude.authed ? '✅ authenticated' : '⚠️ not authenticated';
|
|
222
|
-
const xStatus = providers.codex.authed ? '✅ authenticated' : providers.codex.installed ? '⚠️ login needed' : '❌ not found';
|
|
223
|
-
lines.push(` 🟠 Claude ${cStatus} ${c(A.dim, providers.claude.models)}`);
|
|
224
|
-
lines.push(` 🟢 Codex ${xStatus} ${c(A.dim, providers.codex.models)}`);
|
|
225
252
|
lines.push('');
|
|
226
|
-
|
|
227
|
-
lines.push(` 🌡️ ${c(A.bold, 'Pressure')} ${c(A.dim, '— rolling 5h')}`);
|
|
228
|
-
for (const [label, emoji, key] of [['Claude', '🟠', 'claude'], ['OpenAI', '🟢', 'openai']]) {
|
|
229
|
-
lines.push(` ${emoji} ${label}`);
|
|
230
|
-
for (const tier of ['think', 'execute', 'search']) {
|
|
231
|
-
const p = pressure[key]?.[tier] || { pressure: 0 };
|
|
232
|
-
const tierLabel = (tier.charAt(0).toUpperCase() + tier.slice(1)).padEnd(8);
|
|
233
|
-
lines.push(` ${c(A.dim, tierLabel)} ${pressureBar(p.pressure)}`);
|
|
234
|
-
}
|
|
235
|
-
if (key === 'claude') lines.push('');
|
|
236
|
-
}
|
|
253
|
+
lines.push(` 🧠 ${bold(`Dual-Brain v${VERSION}`)}`);
|
|
237
254
|
lines.push('');
|
|
238
255
|
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
256
|
+
// Quick reference box
|
|
257
|
+
lines.push(' ┌─────────────────────────────┐');
|
|
258
|
+
if (IS_REPLIT) {
|
|
259
|
+
lines.push(` │ ${magenta('At')} ${blue('~/workspace')}${magenta('$ prompt:')} │`);
|
|
260
|
+
lines.push(` │ ${cyan('! npx dual-brain')} = this menu│`);
|
|
261
|
+
} else {
|
|
262
|
+
lines.push(` │ ${magenta('At shell prompt:')} │`);
|
|
263
|
+
lines.push(` │ ${cyan('npx dual-brain')} = this menu │`);
|
|
242
264
|
}
|
|
243
|
-
|
|
244
|
-
lines.push(
|
|
245
|
-
lines.push(
|
|
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(' └─────────────────────────────┘');
|
|
246
272
|
lines.push('');
|
|
247
273
|
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
const lines = [];
|
|
253
|
-
lines.push('');
|
|
254
|
-
lines.push(c(A.bold, ' 🧭 Last Routing Decision'));
|
|
255
|
-
lines.push(c(A.dim, ' ' + '─'.repeat(40)));
|
|
274
|
+
// Provider status line
|
|
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')}`);
|
|
256
278
|
|
|
257
|
-
|
|
258
|
-
|
|
279
|
+
// Missing provider nudge
|
|
280
|
+
if (!providers.claude.authed || !providers.codex.authed) {
|
|
259
281
|
lines.push('');
|
|
260
|
-
lines.push(
|
|
261
|
-
|
|
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`);
|
|
262
286
|
}
|
|
263
287
|
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
lines.push(` 🎯 Actual ${decision.actual_model || 'unknown'}`);
|
|
270
|
-
lines.push(` ${followed ? '✅' : '⚠️'} Followed ${followed ? 'yes' : 'no'}`);
|
|
271
|
-
lines.push(` 🎛️ Profile ${profile.name}`);
|
|
272
|
-
lines.push('');
|
|
273
|
-
|
|
274
|
-
if (followed) {
|
|
275
|
-
lines.push(' ✅ Routing matched the recommendation.');
|
|
276
|
-
} else {
|
|
277
|
-
lines.push(' ⚠️ Recommendation was overridden.');
|
|
288
|
+
// Replit-tools check
|
|
289
|
+
if (IS_REPLIT && !hasReplitTools) {
|
|
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`);
|
|
278
293
|
}
|
|
279
294
|
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
}
|
|
295
|
+
// Recent sessions
|
|
296
|
+
if (sessions.length > 0) {
|
|
297
|
+
lines.push('');
|
|
298
|
+
lines.push(` ${bold('Recent (last 24h):')}`);
|
|
299
|
+
for (let i = 0; i < sessions.length; i++) {
|
|
300
|
+
const s = sessions[i];
|
|
301
|
+
const num = String(i + 1);
|
|
302
|
+
const toolLabel = s.tool === 'codex' ? orange('cdx') : blue('cld');
|
|
303
|
+
const ago = timeAgo(s.lastSeen).padEnd(9);
|
|
304
|
+
lines.push(` ${bold('[' + num + ']')} ${toolLabel} ${dim(ago)} ${snippet(s.firstPrompt)}`);
|
|
305
|
+
}
|
|
306
|
+
}
|
|
284
307
|
|
|
285
|
-
|
|
286
|
-
const lines = [];
|
|
308
|
+
// Session manager box
|
|
287
309
|
lines.push('');
|
|
288
|
-
lines.push(
|
|
289
|
-
lines.push(
|
|
310
|
+
lines.push(' ┌─────────────────────────────┐');
|
|
311
|
+
lines.push(' │ 🧠 Dual-Brain Session Mgr │');
|
|
312
|
+
lines.push(' └─────────────────────────────┘');
|
|
313
|
+
|
|
314
|
+
const runParts = [];
|
|
315
|
+
if (running.claude > 0) runParts.push(`${running.claude} claude`);
|
|
316
|
+
if (running.codex > 0) runParts.push(`${running.codex} codex`);
|
|
317
|
+
if (runParts.length > 0) lines.push(` ${dim('(' + runParts.join(', ') + ' running)')}`);
|
|
290
318
|
lines.push('');
|
|
291
319
|
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
lines.push(`
|
|
295
|
-
lines.push(`
|
|
320
|
+
// Menu options
|
|
321
|
+
lines.push(` ${bold('[c]')} Continue last session`);
|
|
322
|
+
if (sessions.length > 0) lines.push(` ${bold('[1-9]')} Resume numbered above`);
|
|
323
|
+
lines.push(` ${bold('[n]')} New session`);
|
|
324
|
+
lines.push(` ${bold('[p]')} Profile ${dim('(' + pf.emoji + ' ' + profile.name + ')')}`);
|
|
325
|
+
lines.push(` ${bold('[b]')} Budget ${dim('($' + profile.budgets.session_limit_usd + ' session / $' + profile.budgets.daily_limit_usd + ' daily)')}`);
|
|
326
|
+
lines.push(` ${bold('[j]')} Login to Claude`);
|
|
327
|
+
lines.push(` ${bold('[k]')} Login to Codex`);
|
|
328
|
+
if (IS_REPLIT && !hasReplitTools) lines.push(` ${bold('[t]')} Install replit-tools`);
|
|
329
|
+
lines.push(` ${bold('[s]')} Skip — just shell`);
|
|
296
330
|
lines.push('');
|
|
297
331
|
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
332
|
+
return { lines, sessions, providers };
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
// ─── Profile Picker ───────────────────────────────────────────────────────
|
|
336
|
+
|
|
337
|
+
function showProfilePicker(rl) {
|
|
338
|
+
return new Promise((resolve) => {
|
|
339
|
+
const current = loadProfile();
|
|
340
|
+
console.log('');
|
|
341
|
+
console.log(` ${bold('🎛️ Switch Profile:')}`);
|
|
342
|
+
console.log('');
|
|
343
|
+
for (const [i, [name, pf]] of Object.entries(PROFILES).entries()) {
|
|
344
|
+
const active = name === current.name ? ' ✅' : '';
|
|
345
|
+
console.log(` ${bold('[' + (i + 1) + ']')} ${pf.emoji} ${name.padEnd(15)} ${dim(pf.desc)}${active}`);
|
|
346
|
+
}
|
|
347
|
+
console.log(` ${bold('[q]')} Cancel`);
|
|
348
|
+
console.log('');
|
|
349
|
+
|
|
350
|
+
rl.question(' Choice: ', (answer) => {
|
|
351
|
+
const names = Object.keys(PROFILES);
|
|
352
|
+
const idx = parseInt(answer, 10) - 1;
|
|
353
|
+
if (idx >= 0 && idx < names.length) {
|
|
354
|
+
let customOverrides = null;
|
|
355
|
+
try {
|
|
356
|
+
const existing = JSON.parse(readFileSync(PROFILE_FILE, 'utf8'));
|
|
357
|
+
if (existing.custom_overrides?.budgets) customOverrides = { budgets: existing.custom_overrides.budgets };
|
|
358
|
+
} catch {}
|
|
359
|
+
saveProfile(names[idx], customOverrides);
|
|
360
|
+
const pf = PROFILES[names[idx]];
|
|
361
|
+
console.log(` ✅ Switched to ${pf.emoji} ${pf.label}`);
|
|
362
|
+
}
|
|
363
|
+
resolve();
|
|
364
|
+
});
|
|
365
|
+
});
|
|
366
|
+
}
|
|
302
367
|
|
|
303
|
-
|
|
304
|
-
|
|
368
|
+
// ─── Budget Editor ────────────────────────────────────────────────────────
|
|
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
|
+
});
|
|
305
407
|
}
|
|
306
408
|
|
|
307
|
-
// ───
|
|
409
|
+
// ─── Session Runner ───────────────────────────────────────────────────────
|
|
308
410
|
|
|
309
|
-
function
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
const
|
|
314
|
-
|
|
315
|
-
console.log(
|
|
411
|
+
function runSession(cmd, args, label) {
|
|
412
|
+
console.log('');
|
|
413
|
+
console.log(` ${label}...`);
|
|
414
|
+
console.log('');
|
|
415
|
+
const result = spawnSync(cmd, args, { stdio: 'inherit' });
|
|
416
|
+
console.log('');
|
|
417
|
+
console.log(' Exited. Returning to menu...');
|
|
418
|
+
return result.status || 0;
|
|
316
419
|
}
|
|
317
420
|
|
|
318
|
-
// ───
|
|
421
|
+
// ─── Main Loop ────────────────────────────────────────────────────────────
|
|
319
422
|
|
|
320
|
-
function
|
|
321
|
-
|
|
322
|
-
let flash = null;
|
|
323
|
-
let flashTimeout = null;
|
|
324
|
-
let refreshTimer = null;
|
|
423
|
+
async function mainLoop() {
|
|
424
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
325
425
|
|
|
326
|
-
|
|
327
|
-
let budgetSession = '';
|
|
328
|
-
let budgetDaily = '';
|
|
329
|
-
let budgetField = 'session';
|
|
426
|
+
const ask = () => new Promise(resolve => rl.question(' Choice: ', resolve));
|
|
330
427
|
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
flashTimeout = setTimeout(() => { flash = null; render(); }, ms);
|
|
335
|
-
}
|
|
428
|
+
while (true) {
|
|
429
|
+
const { lines, sessions } = renderMenu();
|
|
430
|
+
for (const l of lines) console.log(l);
|
|
336
431
|
|
|
337
|
-
|
|
338
|
-
return {
|
|
339
|
-
profile: loadProfile(),
|
|
340
|
-
providers: detectProviders(),
|
|
341
|
-
pressure: loadPressure(),
|
|
342
|
-
cost: loadTodayCost(),
|
|
343
|
-
flash,
|
|
344
|
-
};
|
|
345
|
-
}
|
|
432
|
+
const choice = (await ask()).trim().toLowerCase();
|
|
346
433
|
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
} else if (view === 'explain') {
|
|
352
|
-
const decision = loadLastDecision();
|
|
353
|
-
const profile = loadProfile();
|
|
354
|
-
screen = renderExplain(decision, profile);
|
|
355
|
-
} else if (view === 'budget') {
|
|
356
|
-
screen = renderBudgetEditor(budgetSession, budgetDaily, budgetField, flash);
|
|
434
|
+
if (choice === 's' || choice === 'q') {
|
|
435
|
+
console.log('');
|
|
436
|
+
rl.close();
|
|
437
|
+
return;
|
|
357
438
|
}
|
|
358
|
-
process.stdout.write(A.home + A.clear + screen);
|
|
359
|
-
}
|
|
360
|
-
|
|
361
|
-
function startRefresh() {
|
|
362
|
-
stopRefresh();
|
|
363
|
-
refreshTimer = setInterval(render, 2000);
|
|
364
|
-
}
|
|
365
439
|
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
440
|
+
if (choice === 'c' || choice === '') {
|
|
441
|
+
// Continue most recent session
|
|
442
|
+
if (sessions.length > 0) {
|
|
443
|
+
const s = sessions[0];
|
|
444
|
+
if (s.tool === 'codex') {
|
|
445
|
+
runSession('codex', ['--dangerously-bypass-approvals-and-sandbox', 'resume', s.id], `Resuming codex session ${s.id.slice(0, 8)}`);
|
|
446
|
+
} else {
|
|
447
|
+
runSession('claude', ['-r', s.id, '--dangerously-skip-permissions'], `Resuming session ${s.id.slice(0, 8)}`);
|
|
448
|
+
}
|
|
449
|
+
} else {
|
|
450
|
+
runSession('claude', ['--dangerously-skip-permissions'], 'Starting new session');
|
|
451
|
+
}
|
|
452
|
+
continue;
|
|
453
|
+
}
|
|
369
454
|
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
455
|
+
const num = parseInt(choice, 10);
|
|
456
|
+
if (num >= 1 && num <= 9 && sessions[num - 1]) {
|
|
457
|
+
const s = sessions[num - 1];
|
|
458
|
+
if (s.tool === 'codex') {
|
|
459
|
+
runSession('codex', ['--dangerously-bypass-approvals-and-sandbox', 'resume', s.id], `Resuming codex session ${s.id.slice(0, 8)}`);
|
|
460
|
+
} else {
|
|
461
|
+
runSession('claude', ['-r', s.id, '--dangerously-skip-permissions'], `Resuming session ${s.id.slice(0, 8)}`);
|
|
462
|
+
}
|
|
463
|
+
continue;
|
|
464
|
+
}
|
|
377
465
|
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
if (existing.custom_overrides?.budgets) customOverrides = { budgets: existing.custom_overrides.budgets };
|
|
383
|
-
} catch {}
|
|
384
|
-
saveProfile(name, customOverrides);
|
|
385
|
-
const pf = PROFILES[name];
|
|
386
|
-
setFlash(`✅ Profile switched: ${pf.emoji} ${pf.label}`);
|
|
387
|
-
render();
|
|
388
|
-
}
|
|
466
|
+
if (choice === 'n') {
|
|
467
|
+
runSession('claude', ['--dangerously-skip-permissions'], 'Starting new session');
|
|
468
|
+
continue;
|
|
469
|
+
}
|
|
389
470
|
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
process.stdin.resume();
|
|
395
|
-
|
|
396
|
-
process.on('SIGINT', cleanup);
|
|
397
|
-
process.on('SIGTERM', cleanup);
|
|
398
|
-
process.on('uncaughtException', (err) => {
|
|
399
|
-
cleanup();
|
|
400
|
-
console.error(err);
|
|
401
|
-
});
|
|
471
|
+
if (choice === 'p') {
|
|
472
|
+
await showProfilePicker(rl);
|
|
473
|
+
continue;
|
|
474
|
+
}
|
|
402
475
|
|
|
403
|
-
|
|
404
|
-
|
|
476
|
+
if (choice === 'b') {
|
|
477
|
+
await showBudgetEditor(rl);
|
|
478
|
+
continue;
|
|
479
|
+
}
|
|
405
480
|
|
|
406
|
-
|
|
407
|
-
|
|
481
|
+
if (choice === 'j') {
|
|
482
|
+
console.log('');
|
|
483
|
+
console.log(' Starting Claude login...');
|
|
484
|
+
console.log('');
|
|
485
|
+
spawnSync('claude', ['login'], { stdio: 'inherit' });
|
|
486
|
+
continue;
|
|
487
|
+
}
|
|
408
488
|
|
|
409
|
-
if (
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
budgetField = budgetField === 'session' ? 'daily' : 'session';
|
|
418
|
-
render();
|
|
419
|
-
return;
|
|
489
|
+
if (choice === 'k') {
|
|
490
|
+
const codexPath = spawnSync('which', ['codex'], { encoding: 'utf8', stdio: 'pipe', timeout: 3000 });
|
|
491
|
+
if (codexPath.status !== 0) {
|
|
492
|
+
console.log('');
|
|
493
|
+
console.log(` Codex not installed. Run: ${cyan('npm i -g @openai/codex')}`);
|
|
494
|
+
console.log('');
|
|
495
|
+
await ask();
|
|
496
|
+
continue;
|
|
420
497
|
}
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
saveBudget(s, daily);
|
|
427
|
-
view = 'dashboard';
|
|
428
|
-
startRefresh();
|
|
429
|
-
setFlash(`✅ Budget updated: Session $${s} · Daily $${daily}`);
|
|
430
|
-
render();
|
|
431
|
-
return;
|
|
432
|
-
}
|
|
433
|
-
if (key?.name === 'backspace') {
|
|
434
|
-
if (budgetField === 'session') budgetSession = budgetSession.slice(0, -1);
|
|
435
|
-
else budgetDaily = budgetDaily.slice(0, -1);
|
|
436
|
-
render();
|
|
437
|
-
return;
|
|
438
|
-
}
|
|
439
|
-
if (str && /[0-9.]/.test(str)) {
|
|
440
|
-
if (budgetField === 'session') budgetSession += str;
|
|
441
|
-
else budgetDaily += str;
|
|
442
|
-
render();
|
|
443
|
-
return;
|
|
444
|
-
}
|
|
445
|
-
return;
|
|
498
|
+
console.log('');
|
|
499
|
+
console.log(' Starting Codex login...');
|
|
500
|
+
console.log('');
|
|
501
|
+
spawnSync(codexPath.stdout.trim(), ['login'], { stdio: 'inherit' });
|
|
502
|
+
continue;
|
|
446
503
|
}
|
|
447
504
|
|
|
448
|
-
if (
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
505
|
+
if (choice === 't' && IS_REPLIT) {
|
|
506
|
+
console.log('');
|
|
507
|
+
console.log(' Installing replit-tools...');
|
|
508
|
+
console.log('');
|
|
509
|
+
spawnSync('npx', ['-y', 'data-tools'], { stdio: 'inherit', cwd: CWD });
|
|
510
|
+
console.log('');
|
|
511
|
+
console.log(' ✅ replit-tools installed. You may need to restart your shell.');
|
|
512
|
+
console.log('');
|
|
513
|
+
await ask();
|
|
514
|
+
continue;
|
|
453
515
|
}
|
|
454
516
|
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
if (str === '1') return switchProfile('balanced');
|
|
458
|
-
if (str === '2') return switchProfile('cost-saver');
|
|
459
|
-
if (str === '3') return switchProfile('quality-first');
|
|
460
|
-
if (str === 'r') { render(); return; }
|
|
461
|
-
if (str === 'e') {
|
|
462
|
-
view = 'explain';
|
|
463
|
-
stopRefresh();
|
|
464
|
-
render();
|
|
465
|
-
return;
|
|
466
|
-
}
|
|
467
|
-
if (str === 'b') {
|
|
468
|
-
view = 'budget';
|
|
469
|
-
stopRefresh();
|
|
470
|
-
const profile = loadProfile();
|
|
471
|
-
budgetSession = String(profile.budgets.session_limit_usd);
|
|
472
|
-
budgetDaily = String(profile.budgets.daily_limit_usd);
|
|
473
|
-
budgetField = 'session';
|
|
474
|
-
flash = null;
|
|
475
|
-
render();
|
|
476
|
-
return;
|
|
477
|
-
}
|
|
478
|
-
});
|
|
517
|
+
console.log(` Unknown option: ${choice}`);
|
|
518
|
+
}
|
|
479
519
|
}
|
|
480
520
|
|
|
481
|
-
// ───
|
|
521
|
+
// ─── Non-Interactive Fallback ─────────────────────────────────────────────
|
|
522
|
+
|
|
523
|
+
function renderStatic() {
|
|
524
|
+
const { lines } = renderMenu();
|
|
525
|
+
for (const l of lines) console.log(l);
|
|
526
|
+
}
|
|
482
527
|
|
|
483
|
-
|
|
528
|
+
// ─── Entry ────────────────────────────────────────────────────────────────
|
|
484
529
|
|
|
485
|
-
if (
|
|
486
|
-
|
|
530
|
+
if (process.stdin.isTTY && process.stdout.isTTY && !process.env.CI) {
|
|
531
|
+
mainLoop().catch(err => { console.error(err); process.exit(1); });
|
|
487
532
|
} else {
|
|
488
533
|
renderStatic();
|
|
489
534
|
}
|
package/install.mjs
CHANGED
|
@@ -334,7 +334,7 @@ function install(workspace, env, mode) {
|
|
|
334
334
|
'test-orchestrator.mjs', 'setup-wizard.mjs', 'health-check.mjs',
|
|
335
335
|
'install-git-hooks.mjs', 'session-report.mjs', 'budget-balancer.mjs',
|
|
336
336
|
'gpt-work-dispatcher.mjs', 'profiles.mjs',
|
|
337
|
-
'summary-checkpoint.mjs', 'decision-ledger.mjs',
|
|
337
|
+
'summary-checkpoint.mjs', 'decision-ledger.mjs', 'control-panel.mjs',
|
|
338
338
|
];
|
|
339
339
|
for (const h of HOOKS) cpSync(join(__dirname, 'hooks', h), join(target, 'hooks', h));
|
|
340
340
|
actions.push(`✓ ${HOOKS.length} hook scripts`);
|
|
@@ -381,104 +381,36 @@ function install(workspace, env, mode) {
|
|
|
381
381
|
|
|
382
382
|
// ─── Status Report ──────────────────────────────────────────────────────────
|
|
383
383
|
|
|
384
|
-
function
|
|
385
|
-
|
|
386
|
-
const MODE_EMOJIS = {
|
|
387
|
-
'dual': '🧠',
|
|
388
|
-
'claude-only': '🟠',
|
|
389
|
-
'openai-only': '🟢',
|
|
390
|
-
'detect-only': '🔎',
|
|
391
|
-
};
|
|
392
|
-
|
|
393
|
-
function printReport(env, mode, actions) {
|
|
384
|
+
function printReport(env, mode, actions, isDryRun) {
|
|
394
385
|
const lines = [];
|
|
395
386
|
|
|
396
387
|
lines.push(br('╔', '╗'));
|
|
397
|
-
lines.push(ln(`🧠 Dual-Brain
|
|
388
|
+
lines.push(ln(`🧠 Dual-Brain v${VERSION}`));
|
|
398
389
|
lines.push(sep());
|
|
399
390
|
|
|
400
|
-
|
|
391
|
+
const cAuth = env.claude.authed ? '✅' : env.claude.installed ? '⚠️' : '❌';
|
|
392
|
+
const xAuth = env.codex.authed ? '✅' : env.codex.installed ? '⚠️' : '❌';
|
|
393
|
+
lines.push(ln(` 🟠 Claude ${cAuth} 🟢 Codex ${xAuth}`));
|
|
394
|
+
|
|
401
395
|
if (env.isReplit) {
|
|
402
|
-
lines.push(ln(` 🌀
|
|
403
|
-
} else {
|
|
404
|
-
lines.push(ln(' Platform: standalone'));
|
|
396
|
+
lines.push(ln(` 🌀 Replit${env.hasReplitTools ? ' + replit-tools' : ''}`));
|
|
405
397
|
}
|
|
406
398
|
|
|
407
|
-
const cVer = env.claude.version ? ` ${env.claude.version}` : '';
|
|
408
|
-
const cAuth = env.claude.authed ? '✅ authenticated' : env.claude.installed ? '⚠️ login needed' : '❌ not found';
|
|
409
|
-
lines.push(ln(` 🟠 Claude: ${cAuth}${cVer}`));
|
|
410
|
-
|
|
411
|
-
const xVer = env.codex.version ? ` ${env.codex.version}` : '';
|
|
412
|
-
const xAuth = env.codex.authed ? '✅ authenticated' : env.codex.installed ? '⚠️ login needed' : '❌ not found';
|
|
413
|
-
lines.push(ln(` 🟢 Codex: ${xAuth}${xVer}`));
|
|
414
|
-
|
|
415
|
-
lines.push(sep());
|
|
416
|
-
lines.push(ln(`${MODE_EMOJIS[mode.mode] || '🧠'} Mode: ${MODE_LABELS[mode.mode]}`));
|
|
417
|
-
|
|
418
399
|
if (actions) {
|
|
419
400
|
lines.push(sep());
|
|
420
|
-
lines.push(ln('📝 Installed'));
|
|
421
401
|
for (const a of actions) lines.push(ln(` ${a}`));
|
|
422
|
-
}
|
|
423
|
-
|
|
424
|
-
const needsAction = !env.claude.authed || !env.codex.authed;
|
|
425
|
-
if (needsAction && mode.mode !== 'dual') {
|
|
426
402
|
lines.push(sep());
|
|
427
|
-
lines.push(ln('
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
}
|
|
431
|
-
if (!env.claude.authed) {
|
|
432
|
-
lines.push(ln(' claude login'));
|
|
433
|
-
}
|
|
434
|
-
if (!env.codex.installed) {
|
|
435
|
-
lines.push(ln(' npm i -g @openai/codex'));
|
|
436
|
-
}
|
|
437
|
-
if (!env.codex.authed && env.codex.installed) {
|
|
438
|
-
lines.push(ln(' codex login'));
|
|
439
|
-
}
|
|
440
|
-
lines.push(ln(' Then run: npx dual-brain'));
|
|
441
|
-
}
|
|
442
|
-
|
|
443
|
-
lines.push(sep());
|
|
444
|
-
if (actions) {
|
|
445
|
-
lines.push(ln(mode.mode === 'dual'
|
|
446
|
-
? '✅ Ready: both providers active, no restart needed'
|
|
447
|
-
: '✅ Ready: hooks active, run commands above for full power'));
|
|
448
|
-
} else {
|
|
403
|
+
lines.push(ln('✅ Installed — launching session manager...'));
|
|
404
|
+
} else if (isDryRun) {
|
|
405
|
+
lines.push(sep());
|
|
449
406
|
lines.push(ln('Dry run — no files written'));
|
|
450
407
|
}
|
|
408
|
+
|
|
451
409
|
lines.push(br('╚', '╝'));
|
|
452
410
|
|
|
453
411
|
console.log('');
|
|
454
412
|
for (const l of lines) console.log(` ${l}`);
|
|
455
413
|
console.log('');
|
|
456
|
-
|
|
457
|
-
if (actions) {
|
|
458
|
-
console.log(' 🧭 What changed:');
|
|
459
|
-
console.log(' Every Claude Code session now auto-routes agent work by');
|
|
460
|
-
console.log(' complexity — cheap models for search, mid-tier for execution,');
|
|
461
|
-
console.log(' best models for thinking. Cost is tracked automatically.');
|
|
462
|
-
if (mode.mode === 'dual') {
|
|
463
|
-
console.log(' 🧠 Both Claude and GPT are available as work providers.');
|
|
464
|
-
}
|
|
465
|
-
console.log('');
|
|
466
|
-
console.log(' ⌨️ Open the control panel:');
|
|
467
|
-
console.log(` ${cmd('npx dual-brain status')}`);
|
|
468
|
-
console.log('');
|
|
469
|
-
console.log(' 🩺 In-session tools (ask Claude to run):');
|
|
470
|
-
console.log(' node .claude/hooks/health-check.mjs # verify setup');
|
|
471
|
-
console.log(' node .claude/hooks/cost-report.mjs # see activity');
|
|
472
|
-
console.log(' node .claude/hooks/decision-ledger.mjs # routing insights');
|
|
473
|
-
if (mode.openaiEnabled) {
|
|
474
|
-
console.log(' node .claude/hooks/dual-brain-review.mjs # GPT code review');
|
|
475
|
-
}
|
|
476
|
-
console.log('');
|
|
477
|
-
console.log(' ⚙️ Customize:');
|
|
478
|
-
console.log(' .claude/review-rules.md # your project\'s review rules');
|
|
479
|
-
console.log(' .claude/orchestrator.json # routing, budgets, tiers');
|
|
480
|
-
console.log('');
|
|
481
|
-
}
|
|
482
414
|
}
|
|
483
415
|
|
|
484
416
|
// ─── Profile System ────────────────────────────────────────────────────────
|
|
@@ -539,70 +471,13 @@ function saveProfile(workspace, name, customOverrides) {
|
|
|
539
471
|
|
|
540
472
|
// ─── Subcommand: status ────────────────────────────────────────────────────
|
|
541
473
|
|
|
542
|
-
function
|
|
543
|
-
const
|
|
544
|
-
const
|
|
545
|
-
const
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
lines.push(br('╔', '╗'));
|
|
550
|
-
lines.push(ln(`Dual-Brain Status — v${VERSION}`));
|
|
551
|
-
lines.push(sep());
|
|
552
|
-
|
|
553
|
-
lines.push(ln(`Mode: ${MODE_LABELS[mode.mode]}`));
|
|
554
|
-
lines.push(ln(`Profile: ${profile.name}`));
|
|
555
|
-
lines.push(ln(` ${PROFILES[profile.name]?.description || ''}`));
|
|
556
|
-
if (profile.switched_at) {
|
|
557
|
-
lines.push(ln(` Set: ${profile.switched_at.slice(0, 16).replace('T', ' ')}`));
|
|
558
|
-
}
|
|
559
|
-
|
|
560
|
-
lines.push(sep());
|
|
561
|
-
|
|
562
|
-
lines.push(ln('Budget Limits'));
|
|
563
|
-
lines.push(ln(` Session: warn $${profile.budgets.session_warn_usd} / limit $${profile.budgets.session_limit_usd}`));
|
|
564
|
-
lines.push(ln(` Daily: warn $${profile.budgets.daily_warn_usd} / limit $${profile.budgets.daily_limit_usd}`));
|
|
565
|
-
|
|
566
|
-
lines.push(sep());
|
|
567
|
-
|
|
568
|
-
lines.push(ln('Providers'));
|
|
569
|
-
const cAuth = env.claude.authed ? 'authenticated' : 'not authenticated';
|
|
570
|
-
const xAuth = env.codex.authed ? 'authenticated' : env.codex.installed ? 'not authenticated' : 'not found';
|
|
571
|
-
lines.push(ln(` Claude: ${statusIcon(env.claude.authed)} ${cAuth}`));
|
|
572
|
-
lines.push(ln(` Codex: ${statusIcon(env.codex.authed)} ${xAuth}`));
|
|
573
|
-
|
|
574
|
-
lines.push(sep());
|
|
575
|
-
|
|
576
|
-
lines.push(ln('Quality Gate'));
|
|
577
|
-
lines.push(ln(` Reviews from: ${profile.quality_gate.sensitivity_floor} risk+`));
|
|
578
|
-
lines.push(ln(` Dual-brain at: ${profile.quality_gate.dual_brain_minimum} risk+`));
|
|
579
|
-
|
|
580
|
-
const balancer = join(workspace, '.claude', 'hooks', 'budget-balancer.mjs');
|
|
581
|
-
if (existsSync(balancer)) {
|
|
582
|
-
const proc = run(process.execPath, [balancer]);
|
|
583
|
-
if (proc.status === 0 && proc.stdout.trim()) {
|
|
584
|
-
lines.push(sep());
|
|
585
|
-
lines.push(ln('Provider Pressure (5hr rolling)'));
|
|
586
|
-
for (const l of proc.stdout.trim().split('\n')) {
|
|
587
|
-
if (l.includes('█') || l.includes('░') || l.includes('Recommendation')) {
|
|
588
|
-
const cleaned = l.replace(/[║╔╗╠╣╚╝═]/g, '').trim();
|
|
589
|
-
if (cleaned) lines.push(ln(` ${cleaned}`));
|
|
590
|
-
}
|
|
591
|
-
}
|
|
592
|
-
}
|
|
593
|
-
}
|
|
594
|
-
|
|
595
|
-
lines.push(br('╚', '╝'));
|
|
596
|
-
|
|
597
|
-
console.log('');
|
|
598
|
-
for (const l of lines) console.log(` ${l}`);
|
|
599
|
-
console.log('');
|
|
600
|
-
|
|
601
|
-
if (IS_REPLIT) {
|
|
602
|
-
console.log(' Quick actions (paste into shell):');
|
|
603
|
-
console.log(` ${cmd('npx dual-brain mode cost-saver')} # switch profile`);
|
|
604
|
-
console.log(` ${cmd('npx dual-brain budget 8 25')} # set limits`);
|
|
605
|
-
console.log('');
|
|
474
|
+
function launchPanel() {
|
|
475
|
+
const panelPath = join(resolve(process.cwd()), '.claude', 'hooks', 'control-panel.mjs');
|
|
476
|
+
const pkgPanel = join(__dirname, 'hooks', 'control-panel.mjs');
|
|
477
|
+
const panel = existsSync(panelPath) ? panelPath : existsSync(pkgPanel) ? pkgPanel : null;
|
|
478
|
+
if (panel) {
|
|
479
|
+
const { status } = spawnSync(process.execPath, [panel], { stdio: 'inherit' });
|
|
480
|
+
process.exit(status || 0);
|
|
606
481
|
}
|
|
607
482
|
}
|
|
608
483
|
|
|
@@ -797,15 +672,7 @@ function cmdExplain() {
|
|
|
797
672
|
|
|
798
673
|
function main() {
|
|
799
674
|
if (subcommand === 'status') {
|
|
800
|
-
|
|
801
|
-
const panelPath = join(resolve(process.cwd()), '.claude', 'hooks', 'control-panel.mjs');
|
|
802
|
-
const pkgPanel = join(__dirname, 'hooks', 'control-panel.mjs');
|
|
803
|
-
const panel = existsSync(panelPath) ? panelPath : existsSync(pkgPanel) ? pkgPanel : null;
|
|
804
|
-
if (panel && process.stdin.isTTY && process.stdout.isTTY && !process.env.CI) {
|
|
805
|
-
const { status } = spawnSync(process.execPath, [panel], { stdio: 'inherit' });
|
|
806
|
-
process.exit(status || 0);
|
|
807
|
-
}
|
|
808
|
-
cmdStatus();
|
|
675
|
+
launchPanel();
|
|
809
676
|
return;
|
|
810
677
|
}
|
|
811
678
|
if (subcommand === 'mode') { cmdMode(); return; }
|
|
@@ -819,13 +686,29 @@ function main() {
|
|
|
819
686
|
if (jsonOut) {
|
|
820
687
|
console.log(JSON.stringify({ version: VERSION, env, mode }, null, 2));
|
|
821
688
|
} else {
|
|
822
|
-
printReport(env, mode, null);
|
|
689
|
+
printReport(env, mode, null, true);
|
|
823
690
|
}
|
|
824
691
|
process.exit(0);
|
|
825
692
|
}
|
|
826
693
|
|
|
694
|
+
// Check for replit-tools on Replit
|
|
695
|
+
if (env.isReplit && !env.hasReplitTools) {
|
|
696
|
+
console.log('');
|
|
697
|
+
console.log(' ⚠️ replit-tools not found — recommended for Replit environments.');
|
|
698
|
+
console.log(' Dual-brain works best alongside replit-tools for persistent auth,');
|
|
699
|
+
console.log(' session management, and shell integration.');
|
|
700
|
+
console.log('');
|
|
701
|
+
console.log(` Install: ${cmd('npx -y data-tools')}`);
|
|
702
|
+
console.log('');
|
|
703
|
+
}
|
|
704
|
+
|
|
827
705
|
const actions = install(env.workspace, env, mode);
|
|
828
706
|
printReport(env, mode, actions);
|
|
707
|
+
|
|
708
|
+
// After install, launch the session manager (interactive TTY only)
|
|
709
|
+
if (process.stdin.isTTY && process.stdout.isTTY && !process.env.CI) {
|
|
710
|
+
launchPanel();
|
|
711
|
+
}
|
|
829
712
|
}
|
|
830
713
|
|
|
831
714
|
main();
|
package/package.json
CHANGED