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.
@@ -1,48 +1,47 @@
1
1
  #!/usr/bin/env node
2
2
  /**
3
- * control-panel.mjs — Interactive TUI control panel for Dual-Brain Orchestrator.
3
+ * control-panel.mjs — Session manager + control panel for Dual-Brain.
4
4
  *
5
- * Keyboard-driven dashboard with live-updating pressure, profile switching,
6
- * inline budget editing, and routing decision viewer.
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 color = !process.env.NO_COLOR;
29
- const A = {
30
- altOn: '\x1b[?1049h', altOff: '\x1b[?1049l',
31
- clear: '\x1b[2J', home: '\x1b[H',
32
- hide: '\x1b[?25l', show: '\x1b[?25h',
33
- reset: '\x1b[0m', bold: '\x1b[1m', dim: '\x1b[2m',
34
- red: '\x1b[31m', green: '\x1b[32m', yellow: '\x1b[33m',
35
- blue: '\x1b[34m', cyan: '\x1b[36m', gray: '\x1b[90m',
36
- white: '\x1b[37m',
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: 'Standard routing — best model per tier' },
44
- 'cost-saver': { emoji: '💸', label: 'Cost-saver', desc: 'Minimize spend prefer cheaper models' },
45
- 'quality-first': { emoji: '💎', label: 'Quality-first', desc: 'Maximum quality — dual-brain for medium+' },
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, gate: PROFILE_GATE.balanced, switched_at: null };
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
- function saveBudget(sessionLimit, dailyLimit) {
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 = { authed: false, models: 'opus / sonnet / haiku' };
108
- const codex = { authed: false, installed: false, models: 'gpt-5.5 / gpt-5.4 / gpt-4.1-mini' };
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(process.env.HOME || '', '.claude', '.credentials.json'),
112
- join(process.env.HOME || '', '.claude', 'credentials.json'),
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 which = spawnSync('which', ['codex'], { encoding: 'utf8', stdio: 'pipe', timeout: 3000 });
127
- if (which.status === 0 && which.stdout.trim()) {
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(which.stdout.trim(), ['login', 'status'], { encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'], timeout: 5000 });
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
- function loadPressure() {
138
- try {
139
- const today = new Date().toISOString().slice(0, 10);
140
- const summaryPath = join(__dirname, `usage-summary-${today}.json`);
141
- const summary = JSON.parse(readFileSync(summaryPath, 'utf8'));
142
- const cutoff = Date.now() - 5 * 60 * 60 * 1000;
143
- const result = {};
144
- for (const provider of ['claude', 'openai']) {
145
- result[provider] = {};
146
- for (const tier of ['think', 'execute', 'search']) {
147
- const ts = (summary.pressure?.[provider]?.[tier] || []).filter(t => Date.parse(t) >= cutoff);
148
- const BUDGETS = { think: 45, execute: 364, search: 2000 };
149
- const calls = ts.length;
150
- const pressure = Math.min(1, calls / (BUDGETS[tier] || 364));
151
- result[provider][tier] = { calls, pressure };
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
- return result;
155
- } catch {
156
- return {
157
- claude: { think: { calls: 0, pressure: 0 }, execute: { calls: 0, pressure: 0 }, search: { calls: 0, pressure: 0 } },
158
- openai: { think: { calls: 0, pressure: 0 }, execute: { calls: 0, pressure: 0 }, search: { calls: 0, pressure: 0 } },
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 loadTodayCost() {
164
- try {
165
- const today = new Date().toISOString().slice(0, 10);
166
- const summary = JSON.parse(readFileSync(join(__dirname, `usage-summary-${today}.json`), 'utf8'));
167
- return summary.totals?.cost_estimate || 0;
168
- } catch { return 0; }
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 loadLastDecision() {
172
- const today = new Date().toISOString().slice(0, 10);
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 lines = readFileSync(logFile, 'utf8').split('\n').filter(Boolean);
177
- for (let i = lines.length - 1; i >= 0; i--) {
178
- try {
179
- const e = JSON.parse(lines[i]);
180
- if (e.type === 'tier_recommendation') return e;
181
- } catch {}
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 null;
230
+ return { claude, codex };
185
231
  }
186
232
 
187
- // ─── Rendering ─────────────────────────────────────────────────────────────
188
-
189
- function pressureBar(p, w = 10) {
190
- const filled = Math.min(w, Math.round(p * w));
191
- const bar = '▓'.repeat(filled) + '░'.repeat(w - filled);
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
- function renderDashboard(state) {
203
- const { profile, providers, pressure, cost, flash } = state;
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 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';
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
- if (flash) {
240
- lines.push(` ${flash}`);
241
- lines.push('');
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(c(A.dim, ''.repeat(30)));
245
- lines.push(` ⌨️ ${c(A.bold, '1')} Balanced ${c(A.bold, '2')} Cost-saver ${c(A.bold, '3')} Quality-first ${c(A.bold, 'b')} Budget ${c(A.bold, 'e')} Explain ${c(A.bold, 'q')} Quit`);
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
- return lines.join('\n');
249
- }
250
-
251
- function renderExplain(decision, profile) {
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
- if (!decision) {
258
- lines.push(' 💤 No routing decisions recorded today.');
279
+ // Missing provider nudge
280
+ if (!providers.claude.authed || !providers.codex.authed) {
259
281
  lines.push('');
260
- lines.push(c(A.dim, ' Press any key to go back'));
261
- return lines.join('\n');
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
- 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
- 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
- lines.push('');
281
- lines.push(c(A.dim, ' Press any key to go back'));
282
- return lines.join('\n');
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
- function renderBudgetEditor(sessionVal, dailyVal, field, flash) {
286
- const lines = [];
308
+ // Session manager box
287
309
  lines.push('');
288
- lines.push(c(A.bold, ' 💵 Edit Budget'));
289
- lines.push(c(A.dim, ' ' + '─'.repeat(40)));
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
- const sCursor = field === 'session' ? '_' : '';
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') : ''}`);
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
- if (flash) {
299
- lines.push(` ${flash}`);
300
- lines.push('');
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
- lines.push(c(A.dim, ' Type numbers · Tab next · Enter save · Esc cancel'));
304
- return lines.join('\n');
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
- // ─── Static (non-TTY) Output ───────────────────────────────────────────────
409
+ // ─── Session Runner ───────────────────────────────────────────────────────
308
410
 
309
- function renderStatic() {
310
- const profile = loadProfile();
311
- const providers = detectProviders();
312
- const pressure = loadPressure();
313
- const cost = loadTodayCost();
314
- const state = { profile, providers, pressure, cost, flash: null };
315
- console.log(renderDashboard(state));
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
- // ─── Interactive TUI ───────────────────────────────────────────────────────
421
+ // ─── Main Loop ────────────────────────────────────────────────────────────
319
422
 
320
- function startTUI() {
321
- let view = 'dashboard';
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
- // Budget editor state
327
- let budgetSession = '';
328
- let budgetDaily = '';
329
- let budgetField = 'session';
426
+ const ask = () => new Promise(resolve => rl.question(' Choice: ', resolve));
330
427
 
331
- function setFlash(msg, ms = 3000) {
332
- flash = msg;
333
- clearTimeout(flashTimeout);
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
- function loadState() {
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
- function render() {
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);
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
- function stopRefresh() {
367
- if (refreshTimer) { clearInterval(refreshTimer); refreshTimer = null; }
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
- function cleanup() {
371
- stopRefresh();
372
- clearTimeout(flashTimeout);
373
- process.stdin.setRawMode(false);
374
- process.stdout.write(A.reset + A.show + A.altOff);
375
- process.exit(0);
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
- function switchProfile(name) {
379
- let customOverrides = null;
380
- try {
381
- const existing = JSON.parse(readFileSync(PROFILE_FILE, 'utf8'));
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
- // Setup
391
- process.stdout.write(A.altOn + A.hide);
392
- readline.emitKeypressEvents(process.stdin);
393
- process.stdin.setRawMode(true);
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
- render();
404
- startRefresh();
476
+ if (choice === 'b') {
477
+ await showBudgetEditor(rl);
478
+ continue;
479
+ }
405
480
 
406
- process.stdin.on('keypress', (str, key) => {
407
- if (key?.ctrl && key?.name === 'c') return cleanup();
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 (view === 'budget') {
410
- if (key?.name === 'escape') {
411
- view = 'dashboard';
412
- startRefresh();
413
- render();
414
- return;
415
- }
416
- if (key?.name === 'tab') {
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
- if (key?.name === 'return') {
422
- const s = parseFloat(budgetSession);
423
- const d = parseFloat(budgetDaily);
424
- if (isNaN(s) || s <= 0) { setFlash('❌ Invalid session limit'); render(); return; }
425
- const daily = (isNaN(d) || d <= 0) ? s * 3 : d;
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 (view === 'explain') {
449
- view = 'dashboard';
450
- startRefresh();
451
- render();
452
- return;
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
- // Dashboard keys
456
- if (key?.name === 'q' || key?.name === 'escape') return cleanup();
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
- // ─── Entry ─────────────────────────────────────────────────────────────────
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
- const interactive = process.stdin.isTTY && process.stdout.isTTY && !process.env.CI;
528
+ // ─── Entry ────────────────────────────────────────────────────────────────
484
529
 
485
- if (interactive) {
486
- startTUI();
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 statusIcon(val) { return val ? '✅' : '❌'; }
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 Orchestrator v${VERSION}`));
388
+ lines.push(ln(`🧠 Dual-Brain v${VERSION}`));
398
389
  lines.push(sep());
399
390
 
400
- lines.push(ln('🌎 Environment'));
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(` 🌀 Platform: Replit${env.hasReplitTools ? ' + replit-tools' : ''}`));
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('🔓 Unlock full power:'));
428
- if (!env.claude.installed) {
429
- lines.push(ln(' curl -fsSL https://claude.ai/install.sh | sh'));
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 cmdStatus() {
543
- const workspace = resolve(process.cwd());
544
- const env = detectEnvironment();
545
- const mode = resolveMode(env);
546
- const profile = loadProfile(workspace);
547
-
548
- const lines = [];
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
- // Launch interactive TUI if available and TTY
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dual-brain",
3
- "version": "3.3.0",
3
+ "version": "3.4.0",
4
4
  "description": "Dual-provider orchestration for Claude Code — tiered routing, budget balancing, and GPT dual-brain review across Claude + OpenAI subscriptions",
5
5
  "type": "module",
6
6
  "bin": {