fuzzyworld

when-am-i / files / scripts / usage.mts

usage.mts

Blame: the commit that last changed each line · file · history

091a00cb 2026-06-20 Pavel Safronov1// Print the session usage trailer for commit messages.
2//
3// npm run usage
4//
5// Emits the two-line trailer this project appends to commit messages — a session
6// token tally (summed from the Claude Code transcript JSONL for this repo) and
7// rtk's lifetime savings — already formatted in human units (K/M/G). Copy-paste
8// the output straight into the message shown for commit approval.
9
10import { readFileSync, readdirSync, statSync } from 'node:fs';
11import { execSync } from 'node:child_process';
12import os from 'node:os';
13import path from 'node:path';
14
15/** 26594 -> "26.6K", 5_110_043 -> "5.1M". */
16function human(n: number): string {
17 if (n >= 1e9) return `${(n / 1e9).toFixed(1)}G`;
18 if (n >= 1e6) return `${(n / 1e6).toFixed(1)}M`;
19 if (n >= 1e3) return `${(n / 1e3).toFixed(1)}K`;
20 return String(n);
21}
22
23// The transcript dir mirrors the cwd with every non-alphanumeric char as '-'.
24function transcriptDir(): string {
25 const slug = process.cwd().replace(/[^a-zA-Z0-9]/g, '-');
26 return path.join(os.homedir(), '.claude', 'projects', slug);
27}
28
29function latestTranscript(dir: string): string | null {
30 let files: string[];
31 try {
32 files = readdirSync(dir).filter((f) => f.endsWith('.jsonl'));
33 } catch {
34 return null;
35 }
36 if (files.length === 0) return null;
37 return files
38 .map((f) => path.join(dir, f))
39 .sort((a, b) => statSync(b).mtimeMs - statSync(a).mtimeMs)[0];
40}
41
42function sessionTally(file: string): { in: number; out: number; cached: number } {
43 const totals = { in: 0, out: 0, cached: 0 };
44 for (const line of readFileSync(file, 'utf-8').split('\n')) {
45 if (!line.trim()) continue;
46 try {
47 const u = JSON.parse(line)?.message?.usage;
48 if (!u) continue;
49 totals.in += u.input_tokens ?? 0;
50 totals.out += u.output_tokens ?? 0;
51 totals.cached += u.cache_read_input_tokens ?? 0;
52 } catch {
53 // Skip any non-JSON / partial line.
54 }
55 }
56 return totals;
57}
58
59// Parse `rtk gain` for "Total commands:" and "Tokens saved: 81.1K (90.9%)".
60function rtkLine(): string {
61 try {
62 const out = execSync('rtk gain', { encoding: 'utf-8' });
63 const cmds = /Total commands:\s*([\d,]+)/.exec(out)?.[1]?.replace(/,/g, '') ?? '?';
64 const saved = /Tokens saved:\s*([\d.]+[KMG]?)\s*\(([\d.]+)%\)/.exec(out);
65 const amount = saved?.[1] ?? '?';
66 const pct = saved?.[2] ?? '?';
67 return `rtk: ${amount} saved, ${pct}% over ${cmds} cmds (global/lifetime)`;
68 } catch {
69 return 'rtk: (rtk gain unavailable)';
70 }
71}
72
73const dir = transcriptDir();
74const file = latestTranscript(dir);
75if (!file) {
76 console.error(`No transcript JSONL found in ${dir}`);
77 process.exit(1);
78}
79
80const t = sessionTally(file);
81console.log(`Usage: ${human(t.in)} in / ${human(t.out)} out / ${human(t.cached)} cached (session tally)`);
82console.log(rtkLine());