dual-brain 3.1.0 → 3.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CLAUDE.md +33 -1
- package/hooks/budget-balancer.mjs +45 -6
- package/hooks/control-panel.mjs +489 -0
- package/hooks/cost-logger.mjs +51 -26
- package/hooks/decision-ledger.mjs +299 -0
- package/hooks/dual-brain-review.mjs +106 -17
- package/hooks/dual-brain-think.mjs +81 -17
- package/hooks/enforce-tier.mjs +103 -10
- package/hooks/gpt-work-dispatcher.mjs +50 -6
- package/hooks/profiles.mjs +203 -0
- package/hooks/quality-gate.mjs +34 -6
- package/hooks/summary-checkpoint.mjs +231 -0
- package/install.mjs +402 -33
- package/package.json +2 -2
- package/hooks/usage-2026-05-14.jsonl +0 -5
package/CLAUDE.md
CHANGED
|
@@ -15,7 +15,26 @@ Route subagents by task complexity:
|
|
|
15
15
|
For isolated or parallel work, dispatch to GPT via Codex CLI:
|
|
16
16
|
|
|
17
17
|
- `node .claude/hooks/gpt-work-dispatcher.mjs --task "..." --model gpt-5.4` — execution tasks
|
|
18
|
-
|
|
18
|
+
|
|
19
|
+
## Dual-Brain Collaboration
|
|
20
|
+
|
|
21
|
+
Dual-brain is a multi-round conversation between Claude and GPT — not a single-shot dispatch.
|
|
22
|
+
|
|
23
|
+
**Think flow** (architecture decisions):
|
|
24
|
+
1. Round 1: `node .claude/hooks/dual-brain-think.mjs --question "..."`
|
|
25
|
+
→ GPT gives independent analysis
|
|
26
|
+
2. You analyze the same question independently
|
|
27
|
+
3. Round 2: `node .claude/hooks/dual-brain-think.mjs --question "..." --round 2 --claude-says "<your analysis>"`
|
|
28
|
+
→ GPT responds to your points: agreements, pushback, refined recommendation
|
|
29
|
+
4. You synthesize both rounds into a final decision
|
|
30
|
+
|
|
31
|
+
**Review flow** (code review):
|
|
32
|
+
1. Round 1: `node .claude/hooks/dual-brain-review.mjs`
|
|
33
|
+
→ GPT reviews the diff independently
|
|
34
|
+
2. You review the same diff independently
|
|
35
|
+
3. Round 2: `node .claude/hooks/dual-brain-review.mjs --round 2 --claude-review "<your findings>"`
|
|
36
|
+
→ GPT confirms shared findings, acknowledges misses, disputes false positives
|
|
37
|
+
4. You synthesize into a final review verdict
|
|
19
38
|
|
|
20
39
|
## Routing Rules
|
|
21
40
|
|
|
@@ -32,9 +51,22 @@ Before ending a session with code changes:
|
|
|
32
51
|
|
|
33
52
|
Gate statuses: `pass` (safe to end), `issues_found` (fix first), `needs_human_review` (GPT unavailable).
|
|
34
53
|
|
|
54
|
+
## Profiles
|
|
55
|
+
|
|
56
|
+
Active profile controls routing posture, budgets, and quality gate behavior.
|
|
57
|
+
Profile persists to `.claude/dual-brain.profile.json` (gitignored).
|
|
58
|
+
|
|
59
|
+
- **balanced** (default): Best model per tier, normal budgets, reviews at medium+ risk
|
|
60
|
+
- **cost-saver**: Prefer cheaper models, lower budgets, skip GPT for non-critical
|
|
61
|
+
- **quality-first**: Dual-brain for medium+ risk, higher budgets, stricter reviews
|
|
62
|
+
|
|
63
|
+
Switch profiles: `npx dual-brain mode cost-saver`
|
|
64
|
+
Check status: `npx dual-brain status`
|
|
65
|
+
|
|
35
66
|
## Available Tools
|
|
36
67
|
|
|
37
68
|
- `node .claude/hooks/cost-report.mjs` — activity and cost estimates
|
|
38
69
|
- `node .claude/hooks/health-check.mjs` — verify system health
|
|
39
70
|
- `node .claude/hooks/budget-balancer.mjs` — provider balance status
|
|
71
|
+
- `node .claude/hooks/decision-ledger.mjs` — routing outcome insights
|
|
40
72
|
- `node .claude/hooks/test-orchestrator.mjs` — run self-tests
|
|
@@ -48,13 +48,39 @@ const WINDOW_BUDGETS = {
|
|
|
48
48
|
},
|
|
49
49
|
};
|
|
50
50
|
|
|
51
|
-
/**
|
|
52
|
-
const
|
|
51
|
+
/** Static fallback tokens per call, by tier */
|
|
52
|
+
const TOKENS_PER_CALL_DEFAULT = {
|
|
53
53
|
search: 2_500,
|
|
54
54
|
execute: 5_500,
|
|
55
55
|
think: 11_000,
|
|
56
56
|
};
|
|
57
57
|
|
|
58
|
+
/** Load moving averages from summary checkpoint, fall back to static defaults */
|
|
59
|
+
function getTokensPerCall() {
|
|
60
|
+
try {
|
|
61
|
+
const today = new Date().toISOString().slice(0, 10);
|
|
62
|
+
const summaryPath = join(__dirname, `usage-summary-${today}.json`);
|
|
63
|
+
const summary = JSON.parse(readFileSync(summaryPath, 'utf8'));
|
|
64
|
+
const avgs = summary.token_averages || {};
|
|
65
|
+
const result = { ...TOKENS_PER_CALL_DEFAULT };
|
|
66
|
+
for (const tier of ['search', 'execute', 'think']) {
|
|
67
|
+
// Check both providers for averages, prefer whichever has data
|
|
68
|
+
for (const provider of ['claude', 'openai']) {
|
|
69
|
+
const key = `${provider}:${tier}`;
|
|
70
|
+
if (avgs[key]?.count >= 5) {
|
|
71
|
+
result[tier] = Math.round(avgs[key].avg_input + avgs[key].avg_output);
|
|
72
|
+
break;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
return result;
|
|
77
|
+
} catch {
|
|
78
|
+
return { ...TOKENS_PER_CALL_DEFAULT };
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const TOKENS_PER_CALL = getTokensPerCall();
|
|
83
|
+
|
|
58
84
|
/** Default pressure thresholds (fraction 0–1) */
|
|
59
85
|
const DEFAULT_THRESHOLDS = {
|
|
60
86
|
warm: 0.65,
|
|
@@ -286,13 +312,26 @@ function chooseProvider(taskProfile = {}) {
|
|
|
286
312
|
score -= PRESSURE_PENALTY[tierStatus.state] ?? 0;
|
|
287
313
|
|
|
288
314
|
// Latency penalty (OpenAI only — Codex has higher startup overhead)
|
|
315
|
+
// Uses adaptive threshold from observed Codex startup times when available
|
|
289
316
|
if (provider === "openai") {
|
|
290
|
-
|
|
291
|
-
|
|
317
|
+
let minTaskMs = 180_000;
|
|
318
|
+
try {
|
|
319
|
+
const today = new Date().toISOString().slice(0, 10);
|
|
320
|
+
const summaryPath = join(__dirname, `usage-summary-${today}.json`);
|
|
321
|
+
const summary = JSON.parse(readFileSync(summaryPath, 'utf8'));
|
|
322
|
+
const latencies = (summary.codex_latencies || []).map(l => l.startup_ms).filter(Boolean);
|
|
323
|
+
if (latencies.length >= 5) {
|
|
324
|
+
const sorted = latencies.sort((a, b) => a - b);
|
|
325
|
+
const p75 = sorted[Math.floor(sorted.length * 0.75)];
|
|
326
|
+
minTaskMs = Math.max(90_000, p75 * 4);
|
|
327
|
+
}
|
|
328
|
+
} catch {}
|
|
329
|
+
|
|
330
|
+
if (estimatedDurationMs < minTaskMs) {
|
|
331
|
+
score -= 25;
|
|
292
332
|
} else if (estimatedDurationMs < 600_000) {
|
|
293
|
-
score -= 10;
|
|
333
|
+
score -= 10;
|
|
294
334
|
}
|
|
295
|
-
// >= 10 min: no penalty
|
|
296
335
|
}
|
|
297
336
|
|
|
298
337
|
// Underused bonus
|
|
@@ -0,0 +1,489 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* control-panel.mjs — Interactive TUI control panel for Dual-Brain Orchestrator.
|
|
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.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import readline from 'readline';
|
|
12
|
+
import { existsSync, readFileSync, renameSync, writeFileSync } from 'fs';
|
|
13
|
+
import { dirname, join } from 'path';
|
|
14
|
+
import { fileURLToPath } from 'url';
|
|
15
|
+
import { spawnSync } from 'child_process';
|
|
16
|
+
|
|
17
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
18
|
+
const CONFIG_FILE = join(__dirname, '..', 'orchestrator.json');
|
|
19
|
+
const PROFILE_FILE = join(__dirname, '..', 'dual-brain.profile.json');
|
|
20
|
+
const VERSION = (() => {
|
|
21
|
+
try { return JSON.parse(readFileSync(join(__dirname, '..', '..', 'dual-brain', 'package.json'), 'utf8')).version; } catch {}
|
|
22
|
+
try { return JSON.parse(readFileSync(join(__dirname, '..', 'package.json'), 'utf8')).version; } catch {}
|
|
23
|
+
return '?';
|
|
24
|
+
})();
|
|
25
|
+
|
|
26
|
+
// ─── ANSI ──────────────────────────────────────────────────────────────────
|
|
27
|
+
|
|
28
|
+
const color = !process.env.NO_COLOR;
|
|
29
|
+
const A = {
|
|
30
|
+
altOn: '\x1b[?1049h', altOff: '\x1b[?1049l',
|
|
31
|
+
clear: '\x1b[2J', home: '\x1b[H',
|
|
32
|
+
hide: '\x1b[?25l', show: '\x1b[?25h',
|
|
33
|
+
reset: '\x1b[0m', bold: '\x1b[1m', dim: '\x1b[2m',
|
|
34
|
+
red: '\x1b[31m', green: '\x1b[32m', yellow: '\x1b[33m',
|
|
35
|
+
blue: '\x1b[34m', cyan: '\x1b[36m', gray: '\x1b[90m',
|
|
36
|
+
white: '\x1b[37m',
|
|
37
|
+
};
|
|
38
|
+
const c = (code, s) => color ? `${code}${s}${A.reset}` : s;
|
|
39
|
+
|
|
40
|
+
// ─── Profiles ──────────────────────────────────────────────────────────────
|
|
41
|
+
|
|
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+' },
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
const PROFILE_BUDGETS = {
|
|
49
|
+
balanced: { session_warn_usd: 5, session_limit_usd: 10, daily_warn_usd: 20, daily_limit_usd: 50 },
|
|
50
|
+
'cost-saver': { session_warn_usd: 2, session_limit_usd: 5, daily_warn_usd: 8, daily_limit_usd: 20 },
|
|
51
|
+
'quality-first': { session_warn_usd: 15, session_limit_usd: 30, daily_warn_usd: 50, daily_limit_usd: 100 },
|
|
52
|
+
};
|
|
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
|
+
function loadProfile() {
|
|
67
|
+
try {
|
|
68
|
+
const data = JSON.parse(readFileSync(PROFILE_FILE, 'utf8'));
|
|
69
|
+
const name = data.active && PROFILES[data.active] ? data.active : 'balanced';
|
|
70
|
+
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
|
+
};
|
|
77
|
+
} catch {
|
|
78
|
+
return { name: 'balanced', budgets: PROFILE_BUDGETS.balanced, gate: PROFILE_GATE.balanced, switched_at: null };
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function saveProfile(name, customOverrides) {
|
|
83
|
+
const data = { active: name, switched_at: new Date().toISOString() };
|
|
84
|
+
if (customOverrides) data.custom_overrides = customOverrides;
|
|
85
|
+
const tmp = PROFILE_FILE + '.tmp.' + process.pid;
|
|
86
|
+
writeFileSync(tmp, JSON.stringify(data, null, 2) + '\n');
|
|
87
|
+
renameSync(tmp, PROFILE_FILE);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function saveBudget(sessionLimit, dailyLimit) {
|
|
91
|
+
let existing = {};
|
|
92
|
+
try { existing = JSON.parse(readFileSync(PROFILE_FILE, 'utf8')); } catch {}
|
|
93
|
+
const custom = existing.custom_overrides || {};
|
|
94
|
+
custom.budgets = {
|
|
95
|
+
session_warn_usd: +(sessionLimit * 0.6).toFixed(2),
|
|
96
|
+
session_limit_usd: sessionLimit,
|
|
97
|
+
daily_warn_usd: +(dailyLimit * 0.6).toFixed(2),
|
|
98
|
+
daily_limit_usd: dailyLimit,
|
|
99
|
+
};
|
|
100
|
+
const data = { active: existing.active || 'balanced', switched_at: existing.switched_at || new Date().toISOString(), custom_overrides: custom };
|
|
101
|
+
const tmp = PROFILE_FILE + '.tmp.' + process.pid;
|
|
102
|
+
writeFileSync(tmp, JSON.stringify(data, null, 2) + '\n');
|
|
103
|
+
renameSync(tmp, PROFILE_FILE);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
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' };
|
|
109
|
+
|
|
110
|
+
const credPaths = [
|
|
111
|
+
join(process.env.HOME || '', '.claude', '.credentials.json'),
|
|
112
|
+
join(process.env.HOME || '', '.claude', 'credentials.json'),
|
|
113
|
+
];
|
|
114
|
+
for (const p of credPaths) {
|
|
115
|
+
try {
|
|
116
|
+
const cred = JSON.parse(readFileSync(p, 'utf8'));
|
|
117
|
+
if (cred.claudeAiOauth || cred.apiKey || cred.oauth_token) { claude.authed = true; break; }
|
|
118
|
+
} catch {}
|
|
119
|
+
}
|
|
120
|
+
if (!claude.authed) {
|
|
121
|
+
const r = spawnSync('claude', ['auth', 'status'], { encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'], timeout: 5000 });
|
|
122
|
+
const out = ((r.stdout || '') + (r.stderr || '')).toLowerCase();
|
|
123
|
+
if (out.includes('logged in') || out.includes('authenticated')) claude.authed = true;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
const which = spawnSync('which', ['codex'], { encoding: 'utf8', stdio: 'pipe', timeout: 3000 });
|
|
127
|
+
if (which.status === 0 && which.stdout.trim()) {
|
|
128
|
+
codex.installed = true;
|
|
129
|
+
const login = spawnSync(which.stdout.trim(), ['login', 'status'], { encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'], timeout: 5000 });
|
|
130
|
+
const out = ((login.stdout || '') + (login.stderr || '')).toLowerCase();
|
|
131
|
+
if (login.status === 0 || out.includes('logged in') || out.includes('authenticated')) codex.authed = true;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
return { claude, codex };
|
|
135
|
+
}
|
|
136
|
+
|
|
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 };
|
|
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 } },
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
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; }
|
|
169
|
+
}
|
|
170
|
+
|
|
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;
|
|
175
|
+
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
|
+
}
|
|
183
|
+
} catch {}
|
|
184
|
+
return null;
|
|
185
|
+
}
|
|
186
|
+
|
|
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}`;
|
|
200
|
+
}
|
|
201
|
+
|
|
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';
|
|
209
|
+
|
|
210
|
+
const lines = [];
|
|
211
|
+
lines.push('');
|
|
212
|
+
lines.push(c(A.bold, ` 🧠 Dual-Brain Control Panel v${VERSION}`) + ` ${c(A.green, '🟢 Live')} ${c(A.dim, time)}`);
|
|
213
|
+
lines.push('');
|
|
214
|
+
lines.push(` ${mode}`);
|
|
215
|
+
lines.push(` 🎛️ Profile ${pf.emoji} ${c(A.bold, pf.label)} ${c(A.dim, pf.desc)}`);
|
|
216
|
+
lines.push(` 💵 Budget Session $${cost.toFixed(2)} / $${profile.budgets.session_limit_usd} Daily / $${profile.budgets.daily_limit_usd}`);
|
|
217
|
+
lines.push(` 🛡️ Gate Reviews ${profile.gate.sensitivity_floor}+ Dual-brain ${profile.gate.dual_brain_minimum}+`);
|
|
218
|
+
lines.push('');
|
|
219
|
+
|
|
220
|
+
lines.push(` 🔌 ${c(A.bold, 'Providers')}`);
|
|
221
|
+
const cStatus = providers.claude.authed ? '✅ authenticated' : '⚠️ not authenticated';
|
|
222
|
+
const xStatus = providers.codex.authed ? '✅ authenticated' : providers.codex.installed ? '⚠️ login needed' : '❌ not found';
|
|
223
|
+
lines.push(` 🟠 Claude ${cStatus} ${c(A.dim, providers.claude.models)}`);
|
|
224
|
+
lines.push(` 🟢 Codex ${xStatus} ${c(A.dim, providers.codex.models)}`);
|
|
225
|
+
lines.push('');
|
|
226
|
+
|
|
227
|
+
lines.push(` 🌡️ ${c(A.bold, 'Pressure')} ${c(A.dim, '— rolling 5h')}`);
|
|
228
|
+
for (const [label, emoji, key] of [['Claude', '🟠', 'claude'], ['OpenAI', '🟢', 'openai']]) {
|
|
229
|
+
lines.push(` ${emoji} ${label}`);
|
|
230
|
+
for (const tier of ['think', 'execute', 'search']) {
|
|
231
|
+
const p = pressure[key]?.[tier] || { pressure: 0 };
|
|
232
|
+
const tierLabel = (tier.charAt(0).toUpperCase() + tier.slice(1)).padEnd(8);
|
|
233
|
+
lines.push(` ${c(A.dim, tierLabel)} ${pressureBar(p.pressure)}`);
|
|
234
|
+
}
|
|
235
|
+
if (key === 'claude') lines.push('');
|
|
236
|
+
}
|
|
237
|
+
lines.push('');
|
|
238
|
+
|
|
239
|
+
if (flash) {
|
|
240
|
+
lines.push(` ${flash}`);
|
|
241
|
+
lines.push('');
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
lines.push(c(A.dim, ' ─'.repeat(30)));
|
|
245
|
+
lines.push(` ⌨️ ${c(A.bold, '1')} Balanced ${c(A.bold, '2')} Cost-saver ${c(A.bold, '3')} Quality-first ${c(A.bold, 'b')} Budget ${c(A.bold, 'e')} Explain ${c(A.bold, 'q')} Quit`);
|
|
246
|
+
lines.push('');
|
|
247
|
+
|
|
248
|
+
return lines.join('\n');
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
function renderExplain(decision, profile) {
|
|
252
|
+
const lines = [];
|
|
253
|
+
lines.push('');
|
|
254
|
+
lines.push(c(A.bold, ' 🧭 Last Routing Decision'));
|
|
255
|
+
lines.push(c(A.dim, ' ' + '─'.repeat(40)));
|
|
256
|
+
|
|
257
|
+
if (!decision) {
|
|
258
|
+
lines.push(' 💤 No routing decisions recorded today.');
|
|
259
|
+
lines.push('');
|
|
260
|
+
lines.push(c(A.dim, ' Press any key to go back'));
|
|
261
|
+
return lines.join('\n');
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
const time = decision.timestamp?.slice(11, 19) || '??:??:??';
|
|
265
|
+
const followed = decision.followed;
|
|
266
|
+
lines.push(` 🕐 Time ${time}`);
|
|
267
|
+
lines.push(` 🔎 Detected ${decision.detected_tier || 'unknown'} tier`);
|
|
268
|
+
lines.push(` 🧠 Recommended ${decision.recommended_model || 'unknown'}`);
|
|
269
|
+
lines.push(` 🎯 Actual ${decision.actual_model || 'unknown'}`);
|
|
270
|
+
lines.push(` ${followed ? '✅' : '⚠️'} Followed ${followed ? 'yes' : 'no'}`);
|
|
271
|
+
lines.push(` 🎛️ Profile ${profile.name}`);
|
|
272
|
+
lines.push('');
|
|
273
|
+
|
|
274
|
+
if (followed) {
|
|
275
|
+
lines.push(' ✅ Routing matched the recommendation.');
|
|
276
|
+
} else {
|
|
277
|
+
lines.push(' ⚠️ Recommendation was overridden.');
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
lines.push('');
|
|
281
|
+
lines.push(c(A.dim, ' Press any key to go back'));
|
|
282
|
+
return lines.join('\n');
|
|
283
|
+
}
|
|
284
|
+
|
|
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('');
|
|
291
|
+
|
|
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') : ''}`);
|
|
296
|
+
lines.push('');
|
|
297
|
+
|
|
298
|
+
if (flash) {
|
|
299
|
+
lines.push(` ${flash}`);
|
|
300
|
+
lines.push('');
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
lines.push(c(A.dim, ' Type numbers · Tab next · Enter save · Esc cancel'));
|
|
304
|
+
return lines.join('\n');
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
// ─── Static (non-TTY) Output ───────────────────────────────────────────────
|
|
308
|
+
|
|
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));
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
// ─── Interactive TUI ───────────────────────────────────────────────────────
|
|
319
|
+
|
|
320
|
+
function startTUI() {
|
|
321
|
+
let view = 'dashboard';
|
|
322
|
+
let flash = null;
|
|
323
|
+
let flashTimeout = null;
|
|
324
|
+
let refreshTimer = null;
|
|
325
|
+
|
|
326
|
+
// Budget editor state
|
|
327
|
+
let budgetSession = '';
|
|
328
|
+
let budgetDaily = '';
|
|
329
|
+
let budgetField = 'session';
|
|
330
|
+
|
|
331
|
+
function setFlash(msg, ms = 3000) {
|
|
332
|
+
flash = msg;
|
|
333
|
+
clearTimeout(flashTimeout);
|
|
334
|
+
flashTimeout = setTimeout(() => { flash = null; render(); }, ms);
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
function loadState() {
|
|
338
|
+
return {
|
|
339
|
+
profile: loadProfile(),
|
|
340
|
+
providers: detectProviders(),
|
|
341
|
+
pressure: loadPressure(),
|
|
342
|
+
cost: loadTodayCost(),
|
|
343
|
+
flash,
|
|
344
|
+
};
|
|
345
|
+
}
|
|
346
|
+
|
|
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
|
+
}
|
|
360
|
+
|
|
361
|
+
function startRefresh() {
|
|
362
|
+
stopRefresh();
|
|
363
|
+
refreshTimer = setInterval(render, 2000);
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
function stopRefresh() {
|
|
367
|
+
if (refreshTimer) { clearInterval(refreshTimer); refreshTimer = null; }
|
|
368
|
+
}
|
|
369
|
+
|
|
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
|
+
}
|
|
377
|
+
|
|
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
|
+
}
|
|
389
|
+
|
|
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
|
+
});
|
|
402
|
+
|
|
403
|
+
render();
|
|
404
|
+
startRefresh();
|
|
405
|
+
|
|
406
|
+
process.stdin.on('keypress', (str, key) => {
|
|
407
|
+
if (key?.ctrl && key?.name === 'c') return cleanup();
|
|
408
|
+
|
|
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;
|
|
420
|
+
}
|
|
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;
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
if (view === 'explain') {
|
|
449
|
+
view = 'dashboard';
|
|
450
|
+
startRefresh();
|
|
451
|
+
render();
|
|
452
|
+
return;
|
|
453
|
+
}
|
|
454
|
+
|
|
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
|
+
});
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
// ─── Entry ─────────────────────────────────────────────────────────────────
|
|
482
|
+
|
|
483
|
+
const interactive = process.stdin.isTTY && process.stdout.isTTY && !process.env.CI;
|
|
484
|
+
|
|
485
|
+
if (interactive) {
|
|
486
|
+
startTUI();
|
|
487
|
+
} else {
|
|
488
|
+
renderStatic();
|
|
489
|
+
}
|