fuzzyworld

when-am-i / commit

chore: add `npm run usage` to print the commit usage trailer

091a00cbe3a2d655bd7a39eae9c0984f442ab473 · Pavel Safronov · 2026-06-20 11:56:21 -0700 · parents 73912a477217

chore: add `npm run usage` to print the commit usage trailer

scripts/usage.mts sums the session token tally from this repo's Claude Code
transcript JSONL and reads rtk's lifetime savings, printing the two-line
Usage/rtk trailer in human units — ready to paste into a commit-approval message.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
 package.json      |  1 +
 scripts/usage.mts | 82 +++++++++++++++++++++++++++++++++++++++++++++++++++++++
 2 files changed, 83 insertions(+)

diff --git a/package.json b/package.json
index 65e611b..bb75cf1 100644
--- a/package.json
+++ b/package.json
@@ -19,6 +19,7 @@
         "screenshot:calendar": "tsx scripts/screenshot-calendar.mts",
         "screenshot:calendars": "tsx scripts/screenshot-calendars.mts",
         "gen:badges": "tsx scripts/gen-badges.mts",
+        "usage": "tsx scripts/usage.mts",
         "gen:favicon": "sips -z 64 64 calendar-source.png --out src/assets/favicon.png",
         "copy:leaflet-icons": "mkdir -p public && cp node_modules/leaflet/dist/images/marker-icon.png public/ && cp node_modules/leaflet/dist/images/marker-icon-2x.png public/ && cp node_modules/leaflet/dist/images/marker-shadow.png public/",
         "postinstall": "npm run copy:leaflet-icons"
diff --git a/scripts/usage.mts b/scripts/usage.mts
new file mode 100644
index 0000000..0d610ac
--- /dev/null
+++ b/scripts/usage.mts
@@ -0,0 +1,82 @@
+// Print the session usage trailer for commit messages.
+//
+//   npm run usage
+//
+// Emits the two-line trailer this project appends to commit messages — a session
+// token tally (summed from the Claude Code transcript JSONL for this repo) and
+// rtk's lifetime savings — already formatted in human units (K/M/G). Copy-paste
+// the output straight into the message shown for commit approval.
+
+import { readFileSync, readdirSync, statSync } from 'node:fs';
+import { execSync } from 'node:child_process';
+import os from 'node:os';
+import path from 'node:path';
+
+/** 26594 -> "26.6K", 5_110_043 -> "5.1M". */
+function human(n: number): string {
+    if (n >= 1e9) return `${(n / 1e9).toFixed(1)}G`;
+    if (n >= 1e6) return `${(n / 1e6).toFixed(1)}M`;
+    if (n >= 1e3) return `${(n / 1e3).toFixed(1)}K`;
+    return String(n);
+}
+
+// The transcript dir mirrors the cwd with every non-alphanumeric char as '-'.
+function transcriptDir(): string {
+    const slug = process.cwd().replace(/[^a-zA-Z0-9]/g, '-');
+    return path.join(os.homedir(), '.claude', 'projects', slug);
+}
+
+function latestTranscript(dir: string): string | null {
+    let files: string[];
+    try {
+        files = readdirSync(dir).filter((f) => f.endsWith('.jsonl'));
+    } catch {
+        return null;
+    }
+    if (files.length === 0) return null;
+    return files
+        .map((f) => path.join(dir, f))
+        .sort((a, b) => statSync(b).mtimeMs - statSync(a).mtimeMs)[0];
+}
+
+function sessionTally(file: string): { in: number; out: number; cached: number } {
+    const totals = { in: 0, out: 0, cached: 0 };
+    for (const line of readFileSync(file, 'utf-8').split('\n')) {
+        if (!line.trim()) continue;
+        try {
+            const u = JSON.parse(line)?.message?.usage;
+            if (!u) continue;
+            totals.in += u.input_tokens ?? 0;
+            totals.out += u.output_tokens ?? 0;
+            totals.cached += u.cache_read_input_tokens ?? 0;
+        } catch {
+            // Skip any non-JSON / partial line.
+        }
+    }
+    return totals;
+}
+
+// Parse `rtk gain` for "Total commands:" and "Tokens saved: 81.1K (90.9%)".
+function rtkLine(): string {
+    try {
+        const out = execSync('rtk gain', { encoding: 'utf-8' });
+        const cmds = /Total commands:\s*([\d,]+)/.exec(out)?.[1]?.replace(/,/g, '') ?? '?';
+        const saved = /Tokens saved:\s*([\d.]+[KMG]?)\s*\(([\d.]+)%\)/.exec(out);
+        const amount = saved?.[1] ?? '?';
+        const pct = saved?.[2] ?? '?';
+        return `rtk:   ${amount} saved, ${pct}% over ${cmds} cmds (global/lifetime)`;
+    } catch {
+        return 'rtk:   (rtk gain unavailable)';
+    }
+}
+
+const dir = transcriptDir();
+const file = latestTranscript(dir);
+if (!file) {
+    console.error(`No transcript JSONL found in ${dir}`);
+    process.exit(1);
+}
+
+const t = sessionTally(file);
+console.log(`Usage: ${human(t.in)} in / ${human(t.out)} out / ${human(t.cached)} cached (session tally)`);
+console.log(rtkLine());