claude-token-saver 2.1.0 → 2.5.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 +50 -23
- package/bin/cli.js +285 -3
- package/package.json +1 -1
- package/src/advice.js +120 -0
- package/src/caps-cache.js +84 -0
- package/src/demo.js +73 -1
- package/src/format-time.js +44 -0
- package/src/formatters/statusline.js +214 -14
- package/src/formatters/table.js +36 -2
- package/src/handoff.js +161 -0
- package/src/history.js +208 -8
- package/src/installer.js +55 -30
- package/src/window-labels.js +61 -0
- package/examples/statusline-with-rz1989s.sh +0 -52
package/src/history.js
CHANGED
|
@@ -3,6 +3,11 @@
|
|
|
3
3
|
* (none → warning, warning A → warning B, warning → none). One markdown file
|
|
4
4
|
* per calendar day so users can pinpoint "when did this start" easily.
|
|
5
5
|
*
|
|
6
|
+
* Each event is written bilingually: the canonical English line first, the
|
|
7
|
+
* Korean translation as an indented "└" continuation right below. The chip
|
|
8
|
+
* text itself stays as-is (its symbol+English is part of the UX surface), but
|
|
9
|
+
* the diagnostic detail and resolved-status verbs are translated.
|
|
10
|
+
*
|
|
6
11
|
* Storage path is platform-aware (see paths.userDataDir):
|
|
7
12
|
* Windows: %APPDATA%\claude-token-saver\history\YYYY-MM-DD.md
|
|
8
13
|
* macOS: ~/Library/Application Support/claude-token-saver/history/YYYY-MM-DD.md
|
|
@@ -15,6 +20,9 @@
|
|
|
15
20
|
import { readFileSync, writeFileSync, mkdirSync, existsSync, readdirSync } from 'node:fs';
|
|
16
21
|
import { join } from 'node:path';
|
|
17
22
|
import { userDataDir } from './paths.js';
|
|
23
|
+
import { formatResetIn, formatResetClock } from './format-time.js';
|
|
24
|
+
import { labelForKey } from './window-labels.js';
|
|
25
|
+
import { ISSUE_TIPS, CHIP_TO_CODES, CAP_TIPS } from './advice.js';
|
|
18
26
|
|
|
19
27
|
const BASE_DIR = userDataDir();
|
|
20
28
|
const HISTORY_DIR = join(BASE_DIR, 'history');
|
|
@@ -53,14 +61,116 @@ function saveState(state) {
|
|
|
53
61
|
writeFileSync(STATE_PATH, JSON.stringify(state) + '\n');
|
|
54
62
|
}
|
|
55
63
|
|
|
56
|
-
|
|
64
|
+
/**
|
|
65
|
+
* Map a chip's English label to its Korean equivalent.
|
|
66
|
+
* Returns the input unchanged if no mapping is registered (forward-compat
|
|
67
|
+
* with chips added in advice.js after this map was last updated).
|
|
68
|
+
*/
|
|
69
|
+
function chipKo(chip) {
|
|
70
|
+
if (!chip) return chip;
|
|
71
|
+
const map = {
|
|
72
|
+
'⚠ 1M ON': '⚠ 1M 컨텍스트 활성',
|
|
73
|
+
'⚠ Cache miss': '⚠ 캐시 미스',
|
|
74
|
+
'⚠ Rebuild churn': '⚠ 캐시 재빌드 빈발',
|
|
75
|
+
'⚠ Input spike': '⚠ 입력 급증',
|
|
76
|
+
'⚠ Output heavy': '⚠ 출력 과다',
|
|
77
|
+
'⚠ Call surge': '⚠ 호출 급증',
|
|
78
|
+
'⚠ 5m TTL': '⚠ 5분 TTL',
|
|
79
|
+
'⏳ Cache expires': '⏳ 캐시 만료 임박',
|
|
80
|
+
'💰 Cache saved': '💰 캐시 절약',
|
|
81
|
+
'🧠 Cache hit': '🧠 캐시 적중',
|
|
82
|
+
};
|
|
83
|
+
return map[chip] || chip;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Translate the diagnostic detail string to Korean. The detail is constructed
|
|
88
|
+
* in cli.js and follows two stable shapes:
|
|
89
|
+
* "Context auto-promoted to 1M (max single-request {N}k tokens)"
|
|
90
|
+
* "session {ID}: CODE_A, CODE_B"
|
|
91
|
+
* Anything else falls through unchanged.
|
|
92
|
+
*/
|
|
93
|
+
function detailKo(detail) {
|
|
94
|
+
if (!detail) return detail;
|
|
95
|
+
const m1 = detail.match(/^Context auto-promoted to 1M \(max single-request (\d+)k tokens\)$/);
|
|
96
|
+
if (m1) return `1M 컨텍스트 자동 활성 (단일 요청 최대 ${m1[1]}k 토큰)`;
|
|
97
|
+
const m2 = detail.match(/^session ([^:]+): (.+)$/);
|
|
98
|
+
if (m2) {
|
|
99
|
+
const codeKo = {
|
|
100
|
+
LOW_HIT_RATE: '캐시 적중률 낮음',
|
|
101
|
+
FREQUENT_CACHE_REBUILD: '캐시 재빌드 빈발',
|
|
102
|
+
OUTPUT_HEAVY: '출력 과다',
|
|
103
|
+
INPUT_SPIKE: '입력 급증',
|
|
104
|
+
CALL_SURGE: '호출 급증',
|
|
105
|
+
TTL_5M: '5분 TTL',
|
|
106
|
+
};
|
|
107
|
+
const codes = m2[2]
|
|
108
|
+
.split(',')
|
|
109
|
+
.map((c) => c.trim())
|
|
110
|
+
.map((c) => codeKo[c] || c)
|
|
111
|
+
.join(', ');
|
|
112
|
+
return `세션 ${m2[1]}: ${codes}`;
|
|
113
|
+
}
|
|
114
|
+
return detail;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Resolve the diagnostic codes that apply to a transition. We try, in order:
|
|
119
|
+
* 1. `detail` of shape "session ID: CODE_A, CODE_B" (chip transitions with
|
|
120
|
+
* explicit per-session codes — the richest source).
|
|
121
|
+
* 2. `chip` text mapped via CHIP_TO_CODES (covers 1M ON and chips that fire
|
|
122
|
+
* without a per-session detail).
|
|
123
|
+
* Returns an array of unique codes, possibly empty.
|
|
124
|
+
*/
|
|
125
|
+
function codesForEvent(chip, detail) {
|
|
126
|
+
const out = [];
|
|
127
|
+
if (detail) {
|
|
128
|
+
const m = detail.match(/^session [^:]+:\s*(.+)$/);
|
|
129
|
+
if (m) {
|
|
130
|
+
for (const c of m[1].split(',').map((s) => s.trim()).filter(Boolean)) {
|
|
131
|
+
if (!out.includes(c)) out.push(c);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
if (chip && CHIP_TO_CODES[chip]) {
|
|
136
|
+
for (const c of CHIP_TO_CODES[chip]) if (!out.includes(c)) out.push(c);
|
|
137
|
+
}
|
|
138
|
+
return out;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Build the "💡 ..." tip block for a given chip+detail pair. Returns
|
|
143
|
+
* { en: string, ko: string } where each may be empty if no tips apply
|
|
144
|
+
* (resolution events, unknown chips, etc.).
|
|
145
|
+
*/
|
|
146
|
+
function tipsForEvent(chip, detail) {
|
|
147
|
+
const codes = codesForEvent(chip, detail);
|
|
148
|
+
const enLines = [];
|
|
149
|
+
const koLines = [];
|
|
150
|
+
for (const code of codes) {
|
|
151
|
+
const tip = ISSUE_TIPS[code];
|
|
152
|
+
if (!tip) continue;
|
|
153
|
+
enLines.push(` 💡 ${tip.en}`);
|
|
154
|
+
koLines.push(` 💡 ${tip.ko}`);
|
|
155
|
+
}
|
|
156
|
+
return { en: enLines.join('\n'), ko: koLines.join('\n') };
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function appendDayLine(en, ko, date = new Date(), tips = null) {
|
|
57
160
|
ensureDir(HISTORY_DIR);
|
|
58
161
|
const path = join(HISTORY_DIR, `${ymd(date)}.md`);
|
|
162
|
+
// Bilingual event line. Tip lines (when present) follow on their own lines so
|
|
163
|
+
// the file reads as: event-en / └ event-ko / 💡 tip-en / 💡 tip-ko.
|
|
164
|
+
let block = ko && ko !== en ? `${en}\n └ ${ko}\n` : `${en}\n`;
|
|
165
|
+
if (tips && tips.en) block += `${tips.en}\n`;
|
|
166
|
+
if (tips && tips.ko) block += `${tips.ko}\n`;
|
|
59
167
|
if (!existsSync(path)) {
|
|
60
|
-
|
|
168
|
+
const header = `# Token Monitor / 토큰 모니터 — ${ymd(date)}\n\n## Events / 이벤트\n`;
|
|
169
|
+
writeFileSync(path, header + block);
|
|
61
170
|
} else {
|
|
62
171
|
const existing = readFileSync(path, 'utf8');
|
|
63
|
-
|
|
172
|
+
const sep = existing.endsWith('\n') ? '' : '\n';
|
|
173
|
+
writeFileSync(path, existing + sep + block);
|
|
64
174
|
}
|
|
65
175
|
}
|
|
66
176
|
|
|
@@ -77,16 +187,28 @@ export function recordChip(chip, contextHints = {}) {
|
|
|
77
187
|
|
|
78
188
|
if (current === last) return false;
|
|
79
189
|
|
|
80
|
-
|
|
190
|
+
const detail = contextHints.detail || null;
|
|
191
|
+
const detailEn = detail ? ` — ${detail}` : '';
|
|
192
|
+
const detailKr = detail ? ` — ${detailKo(detail)}` : '';
|
|
193
|
+
|
|
194
|
+
let en, ko;
|
|
195
|
+
// Tips only on warning entry/transition (not resolution) — based on the
|
|
196
|
+
// *new* chip so users see how to handle what's currently active.
|
|
197
|
+
let tips = null;
|
|
81
198
|
if (current && !last) {
|
|
82
|
-
|
|
199
|
+
en = `- ${hms(now)} ${current}${detailEn}`;
|
|
200
|
+
ko = `${chipKo(current)}${detailKr}`;
|
|
201
|
+
tips = tipsForEvent(current, detail);
|
|
83
202
|
} else if (current && last) {
|
|
84
|
-
|
|
203
|
+
en = `- ${hms(now)} ${last} → ${current}${detailEn}`;
|
|
204
|
+
ko = `${chipKo(last)} → ${chipKo(current)}${detailKr}`;
|
|
205
|
+
tips = tipsForEvent(current, detail);
|
|
85
206
|
} else {
|
|
86
207
|
// current === null, last was something — warning resolved
|
|
87
|
-
|
|
208
|
+
en = `- ${hms(now)} ✓ resolved (was ${last})`;
|
|
209
|
+
ko = `✓ 해소됨 (이전: ${chipKo(last)})`;
|
|
88
210
|
}
|
|
89
|
-
appendDayLine(
|
|
211
|
+
appendDayLine(en, ko, now, tips);
|
|
90
212
|
saveState({ chip: current, ts: now.toISOString() });
|
|
91
213
|
return true;
|
|
92
214
|
}
|
|
@@ -110,6 +232,84 @@ export function readRecent(days = 7) {
|
|
|
110
232
|
return out;
|
|
111
233
|
}
|
|
112
234
|
|
|
235
|
+
/**
|
|
236
|
+
* Record entering or exiting the cap-warn (>=90%) zone for a rate-limit
|
|
237
|
+
* window. Each window (keyed by its stdin name — e.g. `five_hour`, `seven_day`)
|
|
238
|
+
* has its own dedup slot, so the daily file gets two transitions max per
|
|
239
|
+
* window per warning episode.
|
|
240
|
+
*
|
|
241
|
+
* @param {{ key: string, usedPct: number, resetsAt: number|null } | null} window
|
|
242
|
+
* @returns {boolean} true when a line was appended
|
|
243
|
+
*/
|
|
244
|
+
export function recordCapTransition(window) {
|
|
245
|
+
if (!window || typeof window.key !== 'string') return false;
|
|
246
|
+
const state = loadState();
|
|
247
|
+
const slotKey = `cap_${window.key}`;
|
|
248
|
+
const wasWarn = !!state[slotKey];
|
|
249
|
+
const isWarn = Number.isFinite(window.usedPct) && window.usedPct >= 90;
|
|
250
|
+
if (wasWarn === isWarn) return false;
|
|
251
|
+
|
|
252
|
+
const now = new Date();
|
|
253
|
+
const labels = labelForKey(window.key);
|
|
254
|
+
const labelEn = labels.short;
|
|
255
|
+
// Korean labels map only the well-known windows; everything else falls back
|
|
256
|
+
// to the English short label (still readable for the bilingual line).
|
|
257
|
+
const KO_OVERRIDES = {
|
|
258
|
+
five_hour: '5시간 윈도',
|
|
259
|
+
seven_day: '7일 윈도',
|
|
260
|
+
seven_day_sonnet: '7일 윈도 (Sonnet)',
|
|
261
|
+
seven_day_opus: '7일 윈도 (Opus)',
|
|
262
|
+
};
|
|
263
|
+
const labelKo = KO_OVERRIDES[window.key] || labels.short;
|
|
264
|
+
let en;
|
|
265
|
+
let ko;
|
|
266
|
+
if (isWarn) {
|
|
267
|
+
const pct = Math.round(window.usedPct);
|
|
268
|
+
const reset = formatResetIn(window.resetsAt, now);
|
|
269
|
+
const clock = formatResetClock(window.resetsAt, now);
|
|
270
|
+
const tail = reset && clock
|
|
271
|
+
? ` (resets in ${reset}, at ${clock})`
|
|
272
|
+
: reset
|
|
273
|
+
? ` (resets in ${reset})`
|
|
274
|
+
: clock
|
|
275
|
+
? ` (resets at ${clock})`
|
|
276
|
+
: '';
|
|
277
|
+
const tailKo = reset && clock
|
|
278
|
+
? ` (${clock}에 리셋, 남은 ${reset})`
|
|
279
|
+
: reset
|
|
280
|
+
? ` (리셋까지 ${reset})`
|
|
281
|
+
: clock
|
|
282
|
+
? ` (${clock}에 리셋)`
|
|
283
|
+
: '';
|
|
284
|
+
en = `- ${hms(now)} 🚨 ${labelEn} ${pct}% cap warning${tail}`;
|
|
285
|
+
ko = `🚨 ${labelKo} ${pct}% 캡 경고${tailKo}`;
|
|
286
|
+
} else {
|
|
287
|
+
en = `- ${hms(now)} ✓ ${labelEn} cap warning resolved`;
|
|
288
|
+
ko = `✓ ${labelKo} 캡 경고 해소`;
|
|
289
|
+
}
|
|
290
|
+
// Cap-warn entry → handoff tip; resolution → no tip (just the ✓ line).
|
|
291
|
+
const capTips = isWarn ? { en: ` 💡 ${CAP_TIPS.en}`, ko: ` 💡 ${CAP_TIPS.ko}` } : null;
|
|
292
|
+
appendDayLine(en, ko, now, capTips);
|
|
293
|
+
state[slotKey] = isWarn;
|
|
294
|
+
saveState(state);
|
|
295
|
+
return true;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
/**
|
|
299
|
+
* Record a handoff write — invoked by the `handoff` subcommand so
|
|
300
|
+
* `claude-token-saver history` shows when work was backed up to a HANDOFF file.
|
|
301
|
+
*
|
|
302
|
+
* @param {string} filePath
|
|
303
|
+
* @returns {boolean}
|
|
304
|
+
*/
|
|
305
|
+
export function recordHandoff(filePath) {
|
|
306
|
+
const now = new Date();
|
|
307
|
+
const en = `- ${hms(now)} 📝 handoff written: ${filePath}`;
|
|
308
|
+
const ko = `📝 핸드오프 백업 작성: ${filePath}`;
|
|
309
|
+
appendDayLine(en, ko, now);
|
|
310
|
+
return true;
|
|
311
|
+
}
|
|
312
|
+
|
|
113
313
|
/**
|
|
114
314
|
* List all available history file dates (sorted newest first).
|
|
115
315
|
*/
|
package/src/installer.js
CHANGED
|
@@ -15,7 +15,7 @@ import { claudeUserDir } from './paths.js';
|
|
|
15
15
|
|
|
16
16
|
const SKILL_BODY = `---
|
|
17
17
|
name: claude-token-saver
|
|
18
|
-
description: Use when the user mentions Claude Code token usage, prompt cache hit rate, TTL/expiry, the 1M context window, cache misses, output spikes, or anything in the statusline produced by claude-token-saver (chips like "⚠ 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
|
|
18
|
+
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\`.
|
|
19
19
|
---
|
|
20
20
|
|
|
21
21
|
# claude-token-saver — Claude Code Token Monitor
|
|
@@ -26,28 +26,38 @@ countdown, savings, and (when relevant) a leading warning chip.
|
|
|
26
26
|
|
|
27
27
|
## When this skill should activate
|
|
28
28
|
|
|
29
|
-
- The user references any chip wording:
|
|
30
|
-
\`⚠
|
|
31
|
-
\`⚠ Call surge\`.
|
|
29
|
+
- The user references any chip wording: \`🚨 5H NN%\`, \`🚨 7D NN%\`,
|
|
30
|
+
\`⚠ 1M ON\`, \`⚠ Input spike\`, \`⚠ Cache miss\`, \`⚠ 5m TTL\`,
|
|
31
|
+
\`⚠ Rebuild churn\`, \`⚠ Output heavy\`, \`⚠ Call surge\`.
|
|
32
32
|
- The user asks "why is my cache hit rate low", "what does this warning mean",
|
|
33
33
|
"when did this start happening", or similar.
|
|
34
|
+
- The user is approaching a rate-limit cap and wants to back up the current
|
|
35
|
+
work so a fresh session can continue (point them at
|
|
36
|
+
\`claude-token-saver handoff\`).
|
|
34
37
|
- The user wants to see the token-usage history file or asks for a summary
|
|
35
38
|
of recent warnings.
|
|
36
39
|
|
|
37
40
|
## What to do
|
|
38
41
|
|
|
39
|
-
1. **
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
42
|
+
1. **Lead with the most recent warning + how to handle it.** Run
|
|
43
|
+
\`claude-token-saver last\` first. It returns the most recent warning event
|
|
44
|
+
(chip + detail + timestamp) plus the full advice block for it. Surface that
|
|
45
|
+
to the user before anything else — this is what they came for.
|
|
46
|
+
2. **Identify the chip.** If the user pasted a statusline (instead of relying
|
|
47
|
+
on \`last\`), pull out the leading \`⚠ ...\` chip. That maps to a specific
|
|
48
|
+
issue category.
|
|
49
|
+
3. **Show recent history.** Run \`claude-token-saver history\` (default last 7
|
|
50
|
+
days) to see the chronology of warning transitions. Each entry is timestamped,
|
|
51
|
+
bilingual (English line + 한국어), and includes a \`💡\` action tip inline.
|
|
52
|
+
4. **Drill down on the live state.** Run \`claude-token-saver --days 1\` (or
|
|
45
53
|
another window) to render the full table view, which lists per-session
|
|
46
54
|
spikes and recommended actions.
|
|
47
|
-
|
|
55
|
+
5. **Explain the warning** in plain language. Use the chip → cause table:
|
|
48
56
|
|
|
49
57
|
| Chip | Likely cause |
|
|
50
58
|
| ------------------ | ----------------------------------------------------- |
|
|
59
|
+
| \`🚨 5H NN%\` | 5-hour rate-limit window at NN% (>=90%). Cap is imminent. |
|
|
60
|
+
| \`🚨 7D NN%\` | 7-day rate-limit window at NN% (>=90%). Pace yourself. |
|
|
51
61
|
| \`⚠ 1M ON\` | Auto-promoted to 1M context (Opus 4.7+ Max default). |
|
|
52
62
|
| \`⚠ Input spike\` | One request consumed >250k or >3× the recent p95. |
|
|
53
63
|
| \`⚠ Cache miss\` | Cache hit rate dropped below ~70%. |
|
|
@@ -56,17 +66,24 @@ countdown, savings, and (when relevant) a leading warning chip.
|
|
|
56
66
|
| \`⚠ Output heavy\` | Output ratio dominates input — inspect long generations. |
|
|
57
67
|
| \`⚠ Call surge\` | Request count is well above baseline. |
|
|
58
68
|
|
|
59
|
-
|
|
60
|
-
\`
|
|
61
|
-
|
|
62
|
-
|
|
69
|
+
6. **Suggest the next action.** For \`🚨 5H/7D\` chips, recommend running
|
|
70
|
+
\`claude-token-saver handoff\` to back up the current work to a
|
|
71
|
+
\`HANDOFF-*.md\` file before the cap hits, then continue in a fresh
|
|
72
|
+
session. For 1M ON, mention \`CLAUDE_CODE_DISABLE_1M_CONTEXT=1\`. For
|
|
73
|
+
5m TTL, point at the Max plan's 1h bucket. For input spike, suggest
|
|
74
|
+
splitting the conversation or compacting context.
|
|
63
75
|
|
|
64
76
|
## Useful commands
|
|
65
77
|
|
|
78
|
+
- \`claude-token-saver last\` — most recent warning + full advice (start here).
|
|
79
|
+
- \`claude-token-saver last --days 7\` — widen the lookback window.
|
|
66
80
|
- \`claude-token-saver\` — full table report (default last 1 day).
|
|
67
81
|
- \`claude-token-saver --days 7\` — wider window.
|
|
68
|
-
- \`claude-token-saver history\` — recent warning transitions per day
|
|
82
|
+
- \`claude-token-saver history\` — recent warning transitions per day, with
|
|
83
|
+
inline \`💡\` action tips.
|
|
69
84
|
- \`claude-token-saver history --days 30\` — longer history.
|
|
85
|
+
- \`claude-token-saver handoff\` — write a HANDOFF-*.md template in cwd
|
|
86
|
+
capturing git status + cap snapshot, so a fresh session can resume cleanly.
|
|
70
87
|
- \`claude-token-saver mode\` — show statusline preferences.
|
|
71
88
|
- \`claude-token-saver mode icon verbose 1d\` — change preferences.
|
|
72
89
|
|
|
@@ -85,23 +102,31 @@ description: Show recent claude-token-saver warning history and a fresh report.
|
|
|
85
102
|
---
|
|
86
103
|
|
|
87
104
|
You are responding to the \`/token-monitor\` slash command. The user wants a
|
|
88
|
-
quick read of their Claude Code token usage and any active warnings
|
|
105
|
+
quick read of their Claude Code token usage and any active warnings — most
|
|
106
|
+
importantly: **what just happened, and how do I handle it?**
|
|
89
107
|
|
|
90
108
|
Steps:
|
|
91
109
|
|
|
92
|
-
1. Run \`claude-token-saver
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
110
|
+
1. **Lead with the most recent warning.** Run \`claude-token-saver last\`
|
|
111
|
+
first and surface its output verbatim (or lightly summarized) at the top
|
|
112
|
+
of your reply. This returns the latest warning event (chip + detail +
|
|
113
|
+
timestamp) followed by the full advice block. If \`last\` says no recent
|
|
114
|
+
warnings, mention that and skip ahead — you can stop here unless the user
|
|
115
|
+
asked for more.
|
|
116
|
+
2. Run \`claude-token-saver history --days 7\` and capture the output. Use it
|
|
117
|
+
only to add context — e.g. "this is the 3rd cache miss today" — not to
|
|
118
|
+
re-print the whole file. Each entry is bilingual and includes a \`💡\`
|
|
119
|
+
action tip inline.
|
|
120
|
+
3. Run \`claude-token-saver --days 1\` and capture the output for any extra
|
|
121
|
+
color you want to add: TTL breakdown, cost impact, daily trend, or active
|
|
122
|
+
spikes. Skip if step 1 already covered what the user needs.
|
|
123
|
+
4. Summarize for the user:
|
|
124
|
+
- **What just fired** — the chip + the time + a sentence on what caused it
|
|
125
|
+
(from \`last\`).
|
|
126
|
+
- **What to do** — the action tip from \`last\`. For cap-warn (\`🚨 5H/7D NN%\`),
|
|
127
|
+
surface \`claude-token-saver handoff\` prominently so they can back up
|
|
128
|
+
state before the cap blocks them.
|
|
129
|
+
- **Today's pattern** (optional) — when warnings cluster in time, mention it.
|
|
105
130
|
|
|
106
131
|
Keep the summary to ~10 lines. The user can re-run the underlying commands
|
|
107
132
|
themselves for the full output.
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Friendly labels + icons for `rate_limits.*` keys from Claude Code's stdin
|
|
3
|
+
* payload. Known keys (`five_hour`, `seven_day`) get curated short/long labels
|
|
4
|
+
* and dedicated icons. Unknown keys are passed through with a derived label so
|
|
5
|
+
* any future window Anthropic adds (e.g. `seven_day_sonnet`) renders without
|
|
6
|
+
* a code change.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
// `short` is used by the cap-warn chip and history records (stable shape so
|
|
10
|
+
// parsers/dedup keep working). `usageLabel` is the friendlier word that the
|
|
11
|
+
// always-on usage segment renders next to the icon — empty string means the
|
|
12
|
+
// icon alone carries the meaning (5H 'session/now' is the implicit default).
|
|
13
|
+
const KNOWN = {
|
|
14
|
+
// ✦ reads as "AI token unit" (Anthropic/OpenAI/Gemini sparkle motif), more
|
|
15
|
+
// on-theme than the 🪙 coin which suggested in-app currency. Same width as
|
|
16
|
+
// 📅 in monospace terminals so the chip alignment stays stable.
|
|
17
|
+
// 'current' mirrors how `weekly` reads next to 7D — names the window in
|
|
18
|
+
// plain English so a glance at `✦ current ████▒░ 72%` tells the eye what's
|
|
19
|
+
// being measured without parsing the icon's meaning.
|
|
20
|
+
five_hour: { short: '5H', long: 'Current session', icon: '✦', usageLabel: 'current' },
|
|
21
|
+
seven_day: { short: '7D', long: 'Current week', icon: '📅', usageLabel: 'weekly' },
|
|
22
|
+
// Speculative — `/usage` shows a Sonnet-only weekly bucket, so if Anthropic
|
|
23
|
+
// ever surfaces it on stdin we render with a sensible default already.
|
|
24
|
+
seven_day_sonnet: { short: '7D-S', long: 'Current week (Sonnet)', icon: '🅂', usageLabel: 'weekly (Sonnet)' },
|
|
25
|
+
seven_day_opus: { short: '7D-O', long: 'Current week (Opus)', icon: '🅾', usageLabel: 'weekly (Opus)' },
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
function deriveShort(key) {
|
|
29
|
+
// "five_hour" → "5H"; "seven_day_sonnet" → "7DS"; arbitrary key → uppercase initials
|
|
30
|
+
const m = key.match(/^(\d+)_?([a-z]+)/);
|
|
31
|
+
if (m) {
|
|
32
|
+
const num = m[1];
|
|
33
|
+
const word = m[2];
|
|
34
|
+
const letter = word.charAt(0).toUpperCase();
|
|
35
|
+
const tail = key.slice(m[0].length);
|
|
36
|
+
const suffix = tail
|
|
37
|
+
.split('_')
|
|
38
|
+
.filter(Boolean)
|
|
39
|
+
.map((s) => s.charAt(0).toUpperCase())
|
|
40
|
+
.join('');
|
|
41
|
+
return `${num}${letter}${suffix}`;
|
|
42
|
+
}
|
|
43
|
+
return key
|
|
44
|
+
.split('_')
|
|
45
|
+
.filter(Boolean)
|
|
46
|
+
.map((s) => s.charAt(0).toUpperCase())
|
|
47
|
+
.join('') || key;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function deriveLong(key) {
|
|
51
|
+
return key
|
|
52
|
+
.split('_')
|
|
53
|
+
.map((w) => (w ? w[0].toUpperCase() + w.slice(1) : w))
|
|
54
|
+
.join(' ');
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function labelForKey(key) {
|
|
58
|
+
if (KNOWN[key]) return KNOWN[key];
|
|
59
|
+
const short = deriveShort(key);
|
|
60
|
+
return { short, long: deriveLong(key), icon: '⏱', usageLabel: short };
|
|
61
|
+
}
|
|
@@ -1,52 +0,0 @@
|
|
|
1
|
-
#!/bin/sh
|
|
2
|
-
# Combine rz1989s/claude-code-statusline (rich layout: repo, cost, MCP, prayer times)
|
|
3
|
-
# with claude-token-saver (cache hit rate, TTL countdown, 1M-context detection,
|
|
4
|
-
# spike diagnosis). The two projects don't overlap — rz1989s runs first, our
|
|
5
|
-
# cache chip is appended as the final segment.
|
|
6
|
-
#
|
|
7
|
-
# Install:
|
|
8
|
-
# 1) Follow rz1989s install instructions so bash ~/.claude/statusline.sh works:
|
|
9
|
-
# https://github.com/rz1989s/claude-code-statusline
|
|
10
|
-
# 2) npm install -g claude-token-saver (or rely on npx — fallback below)
|
|
11
|
-
# 3) Save this file as: ~/.claude/statusline-with-rz1989s.sh
|
|
12
|
-
# chmod +x ~/.claude/statusline-with-rz1989s.sh
|
|
13
|
-
# 4) In ~/.claude/settings.json:
|
|
14
|
-
# {
|
|
15
|
-
# "statusLine": {
|
|
16
|
-
# "type": "command",
|
|
17
|
-
# "command": "bash ~/.claude/statusline-with-rz1989s.sh",
|
|
18
|
-
# "refreshInterval": 1
|
|
19
|
-
# }
|
|
20
|
-
# }
|
|
21
|
-
#
|
|
22
|
-
# refreshInterval: 1 keeps our TTL countdown ticking while you're idle.
|
|
23
|
-
# Drop to 2 or 5 for lower local CPU if your rz1989s config does heavy work.
|
|
24
|
-
|
|
25
|
-
# Claude Code sends the session JSON on stdin. Both tools want to read it,
|
|
26
|
-
# so we buffer it and tee to each.
|
|
27
|
-
input=$(cat)
|
|
28
|
-
|
|
29
|
-
# --- 1) rz1989s layout (if installed) ---
|
|
30
|
-
RZ_STATUSLINE="${CLAUDE_RZ_STATUSLINE:-$HOME/.claude/statusline.sh}"
|
|
31
|
-
if [ -f "$RZ_STATUSLINE" ]; then
|
|
32
|
-
printf '%s' "$input" | bash "$RZ_STATUSLINE"
|
|
33
|
-
# Separator between the two tools. Dim pipe.
|
|
34
|
-
printf ' \033[90m|\033[00m '
|
|
35
|
-
fi
|
|
36
|
-
|
|
37
|
-
# --- 2) claude-token-saver ---
|
|
38
|
-
# Pass --exclude-session so the current session's tool calls don't reset the
|
|
39
|
-
# TTL countdown. The path comes from the session JSON if present.
|
|
40
|
-
session_path=$(printf '%s' "$input" | sed -n 's/.*"path"[[:space:]]*:[[:space:]]*"\([^"]*\.jsonl\)".*/\1/p' | head -n1)
|
|
41
|
-
exclude_flag=""
|
|
42
|
-
if [ -n "$session_path" ]; then
|
|
43
|
-
exclude_flag="--exclude-session $session_path"
|
|
44
|
-
fi
|
|
45
|
-
|
|
46
|
-
if command -v claude-token-saver >/dev/null 2>&1; then
|
|
47
|
-
# shellcheck disable=SC2086
|
|
48
|
-
claude-token-saver --statusline --icon $exclude_flag 2>/dev/null || true
|
|
49
|
-
else
|
|
50
|
-
# shellcheck disable=SC2086
|
|
51
|
-
npx --yes claude-token-saver@latest --statusline --icon $exclude_flag 2>/dev/null || true
|
|
52
|
-
fi
|