claude-token-saver 2.0.2 → 2.1.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 CHANGED
@@ -1,6 +1,6 @@
1
1
  # claude-token-saver
2
2
 
3
- > **Renamed from `claude-cache-monitor` in v2.0.** The old name still works `npm i claude-cache-monitor` now redirects here, and the `claude-cache-monitor` binary remains available as an alias. See [migration notes](#migration-from-claude-cache-monitor).
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
5
  > 📺 **HNPulse Shorts** — 이 도구가 만들어진 배경 (캐시 TTL 1h→5m 변경 이슈):
6
6
  > **[▶ Watch the Short](https://www.youtube.com/shorts/oSx2sg935nI)** · [All HNPulse Shorts](https://www.youtube.com/@HNPulseKR/shorts)
@@ -9,6 +9,13 @@
9
9
 
10
10
  **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
11
 
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
+
14
+ v2.1 (2026-04) adds the workflow that follows the diagnosis:
15
+ - **`claude-token-saver install`** — one command writes a Claude Code Skill (auto-activates when you mention "cache hit rate" / "1M context" / etc.) and a `/token-monitor` slash command.
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
+ - **Cross-platform paths** — Windows (`%APPDATA%`), macOS (`~/Library/Application Support`), Linux (`~/.config` / XDG) all handled.
18
+
12
19
  v1.5 adds three things on top of the original `claude-cache-monitor`:
13
20
  - **Spike diagnosis** — detect recent sessions whose input tokens exploded vs. your own baseline, and name the cause (1M context, 5m TTL churn, cache rebuild, chatty output).
14
21
  - **1M-context detection** — Opus 4.7+ auto-enables 1M context on Max plans, silently. This tool surfaces it on the statusline as `Ctx 1M` (red) vs. `Ctx 200k` (green), with the OS-specific command to turn it off.
@@ -47,6 +54,12 @@ npx claude-token-saver --format json
47
54
  npx claude-token-saver --format csv
48
55
  ```
49
56
 
57
+ > **Upgrading from `claude-cache-monitor`?** Uninstall the old package first, then install the new one and update your settings command name:
58
+ > ```bash
59
+ > npm uninstall -g claude-cache-monitor && npm i -g claude-token-saver
60
+ > ```
61
+ > After upgrading, change any `claude-cache-monitor …` invocations (including `statusLine.command` in `~/.claude/settings.json`) to `claude-token-saver …`. See [Migration](#migration-from-claude-cache-monitor).
62
+
50
63
  ## Spike Diagnosis (new in v1.5.0)
51
64
 
52
65
  When you run `npx claude-token-saver`, sessions from the last 24 hours whose total input tokens are **≥ 3× your p95 baseline** (or whose single-request context exceeds 250k, indicating 1M context) appear at the top of the report with root causes and remediation commands. Example output:
@@ -206,6 +219,48 @@ Claude Code calls this every ~300ms on events, plus once per `refreshInterval` s
206
219
 
207
220
  Statusline mode uses the last 7 days by default (override with `--days N`) and never emits multi-line errors, so your statusline stays clean even when there's no session data yet.
208
221
 
222
+ ## Claude Code integration (`install`) — new in v2.1
223
+
224
+ One command wires up everything else this README mentions:
225
+
226
+ ```bash
227
+ claude-token-saver install
228
+ ```
229
+
230
+ This writes two files under your Claude user dir:
231
+ - `~/.claude/skills/claude-token-saver/SKILL.md` — auto-activates whenever you mention chip wording ("⚠ 1M ON", "cache miss", etc.) or ask about token usage. Claude Code will then know to read history, drill into the table report, and explain the warning.
232
+ - `~/.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.
233
+
234
+ Re-run with `--force` to overwrite. Install only one piece with `install --skill` or `install --command`.
235
+
236
+ ## Warning history (`history`) — new in v2.1
237
+
238
+ The statusline path auto-logs every chip transition (none → ⚠, ⚠ A → ⚠ B, ⚠ → resolved) to a daily Markdown file. Read it back with:
239
+
240
+ ```bash
241
+ claude-token-saver history # last 7 days
242
+ claude-token-saver history --days 30 # wider window
243
+ claude-token-saver history --list # just list available dates
244
+ ```
245
+
246
+ Sample output:
247
+
248
+ ```
249
+ # Token Monitor — 2026-04-25
250
+
251
+ ## Events
252
+ - 09:14:02 ⚠ 1M ON — Context auto-promoted to 1M (max single-request 280k tokens)
253
+ - 09:42:18 ⚠ 1M ON → ⚠ Cache miss — session abc12345: LOW_HIT_RATE
254
+ - 10:05:47 ✓ resolved (was ⚠ Cache miss)
255
+ ```
256
+
257
+ Storage paths (cross-platform):
258
+ - **Windows**: `%APPDATA%\claude-token-saver\history\YYYY-MM-DD.md`
259
+ - **macOS**: `~/Library/Application Support/claude-token-saver/history/YYYY-MM-DD.md`
260
+ - **Linux**: `$XDG_CONFIG_HOME/claude-token-saver/history/YYYY-MM-DD.md` (or `~/.config/...`)
261
+
262
+ Each day's file is plain Markdown — open it in any editor. Transitions are deduped, so the 1Hz statusline refresh doesn't spam.
263
+
209
264
  ## Hook Setup
210
265
 
211
266
  Automatically logs cache stats on every tool call and alerts when hit rate drops below a threshold.
@@ -342,24 +397,52 @@ Zero dependencies.
342
397
 
343
398
  ## Migration from claude-cache-monitor
344
399
 
345
- v2.0 renamed the package to reflect the expanded scope (spike diagnosis + 1M-context detection + remediation, not just cache monitoring). **No action required** in most cases:
400
+ v2.0 renamed the package to reflect the expanded scope (spike diagnosis + 1M-context detection + remediation, not just cache monitoring).
401
+
402
+ ### New users
403
+
404
+ ```bash
405
+ npm i -g claude-token-saver
406
+ # or, no install:
407
+ npx claude-token-saver
408
+ ```
409
+
410
+ Skip the rest of this section.
346
411
 
347
- - `npm i claude-cache-monitor` still installs — the old package is deprecated and redirects here.
348
- - The binary `claude-cache-monitor` still works alongside the new `claude-token-saver` (both map to the same entry point).
349
- - Your existing `statusLine.command` setting in `~/.claude/settings.json` keeps working.
412
+ ### Upgrading from `claude-cache-monitor` v1.x
350
413
 
351
- If you want to update:
414
+ Two steps:
352
415
 
353
416
  ```bash
417
+ # 1. Remove the old package (its claude-cache-monitor bin is now obsolete).
354
418
  npm uninstall -g claude-cache-monitor
419
+
420
+ # 2. Install the new one.
355
421
  npm i -g claude-token-saver
422
+ ```
423
+
424
+ Then update any `claude-cache-monitor …` references. The main one is `statusLine.command` in `~/.claude/settings.json`:
425
+
426
+ ```jsonc
427
+ // before
428
+ "command": "claude-cache-monitor --statusline --icon"
429
+
430
+ // after
431
+ "command": "claude-token-saver --statusline --icon"
432
+ ```
356
433
 
357
- # then in ~/.claude/settings.json, change:
358
- # "command": "claude-cache-monitor --statusline --icon"
359
- # to:
360
- # "command": "claude-token-saver --statusline --icon"
434
+ ### Why we dropped the `claude-cache-monitor` bin alias
435
+
436
+ Earlier v2.0 releases shipped a `claude-cache-monitor` bin alongside `claude-token-saver` so existing settings would keep working without edits. In practice this caused an `EEXIST: file already exists` error on `npm i -g claude-token-saver` when v1.x was still installed — and that collision forced the uninstall step anyway. Dropping the alias makes the upgrade path a clean two-liner and lets `npm i -g claude-token-saver` succeed directly if you've never installed the old one.
437
+
438
+ ### Zero-install (npx)
439
+
440
+ ```bash
441
+ npx claude-token-saver@latest --statusline --icon
361
442
  ```
362
443
 
444
+ No uninstall needed; npm just fetches the new name.
445
+
363
446
  ## License
364
447
 
365
448
  MIT
package/bin/cli.js CHANGED
@@ -62,6 +62,113 @@ function hasFlag(name) {
62
62
  }
63
63
 
64
64
  async function main() {
65
+ // Subcommand: history — print recent warning transitions captured by the
66
+ // statusline. One markdown file per day, persisted under the platform-
67
+ // specific user-data dir.
68
+ // claude-token-saver history # last 7 days
69
+ // claude-token-saver history --days 30 # custom window
70
+ // claude-token-saver history --list # just list available dates
71
+ if (args[0] === 'history') {
72
+ const { readRecent, listDates, historyDir } = await import('../src/history.js');
73
+ if (hasFlag('--list')) {
74
+ const dates = listDates();
75
+ if (dates.length === 0) {
76
+ console.log(`No history yet. Files will appear under: ${historyDir()}`);
77
+ return;
78
+ }
79
+ console.log(`History (${historyDir()}):`);
80
+ for (const d of dates) console.log(` ${d}`);
81
+ return;
82
+ }
83
+ const days = parseFloat(getArg('--days') || '7');
84
+ const recent = readRecent(days);
85
+ if (recent.length === 0) {
86
+ console.log(`No warning history in the last ${days} day${days === 1 ? '' : 's'}.`);
87
+ console.log(`(Files would be written to: ${historyDir()})`);
88
+ return;
89
+ }
90
+ for (const { content } of recent) {
91
+ console.log(content.replace(/\n+$/, ''));
92
+ console.log('');
93
+ }
94
+ return;
95
+ }
96
+
97
+ // Subcommand: install — write the Claude Code Skill and slash command so
98
+ // /token-monitor and the auto-trigger skill become available without any
99
+ // manual file editing. Cross-platform (uses node:path + node:fs).
100
+ // claude-token-saver install # install both
101
+ // claude-token-saver install --skill # only the skill
102
+ // claude-token-saver install --command # only the slash command
103
+ // claude-token-saver install --force # overwrite existing files
104
+ if (args[0] === 'install') {
105
+ const { installSkill, installCommand, installAll } = await import('../src/installer.js');
106
+ const force = hasFlag('--force');
107
+ const onlySkill = hasFlag('--skill');
108
+ const onlyCommand = hasFlag('--command');
109
+ const print = (kind, r) => {
110
+ const verb = r.action === 'exists' ? 'already exists' : r.action;
111
+ console.log(` ${kind}: ${r.path} (${verb})`);
112
+ };
113
+ if (onlySkill && !onlyCommand) {
114
+ print('skill', installSkill({ force }));
115
+ } else if (onlyCommand && !onlySkill) {
116
+ print('command', installCommand({ force }));
117
+ } else {
118
+ const r = installAll({ force });
119
+ print('skill', r.skill);
120
+ print('command', r.command);
121
+ }
122
+ console.log('');
123
+ console.log('Open Claude Code in any directory and try:');
124
+ console.log(' /token-monitor');
125
+ console.log('Or just mention "cache hit rate" / "1M context" — the skill auto-activates.');
126
+ if (!force) {
127
+ console.log('');
128
+ console.log('Tip: re-run with --force to overwrite existing files.');
129
+ }
130
+ return;
131
+ }
132
+
133
+ // Subcommand: mode — persist statusline preferences so future runs pick
134
+ // them up without flags or wrapper edits.
135
+ // claude-token-saver mode # show current config
136
+ // claude-token-saver mode icon verbose # set icon + verbose
137
+ // claude-token-saver mode reset # clear back to defaults
138
+ if (args[0] === 'mode') {
139
+ const { applyMode, loadConfig, configPath, statuslineDefaults, VALID_KEYWORDS } =
140
+ await import('../src/config.js');
141
+ const words = args.slice(1);
142
+ if (words.length === 0) {
143
+ const eff = statuslineDefaults();
144
+ const raw = loadConfig().statusline || {};
145
+ console.log('Statusline mode (effective):');
146
+ console.log(` icon: ${eff.icon}`);
147
+ console.log(` verbose: ${eff.verbose}`);
148
+ console.log(` timer: ${eff.timer}`);
149
+ console.log(` color: ${eff.color}`);
150
+ console.log(` window: ${eff.windowLabel} (${eff.windowHours}h)`);
151
+ console.log('');
152
+ console.log(`Stored config (${configPath()}):`);
153
+ console.log(` ${Object.keys(raw).length === 0 ? '(none — using defaults)' : JSON.stringify(raw)}`);
154
+ console.log('');
155
+ console.log('Change with: claude-token-saver mode <keywords...>');
156
+ console.log(`Keywords: ${VALID_KEYWORDS.join(', ')}`);
157
+ return;
158
+ }
159
+ const { applied, unknown } = applyMode(words);
160
+ if (unknown.length) {
161
+ console.error(`Unknown keyword${unknown.length > 1 ? 's' : ''}: ${unknown.join(', ')}`);
162
+ console.error(`Valid: ${VALID_KEYWORDS.join(', ')}`);
163
+ process.exit(1);
164
+ }
165
+ const eff = statuslineDefaults();
166
+ console.log(`Updated: ${applied.join(', ')}`);
167
+ console.log(`Now: icon=${eff.icon} verbose=${eff.verbose} timer=${eff.timer} color=${eff.color} window=${eff.windowLabel}`);
168
+ console.log('Statusline picks up the change on the next refresh (~1s).');
169
+ return;
170
+ }
171
+
65
172
  // Hook management
66
173
  if (hasFlag('--install-hook')) {
67
174
  const { installHook } = await import('../src/hook-manager.js');
@@ -85,10 +192,89 @@ async function main() {
85
192
  // Statusline mode shortcut
86
193
  const isStatusline = hasFlag('--statusline') || getArg('--format') === 'statusline';
87
194
 
195
+ // Demo mode — render synthetic warning-case data through the real
196
+ // formatter for screencasts/marketing GIFs. `--demo cycle` rotates through
197
+ // every scenario based on wall clock so a screen recorder picks them up.
198
+ const demoArg = getArg('--demo');
199
+
200
+ // `claude-token-saver --demo table` (no --statusline) — full table view
201
+ // with all six issue drill-downs at once, for marketing screencasts.
202
+ if (!isStatusline && demoArg === 'table') {
203
+ const { buildTableDemoData } = await import('../src/demo.js');
204
+ const { formatReport } = await import('../src/formatters/table.js');
205
+ const data = buildTableDemoData({ version: PKG_VERSION });
206
+ console.log(formatReport(data));
207
+ return;
208
+ }
209
+
210
+ if (isStatusline && demoArg) {
211
+ const { buildScenarioData, listScenarios } = await import('../src/demo.js');
212
+ const { statuslineDefaults } = await import('../src/config.js');
213
+ const cfg = statuslineDefaults();
214
+ const cycleSeconds = parseFloat(getArg('--demo-cycle-sec') || '3');
215
+ const data = buildScenarioData(demoArg, {
216
+ cycleSeconds,
217
+ windowHours: cfg.windowHours,
218
+ windowLabel: cfg.windowLabel,
219
+ days: cfg.windowHours / 24,
220
+ version: PKG_VERSION,
221
+ });
222
+ if (!data) {
223
+ const known = listScenarios().map((s) => s.name).concat(['cycle']).join(', ');
224
+ console.error(`Unknown demo scenario: ${demoArg}`);
225
+ console.error(`Valid: ${known}`);
226
+ process.exit(1);
227
+ }
228
+ const { formatReport } = await import('../src/formatters/statusline.js');
229
+ const isIcon = hasFlag('--icon')
230
+ ? true
231
+ : (hasFlag('--no-icon') || hasFlag('--text') ? false : cfg.icon);
232
+ const isVerbose = hasFlag('--verbose')
233
+ ? true
234
+ : (hasFlag('--no-verbose') || hasFlag('--compact') ? false : cfg.verbose);
235
+ const showTimer = hasFlag('--no-timer') ? false : cfg.timer;
236
+ const colorOk = !hasFlag('--no-color') && !process.env.NO_COLOR && cfg.color;
237
+ const out = formatReport(data, {
238
+ color: colorOk,
239
+ verbose: isVerbose,
240
+ timer: showTimer,
241
+ mode: isIcon ? 'icon' : 'text',
242
+ });
243
+ // For `cycle` mode, prefix with the scenario label so the screen recorder
244
+ // shows what the viewer is looking at (only when explicitly requested).
245
+ if (demoArg === 'cycle' && hasFlag('--demo-label')) {
246
+ const gray = colorOk ? '\x1b[90m' : '';
247
+ const reset = colorOk ? '\x1b[0m' : '';
248
+ console.log(`${gray}[${data._demoLabel}]${reset} ${out}`);
249
+ } else {
250
+ console.log(out);
251
+ }
252
+ return;
253
+ }
254
+
88
255
  // Report generation
89
- // Statusline default = 7 days (fast, called every ~300ms). Others = 30 days.
90
- const defaultDays = isStatusline ? 7 : 30;
91
- const days = parseInt(getArg('--days') || getArg('-d') || String(defaultDays), 10);
256
+ // Statusline window comes from persisted config hours-precise so users
257
+ // can pick `1h` / `6h` etc, not just whole days. Other formats default to
258
+ // 30 days as before.
259
+ let windowHours = 30 * 24;
260
+ let windowLabel = '30d';
261
+ if (isStatusline) {
262
+ const { statuslineDefaults } = await import('../src/config.js');
263
+ const d = statuslineDefaults();
264
+ windowHours = d.windowHours;
265
+ windowLabel = d.windowLabel;
266
+ }
267
+ // CLI overrides: --hours wins over --days; both win over config.
268
+ const hoursArg = getArg('--hours');
269
+ const daysArg = getArg('--days') || getArg('-d');
270
+ if (hoursArg !== undefined) {
271
+ windowHours = parseFloat(hoursArg);
272
+ windowLabel = `${windowHours}h`;
273
+ } else if (daysArg !== undefined) {
274
+ windowHours = parseFloat(daysArg) * 24;
275
+ windowLabel = `${parseFloat(daysArg)}d`;
276
+ }
277
+ const days = windowHours / 24;
92
278
  const format = isStatusline ? 'statusline' : (getArg('--format') || getArg('-f') || 'table');
93
279
  const projectFilter = getArg('--project') || getArg('-p');
94
280
 
@@ -136,9 +322,11 @@ async function main() {
136
322
  // actionable right now. 1M context is always shown; otherwise only fire
137
323
  // if the most recent session actually appears in the spike list.
138
324
  let spikeChip = null;
325
+ let chipDetail = null;
139
326
  if (format === 'statusline') {
140
327
  if (contextWindow.size === '1M') {
141
328
  spikeChip = chipForIssues([], contextWindow);
329
+ chipDetail = `Context auto-promoted to 1M (max single-request ${Math.round(contextWindow.maxContext / 1000)}k tokens)`;
142
330
  } else {
143
331
  const recentSession = sessions
144
332
  .slice()
@@ -150,8 +338,21 @@ async function main() {
150
338
  const m = sessionMetrics(recentSession);
151
339
  const issues = diagnoseSession(m, spikeReport.baseline);
152
340
  spikeChip = chipForIssues(issues, contextWindow);
341
+ const titles = issues
342
+ .map((i) => i.code)
343
+ .slice(0, 2)
344
+ .join(', ');
345
+ chipDetail = `session ${recentSession.sessionId?.slice(0, 8) || ''}: ${titles}`;
153
346
  }
154
347
  }
348
+ // Persist transitions to ~/.config/claude-token-saver/history/YYYY-MM-DD.md
349
+ // so /token-monitor and `claude-token-saver history` can replay them.
350
+ try {
351
+ const { recordChip } = await import('../src/history.js');
352
+ recordChip(spikeChip, { detail: chipDetail });
353
+ } catch {
354
+ // history is non-critical — don't let it break the statusline render
355
+ }
155
356
  }
156
357
 
157
358
  // Last API activity feeds the statusline TTL countdown.
@@ -181,7 +382,7 @@ async function main() {
181
382
  ttl,
182
383
  anomalies,
183
384
  cost,
184
- options: { days, version: PKG_VERSION },
385
+ options: { days, windowHours, windowLabel, version: PKG_VERSION },
185
386
  lastActivity,
186
387
  spikeReport,
187
388
  contextWindow,
@@ -197,13 +398,25 @@ async function main() {
197
398
  output = formatReport(data);
198
399
  } else if (format === 'statusline') {
199
400
  const { formatReport } = await import('../src/formatters/statusline.js');
200
- const colorOk = !hasFlag('--no-color') && !process.env.NO_COLOR;
201
- const mode = hasFlag('--icon') ? 'icon' : 'text';
401
+ const { statuslineDefaults } = await import('../src/config.js');
402
+ const cfg = statuslineDefaults();
403
+
404
+ // CLI flags take precedence; otherwise fall back to persisted config.
405
+ const isIcon = hasFlag('--icon')
406
+ ? true
407
+ : (hasFlag('--no-icon') || hasFlag('--text') ? false : cfg.icon);
408
+ const isVerbose = hasFlag('--verbose')
409
+ ? true
410
+ : (hasFlag('--no-verbose') || hasFlag('--compact') ? false : cfg.verbose);
411
+ const showTimer = hasFlag('--no-timer') ? false : cfg.timer;
412
+ const colorOk =
413
+ !hasFlag('--no-color') && !process.env.NO_COLOR && cfg.color;
414
+
202
415
  output = formatReport(data, {
203
416
  color: colorOk,
204
- verbose: hasFlag('--verbose'),
205
- timer: !hasFlag('--no-timer'),
206
- mode,
417
+ verbose: isVerbose,
418
+ timer: showTimer,
419
+ mode: isIcon ? 'icon' : 'text',
207
420
  });
208
421
  } else {
209
422
  const { formatReport } = await import('../src/formatters/table.js');
package/package.json CHANGED
@@ -1,11 +1,10 @@
1
1
  {
2
2
  "name": "claude-token-saver",
3
- "version": "2.0.2",
3
+ "version": "2.1.0",
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",
8
- "claude-cache-monitor": "./bin/cli.js"
7
+ "claude-token-saver": "./bin/cli.js"
9
8
  },
10
9
  "files": [
11
10
  "bin/",
package/src/advice.js CHANGED
@@ -28,94 +28,99 @@ function toggleShortcut() {
28
28
 
29
29
  export const ISSUE_MESSAGES = {
30
30
  LARGE_INPUT_PER_REQUEST: {
31
- title: '요청당 입력 토큰이 평소보다 매우 큽니다 (1M 컨텍스트 의심)',
31
+ title: 'Per-request input tokens are unusually large (1M context suspected)',
32
32
  explain:
33
- 'Opus 4.7부터 1M 컨텍스트가 표준 가격으로 풀리면서 Max 플랜은 자동으로 1M로 승격됩니다. ' +
34
- ' 컨텍스트가 200k 넘으면 long-context 단가가 적용되고 캐시 재사용도 어려워집니다.',
33
+ 'Since Opus 4.7, 1M context is priced at the standard rate, and Max plans auto-promote ' +
34
+ 'sessions to 1M. Once context goes past 200k, long-context pricing kicks in and cache reuse drops.',
35
35
  actions: () => [
36
36
  {
37
- label: '1M 컨텍스트 OFF (환경변수)',
37
+ label: 'Disable 1M context (env var)',
38
38
  commands: disable1mEnvSnippet(),
39
39
  },
40
40
  {
41
- label: '세션 내 토글',
42
- commands: [`단축키 ${toggleShortcut()} 즉시 On/Off`],
41
+ label: 'In-session toggle',
42
+ commands: [`Press ${toggleShortcut()} to toggle on/off instantly`],
43
43
  },
44
44
  {
45
- label: '⚠ 알려진 버그 #31640',
45
+ label: '⚠ Known bug #31640',
46
46
  commands: [
47
- '/model 200k 선택해도 컨텍스트가 1M에 머무는 케이스가 있습니다.',
48
- '확실히 끄려면 환경변수를 설정한 Claude Code를 재시작하세요.',
47
+ '/model 200k selection sometimes does not stick — context stays at 1M.',
48
+ 'To force off: set the env var above and restart Claude Code.',
49
49
  ],
50
50
  },
51
51
  ],
52
52
  },
53
53
  LOW_HIT_RATE: {
54
- title: '캐시 히트율이 낮습니다',
54
+ title: 'Cache hit rate is low',
55
55
  explain:
56
- '캐시 히트율이 떨어지면 같은 프롬프트 prefix 매번 다시 작성하게 되어 입력 비용이 커집니다.',
56
+ 'A low hit rate means the same prompt prefix is being rewritten on every call, ' +
57
+ 'inflating input cost.',
57
58
  actions: () => [
58
59
  {
59
- label: '세션을 너무 자주 새로 열지 않았는지 확인',
60
- commands: [' 작업은 같은 세션에서 이어가세요 (컨텍스트 전환 = 캐시 미스)'],
60
+ label: 'Avoid opening fresh sessions too often',
61
+ commands: ['Continue the same task in the same session (context switching = cache miss)'],
61
62
  },
62
63
  {
63
- label: '프롬프트 prefix 안정화',
64
- commands: ['시스템 프롬프트·도구 정의가 요청마다 바뀌면 캐시가 매번 무효화됩니다'],
64
+ label: 'Stabilize the prompt prefix',
65
+ commands: ['System prompts / tool definitions that change per request invalidate the cache every time'],
65
66
  },
66
67
  ],
67
68
  },
68
69
  BUCKET_5M_DOMINANT: {
69
- title: '5 TTL 쓰기가 대부분입니다',
70
+ title: 'Most cache writes are landing in the 5-minute TTL bucket',
70
71
  explain:
71
- 'Pro 플랜은 5분 TTL 고정됩니다. 5 이상 간격이 벌어지면 캐시가 만료되어 재작성 비용이 발생합니다.',
72
+ 'Pro plan is locked to 5m TTL. Gaps longer than 5 minutes expire the cache and force ' +
73
+ 'a costly rebuild.',
72
74
  actions: () => [
73
75
  {
74
- label: '5 규칙',
76
+ label: 'The 5-minute rule',
75
77
  commands: [
76
- '5 안에 아무 프롬프트라도 보내면 prefix 캐시가 유지됩니다',
77
- ' 작업이 필요하면 Max 플랜으로 1h TTL 자동 적용',
78
+ 'Sending any prompt within 5 minutes keeps the prefix cache warm',
79
+ 'Upgrade to Max for the 1h TTL bucket on long tasks',
78
80
  ],
79
81
  },
80
82
  ],
81
83
  },
82
84
  HIGH_OUTPUT_RATIO: {
83
- title: 'Output 비중이 비정상적으로 높습니다',
85
+ title: 'Output share is abnormally high',
84
86
  explain:
85
- 'Output 토큰은 입력보다 5배 이상 비쌉니다. 에이전트가 장문을 반복 생성하지 않는지 확인하세요.',
87
+ 'Output tokens are 5x+ pricier than input. Check whether the agent is regenerating ' +
88
+ 'long content unnecessarily.',
86
89
  actions: () => [
87
90
  {
88
- label: '출력 길이 제한',
91
+ label: 'Cap output length',
89
92
  commands: [
90
- '불필요한 전체 파일 쓰기·재생성 지양 (Edit 툴 활용)',
91
- ' 문서·README 생성 요청을 스크립트화해서 줄이세요',
93
+ 'Avoid full-file rewrites prefer the Edit tool',
94
+ 'Move long doc/README generation requests into scripts to shrink output',
92
95
  ],
93
96
  },
94
97
  ],
95
98
  },
96
99
  HIGH_REQUEST_COUNT: {
97
- title: '세션의 API 호출 수가 평소의 3배 이상입니다',
100
+ title: 'API calls in this session are 3x+ the baseline',
98
101
  explain:
99
- ' 호출이 과도하거나 루프/재시도가 많으면 호출당 prefix 재전송으로 입력 비용이 폭증합니다.',
102
+ 'Excessive tool calls or retry/loop patterns rebroadcast the prefix on every call, ' +
103
+ 'spiking input cost.',
100
104
  actions: () => [
101
105
  {
102
- label: '병렬/일괄 처리',
103
- commands: ['독립적인 조사는 메시지에 여러 도구 호출로 묶으세요'],
106
+ label: 'Parallel / batch processing',
107
+ commands: ['Bundle independent investigations into one message with multiple tool calls'],
104
108
  },
105
109
  {
106
- label: '루프 감지',
107
- commands: ['같은 테스트·검색을 반복하는 에이전트 루프가 없는지 확인'],
110
+ label: 'Watch for loops',
111
+ commands: ['Check that the agent is not repeating the same test/search in a loop'],
108
112
  },
109
113
  ],
110
114
  },
111
115
  FREQUENT_CACHE_REBUILD: {
112
- title: '캐시 쓰기가 읽기보다 많습니다',
116
+ title: 'Cache writes outweigh cache reads',
113
117
  explain:
114
- '캐시를 만들고 재사용하지 못하고 있습니다. 세션이 자주 끊기거나 TTL이 만료된 새로 시작한 경우 흔합니다.',
118
+ 'The cache is being created but not reused. Common when sessions are short-lived or ' +
119
+ 'restarted after TTL expiry.',
115
120
  actions: () => [
116
121
  {
117
- label: '세션 지속 시간 확인',
118
- commands: [' 작업은 같은 Claude Code 세션에서 이어가세요'],
122
+ label: 'Check session continuity',
123
+ commands: ['Continue one task in one Claude Code session'],
119
124
  },
120
125
  ],
121
126
  },
@@ -124,15 +129,17 @@ export const ISSUE_MESSAGES = {
124
129
  /**
125
130
  * For the statusline: the single most relevant short chip (1~2 words).
126
131
  * Priority reflects what a user can act on *right now*.
132
+ * Kept short and English-only — these ride along on a single-line statusline
133
+ * shown to a global audience.
127
134
  */
128
135
  export function chipForIssues(issues, contextWindow) {
129
- if (contextWindow?.size === '1M') return '⚠ 1M컨텍스트';
136
+ if (contextWindow?.size === '1M') return '⚠ 1M ON';
130
137
  const codes = issues.map((i) => i.code);
131
- if (codes.includes('LARGE_INPUT_PER_REQUEST')) return '⚠ 입력폭주';
138
+ if (codes.includes('LARGE_INPUT_PER_REQUEST')) return '⚠ Input spike';
132
139
  if (codes.includes('BUCKET_5M_DOMINANT')) return '⚠ 5m TTL';
133
- if (codes.includes('LOW_HIT_RATE')) return '⚠ 캐시미스';
134
- if (codes.includes('FREQUENT_CACHE_REBUILD')) return '⚠ 재작성';
135
- if (codes.includes('HIGH_OUTPUT_RATIO')) return '⚠ 출력과다';
136
- if (codes.includes('HIGH_REQUEST_COUNT')) return '⚠ 호출폭주';
140
+ if (codes.includes('LOW_HIT_RATE')) return '⚠ Cache miss';
141
+ if (codes.includes('FREQUENT_CACHE_REBUILD')) return '⚠ Rebuild churn';
142
+ if (codes.includes('HIGH_OUTPUT_RATIO')) return '⚠ Output heavy';
143
+ if (codes.includes('HIGH_REQUEST_COUNT')) return '⚠ Call surge';
137
144
  return null;
138
145
  }