claude-token-saver 3.23.3 β†’ 3.25.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
@@ -130,6 +130,8 @@ With an empty ledger (no measured delegation yet) row 1 is not drawn and the lay
130
130
  | `✦ current` / `πŸ“… weekly` | 5-hour / 7-day rate-limit window usage + reset time |
131
131
  | `πŸ“¦` | Context usage (e.g. `Ctx 68% of 1M`) β€” colored by fill. Current models default to 1M with no premium, but token volume itself drives per-turn cost and 5H/7D burn |
132
132
  | `πŸ’°` | Cumulative savings from prompt caching β€” a **different** number from row 1's `πŸ”€` (model routing) |
133
+ | `v3.24.0` | The version you are running. Gray, at the tail, when it is the latest one |
134
+ | `⬆ v3.24.0 β†’ 3.25.0` | A newer release exists. Actionable, so it moves to the front of the line ([Update notifications](#-update-notifications)) |
133
135
 
134
136
  When something is wrong, a **warning chip leads the line**:
135
137
 
@@ -158,7 +160,12 @@ Run these in your shell (inside Claude Code, the `/claude-token-saver` Skill is
158
160
  | `claude-token-saver route-scan` | Detect recurring easy work on expensive models β†’ propose haiku-delegation ratchet rules (below) |
159
161
  | `claude-token-saver route-scan savings` | The routing-savings ledger β€” per-model-change rollup + per-run log (the evidence behind the figure) |
160
162
  | `claude-token-saver compact-window` | Warn when a 1M-context session has no auto-compact cap β†’ pin 400k with `set` (below) |
161
- | `claude-token-saver korean on\|off\|status` | Inject Korean writing guidance at session start (below) |
163
+ | `claude-token-saver korean on\|off\|status` | Inject Korean writing guidance at session start and install the write-time check (below) |
164
+ | `claude-token-saver korean lint block\|warn\|off` | How the write-time check handles findings |
165
+ | `claude-token-saver korean lint scope all\|prose` | Check every text file, or documents only |
166
+ | `claude-token-saver --version` | Print the installed version |
167
+ | `claude-token-saver update-check` | Is a newer version out? (`--refresh` to ask now, `--dismiss` to mute this version's offer) |
168
+ | `claude-token-saver upgrade` | Install the latest release with the package manager that installed this copy (`--print` shows the command only) |
162
169
  | `claude-token-saver install` | Manually register Skill + statusline |
163
170
 
164
171
  Switch output language with `mode ko` / `mode en` (English default; statusline chips stay symbolic).
@@ -182,6 +189,16 @@ Switch output language with `mode ko` / `mode en` (English default; statusline c
182
189
  | `--install-hook` / `--uninstall-hook` | Manage the PostToolUse hook | – |
183
190
  </details>
184
191
 
192
+ ## ⬆ Update notifications
193
+
194
+ A statusline cannot open a dialog, and it re-renders every ~300ms, so it can never touch the network while drawing. The notification is therefore split in two:
195
+
196
+ - **The statusline tells you.** Up to date: a quiet gray `v3.24.0` at the tail. Newer release out: `⬆ v3.24.0 β†’ 3.25.0` in yellow, moved to the front. Never red β€” nothing is broken.
197
+ - **Session start asks you.** On a new session or `/clear`, the SessionStart hook injects one line telling the model a newer version exists and to ask before installing anything. Only after you agree does it run `claude-token-saver upgrade`.
198
+ - **Declining sticks.** `claude-token-saver update-check --dismiss` mutes the offer for that version; the next release asks again. The statusline chip stays β€” you declined the question, not the fact.
199
+
200
+ The registry lookup runs at most once every 24h in a detached background process and only ever writes a cache file (`update-check.json`) β€” the same shape npm's `update-notifier` uses. A failed check still stamps its timestamp, so an offline machine backs off instead of retrying on every render. Turn checks off entirely with `CTS_NO_UPDATE_CHECK=1` or `NO_UPDATE_NOTIFIER`.
201
+
185
202
  ## πŸ…· Harness mode
186
203
 
187
204
  Bootstrap five engineering principles (Ratchet Β· Evidence Β· PEV Β· Structured Task Β· Default Safe Path) into `CLAUDE.md` with one command; the statusline scores it as `πŸ…· 5/5`. When the same error keeps recurring, a `πŸ…·βš  ratchet?` nudge appears so you can promote it to a rule.
@@ -313,6 +330,29 @@ Three things change. Clauses chained with em dashes become separate sentences, s
313
330
 
314
331
  The technical content is identical in both. The guidance touches sentence construction only, not judgement or accuracy: the answer does not change, it just stops needing a second read. In a channel people scroll through, that difference cuts follow-up questions β€” and the tokens those follow-ups would have cost.
315
332
 
333
+ ### The write-time check (v3.24.0)
334
+
335
+ Injecting the guidance once at session start turned out to be half the job. The model reads it, then writes dozens of files over the next hours with nothing re-reading the output. Sessions with the guidance active still shipped violations into documents, and it surfaced only when a human read the finished artifact. An August 2026 fix reworded the scope sentence to address this; it recurred, because rewording an instruction does not add a checkpoint.
336
+
337
+ From v3.24.0 `korean on` also installs a PostToolUse hook. It opens the file the model just wrote, runs the clauses a machine can decide, and hands any findings back. The file is already saved, so nothing is lost β€” the model fixes it on the spot.
338
+
339
+ ```bash
340
+ claude-token-saver korean lint block # default: findings are handed back as blocking feedback
341
+ claude-token-saver korean lint warn # print findings, do not block
342
+ claude-token-saver korean lint off # disable the check
343
+
344
+ claude-token-saver korean lint scope all # default: every text file the session writes
345
+ claude-token-saver korean lint scope prose # documents only
346
+
347
+ claude-token-saver korean lint docs/*.md # check files already on disk
348
+ ```
349
+
350
+ Checked: 15 figurative phrases, translationese markers, separators (`β€”`Β·`γ…‘`Β·`|`), three or more `의` particles in one phrase, and a period after a nominal ending. Clauses that need judgement stay with the guidance text.
351
+
352
+ The default `all` scope covers code comments, UI strings, subtitles, templates, and build output, not just documents. The vendored guidance exempts comments, but comments are read by people and generated artifacts (PDF, HTML) are assembled from those strings, so exempting them reopens the exact gap that was reported. Only installed dependencies, VCS internals, lockfiles, and binary or image files are skipped; `dist/` and `build/` are checked. `korean lint scope prose` restores the narrow reading.
353
+
354
+ The scope sentence in the injected guidance is generated from the same setting, so the model is never told one rule while being corrected against another.
355
+
316
356
  ### The encoding rule that ships with it (v3.23.2)
317
357
 
318
358
  Alongside the writing guidance, one more line is injected: **non-ASCII strings in tool-call parameters must be written as literal UTF-8, never as `\uXXXX` unicode escapes.**
@@ -414,6 +454,11 @@ Also update `statusLine.command` in `~/.claude/settings.json` to `claude-token-s
414
454
 
415
455
  ## Release notes
416
456
 
457
+ ### v3.25.0 (2026-09-04)
458
+ - **The statusline now shows which version is running** β€” until now the version lived only in the table report's footer, so "which version am I on" meant running a full report. A `--version` flag was added alongside it.
459
+ - **A new release asks at session start** β€” a statusline cannot open a dialog, so telling and asking are split. The statusline only reports (`⬆ v3.24.0 β†’ 3.25.0`); the SessionStart hook injects a line telling the model to ask the user whether to upgrade. On yes it runs `claude-token-saver upgrade`, which uses the package manager that installed this copy; on no, `update-check --dismiss` mutes that version until a newer one ships.
460
+ - **The check never blocks a render** β€” the registry lookup runs at most once every 24h in a detached background process, and the render path only reads the cache file. A failed check still stamps its timestamp, so an offline machine backs off instead of retrying every render. Disable with `CTS_NO_UPDATE_CHECK=1` or `NO_UPDATE_NOTIFIER`.
461
+
417
462
  ### v3.21.0 (2026-08-22)
418
463
  - **The install shows what it is about to enable, then asks** β€” the harness 5 principles and the Korean writing guidance used to be switched on by the installer, leaving the user with the result rather than the choice. The install now prints the five principle headings, and for the Korean guidance what it changes plus its per-session cost and source, before asking. Locale detection is demoted from an answer to the question's default, so an English-locale machine used for Korean work can enable it on the spot.
419
464
  - **Unattended installs behave exactly as before** β€” npm `postinstall`, CI, piped stdin and `CTS_NO_INPUT=1` skip the question and apply the old defaults, because a blocked prompt hangs an install. `--yes` and `--no-input` force that path explicitly. A non-interactive install on a non-Korean machine now leaves the setting undecided instead of recording an answer nobody gave, so a later run at a terminal still asks.
package/README.md CHANGED
@@ -129,6 +129,8 @@ npm i -g claude-token-saver
129
129
  | `✦ current` / `πŸ“… weekly` | 5μ‹œκ°„ / 7일 rate-limit μœˆλ„ μ‚¬μš©λ₯  + 리셋 μ‹œκ° |
130
130
  | `πŸ“¦` | μ»¨ν…μŠ€νŠΈ μ‚¬μš©λ₯ μž…λ‹ˆλ‹€(예: `Ctx 68% of 1M`). μ‚¬μš©λ₯ μ— 따라 λ…Ήμƒ‰Β·λ…Έλž€μƒ‰Β·λΉ¨κ°„μƒ‰μœΌλ‘œ ν‘œμ‹œν•©λ‹ˆλ‹€. μ΅œμ‹  λͺ¨λΈμ€ 1M μ»¨ν…μŠ€νŠΈκ°€ 기본이고 별도 μš”κΈˆμ΄ λΆ™μ§€ μ•Šμ§€λ§Œ, ν† ν°λŸ‰ μžμ²΄κ°€ ν„΄λ‹Ή λΉ„μš©κ³Ό 5μ‹œκ°„Β·7일 ν•œλ„λ₯Ό λΉ λ₯΄κ²Œ μ†Œλͺ¨μ‹œν‚΅λ‹ˆλ‹€ |
131
131
  | `πŸ’°` | ν”„λ‘¬ν”„νŠΈ μΊμ‹œκ°€ μ ˆμ•½ν•΄ μ€€ λˆ„μ  κΈˆμ•‘μž…λ‹ˆλ‹€. 첫째 μ€„μ˜ `πŸ”€`(λͺ¨λΈ λΌμš°νŒ… μ ˆκ°μ•‘)μ™€λŠ” **μ„œλ‘œ λ‹€λ₯Έ μˆ˜μΉ˜μž…λ‹ˆλ‹€** |
132
+ | `v3.24.0` | μ§€κΈˆ μ‹€ν–‰ 쀑인 claude-token-saver의 λ²„μ „μž…λ‹ˆλ‹€. μ΅œμ‹ μ΄λ©΄ νšŒμƒ‰μœΌλ‘œ 쀄 끝에 쑰용히 λ†“μž…λ‹ˆλ‹€ |
133
+ | `⬆ v3.24.0 β†’ 3.25.0` | μƒˆ 버전이 λ°°ν¬λ˜μ–΄ μžˆλ‹€λŠ” ν‘œμ‹œμž…λ‹ˆλ‹€. μ‘°μΉ˜κ°€ ν•„μš”ν•œ μΉ©μ΄λ―€λ‘œ 쀄 μ•žμͺ½μœΌλ‘œ μ˜¬λΌμ˜΅λ‹ˆλ‹€ ([μ—…λ°μ΄νŠΈ μ•ˆλ‚΄](#-μ—…λ°μ΄νŠΈ-μ•ˆλ‚΄)) |
132
134
 
133
135
  λ¬Έμ œκ°€ κ°μ§€λ˜λ©΄ **κ²½κ³  칩을 쀄 맨 μ•žμ—** λΆ™μž…λ‹ˆλ‹€.
134
136
 
@@ -157,11 +159,26 @@ Claude Code μ•ˆμ—μ„œ `/claude-token-saver` Skill을 μ‹€ν–‰ν•˜κ±°λ‚˜, 칩에 적
157
159
  | `claude-token-saver route-scan` | μƒμœ„ λͺ¨λΈμ΄ 반볡 μ²˜λ¦¬ν•œ μ‰¬μš΄ μž‘μ—…μ„ 감지해 haiku μœ„μž„ λž«μ³‡ 룰을 μ œμ•ˆν•©λ‹ˆλ‹€ (μ•„λž˜ μ°Έκ³ ) |
158
160
  | `claude-token-saver route-scan savings` | λΌμš°νŒ… 절감 원μž₯μž…λ‹ˆλ‹€. λͺ¨λΈ 이동별 합계와 싀행별 내역을 ν•¨κ»˜ 보여 μ£Όλ©°, ν‘œμ‹œλ˜λŠ” κΈˆμ•‘μ˜ κ·Όκ±°κ°€ λ©λ‹ˆλ‹€ |
159
161
  | `claude-token-saver compact-window` | 1M μ»¨ν…μŠ€νŠΈλ₯Ό μ“°λ©΄μ„œ μžλ™ μ••μΆ• 창이 μ„€μ •λ˜μ§€ μ•Šμ•˜μœΌλ©΄ κ²½κ³ ν•˜κ³ , `set`으둜 40λ§Œμ— κ³ μ •ν•©λ‹ˆλ‹€ (μ•„λž˜ μ°Έκ³ ) |
160
- | `claude-token-saver korean on\|off\|status` | ν•œκ΅­μ–΄ 문체 지침을 μ„Έμ…˜ μ‹œμž‘ μ‹œ μ£Όμž…ν•©λ‹ˆλ‹€ (μ•„λž˜ μ°Έκ³ ) |
162
+ | `claude-token-saver korean on\|off\|status` | ν•œκ΅­μ–΄ 문체 지침을 μ„Έμ…˜ μ‹œμž‘ μ‹œ μ£Όμž…ν•˜κ³ , μ“°κΈ° μ‹œμ  검사λ₯Ό ν•¨κ»˜ μ„€μΉ˜ν•©λ‹ˆλ‹€ (μ•„λž˜ μ°Έκ³ ) |
163
+ | `claude-token-saver korean lint block\|warn\|off` | μ“°κΈ° μ‹œμ  검사가 μœ„λ°˜μ„ μ–΄λ–»κ²Œ μ²˜λ¦¬ν• μ§€ μ •ν•©λ‹ˆλ‹€ |
164
+ | `claude-token-saver korean lint scope all\|prose` | 검사 λ²”μœ„λ₯Ό λͺ¨λ“  ν…μŠ€νŠΈ 파일과 λ¬Έμ„œ μ „μš© μ‚¬μ΄μ—μ„œ κ³ λ¦…λ‹ˆλ‹€ |
165
+ | `claude-token-saver --version` | μ„€μΉ˜λœ 버전을 좜λ ₯ν•©λ‹ˆλ‹€ |
166
+ | `claude-token-saver update-check` | μƒˆ 버전이 μžˆλŠ”μ§€ ν™•μΈν•©λ‹ˆλ‹€ (`--refresh`둜 μ¦‰μ‹œ 쑰회, `--dismiss`둜 κ·Έ 버전 μ•ˆλ‚΄ 끄기) |
167
+ | `claude-token-saver upgrade` | μ„€μΉ˜ κ²½λ‘œμ— λ§žλŠ” λͺ…λ ΉμœΌλ‘œ μ΅œμ‹  버전을 μ„€μΉ˜ν•©λ‹ˆλ‹€ (`--print`둜 μ‹€ν–‰ 없이 λͺ…λ Ήλ§Œ 확인) |
161
168
  | `claude-token-saver install` | SkillΒ·statusline μˆ˜λ™ 등둝 |
162
169
 
163
170
  좜λ ₯ μ–Έμ–΄λŠ” `mode ko`와 `mode en`으둜 μ „ν™˜ν•©λ‹ˆλ‹€. 기본값은 μ˜μ–΄μ΄λ©°, statusline의 칩은 μ–Έμ œλ‚˜ 기호둜 ν‘œμ‹œν•©λ‹ˆλ‹€. 전체 μ˜΅μ…˜μ€ [영문 README](./README.en.md#options)λ₯Ό μ°Έκ³ ν•˜μ‹­μ‹œμ˜€.
164
171
 
172
+ ## ⬆ μ—…λ°μ΄νŠΈ μ•ˆλ‚΄
173
+
174
+ statusline은 λŒ€ν™” μƒμžλ₯Ό λ„μšΈ 수 μ—†κ³ , 300λ°€λ¦¬μ΄ˆλ§ˆλ‹€ λ‹€μ‹œ κ·Έλ €μ§€κΈ° λ•Œλ¬Έμ— κ·Έλ¦¬λŠ” μ‹œμ μ— λ„€νŠΈμ›Œν¬λ₯Ό μ“Έ μˆ˜λ„ μ—†μŠ΅λ‹ˆλ‹€. κ·Έλž˜μ„œ μ•ˆλ‚΄λ₯Ό 두 μ§€μ μœΌλ‘œ λ‚˜λˆ„μ—ˆμŠ΅λ‹ˆλ‹€.
175
+
176
+ - **statusline은 μ•Œλ¦¬κΈ°λ§Œ ν•©λ‹ˆλ‹€.** μ΅œμ‹  버전이면 쀄 끝에 `v3.24.0`을 νšŒμƒ‰μœΌλ‘œ 쑰용히 ν‘œμ‹œν•˜κ³ , μƒˆ 버전이 있으면 `⬆ v3.24.0 β†’ 3.25.0`을 쀄 μ•žμͺ½μ— λ…Έλž€μƒ‰μœΌλ‘œ μ˜¬λ¦½λ‹ˆλ‹€. 빨간색은 μ“°μ§€ μ•ŠμŠ΅λ‹ˆλ‹€. 무엇도 κ³ μž₯ λ‚œ μƒνƒœκ°€ μ•„λ‹ˆκΈ° λ•Œλ¬Έμž…λ‹ˆλ‹€.
177
+ - **λ¬»λŠ” 일은 μ„Έμ…˜ μ‹œμž‘μ—μ„œ ν•©λ‹ˆλ‹€.** μƒˆ μ„Έμ…˜μ΄λ‚˜ `/clear` μ‹œμ μ— SessionStart 훅이 "μƒˆ 버전이 μžˆμœΌλ‹ˆ μ‚¬μš©μžμ—κ²Œ μ—…κ·Έλ ˆμ΄λ“œν• μ§€ 물어보라"λŠ” ν•œ 쀄을 λͺ¨λΈμ—κ²Œ μ£Όμž…ν•©λ‹ˆλ‹€. λͺ¨λΈμ€ μ‚¬μš©μžμ—κ²Œ ν™•μΈν•œ λ’€μ—λ§Œ `claude-token-saver upgrade`λ₯Ό μ‹€ν–‰ν•©λ‹ˆλ‹€. 묻지 μ•Šκ³  μ„€μΉ˜ν•˜μ§€ μ•ŠμŠ΅λ‹ˆλ‹€.
178
+ - **κ±°μ ˆμ€ κΈ°μ–΅ν•©λ‹ˆλ‹€.** μ‚¬μš©μžκ°€ μ›μΉ˜ μ•ŠμœΌλ©΄ `claude-token-saver update-check --dismiss`둜 κ·Έ 버전을 묻지 μ•Šλ„λ‘ μ„€μ •ν•©λ‹ˆλ‹€. 더 μƒˆλ‘œμš΄ 버전이 배포되면 λ‹€μ‹œ λ¬»μŠ΅λ‹ˆλ‹€. statusline 칩은 κ·ΈλŒ€λ‘œ λ‚¨μŠ΅λ‹ˆλ‹€. κ±°μ ˆν•œ 것은 μ§ˆλ¬Έμ΄μ§€, μƒˆ 버전이 μžˆλ‹€λŠ” 사싀이 μ•„λ‹ˆκΈ° λ•Œλ¬Έμž…λ‹ˆλ‹€.
179
+
180
+ 버전 μ‘°νšŒλŠ” 24μ‹œκ°„μ— ν•œ 번, λΆ„λ¦¬λœ λ°±κ·ΈλΌμš΄λ“œ ν”„λ‘œμ„ΈμŠ€κ°€ μˆ˜ν–‰ν•˜κ³  결과만 νŒŒμΌμ— λ‚¨κΉλ‹ˆλ‹€(`update-check.json`). 이 방식은 npm의 `update-notifier`κ°€ μ“°λŠ” 것과 κ°™μŠ΅λ‹ˆλ‹€. λ„€νŠΈμ›Œν¬κ°€ 끊겨 μžˆμ–΄λ„ μ‹€νŒ¨ μ‹œκ°μ„ 기둝해 λ‘λ―€λ‘œ λ§€ λ Œλ”λ§ˆλ‹€ μž¬μ‹œλ„ν•˜μ§€ μ•ŠμŠ΅λ‹ˆλ‹€. 확인 자체λ₯Ό 끄렀면 ν™˜κ²½ λ³€μˆ˜ `CTS_NO_UPDATE_CHECK=1` λ˜λŠ” `NO_UPDATE_NOTIFIER`λ₯Ό μ„€μ •ν•˜μ‹­μ‹œμ˜€.
181
+
165
182
  ## πŸ…· Harness λͺ¨λ“œ
166
183
 
167
184
  λ‹€μ„― 원칙(Ratchet Β· Evidence Β· PEV Β· Structured Task Β· Default Safe Path)을 ν•œ 쀄 λͺ…λ ΉμœΌλ‘œ `CLAUDE.md`에 μ…‹μ—…ν•˜κ³  statusline이 `πŸ…· 5/5`둜 μ μˆ˜ν™”ν•©λ‹ˆλ‹€. 같은 μ—λŸ¬κ°€ 반볡되면 `πŸ…·βš  ratchet?` μ•Œλ¦Όμ΄ λ– μ„œ 룰둜 μŠΉκ²©ν•  수 μžˆμŠ΅λ‹ˆλ‹€.
@@ -303,6 +320,29 @@ Claude Code의 output styleλ‘œλ„ 같은 일을 ν•  수 μžˆμ§€λ§Œ, output style
303
320
 
304
321
  기술적 λ‚΄μš©μ€ μ–‘μͺ½μ΄ λ™μΌν•©λ‹ˆλ‹€. 지침은 νŒλ‹¨μ΄λ‚˜ 정확도가 μ•„λ‹ˆλΌ λ¬Έμž₯의 μ™„μ„±λ„μ—λ§Œ κ΄€μ—¬ν•˜λ―€λ‘œ, 닡이 λ‹¬λΌμ§€λŠ” 것이 μ•„λ‹ˆλΌ 같은 닡을 λ‹€μ‹œ 읽지 μ•Šμ•„λ„ λ˜λŠ” ν˜•νƒœλ‘œ λ§Œλ“€μ–΄ μ€λ‹ˆλ‹€. μŠ¬λž™μ²˜λŸΌ μ‚¬λžŒμ΄ μŠ€ν¬λ‘€ν•˜λ©° μ½λŠ” μ±„λ„μ—μ„œλŠ” 이 차이가 λ˜λ¬»λŠ” 횟수λ₯Ό 쀄이고, λ˜λ¬»μ§€ μ•ŠλŠ” 만큼 토큰도 μ•„λ‚λ‹ˆλ‹€.
305
322
 
323
+ ### μ“°κΈ° μ‹œμ  검사 (v3.24.0)
324
+
325
+ 지침을 μ„Έμ…˜ μ‹œμž‘μ— ν•œ 번 λ„£λŠ” κ²ƒλ§ŒμœΌλ‘œλŠ” λΆ€μ‘±ν–ˆμŠ΅λ‹ˆλ‹€. λͺ¨λΈμ€ 지침을 ν•œ 번 읽고 κ·Έ λ’€λ‘œ 파일 μˆ˜μ‹­ 개λ₯Ό μ“°λŠ”λ°, κ·Έλ™μ•ˆ 결과물을 λ‹€μ‹œ 읽어 λ³΄λŠ” 단계가 μ—†μ—ˆμŠ΅λ‹ˆλ‹€. κ·Έλž˜μ„œ 지침이 μΌœμ§„ μ„Έμ…˜μ΄ κ·œμ•½μ— μ–΄κΈ‹λ‚˜λŠ” λ¬Έμž₯을 λ¬Έμ„œμ— κ·ΈλŒ€λ‘œ μ‹€μ–΄ λ³΄λƒˆκ³ , μ‚¬λžŒμ΄ 완성본을 읽을 λ•Œμ—μ•Ό λ“œλŸ¬λ‚¬μŠ΅λ‹ˆλ‹€. 2026λ…„ 8월에 적용 λ²”μœ„ λ¬Έμž₯을 κ³ μ³μ„œ 같은 문제λ₯Ό 작으렀 ν–ˆμ§€λ§Œ, λ¬Έμž₯을 고쳐도 검사 단계가 μ—†λ‹€λŠ” 쑰건은 κ·ΈλŒ€λ‘œμ˜€κΈ° λ•Œλ¬Έμ— μž¬λ°œν–ˆμŠ΅λ‹ˆλ‹€.
326
+
327
+ v3.24.0λΆ€ν„° `korean on`이 PostToolUse 훅을 ν•¨κ»˜ μ„€μΉ˜ν•©λ‹ˆλ‹€. λͺ¨λΈμ΄ 방금 μ“΄ νŒŒμΌμ„ μ—΄μ–΄μ„œ κΈ°κ³„λ‘œ νŒμ •ν•  수 μžˆλŠ” 쑰항을 κ²€μ‚¬ν•˜κ³ , μœ„λ°˜μ΄ 있으면 λͺ¨λΈμ—κ²Œ 되돌렀 λ³΄λƒ…λ‹ˆλ‹€. νŒŒμΌμ€ 이미 μ €μž₯된 λ’€μ΄λ―€λ‘œ μžƒλŠ” 것은 μ—†κ³ , λͺ¨λΈμ΄ κ·Έ μžλ¦¬μ—μ„œ κ³ μΉ©λ‹ˆλ‹€.
328
+
329
+ ```bash
330
+ claude-token-saver korean lint block # κΈ°λ³Έκ°’. μœ„λ°˜μ„ 되돌렀 보내 고치게 ν•©λ‹ˆλ‹€
331
+ claude-token-saver korean lint warn # μ•Œλ¦¬κΈ°λ§Œ ν•˜κ³  진행을 막지 μ•ŠμŠ΅λ‹ˆλ‹€
332
+ claude-token-saver korean lint off # κ²€μ‚¬ν•˜μ§€ μ•ŠμŠ΅λ‹ˆλ‹€
333
+
334
+ claude-token-saver korean lint scope all # κΈ°λ³Έκ°’. μ„Έμ…˜μ΄ μ“΄ λͺ¨λ“  ν…μŠ€νŠΈ 파일
335
+ claude-token-saver korean lint scope prose # λ§ˆν¬λ‹€μš΄Β·ν…μŠ€νŠΈ λ¬Έμ„œλ§Œ
336
+
337
+ claude-token-saver korean lint docs/*.md # 이미 μ €μž₯된 νŒŒμΌμ„ 직접 검사
338
+ ```
339
+
340
+ 검사 ν•­λͺ©μ€ μ‚¬λžŒμ΄ νŒμ •ν•  ν•„μš”κ°€ μ—†λŠ” κ²ƒλ“€μž…λ‹ˆλ‹€. λΉ„μœ  μ–΄νœ˜ 15μ’…(`~λŠ” 자리`, `~의 흐름`, `λ‹ΏλŠ”λ‹€`, `κ±·μ–΄λ‚΄λ‹€`, `발λͺ©μ„ μž‘λ‹€` λ“±), λ²ˆμ—­μ²΄ ν‘œμ§€(`~에 λŒ€ν•œ`, `~λ₯Ό μœ„ν•œ`, `~λ˜μ–΄μ§€`), κ΅¬λΆ„μž(`β€”`Β·`γ…‘`Β·`|`), ν•œ ꡬ에 μ„Έ 번 이상 μ΄μ–΄μ§€λŠ” 쑰사 `의`, λͺ…μ‚¬ν˜• μ’…κ²° λ’€μ˜ λ§ˆμΉ¨ν‘œμž…λ‹ˆλ‹€. νŒλ‹¨μ΄ ν•„μš”ν•œ μ‘°ν•­(μ„±λΆ„ μƒλž΅, ν•œμžμ–΄ 선택)은 κ·ΈλŒ€λ‘œ 지침이 λ‹΄λ‹Ήν•©λ‹ˆλ‹€.
341
+
342
+ κΈ°λ³Έ λ²”μœ„ `all`은 λ¬Έμ„œλΏ μ•„λ‹ˆλΌ **μ½”λ“œ 주석과 화면에 λ‚˜κ°€λŠ” λ¬Έμžμ—΄, μžλ§‰Β·ν…œν”Œλ¦Ώ, 생성 κ²°κ³Όλ¬ΌκΉŒμ§€** κ²€μ‚¬ν•©λ‹ˆλ‹€. λ²€λ”λ§ν•œ μ§€μΉ¨ 원문은 μ½”λ“œ 주석을 μ˜ˆμ™Έλ‘œ λ‘μ§€λ§Œ, 주석도 μ‚¬λžŒμ΄ 읽고 PDFΒ·HTML 같은 μ‚°μΆœλ¬Όμ€ κ·Έ λ¬Έμžμ—΄λ“€λ‘œ 쑰립되기 λ•Œλ¬Έμ— μ˜ˆμ™Έλ‘œ 두면 μ •ν™•νžˆ λ¬Έμ œκ°€ 됐던 κ²½λ‘œκ°€ λ‹€μ‹œ μ—΄λ¦½λ‹ˆλ‹€. κ²€μ‚¬μ—μ„œ λΉ μ§€λŠ” 것은 μ„€μΉ˜λœ μ˜μ‘΄μ„±(`node_modules`), VCS λ‚΄λΆ€, 락 파일, 그리고 이진·이미지 νŒŒμΌλΏμž…λ‹ˆλ‹€. `dist`λ‚˜ `build` 같은 μ‚°μΆœλ¬Ό λ””λ ‰ν„°λ¦¬λŠ” κ²€μ‚¬ν•©λ‹ˆλ‹€. 원문 κ·œμ•½λŒ€λ‘œ λ¬Έμ„œλ§Œ 보고 μ‹ΆμœΌλ©΄ `korean lint scope prose`둜 λ˜λŒλ¦½λ‹ˆλ‹€.
343
+
344
+ μ£Όμž…λ¬Έμ˜ 적용 λ²”μœ„ μ•ˆλ‚΄λ„ 이 μ„€μ •μ—μ„œ μƒμ„±ν•©λ‹ˆλ‹€. λͺ¨λΈμ—κ²Œ μ•Œλ¦¬λŠ” λ²”μœ„μ™€ κ²€μ‚¬ν•˜λŠ” λ²”μœ„κ°€ κ°ˆλΌμ§€λ©΄ 8μ›”κ³Ό 같은 μƒνƒœλ‘œ λŒμ•„κ°€κΈ° λ•Œλ¬Έμž…λ‹ˆλ‹€.
345
+
306
346
  ### ν•¨κ»˜ μ£Όμž…λ˜λŠ” 인코딩 κ·œμΉ™ (v3.23.2)
307
347
 
308
348
  문체 μ§€μΉ¨κ³Ό λ³„λ„λ‘œ, **도ꡬ 호좜 μΈμžμ— λ‹΄λŠ” λΉ„ASCII λ¬Έμžμ—΄μ€ λ¦¬ν„°λŸ΄ UTF-8둜 μ“°κ³  `\uXXXX` μœ λ‹ˆμ½”λ“œ μ΄μŠ€μΌ€μ΄ν”„λ‘œλŠ” μ“°μ§€ 말라**λŠ” ν•œ 쀄이 같이 μ£Όμž…λ©λ‹ˆλ‹€.
@@ -380,6 +420,11 @@ npm uninstall -g claude-cache-monitor && npm i -g claude-token-saver
380
420
 
381
421
  ## 릴리슀 λ…ΈνŠΈ
382
422
 
423
+ ### v3.25.0 (2026-09-04)
424
+ - **statusline에 μ‹€ν–‰ 쀑인 버전을 ν‘œμ‹œν•©λ‹ˆλ‹€.** μ§€κΈˆκΉŒμ§€λŠ” ν‘œ ν˜•μ‹ 리포트의 κ°μ£Όμ—λ§Œ 버전이 μžˆμ—ˆκΈ° λ•Œλ¬Έμ—, μ–΄λ–€ 버전이 λ„λŠ”μ§€ ν™•μΈν•˜λ €λ©΄ 전체 리포트λ₯Ό μ‹€ν–‰ν•΄μ•Ό ν–ˆμŠ΅λ‹ˆλ‹€. `--version` ν”Œλž˜κ·Έλ„ ν•¨κ»˜ μΆ”κ°€ν–ˆμŠ΅λ‹ˆλ‹€.
425
+ - **μƒˆ 버전이 λ‚˜μ˜€λ©΄ μ„Έμ…˜ μ‹œμž‘μ— λ¬Όμ–΄λ΄…λ‹ˆλ‹€.** statusline은 λŒ€ν™” μƒμžλ₯Ό λ„μšΈ 수 μ—†μœΌλ―€λ‘œ, μ•Œλ¦Όκ³Ό μ§ˆλ¬Έμ„ λ‚˜λˆ„μ—ˆμŠ΅λ‹ˆλ‹€. statusline은 `⬆ v3.24.0 β†’ 3.25.0`으둜 μ•Œλ¦¬κΈ°λ§Œ ν•˜κ³ , μ‹€μ œ μ§ˆλ¬Έμ€ SessionStart 훅이 λͺ¨λΈμ—κ²Œ "μ‚¬μš©μžμ—κ²Œ μ—…κ·Έλ ˆμ΄λ“œ μ—¬λΆ€λ₯Ό ν™•μΈν•˜λΌ"κ³  μ£Όμž…ν•΄μ„œ μ΄λ£¨μ–΄μ§‘λ‹ˆλ‹€. μŠΉλ‚™ν•˜λ©΄ `claude-token-saver upgrade`κ°€ μ„€μΉ˜ κ²½λ‘œμ— λ§žλŠ” λͺ…령을 μ‹€ν–‰ν•˜κ³ , κ±°μ ˆν•˜λ©΄ `update-check --dismiss`κ°€ κ·Έ 버전을 더 묻지 μ•Šλ„λ‘ κΈ°λ‘ν•©λ‹ˆλ‹€.
426
+ - **버전 확인이 λ Œλ”λ₯Ό λΆ™μž‘μ§€ μ•ŠμŠ΅λ‹ˆλ‹€.** μ‘°νšŒλŠ” 24μ‹œκ°„μ— ν•œ 번 λΆ„λ¦¬λœ λ°±κ·ΈλΌμš΄λ“œ ν”„λ‘œμ„ΈμŠ€κ°€ μˆ˜ν–‰ν•˜κ³ , λ Œλ” κ²½λ‘œλŠ” μΊμ‹œ 파일만 μ½μŠ΅λ‹ˆλ‹€. 확인에 μ‹€νŒ¨ν•΄λ„ μ‹œκ°μ„ κΈ°λ‘ν•˜λ―€λ‘œ μ˜€ν”„λΌμΈμ—μ„œ λ§€ λ Œλ”λ§ˆλ‹€ μž¬μ‹œλ„ν•˜μ§€ μ•ŠμŠ΅λ‹ˆλ‹€. `CTS_NO_UPDATE_CHECK=1` λ˜λŠ” `NO_UPDATE_NOTIFIER`둜 끌 수 μžˆμŠ΅λ‹ˆλ‹€.
427
+
383
428
  ### v3.21.0 (2026-08-22)
384
429
  - **μ„€μΉ˜κ°€ 무엇을 μΌœλŠ”μ§€ 보여 μ€€ λ’€ λ¬Όμ–΄λ΄…λ‹ˆλ‹€** β€” μ§€κΈˆκΉŒμ§€λŠ” πŸ…· Harness 5원칙과 ν•œκ΅­μ–΄ 문체 지침을 μ„€μΉ˜κ°€ 슀슀둜 μΌ°κ³ , μ‚¬μš©μžλŠ” 결과만 λ³Ό 수 μžˆμ—ˆμŠ΅λ‹ˆλ‹€. 이제 harnessλŠ” λ‹€μ„― μ›μΉ™μ˜ 제λͺ©μ„, ν•œκ΅­μ–΄ 지침은 λ°”κΎΈλŠ” λ‚΄μš©κ³Ό μ„Έμ…˜λ‹Ή λΉ„μš©κ³Ό 좜처λ₯Ό λ¨Όμ € 좜λ ₯ν•œ λ‹€μŒ μΌ€μ§€ λ¬»μŠ΅λ‹ˆλ‹€. 둜캘 κ°μ§€λŠ” 닡이 μ•„λ‹ˆλΌ 질문의 κΈ°λ³Έκ°’μœΌλ‘œ λ‚΄λ €μ™”μœΌλ―€λ‘œ, μ˜μ–΄ λ‘œμΊ˜μ—μ„œ ν•œκ΅­μ–΄λ‘œ μž‘μ—…ν•˜λŠ” κ²½μš°μ—λ„ κ·Έ μžλ¦¬μ—μ„œ μΌ€ 수 μžˆμŠ΅λ‹ˆλ‹€.
385
430
  - **μ‚¬λžŒμ΄ λΆ™μ–΄ μžˆμ§€ μ•Šμ€ μ„€μΉ˜λŠ” μ˜ˆμ „λŒ€λ‘œ λ™μž‘ν•©λ‹ˆλ‹€** β€” npm의 `postinstall`, CI, νŒŒμ΄ν”„ μž…λ ₯, `CTS_NO_INPUT=1`μ—μ„œλŠ” μ§ˆλ¬Έμ„ κ±΄λ„ˆλ›°κ³  κΈ°μ‘΄ 기본값을 κ·ΈλŒ€λ‘œ μ μš©ν•©λ‹ˆλ‹€. ν”„λ‘¬ν”„νŠΈκ°€ 멈좰 μ„œλ©΄ μ„€μΉ˜κ°€ 걸리기 λ•Œλ¬Έμž…λ‹ˆλ‹€. `--yes`와 `--no-input`으둜 직접 κ°•μ œν•  μˆ˜λ„ μžˆμŠ΅λ‹ˆλ‹€. λΉ„λŒ€ν™”ν˜•μ΄λ©΄μ„œ 둜캘이 ν•œκ΅­μ–΄κ°€ μ•„λ‹Œ κ²½μš°μ—λŠ” 섀정을 μ €μž₯ν•˜μ§€ μ•Šκ³  λ―Έκ²°μ •μœΌλ‘œ 남겨 λ‘λ―€λ‘œ, λ‚˜μ€‘μ— ν„°λ―Έλ„μ—μ„œ μ„€μΉ˜ν•˜λ©΄ κ·Έλ•Œ λ¬Όμ–΄λ΄…λ‹ˆλ‹€.
package/bin/cli.js CHANGED
@@ -52,6 +52,7 @@ import { estimateCost } from '../src/cost.js';
52
52
  import { chipForIssues } from '../src/advice.js';
53
53
  import { debug } from '../src/debug.js';
54
54
  import { createArgs } from '../src/cli-args.js';
55
+ import { updateStatus, maybeSpawnUpdateCheck } from '../src/update-check.js';
55
56
 
56
57
  const args = process.argv.slice(2);
57
58
 
@@ -66,6 +67,22 @@ const PKG_VERSION = (() => {
66
67
 
67
68
  const { getArg, hasFlag, numArg } = createArgs(args);
68
69
 
70
+ /**
71
+ * Version/update state for the statusline chip. Reads a cache file and, when
72
+ * that cache has aged past the check interval, kicks a detached child to
73
+ * refresh it for a later render. Never awaits the network, never throws into
74
+ * the render: a registry outage must not cost the statusline its other chips.
75
+ */
76
+ function readUpdateChip() {
77
+ try {
78
+ maybeSpawnUpdateCheck(PKG_VERSION);
79
+ return updateStatus(PKG_VERSION);
80
+ } catch (e) {
81
+ debug('update-check:chip', e);
82
+ return null;
83
+ }
84
+ }
85
+
69
86
  async function main() {
70
87
  // Subcommand: last β€” print the most recent warning + how to handle it.
71
88
  // Designed for the auto-trigger skill so the user immediately sees
@@ -166,6 +183,37 @@ async function main() {
166
183
  return (await import('../src/commands/compact-window.js')).run({ args, hasFlag });
167
184
  }
168
185
 
186
+ // `--version` / `-v` β€” the flag every CLI is expected to answer. Until now
187
+ // the version was only visible in the table view's footer, which meant
188
+ // "which version am I on" required running a full report.
189
+ if (hasFlag('--version') || hasFlag('-v')) {
190
+ console.log(PKG_VERSION);
191
+ return;
192
+ }
193
+
194
+ // Subcommand: update-check β€” the registry lookup behind the ⬆ statusline
195
+ // chip and the session-start upgrade offer.
196
+ // claude-token-saver update-check # print cached status
197
+ // claude-token-saver update-check --refresh # hit the registry now (detached child uses this)
198
+ // claude-token-saver update-check --dismiss # stop offering THIS version at session start
199
+ if (args[0] === 'update-check') {
200
+ return (await import('../src/commands/update-check.js')).run({
201
+ hasFlag,
202
+ version: PKG_VERSION,
203
+ });
204
+ }
205
+
206
+ // Subcommand: upgrade β€” run the install command that matches how this copy
207
+ // got here, then confirm the new version.
208
+ // claude-token-saver upgrade # install the latest release
209
+ // claude-token-saver upgrade --print # just show the command, run nothing
210
+ if (args[0] === 'upgrade') {
211
+ return (await import('../src/commands/upgrade.js')).run({
212
+ hasFlag,
213
+ version: PKG_VERSION,
214
+ });
215
+ }
216
+
169
217
  // Hook management
170
218
  if (hasFlag('--install-hook')) {
171
219
  const { installHook } = await import('../src/hook-manager.js');
@@ -319,7 +367,7 @@ async function main() {
319
367
  } catch (e) { debug('caps-cache:persist', e); }
320
368
  }
321
369
  console.log(formatNoSession(
322
- { caps, model, windowLabel },
370
+ { caps, model, windowLabel, version: PKG_VERSION, update: readUpdateChip() },
323
371
  { color: colorOk, mode: isIcon ? 'icon' : 'text' },
324
372
  ));
325
373
  return;
@@ -471,6 +519,8 @@ async function main() {
471
519
  anomalies,
472
520
  cost,
473
521
  options: { days, windowHours, windowLabel, version: PKG_VERSION },
522
+ // Cached-only; the background refresh it may trigger lands on a later render.
523
+ update: format === 'statusline' ? readUpdateChip() : null,
474
524
  lastActivity,
475
525
  spikeReport,
476
526
  contextWindow,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-token-saver",
3
- "version": "3.23.3",
3
+ "version": "3.25.0",
4
4
  "description": "Route the easy work your expensive Claude model keeps repeating down to haiku/sonnet β€” post-hoc session analysis, no realtime router, no extra LLM calls.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -11,14 +11,144 @@
11
11
  * machine. This ships the guidance with the package and delivers it through
12
12
  * the SessionStart hook that is already installed, so it applies everywhere
13
13
  * the CLI is installed and leaves the output-style slot free.
14
+ *
15
+ * Injection alone turned out to be half the job. The guidance is read once at
16
+ * session start and never again, so a long session writes documents that drift
17
+ * back to the patterns it forbids, and the drift is caught only when a human
18
+ * reads the finished file. `korean on` therefore also installs a PostToolUse
19
+ * hook that runs the machine-checkable clauses over the prose the model just
20
+ * wrote:
21
+ * claude-token-saver korean lint block|warn|off # how findings are handled
22
+ * claude-token-saver korean lint <file...> # check files on disk
14
23
  */
15
24
 
16
- export async function run({ args }) {
25
+ // Enforcement is only useful if it is on by default β€” a check the user has to
26
+ // discover is the same hole in a different shape.
27
+ function koreanLintMode(cfg) {
28
+ const mode = cfg?.koreanStyle?.lint;
29
+ return mode === 'off' || mode === 'warn' ? mode : 'block';
30
+ }
31
+
32
+ // `all` checks every text file the session writes, including Korean sitting in
33
+ // comments and UI strings. That is wider than the vendored guidance's own
34
+ // exemption list, and deliberately so: a comment is read by a person, and
35
+ // generated artifacts (PDF, HTML, captions) are assembled from those strings,
36
+ // so exempting them reopens the gap for exactly the outputs users complained
37
+ // about. `prose` restores the narrow reading.
38
+ function koreanLintScope(cfg) {
39
+ return cfg?.koreanStyle?.lintScope === 'prose' ? 'prose' : 'all';
40
+ }
41
+
42
+ export async function run({ args, hasFlag }) {
17
43
  const sub = args[1] || 'status';
18
44
  const ks = await import('../korean-style.js');
19
- const { userLanguage } = await import('../config.js');
45
+ const { userLanguage, loadConfig, saveConfig } = await import('../config.js');
20
46
  const lang = userLanguage();
21
47
 
48
+ // PostToolUse hook. Claude Code feeds the tool-call payload on stdin; we
49
+ // check the prose the model just wrote and hand any findings straight back
50
+ // to it. Silent and cheap when the file is clean, which is the common case.
51
+ if (hasFlag?.('--hook') || sub === '--hook') {
52
+ const { readStdinJson } = await import('../stdin-payload.js');
53
+ const payload = readStdinJson();
54
+ if (!payload || !ks.koreanStyleEnabled()) return;
55
+ const mode = koreanLintMode(loadConfig());
56
+ if (mode === 'off') return;
57
+ const { createRequire } = await import('node:module');
58
+ const req = createRequire(import.meta.url);
59
+ const lint = req('../korean-lint.cjs');
60
+ const result = lint.lintToolUse(payload, { scope: koreanLintScope(loadConfig()) });
61
+ if (!result) return;
62
+ const message = lint.formatFindings(result.filePath, result.findings);
63
+ if (mode === 'block') {
64
+ // Exit 2 is Claude Code's blocking-feedback channel: the file is already
65
+ // written, so nothing is lost, but the model must address this before it
66
+ // moves on. That is the whole point β€” a warning it can scroll past is
67
+ // what failed in the first place.
68
+ process.stderr.write(message + '\n');
69
+ process.exit(2);
70
+ }
71
+ console.log(message);
72
+ return;
73
+ }
74
+
75
+ // Manual run over files already on disk: `korean lint docs/*.md`.
76
+ if (sub === 'lint' && args.length > 2 && !['on', 'off', 'warn', 'block', 'scope'].includes(args[2])) {
77
+ const { readFileSync } = await import('node:fs');
78
+ const { createRequire } = await import('node:module');
79
+ const lint = createRequire(import.meta.url)('../korean-lint.cjs');
80
+ let total = 0;
81
+ for (const file of args.slice(2)) {
82
+ let text;
83
+ try {
84
+ text = readFileSync(file, 'utf8');
85
+ } catch (e) {
86
+ console.error(`${file}: ${e.message}`);
87
+ continue;
88
+ }
89
+ const findings = lint.lintKoreanText(text, { code: !lint.isProseFile(file) });
90
+ total += findings.length;
91
+ if (findings.length) console.log(lint.formatFindings(file, findings));
92
+ }
93
+ if (total === 0) {
94
+ console.log(lang === 'ko' ? '문체 κ·œμ•½ μœ„λ°˜μ΄ μ—†μŠ΅λ‹ˆλ‹€.' : 'No findings.');
95
+ }
96
+ process.exit(total > 0 ? 1 : 0);
97
+ }
98
+
99
+ // Scope switch: `korean lint scope all|prose`.
100
+ if (sub === 'lint' && args[2] === 'scope') {
101
+ const cfg = loadConfig();
102
+ const next = args[3];
103
+ if (!next) {
104
+ console.log(lang === 'ko'
105
+ ? `검사 λ²”μœ„: ${koreanLintScope(cfg)}`
106
+ : `Check scope: ${koreanLintScope(cfg)}`);
107
+ return;
108
+ }
109
+ if (!['all', 'prose'].includes(next)) {
110
+ console.error('Usage: claude-token-saver korean lint scope [all|prose]');
111
+ process.exit(1);
112
+ }
113
+ cfg.koreanStyle = cfg.koreanStyle || {};
114
+ cfg.koreanStyle.lintScope = next;
115
+ saveConfig(cfg);
116
+ console.log(lang === 'ko'
117
+ ? (next === 'all'
118
+ ? '검사 λ²”μœ„: all β€” λ¬Έμ„œμ™€ μ½”λ“œ 주석, UI λ¬Έμžμ—΄κΉŒμ§€ μ„Έμ…˜μ΄ μ“΄ λͺ¨λ“  ν…μŠ€νŠΈ νŒŒμΌμ„ κ²€μ‚¬ν•©λ‹ˆλ‹€.'
119
+ : '검사 λ²”μœ„: prose β€” λ§ˆν¬λ‹€μš΄κ³Ό ν…μŠ€νŠΈ λ¬Έμ„œλ§Œ κ²€μ‚¬ν•©λ‹ˆλ‹€.')
120
+ : (next === 'all'
121
+ ? 'Check scope: all β€” every text file the session writes, comments and UI strings included.'
122
+ : 'Check scope: prose β€” documents only.'));
123
+ return;
124
+ }
125
+
126
+ // Enforcement mode: `korean lint block|warn|off`.
127
+ if (sub === 'lint') {
128
+ const mode = args[2];
129
+ const cfg = loadConfig();
130
+ if (!mode) {
131
+ console.log(lang === 'ko'
132
+ ? `μ“°κΈ° μ‹œμ  검사: ${koreanLintMode(cfg)} (λ²”μœ„ ${koreanLintScope(cfg)})`
133
+ : `Write-time check: ${koreanLintMode(cfg)} (scope ${koreanLintScope(cfg)})`);
134
+ return;
135
+ }
136
+ if (!['block', 'warn', 'off'].includes(mode)) {
137
+ console.error('Usage: claude-token-saver korean lint [block|warn|off] | korean lint <file...>');
138
+ process.exit(1);
139
+ }
140
+ cfg.koreanStyle = cfg.koreanStyle || {};
141
+ cfg.koreanStyle.lint = mode;
142
+ saveConfig(cfg);
143
+ const note = {
144
+ block: lang === 'ko' ? 'μœ„λ°˜μ„ λ°œκ²¬ν•˜λ©΄ λͺ¨λΈμ—κ²Œ 되돌렀 보내 고치게 ν•©λ‹ˆλ‹€.' : 'Findings are handed back to the model as blocking feedback.',
145
+ warn: lang === 'ko' ? 'μœ„λ°˜μ„ μ•Œλ¦¬κΈ°λ§Œ ν•˜κ³  진행을 막지 μ•ŠμŠ΅λ‹ˆλ‹€.' : 'Findings are printed as a note and do not block.',
146
+ off: lang === 'ko' ? 'μ“°κΈ° μ‹œμ  검사λ₯Ό ν•˜μ§€ μ•ŠμŠ΅λ‹ˆλ‹€.' : 'The write-time check is disabled.',
147
+ }[mode];
148
+ console.log(`${lang === 'ko' ? 'μ“°κΈ° μ‹œμ  검사' : 'Write-time check'}: ${mode} β€” ${note}`);
149
+ return;
150
+ }
151
+
22
152
  if (sub === 'show') {
23
153
  const text = ks.koreanStyleText();
24
154
  if (!text) {
@@ -32,6 +162,8 @@ export async function run({ args }) {
32
162
  if (sub === 'on' || sub === 'off') {
33
163
  const enabled = sub === 'on';
34
164
  ks.setKoreanStyleEnabled(enabled);
165
+ const { installKoreanLintHook, removeKoreanLintHook } = await import('../installer.js');
166
+ const hook = enabled ? installKoreanLintHook() : removeKoreanLintHook();
35
167
  if (enabled) {
36
168
  console.log(lang === 'ko'
37
169
  ? 'ν•œκ΅­μ–΄ 문체 지침을 μΌ°μŠ΅λ‹ˆλ‹€. λ‹€μŒ μ„Έμ…˜λΆ€ν„° λͺ¨λ“  ν”„λ‘œμ νŠΈμ— μ μš©λ©λ‹ˆλ‹€.'
@@ -42,6 +174,15 @@ export async function run({ args }) {
42
174
  console.log(lang === 'ko'
43
175
  ? ` 좜처: ${ks.KOREAN_STYLE_SOURCE}`
44
176
  : ` Source: ${ks.KOREAN_STYLE_SOURCE}`);
177
+ if (hook.action === 'skipped') {
178
+ console.log(lang === 'ko'
179
+ ? ` ⚠ μ“°κΈ° μ‹œμ  검사 훅을 λͺ» κ±Έμ—ˆμŠ΅λ‹ˆλ‹€: ${hook.reason}`
180
+ : ` ⚠ Could not register the write-time hook: ${hook.reason}`);
181
+ } else {
182
+ console.log(lang === 'ko'
183
+ ? ` μ“°κΈ° μ‹œμ  검사: ${koreanLintMode(loadConfig())}, λ²”μœ„ ${koreanLintScope(loadConfig())} (WriteΒ·Edit 둜 μ“΄ ν•œκ΅­μ–΄λ₯Ό κ²€μ‚¬ν•©λ‹ˆλ‹€)`
184
+ : ` Write-time check: ${koreanLintMode(loadConfig())}, scope ${koreanLintScope(loadConfig())} (runs on Korean written via Write/Edit)`);
185
+ }
45
186
  } else {
46
187
  console.log(lang === 'ko'
47
188
  ? 'ν•œκ΅­μ–΄ 문체 지침을 κ»μŠ΅λ‹ˆλ‹€. λ‹€μŒ μ„Έμ…˜λΆ€ν„° μ£Όμž…ν•˜μ§€ μ•ŠμŠ΅λ‹ˆλ‹€.'
@@ -59,6 +200,7 @@ export async function run({ args }) {
59
200
  if (lang === 'ko') {
60
201
  console.log(`ν•œκ΅­μ–΄ 문체 μ§€μΉ¨: ${on ? '켜짐' : '꺼짐'}`);
61
202
  console.log(` λΉ„μš©: μ„Έμ…˜λ‹Ή μ•½ ${tokens} 토큰 (μ„Έμ…˜ μ‹œμž‘ 1회 μ£Όμž…)`);
203
+ console.log(` μ“°κΈ° μ‹œμ  검사: ${koreanLintMode(loadConfig())}, λ²”μœ„ ${koreanLintScope(loadConfig())} (λ°”κΎΈλ €λ©΄ claude-token-saver korean lint block|warn|off, korean lint scope all|prose)`);
62
204
  console.log(` 좜처: ${ks.KOREAN_STYLE_SOURCE}`);
63
205
  console.log(` λΌμ΄μ„ μŠ€ μ „λ¬Έ: ${ks.KOREAN_STYLE_LICENSE_PATH}`);
64
206
  console.log(on
@@ -67,6 +209,7 @@ export async function run({ args }) {
67
209
  } else {
68
210
  console.log(`Korean writing guidance: ${on ? 'on' : 'off'}`);
69
211
  console.log(` Cost: ~${tokens} tokens per session (injected once at session start)`);
212
+ console.log(` Write-time check: ${koreanLintMode(loadConfig())}, scope ${koreanLintScope(loadConfig())} (change with: claude-token-saver korean lint block|warn|off, korean lint scope all|prose)`);
70
213
  console.log(` Source: ${ks.KOREAN_STYLE_SOURCE}`);
71
214
  console.log(` License text: ${ks.KOREAN_STYLE_LICENSE_PATH}`);
72
215
  console.log(on
@@ -2,9 +2,23 @@
2
2
 
3
3
  */
4
4
 
5
+ import { readFileSync } from 'node:fs';
6
+ import { fileURLToPath } from 'node:url';
7
+ import { dirname, join } from 'node:path';
5
8
  import { readStdinJson } from '../stdin-payload.js';
6
9
  import { debug } from '../debug.js';
7
10
 
11
+ /** This package's own version β€” the baseline the upgrade offer compares against. */
12
+ function readPackageVersion() {
13
+ try {
14
+ const pkg = join(dirname(fileURLToPath(import.meta.url)), '..', '..', 'package.json');
15
+ return JSON.parse(readFileSync(pkg, 'utf8')).version || null;
16
+ } catch (e) {
17
+ debug('route-scan:pkg-version', e);
18
+ return null;
19
+ }
20
+ }
21
+
8
22
  export async function run({ args, hasFlag, numArg }) {
9
23
  const rs = await import('../route-scan.js');
10
24
  const { userLanguage } = await import('../config.js');
@@ -190,10 +204,44 @@ export async function run({ args, hasFlag, numArg }) {
190
204
  koreanBlock = koreanStyleInjection();
191
205
  } catch (e) { debug('route-scan:korean-style', e); /* style is optional */ }
192
206
 
207
+ // Upgrade offer. A statusline cannot open a dialog, so session start is
208
+ // where the *asking* happens: the model gets one line telling it a newer
209
+ // version exists and to ask before installing anything. Cached read only
210
+ // β€” the refresh runs detached off the statusline path, never here.
211
+ let updateBlock = null;
212
+ try {
213
+ const { updateStatus, maybeSpawnUpdateCheck } = await import('../update-check.js');
214
+ const pkgVersion = readPackageVersion();
215
+ // Session start is the one moment where a stale cache matters most, so
216
+ // kick the detached refresh here too. It lands in time for the
217
+ // statusline and for the next session, not for this line.
218
+ if (pkgVersion) maybeSpawnUpdateCheck(pkgVersion);
219
+ const u = pkgVersion ? updateStatus(pkgVersion) : null;
220
+ // `dismissed` is the whole point of asking once: a user who said no to
221
+ // this version must not be asked again every `/clear`.
222
+ if (u && u.available && !u.dismissed) {
223
+ updateBlock = lang === 'ko'
224
+ ? [
225
+ `[claude-token-saver update] μƒˆ 버전이 λ‚˜μ™€ μžˆμŠ΅λ‹ˆλ‹€: v${u.current} β†’ ${u.latest}.`,
226
+ 'μ‚¬μš©μžμ—κ²Œ μ§€κΈˆ μ—…κ·Έλ ˆμ΄λ“œν• μ§€ 물어보고, μŠΉλ‚™ν•˜λ©΄ μ•„λž˜ λͺ…령을 μ‹€ν–‰ν•˜μ‹­μ‹œμ˜€. 묻지 μ•Šκ³  μ„€μΉ˜ν•˜μ§€λŠ” λ§ˆμ‹­μ‹œμ˜€.',
227
+ ' claude-token-saver upgrade # μ—…κ·Έλ ˆμ΄λ“œ μ‹€ν–‰',
228
+ ' claude-token-saver update-check --dismiss # μ‚¬μš©μžκ°€ μ›μΉ˜ μ•ŠμœΌλ©΄ (λ‹€μŒ 버전이 λ‚˜μ˜¬ λ•ŒκΉŒμ§€ λ‹€μ‹œ 묻지 μ•ŠμŠ΅λ‹ˆλ‹€)',
229
+ ].join('\n')
230
+ : [
231
+ `[claude-token-saver update] A newer version is available: v${u.current} β†’ ${u.latest}.`,
232
+ 'Ask the user whether to upgrade now, and run the command below only if they agree. Do not install without asking.',
233
+ ' claude-token-saver upgrade # perform the upgrade',
234
+ ' claude-token-saver update-check --dismiss # if they decline (stays quiet until a newer release)',
235
+ ].join('\n');
236
+ }
237
+ } catch (e) { debug('route-scan:update-check', e); /* the offer is optional */ }
238
+
193
239
  if (open.length === 0 && reviewRules.length === 0) {
240
+ if (updateBlock) console.log(updateBlock);
194
241
  if (koreanBlock) console.log(koreanBlock);
195
242
  return; // nothing else to inject
196
243
  }
244
+ if (updateBlock) console.log(updateBlock);
197
245
  // This text is injected straight into the model's context, so it must
198
246
  // follow the user's configured language β€” a Korean-only briefing in an
199
247
  // English session steers the whole first response into Korean.
@@ -0,0 +1,77 @@
1
+ /**
2
+ * Subcommand: update-check β€” everything around "is a newer version out".
3
+ *
4
+ * claude-token-saver update-check # print the cached answer
5
+ * claude-token-saver update-check --refresh # ask the registry now
6
+ * claude-token-saver update-check --refresh --quiet
7
+ * # what the detached background child runs
8
+ * claude-token-saver update-check --dismiss # stop offering this version at session start
9
+ *
10
+ * The statusline never calls the --refresh path directly; it spawns this
11
+ * command detached so the render itself stays offline and instant.
12
+ */
13
+
14
+ export async function run({ hasFlag, version }) {
15
+ const {
16
+ updateStatus,
17
+ refreshUpdateState,
18
+ dismissUpdate,
19
+ upgradeCommand,
20
+ updateCheckDisabled,
21
+ updateStatePath,
22
+ } = await import('../update-check.js');
23
+ const { userLanguage } = await import('../config.js');
24
+ const lang = userLanguage();
25
+ const quiet = hasFlag('--quiet');
26
+
27
+ if (hasFlag('--refresh')) {
28
+ const res = await refreshUpdateState(version);
29
+ if (quiet) return;
30
+ if (!res.ok) {
31
+ console.error(lang === 'ko'
32
+ ? `μ΅œμ‹  버전을 ν™•μΈν•˜μ§€ λͺ»ν–ˆμŠ΅λ‹ˆλ‹€: ${res.error}`
33
+ : `Could not reach the registry: ${res.error}`);
34
+ process.exit(1);
35
+ }
36
+ }
37
+
38
+ if (hasFlag('--dismiss')) {
39
+ const s = updateStatus(version);
40
+ if (s.latest) dismissUpdate(s.latest);
41
+ console.log(lang === 'ko'
42
+ ? `${s.latest || 'μ΅œμ‹ '} 버전 μ•ˆλ‚΄λ₯Ό λ•λ‹ˆλ‹€. κ·Έ λ‹€μŒ 버전이 λ‚˜μ˜€λ©΄ λ‹€μ‹œ μ•ˆλ‚΄ν•©λ‹ˆλ‹€.`
43
+ : `Muted the ${s.latest || 'latest'} notice. The next release after it will ask again.`);
44
+ return;
45
+ }
46
+
47
+ if (updateCheckDisabled()) {
48
+ console.log(lang === 'ko'
49
+ ? `ν˜„μž¬ 버전 v${version} β€” μ—…λ°μ΄νŠΈ 확인이 ν™˜κ²½ λ³€μˆ˜λ‘œ κΊΌμ Έ μžˆμŠ΅λ‹ˆλ‹€ (CTS_NO_UPDATE_CHECK / NO_UPDATE_NOTIFIER).`
50
+ : `v${version} β€” update checks are disabled by env var (CTS_NO_UPDATE_CHECK / NO_UPDATE_NOTIFIER).`);
51
+ return;
52
+ }
53
+
54
+ const s = updateStatus(version);
55
+ if (s.available) {
56
+ console.log(lang === 'ko'
57
+ ? `μƒˆ 버전이 μžˆμŠ΅λ‹ˆλ‹€: v${version} β†’ ${s.latest}`
58
+ : `Update available: v${version} β†’ ${s.latest}`);
59
+ console.log(lang === 'ko'
60
+ ? `μ—…κ·Έλ ˆμ΄λ“œ: ${upgradeCommand()} (λ˜λŠ” claude-token-saver upgrade)`
61
+ : `Upgrade with: ${upgradeCommand()} (or: claude-token-saver upgrade)`);
62
+ if (s.dismissed) {
63
+ console.log(lang === 'ko'
64
+ ? '이 버전은 μ‚¬μš©μžκ°€ ν•œ 번 λ„˜κΈ΄ μƒνƒœλΌ, μ„Έμ…˜ μ‹œμž‘ μ‹œμ—λŠ” λ‹€μ‹œ 묻지 μ•ŠμŠ΅λ‹ˆλ‹€.'
65
+ : 'This version was dismissed, so the session-start offer stays quiet for it.');
66
+ }
67
+ return;
68
+ }
69
+ console.log(lang === 'ko'
70
+ ? `μ΅œμ‹  버전을 μ“°κ³  μžˆμŠ΅λ‹ˆλ‹€ (v${version}).`
71
+ : `Up to date (v${version}).`);
72
+ if (!s.latest) {
73
+ console.log(lang === 'ko'
74
+ ? `아직 ν™•μΈλœ 기둝이 μ—†μŠ΅λ‹ˆλ‹€. μ§€κΈˆ ν™•μΈν•˜λ €λ©΄: claude-token-saver update-check --refresh (기둝 파일: ${updateStatePath()})`
75
+ : `No check has completed yet. Run: claude-token-saver update-check --refresh (state: ${updateStatePath()})`);
76
+ }
77
+ }