claude-token-saver 2.7.3 β†’ 2.8.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.en.md CHANGED
@@ -53,12 +53,12 @@ Risk chips lead when something's wrong: `🚨 5H 94%`, `⚠ 1M ON`, `⚠ Cache m
53
53
  "statusLine": {
54
54
  "type": "command",
55
55
  "command": "claude-token-saver --statusline --icon",
56
- "refreshInterval": 1
56
+ "refreshInterval": 5
57
57
  }
58
58
  }
59
59
  ```
60
60
 
61
- `refreshInterval: 1` keeps the TTL countdown ticking while idle (Claude Code's statusline is otherwise event-driven). For Windows PowerShell, see `examples/statusline-command.ps1`.
61
+ `refreshInterval: 5` keeps the TTL countdown ticking while idle (Claude Code's statusline is otherwise event-driven). 1s also works, but 5s is the recommended default to avoid constant I/O. For Windows PowerShell, see `examples/statusline-command.ps1`.
62
62
 
63
63
  ## Commands
64
64
 
package/README.md CHANGED
@@ -53,12 +53,12 @@ TTL Breakdown / Cost Impact / Daily Trend ...
53
53
  "statusLine": {
54
54
  "type": "command",
55
55
  "command": "claude-token-saver --statusline --icon",
56
- "refreshInterval": 1
56
+ "refreshInterval": 5
57
57
  }
58
58
  }
59
59
  ```
60
60
 
61
- `refreshInterval: 1`은 TTL μΉ΄μš΄νŠΈλ‹€μš΄μ΄ idle μƒνƒœμ—μ„œλ„ 1μ΄ˆλ§ˆλ‹€ κ°±μ‹ λ˜κ²Œ ν•©λ‹ˆλ‹€. Windows(PowerShell)λŠ” `examples/statusline-command.ps1` μ°Έκ³ .
61
+ `refreshInterval: 5`λŠ” TTL μΉ΄μš΄νŠΈλ‹€μš΄μ΄ idle μƒνƒœμ—μ„œλ„ 5μ΄ˆλ§ˆλ‹€ κ°±μ‹ λ˜κ²Œ ν•©λ‹ˆλ‹€ (1μ΄ˆλ„ κ°€λŠ₯ν•˜μ§€λ§Œ μƒμ‹œ I/O 뢀담을 ν”Όν•˜λ €κ³  5초λ₯Ό κΈ°λ³Έκ°’μœΌλ‘œ ꢌμž₯). Windows(PowerShell)λŠ” `examples/statusline-command.ps1` μ°Έκ³ .
62
62
 
63
63
  ## μ£Όμš” λͺ…λ Ή
64
64
 
package/bin/cli.js CHANGED
@@ -344,6 +344,14 @@ async function main() {
344
344
  };
345
345
  const r = installAll({ force });
346
346
  print('skill', r.skill);
347
+ {
348
+ const s = r.statusline;
349
+ const verb = s.action === 'exists' ? 'already configured (refreshInterval=5)'
350
+ : s.action === 'skipped' ? `skipped β€” ${s.reason}`
351
+ : s.reason ? `${s.action} β€” ${s.reason}`
352
+ : s.action;
353
+ console.log(` statusline: ${s.path} (${verb})`);
354
+ }
347
355
  if (r.legacy.action === 'removed') {
348
356
  print('legacy /token-monitor', r.legacy);
349
357
  console.log(' (consolidated into the skill β€” same workflow, triggered by intent)');
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-token-saver",
3
- "version": "2.7.3",
3
+ "version": "2.8.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": {
@@ -100,11 +100,18 @@ function gaugeBar(pct) {
100
100
  * Format a remaining-seconds countdown as MM:SS (or H:MM when β‰₯ 1h).
101
101
  */
102
102
  function formatTimer(remainingSec) {
103
- if (remainingSec <= 0) return 'EXPIRED';
104
- const totalSec = Math.floor(remainingSec);
103
+ // Defensive: non-finite/NaN inputs (e.g. clock skew, stringified Date) used
104
+ // to slip through and render as "NaN:NaN" or stretched seconds. Treat any
105
+ // weird input as expired rather than rendering garbage in the statusline.
106
+ if (!Number.isFinite(remainingSec) || remainingSec <= 0) return 'EXPIRED';
107
+ const totalSec = Math.max(0, Math.floor(remainingSec));
105
108
  const h = Math.floor(totalSec / 3600);
106
- const m = Math.floor((totalSec % 3600) / 60);
107
- const s = totalSec % 60;
109
+ const mRaw = Math.floor((totalSec % 3600) / 60);
110
+ const sRaw = totalSec % 60;
111
+ // Clamp explicitly so a future regression in the math (or padStart no-op
112
+ // truncation) can never produce m:sss like "4:547".
113
+ const m = Math.min(59, Math.max(0, mRaw));
114
+ const s = Math.min(59, Math.max(0, sRaw));
108
115
  if (h > 0) return `${h}:${String(m).padStart(2, '0')}`;
109
116
  return `${m}:${String(s).padStart(2, '0')}`;
110
117
  }
@@ -195,8 +202,18 @@ export function formatReport(data, { color = true, verbose = false, timer = true
195
202
  // icon verbose: "⏳ Expires 1h 59:58"
196
203
  let ttlSeg;
197
204
  if (timer && lastActivity) {
198
- const elapsed = (Date.now() - lastActivity) / 1000;
199
- const remaining = ttlSeconds - elapsed;
205
+ // Coerce to a numeric ms timestamp. Some upstream paths handed in a Date,
206
+ // a stringified ISO timestamp, or epoch-seconds β€” any of which silently
207
+ // produces NaN/huge values when subtracted from Date.now(), which then
208
+ // bypasses formatTimer's normal MM:SS shape.
209
+ const laMs =
210
+ typeof lastActivity === 'number'
211
+ ? (lastActivity < 1e12 ? lastActivity * 1000 : lastActivity) // seconds β†’ ms
212
+ : (lastActivity instanceof Date ? lastActivity.getTime() : Date.parse(lastActivity));
213
+ const elapsed = Number.isFinite(laMs) ? (Date.now() - laMs) / 1000 : Infinity;
214
+ // Clamp remaining into the bucket so a clock-skew or stale-state edge case
215
+ // can't display a value larger than the bucket itself.
216
+ const remaining = Math.min(ttlSeconds, ttlSeconds - elapsed);
200
217
  const text = formatTimer(remaining);
201
218
  const pct = remaining / ttlSeconds;
202
219
  const timerColor =
package/src/installer.js CHANGED
@@ -13,10 +13,13 @@
13
13
  * exist on every platform.
14
14
  */
15
15
 
16
- import { writeFileSync, mkdirSync, existsSync, unlinkSync } from 'node:fs';
16
+ import { writeFileSync, mkdirSync, existsSync, unlinkSync, readFileSync } from 'node:fs';
17
17
  import { join } from 'node:path';
18
18
  import { claudeUserDir } from './paths.js';
19
19
 
20
+ const STATUSLINE_COMMAND = 'claude-token-saver --statusline --icon';
21
+ const STATUSLINE_REFRESH_INTERVAL = 5;
22
+
20
23
  const SKILL_BODY = `---
21
24
  name: claude-token-saver
22
25
  description: Use when the user mentions Claude Code token usage, prompt cache hit rate, TTL/expiry, the 1M context window, cache misses, output spikes, rate-limit caps (5h/7d), or anything in the statusline produced by claude-token-saver (chips like "🚨 5H 94%", "🚨 7D 92%", "⚠ 1M ON", "⚠ Input spike", "⚠ Cache miss", "⚠ 5m TTL", "⚠ Rebuild churn", "⚠ Output heavy", "⚠ Call surge", "⏳ Cache expires", "πŸ’° Cache saved", "🧠 Cache hit"). Also use when they ask to view token-usage history, want to understand a warning they just saw, or want to back up work before a session cap with \`claude-token-saver handoff\`.
@@ -127,9 +130,63 @@ export function removeLegacyCommand() {
127
130
  return { path: file, action: 'removed' };
128
131
  }
129
132
 
133
+ // Registers/repairs the Claude Code statusLine entry in ~/.claude/settings.json.
134
+ // - No statusLine yet: insert ours with refreshInterval:1.
135
+ // - statusLine already points at claude-token-saver: ensure refreshInterval:1
136
+ // (this is the bit that makes the TTL countdown tick every second while idle).
137
+ // - statusLine points at a different command: leave it alone unless --force.
138
+ export function installStatusline({ force = false } = {}) {
139
+ const dir = claudeUserDir();
140
+ const file = join(dir, 'settings.json');
141
+ mkdirSync(dir, { recursive: true });
142
+
143
+ let settings = {};
144
+ if (existsSync(file)) {
145
+ try {
146
+ settings = JSON.parse(readFileSync(file, 'utf8'));
147
+ } catch (e) {
148
+ return { path: file, action: 'skipped', reason: `unreadable JSON (${e.message})` };
149
+ }
150
+ }
151
+
152
+ const cur = settings.statusLine;
153
+ const targetsUs = cur && typeof cur.command === 'string' && cur.command.includes('claude-token-saver');
154
+
155
+ if (!cur) {
156
+ settings.statusLine = {
157
+ type: 'command',
158
+ command: STATUSLINE_COMMAND,
159
+ refreshInterval: STATUSLINE_REFRESH_INTERVAL,
160
+ };
161
+ writeFileSync(file, JSON.stringify(settings, null, 2) + '\n');
162
+ return { path: file, action: 'created' };
163
+ }
164
+
165
+ if (targetsUs) {
166
+ if (cur.refreshInterval === STATUSLINE_REFRESH_INTERVAL) {
167
+ return { path: file, action: 'exists' };
168
+ }
169
+ cur.refreshInterval = STATUSLINE_REFRESH_INTERVAL;
170
+ writeFileSync(file, JSON.stringify(settings, null, 2) + '\n');
171
+ return { path: file, action: 'updated', reason: 'set refreshInterval=1' };
172
+ }
173
+
174
+ if (!force) {
175
+ return { path: file, action: 'skipped', reason: `existing statusLine command (${cur.command}) β€” re-run with --force to overwrite` };
176
+ }
177
+ settings.statusLine = {
178
+ type: 'command',
179
+ command: STATUSLINE_COMMAND,
180
+ refreshInterval: STATUSLINE_REFRESH_INTERVAL,
181
+ };
182
+ writeFileSync(file, JSON.stringify(settings, null, 2) + '\n');
183
+ return { path: file, action: 'updated', reason: 'replaced previous statusLine' };
184
+ }
185
+
130
186
  export function installAll({ force = false } = {}) {
131
187
  return {
132
188
  skill: installSkill({ force }),
189
+ statusline: installStatusline({ force }),
133
190
  legacy: removeLegacyCommand(),
134
191
  };
135
192
  }