Files
brain/tools/cost-stop-hook.mjs
T
2026-06-15 19:21:13 +03:00

104 lines
3.5 KiB
JavaScript

#!/usr/bin/env node
/**
* Stop-hook — cost-daily.json updater.
*
* On each Stop event, aggregates today's episodes from the current month's
* JSONL and writes/updates `~/.claude/runtime/cost-daily.json`.
*
* Spec: docs/superpowers/specs/2026-05-24-llm-first-router-overhaul-design.md §12.
* Closes brain-retro #9 Candidate 4 («cost-daily.json пуст — cost-tracker не пишет»).
*
* Fail-quiet: all I/O is try/catch; never crashes the Stop-hook pipeline.
*/
import { readFileSync, writeFileSync, existsSync, mkdirSync } from 'fs';
import { join, dirname } from 'path';
import { homedir } from 'os';
import { fileURLToPath } from 'url';
import { aggregateDay } from './cost-aggregator.mjs';
import { PRICING } from './cost-pricing.mjs';
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
export function todayISO(now = new Date()) {
const y = now.getUTCFullYear();
const m = String(now.getUTCMonth() + 1).padStart(2, '0');
const d = String(now.getUTCDate()).padStart(2, '0');
return `${y}-${m}-${d}`;
}
export function currentMonthFile(now = new Date(), repoRoot = process.cwd(), stateDir = 'docs/observer') {
const y = now.getUTCFullYear();
const m = String(now.getUTCMonth() + 1).padStart(2, '0');
return join(repoRoot, stateDir, `episodes-${y}-${m}.jsonl`);
}
export function readEpisodesJsonl(path) {
if (!path || !existsSync(path)) return [];
try {
const raw = readFileSync(path, 'utf-8');
const out = [];
for (const line of raw.split('\n')) {
const s = line.trim();
if (!s) continue;
try { out.push(JSON.parse(s)); } catch { /* skip malformed */ }
}
return out;
} catch { return []; }
}
export function loadCostDaily(path) {
if (!path || !existsSync(path)) return {};
try {
const raw = readFileSync(path, 'utf-8');
const j = JSON.parse(raw);
return (j && typeof j === 'object') ? j : {};
} catch { return {}; }
}
export function runUpdate({ episodes, dateISO, existing, pricing }) {
const day = aggregateDay(episodes, dateISO, pricing);
return { ...existing, [dateISO]: day };
}
function writeCostDaily(path, data) {
try {
mkdirSync(dirname(path), { recursive: true });
writeFileSync(path, JSON.stringify(data, null, 2));
return true;
} catch { return false; }
}
async function main() {
try {
const repoRoot = process.cwd();
const now = new Date();
const date = todayISO(now);
let stateDir = 'docs/observer';
try {
const { loadConfig, resolveStateDir } = await import('./brain-config.mjs');
({ stateDir } = resolveStateDir(loadConfig(repoRoot).state_dir));
} catch (e) {
console.warn('[cost-stop] brain-config недоступен, fallback docs/observer:', e && e.message);
}
const monthFile = currentMonthFile(now, repoRoot, stateDir);
const episodes = readEpisodesJsonl(monthFile);
const costDailyPath = join(homedir(), '.claude', 'runtime', 'cost-daily.json');
const existing = loadCostDaily(costDailyPath);
const updated = runUpdate({ episodes, dateISO: date, existing, pricing: PRICING });
writeCostDaily(costDailyPath, updated);
// Stop-hook contract: print empty JSON to stdout on success, exit 0.
process.stdout.write('{}');
process.exit(0);
} catch {
// Fail-quiet: never crash the Stop pipeline.
process.stdout.write('{}');
process.exit(0);
}
}
const isCli = process.argv[1] && process.argv[1].replace(/\\/g, '/').endsWith('/cost-stop-hook.mjs');
if (isCli) main();