#!/usr/bin/env node /** * analyze.js — Token Monitor analysis CLI * * Reads accumulated JSONL logs from ~/.logs/token-monitor/ and produces: * - Burn rates per account (utilization delta over time) * - Weekly budget reconstruction * - Cycle stagger view (next 48h resets) * - Rotation recommendations (rule-based) * - Underspend alerts * - Log hygiene (--prune) * - Budget JSON for agent consumption (--budget-json) * - Mission cost attribution (--mission, --mission-window) * * Usage: * node analyze.js # full report * node analyze.js --burn-rate # burn rate section only * node analyze.js --weekly # weekly reconstruction only * node analyze.js --stagger # cycle stagger only * node analyze.js --rotation # rotation recommendation only * node analyze.js --json # JSON output (all sections) * node analyze.js --provider team-nadja # filter to one provider * node analyze.js --prune [--dry-run] # log hygiene * node analyze.js --xai # xAI spend section only * node analyze.js --budget-json # agent-consumable budget decision schema * node analyze.js --mission # mission cost (e.g. bookmarko#1 or commons#13) * node analyze.js --mission-window # mission cost, manual time range */ import { readdirSync, readFileSync, writeFileSync, mkdirSync, unlinkSync, existsSync, } from 'fs'; import { homedir } from 'os'; import { join } from 'path'; import { fetchXaiUsage, fetchXaiInvoicePreview, renderXaiSection, aggregateByKey } from './providers/xai-billing.js'; const CONFIG_DIR = join(homedir(), '.config', 'token-monitor'); const CONFIG_FILE = join(CONFIG_DIR, 'config.json'); const MISSION_REPOS_FILE = join(CONFIG_DIR, 'mission-repos.json'); const FORGEJO_URL = process.env.FORGEJO_URL || 'http://localhost:3001'; const FORGEJO_TOKEN = process.env.FORGEJO_TOKEN || null; // ── Config loading ──────────────────────────────────────────────────────────── function loadConfig() { const defaults = { weekly_seat_usd: 7.50, seats_per_team: 1 }; if (!existsSync(CONFIG_FILE)) return defaults; try { return { ...defaults, ...JSON.parse(readFileSync(CONFIG_FILE, 'utf-8')) }; } catch { return defaults; } } function loadMissionRepos() { if (!existsSync(MISSION_REPOS_FILE)) return {}; try { return JSON.parse(readFileSync(MISSION_REPOS_FILE, 'utf-8')); } catch { return {}; } } const LOG_DIR = join(homedir(), '.logs', 'token-monitor'); const TEAMS = ['team-vigilio', 'team-ludo', 'team-molto', 'team-nadja', 'team-buio']; // ── Load logs ──────────────────────────────────────────────────────────────── function loadLogs(providerFilter = null) { if (!existsSync(LOG_DIR)) return []; const files = readdirSync(LOG_DIR) .filter(f => /^\d{4}-\d{2}-\d{2}\.jsonl$/.test(f)) .sort(); const entries = []; for (const file of files) { const content = readFileSync(join(LOG_DIR, file), 'utf-8').trim(); if (!content) continue; for (const line of content.split('\n').filter(Boolean)) { try { const entry = JSON.parse(line); const providers = entry.providers || {}; // Skip test/empty entries — real entries have at least one provider with a type if (!Object.values(providers).some(p => p && p.type)) continue; if (providerFilter && !providers[providerFilter]) continue; entries.push(entry); } catch { /* skip bad lines */ } } } return entries.sort((a, b) => a.ts.localeCompare(b.ts)); } // ── Helpers ─────────────────────────────────────────────────────────────────── 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 getISOWeek(dateStr) { const d = new Date(dateStr); d.setUTCHours(12, 0, 0, 0); d.setUTCDate(d.getUTCDate() + 3 - (d.getUTCDay() + 6) % 7); const week1 = new Date(Date.UTC(d.getUTCFullYear(), 0, 4)); const weekNum = 1 + Math.round(((d - week1) / 86400000 - 3 + (week1.getUTCDay() + 6) % 7) / 7); return `${d.getUTCFullYear()}-W${String(weekNum).padStart(2, '0')}`; } function latestPerProvider(entries, typeFilter = null) { const latest = {}; for (const entry of entries) { for (const [name, p] of Object.entries(entry.providers || {})) { if (typeFilter && p?.type !== typeFilter) continue; if (!latest[name] || entry.ts > latest[name].ts) { latest[name] = { ts: entry.ts, p }; } } } return latest; } // ── Burn rate ───────────────────────────────────────────────────────────────── function computeBurnRate(entries, providerName) { const pts = entries .filter(e => e.providers[providerName]?.utilization_7d != null) .map(e => ({ ts: new Date(e.ts).getTime(), util7d: e.providers[providerName].utilization_7d, })); if (pts.length < 2) return null; const first = pts[0]; const last = pts[pts.length - 1]; const hours = (last.ts - first.ts) / 3_600_000; if (hours < 0.01) return null; const rate = (last.util7d - first.util7d) / hours; const exhaustion = rate > 0 ? (1 - last.util7d) / rate : null; return { rate_per_hour: rate, projected_exhaustion_hours: exhaustion, current_util_7d: last.util7d, first_util_7d: first.util7d, data_points: pts.length, hours_elapsed: hours, first_ts: new Date(first.ts).toISOString(), last_ts: new Date(last.ts).toISOString(), }; } // ── Weekly reconstruction ───────────────────────────────────────────────────── function reconstructWeekly(entries) { const weeks = {}; for (const entry of entries) { const week = getISOWeek(entry.ts); if (!weeks[week]) weeks[week] = { providers: {} }; const w = weeks[week]; const dateStr = entry.ts.slice(0, 10); if (!w.start || dateStr < w.start) w.start = dateStr; if (!w.end || dateStr > w.end) w.end = dateStr; for (const [name, p] of Object.entries(entry.providers || {})) { if (p?.type !== 'teams-direct') continue; if (!w.providers[name]) { w.providers[name] = { samples: 0, peak_util_5h: 0, peak_util_7d: 0, _total_util_7d: 0, exhausted_count: 0, }; } const s = w.providers[name]; s.samples++; if (p.utilization_5h != null) s.peak_util_5h = Math.max(s.peak_util_5h, p.utilization_5h); if (p.utilization_7d != null) { s.peak_util_7d = Math.max(s.peak_util_7d, p.utilization_7d); s._total_util_7d += p.utilization_7d; } if (p.status === 'rejected') s.exhausted_count++; } } // Finalize averages, remove internal accumulator for (const w of Object.values(weeks)) { for (const s of Object.values(w.providers)) { s.avg_util_7d = s.samples > 0 ? s._total_util_7d / s.samples : 0; delete s._total_util_7d; } } return weeks; } // ── Cycle stagger ───────────────────────────────────────────────────────────── function cycleStagger(entries) { const latest = latestPerProvider(entries, 'teams-direct'); const now = Date.now(); const results = []; for (const [provider, { ts, p }] of Object.entries(latest)) { if (p.reset_in_seconds == null) continue; const entryAgeSeconds = (now - new Date(ts).getTime()) / 1000; const resetInSecondsNow = Math.max(0, p.reset_in_seconds - entryAgeSeconds); if (resetInSecondsNow > 172_800) continue; // > 48h, skip results.push({ provider, resets_at_iso: new Date(now + resetInSecondsNow * 1000).toISOString(), resets_in_seconds_from_now: Math.round(resetInSecondsNow), }); } return results.sort((a, b) => a.resets_in_seconds_from_now - b.resets_in_seconds_from_now); } // ── Underspend alerts ───────────────────────────────────────────────────────── function underspendAlerts(entries) { const latest = latestPerProvider(entries, 'teams-direct'); const alerts = []; for (const [provider, { p }] of Object.entries(latest)) { if (p.utilization_5h == null) continue; if (p.status !== 'allowed') continue; if (p.utilization_5h < 0.60 && p.reset_in_seconds != null && p.reset_in_seconds < 7200) { alerts.push({ provider, utilization_5h: p.utilization_5h, reset_in_seconds: p.reset_in_seconds }); } } return alerts; } // ── Rotation rank ───────────────────────────────────────────────────────────── function rotationRank(entries) { const latest = latestPerProvider(entries, 'teams-direct'); const ranked = []; for (const name of TEAMS) { const rec = latest[name]; if (!rec) continue; const p = rec.p; let score, reason, severity; if (p.status === 'invalid_key') { score = -200; reason = '401 invalid key — cannot use'; severity = 'unknown'; } else if (p.status === 'rejected') { const resetIn = p.reset_in_seconds || 999_999; // Among maxed accounts, soonest reset gets slight priority score = -100 + (1 / (resetIn + 1)); reason = `MAXED — avoid until reset in ${formatDuration(resetIn)}`; severity = 'critical'; } else if (p.utilization_7d == null && p.utilization_5h == null) { score = 50; reason = 'DORMANT — hold in reserve for cycle staggering'; severity = 'dormant'; } else { const headroom = 1 - (p.utilization_7d || 0); score = headroom * 100; if (score < 30) { reason = `low headroom — 7d: ${pct(p.utilization_7d)}, use cautiously`; severity = 'warning'; } else { const resetStr = p.reset_in_seconds != null ? `, resets ${formatDuration(p.reset_in_seconds)}` : ''; reason = `${pct(headroom)} headroom — 7d: ${pct(p.utilization_7d)}${resetStr}`; severity = 'ok'; } } ranked.push({ provider: name, score, reason, severity }); } return ranked.sort((a, b) => b.score - a.score); } // ── Log hygiene ─────────────────────────────────────────────────────────────── function pruneLogs(dryRun = false) { if (!existsSync(LOG_DIR)) { console.log('No log directory — nothing to prune.'); return; } const files = readdirSync(LOG_DIR).filter(f => /^\d{4}-\d{2}-\d{2}\.jsonl$/.test(f)); const cutoff = new Date(Date.now() - 30 * 86_400_000).toISOString().slice(0, 10); const toPrune = files.filter(f => f.slice(0, 10) < cutoff); if (toPrune.length === 0) { console.log('No files older than 30 days — nothing to prune.'); return; } const weeksDir = join(LOG_DIR, 'weeks'); if (!dryRun) mkdirSync(weeksDir, { recursive: true }); const weeklyAgg = {}; for (const file of toPrune) { const dateStr = file.slice(0, 10); const week = getISOWeek(dateStr + 'T12:00:00Z'); const content = readFileSync(join(LOG_DIR, file), 'utf-8').trim(); if (!weeklyAgg[week]) { weeklyAgg[week] = { week, start: dateStr, end: dateStr, providers: {} }; } const w = weeklyAgg[week]; if (dateStr < w.start) w.start = dateStr; if (dateStr > w.end) w.end = dateStr; for (const line of content.split('\n').filter(Boolean)) { try { const entry = JSON.parse(line); for (const [name, p] of Object.entries(entry.providers || {})) { if (p?.type !== 'teams-direct') continue; if (!w.providers[name]) { w.providers[name] = { samples: 0, peak_util_5h: 0, peak_util_7d: 0, avg_util_7d: 0, exhausted_count: 0, }; } const s = w.providers[name]; if (p.utilization_5h != null) s.peak_util_5h = Math.max(s.peak_util_5h, p.utilization_5h); if (p.utilization_7d != null) { s.peak_util_7d = Math.max(s.peak_util_7d, p.utilization_7d); s.avg_util_7d = (s.avg_util_7d * s.samples + p.utilization_7d) / (s.samples + 1); } s.samples++; if (p.status === 'rejected') s.exhausted_count++; } } catch { /* skip */ } } } let pruned = 0; for (const [week, data] of Object.entries(weeklyAgg)) { const weekFile = join(weeksDir, `${week}.json`); if (dryRun) { console.log(`[dry-run] Would write ${weekFile}`); } else { if (existsSync(weekFile)) { // Merge with existing weekly file const existing = JSON.parse(readFileSync(weekFile, 'utf-8')); for (const [name, s] of Object.entries(data.providers)) { if (!existing.providers[name]) { existing.providers[name] = s; continue; } const e = existing.providers[name]; const totalSamples = e.samples + s.samples; e.peak_util_5h = Math.max(e.peak_util_5h, s.peak_util_5h); e.peak_util_7d = Math.max(e.peak_util_7d, s.peak_util_7d); e.avg_util_7d = (e.avg_util_7d * e.samples + s.avg_util_7d * s.samples) / totalSamples; e.samples = totalSamples; e.exhausted_count += s.exhausted_count; } writeFileSync(weekFile, JSON.stringify(existing, null, 2)); } else { writeFileSync(weekFile, JSON.stringify(data, null, 2)); } } } for (const file of toPrune) { if (dryRun) { console.log(`[dry-run] Would delete ${join(LOG_DIR, file)}`); } else { unlinkSync(join(LOG_DIR, file)); pruned++; } } if (dryRun) { console.log(`[dry-run] Would prune ${toPrune.length} file(s) into ${Object.keys(weeklyAgg).length} weekly summary file(s).`); } else { console.log(`Pruned ${pruned} file(s) into ${Object.keys(weeklyAgg).length} weekly summary file(s).`); } } // ── Report generation ───────────────────────────────────────────────────────── function generateFullReport(entries, xaiSection = null) { const ts = new Date().toISOString().replace('T', ' ').replace(/\.\d+Z$/, ' UTC'); const width = 56; const lines = [ `Token Analysis — ${ts}`, '═'.repeat(width), '', ]; // ── Burn rates lines.push('Burn Rate'); const latestTeams = latestPerProvider(entries, 'teams-direct'); let anyTeams = false; for (const name of TEAMS) { const rec = latestTeams[name]; if (!rec) continue; anyTeams = true; const p = rec.p; const br = computeBurnRate(entries, name); const pad = name.padEnd(16); if (p.status === 'invalid_key') { lines.push(` ${pad} 401 invalid key`); } else if (p.status === 'rejected') { lines.push(` ${pad} MAXED — resets in ${formatDuration(p.reset_in_seconds)}`); } else if (p.utilization_7d == null && p.utilization_5h == null) { lines.push(` ${pad} DORMANT — cycle not started`); } else if (br && br.data_points >= 2) { const rateStr = `${(br.rate_per_hour * 100).toFixed(1)}%/hr`; const exhStr = br.projected_exhaustion_hours != null ? `exhausts ~${Math.round(br.projected_exhaustion_hours)}h` : 'stable/declining'; lines.push(` ${pad} 7d: ${pct(br.first_util_7d)}→${pct(br.current_util_7d)} over ${br.hours_elapsed.toFixed(1)}h = ${rateStr} | ${exhStr} | ${br.data_points} pts`); } else { lines.push(` ${pad} 7d: ${pct(p.utilization_7d)} (insufficient data for rate)`); } } if (!anyTeams) lines.push(' (no teams data in logs)'); lines.push(''); // ── Reset schedule const stagger = cycleStagger(entries); if (stagger.length > 0) { lines.push('Reset Schedule (next 48h)'); for (const { provider, resets_at_iso, resets_in_seconds_from_now } of stagger) { const timeStr = resets_at_iso.slice(11, 16) + ' UTC'; lines.push(` ${provider.padEnd(16)} ~${formatDuration(resets_in_seconds_from_now).padEnd(10)} (${timeStr})`); } lines.push(''); } // ── Weekly const weekly = reconstructWeekly(entries); const weekKeys = Object.keys(weekly).sort(); if (weekKeys.length > 0) { lines.push('Weekly Reconstruction'); for (const week of weekKeys) { const w = weekly[week]; const note = w.start === w.end ? ' (1 day)' : ''; lines.push(` ${week}${note}`); for (const [name, s] of Object.entries(w.providers)) { const exhStr = s.exhausted_count > 0 ? ` | exhausted: ${s.exhausted_count}x` : ''; lines.push(` ${name.padEnd(14)} peak 7d: ${pct(s.peak_util_7d)} | avg: ${pct(s.avg_util_7d)} | ${s.samples} samples${exhStr}`); } } lines.push(''); } // ── Rotation const rotation = rotationRank(entries); if (rotation.length > 0) { lines.push('Rotation Recommendation'); rotation.forEach(({ provider, reason, severity }, i) => { const icon = severity === 'ok' ? '✓' : severity === 'critical' ? '✗' : severity === 'dormant' ? '~' : '?'; lines.push(` ${i + 1}. ${provider.padEnd(16)} ${icon} ${reason}`); }); lines.push(''); } // ── Underspend alerts const boosts = underspendAlerts(entries); if (boosts.length > 0) { lines.push('⚡ Underspend Alerts (burn before reset)'); for (const { provider, utilization_5h, reset_in_seconds } of boosts) { lines.push(` ${provider}: 5h at ${pct(utilization_5h)}, resets in ${formatDuration(reset_in_seconds)}`); } lines.push(''); } if (entries.length === 0) { lines.push('No log data found. Run monitor.js to start accumulating data.'); } // ── xAI spend (optional — only when management key is available) if (xaiSection) { lines.push(xaiSection); lines.push(''); } return lines.join('\n'); } // ── Budget JSON (Objective 1) ───────────────────────────────────────────────── async function buildBudgetJson(entries, config) { const now = new Date().toISOString(); const latest = latestPerProvider(entries, 'teams-direct'); const ranked = rotationRank(entries); const weeklyUsd = config.weekly_seat_usd; // budget_decision const usable = ranked.filter(r => r.severity !== 'critical' && r.severity !== 'unknown'); const avoid = ranked.filter(r => r.severity === 'critical').map(r => r.provider); const recommended = usable.length > 0 ? usable[0].provider : null; const avoidReasons = ranked .filter(r => avoid.includes(r.provider)) .map(r => r.provider + ': ' + r.reason) .join('; '); const recommendReason = recommended ? `${recommended} at ${pct((latest[recommended]?.p?.utilization_7d) || 0)} 7d utilization` : 'no healthy provider found'; const avoidReason = avoidReasons || 'none maxed'; const reason = avoid.length > 0 ? `${avoidReason}; ${recommendReason}` : recommendReason; // providers const providers = {}; for (const name of TEAMS) { const rec = latest[name]; if (!rec) continue; const p = rec.p; const util7d = p.utilization_7d ?? null; const headroom = util7d != null ? Math.round((1 - util7d) * 100) : null; providers[name] = { status: p.status || 'unknown', utilization_5h: p.utilization_5h ?? null, utilization_7d: util7d, headroom_7d_pct: headroom, estimated_weekly_budget_usd: weeklyUsd, estimated_spend_this_week_usd: util7d != null ? parseFloat((util7d * weeklyUsd).toFixed(2)) : null, reset_in_seconds: p.reset_in_seconds ?? null, severity: ranked.find(r => r.provider === name)?.severity || 'unknown', }; } // alerts const alerts = []; for (const { provider, severity, reason: r } of ranked) { if (severity === 'critical') { const p = latest[provider]?.p; const resetStr = p?.reset_in_seconds != null ? ` — resets in ${formatDuration(p.reset_in_seconds)}` : ''; alerts.push({ level: 'critical', provider, message: `maxed${resetStr}` }); } else if (severity === 'warning') { alerts.push({ level: 'warning', provider, message: r }); } } // xAI section let xai = null; if (XAI_MANAGEMENT_KEY && XAI_TEAM_ID) { try { const now2 = new Date(); const startDate = new Date(Date.UTC(now2.getUTCFullYear(), now2.getUTCMonth(), 1)); const [usageRows, preview] = await Promise.all([ fetchXaiUsage({ managementKey: XAI_MANAGEMENT_KEY, teamId: XAI_TEAM_ID, startDate, endDate: now2 }), fetchXaiInvoicePreview({ managementKey: XAI_MANAGEMENT_KEY, teamId: XAI_TEAM_ID }), ]); const byKey = aggregateByKey(usageRows); const period = `${now2.getUTCFullYear()}-${String(now2.getUTCMonth() + 1).padStart(2, '0')}`; const byKeyObj = {}; const CACHED_UNIT = 'Cached prompt text tokens'; const PROMPT_UNIT = 'Prompt text tokens'; for (const [keyName, { total, byUnit }] of byKey) { byKeyObj[keyName] = { spend_usd: parseFloat(total.toFixed(2)), cached_usd: parseFloat((byUnit[CACHED_UNIT] || 0).toFixed(2)), prompt_usd: parseFloat((byUnit[PROMPT_UNIT] || 0).toFixed(2)), }; } const totalUsd = [...byKey.values()].reduce((s, e) => s + e.total, 0); const prepaidRemaining = preview ? parseFloat(((preview.prepaidCreditsCents - preview.prepaidUsedCents) / 100).toFixed(2)) : null; xai = { period, by_key: byKeyObj, total_usd: parseFloat(totalUsd.toFixed(2)), prepaid_remaining_usd: prepaidRemaining }; // xAI alert if prepaid low if (prepaidRemaining != null && prepaidRemaining < 5) { alerts.push({ level: 'warning', provider: 'xai', message: `prepaid balance low: $${prepaidRemaining.toFixed(2)} remaining` }); } } catch (err) { xai = { error: err.message }; } } return { ts: now, budget_decision: { recommended_provider: recommended, avoid, reason }, providers, ...(xai ? { xai } : {}), alerts, }; } // ── Mission cost tracking (Objective 2) ─────────────────────────────────────── async function resolveMissionWindow(missionRef) { if (!FORGEJO_TOKEN) throw new Error('FORGEJO_TOKEN not set — cannot resolve issue timestamps'); const repos = loadMissionRepos(); const m = missionRef.match(/^([^#]+)#(\d+)$/); if (!m) throw new Error(`Invalid mission ref: ${missionRef} (expected format: repo#num)`); const [, shortRepo, issueNum] = m; const repoFull = repos[shortRepo]; if (!repoFull) throw new Error(`Unknown repo short name: ${shortRepo}. Add it to ${MISSION_REPOS_FILE}`); const headers = { 'Authorization': `token ${FORGEJO_TOKEN}`, 'Content-Type': 'application/json' }; const issueUrl = `${FORGEJO_URL}/api/v1/repos/${repoFull}/issues/${issueNum}`; const issueResp = await fetch(issueUrl, { headers }); if (!issueResp.ok) throw new Error(`Forgejo issue fetch failed: ${issueResp.status}`); const issue = await issueResp.json(); const commentsUrl = `${FORGEJO_URL}/api/v1/repos/${repoFull}/issues/${issueNum}/comments?limit=50`; const commResp = await fetch(commentsUrl, { headers }); const comments = commResp.ok ? await commResp.json() : []; const startTs = issue.created_at; const timestamps = [issue.created_at, issue.updated_at, ...comments.map(c => c.updated_at)].filter(Boolean); const endTs = timestamps.sort().pop(); return { ref: missionRef, repo: repoFull, issue_num: issueNum, title: issue.title, start: startTs, end: endTs, }; } function entriesInWindow(allEntries, startIso, endIso) { const start = new Date(startIso).getTime(); const end = new Date(endIso).getTime(); return allEntries.filter(e => { const t = new Date(e.ts).getTime(); return t >= start && t <= end; }); } async function computeMissionCost(startIso, endIso, missionLabel, config) { const allEntries = loadLogs(); const window = entriesInWindow(allEntries, startIso, endIso); const weeklyUsd = config.weekly_seat_usd; const startDt = new Date(startIso); const endDt = new Date(endIso); const durationMs = endDt - startDt; const durationH = Math.floor(durationMs / 3_600_000); const durationM = Math.floor((durationMs % 3_600_000) / 60_000); const durationStr = durationH > 0 ? `${durationH}h ${durationM}m` : `${durationM}m`; const lines = []; if (missionLabel) lines.push(`Mission Cost: ${missionLabel}`); else lines.push('Mission Cost (time window)'); const startStr = startIso.slice(0, 16).replace('T', ' '); const endStr = endIso.slice(0, 16).replace('T', ' '); lines.push(` Window: ${startStr} → ${endStr} (${durationStr})`); lines.push(' ' + '─'.repeat(51)); let totalEstUsd = 0; let totalXaiUsd = 0; // Anthropic Teams: utilization delta per provider for (const name of TEAMS) { const pts = window .filter(e => e.providers[name]?.utilization_7d != null) .sort((a, b) => a.ts.localeCompare(b.ts)); if (pts.length < 2) continue; const firstUtil = pts[0].providers[name].utilization_7d; const lastUtil = pts[pts.length - 1].providers[name].utilization_7d; const delta = lastUtil - firstUtil; if (Math.abs(delta) < 0.001) continue; const estUsd = delta * weeklyUsd; totalEstUsd += Math.max(0, estUsd); const deltaStr = `${(delta * 100).toFixed(1)}%`; const usdStr = estUsd >= 0 ? `~$${estUsd.toFixed(2)} est.` : `~-$${Math.abs(estUsd).toFixed(2)} (decreased)`; lines.push(` ${name.padEnd(16)} 7d: ${pct(firstUtil)} → ${pct(lastUtil)} (${deltaStr.padStart(5)}) ${usdStr}`); } if (totalEstUsd === 0 && window.length < 2) { lines.push(' (no utilization data in this time window)'); } // xAI: exact spend via management API if (XAI_MANAGEMENT_KEY && XAI_TEAM_ID) { try { const usageRows = await fetchXaiUsage({ managementKey: XAI_MANAGEMENT_KEY, teamId: XAI_TEAM_ID, startDate: new Date(startIso), endDate: new Date(endIso), }); const byKey = aggregateByKey(usageRows); for (const [keyName, { total }] of byKey) { totalXaiUsd += total; lines.push(` ${keyName.padEnd(16)} $${total.toFixed(2)} exact (management API)`); } if (byKey.size === 0) { lines.push(' xAI $0.00 exact (management API)'); } } catch (err) { lines.push(` xAI (error: ${err.message})`); } } lines.push(' ' + '─'.repeat(51)); const totalStr = XAI_MANAGEMENT_KEY ? `~$${totalEstUsd.toFixed(2)} (Anthropic) + $${totalXaiUsd.toFixed(2)} (xAI) = ~$${(totalEstUsd + totalXaiUsd).toFixed(2)}` : `~$${totalEstUsd.toFixed(2)} estimated`; lines.push(` Total estimated: ${totalStr}`); return lines.join('\n'); } // ── Main ────────────────────────────────────────────────────────────────────── const args = process.argv.slice(2); const showBurnRate = args.includes('--burn-rate'); const showWeekly = args.includes('--weekly'); const showStagger = args.includes('--stagger'); const showRotation = args.includes('--rotation'); const showXai = args.includes('--xai'); const isJson = args.includes('--json'); const isBudgetJson = args.includes('--budget-json'); const isPrune = args.includes('--prune'); const isDryRun = args.includes('--dry-run'); const providerIdx = args.indexOf('--provider'); const providerFilter = providerIdx !== -1 ? args[providerIdx + 1] : null; const missionIdx = args.indexOf('--mission'); const missionRef = missionIdx !== -1 ? args[missionIdx + 1] : null; const missionWindowIdx = args.indexOf('--mission-window'); const missionWindowStart = missionWindowIdx !== -1 ? args[missionWindowIdx + 1] : null; const missionWindowEnd = missionWindowIdx !== -1 ? args[missionWindowIdx + 2] : null; const showAll = !showBurnRate && !showWeekly && !showStagger && !showRotation && !showXai && !isPrune && !isBudgetJson && !missionRef && !missionWindowStart; if (isPrune) { pruneLogs(isDryRun); process.exit(0); } const entries = loadLogs(providerFilter); // Early exit for mission-window (no xAI IIFE needed if pure time-window with no key) if (missionWindowStart && !missionRef) { if (!missionWindowEnd) { console.error('Usage: node analyze.js --mission-window '); process.exit(1); } } // ── xAI billing helpers ──────────────────────────────────────────────────── const XAI_MANAGEMENT_KEY = process.env.XAI_MANAGEMENT_KEY || null; const XAI_TEAM_ID = process.env.XAI_TEAM_ID || null; const config = loadConfig(); async function getXaiSection() { if (!XAI_MANAGEMENT_KEY || !XAI_TEAM_ID) return null; try { const now = new Date(); // Current billing period: 1st of this month through today const startDate = new Date(Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), 1)); const endDate = now; const startDateStr = startDate.toISOString().slice(0, 10); const endDateStr = endDate.toISOString().slice(0, 10); const [usageRows, preview] = await Promise.all([ fetchXaiUsage({ managementKey: XAI_MANAGEMENT_KEY, teamId: XAI_TEAM_ID, startDate, endDate }), fetchXaiInvoicePreview({ managementKey: XAI_MANAGEMENT_KEY, teamId: XAI_TEAM_ID }), ]); return renderXaiSection({ usageRows, preview, startDateStr, endDateStr }); } catch (err) { return `xAI Spend\n (error: ${err.message})`; } } // All remaining paths may need xAI data (async), wrap in IIFE (async () => { if (isBudgetJson) { const result = await buildBudgetJson(entries, config); console.log(JSON.stringify(result, null, 2)); process.exit(0); } if (missionWindowStart && missionWindowEnd && !missionRef) { const label = null; const out = await computeMissionCost(missionWindowStart, missionWindowEnd, label, config); console.log(out); process.exit(0); } if (missionRef) { try { const window = await resolveMissionWindow(missionRef); const label = `${window.title} (${missionRef})`; let startIso = window.start; let endIso = window.end; // --start / --end overrides const startOverrideIdx = args.indexOf('--start'); const endOverrideIdx = args.indexOf('--end'); if (startOverrideIdx !== -1) startIso = args[startOverrideIdx + 1]; if (endOverrideIdx !== -1) endIso = args[endOverrideIdx + 1]; const out = await computeMissionCost(startIso, endIso, label, config); console.log(out); } catch (err) { console.error(`Error: ${err.message}`); process.exit(1); } process.exit(0); } if (isJson) { const burnRates = {}; for (const name of TEAMS) { const br = computeBurnRate(entries, name); if (br) burnRates[name] = br; } console.log(JSON.stringify({ timestamp: new Date().toISOString(), burn_rates: burnRates, weekly: reconstructWeekly(entries), stagger: cycleStagger(entries), rotation: rotationRank(entries), underspend_alerts: underspendAlerts(entries), }, null, 2)); process.exit(0); } if (showAll) { const xaiSection = await getXaiSection(); console.log(generateFullReport(entries, xaiSection)); process.exit(0); } if (showXai) { const xaiSection = await getXaiSection(); if (xaiSection) { console.log(xaiSection); } else { console.log('xAI Spend'); console.log(' (XAI_MANAGEMENT_KEY or XAI_TEAM_ID not set — source ~/.secrets/keys.env)'); } process.exit(0); } // Section-specific output const width = 56; if (showBurnRate) { console.log('Burn Rate\n' + '─'.repeat(width)); const latestTeams = latestPerProvider(entries, 'teams-direct'); for (const name of TEAMS) { const rec = latestTeams[name]; if (!rec) continue; const br = computeBurnRate(entries, name); if (br && br.data_points >= 2) { const exhStr = br.projected_exhaustion_hours != null ? `exhausts ~${Math.round(br.projected_exhaustion_hours)}h` : 'stable/declining'; console.log(` ${name.padEnd(16)} ${(br.rate_per_hour * 100).toFixed(1)}%/hr | ${exhStr} | ${br.data_points} pts`); } else { console.log(` ${name.padEnd(16)} insufficient data`); } } } if (showWeekly) { console.log('Weekly Reconstruction\n' + '─'.repeat(width)); const weekly = reconstructWeekly(entries); for (const [week, w] of Object.entries(weekly).sort()) { console.log(` ${week}`); for (const [name, s] of Object.entries(w.providers)) { console.log(` ${name.padEnd(14)} peak 7d: ${pct(s.peak_util_7d)} | avg: ${pct(s.avg_util_7d)} | ${s.samples} samples`); } } } if (showStagger) { console.log('Reset Schedule (next 48h)\n' + '─'.repeat(width)); for (const { provider, resets_in_seconds_from_now, resets_at_iso } of cycleStagger(entries)) { const timeStr = resets_at_iso.slice(11, 16) + ' UTC'; console.log(` ${provider.padEnd(16)} ~${formatDuration(resets_in_seconds_from_now)} (${timeStr})`); } } if (showRotation) { console.log('Rotation Recommendation\n' + '─'.repeat(width)); rotationRank(entries).forEach(({ provider, reason, severity }, i) => { const icon = severity === 'ok' ? '✓' : severity === 'critical' ? '✗' : '~'; console.log(` ${i + 1}. ${provider.padEnd(16)} ${icon} ${reason}`); }); } })();