claude-token-saver 2.7.0 β†’ 2.7.2

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
@@ -2,10 +2,12 @@
2
2
 
3
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
- > πŸ“Ί **HNPulse Shorts** β€” 이 도ꡬ가 λ§Œλ“€μ–΄μ§„ λ°°κ²½ (μΊμ‹œ TTL 1hβ†’5m λ³€κ²½ 이슈):
6
- > **[β–Ά Watch the Short](https://www.youtube.com/shorts/oSx2sg935nI)** Β· [All HNPulse Shorts](https://www.youtube.com/@HNPulseKR/shorts)
5
+ > πŸ“Ί **μΆœμ‹œ μ˜μƒ (60초)** β€” μ™œ λ§Œλ“€μ—ˆκ³ , 무엇을 λ³΄μ—¬μ£ΌλŠ”μ§€:
6
+ > **[β–Ά Watch the launch Short](https://www.youtube.com/shorts/RaD8qMsPTnA)** Β· [All DeepPulse Shorts](https://www.youtube.com/@HNPulseKR/shorts)
7
7
  >
8
- > [![HNPulse Short](https://img.youtube.com/vi/oSx2sg935nI/hqdefault.jpg)](https://www.youtube.com/shorts/oSx2sg935nI)
8
+ > [![claude-token-saver launch](https://img.youtube.com/vi/RaD8qMsPTnA/hqdefault.jpg)](https://www.youtube.com/shorts/RaD8qMsPTnA)
9
+ >
10
+ > Earlier context (cache TTL 1h→5m incident, the reason this tool exists): [HNPulse #025](https://www.youtube.com/shorts/oSx2sg935nI)
9
11
 
10
12
  **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
13
 
package/package.json CHANGED
@@ -1,10 +1,10 @@
1
1
  {
2
2
  "name": "claude-token-saver",
3
- "version": "2.7.0",
3
+ "version": "2.7.2",
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"
7
+ "claude-token-saver": "bin/cli.js"
8
8
  },
9
9
  "files": [
10
10
  "bin/",
@@ -31,6 +31,6 @@
31
31
  "license": "MIT",
32
32
  "repository": {
33
33
  "type": "git",
34
- "url": "https://github.com/rootstudioyaml/claude-token-saver"
34
+ "url": "git+https://github.com/rootstudioyaml/claude-token-saver.git"
35
35
  }
36
36
  }
package/src/advice.js CHANGED
@@ -285,6 +285,102 @@ export const ISSUE_MESSAGES = {
285
285
  },
286
286
  ],
287
287
  },
288
+ TTL_EXPIRY_IMMINENT: {
289
+ title: 'Cache TTL is about to expire',
290
+ titleKo: 'μΊμ‹œ TTL 만료 μž„λ°•',
291
+ explain:
292
+ 'Once the TTL window closes, the entire prefix cache is discarded and must be rebuilt ' +
293
+ 'from scratch on the next call β€” paying full input cost again.',
294
+ explainKo:
295
+ 'TTL 창이 λ‹«νžˆλ©΄ prefix μΊμ‹œ 전체가 폐기되고 λ‹€μŒ ν˜ΈμΆœμ—μ„œ μ²˜μŒλΆ€ν„° μž¬κ΅¬μΆ•ν•΄μ•Ό ν•©λ‹ˆλ‹€ β€” ' +
296
+ '전체 μž…λ ₯ λΉ„μš©μ΄ λ‹€μ‹œ μ²­κ΅¬λ©λ‹ˆλ‹€.',
297
+ actions: () => [
298
+ {
299
+ label: 'Send any prompt within the TTL window',
300
+ labelKo: 'TTL μ°½ μ•ˆμ— μ–΄λ–€ ν”„λ‘¬ν”„νŠΈλ“  보내기',
301
+ commands: [
302
+ 'Even a short "." or summary request resets the TTL clock',
303
+ 'Pro plan TTL = 5 min; Max plan TTL = 1 hour',
304
+ ],
305
+ commandsKo: [
306
+ '"." ν•œ κΈ€μžλ‚˜ 짧은 μš”μ•½ μš”μ²­λ§Œ 해도 TTL 타이머가 리셋됨',
307
+ 'Pro ν”Œλžœ TTL = 5λΆ„; Max ν”Œλžœ TTL = 1μ‹œκ°„',
308
+ ],
309
+ },
310
+ {
311
+ label: 'Use /compact before going idle',
312
+ labelKo: '자리λ₯Ό λΉ„μš°κΈ° 전에 /compact μ‹€ν–‰',
313
+ commands: [
314
+ '/compact summarizes the conversation in place and re-anchors the cache',
315
+ 'The compacted summary is much smaller β€” next rebuild is cheaper',
316
+ ],
317
+ commandsKo: [
318
+ '/compactλŠ” λŒ€ν™”λ₯Ό κ·Έ μžλ¦¬μ—μ„œ μš”μ•½ν•˜κ³  μΊμ‹œλ₯Ό μž¬κ³ μ •',
319
+ 'μš”μ•½λœ μ»¨ν…μŠ€νŠΈλŠ” 훨씬 μž‘μ•„μ„œ λ‹€μŒ μž¬λΉŒλ“œ λΉ„μš©λ„ 쀄어듦',
320
+ ],
321
+ },
322
+ {
323
+ label: 'Upgrade to Max for 1h TTL',
324
+ labelKo: 'Max ν”ŒλžœμœΌλ‘œ μ—…κ·Έλ ˆμ΄λ“œν•΄ 1μ‹œκ°„ TTL 확보',
325
+ commands: [
326
+ 'Pro plan is capped at 5-minute TTL β€” any break longer than that expires the cache',
327
+ 'Max plan uses the 1-hour extended TTL bucket by default',
328
+ ],
329
+ commandsKo: [
330
+ 'Pro ν”Œλžœμ€ 5λΆ„ TTL κ³ μ • β€” 5λΆ„ 이상 곡백이면 μΊμ‹œ 만료',
331
+ 'Max ν”Œλžœμ€ 기본적으둜 1μ‹œκ°„ TTL 버킷을 μ‚¬μš©',
332
+ ],
333
+ },
334
+ ],
335
+ },
336
+ CONTEXT_NEAR_LIMIT: {
337
+ title: 'Context window is approaching the limit',
338
+ titleKo: 'μ»¨ν…μŠ€νŠΈ 창이 ν•œκ³„μ— κ·Όμ ‘',
339
+ explain:
340
+ 'As context grows toward 200k (or 1M), each turn becomes more expensive and cache ' +
341
+ 'reuse efficiency drops. Past 200k, long-context pricing applies.',
342
+ explainKo:
343
+ 'μ»¨ν…μŠ€νŠΈκ°€ 200k(λ˜λŠ” 1M)에 κ°€κΉŒμ›Œμ§ˆμˆ˜λ‘ λ§€ ν„΄ λΉ„μš©μ΄ λ†’μ•„μ§€κ³  μΊμ‹œ μž¬μ‚¬μš© 효율이 λ–¨μ–΄μ§‘λ‹ˆλ‹€. ' +
344
+ '200kλ₯Ό λ„˜μœΌλ©΄ μž₯κΈ° μ»¨ν…μŠ€νŠΈ μš”κΈˆμ΄ μ μš©λ©λ‹ˆλ‹€.',
345
+ actions: () => [
346
+ {
347
+ label: '/compact at the next natural break',
348
+ labelKo: 'λ‹€μŒ μžμ—°μŠ€λŸ¬μš΄ μ€‘λ‹¨μ μ—μ„œ /compact',
349
+ commands: [
350
+ '/compact replaces the full conversation with a compact summary',
351
+ 'Run it before context hits the limit β€” not after (compaction itself costs tokens)',
352
+ ],
353
+ commandsKo: [
354
+ '/compactλŠ” 전체 λŒ€ν™”λ₯Ό μ••μΆ• μš”μ•½μœΌλ‘œ ꡐ체',
355
+ 'ν•œκ³„μ— λ„λ‹¬ν•˜κΈ° 전에 μ‹€ν–‰ β€” 이후엔 compact μžμ²΄λ„ λΉ„μŒˆ',
356
+ ],
357
+ },
358
+ {
359
+ label: 'Re-attach only what you need after /clear',
360
+ labelKo: '/clear ν›„ ν•„μš”ν•œ κ²ƒλ§Œ μž¬μ²¨λΆ€',
361
+ commands: [
362
+ '/clear resets context to zero β€” cheapest option when task scope has shifted',
363
+ 'Re-read only the files Claude needs right now, not everything from before',
364
+ ],
365
+ commandsKo: [
366
+ '/clearλŠ” μ»¨ν…μŠ€νŠΈλ₯Ό μ™„μ „ μ΄ˆκΈ°ν™” β€” μž‘μ—… λ²”μœ„κ°€ λ°”λ€Œμ—ˆμ„ λ•Œ κ°€μž₯ μ €λ ΄',
367
+ 'μ§€κΈˆ ν•„μš”ν•œ 파일만 λ‹€μ‹œ 읽기, 이전 파일 μ „λΆ€ μž¬λ‘œλ“œ κΈˆμ§€',
368
+ ],
369
+ },
370
+ {
371
+ label: 'Avoid 1M context unless required',
372
+ labelKo: 'ν•„μš”ν•˜μ§€ μ•ŠμœΌλ©΄ 1M μ»¨ν…μŠ€νŠΈ λΉ„ν™œμ„±ν™”',
373
+ commands: [
374
+ 'export CLAUDE_MODEL_CONTEXT=200000 # force 200k cap',
375
+ 'Max plan auto-promotes to 1M β€” disable if you don\'t need it',
376
+ ],
377
+ commandsKo: [
378
+ 'export CLAUDE_MODEL_CONTEXT=200000 # 200k둜 κ³ μ •',
379
+ 'Max ν”Œλžœμ€ μžλ™μœΌλ‘œ 1M 승격 β€” λΆˆν•„μš”ν•˜λ©΄ λΉ„ν™œμ„±ν™”',
380
+ ],
381
+ },
382
+ ],
383
+ },
288
384
  FREQUENT_CACHE_REBUILD: {
289
385
  title: 'Cache writes outweigh cache reads',
290
386
  titleKo: 'μΊμ‹œ μ“°κΈ°κ°€ 읽기보닀 많음',
@@ -362,6 +458,14 @@ export const ISSUE_TIPS = {
362
458
  en: 'Continue one task in one session; use `/compact` at boundaries; avoid re-reading large files (use `git diff`)',
363
459
  ko: 'ν•œ μž‘μ—…μ€ ν•œ μ„Έμ…˜μ—μ„œ; 뢄기점에선 `/compact`; 큰 파일 재읽기 λŒ€μ‹  `git diff`',
364
460
  },
461
+ TTL_EXPIRY_IMMINENT: {
462
+ en: 'Send any prompt now to reset the TTL clock; or `/compact` before going idle (Pro = 5min, Max = 1h)',
463
+ ko: 'μ§€κΈˆ λ°”λ‘œ 아무 ν”„λ‘¬ν”„νŠΈλ‚˜ 보내 TTL 타이머 리셋; λ˜λŠ” 자리 λΉ„μš°κΈ° μ „ `/compact` (Pro = 5λΆ„, Max = 1μ‹œκ°„)',
464
+ },
465
+ CONTEXT_NEAR_LIMIT: {
466
+ en: '`/compact` before hitting the limit; `/clear` + re-attach only needed files; disable 1M: `export CLAUDE_MODEL_CONTEXT=200000`',
467
+ ko: 'ν•œκ³„ 도달 μ „ `/compact`; `/clear` ν›„ ν•„μš”ν•œ 파일만 μž¬μ²¨λΆ€; 1M 끄기: `export CLAUDE_MODEL_CONTEXT=200000`',
468
+ },
365
469
  };
366
470
 
367
471
  /**
@@ -377,6 +481,9 @@ export const CHIP_TO_CODES = {
377
481
  '⚠ Rebuild churn': ['FREQUENT_CACHE_REBUILD'],
378
482
  '⚠ Output heavy': ['HIGH_OUTPUT_RATIO'],
379
483
  '⚠ Call surge': ['HIGH_REQUEST_COUNT'],
484
+ '⏳ Cache expires': ['TTL_EXPIRY_IMMINENT'],
485
+ '⏳': ['TTL_EXPIRY_IMMINENT'],
486
+ 'πŸ“¦': ['CONTEXT_NEAR_LIMIT'],
380
487
  };
381
488
 
382
489
  /**
@@ -5,6 +5,22 @@ import { ISSUE_MESSAGES } from '../advice.js';
5
5
  import { formatResetIn, formatResetClock } from '../format-time.js';
6
6
  import { labelForKey } from '../window-labels.js';
7
7
 
8
+ const C = {
9
+ reset: '\x1b[0m',
10
+ bold: '\x1b[1m',
11
+ blink: '\x1b[5m',
12
+ red: '\x1b[31m',
13
+ yellow: '\x1b[33m',
14
+ cyan: '\x1b[36m',
15
+ };
16
+ // Wrap text with ANSI codes β€” gracefully strips to plain when NO_COLOR set.
17
+ const colorOk = !process.env.NO_COLOR;
18
+ const r = (s) => colorOk ? `${C.red}${s}${C.reset}` : s;
19
+ const rb = (s) => colorOk ? `${C.bold}${C.red}${s}${C.reset}` : s;
20
+ const rbl = (s) => colorOk ? `${C.blink}${C.bold}${C.red}${s}${C.reset}` : s;
21
+ const y = (s) => colorOk ? `${C.yellow}${s}${C.reset}` : s;
22
+ const b = (s) => colorOk ? `${C.bold}${s}${C.reset}` : s;
23
+
8
24
  function pad(str, len, align = 'left') {
9
25
  const s = String(str);
10
26
  if (align === 'right') return s.padStart(len);
@@ -66,11 +82,11 @@ function formatContextSize(n) {
66
82
 
67
83
  function renderSpikeSection(spikes, contextWindow) {
68
84
  const lines = [];
69
- lines.push(' ⚠ Token spike detected');
70
- lines.push(` ${'─'.repeat(50)}`);
85
+ lines.push(rbl(' ⚠ Token spike detected'));
86
+ lines.push(r(` ${'─'.repeat(50)}`));
71
87
  if (contextWindow && contextWindow.size === '1M') {
72
88
  lines.push(
73
- ` Context mode: 1M (max recent single-request input ${formatContextSize(contextWindow.maxContext)} tokens)`,
89
+ r(` Context mode: 1M (max recent single-request input ${formatContextSize(contextWindow.maxContext)} tokens)`),
74
90
  );
75
91
  lines.push('');
76
92
  }
@@ -78,18 +94,19 @@ function renderSpikeSection(spikes, contextWindow) {
78
94
  const m = spike.metrics;
79
95
  const ratioLabel = spike.ratio ? `${spike.ratio.toFixed(1)}Γ— p95` : 'single-request > 250k';
80
96
  lines.push(
81
- ` β€’ ${shortSessionId(m.sessionId)} [${m.projectDir || 'unknown'}] ` +
82
- `total input ${formatContextSize(m.totalInput)} (${ratioLabel}, ${m.requestCount} requests)`,
97
+ r(` β€’ ${shortSessionId(m.sessionId)} [${m.projectDir || 'unknown'}] `) +
98
+ rb(`total input ${formatContextSize(m.totalInput)}`) +
99
+ r(` (${ratioLabel}, ${m.requestCount} requests)`),
83
100
  );
84
101
  if (m.maxContextPerRequest > 0) {
85
102
  lines.push(
86
- ` max single-request context: ${formatContextSize(m.maxContextPerRequest)} tokens`,
103
+ r(` max single-request context: ${formatContextSize(m.maxContextPerRequest)} tokens`),
87
104
  );
88
105
  }
89
106
  for (const issue of spike.issues) {
90
107
  const info = ISSUE_MESSAGES[issue.code];
91
108
  if (!info) continue;
92
- lines.push(` Β· ${info.title}`);
109
+ lines.push(r(` Β· ${info.title}`));
93
110
  }
94
111
  lines.push('');
95
112
  }
@@ -106,22 +123,24 @@ function renderSpikeSection(spikes, contextWindow) {
106
123
  }
107
124
  }
108
125
  if (uniqueIssues.length > 0) {
109
- lines.push(' Recommended actions');
110
- lines.push(` ${'─'.repeat(50)}`);
126
+ lines.push(rb(' Recommended actions'));
127
+ lines.push(r(` ${'─'.repeat(50)}`));
111
128
  for (const issue of uniqueIssues) {
112
129
  const info = ISSUE_MESSAGES[issue.code];
113
130
  if (!info) continue;
114
- lines.push(` β–Έ ${info.title}`);
115
- lines.push(` ${info.explain}`);
131
+ lines.push(rb(` β–Έ ${info.title}`));
132
+ lines.push(r(` ${info.explain}`));
116
133
  for (const action of info.actions()) {
117
- lines.push(` - ${action.label}`);
134
+ lines.push(r(` - ${action.label}`));
118
135
  for (const cmd of action.commands) {
119
- lines.push(` ${cmd}`);
136
+ lines.push(r(` ${cmd}`));
120
137
  }
121
138
  }
122
139
  lines.push('');
123
140
  }
124
141
  }
142
+ lines.push(rbl(' β–Ά 상세 μ²˜μΉ˜λ²•: ') + rb('claude-token-saver last'));
143
+ lines.push('');
125
144
  return lines;
126
145
  }
127
146
 
@@ -132,8 +151,8 @@ function renderCapWarnSection(caps) {
132
151
  );
133
152
  if (warning.length === 0) return [];
134
153
  const lines = [];
135
- lines.push(' 🚨 Rate-limit cap is closing in');
136
- lines.push(` ${'─'.repeat(50)}`);
154
+ lines.push(rbl(' 🚨 Rate-limit cap is closing in'));
155
+ lines.push(r(` ${'─'.repeat(50)}`));
137
156
  for (const win of warning) {
138
157
  const label = labelForKey(win.key).long;
139
158
  const reset = formatResetIn(win.resetsAt);
@@ -142,12 +161,14 @@ function renderCapWarnSection(caps) {
142
161
  if (reset && clock) tail = `, resets in ${reset} (at ${clock})`;
143
162
  else if (reset) tail = `, resets in ${reset}`;
144
163
  else if (clock) tail = `, resets at ${clock}`;
145
- lines.push(` β€’ ${label}: ${Math.round(win.usedPct)}% used${tail}`);
164
+ lines.push(rb(` β€’ ${label}: ${Math.round(win.usedPct)}% used${tail}`));
146
165
  }
147
166
  lines.push('');
148
- lines.push(' Back up work before the cap hits:');
149
- lines.push(' claude-token-saver handoff');
150
- lines.push(' (writes a HANDOFF-*.md so a fresh session can pick up.)');
167
+ lines.push(r(' Back up work before the cap hits:'));
168
+ lines.push(rb(' claude-token-saver handoff'));
169
+ lines.push(r(' (writes a HANDOFF-*.md so a fresh session can pick up.)'));
170
+ lines.push('');
171
+ lines.push(rbl(' β–Ά 상세 μ²˜μΉ˜λ²•: ') + rb('claude-token-saver last'));
151
172
  lines.push('');
152
173
  return lines;
153
174
  }