claude-token-saver 2.2.0 → 2.6.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 +25 -6
- package/bin/cli.js +210 -51
- package/package.json +1 -1
- package/src/advice.js +120 -0
- package/src/caps-cache.js +46 -13
- package/src/demo.js +73 -1
- package/src/format-time.js +44 -0
- package/src/formatters/statusline.js +176 -38
- package/src/formatters/table.js +15 -21
- package/src/handoff.js +19 -20
- package/src/history.js +96 -31
- package/src/installer.js +35 -50
- package/src/window-labels.js +61 -0
package/README.md
CHANGED
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
**Why I built this.** I'm on the Max plan. On Opus 4.6 I never hit the *current-session* cap. After Opus 4.7 rolled out, I started hitting it on the same workflow — repeatedly. The official token statistics didn't match what I was actually feeling, and Claude Code's UI doesn't show prompt-cache health. This tool is what let me see *why*: low cache hit rate, 5m TTL writes that should have been 1h, 1M context auto-promoted in the background.
|
|
13
13
|
|
|
14
14
|
v2.1 (2026-04) adds the workflow that follows the diagnosis:
|
|
15
|
-
- **`claude-token-saver install`** — one command writes a Claude Code Skill
|
|
15
|
+
- **`claude-token-saver install`** — one command writes a Claude Code Skill that auto-activates when you mention "cache hit rate" / "1M context" / "5H cap" — no slash command needed.
|
|
16
16
|
- **`claude-token-saver history`** — every warning chip transition is auto-logged to a daily Markdown file, so you can answer "when did this start" without grepping logs.
|
|
17
17
|
- **Cross-platform paths** — Windows (`%APPDATA%`), macOS (`~/Library/Application Support`), Linux (`~/.config` / XDG) all handled.
|
|
18
18
|
|
|
@@ -208,11 +208,12 @@ One command wires up everything else this README mentions:
|
|
|
208
208
|
claude-token-saver install
|
|
209
209
|
```
|
|
210
210
|
|
|
211
|
-
This writes
|
|
212
|
-
- `~/.claude/skills/claude-token-saver/SKILL.md` — auto-activates whenever you mention chip wording ("⚠ 1M ON", "cache miss", etc.) or ask
|
|
213
|
-
- `~/.claude/commands/token-monitor.md` — adds a `/token-monitor` slash command that runs `claude-token-saver history` + a fresh report and summarizes both for you.
|
|
211
|
+
This writes one file under your Claude user dir:
|
|
212
|
+
- `~/.claude/skills/claude-token-saver/SKILL.md` — auto-activates whenever you mention chip wording ("⚠ 1M ON", "cache miss", "5H cap", etc.) or ask for a token report. Claude Code will then read `claude-token-saver last`, drill into history if needed, and explain the warning.
|
|
214
213
|
|
|
215
|
-
|
|
214
|
+
If you previously installed v2.5.x or earlier, `install` also removes the now-redundant legacy `~/.claude/commands/token-monitor.md` slash command — its workflow is fully absorbed into the skill (same behavior, triggered by intent rather than typing `/token-monitor`).
|
|
215
|
+
|
|
216
|
+
Re-run with `--force` to overwrite the skill file.
|
|
216
217
|
|
|
217
218
|
## Warning history (`history`) — new in v2.1
|
|
218
219
|
|
|
@@ -242,6 +243,24 @@ Storage paths (cross-platform):
|
|
|
242
243
|
|
|
243
244
|
Each day's file is plain Markdown — open it in any editor. Transitions are deduped, so the 1Hz statusline refresh doesn't spam.
|
|
244
245
|
|
|
246
|
+
## Model + /usage segments (new in v2.3, generic in v2.4)
|
|
247
|
+
|
|
248
|
+
Three more segments mirror the data Claude Code's `/usage` slash command shows, so you don't have to slash for it every few minutes:
|
|
249
|
+
|
|
250
|
+
| Segment | Icon mode example | Source |
|
|
251
|
+
| ----------- | --------------------- | ------------------------------- |
|
|
252
|
+
| `model` | `🤖 Opus 4.7` | stdin `model.display_name` |
|
|
253
|
+
| `five_hour` | `✦ current ███▒░░ 47% 🔄 21:10` | stdin `rate_limits.five_hour` |
|
|
254
|
+
| `seven_day` | `📅 weekly ▒░░░░░ 9% 🔄 Thu 13:00` | stdin `rate_limits.seven_day` |
|
|
255
|
+
|
|
256
|
+
The window segments stay quiet under 70% (calm emerald), warm to amber at 70–89%, and yield to the leading `🚨 5H █████▓ 94%` / `🚨 7D █████▒ 92%` cap-warn chip at 90%+ — so you never see the same window twice. The gauge is 6 cells wide using a single density family (`█▓▒░`), so the fill→empty boundary reads as one smooth gradient instead of an awkward step between fractional and shaded glyphs. Colors render as a Tailwind-inspired muted palette (emerald-400 / amber-400 / rose-400) on truecolor terminals (`COLORTERM=truecolor`), with a graceful fallback to 8-color ANSI elsewhere. Filter the layout with `--segments=` if you only want a subset:
|
|
257
|
+
|
|
258
|
+
```bash
|
|
259
|
+
claude-token-saver --statusline --icon --segments=model,five_hour,seven_day,saved
|
|
260
|
+
```
|
|
261
|
+
|
|
262
|
+
`5h` and `7d` are kept as aliases for the older config files. Any new `rate_limits.*` window Anthropic ships (e.g. a Sonnet-only weekly bucket) renders automatically with a derived label — no config or version bump needed. As of 2026-04-25 the stdin contract exposes only `five_hour` + `seven_day`; the third row in `/usage` ("Current week — Sonnet only") is not in the payload yet, so we mirror what's there.
|
|
263
|
+
|
|
245
264
|
## Cap-warn + handoff (new in v2.2)
|
|
246
265
|
|
|
247
266
|
Claude Code's statusline payload now includes rate-limit usage (`rate_limits.five_hour.used_percentage`, `rate_limits.seven_day.used_percentage`). claude-token-saver leads the statusline with a `🚨 5H 94%` (or `🚨 7D 92%`) chip the moment either window crosses **90%**, and writes the transition into history:
|
|
@@ -268,7 +287,7 @@ That writes `./HANDOFF-YYYY-MM-DD-HHMM.md` in the current directory with:
|
|
|
268
287
|
Read the most recent HANDOFF-*.md in this directory and continue the work.
|
|
269
288
|
```
|
|
270
289
|
|
|
271
|
-
The handoff write is also recorded in history (`📝 handoff written: …`), so
|
|
290
|
+
The handoff write is also recorded in history (`📝 handoff written: …`), so `claude-token-saver history` and the auto-skill show both the cap-warn and the backup event next to each other.
|
|
272
291
|
|
|
273
292
|
## Hook Setup
|
|
274
293
|
|
package/bin/cli.js
CHANGED
|
@@ -39,6 +39,10 @@ import { dirname, join } from 'node:path';
|
|
|
39
39
|
* "seven_day": { "used_percentage": 7, "resets_at": 1777521600 }
|
|
40
40
|
* }
|
|
41
41
|
* }
|
|
42
|
+
*
|
|
43
|
+
* extractCaps treats `rate_limits` as a generic object so any future window
|
|
44
|
+
* Anthropic adds (e.g. a Sonnet-only weekly bucket) flows through without code
|
|
45
|
+
* changes — known keys get curated labels, unknowns get derived ones.
|
|
42
46
|
*/
|
|
43
47
|
function readStdinJson() {
|
|
44
48
|
if (process.stdin.isTTY) return null;
|
|
@@ -52,22 +56,34 @@ function readStdinJson() {
|
|
|
52
56
|
}
|
|
53
57
|
|
|
54
58
|
function extractCaps(stdinJson) {
|
|
55
|
-
if (!stdinJson || !stdinJson.rate_limits) return null;
|
|
56
|
-
const
|
|
57
|
-
const
|
|
58
|
-
if (!
|
|
59
|
-
const
|
|
60
|
-
if (!Number.isFinite(
|
|
61
|
-
const resetsAt = Number(
|
|
62
|
-
|
|
63
|
-
|
|
59
|
+
if (!stdinJson || !stdinJson.rate_limits || typeof stdinJson.rate_limits !== 'object') return null;
|
|
60
|
+
const windows = [];
|
|
61
|
+
for (const [key, value] of Object.entries(stdinJson.rate_limits)) {
|
|
62
|
+
if (!value || typeof value !== 'object') continue;
|
|
63
|
+
const usedPct = Number(value.used_percentage);
|
|
64
|
+
if (!Number.isFinite(usedPct)) continue;
|
|
65
|
+
const resetsAt = Number(value.resets_at);
|
|
66
|
+
windows.push({
|
|
67
|
+
key,
|
|
68
|
+
usedPct,
|
|
64
69
|
resetsAt: Number.isFinite(resetsAt) ? resetsAt : null,
|
|
65
|
-
};
|
|
66
|
-
}
|
|
67
|
-
return {
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
return windows.length ? { windows } : null;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Pull the human-friendly model name out of Claude Code's stdin payload.
|
|
77
|
+
* `model.display_name` is the contract; fall back to `model.id` when it's
|
|
78
|
+
* absent. Returns null when nothing usable is in the JSON.
|
|
79
|
+
*/
|
|
80
|
+
function extractModel(stdinJson) {
|
|
81
|
+
if (!stdinJson || !stdinJson.model) return null;
|
|
82
|
+
const m = stdinJson.model;
|
|
83
|
+
if (typeof m === 'string') return m;
|
|
84
|
+
if (typeof m.display_name === 'string' && m.display_name) return m.display_name;
|
|
85
|
+
if (typeof m.id === 'string' && m.id) return m.id;
|
|
86
|
+
return null;
|
|
71
87
|
}
|
|
72
88
|
|
|
73
89
|
import { parseAllSessions, getLastUserMessageTime } from '../src/parser.js';
|
|
@@ -108,7 +124,151 @@ function hasFlag(name) {
|
|
|
108
124
|
return args.includes(name);
|
|
109
125
|
}
|
|
110
126
|
|
|
127
|
+
/**
|
|
128
|
+
* Scan recent history file contents (newest day first) and return the most
|
|
129
|
+
* recent warning event — `{ time, chip, detail, codes, isCap, capLabel, capPct }`.
|
|
130
|
+
* Returns null when no warning is found in the window.
|
|
131
|
+
*
|
|
132
|
+
* Recognized event lines (from history.js appendDayLine output):
|
|
133
|
+
* - HH:MM:SS ⚠ Cache miss — session abc1: LOW_HIT_RATE
|
|
134
|
+
* - HH:MM:SS ⚠ A → ⚠ B — detail
|
|
135
|
+
* - HH:MM:SS 🚨 5H 94% cap warning (resets in ...)
|
|
136
|
+
* - HH:MM:SS ✓ resolved (was ...) ← skip
|
|
137
|
+
* - HH:MM:SS ✓ 5H cap warning resolved ← skip
|
|
138
|
+
* - HH:MM:SS 📝 handoff written: ... ← skip
|
|
139
|
+
*/
|
|
140
|
+
function findLatestWarning(historyEntries, chipToCodes) {
|
|
141
|
+
const warnings = [];
|
|
142
|
+
for (const { date, content } of historyEntries) {
|
|
143
|
+
const lines = content.split('\n');
|
|
144
|
+
for (const line of lines) {
|
|
145
|
+
// Skip non-event lines
|
|
146
|
+
const m = line.match(/^- (\d{2}:\d{2}:\d{2})\s+(.+)$/);
|
|
147
|
+
if (!m) continue;
|
|
148
|
+
const time = m[1];
|
|
149
|
+
const rest = m[2];
|
|
150
|
+
// Skip resolutions and handoff entries
|
|
151
|
+
if (rest.startsWith('✓ ') || rest.startsWith('📝 ')) continue;
|
|
152
|
+
// Cap-warn line: `🚨 5H 94% cap warning (...)`
|
|
153
|
+
const cap = rest.match(/^🚨\s+(\S+)\s+(\d+)%\s+cap warning(?:\s*\((.+)\))?$/);
|
|
154
|
+
if (cap) {
|
|
155
|
+
warnings.push({
|
|
156
|
+
date,
|
|
157
|
+
time,
|
|
158
|
+
chip: `🚨 ${cap[1]} ${cap[2]}%`,
|
|
159
|
+
isCap: true,
|
|
160
|
+
capLabel: cap[1],
|
|
161
|
+
capPct: parseInt(cap[2], 10),
|
|
162
|
+
capReset: cap[3] || null,
|
|
163
|
+
codes: [],
|
|
164
|
+
detail: null,
|
|
165
|
+
});
|
|
166
|
+
continue;
|
|
167
|
+
}
|
|
168
|
+
// Chip line — last token after the chip is `— detail` (optional). The
|
|
169
|
+
// chip itself can be a plain `⚠ X` or a `⚠ A → ⚠ B` transition; we want
|
|
170
|
+
// the *current* chip (right side of the arrow if present).
|
|
171
|
+
const arrowMatch = rest.match(/^(.+?)\s+→\s+(.+?)(?:\s+—\s+(.+))?$/);
|
|
172
|
+
let chip;
|
|
173
|
+
let detail = null;
|
|
174
|
+
if (arrowMatch) {
|
|
175
|
+
chip = arrowMatch[2].trim();
|
|
176
|
+
detail = arrowMatch[3] || null;
|
|
177
|
+
} else {
|
|
178
|
+
const plain = rest.match(/^(\S+(?:\s+\S+)*?)(?:\s+—\s+(.+))?$/);
|
|
179
|
+
if (!plain) continue;
|
|
180
|
+
chip = plain[1].trim();
|
|
181
|
+
detail = plain[2] || null;
|
|
182
|
+
}
|
|
183
|
+
// Resolve codes: detail "session ID: A, B" → codes; else CHIP_TO_CODES.
|
|
184
|
+
const codes = [];
|
|
185
|
+
if (detail) {
|
|
186
|
+
const dm = detail.match(/^session [^:]+:\s*(.+)$/);
|
|
187
|
+
if (dm) {
|
|
188
|
+
for (const c of dm[1].split(',').map((s) => s.trim()).filter(Boolean)) {
|
|
189
|
+
if (!codes.includes(c)) codes.push(c);
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
if (chipToCodes[chip]) {
|
|
194
|
+
for (const c of chipToCodes[chip]) if (!codes.includes(c)) codes.push(c);
|
|
195
|
+
}
|
|
196
|
+
warnings.push({ date, time, chip, isCap: false, codes, detail });
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
return warnings.length ? warnings[warnings.length - 1] : null;
|
|
200
|
+
}
|
|
201
|
+
|
|
111
202
|
async function main() {
|
|
203
|
+
// Subcommand: last — print the most recent warning + how to handle it.
|
|
204
|
+
// Designed for the auto-trigger skill so the user immediately sees
|
|
205
|
+
// "what just fired and how to fix it" without having to read the whole
|
|
206
|
+
// history file.
|
|
207
|
+
// claude-token-saver last # search last 1 day
|
|
208
|
+
// claude-token-saver last --days 7 # widen the lookback
|
|
209
|
+
if (args[0] === 'last') {
|
|
210
|
+
const { readRecent, historyDir } = await import('../src/history.js');
|
|
211
|
+
const { ISSUE_MESSAGES, CHIP_TO_CODES, CAP_TIPS } = await import('../src/advice.js');
|
|
212
|
+
const days = parseFloat(getArg('--days') || '1');
|
|
213
|
+
const recent = readRecent(days);
|
|
214
|
+
const latest = findLatestWarning(recent, CHIP_TO_CODES);
|
|
215
|
+
if (!latest) {
|
|
216
|
+
console.log(`No warnings in the last ${days} day${days === 1 ? '' : 's'}.`);
|
|
217
|
+
console.log(`(History dir: ${historyDir()})`);
|
|
218
|
+
return;
|
|
219
|
+
}
|
|
220
|
+
// Header
|
|
221
|
+
console.log(`Most recent warning — ${latest.date} ${latest.time}`);
|
|
222
|
+
console.log(` ${latest.chip}${latest.detail ? ` — ${latest.detail}` : ''}`);
|
|
223
|
+
console.log('');
|
|
224
|
+
// Cap-warn path: handoff is the recommendation. Print the bilingual tip
|
|
225
|
+
// and a one-line "how to back up" pointer.
|
|
226
|
+
if (latest.isCap) {
|
|
227
|
+
if (latest.capReset) console.log(` Cap window: ${latest.capReset}`);
|
|
228
|
+
console.log('');
|
|
229
|
+
console.log('💡 ' + CAP_TIPS.en);
|
|
230
|
+
console.log('💡 ' + CAP_TIPS.ko);
|
|
231
|
+
console.log('');
|
|
232
|
+
console.log('Run:');
|
|
233
|
+
console.log(' claude-token-saver handoff');
|
|
234
|
+
return;
|
|
235
|
+
}
|
|
236
|
+
// Chip warning path: render full ISSUE_MESSAGES advice for each code,
|
|
237
|
+
// bilingual (English first, `└ Korean` continuation per line — matches
|
|
238
|
+
// the history.md format).
|
|
239
|
+
if (latest.codes.length === 0) {
|
|
240
|
+
console.log('(No diagnostic code attached — open the table view: `claude-token-saver --days 1`)');
|
|
241
|
+
console.log('(진단 코드 없음 — 표 뷰를 열어보세요: `claude-token-saver --days 1`)');
|
|
242
|
+
return;
|
|
243
|
+
}
|
|
244
|
+
for (const code of latest.codes) {
|
|
245
|
+
const msg = ISSUE_MESSAGES[code];
|
|
246
|
+
if (!msg) {
|
|
247
|
+
console.log(`Code: ${code} (no advice registered)`);
|
|
248
|
+
continue;
|
|
249
|
+
}
|
|
250
|
+
console.log(`▎ ${msg.title}`);
|
|
251
|
+
if (msg.titleKo && msg.titleKo !== msg.title) console.log(` └ ${msg.titleKo}`);
|
|
252
|
+
console.log(` ${msg.explain}`);
|
|
253
|
+
if (msg.explainKo && msg.explainKo !== msg.explain) console.log(` └ ${msg.explainKo}`);
|
|
254
|
+
const actions = typeof msg.actions === 'function' ? msg.actions() : msg.actions || [];
|
|
255
|
+
for (const a of actions) {
|
|
256
|
+
console.log('');
|
|
257
|
+
console.log(` ${a.label}:`);
|
|
258
|
+
if (a.labelKo && a.labelKo !== a.label) console.log(` └ ${a.labelKo}:`);
|
|
259
|
+
const cmds = a.commands || [];
|
|
260
|
+
const cmdsKo = a.commandsKo || [];
|
|
261
|
+
for (let i = 0; i < cmds.length; i++) {
|
|
262
|
+
console.log(` - ${cmds[i]}`);
|
|
263
|
+
const ko = cmdsKo[i];
|
|
264
|
+
if (ko && ko !== cmds[i]) console.log(` └ ${ko}`);
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
console.log('');
|
|
268
|
+
}
|
|
269
|
+
return;
|
|
270
|
+
}
|
|
271
|
+
|
|
112
272
|
// Subcommand: history — print recent warning transitions captured by the
|
|
113
273
|
// statusline. One markdown file per day, persisted under the platform-
|
|
114
274
|
// specific user-data dir.
|
|
@@ -169,38 +329,31 @@ async function main() {
|
|
|
169
329
|
return;
|
|
170
330
|
}
|
|
171
331
|
|
|
172
|
-
// Subcommand: install — write the Claude Code
|
|
173
|
-
//
|
|
174
|
-
//
|
|
175
|
-
//
|
|
176
|
-
// claude-token-saver install
|
|
177
|
-
// claude-token-saver install --
|
|
178
|
-
// claude-token-saver install --force # overwrite existing files
|
|
332
|
+
// Subcommand: install — write the Claude Code auto-trigger skill so the
|
|
333
|
+
// user can just mention chip wording and Claude responds. v2.6.0 dropped
|
|
334
|
+
// the redundant /token-monitor slash command in favor of the skill alone;
|
|
335
|
+
// a legacy command file is removed automatically. Cross-platform.
|
|
336
|
+
// claude-token-saver install # install/update the skill
|
|
337
|
+
// claude-token-saver install --force # overwrite existing skill file
|
|
179
338
|
if (args[0] === 'install') {
|
|
180
|
-
const {
|
|
339
|
+
const { installAll } = await import('../src/installer.js');
|
|
181
340
|
const force = hasFlag('--force');
|
|
182
|
-
const onlySkill = hasFlag('--skill');
|
|
183
|
-
const onlyCommand = hasFlag('--command');
|
|
184
341
|
const print = (kind, r) => {
|
|
185
342
|
const verb = r.action === 'exists' ? 'already exists' : r.action;
|
|
186
343
|
console.log(` ${kind}: ${r.path} (${verb})`);
|
|
187
344
|
};
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
print('
|
|
192
|
-
|
|
193
|
-
const r = installAll({ force });
|
|
194
|
-
print('skill', r.skill);
|
|
195
|
-
print('command', r.command);
|
|
345
|
+
const r = installAll({ force });
|
|
346
|
+
print('skill', r.skill);
|
|
347
|
+
if (r.legacy.action === 'removed') {
|
|
348
|
+
print('legacy /token-monitor', r.legacy);
|
|
349
|
+
console.log(' (consolidated into the skill — same workflow, triggered by intent)');
|
|
196
350
|
}
|
|
197
351
|
console.log('');
|
|
198
|
-
console.log('Open Claude Code in any directory and
|
|
199
|
-
console.log(' /
|
|
200
|
-
console.log('Or just mention "cache hit rate" / "1M context" — the skill auto-activates.');
|
|
352
|
+
console.log('Open Claude Code in any directory and just mention:');
|
|
353
|
+
console.log(' "cache hit rate" / "1M context" / "5H cap" — the skill auto-activates.');
|
|
201
354
|
if (!force) {
|
|
202
355
|
console.log('');
|
|
203
|
-
console.log('Tip: re-run with --force to overwrite existing
|
|
356
|
+
console.log('Tip: re-run with --force to overwrite the existing skill file.');
|
|
204
357
|
}
|
|
205
358
|
return;
|
|
206
359
|
}
|
|
@@ -399,24 +552,30 @@ async function main() {
|
|
|
399
552
|
const contextWindow = detectContextWindow(sessions, { recentHours: 24 });
|
|
400
553
|
|
|
401
554
|
// Claude Code feeds the statusline command a JSON blob on stdin every
|
|
402
|
-
// refresh. Pull rate_limits out of it so we can surface cap-warn
|
|
403
|
-
// chips,
|
|
404
|
-
//
|
|
405
|
-
//
|
|
555
|
+
// refresh. Pull rate_limits + model out of it so we can surface cap-warn
|
|
556
|
+
// (>=90%) chips, always-on usage segments, the model chip, record cap
|
|
557
|
+
// transitions, and seed the table view's warning box. The table path falls
|
|
558
|
+
// back to the most-recent cached snapshot so the table view (which
|
|
559
|
+
// doesn't pipe stdin) still has the data.
|
|
406
560
|
const stdinJson = readStdinJson();
|
|
407
561
|
let caps = extractCaps(stdinJson);
|
|
408
|
-
|
|
562
|
+
let model = extractModel(stdinJson);
|
|
563
|
+
if (isStatusline && (caps || model)) {
|
|
409
564
|
try {
|
|
410
|
-
const {
|
|
411
|
-
|
|
565
|
+
const { persistSnapshot } = await import('../src/caps-cache.js');
|
|
566
|
+
persistSnapshot({ caps, model });
|
|
412
567
|
} catch {
|
|
413
568
|
// non-critical
|
|
414
569
|
}
|
|
415
570
|
}
|
|
416
|
-
if (!isStatusline && !caps) {
|
|
571
|
+
if (!isStatusline && (!caps || !model)) {
|
|
417
572
|
try {
|
|
418
|
-
const {
|
|
419
|
-
|
|
573
|
+
const { loadRecentSnapshot } = await import('../src/caps-cache.js');
|
|
574
|
+
const snap = loadRecentSnapshot();
|
|
575
|
+
if (snap) {
|
|
576
|
+
if (!caps && snap.caps) caps = snap.caps;
|
|
577
|
+
if (!model && snap.model) model = snap.model;
|
|
578
|
+
}
|
|
420
579
|
} catch {
|
|
421
580
|
// ignore
|
|
422
581
|
}
|
|
@@ -450,15 +609,14 @@ async function main() {
|
|
|
450
609
|
}
|
|
451
610
|
}
|
|
452
611
|
// Persist transitions to ~/.config/claude-token-saver/history/YYYY-MM-DD.md
|
|
453
|
-
// so
|
|
612
|
+
// so `claude-token-saver history` and the auto-skill can replay them.
|
|
454
613
|
try {
|
|
455
614
|
const { recordChip, recordCapTransition } = await import('../src/history.js');
|
|
456
615
|
recordChip(spikeChip, { detail: chipDetail });
|
|
457
616
|
// Cap-warn transitions are tracked independently per window — a session
|
|
458
617
|
// can hit 90% on the 5h window even when no spike chip is firing.
|
|
459
|
-
if (caps) {
|
|
460
|
-
|
|
461
|
-
recordCapTransition('seven_day', caps.sevenDay);
|
|
618
|
+
if (caps && Array.isArray(caps.windows)) {
|
|
619
|
+
for (const win of caps.windows) recordCapTransition(win);
|
|
462
620
|
}
|
|
463
621
|
} catch {
|
|
464
622
|
// history is non-critical — don't let it break the statusline render
|
|
@@ -498,6 +656,7 @@ async function main() {
|
|
|
498
656
|
contextWindow,
|
|
499
657
|
spikeChip,
|
|
500
658
|
caps,
|
|
659
|
+
model,
|
|
501
660
|
};
|
|
502
661
|
|
|
503
662
|
let output;
|
package/package.json
CHANGED
package/src/advice.js
CHANGED
|
@@ -26,106 +26,226 @@ function toggleShortcut() {
|
|
|
26
26
|
return platformKind() === 'win' ? 'Alt + P' : '⌥ P (mac) / Alt + P (linux)';
|
|
27
27
|
}
|
|
28
28
|
|
|
29
|
+
/**
|
|
30
|
+
* Each entry has parallel English / Korean fields. table.js only reads the
|
|
31
|
+
* English fields (kept stable so the existing report layout doesn't shift).
|
|
32
|
+
* `last` (bin/cli.js) renders both, English first then `└ Korean` continuation
|
|
33
|
+
* — same bilingual style as the history.md file.
|
|
34
|
+
*
|
|
35
|
+
* `commandsKo` is optional per action: when a command is a literal code
|
|
36
|
+
* snippet (e.g. `export CLAUDE_CODE_DISABLE_1M_CONTEXT=1`), there's nothing
|
|
37
|
+
* to translate, so it's fine to omit and fall back to `commands`. When the
|
|
38
|
+
* command is descriptive prose, provide a Korean version.
|
|
39
|
+
*/
|
|
29
40
|
export const ISSUE_MESSAGES = {
|
|
30
41
|
LARGE_INPUT_PER_REQUEST: {
|
|
31
42
|
title: 'Per-request input tokens are unusually large (1M context suspected)',
|
|
43
|
+
titleKo: '요청당 입력 토큰이 비정상적으로 큼 (1M 컨텍스트 의심)',
|
|
32
44
|
explain:
|
|
33
45
|
'Since Opus 4.7, 1M context is priced at the standard rate, and Max plans auto-promote ' +
|
|
34
46
|
'sessions to 1M. Once context goes past 200k, long-context pricing kicks in and cache reuse drops.',
|
|
47
|
+
explainKo:
|
|
48
|
+
'Opus 4.7부터 1M 컨텍스트가 표준 요금이 되었고, Max 플랜은 세션을 자동으로 1M으로 승격합니다. ' +
|
|
49
|
+
'200k를 넘는 순간 장기 컨텍스트 요금이 적용되며 캐시 재사용률도 떨어집니다.',
|
|
35
50
|
actions: () => [
|
|
36
51
|
{
|
|
37
52
|
label: 'Disable 1M context (env var)',
|
|
53
|
+
labelKo: '1M 컨텍스트 끄기 (환경변수)',
|
|
38
54
|
commands: disable1mEnvSnippet(),
|
|
39
55
|
},
|
|
40
56
|
{
|
|
41
57
|
label: 'In-session toggle',
|
|
58
|
+
labelKo: '세션 내 즉시 토글',
|
|
42
59
|
commands: [`Press ${toggleShortcut()} to toggle on/off instantly`],
|
|
60
|
+
commandsKo: [`${toggleShortcut()} 누르면 즉시 on/off 토글`],
|
|
43
61
|
},
|
|
44
62
|
{
|
|
45
63
|
label: '⚠ Known bug #31640',
|
|
64
|
+
labelKo: '⚠ 알려진 버그 #31640',
|
|
46
65
|
commands: [
|
|
47
66
|
'/model 200k selection sometimes does not stick — context stays at 1M.',
|
|
48
67
|
'To force off: set the env var above and restart Claude Code.',
|
|
49
68
|
],
|
|
69
|
+
commandsKo: [
|
|
70
|
+
'/model 200k 선택이 가끔 적용되지 않음 — 컨텍스트가 1M으로 유지됨.',
|
|
71
|
+
'강제로 끄려면 위의 환경변수를 설정하고 Claude Code를 재시작.',
|
|
72
|
+
],
|
|
50
73
|
},
|
|
51
74
|
],
|
|
52
75
|
},
|
|
53
76
|
LOW_HIT_RATE: {
|
|
54
77
|
title: 'Cache hit rate is low',
|
|
78
|
+
titleKo: '캐시 적중률이 낮음',
|
|
55
79
|
explain:
|
|
56
80
|
'A low hit rate means the same prompt prefix is being rewritten on every call, ' +
|
|
57
81
|
'inflating input cost.',
|
|
82
|
+
explainKo:
|
|
83
|
+
'적중률이 낮다는 건 동일한 프롬프트 prefix가 매 호출마다 다시 쓰이고 있다는 뜻이며, ' +
|
|
84
|
+
'입력 비용을 부풀립니다.',
|
|
58
85
|
actions: () => [
|
|
59
86
|
{
|
|
60
87
|
label: 'Avoid opening fresh sessions too often',
|
|
88
|
+
labelKo: '새 세션을 너무 자주 열지 말기',
|
|
61
89
|
commands: ['Continue the same task in the same session (context switching = cache miss)'],
|
|
90
|
+
commandsKo: ['같은 작업은 같은 세션에서 계속 (컨텍스트 전환 = 캐시 미스)'],
|
|
62
91
|
},
|
|
63
92
|
{
|
|
64
93
|
label: 'Stabilize the prompt prefix',
|
|
94
|
+
labelKo: '프롬프트 prefix 안정화',
|
|
65
95
|
commands: ['System prompts / tool definitions that change per request invalidate the cache every time'],
|
|
96
|
+
commandsKo: ['요청마다 바뀌는 시스템 프롬프트 / 도구 정의는 매번 캐시를 무효화'],
|
|
66
97
|
},
|
|
67
98
|
],
|
|
68
99
|
},
|
|
69
100
|
BUCKET_5M_DOMINANT: {
|
|
70
101
|
title: 'Most cache writes are landing in the 5-minute TTL bucket',
|
|
102
|
+
titleKo: '캐시 쓰기 대부분이 5분 TTL 버킷에 들어가고 있음',
|
|
71
103
|
explain:
|
|
72
104
|
'Pro plan is locked to 5m TTL. Gaps longer than 5 minutes expire the cache and force ' +
|
|
73
105
|
'a costly rebuild.',
|
|
106
|
+
explainKo:
|
|
107
|
+
'Pro 플랜은 5분 TTL로 고정됩니다. 5분을 넘는 공백마다 캐시가 만료되고 비싼 재빌드가 강제됩니다.',
|
|
74
108
|
actions: () => [
|
|
75
109
|
{
|
|
76
110
|
label: 'The 5-minute rule',
|
|
111
|
+
labelKo: '5분 규칙',
|
|
77
112
|
commands: [
|
|
78
113
|
'Sending any prompt within 5 minutes keeps the prefix cache warm',
|
|
79
114
|
'Upgrade to Max for the 1h TTL bucket on long tasks',
|
|
80
115
|
],
|
|
116
|
+
commandsKo: [
|
|
117
|
+
'5분 이내 어떤 프롬프트든 보내면 prefix 캐시가 유지됨',
|
|
118
|
+
'긴 작업은 Max 플랜으로 업그레이드해서 1시간 TTL 버킷 사용',
|
|
119
|
+
],
|
|
81
120
|
},
|
|
82
121
|
],
|
|
83
122
|
},
|
|
84
123
|
HIGH_OUTPUT_RATIO: {
|
|
85
124
|
title: 'Output share is abnormally high',
|
|
125
|
+
titleKo: '출력 비중이 비정상적으로 높음',
|
|
86
126
|
explain:
|
|
87
127
|
'Output tokens are 5x+ pricier than input. Check whether the agent is regenerating ' +
|
|
88
128
|
'long content unnecessarily.',
|
|
129
|
+
explainKo:
|
|
130
|
+
'출력 토큰은 입력보다 5배 이상 비쌉니다. 에이전트가 불필요하게 긴 컨텐츠를 ' +
|
|
131
|
+
'반복 생성하고 있지 않은지 확인하세요.',
|
|
89
132
|
actions: () => [
|
|
90
133
|
{
|
|
91
134
|
label: 'Cap output length',
|
|
135
|
+
labelKo: '출력 길이 줄이기',
|
|
92
136
|
commands: [
|
|
93
137
|
'Avoid full-file rewrites — prefer the Edit tool',
|
|
94
138
|
'Move long doc/README generation requests into scripts to shrink output',
|
|
95
139
|
],
|
|
140
|
+
commandsKo: [
|
|
141
|
+
'파일 전체 재작성 피하기 — Edit 도구 우선 사용',
|
|
142
|
+
'긴 문서/README 생성 요청은 스크립트로 옮겨 출력 축소',
|
|
143
|
+
],
|
|
96
144
|
},
|
|
97
145
|
],
|
|
98
146
|
},
|
|
99
147
|
HIGH_REQUEST_COUNT: {
|
|
100
148
|
title: 'API calls in this session are 3x+ the baseline',
|
|
149
|
+
titleKo: '이번 세션의 API 호출이 평소 대비 3배 이상',
|
|
101
150
|
explain:
|
|
102
151
|
'Excessive tool calls or retry/loop patterns rebroadcast the prefix on every call, ' +
|
|
103
152
|
'spiking input cost.',
|
|
153
|
+
explainKo:
|
|
154
|
+
'과도한 도구 호출 또는 재시도/루프 패턴은 매 호출마다 prefix를 재전송해 입력 비용을 폭증시킵니다.',
|
|
104
155
|
actions: () => [
|
|
105
156
|
{
|
|
106
157
|
label: 'Parallel / batch processing',
|
|
158
|
+
labelKo: '병렬 / 배치 처리',
|
|
107
159
|
commands: ['Bundle independent investigations into one message with multiple tool calls'],
|
|
160
|
+
commandsKo: ['독립적인 조사 작업은 도구 호출 여러 개를 한 메시지로 묶어서 보내기'],
|
|
108
161
|
},
|
|
109
162
|
{
|
|
110
163
|
label: 'Watch for loops',
|
|
164
|
+
labelKo: '루프 감시',
|
|
111
165
|
commands: ['Check that the agent is not repeating the same test/search in a loop'],
|
|
166
|
+
commandsKo: ['에이전트가 동일한 테스트/검색을 루프로 반복하고 있지 않은지 확인'],
|
|
112
167
|
},
|
|
113
168
|
],
|
|
114
169
|
},
|
|
115
170
|
FREQUENT_CACHE_REBUILD: {
|
|
116
171
|
title: 'Cache writes outweigh cache reads',
|
|
172
|
+
titleKo: '캐시 쓰기가 읽기보다 많음',
|
|
117
173
|
explain:
|
|
118
174
|
'The cache is being created but not reused. Common when sessions are short-lived or ' +
|
|
119
175
|
'restarted after TTL expiry.',
|
|
176
|
+
explainKo:
|
|
177
|
+
'캐시가 만들어지지만 재사용되지 않고 있습니다. 세션이 짧거나 TTL 만료 후 재시작될 때 흔합니다.',
|
|
120
178
|
actions: () => [
|
|
121
179
|
{
|
|
122
180
|
label: 'Check session continuity',
|
|
181
|
+
labelKo: '세션 연속성 점검',
|
|
123
182
|
commands: ['Continue one task in one Claude Code session'],
|
|
183
|
+
commandsKo: ['하나의 작업은 하나의 Claude Code 세션에서 계속'],
|
|
124
184
|
},
|
|
125
185
|
],
|
|
126
186
|
},
|
|
127
187
|
};
|
|
128
188
|
|
|
189
|
+
/**
|
|
190
|
+
* One-line action tips per issue code — bilingual. Used by history.js to
|
|
191
|
+
* inline a "what to do" hint right below each warning event in the daily
|
|
192
|
+
* markdown file, so the file alone is enough to answer "what was the warning,
|
|
193
|
+
* and how do I fix it" without re-running the tool.
|
|
194
|
+
*
|
|
195
|
+
* The full multi-step advice still lives in `ISSUE_MESSAGES[code].actions()`;
|
|
196
|
+
* `claude-token-saver last` prints that long form on demand.
|
|
197
|
+
*/
|
|
198
|
+
export const ISSUE_TIPS = {
|
|
199
|
+
LARGE_INPUT_PER_REQUEST: {
|
|
200
|
+
en: 'Disable 1M context: `export CLAUDE_CODE_DISABLE_1M_CONTEXT=1` (or ⌥P toggle)',
|
|
201
|
+
ko: '1M 컨텍스트 끄기: `export CLAUDE_CODE_DISABLE_1M_CONTEXT=1` (또는 ⌥P 토글)',
|
|
202
|
+
},
|
|
203
|
+
LOW_HIT_RATE: {
|
|
204
|
+
en: 'Continue same task in same session; keep prompt prefix stable',
|
|
205
|
+
ko: '같은 작업은 같은 세션에서 계속; 프롬프트 prefix 안정 유지',
|
|
206
|
+
},
|
|
207
|
+
BUCKET_5M_DOMINANT: {
|
|
208
|
+
en: 'Send any prompt within 5min to keep cache warm; Max plan unlocks 1h TTL',
|
|
209
|
+
ko: '5분 이내 한 번 더 보내 캐시 유지; Max 플랜은 1시간 TTL 제공',
|
|
210
|
+
},
|
|
211
|
+
HIGH_OUTPUT_RATIO: {
|
|
212
|
+
en: 'Avoid full-file rewrites — prefer Edit tool; move long generations to scripts',
|
|
213
|
+
ko: '파일 전체 재작성 피하기 — Edit 도구 사용; 긴 생성은 스크립트로',
|
|
214
|
+
},
|
|
215
|
+
HIGH_REQUEST_COUNT: {
|
|
216
|
+
en: 'Bundle independent calls into one message; check for retry loops',
|
|
217
|
+
ko: '독립 호출은 한 메시지에 묶기; 재시도 루프 점검',
|
|
218
|
+
},
|
|
219
|
+
FREQUENT_CACHE_REBUILD: {
|
|
220
|
+
en: 'Continue one task in one session — short sessions trigger rebuild',
|
|
221
|
+
ko: '한 작업은 한 세션에서 — 짧은 세션은 재빌드 유발',
|
|
222
|
+
},
|
|
223
|
+
};
|
|
224
|
+
|
|
225
|
+
/**
|
|
226
|
+
* Chip text → diagnostic codes. Some chips fire without a `detail` line that
|
|
227
|
+
* includes the code (e.g. `⚠ 1M ON`, which only carries context info), so we
|
|
228
|
+
* need a fallback so history.js can still surface the right tip.
|
|
229
|
+
*/
|
|
230
|
+
export const CHIP_TO_CODES = {
|
|
231
|
+
'⚠ 1M ON': ['LARGE_INPUT_PER_REQUEST'],
|
|
232
|
+
'⚠ Cache miss': ['LOW_HIT_RATE'],
|
|
233
|
+
'⚠ Input spike': ['LARGE_INPUT_PER_REQUEST'],
|
|
234
|
+
'⚠ 5m TTL': ['BUCKET_5M_DOMINANT'],
|
|
235
|
+
'⚠ Rebuild churn': ['FREQUENT_CACHE_REBUILD'],
|
|
236
|
+
'⚠ Output heavy': ['HIGH_OUTPUT_RATIO'],
|
|
237
|
+
'⚠ Call surge': ['HIGH_REQUEST_COUNT'],
|
|
238
|
+
};
|
|
239
|
+
|
|
240
|
+
/**
|
|
241
|
+
* Cap-warn (5h/7d >=90%) advice — bilingual. Always points at `handoff`
|
|
242
|
+
* because once the cap blocks you, you need a fresh session to continue.
|
|
243
|
+
*/
|
|
244
|
+
export const CAP_TIPS = {
|
|
245
|
+
en: 'Run `claude-token-saver handoff` to back up state before the cap blocks you',
|
|
246
|
+
ko: '`claude-token-saver handoff` 실행해서 캡 도달 전에 상태 백업',
|
|
247
|
+
};
|
|
248
|
+
|
|
129
249
|
/**
|
|
130
250
|
* For the statusline: the single most relevant short chip (1~2 words).
|
|
131
251
|
* Priority reflects what a user can act on *right now*.
|