dual-brain 0.1.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.
Files changed (68) hide show
  1. package/AGENTS.md +97 -0
  2. package/CLAUDE.md +147 -0
  3. package/LICENSE +21 -0
  4. package/README.md +197 -0
  5. package/agents/implementer.md +22 -0
  6. package/agents/researcher.md +25 -0
  7. package/agents/verifier.md +30 -0
  8. package/bin/dual-brain.mjs +2868 -0
  9. package/hooks/auto-update-wrapper.mjs +102 -0
  10. package/hooks/auto-update.sh +67 -0
  11. package/hooks/budget-balancer.mjs +679 -0
  12. package/hooks/control-panel.mjs +1195 -0
  13. package/hooks/cost-logger.mjs +286 -0
  14. package/hooks/cost-report.mjs +351 -0
  15. package/hooks/decision-ledger.mjs +299 -0
  16. package/hooks/dual-brain-review.mjs +404 -0
  17. package/hooks/dual-brain-think.mjs +393 -0
  18. package/hooks/enforce-tier.mjs +469 -0
  19. package/hooks/failure-detector.mjs +138 -0
  20. package/hooks/gpt-work-dispatcher.mjs +512 -0
  21. package/hooks/head-guard.mjs +105 -0
  22. package/hooks/health-check.mjs +444 -0
  23. package/hooks/install-git-hooks.mjs +106 -0
  24. package/hooks/model-registry.mjs +859 -0
  25. package/hooks/plan-generator.mjs +544 -0
  26. package/hooks/profiles.mjs +254 -0
  27. package/hooks/quality-gate.mjs +355 -0
  28. package/hooks/risk-classifier.mjs +41 -0
  29. package/hooks/session-report.mjs +514 -0
  30. package/hooks/setup-wizard.mjs +130 -0
  31. package/hooks/summary-checkpoint.mjs +432 -0
  32. package/hooks/task-classifier.mjs +328 -0
  33. package/hooks/test-orchestrator.mjs +1077 -0
  34. package/hooks/vibe-memory.mjs +463 -0
  35. package/hooks/vibe-router.mjs +387 -0
  36. package/hooks/wave-orchestrator.mjs +1397 -0
  37. package/install.mjs +1541 -0
  38. package/mcp-server/README.md +81 -0
  39. package/mcp-server/index.mjs +388 -0
  40. package/orchestrator.json +215 -0
  41. package/package.json +108 -0
  42. package/playbooks/debug.json +49 -0
  43. package/playbooks/refactor.json +57 -0
  44. package/playbooks/security-audit.json +57 -0
  45. package/playbooks/security.json +38 -0
  46. package/playbooks/test-gen.json +48 -0
  47. package/plugin.json +22 -0
  48. package/review-rules.md +17 -0
  49. package/shell-hook.sh +26 -0
  50. package/skills/go.md +22 -0
  51. package/skills/review.md +19 -0
  52. package/skills/status.md +13 -0
  53. package/skills/think.md +22 -0
  54. package/src/brief.mjs +266 -0
  55. package/src/decide.mjs +635 -0
  56. package/src/decompose.mjs +331 -0
  57. package/src/detect.mjs +345 -0
  58. package/src/dispatch.mjs +942 -0
  59. package/src/health.mjs +253 -0
  60. package/src/index.mjs +44 -0
  61. package/src/install-hooks.mjs +100 -0
  62. package/src/playbook.mjs +257 -0
  63. package/src/profile.mjs +990 -0
  64. package/src/redact.mjs +192 -0
  65. package/src/repo.mjs +292 -0
  66. package/src/session.mjs +1036 -0
  67. package/src/tui.mjs +197 -0
  68. package/src/update-check.mjs +35 -0
@@ -0,0 +1,1195 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * control-panel.mjs — Session launcher for Dual-Brain.
4
+ *
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.
8
+ */
9
+
10
+ import readline from 'readline';
11
+ import { existsSync, readFileSync, readdirSync, statSync, renameSync, writeFileSync } from 'fs';
12
+ import { dirname, join } from 'path';
13
+ import { fileURLToPath } from 'url';
14
+ import { spawnSync } from 'child_process';
15
+
16
+ const __dirname = dirname(fileURLToPath(import.meta.url));
17
+ const PROFILE_FILE = join(__dirname, '..', 'dual-brain.profile.json');
18
+ const PERMISSIONS_FILE = join(__dirname, '..', 'dual-brain.permissions.json');
19
+ const LAUNCHED_MARKER = join(__dirname, '..', '.launched');
20
+ const VERSION_STAMP_FILE = join(__dirname, '..', 'dual-brain.version.json');
21
+ const UPDATE_CACHE_FILE = join(__dirname, '..', 'dual-brain.update-check.json');
22
+ const UPDATE_CACHE_TTL_MS = 60 * 60 * 1000;
23
+ const VERSION = (() => {
24
+ try {
25
+ const stamp = JSON.parse(readFileSync(VERSION_STAMP_FILE, 'utf8'));
26
+ if (stamp.version) return stamp.version;
27
+ } catch {}
28
+ try { return JSON.parse(readFileSync(join(__dirname, '..', 'package.json'), 'utf8')).version; } catch {}
29
+ return '?';
30
+ })();
31
+
32
+ const IS_REPLIT = !!(process.env.REPL_ID || process.env.REPL_SLUG);
33
+ const HOME = process.env.HOME || process.env.USERPROFILE || '';
34
+ const CWD = process.cwd();
35
+
36
+ // ─── ANSI ──────────────────────────────────────────────────────────────────
37
+
38
+ const noColor = !!process.env.NO_COLOR;
39
+ const e = (code, s) => noColor ? s : `\x1b[${code}m${s}\x1b[0m`;
40
+ const bold = s => e('1', s);
41
+ const dim = s => e('2', s);
42
+ const cyan = s => e('36', s);
43
+ const green = s => e('32', s);
44
+ const yellow = s => e('33', s);
45
+ const orange = s => e('1;38;5;208', s);
46
+ const blue = s => e('1;38;5;33', s);
47
+
48
+ function readJsonFile(path) {
49
+ try {
50
+ return JSON.parse(readFileSync(path, 'utf8'));
51
+ } catch {
52
+ return null;
53
+ }
54
+ }
55
+
56
+ function writeJsonFile(path, value) {
57
+ writeFileSync(path, JSON.stringify(value, null, 2) + '\n');
58
+ }
59
+
60
+ function loadPermissions() {
61
+ const defaults = {
62
+ claude_skip_permissions: false,
63
+ codex_bypass_sandbox: false,
64
+ };
65
+ try {
66
+ const data = JSON.parse(readFileSync(PERMISSIONS_FILE, 'utf8'));
67
+ return {
68
+ claude_skip_permissions: !!data.claude_skip_permissions,
69
+ codex_bypass_sandbox: !!data.codex_bypass_sandbox,
70
+ };
71
+ } catch {
72
+ return defaults;
73
+ }
74
+ }
75
+
76
+ function savePermissions(perms) {
77
+ const next = {
78
+ claude_skip_permissions: !!perms.claude_skip_permissions,
79
+ codex_bypass_sandbox: !!perms.codex_bypass_sandbox,
80
+ };
81
+ const tmp = PERMISSIONS_FILE + '.tmp.' + process.pid;
82
+ writeFileSync(tmp, JSON.stringify(next, null, 2) + '\n');
83
+ renameSync(tmp, PERMISSIONS_FILE);
84
+ }
85
+
86
+ function compareVersions(a, b) {
87
+ const aParts = String(a || '').replace(/^v/i, '').split('.').map(n => parseInt(n, 10) || 0);
88
+ const bParts = String(b || '').replace(/^v/i, '').split('.').map(n => parseInt(n, 10) || 0);
89
+ const len = Math.max(aParts.length, bParts.length);
90
+ for (let i = 0; i < len; i++) {
91
+ const diff = (aParts[i] || 0) - (bParts[i] || 0);
92
+ if (diff !== 0) return diff;
93
+ }
94
+ return 0;
95
+ }
96
+
97
+ function getInstalledVersion() {
98
+ return readJsonFile(VERSION_STAMP_FILE)?.version || VERSION;
99
+ }
100
+
101
+ function getCachedUpdateStatus() {
102
+ const cache = readJsonFile(UPDATE_CACHE_FILE);
103
+ if (!cache?.checked_at) return null;
104
+ const age = Date.now() - Date.parse(cache.checked_at);
105
+ if (!Number.isFinite(age) || age < 0 || age > UPDATE_CACHE_TTL_MS) return null;
106
+ return cache;
107
+ }
108
+
109
+ function writeUpdateStatusCache(result) {
110
+ writeJsonFile(UPDATE_CACHE_FILE, {
111
+ checked_at: new Date().toISOString(),
112
+ ...result,
113
+ });
114
+ }
115
+
116
+ function checkForUpdate({ force = false } = {}) {
117
+ const installed = getInstalledVersion();
118
+
119
+ if (!force) {
120
+ const cached = getCachedUpdateStatus();
121
+ if (cached && cached.installed === installed) {
122
+ return {
123
+ updateAvailable: !!cached.updateAvailable,
124
+ installed: cached.installed,
125
+ latest: cached.latest || installed,
126
+ checkedAt: cached.checked_at,
127
+ };
128
+ }
129
+ }
130
+
131
+ try {
132
+ const result = spawnSync('npm', ['view', 'dual-brain', 'version', '--json'], {
133
+ encoding: 'utf8',
134
+ stdio: ['pipe', 'pipe', 'pipe'],
135
+ timeout: 5000,
136
+ });
137
+ if (result.status !== 0 || !result.stdout.trim()) return null;
138
+ const latestRaw = JSON.parse(result.stdout);
139
+ const latest = Array.isArray(latestRaw) ? latestRaw[latestRaw.length - 1] : latestRaw;
140
+ if (!latest) return null;
141
+ const payload = {
142
+ updateAvailable: compareVersions(latest, installed) > 0,
143
+ installed,
144
+ latest,
145
+ };
146
+ writeUpdateStatusCache(payload);
147
+ return payload;
148
+ } catch {
149
+ return null;
150
+ }
151
+ }
152
+
153
+ function formatVersionStatus(updateInfo) {
154
+ const installed = updateInfo?.installed || getInstalledVersion();
155
+ if (updateInfo?.updateAvailable && updateInfo.latest) return `v${installed} → v${updateInfo.latest} available`;
156
+ if (updateInfo?.latest && updateInfo.latest === installed) return `v${installed} (up to date)`;
157
+ return `v${installed}`;
158
+ }
159
+
160
+ // ─── Profiles ──────────────────────────────────────────────────────────────
161
+
162
+ const PROFILES = {
163
+ auto: { emoji: '🤖', uiLabel: 'Auto', desc: 'Adapts routing based on task risk, provider health, and outcomes' },
164
+ balanced: { emoji: '⚖️', uiLabel: 'Balanced', desc: 'Routes by complexity, uses both providers evenly' },
165
+ 'cost-saver': { emoji: '🛡️', uiLabel: 'Conservative', desc: 'Fewer GPT dispatches, sticks to Claude for most work' },
166
+ 'quality-first': { emoji: '🚀', uiLabel: 'Aggressive', desc: 'Maximizes both subscriptions, dual-brain for medium+ risk' },
167
+ };
168
+
169
+ const PROFILE_BUDGETS = {
170
+ auto: { session_warn_usd: 5, session_limit_usd: 10, daily_warn_usd: 20, daily_limit_usd: 50 },
171
+ balanced: { session_warn_usd: 5, session_limit_usd: 10, daily_warn_usd: 20, daily_limit_usd: 50 },
172
+ 'cost-saver': { session_warn_usd: 2, session_limit_usd: 5, daily_warn_usd: 8, daily_limit_usd: 20 },
173
+ 'quality-first': { session_warn_usd: 15, session_limit_usd: 30, daily_warn_usd: 50, daily_limit_usd: 100 },
174
+ };
175
+
176
+ function loadProfile() {
177
+ try {
178
+ const data = JSON.parse(readFileSync(PROFILE_FILE, 'utf8'));
179
+ const name = data.active && PROFILES[data.active] ? data.active : 'auto';
180
+ const custom = data.custom_overrides || {};
181
+ return { name, budgets: { ...PROFILE_BUDGETS[name], ...custom.budgets }, hasCustomBudget: !!custom.budgets };
182
+ } catch {
183
+ return { name: 'auto', budgets: PROFILE_BUDGETS.auto, hasCustomBudget: false };
184
+ }
185
+ }
186
+
187
+ function saveProfile(name, customOverrides) {
188
+ const data = { active: name, switched_at: new Date().toISOString() };
189
+ if (customOverrides) data.custom_overrides = customOverrides;
190
+ const tmp = PROFILE_FILE + '.tmp.' + process.pid;
191
+ writeFileSync(tmp, JSON.stringify(data, null, 2) + '\n');
192
+ renameSync(tmp, PROFILE_FILE);
193
+ }
194
+
195
+ // ─── First-Run Detection ──────────────────────────────────────────────────
196
+
197
+ function isFirstRun() {
198
+ if (existsSync(LAUNCHED_MARKER)) return false;
199
+ // Also check Claude history for any session in this workspace
200
+ const historyFile = join(HOME, '.claude', 'history.jsonl');
201
+ if (existsSync(historyFile)) {
202
+ try {
203
+ const content = readFileSync(historyFile, 'utf8');
204
+ if (content.includes('"sessionId"')) return false;
205
+ } catch {}
206
+ }
207
+ return true;
208
+ }
209
+
210
+ function markLaunched() {
211
+ try { writeFileSync(LAUNCHED_MARKER, new Date().toISOString() + '\n'); } catch {}
212
+ }
213
+
214
+ // ─── Provider Detection ───────────────────────────────────────────────────
215
+
216
+ function detectProviders() {
217
+ const claude = { installed: false, authed: false };
218
+ const codex = { installed: false, authed: false };
219
+
220
+ const claudeCheck = spawnSync('which', ['claude'], { encoding: 'utf8', stdio: 'pipe', timeout: 3000 });
221
+ claude.installed = claudeCheck.status === 0 && !!claudeCheck.stdout.trim();
222
+
223
+ const credPaths = [
224
+ join(HOME, '.claude', '.credentials.json'),
225
+ join(HOME, '.claude', 'credentials.json'),
226
+ join(CWD, '.replit-tools', '.claude-persistent', '.credentials.json'),
227
+ ];
228
+ for (const p of credPaths) {
229
+ try {
230
+ const cred = JSON.parse(readFileSync(p, 'utf8'));
231
+ if (cred.claudeAiOauth || cred.apiKey || cred.oauth_token) { claude.authed = true; break; }
232
+ } catch {}
233
+ }
234
+ if (!claude.authed && claude.installed) {
235
+ const r = spawnSync('claude', ['auth', 'status'], { encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'], timeout: 5000 });
236
+ const out = ((r.stdout || '') + (r.stderr || '')).toLowerCase();
237
+ if (out.includes('logged in') || out.includes('authenticated')) claude.authed = true;
238
+ }
239
+
240
+ const codexCheck = spawnSync('which', ['codex'], { encoding: 'utf8', stdio: 'pipe', timeout: 3000 });
241
+ if (codexCheck.status === 0 && codexCheck.stdout.trim()) {
242
+ codex.installed = true;
243
+ const login = spawnSync(codexCheck.stdout.trim(), ['login', 'status'], { encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'], timeout: 5000 });
244
+ const out = ((login.stdout || '') + (login.stderr || '')).toLowerCase();
245
+ const ok = login.status === 0 || (out.includes('logged in') && !out.includes('not logged in'));
246
+ if (ok) codex.authed = true;
247
+ }
248
+
249
+ return { claude, codex };
250
+ }
251
+
252
+ // ─── Session Discovery ────────────────────────────────────────────────────
253
+
254
+ function getRecentSessions() {
255
+ const cutoff = Date.now() - 24 * 60 * 60 * 1000;
256
+ const sessions = new Map();
257
+
258
+ const isRealPrompt = (txt) => {
259
+ if (!txt) return false;
260
+ const t = txt.trim();
261
+ if (!t) return false;
262
+ if (/^[✅❌📦🔗⚠️🚀🎉🔧📝]/.test(t)) return false;
263
+ if (/Claude (history|binary|versions) symlink/.test(t)) return false;
264
+ if (t.startsWith('# AGENTS.md')) return false;
265
+ return true;
266
+ };
267
+
268
+ const historyFile = join(HOME, '.claude', 'history.jsonl');
269
+ if (existsSync(historyFile)) {
270
+ try {
271
+ const lines = readFileSync(historyFile, 'utf8').trim().split('\n');
272
+ const entries = [];
273
+ for (const line of lines) {
274
+ try { const j = JSON.parse(line); if (j.sessionId && j.timestamp) entries.push(j); } catch {}
275
+ }
276
+ entries.sort((a, b) => a.timestamp - b.timestamp);
277
+ for (const j of entries) {
278
+ const key = 'claude:' + j.sessionId;
279
+ if (!sessions.has(key)) {
280
+ sessions.set(key, { tool: 'claude', id: j.sessionId, firstSeen: j.timestamp, lastSeen: j.timestamp, firstPrompt: '' });
281
+ }
282
+ const s = sessions.get(key);
283
+ if (j.timestamp < s.firstSeen) s.firstSeen = j.timestamp;
284
+ if (j.timestamp > s.lastSeen) s.lastSeen = j.timestamp;
285
+ if (!s.firstPrompt && isRealPrompt(j.display)) s.firstPrompt = j.display;
286
+ }
287
+ for (const [key, s] of sessions) {
288
+ if (s.tool === 'claude' && !s.firstPrompt) sessions.delete(key);
289
+ }
290
+ } catch {}
291
+ }
292
+
293
+ const codexDir = join(HOME, '.codex', 'sessions');
294
+ if (existsSync(codexDir)) {
295
+ const walk = (dir) => {
296
+ let results = [];
297
+ try {
298
+ for (const entry of readdirSync(dir, { withFileTypes: true })) {
299
+ const full = join(dir, entry.name);
300
+ if (entry.isDirectory()) results = results.concat(walk(full));
301
+ else if (entry.isFile() && entry.name.endsWith('.jsonl')) results.push(full);
302
+ }
303
+ } catch {}
304
+ return results;
305
+ };
306
+ for (const f of walk(codexDir)) {
307
+ try {
308
+ const stat = statSync(f);
309
+ if (stat.mtimeMs < cutoff) continue;
310
+ const content = readFileSync(f, 'utf8');
311
+ const lns = content.trim().split('\n');
312
+ if (!lns.length) continue;
313
+ const meta = JSON.parse(lns[0]);
314
+ if (meta.type !== 'session_meta' || !meta.payload) continue;
315
+ if (meta.payload.cwd !== CWD) continue;
316
+ const id = meta.payload.id;
317
+ const firstTs = Date.parse(meta.payload.timestamp || meta.timestamp);
318
+ let lastTs = firstTs;
319
+ let firstPrompt = '';
320
+ let realMsgCount = 0;
321
+ for (const ln of lns) {
322
+ try {
323
+ const j = JSON.parse(ln);
324
+ if (j.timestamp) lastTs = Math.max(lastTs, Date.parse(j.timestamp));
325
+ if (j.type === 'event_msg' && j.payload?.type === 'user_message') {
326
+ const text = (j.payload.message || '').trim();
327
+ if (text) { if (!firstPrompt) firstPrompt = text; realMsgCount++; }
328
+ }
329
+ } catch {}
330
+ }
331
+ if (realMsgCount === 0 || !firstPrompt) continue;
332
+ if (/^(you are |you're |\*\*role\*\*|<role>|## role)/i.test(firstPrompt)) continue;
333
+ if (realMsgCount === 1 && firstPrompt.length > 500) continue;
334
+ sessions.set('codex:' + id, { tool: 'codex', id, firstSeen: firstTs, lastSeen: lastTs, firstPrompt });
335
+ } catch {}
336
+ }
337
+ }
338
+
339
+ return Array.from(sessions.values())
340
+ .filter(s => (s.lastSeen || 0) >= cutoff)
341
+ .sort((a, b) => (b.lastSeen || 0) - (a.lastSeen || 0))
342
+ .slice(0, 9);
343
+ }
344
+
345
+ function timeAgo(ts) {
346
+ const mins = Math.round((Date.now() - ts) / 60000);
347
+ if (mins < 1) return 'just now';
348
+ if (mins < 60) return mins + 'm ago';
349
+ const h = Math.round(mins / 60);
350
+ return h + 'h ago';
351
+ }
352
+
353
+ function snippet(s, n = 35) {
354
+ const clean = (s || '').replace(/\s+/g, ' ').trim();
355
+ return clean.length > n ? clean.slice(0, n - 1) + '…' : clean;
356
+ }
357
+
358
+ function countRunning() {
359
+ let claude = 0, codex = 0;
360
+ try {
361
+ const r = spawnSync('pgrep', ['-x', 'claude'], { encoding: 'utf8', stdio: 'pipe', timeout: 2000 });
362
+ claude = (r.stdout || '').trim().split('\n').filter(Boolean).length;
363
+ } catch {}
364
+ try {
365
+ const r = spawnSync('pgrep', ['-x', 'codex'], { encoding: 'utf8', stdio: 'pipe', timeout: 2000 });
366
+ codex = (r.stdout || '').trim().split('\n').filter(Boolean).length;
367
+ } catch {}
368
+ return { claude, codex };
369
+ }
370
+
371
+ // ─── Provider Balance ─────────────────────────────────────────────────────
372
+
373
+ function loadProviderBalance() {
374
+ const today = new Date().toISOString().slice(0, 10);
375
+ const logFile = join(__dirname, `usage-${today}.jsonl`);
376
+ if (!existsSync(logFile)) return { claude: 0, openai: 0, total: 0, label: 'No activity yet' };
377
+
378
+ let claude = 0, openai = 0;
379
+ try {
380
+ const lines = readFileSync(logFile, 'utf8').split('\n').filter(Boolean);
381
+ for (const line of lines) {
382
+ try {
383
+ const e = JSON.parse(line);
384
+ if (e.provider === 'claude') claude++;
385
+ else if (e.provider === 'openai') openai++;
386
+ } catch {}
387
+ }
388
+ } catch {}
389
+
390
+ const total = claude + openai;
391
+ if (total === 0) return { claude: 0, openai: 0, total: 0, label: 'No activity yet' };
392
+
393
+ const claudePct = Math.round((claude / total) * 100);
394
+ const openaiPct = 100 - claudePct;
395
+
396
+ let label;
397
+ if (openaiPct === 0) label = 'Claude only — GPT subscription unused';
398
+ else if (claudePct === 0) label = 'GPT only — Claude subscription unused';
399
+ else if (Math.abs(claudePct - openaiPct) <= 20) label = 'Well balanced';
400
+ else if (claudePct > openaiPct) label = `Claude-heavy — GPT has capacity`;
401
+ else label = `GPT-heavy — Claude has capacity`;
402
+
403
+ return { claude: claudePct, openai: openaiPct, total, label };
404
+ }
405
+
406
+ function balanceBar(claudePct, openaiPct, width = 20) {
407
+ if (claudePct === 0 && openaiPct === 0) return dim('░'.repeat(width) + ' no activity');
408
+ const cFill = Math.round((claudePct / 100) * width);
409
+ const oFill = width - cFill;
410
+ const cBar = noColor ? '█'.repeat(cFill) : `\x1b[38;5;208m${'█'.repeat(cFill)}\x1b[0m`;
411
+ const oBar = noColor ? '▓'.repeat(oFill) : `\x1b[32m${'▓'.repeat(oFill)}\x1b[0m`;
412
+ return `${cBar}${oBar} ${orange(claudePct + '%')} Claude · ${green(openaiPct + '%')} GPT`;
413
+ }
414
+
415
+ // ─── Auth Detail Helpers ──────────────────────────────────────────────────
416
+
417
+ function getClaudeAuthDetail() {
418
+ const credPaths = [
419
+ join(HOME, '.claude', '.credentials.json'),
420
+ join(HOME, '.claude', 'credentials.json'),
421
+ join(CWD, '.replit-tools', '.claude-persistent', '.credentials.json'),
422
+ ];
423
+ for (const p of credPaths) {
424
+ try {
425
+ const cred = JSON.parse(readFileSync(p, 'utf8'));
426
+ if (cred.claudeAiOauth) {
427
+ const exp = cred.claudeAiOauth.expiresAt;
428
+ let expiryText = 'n/a';
429
+ if (exp) {
430
+ const remaining = exp - Date.now();
431
+ if (remaining <= 0) expiryText = 'expired';
432
+ else {
433
+ const h = Math.floor(remaining / 3600000);
434
+ const m = Math.floor((remaining % 3600000) / 60000);
435
+ expiryText = `${h}h ${m}m remaining`;
436
+ }
437
+ }
438
+ return { method: 'subscription (OAuth)', expiry: expiryText, storage: p.replace(HOME, '~') };
439
+ }
440
+ if (cred.apiKey) return { method: 'API key', expiry: 'n/a', storage: p.replace(HOME, '~') };
441
+ } catch {}
442
+ }
443
+ return { method: 'unknown', expiry: 'n/a', storage: 'n/a' };
444
+ }
445
+
446
+ function getCodexAuthDetail() {
447
+ const authPath = join(HOME, '.codex', 'auth.json');
448
+ try {
449
+ const stat = statSync(authPath);
450
+ return {
451
+ method: 'subscription (device-auth)',
452
+ lastRefresh: timeAgo(stat.mtimeMs),
453
+ storage: '~/.codex/auth.json',
454
+ };
455
+ } catch {}
456
+ return { method: 'unknown', lastRefresh: 'n/a', storage: 'n/a' };
457
+ }
458
+
459
+ // ─── Submenu: Auth ────────────────────────────────────────────────────────
460
+
461
+ async function showAuthMenu(rl, providers) {
462
+ const ask = () => new Promise(resolve => rl.question(' Choice: ', resolve));
463
+
464
+ while (true) {
465
+ const claudeDetail = getClaudeAuthDetail();
466
+ const codexDetail = getCodexAuthDetail();
467
+ const cStat = providers.claude.authed ? green('✅ authenticated') : yellow('❌ not authenticated');
468
+ const xStat = providers.codex.authed ? green('✅ authenticated') : yellow('❌ not authenticated');
469
+
470
+ console.log('');
471
+ console.log(` ${bold('🔑 Auth Management')}`);
472
+ console.log(' ' + '─'.repeat(44));
473
+ console.log(` 🟠 Claude ${cStat}`);
474
+ if (providers.claude.authed) {
475
+ console.log(` Method: ${dim(claudeDetail.method)}`);
476
+ console.log(` Expiry: ${dim(claudeDetail.expiry)}`);
477
+ console.log(` Storage: ${dim(claudeDetail.storage)}`);
478
+ }
479
+ console.log('');
480
+ console.log(` 🟢 Codex ${xStat}`);
481
+ if (providers.codex.authed) {
482
+ console.log(` Method: ${dim(codexDetail.method)}`);
483
+ console.log(` Refresh: ${dim(codexDetail.lastRefresh)}`);
484
+ console.log(` Storage: ${dim(codexDetail.storage)}`);
485
+ }
486
+ console.log('');
487
+ if (!providers.claude.authed) console.log(` ${bold('[j]')} Sign in to Claude`);
488
+ if (providers.codex.installed && !providers.codex.authed) console.log(` ${bold('[k]')} Sign in to Codex ${dim('(ChatGPT subscription)')}`);
489
+ if (providers.claude.authed || providers.codex.authed) console.log(` ${bold('[r]')} Refresh all tokens`);
490
+ console.log(` ${bold('[q]')} Back to main menu`);
491
+ console.log('');
492
+
493
+ const choice = (await ask()).trim().toLowerCase();
494
+ if (choice === 'q' || choice === '') return;
495
+
496
+ if (choice === 'j') {
497
+ console.log('');
498
+ const r = spawnSync('claude', ['login'], { stdio: 'inherit' });
499
+ const fresh = detectProviders();
500
+ providers.claude = fresh.claude;
501
+ if (providers.claude.authed) {
502
+ console.log(` ${green('Claude authenticated.')}`);
503
+ } else {
504
+ console.log(` ${yellow('Claude login did not complete.')} Try again or check your subscription.`);
505
+ }
506
+ continue;
507
+ }
508
+ if (choice === 'k' && providers.codex.installed) {
509
+ const codexPath = spawnSync('which', ['codex'], { encoding: 'utf8', stdio: 'pipe', timeout: 3000 });
510
+ if (codexPath.status === 0) {
511
+ console.log('');
512
+ console.log(` Open: ${cyan('https://auth.openai.com/codex/device')}`);
513
+ console.log('');
514
+ spawnSync(codexPath.stdout.trim(), ['login', '--device-auth'], { stdio: 'inherit' });
515
+ const fresh = detectProviders();
516
+ providers.codex = fresh.codex;
517
+ if (providers.codex.authed) {
518
+ console.log(` ${green('Codex authenticated.')}`);
519
+ } else {
520
+ console.log(` ${yellow('Codex login did not complete.')} Try again.`);
521
+ }
522
+ }
523
+ continue;
524
+ }
525
+ if (choice === 'r') {
526
+ console.log('');
527
+ const refreshScript = join(CWD, '.replit-tools', 'scripts', 'claude-auth-refresh.sh');
528
+ if (existsSync(refreshScript)) {
529
+ console.log(' Refreshing Claude token...');
530
+ const r = spawnSync('bash', [refreshScript, '--force'], { encoding: 'utf8', stdio: 'pipe', timeout: 10000 });
531
+ console.log(` ${(r.stdout || '').trim() || 'Done'}`);
532
+ }
533
+ console.log(' Codex tokens refreshed on next API call.');
534
+ console.log('');
535
+ continue;
536
+ }
537
+ }
538
+ }
539
+
540
+ // ─── Submenu: Budget ──────────────────────────────────────────────────────
541
+
542
+ async function showBudgetMenu(rl) {
543
+ const ask = () => new Promise(resolve => rl.question(' Choice: ', resolve));
544
+
545
+ while (true) {
546
+ const profile = loadProfile();
547
+ const balance = loadProviderBalance();
548
+
549
+ console.log('');
550
+ console.log(` ${bold('💵 Budget & Spend')}`);
551
+ console.log(' ' + '─'.repeat(44));
552
+ console.log(` Session: ⚠️ $${profile.budgets.session_warn_usd} warn · 🛑 $${profile.budgets.session_limit_usd} limit`);
553
+ console.log(` Daily: ⚠️ $${profile.budgets.daily_warn_usd} warn · 🛑 $${profile.budgets.daily_limit_usd} limit`);
554
+ console.log('');
555
+ console.log(` Today: ${balance.total} calls · ${balance.label}`);
556
+ console.log(` ${balanceBar(balance.claude, balance.openai)}`);
557
+ console.log('');
558
+ console.log(` ${bold('[c]')} Change budget limits`);
559
+ console.log(` ${bold('[r]')} Full cost report`);
560
+ console.log(` ${bold('[q]')} Back to main menu`);
561
+ console.log('');
562
+
563
+ const choice = (await ask()).trim().toLowerCase();
564
+ if (choice === 'q' || choice === '') return;
565
+
566
+ if (choice === 'c') {
567
+ const sessionAns = await new Promise(r => rl.question(' New session limit ($): ', r));
568
+ const sessionVal = parseFloat(sessionAns);
569
+ if (isNaN(sessionVal) || sessionVal <= 0) { console.log(' Invalid number.'); continue; }
570
+ const dailyAns = await new Promise(r => rl.question(` New daily limit ($ default ${sessionVal * 3}): `, r));
571
+ const dailyVal = dailyAns.trim() ? parseFloat(dailyAns) : sessionVal * 3;
572
+ if (isNaN(dailyVal) || dailyVal <= 0) { console.log(' Invalid number.'); continue; }
573
+
574
+ const customOverrides = {
575
+ budgets: {
576
+ session_warn_usd: +(sessionVal * 0.6).toFixed(2),
577
+ session_limit_usd: sessionVal,
578
+ daily_warn_usd: +(dailyVal * 0.6).toFixed(2),
579
+ daily_limit_usd: dailyVal,
580
+ },
581
+ };
582
+ let existing = {};
583
+ try { existing = JSON.parse(readFileSync(PROFILE_FILE, 'utf8')); } catch {}
584
+ saveProfile(existing.active || 'auto', customOverrides);
585
+ console.log(` ✅ Budget updated: $${sessionVal}/session, $${dailyVal}/day`);
586
+ continue;
587
+ }
588
+
589
+ if (choice === 'r') {
590
+ console.log('');
591
+ spawnSync(process.execPath, [join(__dirname, 'cost-report.mjs')], { stdio: 'inherit' });
592
+ console.log('');
593
+ await new Promise(r => rl.question(' Press Enter to continue...', r));
594
+ continue;
595
+ }
596
+ }
597
+ }
598
+
599
+ // ─── Submenu: Tools Dashboard ─────────────────────────────────────────────
600
+
601
+ async function showToolsMenu(rl) {
602
+ const ask = () => new Promise(resolve => rl.question(' Choice: ', resolve));
603
+
604
+ while (true) {
605
+ const updateInfo = checkForUpdate();
606
+ console.log('');
607
+ console.log(` ${bold('🛠️ Tools & Diagnostics')}`);
608
+ console.log(' ' + '─'.repeat(44));
609
+ console.log(` ${bold('[1]')} Health check`);
610
+ console.log(` ${bold('[2]')} Cost report`);
611
+ console.log(` ${bold('[3]')} Decision ledger insights`);
612
+ console.log(` ${bold('[4]')} Run test suite (40 tests)`);
613
+ console.log(` ${bold('[5]')} Session report`);
614
+ console.log(` ${bold('[u]')} Update Dual Brain ${dim('(' + formatVersionStatus(updateInfo) + ')')}`);
615
+ console.log(` ${bold('[q]')} Back to main menu`);
616
+ console.log('');
617
+
618
+ const choice = (await ask()).trim().toLowerCase();
619
+ if (choice === 'q' || choice === '') return;
620
+
621
+ const tools = {
622
+ '1': 'health-check.mjs',
623
+ '2': 'cost-report.mjs',
624
+ '3': 'decision-ledger.mjs',
625
+ '4': 'test-orchestrator.mjs',
626
+ '5': 'session-report.mjs',
627
+ };
628
+
629
+ if (tools[choice]) {
630
+ console.log('');
631
+ spawnSync(process.execPath, [join(__dirname, tools[choice])], { stdio: 'inherit' });
632
+ console.log('');
633
+ await new Promise(r => rl.question(' Press Enter to continue...', r));
634
+ continue;
635
+ }
636
+
637
+ if (choice === 'u') {
638
+ console.log('');
639
+ const result = spawnSync('npx', ['-y', 'dual-brain', 'update'], { stdio: 'inherit', cwd: CWD });
640
+ console.log('');
641
+ if (result.status === 0) {
642
+ console.log(' ✅ Dual-brain hooks refreshed.');
643
+ } else {
644
+ console.log(' ⚠️ Update did not complete.');
645
+ }
646
+ console.log('');
647
+ await new Promise(r => rl.question(' Press Enter to continue...', r));
648
+ }
649
+ }
650
+ }
651
+
652
+ // ─── Submenu: Vibe Workflow ───────────────────────────────────────────────
653
+
654
+ async function showVibeWorkflow(rl) {
655
+ console.log('');
656
+ console.log(` ${bold('Vibe Workflow')} ${dim('— describe what you want, we orchestrate it')}`);
657
+ console.log('');
658
+ console.log(` Tell us what to build, fix, or change in plain English.`);
659
+ console.log(` The wave orchestrator will plan, dispatch agents, test, and review.`);
660
+ console.log('');
661
+
662
+ const utterance = await new Promise(resolve => {
663
+ rl.question(` ${bold('What do you want?')} `, resolve);
664
+ });
665
+
666
+ const trimmed = utterance.trim();
667
+ if (!trimmed || trimmed === 'q') return;
668
+
669
+ // Ask dry-run or execute
670
+ console.log('');
671
+ const mode = await new Promise(resolve => {
672
+ rl.question(` ${bold('[d]')} Dry run (plan only) ${bold('[g]')} Go (execute) ${bold('[q]')} Cancel: `, resolve);
673
+ });
674
+
675
+ const modeChoice = mode.trim().toLowerCase();
676
+ if (modeChoice === 'q' || !modeChoice) return;
677
+
678
+ const isDryRun = modeChoice === 'd';
679
+ const args = isDryRun
680
+ ? ['hooks/wave-orchestrator.mjs', '--dry-run', trimmed]
681
+ : ['hooks/wave-orchestrator.mjs', trimmed];
682
+
683
+ console.log('');
684
+ console.log(` ${isDryRun ? 'Planning' : 'Orchestrating'}...`);
685
+ console.log('');
686
+
687
+ const result = spawnSync('node', args, {
688
+ cwd: join(__dirname, '..'),
689
+ stdio: 'inherit',
690
+ encoding: 'utf8',
691
+ timeout: 600_000,
692
+ });
693
+
694
+ if (result.status !== 0) {
695
+ console.log('');
696
+ console.log(` ${noColor ? '[!]' : '⚠️'} Wave orchestrator exited with code ${result.status}`);
697
+ if (result.error) console.log(` ${dim(result.error.message)}`);
698
+ }
699
+
700
+ console.log('');
701
+ const next = await new Promise(resolve => {
702
+ rl.question(` Press Enter to return to menu...`, resolve);
703
+ });
704
+ }
705
+
706
+ // ─── Menu Renderers ───────────────────────────────────────────────────────
707
+
708
+ function renderFirstRunMenu(providers) {
709
+ const lines = [];
710
+ const updateInfo = checkForUpdate();
711
+
712
+ lines.push('');
713
+ lines.push(` 🧠 ${bold('Data Tools')} ${dim('—')} ${bold('Dual Brain')} ${dim(`v${VERSION}`)}`);
714
+ lines.push(` ${dim(formatVersionStatus(updateInfo))}`);
715
+ lines.push(` ${dim('Powered by replit-tools by Steve Moraco')}`);
716
+ lines.push('');
717
+
718
+ // Provider status
719
+ const cStat = providers.claude.authed ? (noColor ? '[OK]' : '✅') : providers.claude.installed ? (noColor ? '[!]' : '⚠️') : (noColor ? '[X]' : '❌');
720
+ const xStat = providers.codex.authed ? (noColor ? '[OK]' : '✅') : providers.codex.installed ? (noColor ? '[!]' : '⚠️') : (noColor ? '[X]' : '❌');
721
+ lines.push(` ${noColor ? '' : '🟠 '}Claude ${cStat} ${noColor ? '' : '🟢 '}Codex ${xStat}`);
722
+
723
+ if (providers.claude.authed && providers.codex.authed) {
724
+ lines.push(` ${green('Both providers ready — full dual-brain mode')}`);
725
+ } else if (providers.claude.authed) {
726
+ lines.push(` ${dim('Claude ready. Add Codex for dual-brain features.')}`);
727
+ } else if (!providers.claude.installed) {
728
+ lines.push(` ${yellow('Claude not found — needed to start.')}`);
729
+ } else {
730
+ lines.push(` ${yellow('Claude needs login to start.')}`);
731
+ }
732
+
733
+ lines.push('');
734
+
735
+ // Auth actions if needed
736
+ if (!providers.claude.authed || !providers.codex.authed) {
737
+ if (!providers.claude.installed) {
738
+ lines.push(` ${dim('Install Claude:')} ${cyan('curl -fsSL https://claude.ai/install.sh | sh')}`);
739
+ }
740
+ if (!providers.claude.authed && providers.claude.installed) {
741
+ lines.push(` ${bold('[j]')} Sign in to Claude`);
742
+ }
743
+ if (!providers.codex.installed) {
744
+ lines.push(` ${dim('Install Codex:')} ${cyan('npm i -g @openai/codex')}`);
745
+ } else if (!providers.codex.authed) {
746
+ lines.push(` ${bold('[k]')} Sign in to Codex ${dim('(optional — enables GPT collaboration)')}`);
747
+ }
748
+ lines.push('');
749
+ }
750
+
751
+ // Data Tools shortcut
752
+ if (IS_REPLIT && existsSync(join(CWD, '.replit-tools'))) {
753
+ lines.push(` ${bold('[t]')} Open Data Tools dashboard`);
754
+ } else if (IS_REPLIT) {
755
+ lines.push(` ${bold('[t]')} Install replit-tools ${dim('(recommended for Replit)')}`);
756
+ }
757
+
758
+ // Primary actions
759
+ lines.push(` ${bold('[n]')} Start new session`);
760
+ lines.push(` ${bold('[w]')} Vibe workflow ${dim('(natural language → orchestrated work)')}`);
761
+ lines.push(` ${bold('[a]')} Auth management`);
762
+ lines.push(` ${bold('[d]')} Dashboard & diagnostics`);
763
+ lines.push(` ${bold('[s]')} Skip — just shell`);
764
+ lines.push(` ${dim('Enter = new session · [?] help')}`);
765
+
766
+ lines.push('');
767
+
768
+ return lines;
769
+ }
770
+
771
+ function renderReturningMenu(providers, sessions) {
772
+ const profile = loadProfile();
773
+ const permissions = loadPermissions();
774
+ const pf = PROFILES[profile.name];
775
+ const running = countRunning();
776
+ const balance = loadProviderBalance();
777
+ const updateInfo = checkForUpdate();
778
+ const lines = [];
779
+
780
+ lines.push('');
781
+ lines.push(` 🧠 ${bold('Data Tools')} ${dim('—')} ${bold('Dual Brain')} ${dim(`v${VERSION}`)}`);
782
+ lines.push(` ${dim(formatVersionStatus(updateInfo))}`);
783
+ lines.push(` ${dim('Powered by replit-tools by Steve Moraco')}`);
784
+ lines.push('');
785
+
786
+ // Provider status
787
+ const cStat = providers.claude.authed ? (noColor ? '[OK]' : '✅') : (noColor ? '[!]' : '⚠️');
788
+ const xStat = providers.codex.authed ? (noColor ? '[OK]' : '✅') : providers.codex.installed ? (noColor ? '[!]' : '⚠️') : (noColor ? '[X]' : '❌');
789
+ let modeStatus = pf.uiLabel;
790
+ if (profile.name === 'auto') {
791
+ if (balance.total === 0) {
792
+ modeStatus = 'Auto · learning your workflow';
793
+ } else if (balance.openai > balance.claude + 20) {
794
+ modeStatus = 'Auto · routing GPT for isolated work';
795
+ } else if (balance.claude > balance.openai + 20) {
796
+ modeStatus = 'Auto · Claude-primary, GPT available';
797
+ } else {
798
+ modeStatus = 'Auto · balanced routing active';
799
+ }
800
+ }
801
+ lines.push(` 🟠 Claude ${cStat} 🟢 Codex ${xStat} ${pf.emoji} ${bold(modeStatus)}`);
802
+
803
+ // Provider balance bar
804
+ lines.push(` ${balanceBar(balance.claude, balance.openai)}`);
805
+ if (balance.total > 0) lines.push(` ${dim(balance.label + ' · ' + balance.total + ' calls today')}`);
806
+
807
+ // Recent sessions
808
+ if (sessions.length > 0) {
809
+ lines.push('');
810
+ lines.push(` ${bold('Recent (last 24h):')}`);
811
+ for (let i = 0; i < sessions.length; i++) {
812
+ const s = sessions[i];
813
+ const num = String(i + 1);
814
+ const toolLabel = s.tool === 'codex' ? orange('cdx') : blue('cld');
815
+ const ago = timeAgo(s.lastSeen).padEnd(9);
816
+ lines.push(` ${bold('[' + num + ']')} ${toolLabel} ${dim(ago)} ${snippet(s.firstPrompt)}`);
817
+ }
818
+ }
819
+
820
+ lines.push('');
821
+
822
+ const runParts = [];
823
+ if (running.claude > 0) runParts.push(`${running.claude} claude`);
824
+ if (running.codex > 0) runParts.push(`${running.codex} codex`);
825
+ if (runParts.length > 0) lines.push(` ${dim('(' + runParts.join(', ') + ' running)')}`);
826
+
827
+ // ── Sessions
828
+ lines.push(` ${dim('─── Sessions')}`);
829
+ lines.push(` ${bold('[c]')} Continue last session`);
830
+ if (sessions.length > 0) lines.push(` ${bold('[1-9]')} Resume numbered above`);
831
+ lines.push(` ${bold('[n]')} New session`);
832
+ lines.push(` ${bold('[w]')} Vibe workflow ${dim('(say what you want, we handle the rest)')}`);
833
+
834
+ // ── Settings
835
+ lines.push('');
836
+ lines.push(` ${dim('─── Settings')}`);
837
+ lines.push(` ${bold('[p]')} Mode: ${dim(pf.uiLabel)}`);
838
+ lines.push(` ${bold('[b]')} Budget: ${dim('$' + profile.budgets.session_limit_usd + '/session, $' + profile.budgets.daily_limit_usd + '/day')}`);
839
+ lines.push(` ${bold('[x]')} Permissions: ${dim(permissions.claude_skip_permissions || permissions.codex_bypass_sandbox ? 'skip-permissions enabled' : 'safe mode')}`);
840
+
841
+ // ── Auth
842
+ lines.push('');
843
+ const authSummary = providers.claude.authed && providers.codex.authed
844
+ ? green('both connected')
845
+ : providers.claude.authed ? yellow('Claude only')
846
+ : yellow('needs setup');
847
+ lines.push(` ${dim('─── Auth')}`);
848
+ lines.push(` ${bold('[a]')} Auth management ${dim('(' + authSummary + ')')}`);
849
+
850
+ // ── Tools
851
+ lines.push('');
852
+ lines.push(` ${dim('─── Tools')}`);
853
+ lines.push(` ${bold('[d]')} Dashboard & diagnostics`);
854
+ lines.push(` ${bold('[u]')} Update Dual Brain ${dim('(' + formatVersionStatus(updateInfo) + ')')}`);
855
+
856
+ if (IS_REPLIT && existsSync(join(CWD, '.replit-tools'))) {
857
+ lines.push(` ${bold('[t]')} Open Data Tools dashboard`);
858
+ } else if (IS_REPLIT) {
859
+ lines.push(` ${bold('[t]')} Install replit-tools`);
860
+ }
861
+
862
+ lines.push('');
863
+ lines.push(` ${bold('[s]')} Exit to shell`);
864
+ if (sessions.length > 0) {
865
+ lines.push(` ${dim('Enter = continue last · [?] help')}`);
866
+ } else {
867
+ lines.push(` ${dim('Enter = new session · [?] help')}`);
868
+ }
869
+ lines.push('');
870
+
871
+ return lines;
872
+ }
873
+
874
+ // ─── Profile Picker ───────────────────────────────────────────────────────
875
+
876
+ function showProfilePicker(rl) {
877
+ return new Promise((resolve) => {
878
+ const current = loadProfile();
879
+ const balance = loadProviderBalance();
880
+ console.log('');
881
+ console.log(` ${bold('Switch routing mode:')}`);
882
+ if (balance.total > 0) {
883
+ console.log(` ${dim('Current balance: Claude ' + balance.claude + '% / GPT ' + balance.openai + '% · ' + balance.label)}`);
884
+ }
885
+ console.log('');
886
+ for (const [i, [name, pf]] of Object.entries(PROFILES).entries()) {
887
+ const active = name === current.name ? ' ✅' : '';
888
+ const recommended = name === 'auto' && current.name !== 'auto' ? dim(' (recommended)') : '';
889
+ console.log(` ${bold('[' + (i + 1) + ']')} ${pf.emoji} ${pf.uiLabel.padEnd(15)} ${dim(pf.desc)}${active}${recommended}`);
890
+ }
891
+ console.log(` ${bold('[q]')} Cancel`);
892
+ console.log('');
893
+
894
+ rl.question(' Choice: ', (answer) => {
895
+ const names = Object.keys(PROFILES);
896
+ const trimmed = answer.trim();
897
+ let selectedName = null;
898
+
899
+ // Try numeric selection first
900
+ const idx = parseInt(trimmed, 10) - 1;
901
+ if (idx >= 0 && idx < names.length) {
902
+ selectedName = names[idx];
903
+ }
904
+
905
+ // Try natural language alias resolution
906
+ if (!selectedName && trimmed && trimmed !== 'q') {
907
+ const PANEL_ALIASES = {
908
+ 'auto': 'auto', 'adaptive': 'auto', 'smart': 'auto', 'default': 'auto', 'normal': 'auto',
909
+ 'balanced': 'balanced', 'even': 'balanced', 'equal': 'balanced',
910
+ 'cost-saver': 'cost-saver', 'cheap': 'cost-saver', 'save': 'cost-saver', 'conservative': 'cost-saver', 'frugal': 'cost-saver', 'budget': 'cost-saver',
911
+ 'quality-first': 'quality-first', 'aggressive': 'quality-first', 'quality': 'quality-first', 'max': 'quality-first', 'full': 'quality-first', 'both': 'quality-first',
912
+ };
913
+ const cleaned = trimmed.toLowerCase()
914
+ .replace(/^(go|be|use|switch to|set|mode)\s+/i, '')
915
+ .replace(/\s+mode$/i, '');
916
+ selectedName = PANEL_ALIASES[cleaned] || null;
917
+ }
918
+
919
+ if (selectedName) {
920
+ let customOverrides = null;
921
+ try {
922
+ const existing = JSON.parse(readFileSync(PROFILE_FILE, 'utf8'));
923
+ if (existing.custom_overrides?.budgets) customOverrides = { budgets: existing.custom_overrides.budgets };
924
+ } catch {}
925
+ saveProfile(selectedName, customOverrides);
926
+ const pf = PROFILES[selectedName];
927
+ console.log(` ✅ Switched to ${pf.emoji} ${pf.uiLabel}`);
928
+ } else if (trimmed && trimmed !== 'q') {
929
+ console.log(` Unknown profile: ${trimmed}. Try: cheap, aggressive, quality, balanced, auto`);
930
+ }
931
+ resolve();
932
+ });
933
+ });
934
+ }
935
+
936
+ // (Cost alert editor removed — replaced by provider balance + mode switching)
937
+
938
+ // ─── Session Runner ───────────────────────────────────────────────────────
939
+
940
+ function runSession(cmd, args, label) {
941
+ const permissions = loadPermissions();
942
+ const finalArgs = [...args];
943
+ if (cmd === 'claude' && permissions.claude_skip_permissions) {
944
+ finalArgs.push('--dangerously-skip-permissions');
945
+ }
946
+ if (cmd === 'codex' && permissions.codex_bypass_sandbox) {
947
+ finalArgs.push('--dangerously-bypass-approvals-and-sandbox');
948
+ }
949
+
950
+ console.log('');
951
+ console.log(` ${label}`);
952
+ console.log(` ${dim('Inside Claude: press Ctrl+C twice to return here.')}`);
953
+ console.log('');
954
+ markLaunched();
955
+ const result = spawnSync(cmd, finalArgs, { stdio: 'inherit' });
956
+ console.log('');
957
+ if (result.status !== 0 && result.status !== null) {
958
+ console.log(` ${yellow('Session exited with code ' + result.status + '.')} ${dim('(' + cmd + ' ' + finalArgs.join(' ') + ')')}`);
959
+ }
960
+ console.log(' Returned to Data Tools — Dual Brain.');
961
+ return result.status || 0;
962
+ }
963
+
964
+ // ─── Main Loop ────────────────────────────────────────────────────────────
965
+
966
+ async function mainLoop() {
967
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
968
+ const ask = () => new Promise(resolve => rl.question(' Choice: ', resolve));
969
+
970
+ while (true) {
971
+ const firstRun = isFirstRun();
972
+ const providers = detectProviders();
973
+ const sessions = firstRun ? [] : getRecentSessions();
974
+
975
+ const lines = firstRun
976
+ ? renderFirstRunMenu(providers)
977
+ : renderReturningMenu(providers, sessions);
978
+
979
+ for (const l of lines) console.log(l);
980
+
981
+ const choice = (await ask()).trim().toLowerCase();
982
+
983
+ if (choice === 's' || choice === 'q') {
984
+ console.log('');
985
+ rl.close();
986
+ return;
987
+ }
988
+
989
+ if (choice === 'c' || choice === '') {
990
+ if (sessions.length > 0) {
991
+ const s = sessions[0];
992
+ if (s.tool === 'codex') {
993
+ runSession('codex', ['resume', s.id], `Resuming codex ${s.id.slice(0, 8)}...`);
994
+ } else {
995
+ runSession('claude', ['-r', s.id], `Resuming session ${s.id.slice(0, 8)}...`);
996
+ }
997
+ } else if (!providers.claude.authed && !providers.claude.installed) {
998
+ console.log('');
999
+ console.log(` ${yellow('Claude is not installed.')} Install first:`);
1000
+ console.log(` ${cyan('curl -fsSL https://claude.ai/install.sh | sh')}`);
1001
+ console.log('');
1002
+ } else if (!providers.claude.authed) {
1003
+ console.log('');
1004
+ console.log(` ${yellow('Claude is not authenticated.')} Press ${bold('[j]')} to sign in first.`);
1005
+ console.log('');
1006
+ } else {
1007
+ runSession('claude', [], 'Starting new session...');
1008
+ }
1009
+ continue;
1010
+ }
1011
+
1012
+ const num = parseInt(choice, 10);
1013
+ if (num >= 1 && num <= 9 && sessions[num - 1]) {
1014
+ const s = sessions[num - 1];
1015
+ if (s.tool === 'codex') {
1016
+ runSession('codex', ['resume', s.id], `Resuming codex ${s.id.slice(0, 8)}...`);
1017
+ } else {
1018
+ runSession('claude', ['-r', s.id], `Resuming session ${s.id.slice(0, 8)}...`);
1019
+ }
1020
+ continue;
1021
+ }
1022
+
1023
+ if (choice === 'w') {
1024
+ await showVibeWorkflow(rl);
1025
+ continue;
1026
+ }
1027
+
1028
+ if (choice === 'n') {
1029
+ if (!providers.claude.authed) {
1030
+ console.log('');
1031
+ console.log(` ${yellow('Claude needs to be authenticated first.')} Press ${bold('[j]')} to sign in.`);
1032
+ console.log('');
1033
+ } else {
1034
+ runSession('claude', [], 'Starting new session...');
1035
+ }
1036
+ continue;
1037
+ }
1038
+
1039
+ if (choice === 'p') {
1040
+ await showProfilePicker(rl);
1041
+ continue;
1042
+ }
1043
+
1044
+ if (choice === 'a') {
1045
+ await showAuthMenu(rl, providers);
1046
+ continue;
1047
+ }
1048
+
1049
+ if (choice === 'b') {
1050
+ await showBudgetMenu(rl);
1051
+ continue;
1052
+ }
1053
+
1054
+ if (choice === 'x' && !firstRun) {
1055
+ const permissions = loadPermissions();
1056
+ if (permissions.claude_skip_permissions || permissions.codex_bypass_sandbox) {
1057
+ savePermissions({
1058
+ claude_skip_permissions: false,
1059
+ codex_bypass_sandbox: false,
1060
+ });
1061
+ console.log('');
1062
+ console.log(` ${green('Permissions set to safe mode.')}`);
1063
+ console.log('');
1064
+ continue;
1065
+ }
1066
+
1067
+ console.log('');
1068
+ const confirm = await new Promise(resolve => rl.question(' WARNING: This enables skip-permissions mode for Claude sessions. Type YES to confirm: ', resolve));
1069
+ if (confirm.trim() === 'YES') {
1070
+ savePermissions({
1071
+ claude_skip_permissions: true,
1072
+ codex_bypass_sandbox: true,
1073
+ });
1074
+ console.log(` ${yellow('Skip-permissions mode enabled for Claude and Codex sessions.')}`);
1075
+ } else {
1076
+ console.log(' No changes made. Safe mode remains enabled.');
1077
+ }
1078
+ console.log('');
1079
+ continue;
1080
+ }
1081
+
1082
+ if (choice === 'd') {
1083
+ await showToolsMenu(rl);
1084
+ continue;
1085
+ }
1086
+
1087
+ if (choice === 'u') {
1088
+ console.log('');
1089
+ console.log(' Updating Dual Brain...');
1090
+ console.log('');
1091
+ const upd = spawnSync('npx', ['-y', 'dual-brain', 'update'], { stdio: 'inherit', cwd: CWD });
1092
+ if (upd.status !== 0) {
1093
+ console.log('');
1094
+ console.log(` ${yellow('Update failed (exit ' + upd.status + ').')} Try manually: ${cyan('npx -y dual-brain@latest')}`);
1095
+ console.log('');
1096
+ }
1097
+ continue;
1098
+ }
1099
+
1100
+ if (choice === 't') {
1101
+ const dtPath = join(CWD, '.replit-tools');
1102
+ if (existsSync(dtPath)) {
1103
+ const scriptPath = join(dtPath, 'scripts', 'setup-claude-code.sh');
1104
+ if (existsSync(scriptPath)) {
1105
+ console.log('');
1106
+ console.log(' Opening Data Tools dashboard...');
1107
+ console.log('');
1108
+ spawnSync('bash', [scriptPath], { stdio: 'inherit', cwd: CWD });
1109
+ } else {
1110
+ console.log('');
1111
+ console.log(` Data Tools present but dashboard script missing.`);
1112
+ console.log(` Try: ${cyan('source .replit-tools/scripts/setup-claude-code.sh')}`);
1113
+ console.log('');
1114
+ }
1115
+ } else if (IS_REPLIT) {
1116
+ console.log('');
1117
+ console.log(' Installing replit-tools (Data Tools)...');
1118
+ console.log('');
1119
+ spawnSync('npx', ['-y', 'data-tools'], { stdio: 'inherit', cwd: CWD });
1120
+ console.log('');
1121
+ console.log(' Done. Press Enter to continue...');
1122
+ const askOnce = () => new Promise(resolve => rl.question('', resolve));
1123
+ await askOnce();
1124
+ } else {
1125
+ console.log('');
1126
+ console.log(` Data Tools is designed for Replit environments.`);
1127
+ console.log('');
1128
+ }
1129
+ continue;
1130
+ }
1131
+
1132
+ if (choice === 'j') {
1133
+ console.log('');
1134
+ console.log(' Starting Claude login...');
1135
+ console.log('');
1136
+ spawnSync('claude', ['login'], { stdio: 'inherit' });
1137
+ continue;
1138
+ }
1139
+
1140
+ if (choice === 'k') {
1141
+ const codexPath = spawnSync('which', ['codex'], { encoding: 'utf8', stdio: 'pipe', timeout: 3000 });
1142
+ if (codexPath.status !== 0) {
1143
+ console.log('');
1144
+ console.log(` Codex not installed. Run: ${cyan('npm i -g @openai/codex')}`);
1145
+ console.log('');
1146
+ await ask();
1147
+ continue;
1148
+ }
1149
+ console.log('');
1150
+ console.log(' Starting Codex login...');
1151
+ console.log('');
1152
+ console.log(` Open: ${cyan('https://auth.openai.com/codex/device')}`);
1153
+ console.log('');
1154
+ spawnSync(codexPath.stdout.trim(), ['login', '--device-auth'], { stdio: 'inherit' });
1155
+ continue;
1156
+ }
1157
+
1158
+ if (choice === '?') {
1159
+ console.log('');
1160
+ console.log(` ${bold('What is Dual Brain?')}`);
1161
+ console.log('');
1162
+ console.log(' Dual Brain orchestrates your Claude + Codex subscriptions together.');
1163
+ console.log(' It routes tasks to the right model: search (fast), execute (edits),');
1164
+ console.log(' think (architecture). Both providers work in parallel when possible.');
1165
+ console.log('');
1166
+ console.log(' Modes: Auto adapts to your workflow. Cost-saver minimizes GPT usage.');
1167
+ console.log(' Quality-first uses dual-brain review on all medium+ risk changes.');
1168
+ console.log('');
1169
+ console.log(` ${dim('Press Enter to return...')}`);
1170
+ await ask();
1171
+ continue;
1172
+ }
1173
+
1174
+ console.log(` Unknown option: ${choice}`);
1175
+ }
1176
+ }
1177
+
1178
+ // ─── Non-Interactive Fallback ─────────────────────────────────────────────
1179
+
1180
+ function renderStatic() {
1181
+ const providers = detectProviders();
1182
+ const sessions = getRecentSessions();
1183
+ const lines = sessions.length > 0
1184
+ ? renderReturningMenu(providers, sessions)
1185
+ : renderFirstRunMenu(providers);
1186
+ for (const l of lines) console.log(l);
1187
+ }
1188
+
1189
+ // ─── Entry ────────────────────────────────────────────────────────────────
1190
+
1191
+ if (process.stdin.isTTY && process.stdout.isTTY && !process.env.CI) {
1192
+ mainLoop().catch(err => { console.error(err); process.exit(1); });
1193
+ } else {
1194
+ renderStatic();
1195
+ }