#!/usr/bin/env node /** * tui.js — Token Monitor live terminal dashboard * * Live ANSI dashboard. Refreshes every 60 seconds. * No external dependencies — pure Node stdlib + project modules. * * Usage: * node tui.js * * Keys: * [r] — force immediate refresh * [q] — quit cleanly (restores terminal) * * Degrades gracefully when XAI_MANAGEMENT_KEY is not set. */ import { spawnSync } from 'child_process'; import { existsSync, readFileSync } from 'fs'; import { homedir } from 'os'; import { join } from 'path'; import { createRequire } from 'module'; import { fileURLToPath } from 'url'; import { dirname } from 'path'; const __dirname = dirname(fileURLToPath(import.meta.url)); // ── ANSI helpers ────────────────────────────────────────────────────────────── const ESC = '\x1b'; const RESET = `${ESC}[0m`; const BOLD = `${ESC}[1m`; const DIM = `${ESC}[2m`; const RED = `${ESC}[31m`; const GREEN = `${ESC}[32m`; const YELLOW = `${ESC}[33m`; const CYAN = `${ESC}[36m`; const WHITE = `${ESC}[37m`; const HIDE_CURSOR = `${ESC}[?25l`; const SHOW_CURSOR = `${ESC}[?25h`; const CURSOR_HOME = `${ESC}[H`; const CLEAR_SCREEN = `${ESC}[2J`; const CLEAR_EOL = `${ESC}[K`; function color(severity) { switch (severity) { case 'ok': return GREEN; case 'warning': return YELLOW; case 'critical': return RED; case 'dormant': return DIM; default: return DIM; } } function severityIcon(severity) { switch (severity) { case 'ok': return '✓'; case 'warning': return '!'; case 'critical': return '✗'; default: return '~'; } } function formatDuration(seconds) { if (seconds == null || isNaN(seconds) || seconds < 0) return '?'; const h = Math.floor(seconds / 3600); const m = Math.floor((seconds % 3600) / 60); if (h > 0) return `${h}h ${m > 0 ? m + 'm' : ''}`.trim(); if (m > 0) return `${m}m`; return `${Math.round(seconds)}s`; } function pct(v) { if (v == null) return ' ?%'; return `${Math.round(v * 100)}%`; } function bar(fraction, width = 7) { if (fraction == null) return ' '.repeat(width); const filled = Math.round(Math.min(1, Math.max(0, fraction)) * width); const empty = width - filled; return '█'.repeat(filled) + '░'.repeat(empty); } // ── Data fetching ───────────────────────────────────────────────────────────── async function fetchBudgetData() { try { const result = spawnSync('node', [join(__dirname, 'analyze.js'), '--budget-json'], { encoding: 'utf-8', timeout: 30_000, env: process.env, }); if (result.status !== 0 || !result.stdout) { return { error: result.stderr?.trim() || 'analyze.js returned no output' }; } return JSON.parse(result.stdout); } catch (err) { return { error: err.message }; } } // ── Layout ──────────────────────────────────────────────────────────────────── const WIDTH = 75; function hline(char = '─', w = WIDTH - 2) { return char.repeat(w); } function border(content) { return `│ ${content}${CLEAR_EOL}`; } function header(ts) { const tsStr = ts ? new Date(ts).toUTCString().replace(' GMT', ' UTC') : ''; const title = 'Token Monitor'; const pad = WIDTH - 4 - title.length - tsStr.length; return `┌─${BOLD}${CYAN} ${title} ${RESET}${'─'.repeat(Math.max(0, pad))}${DIM}${tsStr}${RESET} ─┐`; } function footer() { const hint = '[r] refresh [q] quit'; const pad = WIDTH - 4 - hint.length; return `└${'─'.repeat(pad > 0 ? pad : 0)}${DIM}${hint}${RESET}─┘`; } function renderAnthropicSection(providers, decision) { const lines = []; lines.push(border(`${BOLD}${CYAN}ANTHROPIC TEAMS${RESET}`)); const TEAMS = ['team-vigilio', 'team-ludo', 'team-molto', 'team-nadja', 'team-buio']; for (const name of TEAMS) { const p = providers?.[name]; if (!p) continue; const sev = p.severity || 'unknown'; const c = color(sev); const icon = severityIcon(sev); const shortName = name.replace('team-', ''); if (sev === 'critical') { const resetStr = p.reset_in_seconds != null ? `resets ${formatDuration(p.reset_in_seconds)}` : ''; lines.push(border(` ${c}${icon}${RESET} ${name.padEnd(16)} ${RED}MAXED${RESET} ${DIM}${resetStr}${RESET}`)); } else if (p.utilization_7d == null && p.utilization_5h == null) { lines.push(border(` ${c}${icon}${RESET} ${name.padEnd(16)} ${DIM}DORMANT${RESET}`)); } else if (p.status === 'invalid_key') { lines.push(border(` ${DIM}~${RESET} ${name.padEnd(16)} ${DIM}invalid key${RESET}`)); } else { const util5h = p.utilization_5h != null ? `5h:${pct(p.utilization_5h).padStart(4)}` : ''; const util7d = p.utilization_7d != null ? `7d:${pct(p.utilization_7d).padStart(4)}` : ''; const resetStr = p.reset_in_seconds != null ? ` resets ${formatDuration(p.reset_in_seconds)}` : ''; lines.push(border(` ${c}${icon}${RESET} ${name.padEnd(16)} ${util5h} ${util7d}${DIM}${resetStr}${RESET}`)); } } return lines; } function renderXaiSection(xai) { if (!xai || xai.error) return []; const lines = []; lines.push(border(`${BOLD}${CYAN}xAI${RESET}`)); if (xai.by_key) { const keys = Object.entries(xai.by_key); if (keys.length === 0) { lines.push(border(` ${DIM}(no spend this period)${RESET}`)); } else { const maxSpend = Math.max(...keys.map(([, v]) => v.spend_usd)); for (const [keyName, data] of keys) { const fraction = maxSpend > 0 ? data.spend_usd / maxSpend : 0; const barStr = bar(fraction, 7); const spendStr = `$${data.spend_usd.toFixed(2)}`.padStart(8); lines.push(border(` ${keyName.padEnd(20)} ${CYAN}${spendStr}${RESET} ${DIM}${barStr}${RESET}`)); } } } if (xai.total_usd != null && xai.prepaid_remaining_usd != null) { lines.push(border(` ${hline('─', 44)}`)); const remaining = xai.prepaid_remaining_usd; const remainColor = remaining < 5 ? RED : remaining < 15 ? YELLOW : GREEN; lines.push(border(` ${DIM}Prepaid left:${RESET} ${remainColor}$${remaining.toFixed(2)}${RESET} ${DIM}period: ${xai.period || '?'}${RESET}`)); } return lines; } function renderDecision(decision, lastProbe) { const lines = []; lines.push(border('')); if (decision?.recommended_provider) { lines.push(border(` ${GREEN}→ USE:${RESET} ${BOLD}${decision.recommended_provider}${RESET}`)); } if (decision?.avoid?.length > 0) { lines.push(border(` ${RED}→ AVOID:${RESET} ${decision.avoid.join(', ')}`)); } const probeStr = lastProbe ? `Last probe: ${new Date(lastProbe).toUTCString().slice(17, 25)} UTC` : ''; if (probeStr) { lines.push(border(` ${DIM}${probeStr}${RESET}`)); } return lines; } // ── Render full screen ──────────────────────────────────────────────────────── function render(data, status) { const out = []; out.push(CURSOR_HOME); if (data?.error) { out.push(header(null)); out.push(border('')); out.push(border(` ${RED}Error fetching data:${RESET} ${data.error}`)); out.push(border('')); out.push(border(` ${DIM}${status}${RESET}`)); out.push(border('')); out.push(footer()); process.stdout.write(out.join('\n') + '\n'); return; } const hasXai = data?.xai && !data.xai.error && process.env.XAI_MANAGEMENT_KEY; out.push(header(data?.ts)); out.push(border('')); // Two-column layout: Anthropic left, xAI right const anthLines = renderAnthropicSection(data?.providers, data?.budget_decision); const xaiLines = hasXai ? renderXaiSection(data?.xai) : []; // Output sections const maxRows = Math.max(anthLines.length, xaiLines.length); if (hasXai) { // Render each section separately (full-width) out.push(border(`${BOLD}${CYAN}ANTHROPIC TEAMS${RESET}${''.padEnd(8)}${BOLD}${CYAN}xAI${RESET}`)); out.push(border(hline('─', WIDTH - 2))); // Side-by-side isn't trivial with ANSI — render stacked instead for (const line of anthLines.slice(1)) out.push(line); out.push(border('')); for (const line of xaiLines) out.push(line); } else { for (const line of anthLines) out.push(line); } // Decision + last probe const decisionLines = renderDecision(data?.budget_decision, data?.ts); for (const line of decisionLines) out.push(line); // Alerts if (data?.alerts?.length > 0) { out.push(border('')); out.push(border(` ${YELLOW}${BOLD}Alerts:${RESET}`)); for (const alert of data.alerts.slice(0, 3)) { const c = alert.level === 'critical' ? RED : YELLOW; out.push(border(` ${c}⚠${RESET} ${alert.provider}: ${alert.message}`)); } } out.push(border('')); out.push(border(` ${DIM}${status}${RESET}`)); out.push(border('')); out.push(footer()); process.stdout.write(out.join('\n') + '\n'); } // ── Main loop ───────────────────────────────────────────────────────────────── let lastData = null; let refreshTimer = null; let isRefreshing = false; function statusLine(nextRefreshIn) { const next = nextRefreshIn > 0 ? `next refresh in ${nextRefreshIn}s` : 'refreshing...'; return next; } async function refresh() { if (isRefreshing) return; isRefreshing = true; render(lastData, 'refreshing...'); const data = await fetchBudgetData(); lastData = data; isRefreshing = false; scheduleRefresh(); render(lastData, statusLine(60)); } let countdown = 60; function scheduleRefresh() { if (refreshTimer) clearInterval(refreshTimer); countdown = 60; refreshTimer = setInterval(() => { countdown--; if (!isRefreshing) render(lastData, statusLine(countdown)); if (countdown <= 0) { clearInterval(refreshTimer); refresh(); } }, 1000); } function cleanup() { if (refreshTimer) clearInterval(refreshTimer); process.stdout.write(SHOW_CURSOR); process.stdout.write('\n'); if (process.stdin.isTTY) { process.stdin.setRawMode(false); } process.stdin.pause(); } function start() { // Hide cursor, clear screen process.stdout.write(HIDE_CURSOR); process.stdout.write(CLEAR_SCREEN); // Keyboard input if (process.stdin.isTTY) { process.stdin.setRawMode(true); } process.stdin.resume(); process.stdin.setEncoding('utf-8'); process.stdin.on('data', (key) => { if (key === 'q' || key === '\u0003') { // q or Ctrl-C cleanup(); process.exit(0); } if (key === 'r') { if (refreshTimer) clearInterval(refreshTimer); refresh(); } }); process.on('SIGINT', () => { cleanup(); process.exit(0); }); process.on('SIGTERM', () => { cleanup(); process.exit(0); }); // Initial render placeholder render({ error: null }, 'initializing...'); // First fetch refresh(); } start();