claude-token-saver 2.0.2 → 2.1.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 +93 -10
- package/bin/cli.js +222 -9
- package/package.json +2 -3
- package/src/advice.js +48 -41
- package/src/config.js +151 -0
- package/src/demo.js +251 -0
- package/src/formatters/statusline.js +36 -19
- package/src/formatters/table.js +9 -9
- package/src/history.js +123 -0
- package/src/installer.js +136 -0
- package/src/paths.js +41 -0
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# claude-token-saver
|
|
2
2
|
|
|
3
|
-
> **Renamed from `claude-cache-monitor` in v2.0.** The old
|
|
3
|
+
> **Renamed from `claude-cache-monitor` in v2.0.** The old npm package is deprecated and points here. The `claude-cache-monitor` binary is gone — use `claude-token-saver` instead. See [migration notes](#migration-from-claude-cache-monitor).
|
|
4
4
|
|
|
5
5
|
> 📺 **HNPulse Shorts** — 이 도구가 만들어진 배경 (캐시 TTL 1h→5m 변경 이슈):
|
|
6
6
|
> **[▶ Watch the Short](https://www.youtube.com/shorts/oSx2sg935nI)** · [All HNPulse Shorts](https://www.youtube.com/@HNPulseKR/shorts)
|
|
@@ -9,6 +9,13 @@
|
|
|
9
9
|
|
|
10
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
11
|
|
|
12
|
+
**Why I built this.** I'm on the Max plan. On Opus 4.6 I never hit the *current-session* cap. After Opus 4.7 rolled out, I started hitting it on the same workflow — repeatedly. The official token statistics didn't match what I was actually feeling, and Claude Code's UI doesn't show prompt-cache health. This tool is what let me see *why*: low cache hit rate, 5m TTL writes that should have been 1h, 1M context auto-promoted in the background.
|
|
13
|
+
|
|
14
|
+
v2.1 (2026-04) adds the workflow that follows the diagnosis:
|
|
15
|
+
- **`claude-token-saver install`** — one command writes a Claude Code Skill (auto-activates when you mention "cache hit rate" / "1M context" / etc.) and a `/token-monitor` slash command.
|
|
16
|
+
- **`claude-token-saver history`** — every warning chip transition is auto-logged to a daily Markdown file, so you can answer "when did this start" without grepping logs.
|
|
17
|
+
- **Cross-platform paths** — Windows (`%APPDATA%`), macOS (`~/Library/Application Support`), Linux (`~/.config` / XDG) all handled.
|
|
18
|
+
|
|
12
19
|
v1.5 adds three things on top of the original `claude-cache-monitor`:
|
|
13
20
|
- **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
21
|
- **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.
|
|
@@ -47,6 +54,12 @@ npx claude-token-saver --format json
|
|
|
47
54
|
npx claude-token-saver --format csv
|
|
48
55
|
```
|
|
49
56
|
|
|
57
|
+
> **Upgrading from `claude-cache-monitor`?** Uninstall the old package first, then install the new one and update your settings command name:
|
|
58
|
+
> ```bash
|
|
59
|
+
> npm uninstall -g claude-cache-monitor && npm i -g claude-token-saver
|
|
60
|
+
> ```
|
|
61
|
+
> After upgrading, change any `claude-cache-monitor …` invocations (including `statusLine.command` in `~/.claude/settings.json`) to `claude-token-saver …`. See [Migration](#migration-from-claude-cache-monitor).
|
|
62
|
+
|
|
50
63
|
## Spike Diagnosis (new in v1.5.0)
|
|
51
64
|
|
|
52
65
|
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:
|
|
@@ -206,6 +219,48 @@ Claude Code calls this every ~300ms on events, plus once per `refreshInterval` s
|
|
|
206
219
|
|
|
207
220
|
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.
|
|
208
221
|
|
|
222
|
+
## Claude Code integration (`install`) — new in v2.1
|
|
223
|
+
|
|
224
|
+
One command wires up everything else this README mentions:
|
|
225
|
+
|
|
226
|
+
```bash
|
|
227
|
+
claude-token-saver install
|
|
228
|
+
```
|
|
229
|
+
|
|
230
|
+
This writes two files under your Claude user dir:
|
|
231
|
+
- `~/.claude/skills/claude-token-saver/SKILL.md` — auto-activates whenever you mention chip wording ("⚠ 1M ON", "cache miss", etc.) or ask about token usage. Claude Code will then know to read history, drill into the table report, and explain the warning.
|
|
232
|
+
- `~/.claude/commands/token-monitor.md` — adds a `/token-monitor` slash command that runs `claude-token-saver history` + a fresh report and summarizes both for you.
|
|
233
|
+
|
|
234
|
+
Re-run with `--force` to overwrite. Install only one piece with `install --skill` or `install --command`.
|
|
235
|
+
|
|
236
|
+
## Warning history (`history`) — new in v2.1
|
|
237
|
+
|
|
238
|
+
The statusline path auto-logs every chip transition (none → ⚠, ⚠ A → ⚠ B, ⚠ → resolved) to a daily Markdown file. Read it back with:
|
|
239
|
+
|
|
240
|
+
```bash
|
|
241
|
+
claude-token-saver history # last 7 days
|
|
242
|
+
claude-token-saver history --days 30 # wider window
|
|
243
|
+
claude-token-saver history --list # just list available dates
|
|
244
|
+
```
|
|
245
|
+
|
|
246
|
+
Sample output:
|
|
247
|
+
|
|
248
|
+
```
|
|
249
|
+
# Token Monitor — 2026-04-25
|
|
250
|
+
|
|
251
|
+
## Events
|
|
252
|
+
- 09:14:02 ⚠ 1M ON — Context auto-promoted to 1M (max single-request 280k tokens)
|
|
253
|
+
- 09:42:18 ⚠ 1M ON → ⚠ Cache miss — session abc12345: LOW_HIT_RATE
|
|
254
|
+
- 10:05:47 ✓ resolved (was ⚠ Cache miss)
|
|
255
|
+
```
|
|
256
|
+
|
|
257
|
+
Storage paths (cross-platform):
|
|
258
|
+
- **Windows**: `%APPDATA%\claude-token-saver\history\YYYY-MM-DD.md`
|
|
259
|
+
- **macOS**: `~/Library/Application Support/claude-token-saver/history/YYYY-MM-DD.md`
|
|
260
|
+
- **Linux**: `$XDG_CONFIG_HOME/claude-token-saver/history/YYYY-MM-DD.md` (or `~/.config/...`)
|
|
261
|
+
|
|
262
|
+
Each day's file is plain Markdown — open it in any editor. Transitions are deduped, so the 1Hz statusline refresh doesn't spam.
|
|
263
|
+
|
|
209
264
|
## Hook Setup
|
|
210
265
|
|
|
211
266
|
Automatically logs cache stats on every tool call and alerts when hit rate drops below a threshold.
|
|
@@ -342,24 +397,52 @@ Zero dependencies.
|
|
|
342
397
|
|
|
343
398
|
## Migration from claude-cache-monitor
|
|
344
399
|
|
|
345
|
-
v2.0 renamed the package to reflect the expanded scope (spike diagnosis + 1M-context detection + remediation, not just cache monitoring).
|
|
400
|
+
v2.0 renamed the package to reflect the expanded scope (spike diagnosis + 1M-context detection + remediation, not just cache monitoring).
|
|
401
|
+
|
|
402
|
+
### New users
|
|
403
|
+
|
|
404
|
+
```bash
|
|
405
|
+
npm i -g claude-token-saver
|
|
406
|
+
# or, no install:
|
|
407
|
+
npx claude-token-saver
|
|
408
|
+
```
|
|
409
|
+
|
|
410
|
+
Skip the rest of this section.
|
|
346
411
|
|
|
347
|
-
|
|
348
|
-
- The binary `claude-cache-monitor` still works alongside the new `claude-token-saver` (both map to the same entry point).
|
|
349
|
-
- Your existing `statusLine.command` setting in `~/.claude/settings.json` keeps working.
|
|
412
|
+
### Upgrading from `claude-cache-monitor` v1.x
|
|
350
413
|
|
|
351
|
-
|
|
414
|
+
Two steps:
|
|
352
415
|
|
|
353
416
|
```bash
|
|
417
|
+
# 1. Remove the old package (its claude-cache-monitor bin is now obsolete).
|
|
354
418
|
npm uninstall -g claude-cache-monitor
|
|
419
|
+
|
|
420
|
+
# 2. Install the new one.
|
|
355
421
|
npm i -g claude-token-saver
|
|
422
|
+
```
|
|
423
|
+
|
|
424
|
+
Then update any `claude-cache-monitor …` references. The main one is `statusLine.command` in `~/.claude/settings.json`:
|
|
425
|
+
|
|
426
|
+
```jsonc
|
|
427
|
+
// before
|
|
428
|
+
"command": "claude-cache-monitor --statusline --icon"
|
|
429
|
+
|
|
430
|
+
// after
|
|
431
|
+
"command": "claude-token-saver --statusline --icon"
|
|
432
|
+
```
|
|
356
433
|
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
434
|
+
### Why we dropped the `claude-cache-monitor` bin alias
|
|
435
|
+
|
|
436
|
+
Earlier v2.0 releases shipped a `claude-cache-monitor` bin alongside `claude-token-saver` so existing settings would keep working without edits. In practice this caused an `EEXIST: file already exists` error on `npm i -g claude-token-saver` when v1.x was still installed — and that collision forced the uninstall step anyway. Dropping the alias makes the upgrade path a clean two-liner and lets `npm i -g claude-token-saver` succeed directly if you've never installed the old one.
|
|
437
|
+
|
|
438
|
+
### Zero-install (npx)
|
|
439
|
+
|
|
440
|
+
```bash
|
|
441
|
+
npx claude-token-saver@latest --statusline --icon
|
|
361
442
|
```
|
|
362
443
|
|
|
444
|
+
No uninstall needed; npm just fetches the new name.
|
|
445
|
+
|
|
363
446
|
## License
|
|
364
447
|
|
|
365
448
|
MIT
|
package/bin/cli.js
CHANGED
|
@@ -62,6 +62,113 @@ function hasFlag(name) {
|
|
|
62
62
|
}
|
|
63
63
|
|
|
64
64
|
async function main() {
|
|
65
|
+
// Subcommand: history — print recent warning transitions captured by the
|
|
66
|
+
// statusline. One markdown file per day, persisted under the platform-
|
|
67
|
+
// specific user-data dir.
|
|
68
|
+
// claude-token-saver history # last 7 days
|
|
69
|
+
// claude-token-saver history --days 30 # custom window
|
|
70
|
+
// claude-token-saver history --list # just list available dates
|
|
71
|
+
if (args[0] === 'history') {
|
|
72
|
+
const { readRecent, listDates, historyDir } = await import('../src/history.js');
|
|
73
|
+
if (hasFlag('--list')) {
|
|
74
|
+
const dates = listDates();
|
|
75
|
+
if (dates.length === 0) {
|
|
76
|
+
console.log(`No history yet. Files will appear under: ${historyDir()}`);
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
console.log(`History (${historyDir()}):`);
|
|
80
|
+
for (const d of dates) console.log(` ${d}`);
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
const days = parseFloat(getArg('--days') || '7');
|
|
84
|
+
const recent = readRecent(days);
|
|
85
|
+
if (recent.length === 0) {
|
|
86
|
+
console.log(`No warning history in the last ${days} day${days === 1 ? '' : 's'}.`);
|
|
87
|
+
console.log(`(Files would be written to: ${historyDir()})`);
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
for (const { content } of recent) {
|
|
91
|
+
console.log(content.replace(/\n+$/, ''));
|
|
92
|
+
console.log('');
|
|
93
|
+
}
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// Subcommand: install — write the Claude Code Skill and slash command so
|
|
98
|
+
// /token-monitor and the auto-trigger skill become available without any
|
|
99
|
+
// manual file editing. Cross-platform (uses node:path + node:fs).
|
|
100
|
+
// claude-token-saver install # install both
|
|
101
|
+
// claude-token-saver install --skill # only the skill
|
|
102
|
+
// claude-token-saver install --command # only the slash command
|
|
103
|
+
// claude-token-saver install --force # overwrite existing files
|
|
104
|
+
if (args[0] === 'install') {
|
|
105
|
+
const { installSkill, installCommand, installAll } = await import('../src/installer.js');
|
|
106
|
+
const force = hasFlag('--force');
|
|
107
|
+
const onlySkill = hasFlag('--skill');
|
|
108
|
+
const onlyCommand = hasFlag('--command');
|
|
109
|
+
const print = (kind, r) => {
|
|
110
|
+
const verb = r.action === 'exists' ? 'already exists' : r.action;
|
|
111
|
+
console.log(` ${kind}: ${r.path} (${verb})`);
|
|
112
|
+
};
|
|
113
|
+
if (onlySkill && !onlyCommand) {
|
|
114
|
+
print('skill', installSkill({ force }));
|
|
115
|
+
} else if (onlyCommand && !onlySkill) {
|
|
116
|
+
print('command', installCommand({ force }));
|
|
117
|
+
} else {
|
|
118
|
+
const r = installAll({ force });
|
|
119
|
+
print('skill', r.skill);
|
|
120
|
+
print('command', r.command);
|
|
121
|
+
}
|
|
122
|
+
console.log('');
|
|
123
|
+
console.log('Open Claude Code in any directory and try:');
|
|
124
|
+
console.log(' /token-monitor');
|
|
125
|
+
console.log('Or just mention "cache hit rate" / "1M context" — the skill auto-activates.');
|
|
126
|
+
if (!force) {
|
|
127
|
+
console.log('');
|
|
128
|
+
console.log('Tip: re-run with --force to overwrite existing files.');
|
|
129
|
+
}
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// Subcommand: mode — persist statusline preferences so future runs pick
|
|
134
|
+
// them up without flags or wrapper edits.
|
|
135
|
+
// claude-token-saver mode # show current config
|
|
136
|
+
// claude-token-saver mode icon verbose # set icon + verbose
|
|
137
|
+
// claude-token-saver mode reset # clear back to defaults
|
|
138
|
+
if (args[0] === 'mode') {
|
|
139
|
+
const { applyMode, loadConfig, configPath, statuslineDefaults, VALID_KEYWORDS } =
|
|
140
|
+
await import('../src/config.js');
|
|
141
|
+
const words = args.slice(1);
|
|
142
|
+
if (words.length === 0) {
|
|
143
|
+
const eff = statuslineDefaults();
|
|
144
|
+
const raw = loadConfig().statusline || {};
|
|
145
|
+
console.log('Statusline mode (effective):');
|
|
146
|
+
console.log(` icon: ${eff.icon}`);
|
|
147
|
+
console.log(` verbose: ${eff.verbose}`);
|
|
148
|
+
console.log(` timer: ${eff.timer}`);
|
|
149
|
+
console.log(` color: ${eff.color}`);
|
|
150
|
+
console.log(` window: ${eff.windowLabel} (${eff.windowHours}h)`);
|
|
151
|
+
console.log('');
|
|
152
|
+
console.log(`Stored config (${configPath()}):`);
|
|
153
|
+
console.log(` ${Object.keys(raw).length === 0 ? '(none — using defaults)' : JSON.stringify(raw)}`);
|
|
154
|
+
console.log('');
|
|
155
|
+
console.log('Change with: claude-token-saver mode <keywords...>');
|
|
156
|
+
console.log(`Keywords: ${VALID_KEYWORDS.join(', ')}`);
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
const { applied, unknown } = applyMode(words);
|
|
160
|
+
if (unknown.length) {
|
|
161
|
+
console.error(`Unknown keyword${unknown.length > 1 ? 's' : ''}: ${unknown.join(', ')}`);
|
|
162
|
+
console.error(`Valid: ${VALID_KEYWORDS.join(', ')}`);
|
|
163
|
+
process.exit(1);
|
|
164
|
+
}
|
|
165
|
+
const eff = statuslineDefaults();
|
|
166
|
+
console.log(`Updated: ${applied.join(', ')}`);
|
|
167
|
+
console.log(`Now: icon=${eff.icon} verbose=${eff.verbose} timer=${eff.timer} color=${eff.color} window=${eff.windowLabel}`);
|
|
168
|
+
console.log('Statusline picks up the change on the next refresh (~1s).');
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
171
|
+
|
|
65
172
|
// Hook management
|
|
66
173
|
if (hasFlag('--install-hook')) {
|
|
67
174
|
const { installHook } = await import('../src/hook-manager.js');
|
|
@@ -85,10 +192,89 @@ async function main() {
|
|
|
85
192
|
// Statusline mode shortcut
|
|
86
193
|
const isStatusline = hasFlag('--statusline') || getArg('--format') === 'statusline';
|
|
87
194
|
|
|
195
|
+
// Demo mode — render synthetic warning-case data through the real
|
|
196
|
+
// formatter for screencasts/marketing GIFs. `--demo cycle` rotates through
|
|
197
|
+
// every scenario based on wall clock so a screen recorder picks them up.
|
|
198
|
+
const demoArg = getArg('--demo');
|
|
199
|
+
|
|
200
|
+
// `claude-token-saver --demo table` (no --statusline) — full table view
|
|
201
|
+
// with all six issue drill-downs at once, for marketing screencasts.
|
|
202
|
+
if (!isStatusline && demoArg === 'table') {
|
|
203
|
+
const { buildTableDemoData } = await import('../src/demo.js');
|
|
204
|
+
const { formatReport } = await import('../src/formatters/table.js');
|
|
205
|
+
const data = buildTableDemoData({ version: PKG_VERSION });
|
|
206
|
+
console.log(formatReport(data));
|
|
207
|
+
return;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
if (isStatusline && demoArg) {
|
|
211
|
+
const { buildScenarioData, listScenarios } = await import('../src/demo.js');
|
|
212
|
+
const { statuslineDefaults } = await import('../src/config.js');
|
|
213
|
+
const cfg = statuslineDefaults();
|
|
214
|
+
const cycleSeconds = parseFloat(getArg('--demo-cycle-sec') || '3');
|
|
215
|
+
const data = buildScenarioData(demoArg, {
|
|
216
|
+
cycleSeconds,
|
|
217
|
+
windowHours: cfg.windowHours,
|
|
218
|
+
windowLabel: cfg.windowLabel,
|
|
219
|
+
days: cfg.windowHours / 24,
|
|
220
|
+
version: PKG_VERSION,
|
|
221
|
+
});
|
|
222
|
+
if (!data) {
|
|
223
|
+
const known = listScenarios().map((s) => s.name).concat(['cycle']).join(', ');
|
|
224
|
+
console.error(`Unknown demo scenario: ${demoArg}`);
|
|
225
|
+
console.error(`Valid: ${known}`);
|
|
226
|
+
process.exit(1);
|
|
227
|
+
}
|
|
228
|
+
const { formatReport } = await import('../src/formatters/statusline.js');
|
|
229
|
+
const isIcon = hasFlag('--icon')
|
|
230
|
+
? true
|
|
231
|
+
: (hasFlag('--no-icon') || hasFlag('--text') ? false : cfg.icon);
|
|
232
|
+
const isVerbose = hasFlag('--verbose')
|
|
233
|
+
? true
|
|
234
|
+
: (hasFlag('--no-verbose') || hasFlag('--compact') ? false : cfg.verbose);
|
|
235
|
+
const showTimer = hasFlag('--no-timer') ? false : cfg.timer;
|
|
236
|
+
const colorOk = !hasFlag('--no-color') && !process.env.NO_COLOR && cfg.color;
|
|
237
|
+
const out = formatReport(data, {
|
|
238
|
+
color: colorOk,
|
|
239
|
+
verbose: isVerbose,
|
|
240
|
+
timer: showTimer,
|
|
241
|
+
mode: isIcon ? 'icon' : 'text',
|
|
242
|
+
});
|
|
243
|
+
// For `cycle` mode, prefix with the scenario label so the screen recorder
|
|
244
|
+
// shows what the viewer is looking at (only when explicitly requested).
|
|
245
|
+
if (demoArg === 'cycle' && hasFlag('--demo-label')) {
|
|
246
|
+
const gray = colorOk ? '\x1b[90m' : '';
|
|
247
|
+
const reset = colorOk ? '\x1b[0m' : '';
|
|
248
|
+
console.log(`${gray}[${data._demoLabel}]${reset} ${out}`);
|
|
249
|
+
} else {
|
|
250
|
+
console.log(out);
|
|
251
|
+
}
|
|
252
|
+
return;
|
|
253
|
+
}
|
|
254
|
+
|
|
88
255
|
// Report generation
|
|
89
|
-
// Statusline
|
|
90
|
-
|
|
91
|
-
|
|
256
|
+
// Statusline window comes from persisted config — hours-precise so users
|
|
257
|
+
// can pick `1h` / `6h` etc, not just whole days. Other formats default to
|
|
258
|
+
// 30 days as before.
|
|
259
|
+
let windowHours = 30 * 24;
|
|
260
|
+
let windowLabel = '30d';
|
|
261
|
+
if (isStatusline) {
|
|
262
|
+
const { statuslineDefaults } = await import('../src/config.js');
|
|
263
|
+
const d = statuslineDefaults();
|
|
264
|
+
windowHours = d.windowHours;
|
|
265
|
+
windowLabel = d.windowLabel;
|
|
266
|
+
}
|
|
267
|
+
// CLI overrides: --hours wins over --days; both win over config.
|
|
268
|
+
const hoursArg = getArg('--hours');
|
|
269
|
+
const daysArg = getArg('--days') || getArg('-d');
|
|
270
|
+
if (hoursArg !== undefined) {
|
|
271
|
+
windowHours = parseFloat(hoursArg);
|
|
272
|
+
windowLabel = `${windowHours}h`;
|
|
273
|
+
} else if (daysArg !== undefined) {
|
|
274
|
+
windowHours = parseFloat(daysArg) * 24;
|
|
275
|
+
windowLabel = `${parseFloat(daysArg)}d`;
|
|
276
|
+
}
|
|
277
|
+
const days = windowHours / 24;
|
|
92
278
|
const format = isStatusline ? 'statusline' : (getArg('--format') || getArg('-f') || 'table');
|
|
93
279
|
const projectFilter = getArg('--project') || getArg('-p');
|
|
94
280
|
|
|
@@ -136,9 +322,11 @@ async function main() {
|
|
|
136
322
|
// actionable right now. 1M context is always shown; otherwise only fire
|
|
137
323
|
// if the most recent session actually appears in the spike list.
|
|
138
324
|
let spikeChip = null;
|
|
325
|
+
let chipDetail = null;
|
|
139
326
|
if (format === 'statusline') {
|
|
140
327
|
if (contextWindow.size === '1M') {
|
|
141
328
|
spikeChip = chipForIssues([], contextWindow);
|
|
329
|
+
chipDetail = `Context auto-promoted to 1M (max single-request ${Math.round(contextWindow.maxContext / 1000)}k tokens)`;
|
|
142
330
|
} else {
|
|
143
331
|
const recentSession = sessions
|
|
144
332
|
.slice()
|
|
@@ -150,8 +338,21 @@ async function main() {
|
|
|
150
338
|
const m = sessionMetrics(recentSession);
|
|
151
339
|
const issues = diagnoseSession(m, spikeReport.baseline);
|
|
152
340
|
spikeChip = chipForIssues(issues, contextWindow);
|
|
341
|
+
const titles = issues
|
|
342
|
+
.map((i) => i.code)
|
|
343
|
+
.slice(0, 2)
|
|
344
|
+
.join(', ');
|
|
345
|
+
chipDetail = `session ${recentSession.sessionId?.slice(0, 8) || ''}: ${titles}`;
|
|
153
346
|
}
|
|
154
347
|
}
|
|
348
|
+
// Persist transitions to ~/.config/claude-token-saver/history/YYYY-MM-DD.md
|
|
349
|
+
// so /token-monitor and `claude-token-saver history` can replay them.
|
|
350
|
+
try {
|
|
351
|
+
const { recordChip } = await import('../src/history.js');
|
|
352
|
+
recordChip(spikeChip, { detail: chipDetail });
|
|
353
|
+
} catch {
|
|
354
|
+
// history is non-critical — don't let it break the statusline render
|
|
355
|
+
}
|
|
155
356
|
}
|
|
156
357
|
|
|
157
358
|
// Last API activity feeds the statusline TTL countdown.
|
|
@@ -181,7 +382,7 @@ async function main() {
|
|
|
181
382
|
ttl,
|
|
182
383
|
anomalies,
|
|
183
384
|
cost,
|
|
184
|
-
options: { days, version: PKG_VERSION },
|
|
385
|
+
options: { days, windowHours, windowLabel, version: PKG_VERSION },
|
|
185
386
|
lastActivity,
|
|
186
387
|
spikeReport,
|
|
187
388
|
contextWindow,
|
|
@@ -197,13 +398,25 @@ async function main() {
|
|
|
197
398
|
output = formatReport(data);
|
|
198
399
|
} else if (format === 'statusline') {
|
|
199
400
|
const { formatReport } = await import('../src/formatters/statusline.js');
|
|
200
|
-
const
|
|
201
|
-
const
|
|
401
|
+
const { statuslineDefaults } = await import('../src/config.js');
|
|
402
|
+
const cfg = statuslineDefaults();
|
|
403
|
+
|
|
404
|
+
// CLI flags take precedence; otherwise fall back to persisted config.
|
|
405
|
+
const isIcon = hasFlag('--icon')
|
|
406
|
+
? true
|
|
407
|
+
: (hasFlag('--no-icon') || hasFlag('--text') ? false : cfg.icon);
|
|
408
|
+
const isVerbose = hasFlag('--verbose')
|
|
409
|
+
? true
|
|
410
|
+
: (hasFlag('--no-verbose') || hasFlag('--compact') ? false : cfg.verbose);
|
|
411
|
+
const showTimer = hasFlag('--no-timer') ? false : cfg.timer;
|
|
412
|
+
const colorOk =
|
|
413
|
+
!hasFlag('--no-color') && !process.env.NO_COLOR && cfg.color;
|
|
414
|
+
|
|
202
415
|
output = formatReport(data, {
|
|
203
416
|
color: colorOk,
|
|
204
|
-
verbose:
|
|
205
|
-
timer:
|
|
206
|
-
mode,
|
|
417
|
+
verbose: isVerbose,
|
|
418
|
+
timer: showTimer,
|
|
419
|
+
mode: isIcon ? 'icon' : 'text',
|
|
207
420
|
});
|
|
208
421
|
} else {
|
|
209
422
|
const { formatReport } = await import('../src/formatters/table.js');
|
package/package.json
CHANGED
|
@@ -1,11 +1,10 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "claude-token-saver",
|
|
3
|
-
"version": "2.0
|
|
3
|
+
"version": "2.1.0",
|
|
4
4
|
"description": "Save tokens on Claude Code — spike diagnosis, 1M-context detection, TTL countdown, statusline. (formerly claude-cache-monitor)",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
7
|
-
"claude-token-saver": "./bin/cli.js"
|
|
8
|
-
"claude-cache-monitor": "./bin/cli.js"
|
|
7
|
+
"claude-token-saver": "./bin/cli.js"
|
|
9
8
|
},
|
|
10
9
|
"files": [
|
|
11
10
|
"bin/",
|
package/src/advice.js
CHANGED
|
@@ -28,94 +28,99 @@ function toggleShortcut() {
|
|
|
28
28
|
|
|
29
29
|
export const ISSUE_MESSAGES = {
|
|
30
30
|
LARGE_INPUT_PER_REQUEST: {
|
|
31
|
-
title: '
|
|
31
|
+
title: 'Per-request input tokens are unusually large (1M context suspected)',
|
|
32
32
|
explain:
|
|
33
|
-
'Opus 4.7
|
|
34
|
-
'
|
|
33
|
+
'Since Opus 4.7, 1M context is priced at the standard rate, and Max plans auto-promote ' +
|
|
34
|
+
'sessions to 1M. Once context goes past 200k, long-context pricing kicks in and cache reuse drops.',
|
|
35
35
|
actions: () => [
|
|
36
36
|
{
|
|
37
|
-
label: '1M
|
|
37
|
+
label: 'Disable 1M context (env var)',
|
|
38
38
|
commands: disable1mEnvSnippet(),
|
|
39
39
|
},
|
|
40
40
|
{
|
|
41
|
-
label: '
|
|
42
|
-
commands: [
|
|
41
|
+
label: 'In-session toggle',
|
|
42
|
+
commands: [`Press ${toggleShortcut()} to toggle on/off instantly`],
|
|
43
43
|
},
|
|
44
44
|
{
|
|
45
|
-
label: '⚠
|
|
45
|
+
label: '⚠ Known bug #31640',
|
|
46
46
|
commands: [
|
|
47
|
-
'/model
|
|
48
|
-
'
|
|
47
|
+
'/model 200k selection sometimes does not stick — context stays at 1M.',
|
|
48
|
+
'To force off: set the env var above and restart Claude Code.',
|
|
49
49
|
],
|
|
50
50
|
},
|
|
51
51
|
],
|
|
52
52
|
},
|
|
53
53
|
LOW_HIT_RATE: {
|
|
54
|
-
title: '
|
|
54
|
+
title: 'Cache hit rate is low',
|
|
55
55
|
explain:
|
|
56
|
-
'
|
|
56
|
+
'A low hit rate means the same prompt prefix is being rewritten on every call, ' +
|
|
57
|
+
'inflating input cost.',
|
|
57
58
|
actions: () => [
|
|
58
59
|
{
|
|
59
|
-
label: '
|
|
60
|
-
commands: ['
|
|
60
|
+
label: 'Avoid opening fresh sessions too often',
|
|
61
|
+
commands: ['Continue the same task in the same session (context switching = cache miss)'],
|
|
61
62
|
},
|
|
62
63
|
{
|
|
63
|
-
label: '
|
|
64
|
-
commands: ['
|
|
64
|
+
label: 'Stabilize the prompt prefix',
|
|
65
|
+
commands: ['System prompts / tool definitions that change per request invalidate the cache every time'],
|
|
65
66
|
},
|
|
66
67
|
],
|
|
67
68
|
},
|
|
68
69
|
BUCKET_5M_DOMINANT: {
|
|
69
|
-
title: '5
|
|
70
|
+
title: 'Most cache writes are landing in the 5-minute TTL bucket',
|
|
70
71
|
explain:
|
|
71
|
-
'Pro
|
|
72
|
+
'Pro plan is locked to 5m TTL. Gaps longer than 5 minutes expire the cache and force ' +
|
|
73
|
+
'a costly rebuild.',
|
|
72
74
|
actions: () => [
|
|
73
75
|
{
|
|
74
|
-
label: '5
|
|
76
|
+
label: 'The 5-minute rule',
|
|
75
77
|
commands: [
|
|
76
|
-
'5
|
|
77
|
-
'
|
|
78
|
+
'Sending any prompt within 5 minutes keeps the prefix cache warm',
|
|
79
|
+
'Upgrade to Max for the 1h TTL bucket on long tasks',
|
|
78
80
|
],
|
|
79
81
|
},
|
|
80
82
|
],
|
|
81
83
|
},
|
|
82
84
|
HIGH_OUTPUT_RATIO: {
|
|
83
|
-
title: 'Output
|
|
85
|
+
title: 'Output share is abnormally high',
|
|
84
86
|
explain:
|
|
85
|
-
'Output
|
|
87
|
+
'Output tokens are 5x+ pricier than input. Check whether the agent is regenerating ' +
|
|
88
|
+
'long content unnecessarily.',
|
|
86
89
|
actions: () => [
|
|
87
90
|
{
|
|
88
|
-
label: '
|
|
91
|
+
label: 'Cap output length',
|
|
89
92
|
commands: [
|
|
90
|
-
'
|
|
91
|
-
'
|
|
93
|
+
'Avoid full-file rewrites — prefer the Edit tool',
|
|
94
|
+
'Move long doc/README generation requests into scripts to shrink output',
|
|
92
95
|
],
|
|
93
96
|
},
|
|
94
97
|
],
|
|
95
98
|
},
|
|
96
99
|
HIGH_REQUEST_COUNT: {
|
|
97
|
-
title: '
|
|
100
|
+
title: 'API calls in this session are 3x+ the baseline',
|
|
98
101
|
explain:
|
|
99
|
-
'
|
|
102
|
+
'Excessive tool calls or retry/loop patterns rebroadcast the prefix on every call, ' +
|
|
103
|
+
'spiking input cost.',
|
|
100
104
|
actions: () => [
|
|
101
105
|
{
|
|
102
|
-
label: '
|
|
103
|
-
commands: ['
|
|
106
|
+
label: 'Parallel / batch processing',
|
|
107
|
+
commands: ['Bundle independent investigations into one message with multiple tool calls'],
|
|
104
108
|
},
|
|
105
109
|
{
|
|
106
|
-
label: '
|
|
107
|
-
commands: ['
|
|
110
|
+
label: 'Watch for loops',
|
|
111
|
+
commands: ['Check that the agent is not repeating the same test/search in a loop'],
|
|
108
112
|
},
|
|
109
113
|
],
|
|
110
114
|
},
|
|
111
115
|
FREQUENT_CACHE_REBUILD: {
|
|
112
|
-
title: '
|
|
116
|
+
title: 'Cache writes outweigh cache reads',
|
|
113
117
|
explain:
|
|
114
|
-
'
|
|
118
|
+
'The cache is being created but not reused. Common when sessions are short-lived or ' +
|
|
119
|
+
'restarted after TTL expiry.',
|
|
115
120
|
actions: () => [
|
|
116
121
|
{
|
|
117
|
-
label: '
|
|
118
|
-
commands: ['
|
|
122
|
+
label: 'Check session continuity',
|
|
123
|
+
commands: ['Continue one task in one Claude Code session'],
|
|
119
124
|
},
|
|
120
125
|
],
|
|
121
126
|
},
|
|
@@ -124,15 +129,17 @@ export const ISSUE_MESSAGES = {
|
|
|
124
129
|
/**
|
|
125
130
|
* For the statusline: the single most relevant short chip (1~2 words).
|
|
126
131
|
* Priority reflects what a user can act on *right now*.
|
|
132
|
+
* Kept short and English-only — these ride along on a single-line statusline
|
|
133
|
+
* shown to a global audience.
|
|
127
134
|
*/
|
|
128
135
|
export function chipForIssues(issues, contextWindow) {
|
|
129
|
-
if (contextWindow?.size === '1M') return '⚠ 1M
|
|
136
|
+
if (contextWindow?.size === '1M') return '⚠ 1M ON';
|
|
130
137
|
const codes = issues.map((i) => i.code);
|
|
131
|
-
if (codes.includes('LARGE_INPUT_PER_REQUEST')) return '⚠
|
|
138
|
+
if (codes.includes('LARGE_INPUT_PER_REQUEST')) return '⚠ Input spike';
|
|
132
139
|
if (codes.includes('BUCKET_5M_DOMINANT')) return '⚠ 5m TTL';
|
|
133
|
-
if (codes.includes('LOW_HIT_RATE')) return '⚠
|
|
134
|
-
if (codes.includes('FREQUENT_CACHE_REBUILD')) return '⚠
|
|
135
|
-
if (codes.includes('HIGH_OUTPUT_RATIO')) return '⚠
|
|
136
|
-
if (codes.includes('HIGH_REQUEST_COUNT')) return '⚠
|
|
140
|
+
if (codes.includes('LOW_HIT_RATE')) return '⚠ Cache miss';
|
|
141
|
+
if (codes.includes('FREQUENT_CACHE_REBUILD')) return '⚠ Rebuild churn';
|
|
142
|
+
if (codes.includes('HIGH_OUTPUT_RATIO')) return '⚠ Output heavy';
|
|
143
|
+
if (codes.includes('HIGH_REQUEST_COUNT')) return '⚠ Call surge';
|
|
137
144
|
return null;
|
|
138
145
|
}
|