claude-token-saver 2.0.3 → 2.2.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 CHANGED
@@ -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.
@@ -16,7 +23,7 @@ v1.5 adds three things on top of the original `claude-cache-monitor`:
16
23
 
17
24
  The original functionality still works: cache hit rate, TTL breakdown, cost impact vs. no-cache, TTL countdown timer, and Claude Code statusline integration.
18
25
 
19
- **Runs standalone or as a plugin under another statusline.** Use `npx claude-token-saver` as a one-shot report, wire it into Claude Code's native statusline as the sole segment, or drop it in as an appended segment under a richer statusline like [rz1989s/claude-code-statusline](https://github.com/rz1989s/claude-code-statusline). See [Three Ways to Use It](#three-ways-to-use-it).
26
+ **Run it standalone or wire it into Claude Code's statusline.** Use `npx claude-token-saver` as a one-shot report, or wire it into Claude Code's native statusline for an always-on chip. See [Two Ways to Use It](#two-ways-to-use-it).
20
27
 
21
28
  ---
22
29
 
@@ -29,7 +36,7 @@ v1.5 신규:
29
36
 
30
37
  기존 기능(캐시 히트율·TTL 분포·비용 절감·TTL 카운트다운·statusline)은 그대로 유지됩니다.
31
38
 
32
- **단독 도구로도, 다른 statusline 플러그인으로도 동작합니다.** `npx claude-token-saver` 한 줄로 진단 리포트만 보는 것도 가능하고, Claude Code 내장 statusline에 직접 연결해 상시 표시할 수도 있고, [rz1989s/claude-code-statusline](https://github.com/rz1989s/claude-code-statusline) 같은 기존 statusline 아래에 세그먼트로 덧붙일 수도 있습니다. 자세한 용법은 [Three Ways to Use It](#three-ways-to-use-it) 참고.
39
+ **단독 도구로도, Claude Code statusline 통합으로도 동작합니다.** `npx claude-token-saver` 한 줄로 진단 리포트만 보거나, 내장 statusline에 연결해 상시 표시할 있습니다. 자세한 용법은 [Two Ways to Use It](#two-ways-to-use-it) 참고.
33
40
 
34
41
  ## Quick Start
35
42
 
@@ -92,15 +99,12 @@ Issue codes detected:
92
99
 
93
100
  Remediation commands are chosen from `process.platform` — macOS/Linux/WSL get `~/.zshrc` snippets, Windows gets `setx` and the PowerShell equivalent.
94
101
 
95
- ## Three Ways to Use It
96
-
97
- `claude-token-saver` is primarily a **standalone tool**; the plugin mode is just a convenience for users who already run another statusline.
102
+ ## Two Ways to Use It
98
103
 
99
104
  | Mode | What you run | When to pick this |
100
105
  |---|---|---|
101
106
  | **1. Standalone CLI report** | `npx claude-token-saver` | One-off diagnosis. Prints the full report (spikes + cache + cost + trend). Zero setup. |
102
- | **2. Standalone Claude Code statusline** | `claude-token-saver --statusline` wired via `~/.claude/settings.json` | You want the chip (hit rate · TTL countdown · Ctx 200k/1M · spike) visible all the time. The default story. |
103
- | **3. Plugin under another statusline** | `examples/statusline-with-rz1989s.sh` appends our segment to rz1989s or any wrapper script | You already have a rich statusline (repo info, cost, MCP, prayer times, themes) and want to bolt the token-saver segment on the end. |
107
+ | **2. Claude Code statusline** | `claude-token-saver --statusline` wired via `~/.claude/settings.json` | You want the chip (hit rate · TTL countdown · Ctx 200k/1M · spike) visible all the time. |
104
108
 
105
109
  Detail for each mode below.
106
110
 
@@ -186,22 +190,6 @@ Works best in **Windows Terminal** or **PowerShell 7+** (ANSI color + emoji). Cl
186
190
 
187
191
  Same as Linux — install the package in your WSL Node.js and point to the POSIX sh script.
188
192
 
189
- #### Combine with rz1989s/claude-code-statusline
190
-
191
- 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.
192
-
193
- ```json
194
- {
195
- "statusLine": {
196
- "type": "command",
197
- "command": "bash ~/.claude/statusline-with-rz1989s.sh",
198
- "refreshInterval": 1
199
- }
200
- }
201
- ```
202
-
203
- ---
204
-
205
193
  Claude Code calls this every ~300ms on events, plus once per `refreshInterval` second while idle. Colors are emitted when the terminal supports them:
206
194
 
207
195
  - **Hit rate** — 🟢 ≥85% · 🟡 70–85% · 🔴 <70%
@@ -212,6 +200,76 @@ Claude Code calls this every ~300ms on events, plus once per `refreshInterval` s
212
200
 
213
201
  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.
214
202
 
203
+ ## Claude Code integration (`install`) — new in v2.1
204
+
205
+ One command wires up everything else this README mentions:
206
+
207
+ ```bash
208
+ claude-token-saver install
209
+ ```
210
+
211
+ This writes two files under your Claude user dir:
212
+ - `~/.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.
213
+ - `~/.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.
214
+
215
+ Re-run with `--force` to overwrite. Install only one piece with `install --skill` or `install --command`.
216
+
217
+ ## Warning history (`history`) — new in v2.1
218
+
219
+ The statusline path auto-logs every chip transition (none → ⚠, ⚠ A → ⚠ B, ⚠ → resolved) to a daily Markdown file. Read it back with:
220
+
221
+ ```bash
222
+ claude-token-saver history # last 7 days
223
+ claude-token-saver history --days 30 # wider window
224
+ claude-token-saver history --list # just list available dates
225
+ ```
226
+
227
+ Sample output:
228
+
229
+ ```
230
+ # Token Monitor — 2026-04-25
231
+
232
+ ## Events
233
+ - 09:14:02 ⚠ 1M ON — Context auto-promoted to 1M (max single-request 280k tokens)
234
+ - 09:42:18 ⚠ 1M ON → ⚠ Cache miss — session abc12345: LOW_HIT_RATE
235
+ - 10:05:47 ✓ resolved (was ⚠ Cache miss)
236
+ ```
237
+
238
+ Storage paths (cross-platform):
239
+ - **Windows**: `%APPDATA%\claude-token-saver\history\YYYY-MM-DD.md`
240
+ - **macOS**: `~/Library/Application Support/claude-token-saver/history/YYYY-MM-DD.md`
241
+ - **Linux**: `$XDG_CONFIG_HOME/claude-token-saver/history/YYYY-MM-DD.md` (or `~/.config/...`)
242
+
243
+ Each day's file is plain Markdown — open it in any editor. Transitions are deduped, so the 1Hz statusline refresh doesn't spam.
244
+
245
+ ## Cap-warn + handoff (new in v2.2)
246
+
247
+ Claude Code's statusline payload now includes rate-limit usage (`rate_limits.five_hour.used_percentage`, `rate_limits.seven_day.used_percentage`). claude-token-saver leads the statusline with a `🚨 5H 94%` (or `🚨 7D 92%`) chip the moment either window crosses **90%**, and writes the transition into history:
248
+
249
+ ```
250
+ - 14:32:08 🚨 5H 94% cap warning (resets in 1h 38m)
251
+ - 16:10:21 ✓ 5H cap warning resolved
252
+ ```
253
+
254
+ When you see the chip, back up the work in flight before the cap blocks you:
255
+
256
+ ```bash
257
+ claude-token-saver handoff
258
+ ```
259
+
260
+ That writes `./HANDOFF-YYYY-MM-DD-HHMM.md` in the current directory with:
261
+
262
+ - timestamp, cwd, git branch / HEAD / dirty file list
263
+ - the 5h/7d cap snapshot (and "resets in Hh Mm")
264
+ - empty fillable sections for *what I just did*, *TODO*, *where to pick up next*, *gotchas*
265
+ - a one-line resume prompt for a fresh Claude Code session:
266
+
267
+ ```
268
+ Read the most recent HANDOFF-*.md in this directory and continue the work.
269
+ ```
270
+
271
+ The handoff write is also recorded in history (`📝 handoff written: …`), so `/token-monitor` and `claude-token-saver history` show both the cap-warn and the backup event next to each other.
272
+
215
273
  ## Hook Setup
216
274
 
217
275
  Automatically logs cache stats on every tool call and alerts when hit rate drops below a threshold.
package/bin/cli.js CHANGED
@@ -26,6 +26,50 @@ import { readFileSync } from 'node:fs';
26
26
  import { fileURLToPath } from 'node:url';
27
27
  import { dirname, join } from 'node:path';
28
28
 
29
+ /**
30
+ * Read the JSON blob Claude Code feeds the statusline command on stdin.
31
+ * Returns null when stdin is a TTY or empty (e.g. user invokes `--statusline`
32
+ * by hand) so callers can fall back to flag/env config.
33
+ *
34
+ * The blob shape (subset we consume):
35
+ * {
36
+ * "transcript_path": "...",
37
+ * "rate_limits": {
38
+ * "five_hour": { "used_percentage": 94, "resets_at": 1777099200 },
39
+ * "seven_day": { "used_percentage": 7, "resets_at": 1777521600 }
40
+ * }
41
+ * }
42
+ */
43
+ function readStdinJson() {
44
+ if (process.stdin.isTTY) return null;
45
+ try {
46
+ const raw = readFileSync(0, 'utf8');
47
+ if (!raw || !raw.trim()) return null;
48
+ return JSON.parse(raw);
49
+ } catch {
50
+ return null;
51
+ }
52
+ }
53
+
54
+ function extractCaps(stdinJson) {
55
+ if (!stdinJson || !stdinJson.rate_limits) return null;
56
+ const rl = stdinJson.rate_limits;
57
+ const pick = (obj) => {
58
+ if (!obj || typeof obj !== 'object') return null;
59
+ const used = Number(obj.used_percentage);
60
+ if (!Number.isFinite(used)) return null;
61
+ const resetsAt = Number(obj.resets_at);
62
+ return {
63
+ usedPct: used,
64
+ resetsAt: Number.isFinite(resetsAt) ? resetsAt : null,
65
+ };
66
+ };
67
+ return {
68
+ fiveHour: pick(rl.five_hour),
69
+ sevenDay: pick(rl.seven_day),
70
+ };
71
+ }
72
+
29
73
  import { parseAllSessions, getLastUserMessageTime } from '../src/parser.js';
30
74
  import {
31
75
  dailyTrend,
@@ -53,8 +97,11 @@ const PKG_VERSION = (() => {
53
97
 
54
98
  function getArg(name) {
55
99
  const idx = args.indexOf(name);
56
- if (idx === -1) return undefined;
57
- return args[idx + 1];
100
+ if (idx !== -1) return args[idx + 1];
101
+ const prefix = `${name}=`;
102
+ const eq = args.find((a) => a.startsWith(prefix));
103
+ if (eq) return eq.slice(prefix.length);
104
+ return undefined;
58
105
  }
59
106
 
60
107
  function hasFlag(name) {
@@ -62,6 +109,141 @@ function hasFlag(name) {
62
109
  }
63
110
 
64
111
  async function main() {
112
+ // Subcommand: history — print recent warning transitions captured by the
113
+ // statusline. One markdown file per day, persisted under the platform-
114
+ // specific user-data dir.
115
+ // claude-token-saver history # last 7 days
116
+ // claude-token-saver history --days 30 # custom window
117
+ // claude-token-saver history --list # just list available dates
118
+ if (args[0] === 'history') {
119
+ const { readRecent, listDates, historyDir } = await import('../src/history.js');
120
+ if (hasFlag('--list')) {
121
+ const dates = listDates();
122
+ if (dates.length === 0) {
123
+ console.log(`No history yet. Files will appear under: ${historyDir()}`);
124
+ return;
125
+ }
126
+ console.log(`History (${historyDir()}):`);
127
+ for (const d of dates) console.log(` ${d}`);
128
+ return;
129
+ }
130
+ const days = parseFloat(getArg('--days') || '7');
131
+ const recent = readRecent(days);
132
+ if (recent.length === 0) {
133
+ console.log(`No warning history in the last ${days} day${days === 1 ? '' : 's'}.`);
134
+ console.log(`(Files would be written to: ${historyDir()})`);
135
+ return;
136
+ }
137
+ for (const { content } of recent) {
138
+ console.log(content.replace(/\n+$/, ''));
139
+ console.log('');
140
+ }
141
+ return;
142
+ }
143
+
144
+ // Subcommand: handoff — write a HANDOFF-YYYY-MM-DD-HHMM.md template in cwd
145
+ // capturing git status + the latest cap snapshot, so a fresh Claude Code
146
+ // session can pick up where this one stopped. Pairs with the cap-warn chip:
147
+ // when statusline shows 🚨 5H 90%+, run this to back up state before the cap
148
+ // hits.
149
+ // claude-token-saver handoff # write to cwd
150
+ // claude-token-saver handoff --cwd PATH # custom directory
151
+ if (args[0] === 'handoff') {
152
+ const { writeHandoff } = await import('../src/handoff.js');
153
+ const { recordHandoff } = await import('../src/history.js');
154
+ const cwd = getArg('--cwd') || process.cwd();
155
+ // Cap data only flows in via stdin (Claude Code statusline contract).
156
+ // Direct CLI invocations won't have it — that's fine, the template will
157
+ // note the gap.
158
+ const stdinJson = readStdinJson();
159
+ const caps = extractCaps(stdinJson);
160
+ const { path, git } = writeHandoff({ cwd, caps });
161
+ try { recordHandoff(path); } catch { /* non-critical */ }
162
+ console.log(`Handoff written: ${path}`);
163
+ if (git) {
164
+ console.log(` git: ${git.branch}${git.head ? ` @ ${git.head}` : ''}${git.status ? ' (dirty)' : ' (clean)'}`);
165
+ }
166
+ console.log('');
167
+ console.log('Fill in the empty sections, then start a new Claude Code session with:');
168
+ console.log(' Read the most recent HANDOFF-*.md in this directory and continue the work.');
169
+ return;
170
+ }
171
+
172
+ // Subcommand: install — write the Claude Code Skill and slash command so
173
+ // /token-monitor and the auto-trigger skill become available without any
174
+ // manual file editing. Cross-platform (uses node:path + node:fs).
175
+ // claude-token-saver install # install both
176
+ // claude-token-saver install --skill # only the skill
177
+ // claude-token-saver install --command # only the slash command
178
+ // claude-token-saver install --force # overwrite existing files
179
+ if (args[0] === 'install') {
180
+ const { installSkill, installCommand, installAll } = await import('../src/installer.js');
181
+ const force = hasFlag('--force');
182
+ const onlySkill = hasFlag('--skill');
183
+ const onlyCommand = hasFlag('--command');
184
+ const print = (kind, r) => {
185
+ const verb = r.action === 'exists' ? 'already exists' : r.action;
186
+ console.log(` ${kind}: ${r.path} (${verb})`);
187
+ };
188
+ if (onlySkill && !onlyCommand) {
189
+ print('skill', installSkill({ force }));
190
+ } else if (onlyCommand && !onlySkill) {
191
+ print('command', installCommand({ force }));
192
+ } else {
193
+ const r = installAll({ force });
194
+ print('skill', r.skill);
195
+ print('command', r.command);
196
+ }
197
+ console.log('');
198
+ console.log('Open Claude Code in any directory and try:');
199
+ console.log(' /token-monitor');
200
+ console.log('Or just mention "cache hit rate" / "1M context" — the skill auto-activates.');
201
+ if (!force) {
202
+ console.log('');
203
+ console.log('Tip: re-run with --force to overwrite existing files.');
204
+ }
205
+ return;
206
+ }
207
+
208
+ // Subcommand: mode — persist statusline preferences so future runs pick
209
+ // them up without flags or wrapper edits.
210
+ // claude-token-saver mode # show current config
211
+ // claude-token-saver mode icon verbose # set icon + verbose
212
+ // claude-token-saver mode reset # clear back to defaults
213
+ if (args[0] === 'mode') {
214
+ const { applyMode, loadConfig, configPath, statuslineDefaults, VALID_KEYWORDS } =
215
+ await import('../src/config.js');
216
+ const words = args.slice(1);
217
+ if (words.length === 0) {
218
+ const eff = statuslineDefaults();
219
+ const raw = loadConfig().statusline || {};
220
+ console.log('Statusline mode (effective):');
221
+ console.log(` icon: ${eff.icon}`);
222
+ console.log(` verbose: ${eff.verbose}`);
223
+ console.log(` timer: ${eff.timer}`);
224
+ console.log(` color: ${eff.color}`);
225
+ console.log(` window: ${eff.windowLabel} (${eff.windowHours}h)`);
226
+ console.log('');
227
+ console.log(`Stored config (${configPath()}):`);
228
+ console.log(` ${Object.keys(raw).length === 0 ? '(none — using defaults)' : JSON.stringify(raw)}`);
229
+ console.log('');
230
+ console.log('Change with: claude-token-saver mode <keywords...>');
231
+ console.log(`Keywords: ${VALID_KEYWORDS.join(', ')}`);
232
+ return;
233
+ }
234
+ const { applied, unknown } = applyMode(words);
235
+ if (unknown.length) {
236
+ console.error(`Unknown keyword${unknown.length > 1 ? 's' : ''}: ${unknown.join(', ')}`);
237
+ console.error(`Valid: ${VALID_KEYWORDS.join(', ')}`);
238
+ process.exit(1);
239
+ }
240
+ const eff = statuslineDefaults();
241
+ console.log(`Updated: ${applied.join(', ')}`);
242
+ console.log(`Now: icon=${eff.icon} verbose=${eff.verbose} timer=${eff.timer} color=${eff.color} window=${eff.windowLabel}`);
243
+ console.log('Statusline picks up the change on the next refresh (~1s).');
244
+ return;
245
+ }
246
+
65
247
  // Hook management
66
248
  if (hasFlag('--install-hook')) {
67
249
  const { installHook } = await import('../src/hook-manager.js');
@@ -85,10 +267,94 @@ async function main() {
85
267
  // Statusline mode shortcut
86
268
  const isStatusline = hasFlag('--statusline') || getArg('--format') === 'statusline';
87
269
 
270
+ // Demo mode — render synthetic warning-case data through the real
271
+ // formatter for screencasts/marketing GIFs. `--demo cycle` rotates through
272
+ // every scenario based on wall clock so a screen recorder picks them up.
273
+ const demoArg = getArg('--demo');
274
+
275
+ // `claude-token-saver --demo table` (no --statusline) — full table view
276
+ // with all six issue drill-downs at once, for marketing screencasts.
277
+ if (!isStatusline && demoArg === 'table') {
278
+ const { buildTableDemoData } = await import('../src/demo.js');
279
+ const { formatReport } = await import('../src/formatters/table.js');
280
+ const data = buildTableDemoData({ version: PKG_VERSION });
281
+ console.log(formatReport(data));
282
+ return;
283
+ }
284
+
285
+ if (isStatusline && demoArg) {
286
+ const { buildScenarioData, listScenarios } = await import('../src/demo.js');
287
+ const { statuslineDefaults } = await import('../src/config.js');
288
+ const cfg = statuslineDefaults();
289
+ const cycleSeconds = parseFloat(getArg('--demo-cycle-sec') || '3');
290
+ const data = buildScenarioData(demoArg, {
291
+ cycleSeconds,
292
+ windowHours: cfg.windowHours,
293
+ windowLabel: cfg.windowLabel,
294
+ days: cfg.windowHours / 24,
295
+ version: PKG_VERSION,
296
+ });
297
+ if (!data) {
298
+ const known = listScenarios().map((s) => s.name).concat(['cycle']).join(', ');
299
+ console.error(`Unknown demo scenario: ${demoArg}`);
300
+ console.error(`Valid: ${known}`);
301
+ process.exit(1);
302
+ }
303
+ const { formatReport } = await import('../src/formatters/statusline.js');
304
+ const isIcon = hasFlag('--icon')
305
+ ? true
306
+ : (hasFlag('--no-icon') || hasFlag('--text') ? false : cfg.icon);
307
+ const isVerbose = hasFlag('--verbose')
308
+ ? true
309
+ : (hasFlag('--no-verbose') || hasFlag('--compact') ? false : cfg.verbose);
310
+ const showTimer = hasFlag('--no-timer') ? false : cfg.timer;
311
+ const colorOk = !hasFlag('--no-color') && !process.env.NO_COLOR && cfg.color;
312
+ const segmentsArg = getArg('--segments');
313
+ const segments = segmentsArg
314
+ ? segmentsArg.split(',').map((s) => s.trim()).filter(Boolean)
315
+ : null;
316
+ const out = formatReport(data, {
317
+ color: colorOk,
318
+ verbose: isVerbose,
319
+ timer: showTimer,
320
+ mode: isIcon ? 'icon' : 'text',
321
+ segments,
322
+ });
323
+ // For `cycle` mode, prefix with the scenario label so the screen recorder
324
+ // shows what the viewer is looking at (only when explicitly requested).
325
+ if (demoArg === 'cycle' && hasFlag('--demo-label')) {
326
+ const gray = colorOk ? '\x1b[90m' : '';
327
+ const reset = colorOk ? '\x1b[0m' : '';
328
+ console.log(`${gray}[${data._demoLabel}]${reset} ${out}`);
329
+ } else {
330
+ console.log(out);
331
+ }
332
+ return;
333
+ }
334
+
88
335
  // 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);
336
+ // Statusline window comes from persisted config hours-precise so users
337
+ // can pick `1h` / `6h` etc, not just whole days. Other formats default to
338
+ // 30 days as before.
339
+ let windowHours = 30 * 24;
340
+ let windowLabel = '30d';
341
+ if (isStatusline) {
342
+ const { statuslineDefaults } = await import('../src/config.js');
343
+ const d = statuslineDefaults();
344
+ windowHours = d.windowHours;
345
+ windowLabel = d.windowLabel;
346
+ }
347
+ // CLI overrides: --hours wins over --days; both win over config.
348
+ const hoursArg = getArg('--hours');
349
+ const daysArg = getArg('--days') || getArg('-d');
350
+ if (hoursArg !== undefined) {
351
+ windowHours = parseFloat(hoursArg);
352
+ windowLabel = `${windowHours}h`;
353
+ } else if (daysArg !== undefined) {
354
+ windowHours = parseFloat(daysArg) * 24;
355
+ windowLabel = `${parseFloat(daysArg)}d`;
356
+ }
357
+ const days = windowHours / 24;
92
358
  const format = isStatusline ? 'statusline' : (getArg('--format') || getArg('-f') || 'table');
93
359
  const projectFilter = getArg('--project') || getArg('-p');
94
360
 
@@ -132,13 +398,39 @@ async function main() {
132
398
  const spikeReport = detectSpikes(sessions, { recentHours: 24, multiplier: 3 });
133
399
  const contextWindow = detectContextWindow(sessions, { recentHours: 24 });
134
400
 
401
+ // Claude Code feeds the statusline command a JSON blob on stdin every
402
+ // refresh. Pull rate_limits out of it so we can surface cap-warn (>=90%)
403
+ // chips, record cap transitions, and seed the table view's warning box.
404
+ // The table path falls back to the most-recent cached snapshot so the
405
+ // /token-monitor slash command (which doesn't pipe stdin) still warns.
406
+ const stdinJson = readStdinJson();
407
+ let caps = extractCaps(stdinJson);
408
+ if (isStatusline && caps) {
409
+ try {
410
+ const { persistCaps } = await import('../src/caps-cache.js');
411
+ persistCaps(caps);
412
+ } catch {
413
+ // non-critical
414
+ }
415
+ }
416
+ if (!isStatusline && !caps) {
417
+ try {
418
+ const { loadRecentCaps } = await import('../src/caps-cache.js');
419
+ caps = loadRecentCaps();
420
+ } catch {
421
+ // ignore
422
+ }
423
+ }
424
+
135
425
  // For statusline: attach a single-word chip only when there's something
136
426
  // actionable right now. 1M context is always shown; otherwise only fire
137
427
  // if the most recent session actually appears in the spike list.
138
428
  let spikeChip = null;
429
+ let chipDetail = null;
139
430
  if (format === 'statusline') {
140
431
  if (contextWindow.size === '1M') {
141
432
  spikeChip = chipForIssues([], contextWindow);
433
+ chipDetail = `Context auto-promoted to 1M (max single-request ${Math.round(contextWindow.maxContext / 1000)}k tokens)`;
142
434
  } else {
143
435
  const recentSession = sessions
144
436
  .slice()
@@ -150,8 +442,27 @@ async function main() {
150
442
  const m = sessionMetrics(recentSession);
151
443
  const issues = diagnoseSession(m, spikeReport.baseline);
152
444
  spikeChip = chipForIssues(issues, contextWindow);
445
+ const titles = issues
446
+ .map((i) => i.code)
447
+ .slice(0, 2)
448
+ .join(', ');
449
+ chipDetail = `session ${recentSession.sessionId?.slice(0, 8) || ''}: ${titles}`;
153
450
  }
154
451
  }
452
+ // Persist transitions to ~/.config/claude-token-saver/history/YYYY-MM-DD.md
453
+ // so /token-monitor and `claude-token-saver history` can replay them.
454
+ try {
455
+ const { recordChip, recordCapTransition } = await import('../src/history.js');
456
+ recordChip(spikeChip, { detail: chipDetail });
457
+ // Cap-warn transitions are tracked independently per window — a session
458
+ // can hit 90% on the 5h window even when no spike chip is firing.
459
+ if (caps) {
460
+ recordCapTransition('five_hour', caps.fiveHour);
461
+ recordCapTransition('seven_day', caps.sevenDay);
462
+ }
463
+ } catch {
464
+ // history is non-critical — don't let it break the statusline render
465
+ }
155
466
  }
156
467
 
157
468
  // Last API activity feeds the statusline TTL countdown.
@@ -181,11 +492,12 @@ async function main() {
181
492
  ttl,
182
493
  anomalies,
183
494
  cost,
184
- options: { days, version: PKG_VERSION },
495
+ options: { days, windowHours, windowLabel, version: PKG_VERSION },
185
496
  lastActivity,
186
497
  spikeReport,
187
498
  contextWindow,
188
499
  spikeChip,
500
+ caps,
189
501
  };
190
502
 
191
503
  let output;
@@ -197,13 +509,30 @@ async function main() {
197
509
  output = formatReport(data);
198
510
  } else if (format === 'statusline') {
199
511
  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';
512
+ const { statuslineDefaults } = await import('../src/config.js');
513
+ const cfg = statuslineDefaults();
514
+
515
+ // CLI flags take precedence; otherwise fall back to persisted config.
516
+ const isIcon = hasFlag('--icon')
517
+ ? true
518
+ : (hasFlag('--no-icon') || hasFlag('--text') ? false : cfg.icon);
519
+ const isVerbose = hasFlag('--verbose')
520
+ ? true
521
+ : (hasFlag('--no-verbose') || hasFlag('--compact') ? false : cfg.verbose);
522
+ const showTimer = hasFlag('--no-timer') ? false : cfg.timer;
523
+ const colorOk =
524
+ !hasFlag('--no-color') && !process.env.NO_COLOR && cfg.color;
525
+
526
+ const segmentsArg = getArg('--segments');
527
+ const segments = segmentsArg
528
+ ? segmentsArg.split(',').map((s) => s.trim()).filter(Boolean)
529
+ : null;
202
530
  output = formatReport(data, {
203
531
  color: colorOk,
204
- verbose: hasFlag('--verbose'),
205
- timer: !hasFlag('--no-timer'),
206
- mode,
532
+ verbose: isVerbose,
533
+ timer: showTimer,
534
+ mode: isIcon ? 'icon' : 'text',
535
+ segments,
207
536
  });
208
537
  } else {
209
538
  const { formatReport } = await import('../src/formatters/table.js');
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-token-saver",
3
- "version": "2.0.3",
3
+ "version": "2.2.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": {