dual-brain 3.3.0 → 3.5.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 +433 -343
- package/install.mjs +38 -154
- package/package.json +1 -1
package/hooks/control-panel.mjs
CHANGED
|
@@ -1,48 +1,48 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
/**
|
|
3
|
-
* control-panel.mjs —
|
|
3
|
+
* control-panel.mjs — Session launcher for Dual-Brain.
|
|
4
4
|
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
* Falls back to static emoji output when not in a TTY.
|
|
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.
|
|
9
8
|
*/
|
|
10
9
|
|
|
11
10
|
import readline from 'readline';
|
|
12
|
-
import { existsSync, readFileSync, renameSync, writeFileSync } from 'fs';
|
|
11
|
+
import { existsSync, readFileSync, readdirSync, statSync, renameSync, writeFileSync } from 'fs';
|
|
13
12
|
import { dirname, join } from 'path';
|
|
14
13
|
import { fileURLToPath } from 'url';
|
|
15
14
|
import { spawnSync } from 'child_process';
|
|
16
15
|
|
|
17
16
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
18
|
-
const CONFIG_FILE = join(__dirname, '..', 'orchestrator.json');
|
|
19
17
|
const PROFILE_FILE = join(__dirname, '..', 'dual-brain.profile.json');
|
|
18
|
+
const LAUNCHED_MARKER = join(__dirname, '..', '.launched');
|
|
20
19
|
const VERSION = (() => {
|
|
21
|
-
try { return JSON.parse(readFileSync(join(__dirname, '..', '..', 'dual-brain', 'package.json'), 'utf8')).version; } catch {}
|
|
22
20
|
try { return JSON.parse(readFileSync(join(__dirname, '..', 'package.json'), 'utf8')).version; } catch {}
|
|
23
21
|
return '?';
|
|
24
22
|
})();
|
|
25
23
|
|
|
24
|
+
const IS_REPLIT = !!(process.env.REPL_ID || process.env.REPL_SLUG);
|
|
25
|
+
const HOME = process.env.HOME || process.env.USERPROFILE || '';
|
|
26
|
+
const CWD = process.cwd();
|
|
27
|
+
|
|
26
28
|
// ─── ANSI ──────────────────────────────────────────────────────────────────
|
|
27
29
|
|
|
28
|
-
const
|
|
29
|
-
const
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
};
|
|
38
|
-
const c = (code, s) => color ? `${code}${s}${A.reset}` : s;
|
|
30
|
+
const noColor = !!process.env.NO_COLOR;
|
|
31
|
+
const e = (code, s) => noColor ? s : `\x1b[${code}m${s}\x1b[0m`;
|
|
32
|
+
const bold = s => e('1', s);
|
|
33
|
+
const dim = s => e('2', s);
|
|
34
|
+
const cyan = s => e('36', s);
|
|
35
|
+
const green = s => e('32', s);
|
|
36
|
+
const yellow = s => e('33', s);
|
|
37
|
+
const orange = s => e('1;38;5;208', s);
|
|
38
|
+
const blue = s => e('1;38;5;33', s);
|
|
39
39
|
|
|
40
40
|
// ─── Profiles ──────────────────────────────────────────────────────────────
|
|
41
41
|
|
|
42
42
|
const PROFILES = {
|
|
43
|
-
balanced: { emoji: '⚖️',
|
|
44
|
-
'cost-saver': { emoji: '💸',
|
|
45
|
-
'quality-first': { emoji: '💎',
|
|
43
|
+
balanced: { emoji: '⚖️', uiLabel: 'Default', desc: 'Auto-routes by complexity, standard alerts' },
|
|
44
|
+
'cost-saver': { emoji: '💸', uiLabel: 'Cost-saver', desc: 'Prefers cheaper models, tighter alerts' },
|
|
45
|
+
'quality-first': { emoji: '💎', uiLabel: 'Quality-first', desc: 'Uses best models, dual-brain for medium+ risk' },
|
|
46
46
|
};
|
|
47
47
|
|
|
48
48
|
const PROFILE_BUDGETS = {
|
|
@@ -51,31 +51,14 @@ const PROFILE_BUDGETS = {
|
|
|
51
51
|
'quality-first': { session_warn_usd: 15, session_limit_usd: 30, daily_warn_usd: 50, daily_limit_usd: 100 },
|
|
52
52
|
};
|
|
53
53
|
|
|
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
54
|
function loadProfile() {
|
|
67
55
|
try {
|
|
68
56
|
const data = JSON.parse(readFileSync(PROFILE_FILE, 'utf8'));
|
|
69
57
|
const name = data.active && PROFILES[data.active] ? data.active : 'balanced';
|
|
70
58
|
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
|
-
};
|
|
59
|
+
return { name, budgets: { ...PROFILE_BUDGETS[name], ...custom.budgets }, hasCustomBudget: !!custom.budgets };
|
|
77
60
|
} catch {
|
|
78
|
-
return { name: 'balanced', budgets: PROFILE_BUDGETS.balanced,
|
|
61
|
+
return { name: 'balanced', budgets: PROFILE_BUDGETS.balanced, hasCustomBudget: false };
|
|
79
62
|
}
|
|
80
63
|
}
|
|
81
64
|
|
|
@@ -87,29 +70,38 @@ function saveProfile(name, customOverrides) {
|
|
|
87
70
|
renameSync(tmp, PROFILE_FILE);
|
|
88
71
|
}
|
|
89
72
|
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
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 {}
|
|
104
90
|
}
|
|
105
91
|
|
|
92
|
+
// ─── Provider Detection ───────────────────────────────────────────────────
|
|
93
|
+
|
|
106
94
|
function detectProviders() {
|
|
107
|
-
const claude = {
|
|
108
|
-
const codex = {
|
|
95
|
+
const claude = { installed: false, authed: false };
|
|
96
|
+
const codex = { installed: false, authed: false };
|
|
97
|
+
|
|
98
|
+
const claudeCheck = spawnSync('which', ['claude'], { encoding: 'utf8', stdio: 'pipe', timeout: 3000 });
|
|
99
|
+
claude.installed = claudeCheck.status === 0 && !!claudeCheck.stdout.trim();
|
|
109
100
|
|
|
110
101
|
const credPaths = [
|
|
111
|
-
join(
|
|
112
|
-
join(
|
|
102
|
+
join(HOME, '.claude', '.credentials.json'),
|
|
103
|
+
join(HOME, '.claude', 'credentials.json'),
|
|
104
|
+
join(CWD, '.replit-tools', '.claude-persistent', '.credentials.json'),
|
|
113
105
|
];
|
|
114
106
|
for (const p of credPaths) {
|
|
115
107
|
try {
|
|
@@ -117,16 +109,16 @@ function detectProviders() {
|
|
|
117
109
|
if (cred.claudeAiOauth || cred.apiKey || cred.oauth_token) { claude.authed = true; break; }
|
|
118
110
|
} catch {}
|
|
119
111
|
}
|
|
120
|
-
if (!claude.authed) {
|
|
112
|
+
if (!claude.authed && claude.installed) {
|
|
121
113
|
const r = spawnSync('claude', ['auth', 'status'], { encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'], timeout: 5000 });
|
|
122
114
|
const out = ((r.stdout || '') + (r.stderr || '')).toLowerCase();
|
|
123
115
|
if (out.includes('logged in') || out.includes('authenticated')) claude.authed = true;
|
|
124
116
|
}
|
|
125
117
|
|
|
126
|
-
const
|
|
127
|
-
if (
|
|
118
|
+
const codexCheck = spawnSync('which', ['codex'], { encoding: 'utf8', stdio: 'pipe', timeout: 3000 });
|
|
119
|
+
if (codexCheck.status === 0 && codexCheck.stdout.trim()) {
|
|
128
120
|
codex.installed = true;
|
|
129
|
-
const login = spawnSync(
|
|
121
|
+
const login = spawnSync(codexCheck.stdout.trim(), ['login', 'status'], { encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'], timeout: 5000 });
|
|
130
122
|
const out = ((login.stdout || '') + (login.stderr || '')).toLowerCase();
|
|
131
123
|
if (login.status === 0 || out.includes('logged in') || out.includes('authenticated')) codex.authed = true;
|
|
132
124
|
}
|
|
@@ -134,356 +126,454 @@ function detectProviders() {
|
|
|
134
126
|
return { claude, codex };
|
|
135
127
|
}
|
|
136
128
|
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
129
|
+
// ─── Session Discovery ────────────────────────────────────────────────────
|
|
130
|
+
|
|
131
|
+
function getRecentSessions() {
|
|
132
|
+
const cutoff = Date.now() - 24 * 60 * 60 * 1000;
|
|
133
|
+
const sessions = new Map();
|
|
134
|
+
|
|
135
|
+
const isRealPrompt = (txt) => {
|
|
136
|
+
if (!txt) return false;
|
|
137
|
+
const t = txt.trim();
|
|
138
|
+
if (!t) return false;
|
|
139
|
+
if (/^[✅❌📦🔗⚠️🚀🎉🔧📝]/.test(t)) return false;
|
|
140
|
+
if (/Claude (history|binary|versions) symlink/.test(t)) return false;
|
|
141
|
+
if (t.startsWith('# AGENTS.md')) return false;
|
|
142
|
+
return true;
|
|
143
|
+
};
|
|
144
|
+
|
|
145
|
+
const historyFile = join(HOME, '.claude', 'history.jsonl');
|
|
146
|
+
if (existsSync(historyFile)) {
|
|
147
|
+
try {
|
|
148
|
+
const lines = readFileSync(historyFile, 'utf8').trim().split('\n');
|
|
149
|
+
const entries = [];
|
|
150
|
+
for (const line of lines) {
|
|
151
|
+
try { const j = JSON.parse(line); if (j.sessionId && j.timestamp) entries.push(j); } catch {}
|
|
152
152
|
}
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
153
|
+
entries.sort((a, b) => a.timestamp - b.timestamp);
|
|
154
|
+
for (const j of entries) {
|
|
155
|
+
const key = 'claude:' + j.sessionId;
|
|
156
|
+
if (!sessions.has(key)) {
|
|
157
|
+
sessions.set(key, { tool: 'claude', id: j.sessionId, firstSeen: j.timestamp, lastSeen: j.timestamp, firstPrompt: '' });
|
|
158
|
+
}
|
|
159
|
+
const s = sessions.get(key);
|
|
160
|
+
if (j.timestamp < s.firstSeen) s.firstSeen = j.timestamp;
|
|
161
|
+
if (j.timestamp > s.lastSeen) s.lastSeen = j.timestamp;
|
|
162
|
+
if (!s.firstPrompt && isRealPrompt(j.display)) s.firstPrompt = j.display;
|
|
163
|
+
}
|
|
164
|
+
for (const [key, s] of sessions) {
|
|
165
|
+
if (s.tool === 'claude' && !s.firstPrompt) sessions.delete(key);
|
|
166
|
+
}
|
|
167
|
+
} catch {}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
const codexDir = join(HOME, '.codex', 'sessions');
|
|
171
|
+
if (existsSync(codexDir)) {
|
|
172
|
+
const walk = (dir) => {
|
|
173
|
+
let results = [];
|
|
174
|
+
try {
|
|
175
|
+
for (const entry of readdirSync(dir, { withFileTypes: true })) {
|
|
176
|
+
const full = join(dir, entry.name);
|
|
177
|
+
if (entry.isDirectory()) results = results.concat(walk(full));
|
|
178
|
+
else if (entry.isFile() && entry.name.endsWith('.jsonl')) results.push(full);
|
|
179
|
+
}
|
|
180
|
+
} catch {}
|
|
181
|
+
return results;
|
|
159
182
|
};
|
|
183
|
+
for (const f of walk(codexDir)) {
|
|
184
|
+
try {
|
|
185
|
+
const stat = statSync(f);
|
|
186
|
+
if (stat.mtimeMs < cutoff) continue;
|
|
187
|
+
const content = readFileSync(f, 'utf8');
|
|
188
|
+
const lns = content.trim().split('\n');
|
|
189
|
+
if (!lns.length) continue;
|
|
190
|
+
const meta = JSON.parse(lns[0]);
|
|
191
|
+
if (meta.type !== 'session_meta' || !meta.payload) continue;
|
|
192
|
+
if (meta.payload.cwd !== CWD) continue;
|
|
193
|
+
const id = meta.payload.id;
|
|
194
|
+
const firstTs = Date.parse(meta.payload.timestamp || meta.timestamp);
|
|
195
|
+
let lastTs = firstTs;
|
|
196
|
+
let firstPrompt = '';
|
|
197
|
+
let realMsgCount = 0;
|
|
198
|
+
for (const ln of lns) {
|
|
199
|
+
try {
|
|
200
|
+
const j = JSON.parse(ln);
|
|
201
|
+
if (j.timestamp) lastTs = Math.max(lastTs, Date.parse(j.timestamp));
|
|
202
|
+
if (j.type === 'event_msg' && j.payload?.type === 'user_message') {
|
|
203
|
+
const text = (j.payload.message || '').trim();
|
|
204
|
+
if (text) { if (!firstPrompt) firstPrompt = text; realMsgCount++; }
|
|
205
|
+
}
|
|
206
|
+
} catch {}
|
|
207
|
+
}
|
|
208
|
+
if (realMsgCount === 0 || !firstPrompt) continue;
|
|
209
|
+
if (/^(you are |you're |\*\*role\*\*|<role>|## role)/i.test(firstPrompt)) continue;
|
|
210
|
+
if (realMsgCount === 1 && firstPrompt.length > 500) continue;
|
|
211
|
+
sessions.set('codex:' + id, { tool: 'codex', id, firstSeen: firstTs, lastSeen: lastTs, firstPrompt });
|
|
212
|
+
} catch {}
|
|
213
|
+
}
|
|
160
214
|
}
|
|
215
|
+
|
|
216
|
+
return Array.from(sessions.values())
|
|
217
|
+
.filter(s => (s.lastSeen || 0) >= cutoff)
|
|
218
|
+
.sort((a, b) => (b.lastSeen || 0) - (a.lastSeen || 0))
|
|
219
|
+
.slice(0, 9);
|
|
161
220
|
}
|
|
162
221
|
|
|
163
|
-
function
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
222
|
+
function timeAgo(ts) {
|
|
223
|
+
const mins = Math.round((Date.now() - ts) / 60000);
|
|
224
|
+
if (mins < 1) return 'just now';
|
|
225
|
+
if (mins < 60) return mins + 'm ago';
|
|
226
|
+
const h = Math.round(mins / 60);
|
|
227
|
+
return h + 'h ago';
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
function snippet(s, n = 15) {
|
|
231
|
+
const clean = (s || '').replace(/\s+/g, ' ').trim();
|
|
232
|
+
return clean.length > n ? clean.slice(0, n - 1) + '…' : clean;
|
|
169
233
|
}
|
|
170
234
|
|
|
171
|
-
function
|
|
172
|
-
|
|
173
|
-
const logFile = join(__dirname, `usage-${today}.jsonl`);
|
|
174
|
-
if (!existsSync(logFile)) return null;
|
|
235
|
+
function countRunning() {
|
|
236
|
+
let claude = 0, codex = 0;
|
|
175
237
|
try {
|
|
176
|
-
const
|
|
177
|
-
|
|
178
|
-
try {
|
|
179
|
-
const e = JSON.parse(lines[i]);
|
|
180
|
-
if (e.type === 'tier_recommendation') return e;
|
|
181
|
-
} catch {}
|
|
182
|
-
}
|
|
238
|
+
const r = spawnSync('pgrep', ['-x', 'claude'], { encoding: 'utf8', stdio: 'pipe', timeout: 2000 });
|
|
239
|
+
claude = (r.stdout || '').trim().split('\n').filter(Boolean).length;
|
|
183
240
|
} catch {}
|
|
184
|
-
|
|
241
|
+
try {
|
|
242
|
+
const r = spawnSync('pgrep', ['-x', 'codex'], { encoding: 'utf8', stdio: 'pipe', timeout: 2000 });
|
|
243
|
+
codex = (r.stdout || '').trim().split('\n').filter(Boolean).length;
|
|
244
|
+
} catch {}
|
|
245
|
+
return { claude, codex };
|
|
185
246
|
}
|
|
186
247
|
|
|
187
|
-
// ───
|
|
188
|
-
|
|
189
|
-
function
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
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}`;
|
|
248
|
+
// ─── Cost Alert Label ─────────────────────────────────────────────────────
|
|
249
|
+
|
|
250
|
+
function costAlertLabel(profile) {
|
|
251
|
+
if (profile.hasCustomBudget) return 'Custom';
|
|
252
|
+
if (profile.name === 'balanced') return 'Default';
|
|
253
|
+
if (profile.name === 'cost-saver') return 'Tight';
|
|
254
|
+
if (profile.name === 'quality-first') return 'Relaxed';
|
|
255
|
+
return 'Default';
|
|
200
256
|
}
|
|
201
257
|
|
|
202
|
-
|
|
203
|
-
const { profile, providers, pressure, cost, flash } = state;
|
|
204
|
-
const pf = PROFILES[profile.name];
|
|
205
|
-
const time = new Date().toLocaleTimeString('en-US', { hour12: false });
|
|
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';
|
|
258
|
+
// ─── Menu Renderers ───────────────────────────────────────────────────────
|
|
209
259
|
|
|
260
|
+
function renderFirstRunMenu(providers) {
|
|
210
261
|
const lines = [];
|
|
262
|
+
|
|
211
263
|
lines.push('');
|
|
212
|
-
lines.push(
|
|
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}+`);
|
|
264
|
+
lines.push(` 🧠 ${bold(`Dual-Brain v${VERSION}`)}`);
|
|
218
265
|
lines.push('');
|
|
219
266
|
|
|
220
|
-
|
|
221
|
-
const
|
|
222
|
-
const
|
|
223
|
-
lines.push(`
|
|
224
|
-
|
|
267
|
+
// Provider status
|
|
268
|
+
const cStat = providers.claude.authed ? '✅' : providers.claude.installed ? '⚠️' : '❌';
|
|
269
|
+
const xStat = providers.codex.authed ? '✅' : providers.codex.installed ? '⚠️' : '❌';
|
|
270
|
+
lines.push(` 🟠 Claude ${cStat} 🟢 Codex ${xStat}`);
|
|
271
|
+
|
|
272
|
+
if (providers.claude.authed && providers.codex.authed) {
|
|
273
|
+
lines.push(` ${green('Both providers ready — full dual-brain mode')}`);
|
|
274
|
+
} else if (providers.claude.authed) {
|
|
275
|
+
lines.push(` ${dim('Claude ready. Add Codex for dual-brain features.')}`);
|
|
276
|
+
} else if (!providers.claude.installed) {
|
|
277
|
+
lines.push(` ${yellow('Claude not found — needed to start.')}`);
|
|
278
|
+
} else {
|
|
279
|
+
lines.push(` ${yellow('Claude needs login to start.')}`);
|
|
280
|
+
}
|
|
281
|
+
|
|
225
282
|
lines.push('');
|
|
226
283
|
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
lines.push(`
|
|
284
|
+
// Auth actions if needed
|
|
285
|
+
if (!providers.claude.authed || !providers.codex.authed) {
|
|
286
|
+
if (!providers.claude.installed) {
|
|
287
|
+
lines.push(` ${dim('Install Claude:')} ${cyan('curl -fsSL https://claude.ai/install.sh | sh')}`);
|
|
288
|
+
}
|
|
289
|
+
if (!providers.claude.authed && providers.claude.installed) {
|
|
290
|
+
lines.push(` ${bold('[j]')} Sign in to Claude`);
|
|
234
291
|
}
|
|
235
|
-
if (
|
|
292
|
+
if (!providers.codex.installed) {
|
|
293
|
+
lines.push(` ${dim('Install Codex:')} ${cyan('npm i -g @openai/codex')}`);
|
|
294
|
+
} else if (!providers.codex.authed) {
|
|
295
|
+
lines.push(` ${bold('[k]')} Sign in to Codex ${dim('(optional — enables GPT collaboration)')}`);
|
|
296
|
+
}
|
|
297
|
+
lines.push('');
|
|
236
298
|
}
|
|
237
|
-
lines.push('');
|
|
238
299
|
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
lines.push('');
|
|
300
|
+
// Replit-tools check
|
|
301
|
+
if (IS_REPLIT && !existsSync(join(CWD, '.replit-tools'))) {
|
|
302
|
+
lines.push(` ${bold('[t]')} Install replit-tools ${dim('(recommended for Replit)')}`);
|
|
242
303
|
}
|
|
243
304
|
|
|
244
|
-
|
|
245
|
-
lines.push(`
|
|
305
|
+
// Primary actions
|
|
306
|
+
lines.push(` ${bold('[n]')} Start new session`);
|
|
307
|
+
lines.push(` ${bold('[s]')} Skip — just shell`);
|
|
246
308
|
lines.push('');
|
|
247
309
|
|
|
248
|
-
return lines
|
|
310
|
+
return lines;
|
|
249
311
|
}
|
|
250
312
|
|
|
251
|
-
function
|
|
313
|
+
function renderReturningMenu(providers, sessions) {
|
|
314
|
+
const profile = loadProfile();
|
|
315
|
+
const pf = PROFILES[profile.name];
|
|
316
|
+
const running = countRunning();
|
|
252
317
|
const lines = [];
|
|
318
|
+
|
|
253
319
|
lines.push('');
|
|
254
|
-
lines.push(
|
|
255
|
-
lines.push(
|
|
320
|
+
lines.push(` 🧠 ${bold(`Dual-Brain v${VERSION}`)}`);
|
|
321
|
+
lines.push('');
|
|
322
|
+
|
|
323
|
+
// Compact provider + mode line
|
|
324
|
+
const cStat = providers.claude.authed ? '✅' : '⚠️';
|
|
325
|
+
const xStat = providers.codex.authed ? '✅' : providers.codex.installed ? '⚠️' : '❌';
|
|
326
|
+
lines.push(` 🟠 Claude ${cStat} 🟢 Codex ${xStat} ${pf.emoji} ${bold(pf.uiLabel)}`);
|
|
256
327
|
|
|
257
|
-
|
|
258
|
-
|
|
328
|
+
// Recent sessions
|
|
329
|
+
if (sessions.length > 0) {
|
|
259
330
|
lines.push('');
|
|
260
|
-
lines.push(
|
|
261
|
-
|
|
331
|
+
lines.push(` ${bold('Recent (last 24h):')}`);
|
|
332
|
+
for (let i = 0; i < sessions.length; i++) {
|
|
333
|
+
const s = sessions[i];
|
|
334
|
+
const num = String(i + 1);
|
|
335
|
+
const toolLabel = s.tool === 'codex' ? orange('cdx') : blue('cld');
|
|
336
|
+
const ago = timeAgo(s.lastSeen).padEnd(9);
|
|
337
|
+
lines.push(` ${bold('[' + num + ']')} ${toolLabel} ${dim(ago)} ${snippet(s.firstPrompt)}`);
|
|
338
|
+
}
|
|
262
339
|
}
|
|
263
340
|
|
|
264
|
-
const time = decision.timestamp?.slice(11, 19) || '??:??:??';
|
|
265
|
-
const followed = decision.followed;
|
|
266
|
-
lines.push(` 🕐 Time ${time}`);
|
|
267
|
-
lines.push(` 🔎 Detected ${decision.detected_tier || 'unknown'} tier`);
|
|
268
|
-
lines.push(` 🧠 Recommended ${decision.recommended_model || 'unknown'}`);
|
|
269
|
-
lines.push(` 🎯 Actual ${decision.actual_model || 'unknown'}`);
|
|
270
|
-
lines.push(` ${followed ? '✅' : '⚠️'} Followed ${followed ? 'yes' : 'no'}`);
|
|
271
|
-
lines.push(` 🎛️ Profile ${profile.name}`);
|
|
272
341
|
lines.push('');
|
|
273
342
|
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
}
|
|
343
|
+
const runParts = [];
|
|
344
|
+
if (running.claude > 0) runParts.push(`${running.claude} claude`);
|
|
345
|
+
if (running.codex > 0) runParts.push(`${running.codex} codex`);
|
|
346
|
+
if (runParts.length > 0) lines.push(` ${dim('(' + runParts.join(', ') + ' running)')}`);
|
|
279
347
|
|
|
280
|
-
|
|
281
|
-
lines.push(
|
|
282
|
-
|
|
283
|
-
}
|
|
348
|
+
// Menu options
|
|
349
|
+
lines.push(` ${bold('[c]')} Continue last session`);
|
|
350
|
+
if (sessions.length > 0) lines.push(` ${bold('[1-9]')} Resume numbered above`);
|
|
351
|
+
lines.push(` ${bold('[n]')} New session`);
|
|
352
|
+
lines.push(` ${bold('[p]')} Mode: ${dim(pf.uiLabel)}`);
|
|
353
|
+
lines.push(` ${bold('[b]')} Cost alerts: ${dim(costAlertLabel(profile))}`);
|
|
284
354
|
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
lines.push('');
|
|
288
|
-
lines.push(
|
|
289
|
-
lines.push(c(A.dim, ' ' + '─'.repeat(40)));
|
|
290
|
-
lines.push('');
|
|
355
|
+
// Auth if needed
|
|
356
|
+
if (!providers.claude.authed) lines.push(` ${bold('[j]')} Sign in to Claude`);
|
|
357
|
+
if (providers.codex.installed && !providers.codex.authed) lines.push(` ${bold('[k]')} Sign in to Codex`);
|
|
358
|
+
if (IS_REPLIT && !existsSync(join(CWD, '.replit-tools'))) lines.push(` ${bold('[t]')} Install replit-tools`);
|
|
291
359
|
|
|
292
|
-
|
|
293
|
-
const dCursor = field === 'daily' ? '_' : '';
|
|
294
|
-
lines.push(` Session limit: $${sessionVal}${sCursor}${field === 'session' ? c(A.dim, ' ← editing') : ''}`);
|
|
295
|
-
lines.push(` Daily limit: $${dailyVal}${dCursor}${field === 'daily' ? c(A.dim, ' ← editing') : ''}`);
|
|
360
|
+
lines.push(` ${bold('[s]')} Shell`);
|
|
296
361
|
lines.push('');
|
|
297
362
|
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
lines.push('');
|
|
301
|
-
}
|
|
363
|
+
return lines;
|
|
364
|
+
}
|
|
302
365
|
|
|
303
|
-
|
|
304
|
-
|
|
366
|
+
// ─── Profile Picker ───────────────────────────────────────────────────────
|
|
367
|
+
|
|
368
|
+
function showProfilePicker(rl) {
|
|
369
|
+
return new Promise((resolve) => {
|
|
370
|
+
const current = loadProfile();
|
|
371
|
+
console.log('');
|
|
372
|
+
console.log(` ${bold('Switch mode:')}`);
|
|
373
|
+
console.log('');
|
|
374
|
+
for (const [i, [name, pf]] of Object.entries(PROFILES).entries()) {
|
|
375
|
+
const active = name === current.name ? ' ✅' : '';
|
|
376
|
+
console.log(` ${bold('[' + (i + 1) + ']')} ${pf.emoji} ${pf.uiLabel.padEnd(15)} ${dim(pf.desc)}${active}`);
|
|
377
|
+
}
|
|
378
|
+
console.log(` ${bold('[q]')} Cancel`);
|
|
379
|
+
console.log('');
|
|
380
|
+
|
|
381
|
+
rl.question(' Choice: ', (answer) => {
|
|
382
|
+
const names = Object.keys(PROFILES);
|
|
383
|
+
const idx = parseInt(answer, 10) - 1;
|
|
384
|
+
if (idx >= 0 && idx < names.length) {
|
|
385
|
+
let customOverrides = null;
|
|
386
|
+
try {
|
|
387
|
+
const existing = JSON.parse(readFileSync(PROFILE_FILE, 'utf8'));
|
|
388
|
+
if (existing.custom_overrides?.budgets) customOverrides = { budgets: existing.custom_overrides.budgets };
|
|
389
|
+
} catch {}
|
|
390
|
+
saveProfile(names[idx], customOverrides);
|
|
391
|
+
const pf = PROFILES[names[idx]];
|
|
392
|
+
console.log(` ✅ Switched to ${pf.emoji} ${pf.uiLabel}`);
|
|
393
|
+
}
|
|
394
|
+
resolve();
|
|
395
|
+
});
|
|
396
|
+
});
|
|
305
397
|
}
|
|
306
398
|
|
|
307
|
-
// ───
|
|
399
|
+
// ─── Cost Alert Editor ────────────────────────────────────────────────────
|
|
400
|
+
|
|
401
|
+
function showCostAlertEditor(rl) {
|
|
402
|
+
return new Promise((resolve) => {
|
|
403
|
+
const profile = loadProfile();
|
|
404
|
+
console.log('');
|
|
405
|
+
console.log(` ${bold('Cost alerts')}`);
|
|
406
|
+
console.log(` ${dim('Dual-brain estimates API costs from session activity.')}`);
|
|
407
|
+
console.log(` ${dim('These are alerts, not billing caps.')}`);
|
|
408
|
+
console.log('');
|
|
409
|
+
console.log(` Current: warn at $${profile.budgets.session_warn_usd}/session, $${profile.budgets.daily_warn_usd}/day`);
|
|
410
|
+
console.log(` limit at $${profile.budgets.session_limit_usd}/session, $${profile.budgets.daily_limit_usd}/day`);
|
|
411
|
+
console.log('');
|
|
412
|
+
|
|
413
|
+
rl.question(' Session alert limit ($, Enter = keep): ', (sessionStr) => {
|
|
414
|
+
if (!sessionStr.trim()) return resolve();
|
|
415
|
+
const session = parseFloat(sessionStr);
|
|
416
|
+
if (isNaN(session) || session <= 0) { console.log(' Cancelled.'); return resolve(); }
|
|
417
|
+
|
|
418
|
+
rl.question(' Daily alert limit ($, Enter = auto): ', (dailyStr) => {
|
|
419
|
+
const daily = parseFloat(dailyStr);
|
|
420
|
+
const finalDaily = (isNaN(daily) || daily <= 0) ? session * 3 : daily;
|
|
421
|
+
|
|
422
|
+
let existing = {};
|
|
423
|
+
try { existing = JSON.parse(readFileSync(PROFILE_FILE, 'utf8')); } catch {}
|
|
424
|
+
const custom = existing.custom_overrides || {};
|
|
425
|
+
custom.budgets = {
|
|
426
|
+
session_warn_usd: +(session * 0.6).toFixed(2),
|
|
427
|
+
session_limit_usd: session,
|
|
428
|
+
daily_warn_usd: +(finalDaily * 0.6).toFixed(2),
|
|
429
|
+
daily_limit_usd: finalDaily,
|
|
430
|
+
};
|
|
431
|
+
const data = { active: existing.active || 'balanced', switched_at: existing.switched_at || new Date().toISOString(), custom_overrides: custom };
|
|
432
|
+
const tmp = PROFILE_FILE + '.tmp.' + process.pid;
|
|
433
|
+
writeFileSync(tmp, JSON.stringify(data, null, 2) + '\n');
|
|
434
|
+
renameSync(tmp, PROFILE_FILE);
|
|
435
|
+
|
|
436
|
+
console.log(` ✅ Cost alerts: $${session}/session · $${finalDaily}/day`);
|
|
437
|
+
resolve();
|
|
438
|
+
});
|
|
439
|
+
});
|
|
440
|
+
});
|
|
441
|
+
}
|
|
308
442
|
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
console.log(
|
|
443
|
+
// ─── Session Runner ───────────────────────────────────────────────────────
|
|
444
|
+
|
|
445
|
+
function runSession(cmd, args, label) {
|
|
446
|
+
console.log('');
|
|
447
|
+
console.log(` ${label}`);
|
|
448
|
+
console.log(` ${dim('Inside Claude: press Ctrl+C twice to return here.')}`);
|
|
449
|
+
console.log('');
|
|
450
|
+
markLaunched();
|
|
451
|
+
const result = spawnSync(cmd, args, { stdio: 'inherit' });
|
|
452
|
+
console.log('');
|
|
453
|
+
console.log(' Returned to Dual-Brain.');
|
|
454
|
+
return result.status || 0;
|
|
316
455
|
}
|
|
317
456
|
|
|
318
|
-
// ───
|
|
457
|
+
// ─── Main Loop ────────────────────────────────────────────────────────────
|
|
319
458
|
|
|
320
|
-
function
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
let flashTimeout = null;
|
|
324
|
-
let refreshTimer = null;
|
|
459
|
+
async function mainLoop() {
|
|
460
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
461
|
+
const ask = () => new Promise(resolve => rl.question(' Choice: ', resolve));
|
|
325
462
|
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
463
|
+
while (true) {
|
|
464
|
+
const firstRun = isFirstRun();
|
|
465
|
+
const providers = detectProviders();
|
|
466
|
+
const sessions = firstRun ? [] : getRecentSessions();
|
|
330
467
|
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
flashTimeout = setTimeout(() => { flash = null; render(); }, ms);
|
|
335
|
-
}
|
|
468
|
+
const lines = firstRun
|
|
469
|
+
? renderFirstRunMenu(providers)
|
|
470
|
+
: renderReturningMenu(providers, sessions);
|
|
336
471
|
|
|
337
|
-
|
|
338
|
-
return {
|
|
339
|
-
profile: loadProfile(),
|
|
340
|
-
providers: detectProviders(),
|
|
341
|
-
pressure: loadPressure(),
|
|
342
|
-
cost: loadTodayCost(),
|
|
343
|
-
flash,
|
|
344
|
-
};
|
|
345
|
-
}
|
|
472
|
+
for (const l of lines) console.log(l);
|
|
346
473
|
|
|
347
|
-
|
|
348
|
-
let screen;
|
|
349
|
-
if (view === 'dashboard') {
|
|
350
|
-
screen = renderDashboard(loadState());
|
|
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);
|
|
357
|
-
}
|
|
358
|
-
process.stdout.write(A.home + A.clear + screen);
|
|
359
|
-
}
|
|
474
|
+
const choice = (await ask()).trim().toLowerCase();
|
|
360
475
|
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
476
|
+
if (choice === 's' || choice === 'q') {
|
|
477
|
+
console.log('');
|
|
478
|
+
rl.close();
|
|
479
|
+
return;
|
|
480
|
+
}
|
|
365
481
|
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
482
|
+
if (choice === 'c' || choice === '') {
|
|
483
|
+
if (sessions.length > 0) {
|
|
484
|
+
const s = sessions[0];
|
|
485
|
+
if (s.tool === 'codex') {
|
|
486
|
+
runSession('codex', ['--dangerously-bypass-approvals-and-sandbox', 'resume', s.id], `Resuming codex ${s.id.slice(0, 8)}...`);
|
|
487
|
+
} else {
|
|
488
|
+
runSession('claude', ['-r', s.id, '--dangerously-skip-permissions'], `Resuming session ${s.id.slice(0, 8)}...`);
|
|
489
|
+
}
|
|
490
|
+
} else {
|
|
491
|
+
runSession('claude', ['--dangerously-skip-permissions'], 'Starting new session...');
|
|
492
|
+
}
|
|
493
|
+
continue;
|
|
494
|
+
}
|
|
369
495
|
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
496
|
+
const num = parseInt(choice, 10);
|
|
497
|
+
if (num >= 1 && num <= 9 && sessions[num - 1]) {
|
|
498
|
+
const s = sessions[num - 1];
|
|
499
|
+
if (s.tool === 'codex') {
|
|
500
|
+
runSession('codex', ['--dangerously-bypass-approvals-and-sandbox', 'resume', s.id], `Resuming codex ${s.id.slice(0, 8)}...`);
|
|
501
|
+
} else {
|
|
502
|
+
runSession('claude', ['-r', s.id, '--dangerously-skip-permissions'], `Resuming session ${s.id.slice(0, 8)}...`);
|
|
503
|
+
}
|
|
504
|
+
continue;
|
|
505
|
+
}
|
|
377
506
|
|
|
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
|
-
}
|
|
507
|
+
if (choice === 'n') {
|
|
508
|
+
runSession('claude', ['--dangerously-skip-permissions'], 'Starting new session...');
|
|
509
|
+
continue;
|
|
510
|
+
}
|
|
389
511
|
|
|
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
|
-
});
|
|
512
|
+
if (choice === 'p') {
|
|
513
|
+
await showProfilePicker(rl);
|
|
514
|
+
continue;
|
|
515
|
+
}
|
|
402
516
|
|
|
403
|
-
|
|
404
|
-
|
|
517
|
+
if (choice === 'b') {
|
|
518
|
+
await showCostAlertEditor(rl);
|
|
519
|
+
continue;
|
|
520
|
+
}
|
|
405
521
|
|
|
406
|
-
|
|
407
|
-
|
|
522
|
+
if (choice === 'j') {
|
|
523
|
+
console.log('');
|
|
524
|
+
console.log(' Starting Claude login...');
|
|
525
|
+
console.log('');
|
|
526
|
+
spawnSync('claude', ['login'], { stdio: 'inherit' });
|
|
527
|
+
continue;
|
|
528
|
+
}
|
|
408
529
|
|
|
409
|
-
if (
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
budgetField = budgetField === 'session' ? 'daily' : 'session';
|
|
418
|
-
render();
|
|
419
|
-
return;
|
|
530
|
+
if (choice === 'k') {
|
|
531
|
+
const codexPath = spawnSync('which', ['codex'], { encoding: 'utf8', stdio: 'pipe', timeout: 3000 });
|
|
532
|
+
if (codexPath.status !== 0) {
|
|
533
|
+
console.log('');
|
|
534
|
+
console.log(` Codex not installed. Run: ${cyan('npm i -g @openai/codex')}`);
|
|
535
|
+
console.log('');
|
|
536
|
+
await ask();
|
|
537
|
+
continue;
|
|
420
538
|
}
|
|
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;
|
|
539
|
+
console.log('');
|
|
540
|
+
console.log(' Starting Codex login...');
|
|
541
|
+
console.log('');
|
|
542
|
+
spawnSync(codexPath.stdout.trim(), ['login'], { stdio: 'inherit' });
|
|
543
|
+
continue;
|
|
446
544
|
}
|
|
447
545
|
|
|
448
|
-
if (
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
546
|
+
if (choice === 't' && IS_REPLIT) {
|
|
547
|
+
console.log('');
|
|
548
|
+
console.log(' Installing replit-tools...');
|
|
549
|
+
console.log('');
|
|
550
|
+
spawnSync('npx', ['-y', 'data-tools'], { stdio: 'inherit', cwd: CWD });
|
|
551
|
+
console.log('');
|
|
552
|
+
console.log(' ✅ replit-tools installed.');
|
|
553
|
+
console.log('');
|
|
554
|
+
await ask();
|
|
555
|
+
continue;
|
|
453
556
|
}
|
|
454
557
|
|
|
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
|
-
});
|
|
558
|
+
console.log(` Unknown option: ${choice}`);
|
|
559
|
+
}
|
|
479
560
|
}
|
|
480
561
|
|
|
481
|
-
// ───
|
|
562
|
+
// ─── Non-Interactive Fallback ─────────────────────────────────────────────
|
|
563
|
+
|
|
564
|
+
function renderStatic() {
|
|
565
|
+
const providers = detectProviders();
|
|
566
|
+
const sessions = getRecentSessions();
|
|
567
|
+
const lines = sessions.length > 0
|
|
568
|
+
? renderReturningMenu(providers, sessions)
|
|
569
|
+
: renderFirstRunMenu(providers);
|
|
570
|
+
for (const l of lines) console.log(l);
|
|
571
|
+
}
|
|
482
572
|
|
|
483
|
-
|
|
573
|
+
// ─── Entry ────────────────────────────────────────────────────────────────
|
|
484
574
|
|
|
485
|
-
if (
|
|
486
|
-
|
|
575
|
+
if (process.stdin.isTTY && process.stdout.isTTY && !process.env.CI) {
|
|
576
|
+
mainLoop().catch(err => { console.error(err); process.exit(1); });
|
|
487
577
|
} else {
|
|
488
578
|
renderStatic();
|
|
489
579
|
}
|
package/install.mjs
CHANGED
|
@@ -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 {}
|
|
@@ -334,7 +335,7 @@ function install(workspace, env, mode) {
|
|
|
334
335
|
'test-orchestrator.mjs', 'setup-wizard.mjs', 'health-check.mjs',
|
|
335
336
|
'install-git-hooks.mjs', 'session-report.mjs', 'budget-balancer.mjs',
|
|
336
337
|
'gpt-work-dispatcher.mjs', 'profiles.mjs',
|
|
337
|
-
'summary-checkpoint.mjs', 'decision-ledger.mjs',
|
|
338
|
+
'summary-checkpoint.mjs', 'decision-ledger.mjs', 'control-panel.mjs',
|
|
338
339
|
];
|
|
339
340
|
for (const h of HOOKS) cpSync(join(__dirname, 'hooks', h), join(target, 'hooks', h));
|
|
340
341
|
actions.push(`✓ ${HOOKS.length} hook scripts`);
|
|
@@ -381,104 +382,36 @@ function install(workspace, env, mode) {
|
|
|
381
382
|
|
|
382
383
|
// ─── Status Report ──────────────────────────────────────────────────────────
|
|
383
384
|
|
|
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) {
|
|
385
|
+
function printReport(env, mode, actions, isDryRun) {
|
|
394
386
|
const lines = [];
|
|
395
387
|
|
|
396
388
|
lines.push(br('╔', '╗'));
|
|
397
|
-
lines.push(ln(`🧠 Dual-Brain
|
|
389
|
+
lines.push(ln(`🧠 Dual-Brain v${VERSION}`));
|
|
398
390
|
lines.push(sep());
|
|
399
391
|
|
|
400
|
-
|
|
392
|
+
const cAuth = env.claude.authed ? '✅' : env.claude.installed ? '⚠️' : '❌';
|
|
393
|
+
const xAuth = env.codex.authed ? '✅' : env.codex.installed ? '⚠️' : '❌';
|
|
394
|
+
lines.push(ln(` 🟠 Claude ${cAuth} 🟢 Codex ${xAuth}`));
|
|
395
|
+
|
|
401
396
|
if (env.isReplit) {
|
|
402
|
-
lines.push(ln(` 🌀
|
|
403
|
-
} else {
|
|
404
|
-
lines.push(ln(' Platform: standalone'));
|
|
397
|
+
lines.push(ln(` 🌀 Replit${env.hasReplitTools ? ' + replit-tools' : ''}`));
|
|
405
398
|
}
|
|
406
399
|
|
|
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
400
|
if (actions) {
|
|
419
401
|
lines.push(sep());
|
|
420
|
-
lines.push(ln('📝 Installed'));
|
|
421
402
|
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
403
|
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 {
|
|
404
|
+
lines.push(ln('✅ Installed — launching session manager...'));
|
|
405
|
+
} else if (isDryRun) {
|
|
406
|
+
lines.push(sep());
|
|
449
407
|
lines.push(ln('Dry run — no files written'));
|
|
450
408
|
}
|
|
409
|
+
|
|
451
410
|
lines.push(br('╚', '╝'));
|
|
452
411
|
|
|
453
412
|
console.log('');
|
|
454
413
|
for (const l of lines) console.log(` ${l}`);
|
|
455
414
|
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
415
|
}
|
|
483
416
|
|
|
484
417
|
// ─── Profile System ────────────────────────────────────────────────────────
|
|
@@ -539,70 +472,13 @@ function saveProfile(workspace, name, customOverrides) {
|
|
|
539
472
|
|
|
540
473
|
// ─── Subcommand: status ────────────────────────────────────────────────────
|
|
541
474
|
|
|
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('');
|
|
475
|
+
function launchPanel() {
|
|
476
|
+
const panelPath = join(resolve(process.cwd()), '.claude', 'hooks', 'control-panel.mjs');
|
|
477
|
+
const pkgPanel = join(__dirname, 'hooks', 'control-panel.mjs');
|
|
478
|
+
const panel = existsSync(panelPath) ? panelPath : existsSync(pkgPanel) ? pkgPanel : null;
|
|
479
|
+
if (panel) {
|
|
480
|
+
const { status } = spawnSync(process.execPath, [panel], { stdio: 'inherit' });
|
|
481
|
+
process.exit(status || 0);
|
|
606
482
|
}
|
|
607
483
|
}
|
|
608
484
|
|
|
@@ -797,15 +673,7 @@ function cmdExplain() {
|
|
|
797
673
|
|
|
798
674
|
function main() {
|
|
799
675
|
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();
|
|
676
|
+
launchPanel();
|
|
809
677
|
return;
|
|
810
678
|
}
|
|
811
679
|
if (subcommand === 'mode') { cmdMode(); return; }
|
|
@@ -819,13 +687,29 @@ function main() {
|
|
|
819
687
|
if (jsonOut) {
|
|
820
688
|
console.log(JSON.stringify({ version: VERSION, env, mode }, null, 2));
|
|
821
689
|
} else {
|
|
822
|
-
printReport(env, mode, null);
|
|
690
|
+
printReport(env, mode, null, true);
|
|
823
691
|
}
|
|
824
692
|
process.exit(0);
|
|
825
693
|
}
|
|
826
694
|
|
|
695
|
+
// Check for replit-tools on Replit
|
|
696
|
+
if (env.isReplit && !env.hasReplitTools) {
|
|
697
|
+
console.log('');
|
|
698
|
+
console.log(' ⚠️ replit-tools not found — recommended for Replit environments.');
|
|
699
|
+
console.log(' Dual-brain works best alongside replit-tools for persistent auth,');
|
|
700
|
+
console.log(' session management, and shell integration.');
|
|
701
|
+
console.log('');
|
|
702
|
+
console.log(` Install: ${cmd('npx -y data-tools')}`);
|
|
703
|
+
console.log('');
|
|
704
|
+
}
|
|
705
|
+
|
|
827
706
|
const actions = install(env.workspace, env, mode);
|
|
828
707
|
printReport(env, mode, actions);
|
|
708
|
+
|
|
709
|
+
// After install, launch the session manager (interactive TTY only)
|
|
710
|
+
if (process.stdin.isTTY && process.stdout.isTTY && !process.env.CI) {
|
|
711
|
+
launchPanel();
|
|
712
|
+
}
|
|
829
713
|
}
|
|
830
714
|
|
|
831
715
|
main();
|
package/package.json
CHANGED