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 +346 -0
- package/bin/cli.js +228 -0
- package/examples/statusline-command.ps1 +43 -0
- package/examples/statusline-command.sh +36 -0
- package/examples/statusline-with-rz1989s.sh +52 -0
- package/package.json +37 -0
- package/src/advice.js +138 -0
- package/src/cost.js +150 -0
- package/src/formatters/csv.js +8 -0
- package/src/formatters/json.js +3 -0
- package/src/formatters/statusline.js +164 -0
- package/src/formatters/table.js +235 -0
- package/src/hook-manager.js +89 -0
- package/src/hook.cjs +170 -0
- package/src/parser.js +197 -0
- package/src/stats.js +346 -0
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
|
+
> [](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
|
+
- π’ >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
|
+
- π΄ <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
|