claude-token-saver 2.7.0 → 2.7.1

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/package.json CHANGED
@@ -1,10 +1,10 @@
1
1
  {
2
2
  "name": "claude-token-saver",
3
- "version": "2.7.0",
3
+ "version": "2.7.1",
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
  }