Skip to content

Commit a06d03f

Browse files
committed
feat: auto-scale cost thresholds with active currency
1 parent 6bd5fcb commit a06d03f

File tree

8 files changed

+184
-180
lines changed

8 files changed

+184
-180
lines changed

CLAUDE.md

Lines changed: 15 additions & 79 deletions
Original file line numberDiff line numberDiff line change
@@ -6,29 +6,6 @@ CLI tool that parses Claude Code JSONL logs from `~/.claude/projects/` and calcu
66

77
Go 1.26, stdlib only (zero external deps), GoReleaser for cross-platform builds
88

9-
## Structure
10-
11-
```text
12-
.
13-
├── main.go # CLI flags and entrypoint
14-
├── parser.go # JSONL log walking, file parsing (parseFile), deduplication by requestId
15-
├── pricing.go # Pricing resolution, cost calculation, model name resolution
16-
├── pricing.json # Externalized model pricing data (embedded + fetched from repo)
17-
├── format.go # Terminal and JSON output formatting
18-
├── color.go # ANSI color helpers (custom implementation, no external deps)
19-
├── statusline.go # Statusline mode + session exit hook (reads stdin JSON, outputs formatted cost)
20-
├── statusline_config.go # Statusline customization: config structs, segment registry, renderers
21-
├── currency.go # Config (~/.goccc.json): currency, exchange rates, cost thresholds
22-
├── mcp.go # MCP server detection, per-project disable filtering, plugin walk
23-
├── update.go # Version update checking and remote pricing cache refresh
24-
├── *_test.go # Table-driven tests for each module
25-
├── fixture_test.go # Integration test against realistic JSONL fixture
26-
├── mcp_fixture_test.go # Integration test against realistic MCP fixture (all sources + disabling)
27-
├── testdata/ # Static fixtures: JSONL (multi-turn convo with subagents), MCP (home + project layout)
28-
├── .goreleaser.yml # Release config (darwin/linux/windows, amd64/arm64)
29-
└── README.md # Usage docs and supported models
30-
```
31-
329
## Commands
3310

3411
```bash
@@ -48,69 +25,28 @@ make check
4825

4926
## JSONL Log Format
5027

51-
Claude Code stores conversation logs at `~/.claude/projects/<project-slug>/`.
52-
53-
### File layout
54-
55-
```text
56-
<project-slug>/
57-
<session-uuid>.jsonl # main conversation
58-
<session-uuid>/subagents/
59-
agent-<agentId>.jsonl # one file per subagent
60-
```
61-
62-
### Entry types
63-
64-
Only `type: "assistant"` entries carry `message.model` and `message.usage`. All others are skipped:
65-
`user`, `progress`, `summary`, `queue-operation`, `file-history-snapshot`.
66-
67-
### Usage object (the fields that matter for cost)
68-
69-
```json
70-
"usage": {
71-
"input_tokens": 2739,
72-
"output_tokens": 823,
73-
"cache_read_input_tokens": 23154,
74-
"cache_creation_input_tokens": 2125,
75-
"cache_creation": {
76-
"ephemeral_5m_input_tokens": 0,
77-
"ephemeral_1h_input_tokens": 2125
78-
}
79-
}
80-
```
81-
82-
- `output_tokens` already includes thinking tokens — there is no separate counter
83-
- `cache_creation` sub-object breaks down 5m/1h tiers; `cache_creation_input_tokens` is the flat total (fallback when sub-object is absent in older logs)
84-
- Extra fields (`server_tool_use`, `service_tier`, `inference_geo`, `speed`, `iterations`) are informational only
85-
86-
### Streaming dedup
87-
88-
One API call produces multiple JSONL entries sharing the same `requestId`. `input_tokens` and cache fields are identical across them; `output_tokens` grows. The last entry has the final count — our map-based dedup (overwrite) handles this correctly.
89-
90-
### Special entries
28+
Claude Code stores logs at `~/.claude/projects/<project-slug>/`. Sessions are `<uuid>.jsonl` with subagents in `<uuid>/subagents/agent-<id>.jsonl`.
9129

92-
- `model: "<synthetic>"` + `isApiErrorMessage: true` — rate-limit/error placeholders with all-zero tokens. Filtered out to avoid inflating request counts.
93-
- `isSidechain: true` — present on subagent entries. Informational only; we process all assistant entries regardless.
30+
- Only `type: "assistant"` entries carry `message.model` and `message.usage` — skip all others
31+
- `output_tokens` already includes thinking tokens — no separate counter
32+
- `cache_creation` sub-object breaks down 5m/1h tiers; `cache_creation_input_tokens` is the flat total (fallback for older logs)
33+
- One API call produces multiple entries sharing the same `requestId` — dedup by keeping the last (highest `output_tokens`)
34+
- `model: "<synthetic>"` + `isApiErrorMessage: true` are rate-limit placeholders — filter out to avoid inflating counts
9435

9536
## Conventions
9637

97-
- **Flat package structure** — all code in `package main`, one concern per file
98-
- **Dedup by requestId** — streaming duplicates collapsed by keeping the last entry per `requestId` in a map
99-
- **Externalized pricing** — all model pricing (input, output, cache read/write tiers, long context), global settings (long context threshold, web search cost), family prefixes, display names, and default model live in `pricing.json`. Embedded via `//go:embed`, with a remote-cached copy fetched from the repo every 24h. `initPricing()` prefers cached over embedded. Adding a new model or adjusting pricing requires only editing `pricing.json` — no code changes or binary release needed. Cache fields in JSON are optional — `fillCacheDefaults()` derives them from input price using standard multipliers (0.1x read, 1.25x write-5m, 2x write-1h) when absent
100-
- **Pricing resolution** — exact model ID → longest family prefix match → `defaultPricing`
101-
- **Cache write tiers are trusted from JSONL** — Claude Code now correctly reports `ephemeral_5m_input_tokens` and `ephemeral_1h_input_tokens` per model (e.g. Haiku → 5m, Opus/Sonnet → 1h). Fallback for old logs without `cache_creation` sub-object defaults to 1h. `CacheWrite5m`/`CacheWrite1h` remain separate fields (different pricing multipliers)
102-
- **Shared file parsing**`parseFile()` in parser.go is used by both `parseLogs` (directory walk) and `parseSession` (statusline single-session)
103-
- **Local timezone everywhere** — local midnight for cutoffs, `parsed.Local()` for date bucketing. Never use `UTC()` for user-facing date logic
104-
- **MCP detection is best-effort** — all MCP detection functions return nil/empty on error; statusline never fails due to missing config
105-
- **MCP sources** — six detection paths: `mcpServers` in settings.json, marketplace `enabledPlugins` with `.mcp.json` walk, project-level `.mcp.json` via `cwd` from transcript, `settings.local.json` in project `.claude/`, top-level `mcpServers` in `~/.claude.json`, and per-project `mcpServers` in `~/.claude.json`
106-
- **Config file**`~/.goccc.json` stores currency code, cached rate, timestamp, and cost thresholds (`warn_threshold`/`alert_threshold`). `initConfig()` loads everything once: thresholds first (with swap-correction if misordered), then currency. Exchange rates auto-fetched and cached for 24h. `-currency-symbol` and `-currency-rate` flags override config (both required together). JSON output cost fields always in USD
107-
- **Session end hook**`-session-end` reads `SessionEnd` JSON from stdin, parses the session transcript, and outputs a one-line cost summary. Uses `os.Exit(2)` + ANSI escape to overwrite Claude Code's "hook failed" prefix. Silently exits on any error — never breaks session teardown
108-
- **Customizable statusline**`statusline` key in `~/.goccc.json` configures segment order, visibility, separator, and per-segment emoji/label overrides. No config = current defaults. Segments with no data auto-hide (e.g. `5h`/`7d` on API billing, `mcp` when none detected). `"|"` in the segments array forces a line break. Config structs and segment registry live in `statusline_config.go`
109-
- **Rate limit windows**`formatRateLimitUsage` handles both 5h and 7d windows via `rateLimitWindow` struct. Uses pointer fields — nil when absent (API billing users). Emoji switches to 🪫 at ≤25% remaining. Color thresholds inverted vs cost (yellow ≤50%, red ≤25% remaining)
38+
- **Flat package** — all code in `package main`, one concern per file
39+
- **Externalized pricing** — all pricing lives in `pricing.json` (embedded via `//go:embed`, remote-cached 24h). Adding a model or adjusting pricing = edit `pricing.json` only, no code changes. Cache fields are optional — `fillCacheDefaults()` derives from input price
40+
- **Pricing resolution** — exact model ID → longest family prefix → `defaultPricing`
41+
- **Local timezone everywhere** — local midnight for cutoffs, `parsed.Local()` for date bucketing. Never `UTC()` for user-facing dates
42+
- **MCP detection is best-effort** — returns nil/empty on error; statusline never fails due to missing config
43+
- **Config**`~/.goccc.json` stores currency, thresholds, and statusline config. `initConfig()` loads once. JSON output costs always in USD
44+
- **Session end hook** — uses `os.Exit(2)` + ANSI escape to overwrite Claude Code's "hook failed" prefix. Silently exits on any error
45+
- **Statusline segments** — registry in `statusline_config.go`. Segments with no data auto-hide. `"|"` forces line break
11046

11147
## Don't
11248

113-
- Don't add or change pricing by editing Go code — update `pricing.json` instead (models, families, display_names, long_context_threshold, web_search_cost)
49+
- Don't change pricing in Go code — edit `pricing.json` (models, families, display_names, long_context_threshold, web_search_cost)
11450
- Don't use `log.Fatal` or `panic` — use `fmt.Fprintf(os.Stderr, ...)` + `os.Exit(1)`
11551
- Don't use UTC for day boundaries — use `time.Date(...)` with `now.Location()` for local midnight
11652
- Don't add JSON tags to `Bucket` — it's never directly marshalled; `printJSON` defines its own output structs

README.md

Lines changed: 57 additions & 77 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,9 @@
44
[![Latest Release](https://img.shields.io/github/v/release/backstabslash/goccc?color=blue)](https://github.com/backstabslash/goccc/releases/latest)
55
[![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](LICENSE)
66

7-
A fast, zero-dependency CLI cost calculator and [statusline provider](#claude-code-statusline) for [Claude Code](https://code.claude.com/docs/en/overview) — single binary, no runtime needed.
7+
A fast, zero-dependency CLI cost calculator and [customizable statusline](#claude-code-statusline) for [Claude Code](https://code.claude.com/docs/en/overview) — single binary, no runtime needed.
88

9-
Parses JSONL conversation logs and subagent sessions from `~/.claude/projects/`, deduplicates streaming responses, and breaks down spending by model, day, project, and branch — with accurate cache-tier and web search pricing.
9+
Breaks down your Claude Code spending by model, day, project, and branch — with accurate cache-tier and web search pricing.
1010

1111
![demo](https://github.com/user-attachments/assets/a65fc389-951d-47bc-9a69-5f498f3c1d32)
1212

@@ -62,31 +62,9 @@ goccc -json | jq '.summary.total_cost' # Pipe to jq for custom analysis
6262
goccc -currency-symbol "" -currency-rate 0.92 # One-off currency override
6363
```
6464

65-
### Local Currency
66-
67-
To display costs in your local currency, create `~/.goccc.json`:
68-
69-
```json
70-
{
71-
"currency": "ZAR"
72-
}
73-
```
74-
75-
goccc will auto-fetch the exchange rate from USD and cache it for 24 hours. If the API is unreachable, the last cached rate is used. Set `currency` to any [ISO 4217](https://en.wikipedia.org/wiki/ISO_4217) code (e.g., `EUR`, `GBP`, `ZAR`, `JPY`).
76-
77-
For one-off overrides without a config file, use both flags together:
78-
79-
```bash
80-
goccc -currency-symbol "" -currency-rate 0.92
81-
```
82-
83-
JSON output always reports costs in USD for backward compatibility, with a `currency` metadata object when a non-USD currency is active.
84-
85-
> See also: [Configuration](#configuration) for threshold customization.
86-
8765
## Claude Code Statusline
8866

89-
goccc can serve as a [Claude Code statusline](https://code.claude.com/docs/en/statusline) provider — a live cost dashboard right in your terminal prompt.
67+
goccc can serve as a [Claude Code statusline](https://code.claude.com/docs/en/statusline) — a fully customizable, live cost dashboard right in your terminal prompt.
9068

9169
```text
9270
💸 $1.23 session · 💰 $5.67 today · 💭 45% ctx · 🔌 2 MCPs (confluence, jira) · 🔋 94% (1.5/5h) · 🤖 Opus 4.6
@@ -96,17 +74,15 @@ goccc can serve as a [Claude Code statusline](https://code.claude.com/docs/en/st
9674
- **💰 Today's total** — aggregated across all sessions today (shown only when higher than session cost)
9775
- **💭 Context %** — context window usage percentage
9876
- **🔌 MCPs** — active MCP servers (from settings, marketplace plugins, and project config; respects per-project disables)
99-
- **🔋 5h window** — remaining percentage of the 5-hour usage window with elapsed time (subscription users only; hidden for API billing). Emoji switches to 🪫 below 25%
77+
- **🔋 5h / 7d window** — remaining percentage of the usage window with elapsed time (subscription users only; hidden for API billing). Emoji switches to 🪫 below 25%
10078
- **🤖 Model** — current model
10179

102-
Cost and context values are color-coded yellow → red as they increase. The 5h window is color-coded in reverse — yellow below 50%, red below 25%.
80+
Values are color-coded: cost and context turn yellow → red as they increase; rate limit windows are inverted — yellow below 50%, red below 25% remaining.
10381

10482
### Setup
10583

10684
Add to `~/.claude/settings.json`:
10785

108-
**Using Homebrew** (recommended — fast, no runtime needed):
109-
11086
```json
11187
{
11288
"statusLine": {
@@ -116,16 +92,7 @@ Add to `~/.claude/settings.json`:
11692
}
11793
```
11894

119-
**Using Go** (requires Go installed; binary is cached after first download):
120-
121-
```json
122-
{
123-
"statusLine": {
124-
"type": "command",
125-
"command": "go run github.com/backstabslash/goccc@latest -statusline"
126-
}
127-
}
128-
```
95+
Works with any [install method](#installation). To run without installing: `go run github.com/backstabslash/goccc@latest -statusline`.
12996

13097
### Customization
13198

@@ -144,28 +111,41 @@ The statusline is fully customizable via `~/.goccc.json`. With no config, you ge
144111
}
145112
```
146113

147-
**`segments`** — ordered list of segments to display. Only listed segments are shown. Use `"|"` to force a line break (as shown above).
114+
**`segments`** — ordered list of segments to display. Only listed segments are shown. Use `"|"` to force a line break (multi-line layout). Segments with no data auto-hide.
148115

149116
Available segments:
150117

151-
| Segment | Default | Auto-hides when |
152-
| --------- | --------- | ------------------- |
153-
| `session_cost` | `💸 $X.XX session` | cost is $0 |
154-
| `today_cost` | `💰 $X.XX today` | cost is $0 |
155-
| `ctx` | `💭 XX% ctx` ||
156-
| `model` | `🤖 Model Name` ||
157-
| `mcp` | `🔌 N MCPs (...)` | no MCPs detected |
158-
| `5h` | `🔋 XX% (X/5h)` | absent (API billing) |
159-
| `7d` | `🔋 XX% (X/7d)` | absent (API billing) |
160-
| `tokens` | `📊 XK in / XK out` | both zero |
161-
| `lines` | `📝 +N -N` | both zero |
162-
| `duration` | `⏱️ Xm` | zero |
163-
| `cwd` | `📁 dirname` | empty |
164-
| `version` | `🏷️ X.Y.Z` | empty |
118+
| Segment | Default | Auto-hides when | Overrides |
119+
| --- | --- | --- | --- |
120+
| `session_cost` | `💸 $X.XX session` | cost is $0 | emoji, label |
121+
| `today_cost` | `💰 $X.XX today` | cost is $0 | emoji, label |
122+
| `ctx` | `💭 XX% ctx` || emoji, label |
123+
| `model` | `🤖 Model Name` || emoji |
124+
| `mcp` | `🔌 N MCPs (...)` | no MCPs detected | emoji, label |
125+
| `5h` | `🔋 XX% (X/5h)` | absent (API billing) | emoji |
126+
| `7d` | `🔋 XX% (X/7d)` | absent (API billing) | emoji |
127+
| `tokens` | `📊 XK in / XK out` | both zero | emoji |
128+
| `lines` | `📝 +N -N` | both zero | emoji |
129+
| `duration` | `⏱️ Xm` | zero | emoji |
130+
| `cwd` | `📁 dirname` | empty | emoji |
131+
| `version` | `🏷️ X.Y.Z` | empty | emoji |
165132

166133
**`separator`** — string between segments (default: `" · "`).
167134

168-
**`segment_options`** — per-segment overrides for `emoji` and `label`. Only specified fields are overridden.
135+
**`segment_options`** — per-segment overrides. `emoji` replaces the default icon (for `5h`/`7d`, replaces the dynamic 🔋/🪫). `label` replaces trailing text (only on segments marked above).
136+
137+
#### Multi-line example
138+
139+
Use `"|"` in the segments array to split across lines:
140+
141+
```json
142+
{ "statusline": { "segments": ["session_cost", "today_cost", "|", "ctx", "5h", "tokens", "model"] } }
143+
```
144+
145+
```text
146+
💸 $1.23 session · 💰 $5.67 today
147+
💭 45% ctx · 🔋 94% (1.5/5h) · 📊 12.5K in / 3.2K out · 🤖 Opus 4.6
148+
```
169149

170150
## Session Exit Hook
171151

@@ -198,40 +178,40 @@ The hook runs within Claude Code's 1.5-second timeout. If anything fails, it exi
198178

199179
## Configuration
200180

201-
goccc reads its config from `~/.goccc.json`.
181+
All configuration lives in `~/.goccc.json`. Every field is optional.
202182

203-
### Cost Thresholds
183+
| Key | Description |
184+
| --- | --- |
185+
| `currency` | [ISO 4217](https://en.wikipedia.org/wiki/ISO_4217) currency code (e.g. `EUR`, `GBP`, `JPY`). Rate auto-fetched and cached 24h |
186+
| `warn_threshold` | Yellow color-coding threshold (default: `$25`, auto-scales with currency; custom values used as-is) |
187+
| `alert_threshold` | Red color-coding threshold (default: `$50`, auto-scales with currency; custom values used as-is) |
188+
| `statusline` | [Statusline customization](#customization) — segments, separator, per-segment overrides |
204189

205-
Cost values are color-coded yellow (warning) and red (alert) when they exceed thresholds. The defaults are $25 and $50 per day. To customize:
190+
### Local Currency
206191

207-
```json
208-
{
209-
"warn_threshold": 30,
210-
"alert_threshold": 75
211-
}
212-
```
192+
Set `"currency": "EUR"` (or any ISO 4217 code) in `~/.goccc.json`. goccc auto-fetches the exchange rate from USD and caches it for 24 hours. If the API is unreachable, the last cached rate is used. For one-off overrides without a config file, use `-currency-symbol "€" -currency-rate 0.92` together.
213193

214-
Thresholds are in USD (before currency conversion). They apply to the terminal output, statusline, and session exit hook.
194+
JSON output always reports costs in USD, with a `currency` metadata object when a non-USD currency is active.
215195

216196
## Flags
217197

218198
| Flag | Short | Default | Description |
219199
| --- | --- | --- | --- |
220200
| `-days` | `-d` | `0` | Only show the last N calendar days (0 = all time) |
221-
| `-project` | `-p` | | Filter by project name (substring, case-insensitive) |
222-
| `-daily` | | `false` | Show daily breakdown |
201+
| `-project` | `-p` | | Filter by project name (substring, case-insensitive) |
202+
| `-daily` | | `false` | Show daily breakdown |
223203
| `-monthly` | `-m` | `false` | Show monthly breakdown (mutually exclusive with `-daily`) |
224-
| `-projects` | | `false` | Show per-project breakdown |
225-
| `-all` | | `false` | Show all breakdowns (daily + projects) |
204+
| `-projects` | | `false` | Show per-project breakdown |
205+
| `-all` | | `false` | Show all breakdowns (daily + projects) |
226206
| `-top` | `-n` | `0` | Max entries in breakdowns (0 = all) |
227-
| `-json` | | `false` | Output as JSON |
228-
| `-no-color` | | `false` | Disable colored output (also respects `NO_COLOR` env) |
229-
| `-base-dir` | | `~/.claude` | Base directory for Claude Code data |
230-
| `-session-end` | | `false` | Session exit hook mode (reads SessionEnd JSON from stdin) |
231-
| `-statusline` | | `false` | Statusline mode for Claude Code (reads session JSON from stdin) |
232-
| `-currency-symbol` | | | Override currency symbol (requires `-currency-rate`) |
233-
| `-currency-rate` | | `0` | Override exchange rate from USD (requires `-currency-symbol`) |
234-
| `-version` | `-V` | | Print version and exit |
207+
| `-json` | | `false` | Output as JSON |
208+
| `-no-color` | | `false` | Disable colored output (also respects `NO_COLOR` env) |
209+
| `-base-dir` | | `~/.claude` | Base directory for Claude Code data |
210+
| `-session-end` | | `false` | Session exit hook mode (reads SessionEnd JSON from stdin) |
211+
| `-statusline` | | `false` | Statusline mode for Claude Code (reads session JSON from stdin) |
212+
| `-currency-symbol` | | | Override currency symbol (requires `-currency-rate`) |
213+
| `-currency-rate` | | `0` | Override exchange rate from USD (requires `-currency-symbol`) |
214+
| `-version` | `-V` | | Print version and exit |
235215

236216
## Preserving Log History
237217

0 commit comments

Comments
 (0)