claude-token-saver 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,346 @@
1
+ # claude-token-saver
2
+
3
+ > **Renamed from `claude-cache-monitor` in v2.0.** The old name still works β€” `npm i claude-cache-monitor` now redirects here, and the `claude-cache-monitor` binary remains available as an alias. See [migration notes](#migration-from-claude-cache-monitor).
4
+
5
+ > πŸ“Ί **HNPulse Shorts** β€” 이 도ꡬ가 λ§Œλ“€μ–΄μ§„ λ°°κ²½ (μΊμ‹œ TTL 1hβ†’5m λ³€κ²½ 이슈):
6
+ > **[β–Ά Watch the Short](https://www.youtube.com/shorts/oSx2sg935nI)** Β· [All HNPulse Shorts](https://www.youtube.com/@HNPulseKR/shorts)
7
+ >
8
+ > [![HNPulse Short](https://img.youtube.com/vi/oSx2sg935nI/hqdefault.jpg)](https://www.youtube.com/shorts/oSx2sg935nI)
9
+
10
+ **Save tokens on Claude Code.** Catch the session that suddenly burned 10Γ— your usual input, figure out *why*, and get a one-line remediation you can paste.
11
+
12
+ v1.5 adds three things on top of the original cache monitor:
13
+ - **Spike diagnosis** β€” detect recent sessions whose input tokens exploded vs. your own baseline, and name the cause (1M context, 5m TTL churn, cache rebuild, chatty output).
14
+ - **1M-context detection** β€” Opus 4.7+ auto-enables 1M context on Max plans, silently. This tool surfaces it on the statusline as `Ctx 1M` (red) vs. `Ctx 200k` (green), with the OS-specific command to turn it off.
15
+ - **Actionable advice** β€” OS-aware remediation (`~/.zshrc` vs. `setx`) and the warning for the known `/model` toggle bug ([anthropics/claude-code#31640](https://github.com/anthropics/claude-code/issues/31640)).
16
+
17
+ The original functionality still works: cache hit rate, TTL breakdown, cost impact vs. no-cache, TTL countdown timer, and Claude Code statusline integration.
18
+
19
+ ---
20
+
21
+ **Claude Code 토큰 μ•„κ»΄μ“°κΈ° 도ꡬ.** ν‰μ†Œλ³΄λ‹€ 10λ°° 토큰을 νƒœμš΄ μ„Έμ…˜μ„ μ°Ύκ³ , *μ™œ νŠ€μ—ˆλŠ”μ§€* μ§„λ‹¨ν•˜κ³ , 뢙여넣을 수 μžˆλŠ” ν•œ 쀄 ν•΄κ²°μ±…κΉŒμ§€ λ‚΄μ€λ‹ˆλ‹€.
22
+
23
+ v1.5 μ‹ κ·œ:
24
+ - **토큰 급증 진단** β€” 졜근 μ„Έμ…˜ 쀑 λ‚΄ ν‰μ†Œ 기쀀보닀 μž…λ ₯ 토큰이 ν­μ¦ν•œ μ„Έμ…˜μ„ μ°Ύμ•„ μ›μΈκΉŒμ§€ λΆ„λ₯˜ (1M μ»¨ν…μŠ€νŠΈ / 5λΆ„ TTL 반볡 μ“°κΈ° / μΊμ‹œ μž¬μž‘μ„± / 좜λ ₯ κ³Όλ‹€).
25
+ - **1M μ»¨ν…μŠ€νŠΈ 감지** β€” Opus 4.7λΆ€ν„° Max ν”Œλžœμ€ 1M μ»¨ν…μŠ€νŠΈκ°€ μžλ™ ON. Statusline에 `Ctx 1M`(λΉ¨κ°•) / `Ctx 200k`(초둝)으둜 ν‘œμ‹œν•˜κ³ , OS별 OFF λͺ…λ ΉκΉŒμ§€ μ•ˆλ‚΄.
26
+ - **μ‹€ν–‰ κ°€λŠ₯ν•œ ꢌμž₯ μ•‘μ…˜** β€” macOS/Linux/WSLλŠ” `~/.zshrc`, WindowsλŠ” `setx` λͺ…λ Ή. [`/model`둜 200k 선택해도 1M에 λ¨Έλ¬΄λŠ” μ•Œλ €μ§„ 버그](https://github.com/anthropics/claude-code/issues/31640)에 λŒ€ν•œ κ²½κ³  포함.
27
+
28
+ κΈ°μ‘΄ κΈ°λŠ₯(μΊμ‹œ 히트율·TTL λΆ„ν¬Β·λΉ„μš© 절감·TTL μΉ΄μš΄νŠΈλ‹€μš΄Β·statusline)은 κ·ΈλŒ€λ‘œ μœ μ§€λ©λ‹ˆλ‹€.
29
+
30
+ ## Quick Start
31
+
32
+ ```bash
33
+ # Run instantly (no install required)
34
+ npx claude-token-saver
35
+
36
+ # Last 7 days only
37
+ npx claude-token-saver --days 7
38
+
39
+ # JSON output (for pipelines)
40
+ npx claude-token-saver --format json
41
+
42
+ # CSV output (for spreadsheets)
43
+ npx claude-token-saver --format csv
44
+ ```
45
+
46
+ ## Spike Diagnosis (new in v1.5.0)
47
+
48
+ When you run `npx claude-token-saver`, sessions from the last 24 hours whose total input tokens are **β‰₯ 3Γ— your p95 baseline** (or whose single-request context exceeds 250k, indicating 1M context) appear at the top of the report with root causes and remediation commands. Example output:
49
+
50
+ ```
51
+ ⚠ 토큰 급증 감지
52
+ ──────────────────────────────────────────────────
53
+ μ»¨ν…μŠ€νŠΈ λͺ¨λ“œ μΆ”μ •: 1M (졜근 단일 μš”μ²­ μ΅œλŒ€ 480k 토큰)
54
+
55
+ β€’ a1b2c3d4 [myproject] 총 μž…λ ₯ 320.45M (5.2Γ— p95, μš”μ²­ 142회)
56
+ 단일 μš”μ²­ μ΅œλŒ€ μ»¨ν…μŠ€νŠΈ: 480k 토큰
57
+ Β· μš”μ²­λ‹Ή μž…λ ₯ 토큰이 ν‰μ†Œλ³΄λ‹€ 맀우 ν½λ‹ˆλ‹€ (1M μ»¨ν…μŠ€νŠΈ μ˜μ‹¬)
58
+
59
+ ꢌμž₯ μ•‘μ…˜
60
+ ──────────────────────────────────────────────────
61
+ β–Έ μš”μ²­λ‹Ή μž…λ ₯ 토큰이 ν‰μ†Œλ³΄λ‹€ 맀우 ν½λ‹ˆλ‹€ (1M μ»¨ν…μŠ€νŠΈ μ˜μ‹¬)
62
+ Opus 4.7λΆ€ν„° 1M μ»¨ν…μŠ€νŠΈκ°€ ν‘œμ€€ κ°€κ²©μœΌλ‘œ ν’€λ¦¬λ©΄μ„œ Max ν”Œλžœμ€ μžλ™μœΌλ‘œ 1M둜 μŠΉκ²©λ©λ‹ˆλ‹€ ...
63
+ - 1M μ»¨ν…μŠ€νŠΈ OFF (ν™˜κ²½λ³€μˆ˜)
64
+ echo 'export CLAUDE_CODE_DISABLE_1M_CONTEXT=1' >> ~/.zshrc && source ~/.zshrc
65
+ - μ„Έμ…˜ λ‚΄ ν† κΈ€
66
+ 단좕킀 βŒ₯ P (mac) / Alt + P (linux) 둜 μ¦‰μ‹œ On/Off
67
+ - ⚠ μ•Œλ €μ§„ 버그 #31640
68
+ /model 둜 200k 선택해도 μ»¨ν…μŠ€νŠΈκ°€ 1M에 λ¨Έλ¬΄λŠ” μΌ€μ΄μŠ€κ°€ μžˆμŠ΅λ‹ˆλ‹€.
69
+ ν™•μ‹€νžˆ 끄렀면 μœ„ ν™˜κ²½λ³€μˆ˜λ₯Ό μ„€μ •ν•œ λ’€ Claude Codeλ₯Ό μž¬μ‹œμž‘ν•˜μ„Έμš”.
70
+ ```
71
+
72
+ Issue codes detected:
73
+
74
+ | Code | Meaning |
75
+ |---|---|
76
+ | `LARGE_INPUT_PER_REQUEST` | avg input/request is 8Γ—+ your baseline, or a single request > 250k tokens β†’ 1M context likely |
77
+ | `LOW_HIT_RATE` | cache hit rate < 50% and materially below your baseline |
78
+ | `BUCKET_5M_DOMINANT` | > 70% of cache writes land in the 5m bucket (Pro plan, or Max users getting downgraded) |
79
+ | `HIGH_OUTPUT_RATIO` | output/input > 0.15 (output is 5Γ— input price β€” matters a lot) |
80
+ | `HIGH_REQUEST_COUNT` | session made 3Γ—+ your median request count (tool-loop suspect) |
81
+ | `FREQUENT_CACHE_REBUILD` | `cache_creation` > `cache_read` (cache being made, not reused) |
82
+
83
+ Remediation commands are chosen from `process.platform` β€” macOS/Linux/WSL get `~/.zshrc` snippets, Windows gets `setx` and the PowerShell equivalent.
84
+
85
+ ## Statusline Mode (new in v1.2.0)
86
+
87
+ Always-on one-line display in Claude Code's native statusline β€” no need to run commands manually.
88
+
89
+ Claude Code λ‚΄μž₯ statusline에 ν•œ μ€„λ‘œ μƒμ‹œ ν‘œμ‹œ. μ»€λ§¨λ“œ μˆ˜λ™ μ‹€ν–‰ λΆˆν•„μš”.
90
+
91
+ ```bash
92
+ # Preview (prints one line β€” text mode, default)
93
+ npx claude-token-saver --statusline
94
+ # β†’ Cache hit 97.5% Β· Expires 1h 42:15 Β· Cost saved $4.8K Β· Ctx 200k Β· 7d
95
+
96
+ # When 1M context is silently on and a session is spiking:
97
+ # β†’ Cache hit 88.0% Β· Expires 1h 42:15 Β· Cost saved $4.8K Β· Ctx 1M Β· ⚠ 1Mμ»¨ν…μŠ€νŠΈ Β· 7d
98
+
99
+ # Icon mode (🧠 / ⏳ / πŸ’° / πŸ“¦)
100
+ npx claude-token-saver --statusline --icon
101
+ # β†’ 🧠 97.5% Β· ⏳ 1h 42:15 Β· πŸ’° $4.8K Β· πŸ“¦ 200k Β· 7d
102
+
103
+ # Verbose (longer labels; combines with --icon too)
104
+ npx claude-token-saver --statusline --verbose
105
+ # β†’ Cache hit 97.5% Β· 1h bucket Β· expires in 42:15 Β· Cost saved $4.8K Β· last 7d
106
+
107
+ npx claude-token-saver --statusline --icon --verbose
108
+ # β†’ 🧠 Cache hit 97.5% Β· ⏳ Expires 1h 42:15 Β· πŸ’° Cost saved $4.8K Β· last 7d
109
+
110
+ # Hide the TTL countdown
111
+ npx claude-token-saver --statusline --no-timer
112
+
113
+ # No ANSI color (plain text)
114
+ npx claude-token-saver --statusline --no-color
115
+ ```
116
+
117
+ ### TTL countdown (v1.2.1+)
118
+
119
+ Your subscription plan fixes the TTL bucket (5m for Pro, 1h for Max) β€” the actionable number isn't the bucket, it's **how much time is left on your last API call's cache entry**. The `TTL 1h MM:SS` segment is a live stopwatch against the dominant bucket:
120
+
121
+ ꡬ독 ν”Œλžœμ΄ TTL κ°’(Pro = 5λΆ„, Max = 1μ‹œκ°„)을 κ³ μ •ν•˜λ―€λ‘œ 의미 μžˆλŠ” μˆ˜μΉ˜λŠ” "버킷"이 μ•„λ‹ˆλΌ "λ§ˆμ§€λ§‰ API 호좜의 μΊμ‹œκ°€ λ§Œλ£Œλ˜κΈ°κΉŒμ§€ λͺ‡ 초"μž…λ‹ˆλ‹€. `TTL 1h MM:SS` μ„Έκ·Έλ¨ΌνŠΈκ°€ κ·Έ μŠ€ν†±μ›ŒμΉ˜μž…λ‹ˆλ‹€ (μ•žμͺ½μ΄ 버킷, λ’€μͺ½μ΄ 남은 μ‹œκ°„):
122
+
123
+ - 🟒 &gt;30% remaining β€” plenty of time to send the next prompt within TTL
124
+ - 🟑 10–30% remaining β€” consider firing a cheap prompt soon to keep prefix cached
125
+ - πŸ”΄ &lt;10% remaining or `EXPIRED` β€” next prompt will pay cache-write cost again
126
+
127
+ This enables the "5-minute rule" in practice: a quick dummy question before the timer hits zero resets the TTL and preserves the prefix cache.
128
+
129
+ ### Enable in Claude Code
130
+
131
+ The Claude Code statusline is event-driven β€” it only re-renders on assistant messages / mode changes. To keep the countdown ticking while you're idle, set `refreshInterval: 1` alongside the `statusLine` command.
132
+
133
+ #### macOS / Linux / WSL
134
+
135
+ ```json
136
+ {
137
+ "statusLine": {
138
+ "type": "command",
139
+ "command": "claude-token-saver --statusline --icon",
140
+ "refreshInterval": 1
141
+ }
142
+ }
143
+ ```
144
+
145
+ Or combine with an existing statusline script β€” see [`examples/statusline-command.sh`](examples/statusline-command.sh) for a drop-in that prints `user@host:cwd | <cache monitor>`.
146
+
147
+ #### Windows (native PowerShell, not WSL)
148
+
149
+ Use the PowerShell example in [`examples/statusline-command.ps1`](examples/statusline-command.ps1):
150
+
151
+ ```json
152
+ {
153
+ "statusLine": {
154
+ "type": "command",
155
+ "command": "powershell.exe -NoProfile -ExecutionPolicy Bypass -File %USERPROFILE%\\.claude\\statusline-command.ps1",
156
+ "refreshInterval": 1
157
+ }
158
+ }
159
+ ```
160
+
161
+ Works best in **Windows Terminal** or **PowerShell 7+** (ANSI color + emoji). Classic `conhost cmd` may garble emoji β€” prefer `--no-icon`-style plain text or use Windows Terminal.
162
+
163
+ #### Windows (WSL)
164
+
165
+ Same as Linux β€” install the package in your WSL Node.js and point to the POSIX sh script.
166
+
167
+ #### Combine with rz1989s/claude-code-statusline
168
+
169
+ If you already use [rz1989s/claude-code-statusline](https://github.com/rz1989s/claude-code-statusline) for its rich layout (repo, cost, MCP, prayer times, themes), drop in [`examples/statusline-with-rz1989s.sh`](examples/statusline-with-rz1989s.sh) to append our cache segment at the end β€” no conflict, no feature overlap.
170
+
171
+ ```json
172
+ {
173
+ "statusLine": {
174
+ "type": "command",
175
+ "command": "bash ~/.claude/statusline-with-rz1989s.sh",
176
+ "refreshInterval": 1
177
+ }
178
+ }
179
+ ```
180
+
181
+ ---
182
+
183
+ Claude Code calls this every ~300ms on events, plus once per `refreshInterval` second while idle. Colors are emitted when the terminal supports them:
184
+
185
+ - **Hit rate** β€” 🟒 β‰₯85% Β· 🟑 70–85% Β· πŸ”΄ <70%
186
+ - **TTL bucket** β€” 🟒 1h (good) Β· 🟑 5m (warning)
187
+ - **Countdown** β€” 🟒 >30% remaining Β· 🟑 10–30% Β· πŸ”΄ <10% or EXPIRED
188
+ - **Context window** β€” 🟒 `Ctx 200k` (standard) Β· πŸ”΄ `Ctx 1M` (Opus 4.7+ Max auto-enabled β€” expensive if unintended)
189
+ - **Spike chip** β€” appears only when the current session is flagged (e.g. `⚠ 1Mμ»¨ν…μŠ€νŠΈ`, `⚠ 5m TTL`, `⚠ μΊμ‹œλ―ΈμŠ€`, `⚠ μž…λ ₯폭주`)
190
+
191
+ Statusline mode uses the last 7 days by default (override with `--days N`) and never emits multi-line errors, so your statusline stays clean even when there's no session data yet.
192
+
193
+ ## Hook Setup
194
+
195
+ Automatically logs cache stats on every tool call and alerts when hit rate drops below a threshold.
196
+
197
+ λ§€ 도ꡬ ν˜ΈμΆœλ§ˆλ‹€ μžλ™μœΌλ‘œ μΊμ‹œ 톡계λ₯Ό κΈ°λ‘ν•˜κ³ , 히트율이 μž„κ³„κ°’ μ΄ν•˜λ‘œ λ–¨μ–΄μ§€λ©΄ κ²½κ³ ν•©λ‹ˆλ‹€.
198
+
199
+ ```bash
200
+ # Install hook (default threshold 70%)
201
+ npx claude-token-saver --install-hook
202
+
203
+ # Custom threshold
204
+ npx claude-token-saver --install-hook --threshold 0.8
205
+
206
+ # Remove hook
207
+ npx claude-token-saver --uninstall-hook
208
+ ```
209
+
210
+ When the hook is installed:
211
+ - Session-level stats are automatically recorded to `~/.claude/cache-stats.jsonl`
212
+ - A warning is displayed in Claude Code when hit rate falls below the threshold
213
+
214
+ ## Output Example
215
+
216
+ ```
217
+ Claude Cache Monitor β€” Last 30 days
218
+ ══════════════════════════════════════════════════
219
+
220
+ Summary
221
+ Sessions: 380 | API calls: 10,813 | Model: claude-opus-new
222
+ Cache hit rate: 98.2% | Total input: 1957.94M tokens
223
+
224
+ TTL Breakdown
225
+ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
226
+ β”‚ β”‚ 5m Ephemeral β”‚ 1h Extended β”‚
227
+ β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
228
+ β”‚ Cache writes β”‚ 167.7K (0.5%) β”‚ 34.8M (99.5%) β”‚
229
+ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
230
+
231
+ Cost Impact (estimated)
232
+ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
233
+ β”‚ Actual cost β”‚ $793.93 β”‚
234
+ β”‚ Without cache β”‚ $5958.52 β”‚
235
+ β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
236
+ β”‚ Savings β”‚ $5164.6 (86.7%) β”‚
237
+ β”‚ Extra cost if 5m-only β”‚ +$239.99 β”‚
238
+ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
239
+
240
+ Daily Trend
241
+ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”
242
+ β”‚ Date β”‚ HitRate β”‚ Calls β”‚ Read β”‚ Write β”‚ 5m% β”‚
243
+ β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€
244
+ β”‚ 2026-04-10 β”‚ 98.5% β”‚ 341 β”‚ 88.98M β”‚ 1.35M β”‚ 0.0% β”‚
245
+ β”‚ 2026-04-11 β”‚ 97.1% β”‚ 118 β”‚ 9.51M β”‚ 0.27M β”‚ 0.0% β”‚
246
+ β”‚ 2026-04-12 β”‚ 91.7% β”‚ 77 β”‚ 2.69M β”‚ 0.22M β”‚ 0.0% β”‚
247
+ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”˜
248
+ ```
249
+
250
+ ## Options
251
+
252
+ | Flag | Description | Default |
253
+ |------|-------------|---------|
254
+ | `--days, -d` | Analysis period in days | 30 |
255
+ | `--format, -f` | Output format: `table`, `json`, `csv` | table |
256
+ | `--project, -p` | Filter by project directory | all |
257
+ | `--threshold` | Cache hit rate alert threshold (0.0-1.0) | 0.7 |
258
+ | `--install-hook` | Install Claude Code PostToolUse hook | - |
259
+ | `--uninstall-hook` | Remove hook | - |
260
+ | `--statusline` | Emit one-line output for Claude Code statusline API | - |
261
+ | `--icon` | (with `--statusline`) use 🧠 / ⏳ / πŸ’° icons instead of word labels | text |
262
+ | `--verbose` | (with `--statusline`) use longer labels | - |
263
+ | `--no-timer` | (with `--statusline`) hide the TTL countdown | show |
264
+ | `--no-color` | Strip ANSI escape codes | - |
265
+
266
+ Statusline segments always include the context-window chip (`Ctx 200k`/`Ctx 1M` or `πŸ“¦ 200k`/`πŸ“¦ 1M` in `--icon` mode). A spike chip is appended only when the most recent session is diagnosed as a spike.
267
+
268
+ ## How It Works
269
+
270
+ Claude Code logs usage data for every API call into session JSONL files:
271
+
272
+ ```
273
+ ~/.claude/projects/<project-dir>/<session-id>.jsonl
274
+ ```
275
+
276
+ Each API response contains these fields:
277
+ - `cache_read_input_tokens` β€” tokens read from cache (cheap, 0.1x)
278
+ - `cache_creation_input_tokens` β€” tokens written to cache (expensive, 1.25x~2x)
279
+ - `cache_creation.ephemeral_5m_input_tokens` β€” tokens created with 5-minute TTL
280
+ - `cache_creation.ephemeral_1h_input_tokens` β€” tokens created with 1-hour TTL
281
+
282
+ This tool deduplicates streaming chunks by `requestId` and aggregates by day/session.
283
+
284
+ ## Key Findings
285
+
286
+ Cache TTL is determined by your subscription plan, not by user choice:
287
+
288
+ | Plan | Cache TTL | Controlled by |
289
+ |------|-----------|---------------|
290
+ | **Max** ($100~200/mo) | **1h automatic** | `tengu_prompt_cache_1h_config` feature flag |
291
+ | **Pro** ($20/mo) | **5m fixed** | Not configurable |
292
+ | **API key** | **5m default** (1h via beta header) | `cache_control.ttl` parameter |
293
+
294
+ ## Pricing (updated 2026-04 for Opus 4.7)
295
+
296
+ Cost estimates use current Anthropic pricing, auto-detected from the model id in session logs:
297
+
298
+ | Tier (internal id) | Matching models | Input | 5m Cache Write | 1h Cache Write | Cache Read | Output |
299
+ |---|---|---|---|---|---|---|
300
+ | `claude-opus-new` | Opus **4.5 / 4.6 / 4.7** | $5 | $6.25 | $10 | $0.50 | $25 |
301
+ | `claude-opus-legacy` | Opus 4 / 4.1 / 3 | $15 | $18.75 | $30 | $1.50 | $75 |
302
+ | `claude-sonnet` | Sonnet 3.7 / 4 / 4.5 / 4.6 | $3 | $3.75 | $6 | $0.30 | $15 |
303
+ | `claude-haiku-4-5` | Haiku 4.5 | $1 | $1.25 | $2 | $0.10 | $5 |
304
+ | `claude-haiku-3-5` | Haiku 3.5 | $0.80 | $1 | $1.6 | $0.08 | $4 |
305
+ | `claude-haiku-3` | Haiku 3 | $0.25 | $0.30 | $0.50 | $0.03 | $1.25 |
306
+
307
+ 5m and 1h cache writes are now billed at separate rates (previously applied a single blended rate). Prior versions (≀ 1.0.x) used legacy Opus 4 pricing for all Opus models, over-estimating Opus 4.5+ costs by ~3x β€” **upgrade to 1.1.0 if you run Opus 4.5 or newer**.
308
+
309
+ Source: [Anthropic pricing documentation](https://docs.claude.com/en/docs/about-claude/pricing)
310
+
311
+ ## Platform Support
312
+
313
+ Works on **macOS**, **Windows**, and **Linux**. Requires Node.js >= 18.
314
+
315
+ Zero dependencies.
316
+
317
+ ## Background
318
+
319
+ - [GitHub Issue #46829](https://github.com/anthropics/claude-code/issues/46829): Cache TTL regression analysis
320
+ - [HN Discussion](https://news.ycombinator.com/item?id=47736476): Community reaction (168 points, 142 comments)
321
+ - [HNPulse KR](https://www.youtube.com/@HNPulseKR): Hacker News tech deep-dives in Korean ([Shorts](https://www.youtube.com/@HNPulseKR/shorts))
322
+
323
+ ## Migration from claude-cache-monitor
324
+
325
+ v2.0 renamed the package to reflect the expanded scope (spike diagnosis + 1M-context detection + remediation, not just cache monitoring). **No action required** in most cases:
326
+
327
+ - `npm i claude-cache-monitor` still installs β€” the old package is deprecated and redirects here.
328
+ - The binary `claude-cache-monitor` still works alongside the new `claude-token-saver` (both map to the same entry point).
329
+ - Your existing `statusLine.command` setting in `~/.claude/settings.json` keeps working.
330
+
331
+ If you want to update:
332
+
333
+ ```bash
334
+ npm uninstall -g claude-cache-monitor
335
+ npm i -g claude-token-saver
336
+
337
+ # then in ~/.claude/settings.json, change:
338
+ # "command": "claude-cache-monitor --statusline --icon"
339
+ # to:
340
+ # "command": "claude-token-saver --statusline --icon"
341
+ ```
342
+
343
+ ## License
344
+
345
+ MIT
346
+
package/bin/cli.js ADDED
@@ -0,0 +1,228 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * claude-token-saver CLI (formerly claude-cache-monitor)
5
+ *
6
+ * Usage:
7
+ * npx claude-token-saver # default report (last 30 days)
8
+ * npx claude-token-saver --days 7 # last 7 days
9
+ * npx claude-token-saver --format json # JSON output
10
+ * npx claude-token-saver --format csv # CSV output
11
+ * npx claude-token-saver --project myproj # filter by project
12
+ * npx claude-token-saver --install-hook # install PostToolUse hook
13
+ * npx claude-token-saver --uninstall-hook # remove hook
14
+ * npx claude-token-saver --hook-run # internal: called by hook
15
+ * npx claude-token-saver --statusline # one-line output for Claude Code statusline API
16
+ * npx claude-token-saver --statusline --verbose # longer labels
17
+ * npx claude-token-saver --statusline --no-color # strip ANSI colors
18
+ * npx claude-token-saver --statusline --icon # use 🧠 ⏳ πŸ’° icons
19
+ * npx claude-token-saver --statusline --no-timer # hide the TTL countdown
20
+ * npx claude-token-saver --statusline --exclude-session <path>
21
+ * # exclude a JSONL path from lastActivity
22
+ * # (or set CACHE_MONITOR_EXCLUDE_SESSION env var)
23
+ */
24
+
25
+ import { readFileSync } from 'node:fs';
26
+ import { fileURLToPath } from 'node:url';
27
+ import { dirname, join } from 'node:path';
28
+
29
+ import { parseAllSessions, getLastUserMessageTime } from '../src/parser.js';
30
+ import {
31
+ dailyTrend,
32
+ ttlBreakdown,
33
+ detectAnomalies,
34
+ summary,
35
+ detectSpikes,
36
+ detectContextWindow,
37
+ sessionMetrics,
38
+ diagnoseSession,
39
+ } from '../src/stats.js';
40
+ import { estimateCost } from '../src/cost.js';
41
+ import { chipForIssues } from '../src/advice.js';
42
+
43
+ const args = process.argv.slice(2);
44
+
45
+ const PKG_VERSION = (() => {
46
+ try {
47
+ const pkgPath = join(dirname(fileURLToPath(import.meta.url)), '..', 'package.json');
48
+ return JSON.parse(readFileSync(pkgPath, 'utf8')).version || '';
49
+ } catch {
50
+ return '';
51
+ }
52
+ })();
53
+
54
+ function getArg(name) {
55
+ const idx = args.indexOf(name);
56
+ if (idx === -1) return undefined;
57
+ return args[idx + 1];
58
+ }
59
+
60
+ function hasFlag(name) {
61
+ return args.includes(name);
62
+ }
63
+
64
+ async function main() {
65
+ // Hook management
66
+ if (hasFlag('--install-hook')) {
67
+ const { installHook } = await import('../src/hook-manager.js');
68
+ const threshold = parseFloat(getArg('--threshold') || '0.7');
69
+ await installHook({ threshold });
70
+ return;
71
+ }
72
+
73
+ if (hasFlag('--uninstall-hook')) {
74
+ const { uninstallHook } = await import('../src/hook-manager.js');
75
+ await uninstallHook();
76
+ return;
77
+ }
78
+
79
+ // Hook internal execution
80
+ if (hasFlag('--hook-run')) {
81
+ await import('../src/hook.cjs');
82
+ return;
83
+ }
84
+
85
+ // Statusline mode shortcut
86
+ const isStatusline = hasFlag('--statusline') || getArg('--format') === 'statusline';
87
+
88
+ // Report generation
89
+ // Statusline default = 7 days (fast, called every ~300ms). Others = 30 days.
90
+ const defaultDays = isStatusline ? 7 : 30;
91
+ const days = parseInt(getArg('--days') || getArg('-d') || String(defaultDays), 10);
92
+ const format = isStatusline ? 'statusline' : (getArg('--format') || getArg('-f') || 'table');
93
+ const projectFilter = getArg('--project') || getArg('-p');
94
+
95
+ if (format === 'table') {
96
+ process.stderr.write('Scanning session files...\n');
97
+ }
98
+
99
+ // Exclude the current Claude Code session when computing lastActivity β€”
100
+ // otherwise the agent's own tool calls reset the countdown every few seconds.
101
+ const excludeSessionPath =
102
+ getArg('--exclude-session') || process.env.CACHE_MONITOR_EXCLUDE_SESSION || undefined;
103
+
104
+ const sessions = await parseAllSessions({ days, projectFilter, excludeSessionPath });
105
+
106
+ if (sessions.length === 0) {
107
+ // Statusline must always emit a single line (no multi-line help spam every 300ms)
108
+ if (format === 'statusline') {
109
+ const colorOk = !hasFlag('--no-color') && !process.env.NO_COLOR;
110
+ const gray = colorOk ? '\x1b[90m' : '';
111
+ const reset = colorOk ? '\x1b[0m' : '';
112
+ console.log(`${gray}🧠 no session data · ${days}d${reset}`);
113
+ return;
114
+ }
115
+ console.log('No session data found for the given period.');
116
+ console.log('');
117
+ console.log('This tool analyzes Claude Code session logs (~/.claude/projects/).');
118
+ console.log('');
119
+ console.log('Possible causes:');
120
+ console.log(' - You haven\'t used Claude Code in the last ' + days + ' days');
121
+ console.log(' - You\'re using the Claude API directly (SDK/curl) without Claude Code');
122
+ console.log(' β†’ This tool requires Claude Code. API-only usage does not generate session logs.');
123
+ console.log(' - Try increasing the period: --days 90');
124
+ process.exit(1);
125
+ }
126
+
127
+ const trend = dailyTrend(sessions);
128
+ const ttl = ttlBreakdown(sessions);
129
+ const sum = summary(sessions);
130
+ const anomalies = detectAnomalies(trend);
131
+ const cost = estimateCost(sum, sessions[0]?.model);
132
+ const spikeReport = detectSpikes(sessions, { recentHours: 24, multiplier: 3 });
133
+ const contextWindow = detectContextWindow(sessions, { recentHours: 24 });
134
+
135
+ // For statusline: attach a single-word chip only when there's something
136
+ // actionable right now. 1M context is always shown; otherwise only fire
137
+ // if the most recent session actually appears in the spike list.
138
+ let spikeChip = null;
139
+ if (format === 'statusline') {
140
+ if (contextWindow.size === '1M') {
141
+ spikeChip = chipForIssues([], contextWindow);
142
+ } else {
143
+ const recentSession = sessions
144
+ .slice()
145
+ .sort((a, b) => (b.endTime?.getTime() || 0) - (a.endTime?.getTime() || 0))[0];
146
+ const recentIsSpiking = recentSession && spikeReport.spikes.some(
147
+ (sp) => sp.metrics.sessionId === recentSession.sessionId,
148
+ );
149
+ if (recentIsSpiking) {
150
+ const m = sessionMetrics(recentSession);
151
+ const issues = diagnoseSession(m, spikeReport.baseline);
152
+ spikeChip = chipForIssues(issues, contextWindow);
153
+ }
154
+ }
155
+ }
156
+
157
+ // Last API activity feeds the statusline TTL countdown.
158
+ // For every session that wasn't excluded, take the full endTime (any API call
159
+ // keeps the prefix cache warm β€” it doesn't matter whether it's user- or
160
+ // agent-driven because the cache is shared across sessions by prefix content).
161
+ const otherLastActivity = sessions
162
+ .map((s) => (s.endTime ? s.endTime.getTime() : 0))
163
+ .reduce((a, b) => Math.max(a, b), 0);
164
+ // For the excluded (current) session, only the user's prompts count β€” the
165
+ // agent's tool calls would otherwise reset the countdown every few seconds
166
+ // as long as Claude Code is streaming a response.
167
+ let currentSessionLastUser = 0;
168
+ if (excludeSessionPath) {
169
+ try {
170
+ const t = await getLastUserMessageTime(excludeSessionPath);
171
+ if (t) currentSessionLastUser = t.getTime();
172
+ } catch {
173
+ // ignore β€” keep 0 so it doesn't raise the max
174
+ }
175
+ }
176
+ const lastActivity = Math.max(otherLastActivity, currentSessionLastUser);
177
+
178
+ const data = {
179
+ summary: sum,
180
+ trend,
181
+ ttl,
182
+ anomalies,
183
+ cost,
184
+ options: { days, version: PKG_VERSION },
185
+ lastActivity,
186
+ spikeReport,
187
+ contextWindow,
188
+ spikeChip,
189
+ };
190
+
191
+ let output;
192
+ if (format === 'json') {
193
+ const { formatReport } = await import('../src/formatters/json.js');
194
+ output = formatReport(data);
195
+ } else if (format === 'csv') {
196
+ const { formatReport } = await import('../src/formatters/csv.js');
197
+ output = formatReport(data);
198
+ } else if (format === 'statusline') {
199
+ const { formatReport } = await import('../src/formatters/statusline.js');
200
+ const colorOk = !hasFlag('--no-color') && !process.env.NO_COLOR;
201
+ const mode = hasFlag('--icon') ? 'icon' : 'text';
202
+ output = formatReport(data, {
203
+ color: colorOk,
204
+ verbose: hasFlag('--verbose'),
205
+ timer: !hasFlag('--no-timer'),
206
+ mode,
207
+ });
208
+ } else {
209
+ const { formatReport } = await import('../src/formatters/table.js');
210
+ output = formatReport(data);
211
+ }
212
+
213
+ console.log(output);
214
+ }
215
+
216
+ main().catch((err) => {
217
+ // Statusline mode must never spam multi-line errors (called every ~300ms)
218
+ const isStatusline = process.argv.includes('--statusline') || process.argv.includes('statusline');
219
+ if (isStatusline) {
220
+ const colorOk = !process.argv.includes('--no-color') && !process.env.NO_COLOR;
221
+ const red = colorOk ? '\x1b[31m' : '';
222
+ const reset = colorOk ? '\x1b[0m' : '';
223
+ console.log(`${red}🧠 error${reset}`);
224
+ process.exit(0);
225
+ }
226
+ console.error('Error:', err.message);
227
+ process.exit(1);
228
+ });
@@ -0,0 +1,43 @@
1
+ # Claude Code statusline (Windows PowerShell version)
2
+ # Mirrors the POSIX sh script β€” prints "user@host:cwd" then appends
3
+ # claude-token-saver output as a second segment.
4
+ #
5
+ # Install:
6
+ # 1) npm install -g claude-token-saver
7
+ # 2) Save this file as: %USERPROFILE%\.claude\statusline-command.ps1
8
+ # 3) In %USERPROFILE%\.claude\settings.json add:
9
+ # {
10
+ # "statusLine": {
11
+ # "type": "command",
12
+ # "command": "powershell.exe -NoProfile -ExecutionPolicy Bypass -File %USERPROFILE%\\.claude\\statusline-command.ps1",
13
+ # "refreshInterval": 1
14
+ # }
15
+ # }
16
+ #
17
+ # Requires Windows Terminal or PowerShell 7+ for ANSI color + emoji rendering.
18
+ # (Classic conhost cmd renders colors but may garble emoji.)
19
+
20
+ $stdin = [Console]::In.ReadToEnd()
21
+
22
+ # Extract cwd from the JSON payload without requiring jq.
23
+ $cwdMatch = [regex]::Match($stdin, '"cwd"\s*:\s*"([^"]*)"')
24
+ $cwd = if ($cwdMatch.Success) { $cwdMatch.Groups[1].Value } else { (Get-Location).Path }
25
+
26
+ # 1) user@host:cwd (ANSI: green user@host, blue cwd)
27
+ $esc = [char]27
28
+ Write-Host -NoNewline "$esc[01;32m$env:USERNAME@$env:COMPUTERNAME$esc[00m`:$esc[01;34m$cwd$esc[00m"
29
+
30
+ # 2) cache monitor (appended). Separator " | ". Falls back silently.
31
+ Write-Host -NoNewline " $esc[90m|$esc[00m "
32
+
33
+ $cacheMonitor = Get-Command claude-token-saver -ErrorAction SilentlyContinue
34
+ if ($cacheMonitor) {
35
+ try {
36
+ & claude-token-saver --statusline --icon 2>$null
37
+ } catch { }
38
+ } else {
39
+ # fallback: npx (first run downloads the package; subsequent runs are warm)
40
+ try {
41
+ & npx --yes claude-token-saver@latest --statusline --icon 2>$null
42
+ } catch { }
43
+ }
@@ -0,0 +1,36 @@
1
+ #!/bin/sh
2
+ # Claude Code statusline (POSIX sh β€” works on macOS, Linux, and WSL)
3
+ # Prints "user@host:cwd" then appends claude-token-saver as a second segment.
4
+ #
5
+ # Install:
6
+ # 1) npm install -g claude-token-saver
7
+ # 2) Save this file as: ~/.claude/statusline-command.sh
8
+ # chmod +x ~/.claude/statusline-command.sh (optional)
9
+ # 3) In ~/.claude/settings.json:
10
+ # {
11
+ # "statusLine": {
12
+ # "type": "command",
13
+ # "command": "bash ~/.claude/statusline-command.sh",
14
+ # "refreshInterval": 1
15
+ # }
16
+ # }
17
+ #
18
+ # refreshInterval keeps the TTL countdown ticking while you're idle.
19
+ # Drop to 2 or 5 if you want lower local CPU.
20
+
21
+ input=$(cat)
22
+
23
+ # Extract cwd without jq dependency (jq may not be installed system-wide).
24
+ cwd=$(echo "$input" | sed -n 's/.*"cwd"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p')
25
+ cwd=${cwd:-$(pwd)}
26
+
27
+ # 1) user@host:cwd
28
+ printf '\033[01;32m%s@%s\033[00m:\033[01;34m%s\033[00m' "$(whoami)" "$(hostname -s)" "$cwd"
29
+
30
+ # 2) cache monitor (appended). Separator " | ". Falls back silently.
31
+ printf ' \033[90m|\033[00m '
32
+ if command -v claude-token-saver >/dev/null 2>&1; then
33
+ claude-token-saver --statusline --icon 2>/dev/null || true
34
+ else
35
+ npx --yes claude-token-saver@latest --statusline --icon 2>/dev/null || true
36
+ fi