- analyze.js: burn rate, weekly reconstruction, cycle stagger, rotation rank, underspend alerts, log prune with weekly archive - logger.js: getCachedRun(maxAgeMinutes) — skip probing if recent data exists - monitor.js: cache guard at wake — 20-min dedup, zero extra API calls - test.js: fix type assertion for gemini-api/xai-api providers (+5 passing); add 14 new tests for cache guard and analyze.js (162 total, all green) - docs/analyze.md: usage reference Co-authored-by: Hannibal Smith <hannibal@trentuna.com>
47 lines
1.7 KiB
JavaScript
47 lines
1.7 KiB
JavaScript
/**
|
|
* logger.js — persistent JSONL log to ~/.logs/token-monitor/YYYY-MM-DD.jsonl
|
|
*/
|
|
|
|
import { appendFileSync, mkdirSync, existsSync, readFileSync } from 'fs';
|
|
import { homedir } from 'os';
|
|
import { join } from 'path';
|
|
|
|
export function logRun(data) {
|
|
const dir = join(homedir(), '.logs', 'token-monitor');
|
|
mkdirSync(dir, { recursive: true });
|
|
const file = join(dir, `${new Date().toISOString().slice(0, 10)}.jsonl`);
|
|
appendFileSync(file, JSON.stringify({ ts: new Date().toISOString(), ...data }) + '\n');
|
|
}
|
|
|
|
export function getLogPath() {
|
|
const today = new Date().toISOString().slice(0, 10);
|
|
return join(homedir(), '.logs', 'token-monitor', `${today}.jsonl`);
|
|
}
|
|
|
|
/**
|
|
* Returns the last logged run if it was within maxAgeMinutes, otherwise null.
|
|
* Skips test/empty entries (entries where providers has no typed providers).
|
|
*/
|
|
export function getCachedRun(maxAgeMinutes = 20) {
|
|
const dir = join(homedir(), '.logs', 'token-monitor');
|
|
const today = new Date().toISOString().slice(0, 10);
|
|
const file = join(dir, `${today}.jsonl`);
|
|
if (!existsSync(file)) return null;
|
|
|
|
const lines = readFileSync(file, 'utf-8').trim().split('\n').filter(Boolean);
|
|
|
|
for (let i = lines.length - 1; i >= 0; i--) {
|
|
try {
|
|
const entry = JSON.parse(lines[i]);
|
|
const providers = entry.providers || {};
|
|
// Skip test/empty entries — real entries have at least one provider with a type
|
|
const hasRealData = Object.values(providers).some(p => p && p.type);
|
|
if (!hasRealData) continue;
|
|
|
|
const ageMinutes = (Date.now() - new Date(entry.ts).getTime()) / 60000;
|
|
if (ageMinutes <= maxAgeMinutes) return entry;
|
|
return null; // last real entry is too old
|
|
} catch { continue; }
|
|
}
|
|
return null;
|
|
}
|