tokenspendorg
v0.2.0
Published
Zero-setup CLI to backfill local agent-CLI transcript usage (Claude Code first) into tokenspend.org via POST /api/v1/ingest. Reads ~/.claude locally; only usage metadata is uploaded — never your prompts.
Maintainers
Readme
tokenspendorg
Zero-setup, zero-admin backfill of your local agent-CLI usage history into
tokenspend.org. v1 reads Claude Code's on-disk
transcript store (~/.claude/projects/**/*.jsonl), reconstructs per-call token
usage, and uploads it to the existing flat POST /api/v1/ingest.
ingest and setup read usage metadata only — model, timestamps, token
counts. They never read, store, or transmit your prompts, responses, tool I/O, or
file contents. The separate analyze command opts into
sending locally-redacted conversation text to Haiku on your own machine to
classify it, and uploads only the resulting normalized task summaries — see its
section and the Privacy notes below.
See the full design contract in
../docs/context/08-local-transcript-backfill-cli-spec.md.
Usage
TOKENSPEND_TOKEN="ts_…" \
TOKENSPEND_URL="eu.tokenspend.org" \
npx tokenspendorg ingest --connectors claudePreview first (nothing leaves your machine):
npx tokenspendorg ingest --connectors claude --dry-runGo live for future sessions. setup writes the OpenTelemetry config into the
env block of Claude Code's settings.json (default ~/.claude/settings.json),
tailored to your org's identity mode:
npx tokenspendorg setup --connectors claudeBecause the config lives in settings.json rather than a shell rc file, Claude
Code picks it up on its next startup with no shell restart — and it works
the same whether Claude Code is launched from a terminal, an IDE, the desktop
app, or a wrapper like Conductor (none of which source your shell rc).
If you previously ran an older version that wrote export … lines into a shell
startup file, setup migrates you automatically: it detects the old
tokenspendorg block (in ~/.zshrc, ~/.zshenv, ~/.bashrc,
~/.bash_profile, ~/.profile, ~/.config/fish/config.fish, plus any file it
recorded before or one you pass via --rc-file) and removes it as part of the
update.
The chosen method and version are recorded under setup in
~/.config/tokenspendorg/config.json (setup.version), so re-runs and future
upgrades know how the machine was last configured. Run tokenspendorg config
to see it.
If your machine has an enterprise managed settings file
(/Library/Application Support/ClaudeCode/managed-settings.json on macOS,
/etc/claude-code/managed-settings.json on Linux) that already defines these
telemetry vars, setup won't touch your user settings.json — managed
settings are the highest-precedence scope and can't be overridden, so a
user-level write would silently do nothing. Instead it tells you which keys are
pinned and where, so you (or your admin) can point the managed exporter at this
org.
Preview without writing (--print), or skip the confirmation (--yes). To
write somewhere other than <claude-dir>/settings.json, pass
--settings-file <path>.
Task analysis (analyze)
analyze is a different kind of signal: instead of token counts, it figures out
what you actually do with Claude Code. It condenses your recent sessions,
classifies each into a normalized, reusable task with Haiku, and uploads
those task summaries to POST /api/v1/task-analysis.
# One pass now — preview which sessions would be analyzed, send nothing:
npx tokenspendorg analyze --dry-run
# One real pass:
npx tokenspendorg analyzeEach task looks like Write a web scraper using Playwright to extract product
prices with a category (engineering/research/writing/ops/analysis/
planning/communication), a success level (delivered_clean →
delivered_with_friction → partial → abandoned), and any friction points —
the taxonomy ported from the task-profile plugin.
How Haiku is called. analyze shells out to your local claude CLI in
headless mode (claude -p --model claude-haiku-4-5), so it reuses your existing
Claude Code auth — no extra API key. Those Haiku calls run in a dedicated working
directory (--analyzer-dir, default ~/.config/tokenspendorg/analyzer) with
telemetry disabled, and their own transcripts are excluded from both ingest
and the next analyze pass, so the daemon never measures or re-analyzes itself.
Run it every hour as a background service (macOS / launchd):
npx tokenspendorg analyze --install --interval 1h # asks before writing the agent
npx tokenspendorg analyze --status # is it loaded?
npx tokenspendorg analyze --uninstall--install writes a per-user LaunchAgent (~/Library/LaunchAgents/org.tokenspend.analyzer.plist,
RunAtLoad + KeepAlive) that runs analyze --interval 1h at login and restarts
it if it exits; logs go to ~/Library/Logs/tokenspendorg-analyzer.log. Use
--print to see the agent without installing. Without the service you can just
run analyze --interval 1h in a terminal/tmux — same loop, foreground.
State is tracked per-session in the same --state-file (key claude-analyze), so
re-runs only analyze sessions they haven't seen. --max-sessions caps how many a
single pass classifies (the rest run next pass), keeping per-tick Haiku cost bounded.
Options
| Flag | Env | Default | Meaning |
|---|---|---|---|
| --token | TOKENSPEND_TOKEN | — (required) | org API key ts_…, sent as Authorization: Bearer. |
| --url | TOKENSPEND_URL | https://tokenspend.org | host; a bare host is normalized to https://. |
| --email | TOKENSPEND_EMAIL | — | override the stored identity email (see Identity below). |
| --reconfigure | — | off | re-prompt for email and persist the new answer. |
| --user-id | TOKENSPEND_USER_ID | auto-generated | use a specific user_id (e.g. one from another device); persisted and reused. |
| --connectors | — | claude | comma list of local sources. |
| --claude-dir | — | ~/.claude | Claude store directory. |
| --since / --until | — | — | ISO date window on event timestamp. |
| --limit | — | — | cap records (testing). |
| --state-file | — | ~/.claude/.tokenspend-backfill-state.json | dedup state. |
| --no-dedup | — | off | (ingest) ignore state, re-send everything (footgun). |
| --dry-run | — | off | (ingest/analyze) summarize/preview; send nothing. |
| --print | — | off | (setup/analyze --install) print the settings/agent instead of writing it. |
| --yes | — | off | (setup/analyze --install) skip the confirmation prompt before writing. |
| --interval | — | — | (analyze) run as a loop every <dur> (1h, 30m, 90s). Omit for a single pass. |
| --once | — | off | (analyze) force a single pass even with --interval. |
| --max-sessions | — | 30 | (analyze) cap sessions classified per pass. |
| --haiku-model | — | claude-haiku-4-5 | (analyze) model for classification. |
| --analyzer-dir | — | ~/.config/tokenspendorg/analyzer | (analyze) working dir for the local claude calls; its transcripts are excluded from ingest. |
| --install / --uninstall / --status | — | — | (analyze) manage the launchd background service (macOS). |
| --settings-file | — | <claude-dir>/settings.json | (setup) override the settings.json to write. |
| --managed-settings-file | — | OS managed path | (setup) override the managed-settings.json checked for an existing telemetry override; if it pins these vars, setup writes nothing. |
| --shell | — | — | (setup) only used to locate an old shell block to clean up: bash/zsh/fish. |
| --rc-file | — | — | (setup) only used to locate an old shell block to clean up. |
Identity
A cryptographically-random 128-bit id is generated on first run and persisted
in ~/.config/tokenspendorg/config.json (mode 0600; respects $XDG_CONFIG_HOME).
This id is used as user_id on every upload.
You can override it with --user-id <id> (or TOKENSPEND_USER_ID) — useful to
reuse an id you already have on another device so both machines map to the same
user (especially in anonymous mode, where there's no email to bridge them). An
explicit id is remembered like the email choice. The random default stays
recommended: a guessable id is guessable by anyone holding the org key.
Email is opt-in and org-gated: the CLI only prompts for an email when your
org's identity_mode is "email". It asks once, offering the address detected
from ~/.claude.json or git config user.email as a confirmable default; you
may answer anonymous to stay anonymous. The answer is remembered in the same
config file. Pass --reconfigure to re-prompt and update the stored choice.
The backend resolves a canonical user by checking email first, then the client-supplied id — so multiple machines with the same email key onto one row automatically.
Live telemetry (setup) carries this id as a custom OTEL attribute
tokenspend.user_id, not the reserved user.id. Claude Code keeps its own
random user.id and silently ignores any override of it, so tokenspend sends and
prioritises tokenspend.user_id (the backend reads it ahead of user.id; absent
→ falls back to the native id). In email orgs user.email is sent too and the
backend keys on that. Caveat: a machine with Claude Code managed settings that
pin the OTLP endpoint elsewhere sends live telemetry there, not to tokenspend —
use ingest (which is unaffected) on such hosts.
Privacy & re-run safety
ingest/setup: content never leaves the machine. Onlymodel,ts, token counts, and aconversation_idare uploaded.analyzeis the one exception, and it's bounded. It sends your conversation text — locally redacted for secrets (API keys, tokens, private keys, emails, cards, IBANs) — only to Haiku, on your own machine, via your own Claude Code auth, to classify it. What reaches the tokenspend backend is only the resulting normalized task summaries (task name, category, success, friction) — never your raw prompts, responses, tool I/O, or file contents.- Email is never sent silently. It is transmitted only when your org is in
identity_mode='email'and you have explicitly confirmed or provided it. - Safe to re-run. A local state file records which
message.ids (ingest) and session ids (analyze) were already processed, so re-runs top up only new work.
Development
Near-zero runtime dependencies — Node built-ins only (fetch, node:fs,
node:readline, node:crypto, node:os, node:path, node:util,
node:child_process). TypeScript dev dependency only.
bun install # dev deps (typescript, @types/node)
bun test # run the test suite
bun run build # tsc → dist/ (the shipped, Node-compatible artifact)
node dist/cli.js --helpThe shipped artifact (dist/cli.js, #!/usr/bin/env node) runs on plain Node ≥18.
