Track LLM token usage and cost per GitHub issue. Any agent (Claude Code, OpenCode, Cursor, or your own tooling) calls the CLI to log usage; you browse a TUI to see where time and money went.
- Log token usage from any LLM agent via a single CLI call
- Validates the GitHub issue exists before logging — no phantom entries
- Track by agent name and model separately (e.g.
claude-coderunningclaude-sonnet-4-6) - Sessions with 30-minute idle auto-close for wall-clock time tracking
- GitHub issue metadata (title, labels, URL) cached in the local DB at log time
- TUI: issue list with clickable
#NOSC 8 hyperlinks, per-issue detail with title/labels, token usage chart over time - Open issues in the browser with
o, refresh cached metadata withr - Report and export include issue title and labels
- Ed25519-signed JSON export for auditing
- Pricing config with built-in defaults and per-model overrides
- SQLite storage — local, no external services required
Prerequisites: Go 1.25+
git clone https://github.com/cdimonaco/tokenpile
cd tokenpile
make installThis installs the binary to $GOPATH/bin. Make sure that is on your PATH.
To build without installing:
make build
# produces ./tokenpiletokenpile auth login --provider githubThis opens a browser window for OAuth. The token is stored in your OS keychain (or an encrypted file on headless Linux).
tokenpile skill install --agent claude-code # writes ~/.claude/skills/tokenpile.md
tokenpile skill install --agent codex # appends a block to ~/.codex/AGENTS.md
tokenpile skill install --agent opencode # appends a block to ~/.config/opencode/AGENTS.mdAfter installation, the agent will automatically call tokenpile log at the end of each response where significant work was done.
For codex and opencode, the skill is appended to their shared AGENTS.md file using HTML comment markers (<!-- tokenpile:start --> / <!-- tokenpile:end -->). Your existing instructions are never touched. Running the command again updates only the tokenpile block in place.
See tokenpile skill list for supported agents and their installation status.
tokenpile log \
--issue 42 \
--agent claude-code \
--model claude-sonnet-4-6 \
--tokens-in 12000 \
--tokens-out 3000--repo is optional if you run from inside a git repository with a GitHub remote. Otherwise pass it explicitly:
tokenpile log --issue 42 --agent claude-code --model claude-sonnet-4-6 \
--tokens-in 12000 --tokens-out 3000 --repo owner/repotokenpileIssue list
| Key | Action |
|---|---|
j / k |
navigate up/down |
enter |
open issue detail |
o |
open selected issue in browser |
c |
open chart view |
esc |
go back |
? |
toggle help |
q |
quit |
The list shows a clickable #N link for each issue. In terminals that support OSC 8 hyperlinks (iTerm2, Kitty, most modern terminals) clicking the link opens the issue in your browser.
Issue detail
| Key | Action |
|---|---|
o |
open issue in browser |
r |
refresh title and labels from GitHub |
c |
open chart view |
esc |
go back |
d / w |
day / week granularity (chart) |
q |
quit |
Record token usage for an issue.
tokenpile log --issue <num> --agent <name> --model <model> \
--tokens-in <n> --tokens-out <n> [--repo owner/repo]Sessions are managed automatically. The first call for an (issue, repo) pair starts a session; subsequent calls within 30 minutes reuse it. After 30 minutes of inactivity the session is closed and a new one starts on the next call.
The log command validates that the issue exists on GitHub before inserting the entry. If the issue is not found, the command fails with an error. Issue title and labels are cached in the local DB for use in reports, exports, and the TUI.
Print a per-(agent, model) breakdown for an issue. The header shows the issue title, URL, and labels if they are in the local cache.
tokenpile report --issue 42
tokenpile report --issue 42 --repo owner/repotokenpile auth login --provider github # open browser, store token
tokenpile auth logout --provider github # remove stored token
tokenpile auth status # show login statetokenpile pricing list # show merged config
tokenpile pricing set my-model --in 1.50 --out 6.00 # add/override a modelPrices are per million tokens. Built-in defaults cover the most common Claude, GPT, and Gemini models. User overrides are stored at ~/.config/tokenpile/pricing.yaml and take precedence.
Export usage data as an Ed25519-signed JSON document.
tokenpile export # all data, to stdout
tokenpile export --output data.json # write to file
tokenpile export --repo owner/repo --issue 42 --agent claude-code
tokenpile export --from 2026-01-01T00:00:00Z --to 2026-07-01T00:00:00ZVerify a previously exported file:
tokenpile export verify --file data.jsontokenpile skill list # show agents and install status
tokenpile skill install --agent claude-code # dedicated file: ~/.claude/skills/tokenpile.md
tokenpile skill install --agent codex # append/update block in ~/.codex/AGENTS.md
tokenpile skill install --agent opencode # append/update block in ~/.config/opencode/AGENTS.mdFor shared AGENTS.md targets (codex, opencode) the command prints a summary of what it did — whether it created the file, appended a new block, or updated an existing one — so you always know exactly what changed.
| Variable | Default | Description |
|---|---|---|
TOKENPILE_CONFIG_DIR |
~/.config/tokenpile |
Config directory (overrides XDG) |
TOKENPILE_DATA_DIR |
~/.local/share/tokenpile |
Data directory (overrides XDG) |
TOKENPILE_LOG_LEVEL |
info |
Log level: debug, info, warn, error |
TOKENPILE_LOG_FORMAT |
text |
Log format: text or json |
TOKENPILE_GITHUB_CLIENT_ID |
baked in | Override the built-in GitHub OAuth client ID (development only) |
TOKENPILE_GITHUB_CLIENT_SECRET |
baked in | Override the built-in GitHub OAuth client secret (development only) |
XDG base directories (XDG_CONFIG_HOME, XDG_DATA_HOME) are respected when the TOKENPILE_* overrides are not set.
On first run, an Ed25519 signing keypair is generated at ~/.config/tokenpile/identity.{key,pub} (permissions 0600/0644).
Install asdf and the plugins for Go, golangci-lint, goreleaser, and mockery:
asdf plugin add golang
asdf plugin add golangci-lint
asdf plugin add goreleaser
asdf plugin add mockery
asdf install # reads .tool-versions and installs all pinned versionsmake build # build binary to ./tokenpile
make test # run all tests with race detector
make lint # run golangci-lint
make fmt # format all Go files with gofmt
make generate # regenerate mocks (requires mockery from asdf)
make clean # remove build artifactsCI runs fmt check, lint, and test -race on every push and pull request.
To test the GitHub auth flow locally, create your own OAuth App (GitHub → Settings → Developer settings → OAuth Apps) and set:
export TOKENPILE_GITHUB_CLIENT_ID=your_dev_client_id
export TOKENPILE_GITHUB_CLIENT_SECRET=your_dev_client_secretThese env vars override the baked-in values. Released binaries have the production credentials injected by goreleaser via ldflags — end users do not need to set anything.
cmd/tokenpile/ CLI entry point and composition root
internal/
usage/ shared data types (Entry, Session, Report, ...)
store/ Store interface + SQLite adapter
provider/ AuthProvider, IssueProvider, Issue type, GitHub implementations
pricing/ two-layer pricing config and cost computation
export/ Ed25519-signed canonical JSON export
skill/ embedded agent skill templates
tui/ Bubble Tea TUI
config/ XDG path resolution and Ed25519 identity management
mocks/ generated mocks for unit tests
schema/ JSON Schema for the export document
- Conventional commits:
feat:,fix:,chore:,docs:,test:,refactor:,ci: - One logical change per commit.
- No emojis anywhere.
- All dependencies injected via constructors. No globals.
context.Contextis the first parameter of every function that does I/O.- Package names describe what they contain, not architectural layers. No
domain,model, ordto. - See CLAUDE.md for the full conventions reference.
- Add a template file at
internal/skill/templates/<agent-name>.md. - Add the agent entry to the
agentsslice ininternal/skill/skill.gowith itsInstallPathfunction. SetShared: trueif the target file is shared with other content (e.g. AGENTS.md) — the install will append/update a marker-delimited block instead of overwriting the file. - Add tests in
internal/skill/skill_test.go.
go test ./internal/store/... # store tests only
go test -run TestSQLiteStore ./... # filter by name
go test -race -count=1 ./... # disable test cache, enable race detector