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.
@@ -1,48 +1,48 @@
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 launcher 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
+ * 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 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;
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: '⚖️', 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+' },
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, gate: PROFILE_GATE.balanced, switched_at: null };
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
- 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);
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 = { authed: false, models: 'opus / sonnet / haiku' };
108
- const codex = { authed: false, installed: false, models: 'gpt-5.5 / gpt-5.4 / gpt-4.1-mini' };
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(process.env.HOME || '', '.claude', '.credentials.json'),
112
- join(process.env.HOME || '', '.claude', 'credentials.json'),
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 which = spawnSync('which', ['codex'], { encoding: 'utf8', stdio: 'pipe', timeout: 3000 });
127
- if (which.status === 0 && which.stdout.trim()) {
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(which.stdout.trim(), ['login', 'status'], { encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'], timeout: 5000 });
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
- 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 };
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
- 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 } },
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 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; }
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 loadLastDecision() {
172
- const today = new Date().toISOString().slice(0, 10);
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 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
- }
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
- return null;
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
- // ─── 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}`;
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
- function renderDashboard(state) {
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(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}+`);
264
+ lines.push(` 🧠 ${bold(`Dual-Brain v${VERSION}`)}`);
218
265
  lines.push('');
219
266
 
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)}`);
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
- 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)}`);
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 (key === 'claude') lines.push('');
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
- if (flash) {
240
- lines.push(` ${flash}`);
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
- 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`);
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.join('\n');
310
+ return lines;
249
311
  }
250
312
 
251
- function renderExplain(decision, profile) {
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(c(A.bold, ' 🧭 Last Routing Decision'));
255
- lines.push(c(A.dim, ' ' + '─'.repeat(40)));
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
- if (!decision) {
258
- lines.push(' 💤 No routing decisions recorded today.');
328
+ // Recent sessions
329
+ if (sessions.length > 0) {
259
330
  lines.push('');
260
- lines.push(c(A.dim, ' Press any key to go back'));
261
- return lines.join('\n');
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
- if (followed) {
275
- lines.push(' Routing matched the recommendation.');
276
- } else {
277
- lines.push(' ⚠️ Recommendation was overridden.');
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
- lines.push('');
281
- lines.push(c(A.dim, ' Press any key to go back'));
282
- return lines.join('\n');
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
- function renderBudgetEditor(sessionVal, dailyVal, field, flash) {
286
- const lines = [];
287
- lines.push('');
288
- lines.push(c(A.bold, ' 💵 Edit Budget'));
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
- 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') : ''}`);
360
+ lines.push(` ${bold('[s]')} Shell`);
296
361
  lines.push('');
297
362
 
298
- if (flash) {
299
- lines.push(` ${flash}`);
300
- lines.push('');
301
- }
363
+ return lines;
364
+ }
302
365
 
303
- lines.push(c(A.dim, ' Type numbers · Tab next · Enter save · Esc cancel'));
304
- return lines.join('\n');
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
- // ─── Static (non-TTY) Output ───────────────────────────────────────────────
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
- 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));
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
- // ─── Interactive TUI ───────────────────────────────────────────────────────
457
+ // ─── Main Loop ────────────────────────────────────────────────────────────
319
458
 
320
- function startTUI() {
321
- let view = 'dashboard';
322
- let flash = null;
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
- // Budget editor state
327
- let budgetSession = '';
328
- let budgetDaily = '';
329
- let budgetField = 'session';
463
+ while (true) {
464
+ const firstRun = isFirstRun();
465
+ const providers = detectProviders();
466
+ const sessions = firstRun ? [] : getRecentSessions();
330
467
 
331
- function setFlash(msg, ms = 3000) {
332
- flash = msg;
333
- clearTimeout(flashTimeout);
334
- flashTimeout = setTimeout(() => { flash = null; render(); }, ms);
335
- }
468
+ const lines = firstRun
469
+ ? renderFirstRunMenu(providers)
470
+ : renderReturningMenu(providers, sessions);
336
471
 
337
- function loadState() {
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
- 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);
357
- }
358
- process.stdout.write(A.home + A.clear + screen);
359
- }
474
+ const choice = (await ask()).trim().toLowerCase();
360
475
 
361
- function startRefresh() {
362
- stopRefresh();
363
- refreshTimer = setInterval(render, 2000);
364
- }
476
+ if (choice === 's' || choice === 'q') {
477
+ console.log('');
478
+ rl.close();
479
+ return;
480
+ }
365
481
 
366
- function stopRefresh() {
367
- if (refreshTimer) { clearInterval(refreshTimer); refreshTimer = null; }
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
- 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
- }
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
- 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
- }
507
+ if (choice === 'n') {
508
+ runSession('claude', ['--dangerously-skip-permissions'], 'Starting new session...');
509
+ continue;
510
+ }
389
511
 
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
- });
512
+ if (choice === 'p') {
513
+ await showProfilePicker(rl);
514
+ continue;
515
+ }
402
516
 
403
- render();
404
- startRefresh();
517
+ if (choice === 'b') {
518
+ await showCostAlertEditor(rl);
519
+ continue;
520
+ }
405
521
 
406
- process.stdin.on('keypress', (str, key) => {
407
- if (key?.ctrl && key?.name === 'c') return cleanup();
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 (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;
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
- 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;
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 (view === 'explain') {
449
- view = 'dashboard';
450
- startRefresh();
451
- render();
452
- return;
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
- // 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
- });
558
+ console.log(` Unknown option: ${choice}`);
559
+ }
479
560
  }
480
561
 
481
- // ─── Entry ─────────────────────────────────────────────────────────────────
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
- const interactive = process.stdin.isTTY && process.stdout.isTTY && !process.env.CI;
573
+ // ─── Entry ────────────────────────────────────────────────────────────────
484
574
 
485
- if (interactive) {
486
- startTUI();
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 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) {
385
+ function printReport(env, mode, actions, isDryRun) {
394
386
  const lines = [];
395
387
 
396
388
  lines.push(br('╔', '╗'));
397
- lines.push(ln(`🧠 Dual-Brain Orchestrator v${VERSION}`));
389
+ lines.push(ln(`🧠 Dual-Brain v${VERSION}`));
398
390
  lines.push(sep());
399
391
 
400
- lines.push(ln('🌎 Environment'));
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(` 🌀 Platform: Replit${env.hasReplitTools ? ' + replit-tools' : ''}`));
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('🔓 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 {
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 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('');
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
- // 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();
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dual-brain",
3
- "version": "3.3.0",
3
+ "version": "3.5.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": {