Something went wrong. Try again.
more up to date at codeberg https://codeberg.org/cowie/tau.git
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234#!/usr/bin/env node
import { existsSync, readdirSync, readFileSync } from "node:fs";import { homedir } from "node:os";import { join, resolve } from "node:path";
interface UsageCost { input?: number; output?: number; cacheRead?: number; cacheWrite?: number; total?: number;}
interface Usage { input?: number; output?: number; cacheRead?: number; cacheWrite?: number; totalTokens?: number; cost?: UsageCost;}
interface AssistantMessage { role?: string; provider?: string; usage?: Usage; timestamp?: number;}
interface SessionEntry { type?: string; timestamp?: string; message?: AssistantMessage;}
interface Totals { input: number; output: number; cacheRead: number; cacheWrite: number; totalTokens: number; costInput: number; costOutput: number; costCacheRead: number; costCacheWrite: number; costTotal: number; assistantMessages: number; sessions: Set<string>;}
interface DayStats extends Totals { providers: Map<string, Totals>;}
interface Args { days: number; cwd: string; sessionsBase: string;}
function createTotals(): Totals { return { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, totalTokens: 0, costInput: 0, costOutput: 0, costCacheRead: 0, costCacheWrite: 0, costTotal: 0, assistantMessages: 0, sessions: new Set<string>(), };}
function createDayStats(): DayStats { return { ...createTotals(), providers: new Map<string, Totals>(), };}
function addUsage(totals: Totals, usage: Usage, sessionFile: string): void { const cost = usage.cost ?? {}; totals.input += usage.input ?? 0; totals.output += usage.output ?? 0; totals.cacheRead += usage.cacheRead ?? 0; totals.cacheWrite += usage.cacheWrite ?? 0; totals.totalTokens += usage.totalTokens ?? 0; totals.costInput += cost.input ?? 0; totals.costOutput += cost.output ?? 0; totals.costCacheRead += cost.cacheRead ?? 0; totals.costCacheWrite += cost.cacheWrite ?? 0; totals.costTotal += cost.total ?? 0; totals.assistantMessages += 1; totals.sessions.add(sessionFile);}
function encodeSessionDir(cwd: string): string { const normalized = cwd.startsWith("/") ? cwd.slice(1) : cwd; return `--${normalized.replace(/\//g, "-")}--`;}
function localDayKey(date: Date): string { const year = date.getFullYear(); const month = String(date.getMonth() + 1).padStart(2, "0"); const day = String(date.getDate()).padStart(2, "0"); return `${year}-${month}-${day}`;}
function parseArgs(): Args { const args = process.argv.slice(2); let days = 7; let cwd = process.cwd(); let sessionsBase = join(homedir(), ".pi", "agent", "sessions");
for (let i = 0; i < args.length; i++) { const arg = args[i]; if ((arg === "--days" || arg === "-n") && args[i + 1]) { days = Number.parseInt(args[++i], 10); } else if ((arg === "--cwd" || arg === "--dir" || arg === "-d") && args[i + 1]) { cwd = resolve(args[++i]); } else if (arg === "--sessions-base" && args[i + 1]) { sessionsBase = resolve(args[++i]); } else if (arg === "--help" || arg === "-h") { console.log(`Usage: scripts/stats.ts [options]
Options: -n, --days <days> Number of local calendar days to include (default: 7) -d, --dir, --cwd <path> Project cwd to inspect (default: current cwd) --sessions-base <path> Sessions base directory (default: ~/.pi/agent/sessions) -h, --help Show this help`); process.exit(0); } }
if (!Number.isInteger(days) || days <= 0) { throw new Error("--days must be a positive integer"); }
return { days, cwd: resolve(cwd), sessionsBase };}
function formatInt(value: number): string { return Math.round(value).toLocaleString("en-US");}
function formatCost(value: number): string { return `$${value.toFixed(4)}`;}
function printTotals(label: string, totals: Totals): void { console.log( `${label.padEnd(16)} messages: ${String(totals.assistantMessages).padStart(5)} sessions: ${String(totals.sessions.size).padStart(3)} ` + `input: ${formatInt(totals.input).padStart(12)} output: ${formatInt(totals.output).padStart(10)} ` + `cache read: ${formatInt(totals.cacheRead).padStart(13)} cache write: ${formatInt(totals.cacheWrite).padStart(10)} ` + `total: ${formatInt(totals.totalTokens).padStart(13)} cost: ${formatCost(totals.costTotal).padStart(10)}`, );}
const { days, cwd, sessionsBase } = parseArgs();const sessionsDir = join(sessionsBase, encodeSessionDir(cwd));
if (!existsSync(sessionsDir)) { throw new Error(`Sessions directory not found: ${sessionsDir}`);}
const now = new Date();const start = new Date(now.getFullYear(), now.getMonth(), now.getDate());start.setDate(start.getDate() - days + 1);const end = new Date(now.getFullYear(), now.getMonth(), now.getDate() + 1);
const stats = new Map<string, DayStats>();const grandTotal = createTotals();
for (const file of readdirSync(sessionsDir)) { if (!file.endsWith(".jsonl")) continue;
const path = join(sessionsDir, file); const lines = readFileSync(path, "utf8").split("\n"); for (const line of lines) { if (!line.trim()) continue;
let entry: SessionEntry; try { entry = JSON.parse(line) as SessionEntry; } catch { continue; }
if (entry.type !== "message" || entry.message?.role !== "assistant" || !entry.message.usage) continue;
const timestamp = entry.message.timestamp !== undefined ? new Date(entry.message.timestamp) : new Date(entry.timestamp ?? 0); if (timestamp < start || timestamp >= end) continue;
const dayKey = localDayKey(timestamp); let dayStats = stats.get(dayKey); if (!dayStats) { dayStats = createDayStats(); stats.set(dayKey, dayStats); }
const provider = entry.message.provider ?? "unknown"; let providerStats = dayStats.providers.get(provider); if (!providerStats) { providerStats = createTotals(); dayStats.providers.set(provider, providerStats); }
addUsage(dayStats, entry.message.usage, file); addUsage(providerStats, entry.message.usage, file); addUsage(grandTotal, entry.message.usage, file); }}
console.log(`Usage for ${cwd}`);console.log(`Sessions: ${sessionsDir}`);console.log(`Period: ${localDayKey(start)} through ${localDayKey(new Date(end.getTime() - 1))} (${days} local days)`);console.log("".padEnd(160, "="));
for (const day of [...stats.keys()].sort()) { const dayStats = stats.get(day)!; printTotals(day, dayStats); for (const provider of [...dayStats.providers.keys()].sort()) { printTotals(` ${provider}`, dayStats.providers.get(provider)!); }}
console.log("".padEnd(160, "-"));printTotals("TOTAL", grandTotal);