#!/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();