01 / codex-spend · live on npm
A local CLI that parses Codex sessions on disk and shows token burn by model and by file. 1,474 npm downloads, no telemetry, no auth.
codex-spend answers a question I kept asking after long agent sessions: where did all those tokens go. It is a small CLI you run with npx, it reads the Codex session files already sitting on your disk, and it prints a tidy table of token burn grouped by model and by file. No login, no key, no network.
The tool lives on npm with 1,474 downloads in the last 12 months. Strangers keep installing it, which is the nicest signal a CLI can get. The constraint that shaped everything was simple: do not add auth. If the data is already on disk, parse it there.
Limitation: it is only as good as the log format. If Codex changes how it writes sessions, the parser needs an update.
The core loop is boring in the best way. Walk the sessions directory, read each JSONL line, pull out the model name and usage fields, and accumulate. No API call. The speed comes from staying local and keeping dependencies at zero.
// find every Codex session on disk, no network
import { readdir, readFile } from 'node:fs/promises'
import path from 'node:path'
export async function collectSessions(root) {
const files = await readdir(root)
const sessions = []
for (const f of files.filter(f => f.endsWith('.jsonl'))) {
const raw = await readFile(path.join(root, f), 'utf8')
for (const line of raw.split('\n')) {
if (!line.trim()) continue
sessions.push(JSON.parse(line))
}
}
return sessions
}
// tokens[model] and tokensByFile[file] in one pass
export function summarize(sessions) {
const byModel = {}, byFile = {}
for (const s of sessions) {
const m = s.model ?? 'unknown'
const n = s.usage?.total ?? 0
byModel[m] = (byModel[m] ?? 0) + n
if (s.file) byFile[s.file] = (byFile[s.file] ?? 0) + n
}
return { byModel, byFile, total: Object.values(byModel).reduce((a,b)=>a+b,0) }
}
// npx codex-spend --last 7d or --last 30d
import { program } from 'commander'
program.option('--last <window>', '7d or 30d', '7d')
.action(async (opts) => {
const cutoff = opts.last === '30d' ? daysAgo(30) : daysAgo(7)
const rows = await loadAndFilter(cutoff)
printTable(summarize(rows))
})
An auth screen would have killed the flow. The moment you add a key, the tool needs docs, error states, and support. Keeping it local means the install is one line and the data never leaves the machine. That is the whole pitch.