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 +5 -3
- package/package.json +3 -3
- package/src/advice.js +107 -0
- package/src/formatters/table.js +40 -19
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
|
-
> πΊ
|
|
6
|
-
> **[βΆ Watch the Short](https://www.youtube.com/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
|
-
> [](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.
|
|
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": "
|
|
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
|
/**
|
package/src/formatters/table.js
CHANGED
|
@@ -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)}
|
|
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
|
}
|