claude-token-saver 2.8.6 → 2.9.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.en.md CHANGED
@@ -28,28 +28,23 @@ If postinstall was skipped (e.g. `--ignore-scripts`, sudo, or sandboxed installs
28
28
  claude-token-saver install
29
29
  ```
30
30
 
31
- ## What you see after install
31
+ ## Claude Code statusline
32
32
 
33
- **(A) One-shot report** `claude-token-saver`:
33
+ After install, Claude Code's bottom statusline updates every 5 seconds with cache state (postinstall registers it in `~/.claude/settings.json` automatically).
34
34
 
35
35
  ```
36
- Claude token saver Last 30 days
37
- Context window: 200k ✓ standard
38
- Cache hit rate: 98.2% | Total input: 1957.94M tokens
39
- TTL Breakdown / Cost Impact / Daily Trend ...
36
+ 🤖 Opus 4.7 · 🧠 Cache hit 98.0% · ⏳ Cache expires 59:51 · 📦 Ctx 200k · 💰 Cache saved $221 · last 1d
40
37
  ```
41
38
 
42
- If a session spiked, a `⚠ Spike detected` block leads the report with root cause + a paste-ready remediation command.
43
-
44
- **(B) Statusline chip** — once wired into `~/.claude/settings.json`:
39
+ Risk chips lead when something's wrong:
45
40
 
46
41
  ```
47
- 🧠 97.5% · 1h 42:15 · 💰 $4.8K · 🤖 Opus 4.7 · 5H 47% · 📅 7D 9%
42
+ 🚨 5H 94% (resets in 12m) · 🤖 Opus 4.7 · 🧠 Cache hit 72.1% · Cache miss · 📦 Ctx 200k · last 1d
48
43
  ```
49
44
 
50
- Risk chips lead when something's wrong: `🚨 5H 94%`, `⚠ 1M ON`, `⚠ Cache miss`, `⚠ 5m TTL`.
45
+ Risk chips: `🚨 5H/7D NN%`, `⚠ 1M ON`, `⚠ Input spike`, `⚠ Cache miss`, `⚠ 5m TTL`, `⚠ Rebuild churn`, `⚠ Output heavy`, `⚠ Call surge`.
51
46
 
52
- ## Wire up the statusline
47
+ If postinstall was skipped (you already use a different statusline, etc.), wire it manually:
53
48
 
54
49
  ```json
55
50
  {
@@ -61,19 +56,50 @@ Risk chips lead when something's wrong: `🚨 5H 94%`, `⚠ 1M ON`, `⚠ Cache m
61
56
  }
62
57
  ```
63
58
 
64
- `refreshInterval: 5` keeps the TTL countdown ticking while idle (Claude Code's statusline is otherwise event-driven). 1s also works, but 5s is the recommended default to avoid constant I/O. For Windows PowerShell, see `examples/statusline-command.ps1`.
59
+ `refreshInterval: 5` keeps the TTL countdown ticking while idle. For Windows PowerShell see `examples/statusline-command.ps1`.
60
+
61
+ ## Skill — mention a chip, it activates
62
+
63
+ The Claude Code Skill registered at install time auto-activates whenever you mention chip wording in chat ("cache hit rate", "1M context", "5H cap"). It runs `claude-token-saver last` to surface the most recent warning + remediation, then points you at `history` / `handoff` for follow-up. (v2.6.0 folded the legacy `/token-monitor` slash command into this Skill — older installs are cleaned up automatically the next time you run `claude-token-saver install`.)
64
+
65
+ ## One-shot report
66
+
67
+ Run `claude-token-saver` for the last-day diagnostic table:
68
+
69
+ ```
70
+ Claude Token Saver — Last 1 day
71
+ (claude-token-saver v2.9.0)
72
+ ══════════════════════════════════════════════════
73
+
74
+ Context window: 200k ✓ 200k context (standard)
75
+ Sessions: 11 | API calls: 578 | Cache hit rate: 98.0%
76
+ TTL Breakdown / Cost Impact / Daily Trend …
77
+ ```
78
+
79
+ If a session spiked, a `⚠ Spike detected` block leads the report with the root-cause code (table below) and an OS-aware remediation command.
80
+
81
+ ## Output language
82
+
83
+ `last` / `history` / advice messages render in one language at a time (statusline chips stay symbolic). English is the default — switch via:
84
+
85
+ ```bash
86
+ claude-token-saver mode ko # or: claude-token-saver mode lang=ko
87
+ claude-token-saver mode en # back to English
88
+ claude-token-saver mode # show current settings
89
+ ```
65
90
 
66
91
  ## Commands
67
92
 
68
93
  | Command | What it does |
69
94
  |---|---|
70
- | `claude-token-saver` | Last 30 days diagnostic report |
71
- | `claude-token-saver --days 7` | Change window |
72
- | `claude-token-saver --statusline --icon` | One-line statusline output |
73
- | `claude-token-saver install` | Manually register the Claude Code Skill (fallback when postinstall is skipped) |
95
+ | `claude-token-saver` | Last-1-day diagnostic report (`--days N` to change window) |
96
+ | `claude-token-saver last` | Most recent warning + remediation (the command the Skill invokes) |
74
97
  | `claude-token-saver history` | Last 7 days of chip transitions (1M ON, Cache miss, cap, …) |
75
98
  | `claude-token-saver handoff` | Back current work up to `HANDOFF-YYYY-MM-DD-HHMM.md` before a cap blocks you |
76
- | `claude-token-saver --install-hook` | Auto-log cache stats on every tool call |
99
+ | `claude-token-saver mode [keywords...]` | Configure output (`icon`/`text`, `en`/`ko`, `verbose`, `1d`/`7d`, …) |
100
+ | `claude-token-saver --statusline --icon` | One-line statusline output (called by Claude Code) |
101
+ | `claude-token-saver install` | Manually register Skill + statusline (postinstall fallback) |
102
+ | `claude-token-saver --install-hook` | Optionally auto-log cache stats on every tool call |
77
103
 
78
104
  ## Options
79
105
 
@@ -150,6 +176,21 @@ Node.js ≥ 18 · macOS / Windows / Linux / WSL · zero dependencies.
150
176
 
151
177
  **IntelliJ Claude Code plugin** — the statusline widget fuses prior and current frames at the character level when emoji are in the output, producing artifacts like `Cache expires 59:548`. v2.8.5+ detects `TERMINAL_EMULATOR=JetBrains-JediTerm` and falls back to text mode automatically (`--icon` is also ignored under IntelliJ). Other terminals (iTerm, Terminal, WSL, etc.) are unaffected.
152
178
 
179
+ ## Release notes
180
+
181
+ ### v2.9.0 (2026-04-27)
182
+ - **Output language is now configurable.** `last` / `history` / advice render in a single language at a time. English by default; switch with `claude-token-saver mode ko`. Statusline chips remain symbolic.
183
+ - History files stay bilingual on disk; the language toggle is applied at display time.
184
+
185
+ ### v2.8.6 (2026-04-27)
186
+ - **Skill auto-registers on install.** A `postinstall` hook wires the Claude Code Skill and statusline into `~/.claude` automatically — no second command. `claude-token-saver install` still works as a fallback for `--ignore-scripts` / sudo / sandboxed environments.
187
+ - README polish in both languages; corrected the `claude-cache-monitor` alias-removal note (timing was reversed).
188
+
189
+ ### v2.8.5
190
+ - IntelliJ Claude Code plugin: auto-fall back to text mode when `TERMINAL_EMULATOR=JetBrains-JediTerm` to avoid frame-fusion artefacts.
191
+
192
+ Older versions: see `git log`.
193
+
153
194
  ## License
154
195
 
155
196
  MIT
package/README.md CHANGED
@@ -28,28 +28,23 @@ npm i -g claude-token-saver
28
28
  claude-token-saver install
29
29
  ```
30
30
 
31
- ## 설치 보이는 것
31
+ ## Claude Code statusline
32
32
 
33
- **(A) 단발 리포트** `claude-token-saver` 실행 시:
33
+ 설치 Claude Code 하단 statusline에 캐시 상태가 5초마다 갱신됩니다 (postinstall이 `~/.claude/settings.json`에 자동 등록).
34
34
 
35
35
  ```
36
- Claude 토큰 아껴쓰기 Last 30 days
37
- Context window: 200k ✓ 표준
38
- Cache hit rate: 98.2% | Total input: 1957.94M tokens
39
- TTL Breakdown / Cost Impact / Daily Trend ...
36
+ 🤖 Opus 4.7 · 🧠 Cache hit 98.0% · ⏳ Cache expires 59:51 · 📦 Ctx 200k · 💰 Cache saved $221 · last 1d
40
37
  ```
41
38
 
42
- 급증 세션이 있으면 상단에 `⚠ 토큰 급증 감지` 블록과 원인·해결 명령이 함께 출력됩니다.
43
-
44
- **(B) statusline 한 줄** — `~/.claude/settings.json`에 등록 시:
39
+ 위험 상황에서는 경고 칩이 앞으로 나옵니다.
45
40
 
46
41
  ```
47
- 🧠 97.5% · 1h 42:15 · 💰 $4.8K · 🤖 Opus 4.7 · 5H 47% · 📅 7D 9%
42
+ 🚨 5H 94% (resets in 12m) · 🤖 Opus 4.7 · 🧠 Cache hit 72.1% · Cache miss · 📦 Ctx 200k · last 1d
48
43
  ```
49
44
 
50
- 위험 상황에서는 경고 칩이 맨 앞에 표시됩니다 — `🚨 5H 94%`, `⚠ 1M ON`, `⚠ Cache miss`, `⚠ 5m TTL` 등.
45
+ 표시되는 경고 — `🚨 5H/7D NN%`, `⚠ 1M ON`, `⚠ Input spike`, `⚠ Cache miss`, `⚠ 5m TTL`, `⚠ Rebuild churn`, `⚠ Output heavy`, `⚠ Call surge`.
51
46
 
52
- ## statusline 등록
47
+ 수동 등록이 필요한 경우(다른 statusline 이미 쓰고 있어 postinstall이 건너뛴 경우 등):
53
48
 
54
49
  ```json
55
50
  {
@@ -61,19 +56,50 @@ TTL Breakdown / Cost Impact / Daily Trend ...
61
56
  }
62
57
  ```
63
58
 
64
- `refreshInterval: 5`는 idle 상태에서도 TTL 카운트다운을 5초마다 갱신합니다 (1초도 가능하지만 상시 I/O 부담을 줄이기 위해 5초를 기본값으로 권장합니다). Windows(PowerShell) 환경은 `examples/statusline-command.ps1`을 참고하세요.
59
+ `refreshInterval: 5`는 idle 상태에서도 TTL 카운트다운을 5초마다 갱신합니다. Windows(PowerShell) `examples/statusline-command.ps1` 참고.
60
+
61
+ ## Skill — 칩을 언급하면 자동 활성화
62
+
63
+ 설치 시 함께 등록되는 Claude Code Skill 덕분에 대화 중 "cache hit rate", "1M context", "5H cap" 같은 칩 단어를 언급하면 Claude가 자동으로 `claude-token-saver last`를 실행해 가장 최근 경고와 처방을 보여주고, 필요 시 `history` / `handoff`로 후속 조치를 안내합니다. (v2.6.0에서 레거시 `/token-monitor` 슬래시 커맨드는 Skill로 흡수됐습니다 — 이전 버전 사용자는 `claude-token-saver install`을 한 번 다시 실행하면 자동 정리됩니다.)
64
+
65
+ ## 단발 리포트
66
+
67
+ `claude-token-saver`를 실행하면 최근 1일 진단 표가 출력됩니다.
68
+
69
+ ```
70
+ Claude Token Saver — Last 1 day
71
+ (claude-token-saver v2.9.0)
72
+ ══════════════════════════════════════════════════
73
+
74
+ Context window: 200k ✓ 200k context (standard)
75
+ Sessions: 11 | API calls: 578 | Cache hit rate: 98.0%
76
+ TTL Breakdown / Cost Impact / Daily Trend …
77
+ ```
78
+
79
+ 급증 세션이 있으면 상단에 `⚠ Spike detected` 블록과 원인 코드(아래 표) · OS별 해결 명령이 함께 출력됩니다.
80
+
81
+ ## 출력 언어 전환
82
+
83
+ `last` / `history` / 처방 메시지는 영어가 기본값이며 한 번에 한 언어만 출력합니다 (statusline 칩은 항상 동일한 기호 형식). 한국어로 바꾸려면:
84
+
85
+ ```bash
86
+ claude-token-saver mode ko # 또는: claude-token-saver mode lang=ko
87
+ claude-token-saver mode en # 영어로 복귀
88
+ claude-token-saver mode # 현재 설정 확인
89
+ ```
65
90
 
66
91
  ## 주요 명령
67
92
 
68
93
  | 명령 | 설명 |
69
94
  |---|---|
70
- | `claude-token-saver` | 최근 30일 진단 리포트 |
71
- | `claude-token-saver --days 7` | 기간 변경 |
72
- | `claude-token-saver --statusline --icon` | statusline용 한 줄 출력 |
73
- | `claude-token-saver install` | Claude Code Skill 수동 등록 (postinstall이 막힌 환경 대비) |
95
+ | `claude-token-saver` | 최근 1일 진단 리포트 (`--days N`로 기간 변경) |
96
+ | `claude-token-saver last` | 가장 최근 경고 1건 + 처방 (Skill이 호출하는 명령) |
74
97
  | `claude-token-saver history` | 최근 7일간 칩 전이 로그 (1M ON, Cache miss, cap 등) |
75
98
  | `claude-token-saver handoff` | 현재 작업을 `HANDOFF-YYYY-MM-DD-HHMM.md`로 백업 (cap 임박 시) |
76
- | `claude-token-saver --install-hook` | 도구 호출마다 캐시 통계 자동 로깅 |
99
+ | `claude-token-saver mode [keywords...]` | 출력 모드 설정 (`icon`/`text`, `ko`/`en`, `verbose`, `1d`/`7d` 등) |
100
+ | `claude-token-saver --statusline --icon` | statusline용 한 줄 출력 (Claude Code가 호출) |
101
+ | `claude-token-saver install` | Skill·statusline 수동 등록 (postinstall이 막힌 환경) |
102
+ | `claude-token-saver --install-hook` | 매 도구 호출마다 캐시 통계 자동 로깅 (선택) |
77
103
 
78
104
  전체 옵션은 `--help` 또는 [영문 README](./README.en.md#options).
79
105
 
@@ -110,6 +136,21 @@ Node.js ≥ 18 · macOS / Linux / Windows / WSL · 의존성 0.
110
136
 
111
137
  **IntelliJ Claude Code plugin** — statusline 위젯이 이전 프레임과 새 프레임을 글자 단위로 잘못 합쳐 `Cache expires 59:548` 같은 잔재 문자열이 보이는 버그가 있습니다 (이모지가 포함된 출력에서만 재현). v2.8.5+는 `TERMINAL_EMULATOR=JetBrains-JediTerm`을 감지하면 자동으로 text 모드로 폴백해 이모지 없이 출력합니다 (`--icon` 플래그도 IntelliJ에서는 무시됩니다). 다른 터미널(iTerm, Terminal, WSL 등)에는 영향 없습니다.
112
138
 
139
+ ## 릴리스 노트
140
+
141
+ ### v2.9.0 (2026-04-27)
142
+ - **출력 언어 전환 추가** — `last` / `history` / 처방 메시지가 한 번에 한 언어만 출력합니다. 기본은 영어, `claude-token-saver mode ko`로 한국어 전환 (statusline 칩은 영향 없음).
143
+ - 기존 history 파일은 이중언어로 보관되며, 표시할 때 선택한 언어만 필터링됩니다.
144
+
145
+ ### v2.8.6 (2026-04-27)
146
+ - **Skill 자동 등록** — `npm i -g claude-token-saver` 시 postinstall 훅이 Skill과 statusline을 자동으로 `~/.claude`에 등록. 수동 `claude-token-saver install`은 `--ignore-scripts` / sudo 환경용 폴백으로 유지.
147
+ - 한·영 README 문장 다듬기, `claude-cache-monitor` alias 제거 시점 설명 정정.
148
+
149
+ ### v2.8.5
150
+ - IntelliJ Claude Code plugin에서 statusline 프레임 합성 버그 회피 — `TERMINAL_EMULATOR=JetBrains-JediTerm` 감지 시 자동 text 모드.
151
+
152
+ 이전 버전은 `git log`를 참고하세요.
153
+
113
154
  ## 라이선스
114
155
 
115
156
  MIT
package/bin/cli.js CHANGED
@@ -230,6 +230,8 @@ async function main() {
230
230
  if (args[0] === 'last') {
231
231
  const { readRecent, historyDir } = await import('../src/history.js');
232
232
  const { ISSUE_MESSAGES, CHIP_TO_CODES, CAP_TIPS } = await import('../src/advice.js');
233
+ const { userLanguage } = await import('../src/config.js');
234
+ const lang = userLanguage();
233
235
  const days = parseFloat(getArg('--days') || '1');
234
236
  const recent = readRecent(days);
235
237
  const latest = findLatestWarning(recent, CHIP_TO_CODES);
@@ -247,10 +249,9 @@ async function main() {
247
249
  if (latest.isCap) {
248
250
  if (latest.capReset) console.log(` Cap window: ${latest.capReset}`);
249
251
  console.log('');
250
- console.log('💡 ' + CAP_TIPS.en);
251
- console.log('💡 ' + CAP_TIPS.ko);
252
+ console.log('💡 ' + (lang === 'ko' ? CAP_TIPS.ko : CAP_TIPS.en));
252
253
  console.log('');
253
- console.log('Run:');
254
+ console.log(lang === 'ko' ? '실행:' : 'Run:');
254
255
  console.log(' claude-token-saver handoff');
255
256
  return;
256
257
  }
@@ -258,31 +259,29 @@ async function main() {
258
259
  // bilingual (English first, `└ Korean` continuation per line — matches
259
260
  // the history.md format).
260
261
  if (latest.codes.length === 0) {
261
- console.log('(No diagnostic code attached — open the table view: `claude-token-saver --days 1`)');
262
- console.log('(진단 코드 없음 — 표 뷰를 열어보세요: `claude-token-saver --days 1`)');
262
+ console.log(lang === 'ko'
263
+ ? '(진단 코드 없음 — 표 뷰를 열어보세요: `claude-token-saver --days 1`)'
264
+ : '(No diagnostic code attached — open the table view: `claude-token-saver --days 1`)');
263
265
  return;
264
266
  }
267
+ // Pick a single language per field; fall back to EN when KO is missing.
268
+ const pick = (en, ko) => (lang === 'ko' && ko ? ko : en);
265
269
  for (const code of latest.codes) {
266
270
  const msg = ISSUE_MESSAGES[code];
267
271
  if (!msg) {
268
272
  console.log(`Code: ${code} (no advice registered)`);
269
273
  continue;
270
274
  }
271
- console.log(`▎ ${msg.title}`);
272
- if (msg.titleKo && msg.titleKo !== msg.title) console.log(` ${msg.titleKo}`);
273
- console.log(` ${msg.explain}`);
274
- if (msg.explainKo && msg.explainKo !== msg.explain) console.log(` └ ${msg.explainKo}`);
275
+ console.log(`▎ ${pick(msg.title, msg.titleKo)}`);
276
+ console.log(` ${pick(msg.explain, msg.explainKo)}`);
275
277
  const actions = typeof msg.actions === 'function' ? msg.actions() : msg.actions || [];
276
278
  for (const a of actions) {
277
279
  console.log('');
278
- console.log(` ${a.label}:`);
279
- if (a.labelKo && a.labelKo !== a.label) console.log(` └ ${a.labelKo}:`);
280
+ console.log(` ${pick(a.label, a.labelKo)}:`);
280
281
  const cmds = a.commands || [];
281
282
  const cmdsKo = a.commandsKo || [];
282
283
  for (let i = 0; i < cmds.length; i++) {
283
- console.log(` - ${cmds[i]}`);
284
- const ko = cmdsKo[i];
285
- if (ko && ko !== cmds[i]) console.log(` └ ${ko}`);
284
+ console.log(` - ${pick(cmds[i], cmdsKo[i])}`);
286
285
  }
287
286
  }
288
287
  console.log('');
@@ -297,7 +296,10 @@ async function main() {
297
296
  // claude-token-saver history --days 30 # custom window
298
297
  // claude-token-saver history --list # just list available dates
299
298
  if (args[0] === 'history') {
300
- const { readRecent, listDates, historyDir } = await import('../src/history.js');
299
+ const { readRecent, listDates, historyDir, formatHistoryForLanguage } =
300
+ await import('../src/history.js');
301
+ const { userLanguage } = await import('../src/config.js');
302
+ const lang = userLanguage();
301
303
  if (hasFlag('--list')) {
302
304
  const dates = listDates();
303
305
  if (dates.length === 0) {
@@ -316,7 +318,8 @@ async function main() {
316
318
  return;
317
319
  }
318
320
  for (const { content } of recent) {
319
- console.log(content.replace(/\n+$/, ''));
321
+ const filtered = formatHistoryForLanguage(content, lang);
322
+ console.log(filtered.replace(/\n+$/, ''));
320
323
  console.log('');
321
324
  }
322
325
  return;
@@ -400,11 +403,12 @@ async function main() {
400
403
  const eff = statuslineDefaults();
401
404
  const raw = loadConfig().statusline || {};
402
405
  console.log('Statusline mode (effective):');
403
- console.log(` icon: ${eff.icon}`);
404
- console.log(` verbose: ${eff.verbose}`);
405
- console.log(` timer: ${eff.timer}`);
406
- console.log(` color: ${eff.color}`);
407
- console.log(` window: ${eff.windowLabel} (${eff.windowHours}h)`);
406
+ console.log(` icon: ${eff.icon}`);
407
+ console.log(` verbose: ${eff.verbose}`);
408
+ console.log(` timer: ${eff.timer}`);
409
+ console.log(` color: ${eff.color}`);
410
+ console.log(` language: ${eff.language} (advice / history / last)`);
411
+ console.log(` window: ${eff.windowLabel} (${eff.windowHours}h)`);
408
412
  console.log('');
409
413
  console.log(`Stored config (${configPath()}):`);
410
414
  console.log(` ${Object.keys(raw).length === 0 ? '(none — using defaults)' : JSON.stringify(raw)}`);
@@ -421,7 +425,7 @@ async function main() {
421
425
  }
422
426
  const eff = statuslineDefaults();
423
427
  console.log(`Updated: ${applied.join(', ')}`);
424
- console.log(`Now: icon=${eff.icon} verbose=${eff.verbose} timer=${eff.timer} color=${eff.color} window=${eff.windowLabel}`);
428
+ console.log(`Now: icon=${eff.icon} verbose=${eff.verbose} timer=${eff.timer} color=${eff.color} language=${eff.language} window=${eff.windowLabel}`);
425
429
  console.log('Statusline picks up the change on the next refresh (~1s).');
426
430
  return;
427
431
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-token-saver",
3
- "version": "2.8.6",
3
+ "version": "2.9.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": {
package/src/config.js CHANGED
@@ -45,6 +45,12 @@ const KEYWORDS = {
45
45
  'no-timer': { key: 'timer', value: false },
46
46
  color: { key: 'color', value: true },
47
47
  'no-color': { key: 'color', value: false },
48
+ // Language for advice / history / `last` output. Default is `en`.
49
+ // Statusline chips themselves stay symbolic (e.g. `🚨 5H 94%`) regardless.
50
+ en: { key: 'language', value: 'en' },
51
+ english: { key: 'language', value: 'en' },
52
+ ko: { key: 'language', value: 'ko' },
53
+ korean: { key: 'language', value: 'ko' },
48
54
  };
49
55
 
50
56
  // Window preset accepts forms like:
@@ -68,6 +74,7 @@ function parseWindow(word) {
68
74
  export const VALID_KEYWORDS = Object.keys(KEYWORDS).concat([
69
75
  '<N>h (e.g. 1h, 6h, 24h)',
70
76
  '<N>d (e.g. 1d, 7d, 30d)',
77
+ 'lang=en | lang=ko',
71
78
  'reset',
72
79
  'default',
73
80
  ]);
@@ -90,6 +97,13 @@ export function applyMode(words) {
90
97
  applied.push(lower);
91
98
  continue;
92
99
  }
100
+ const langMatch = lower.match(/^lang(?:uage)?=(en|english|ko|korean)$/);
101
+ if (langMatch) {
102
+ const v = langMatch[1].startsWith('k') ? 'ko' : 'en';
103
+ cfg.statusline.language = v;
104
+ applied.push(`lang=${v}`);
105
+ continue;
106
+ }
93
107
  const hours = parseWindow(lower);
94
108
  if (hours !== null && hours > 0) {
95
109
  cfg.statusline.windowHours = hours;
@@ -136,11 +150,19 @@ export function statuslineDefaults() {
136
150
  verbose: s.verbose !== false,
137
151
  timer: s.timer !== false,
138
152
  color: s.color !== false,
153
+ language: s.language === 'ko' ? 'ko' : 'en',
139
154
  windowHours,
140
155
  windowLabel: formatWindow(windowHours),
141
156
  };
142
157
  }
143
158
 
159
+ // Resolve the user's preferred output language for advice/history/last.
160
+ // Returns 'en' or 'ko'; defaults to 'en' for first-time users.
161
+ export function userLanguage() {
162
+ const s = loadConfig().statusline || {};
163
+ return s.language === 'ko' ? 'ko' : 'en';
164
+ }
165
+
144
166
  /**
145
167
  * Render hours as the most natural unit:
146
168
  * 24h → "1d", 168h → "7d", 6h → "6h", 36h → "36h" (not whole days).
package/src/history.js CHANGED
@@ -151,7 +151,9 @@ function tipsForEvent(chip, detail) {
151
151
  const tip = ISSUE_TIPS[code];
152
152
  if (!tip) continue;
153
153
  enLines.push(` 💡 ${tip.en}`);
154
- koLines.push(` 💡 ${tip.ko}`);
154
+ // KR tip carries the `└` marker so language-filtered renderers can
155
+ // pair it with its EN counterpart (mirrors the event-line continuation).
156
+ koLines.push(` └ 💡 ${tip.ko}`);
155
157
  }
156
158
  return { en: enLines.join('\n'), ko: koLines.join('\n') };
157
159
  }
@@ -288,7 +290,7 @@ export function recordCapTransition(window) {
288
290
  ko = `✓ ${labelKo} 캡 경고 해소`;
289
291
  }
290
292
  // Cap-warn entry → handoff tip; resolution → no tip (just the ✓ line).
291
- const capTips = isWarn ? { en: ` 💡 ${CAP_TIPS.en}`, ko: ` 💡 ${CAP_TIPS.ko}` } : null;
293
+ const capTips = isWarn ? { en: ` 💡 ${CAP_TIPS.en}`, ko: ` 💡 ${CAP_TIPS.ko}` } : null;
292
294
  appendDayLine(en, ko, now, capTips);
293
295
  state[slotKey] = isWarn;
294
296
  saveState(state);
@@ -310,6 +312,57 @@ export function recordHandoff(filePath) {
310
312
  return true;
311
313
  }
312
314
 
315
+ /**
316
+ * Filter a daily-history file's content down to a single language.
317
+ *
318
+ * History files are written bilingual (EN line + ` └ KO` continuation +
319
+ * ` 💡 EN tip` + ` └ 💡 KO tip`). This helper renders just the chosen side
320
+ * for display, while the on-disk file stays bilingual for archival.
321
+ *
322
+ * en: drop every ` └ ...` line (KR continuations + KR tips).
323
+ * ko: replace each EN line with the immediately following ` └ KO` line
324
+ * (preserving the leading `- HH:MM:SS` from the EN line so timestamps
325
+ * still appear); drop unpaired EN-only events.
326
+ *
327
+ * Lines that don't match the bilingual pattern (headers, blank lines,
328
+ * legacy entries from older versions without the `└ 💡` marker) pass through
329
+ * unchanged.
330
+ */
331
+ export function formatHistoryForLanguage(content, lang) {
332
+ if (lang !== 'ko') {
333
+ // English: simply strip the `└` continuations.
334
+ return content
335
+ .split('\n')
336
+ .filter((line) => !/^\s*└\s/.test(line))
337
+ .join('\n');
338
+ }
339
+ // Korean: pair each line with its `└` continuation when present.
340
+ const lines = content.split('\n');
341
+ const out = [];
342
+ for (let i = 0; i < lines.length; i++) {
343
+ const line = lines[i];
344
+ const next = lines[i + 1] || '';
345
+ const cont = next.match(/^(\s*)└\s+(.*)$/);
346
+ // Event line: `- HH:MM:SS <english>` → keep the timestamp prefix, swap text.
347
+ const evt = line.match(/^(- \d{2}:\d{2}:\d{2}\s+)(.*)$/);
348
+ if (evt && cont) {
349
+ out.push(`${evt[1]}${cont[2]}`);
350
+ i++;
351
+ continue;
352
+ }
353
+ // Tip line: ` 💡 <english>` paired with ` └ 💡 <korean>`.
354
+ const tip = line.match(/^(\s*)💡\s+(.*)$/);
355
+ if (tip && cont && /^💡\s/.test(cont[2])) {
356
+ out.push(`${tip[1]}${cont[2]}`);
357
+ i++;
358
+ continue;
359
+ }
360
+ // Header / blank / unpaired line — pass through.
361
+ out.push(line);
362
+ }
363
+ return out.join('\n');
364
+ }
365
+
313
366
  /**
314
367
  * List all available history file dates (sorted newest first).
315
368
  */