claude-token-saver 3.21.0 โ†’ 3.22.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
@@ -61,7 +61,7 @@ By run (newest first):
61
61
  |---|---|
62
62
  | ๐Ÿšจ **No surprise rate limits** | Warns when the 5H/7D window hits 90%; `handoff` backs up your work |
63
63
  | ๐Ÿง  **Cache waste detection** | Hit rate, TTL, 1M-context detection โ€” spikes diagnosed with issue codes |
64
- | ๐Ÿ‡ฐ๐Ÿ‡ท **Korean writing guidance** | Enabled automatically on a Korean locale ([below](#-korean-writing-guidance)) |
64
+ | ๐Ÿ‡ฐ๐Ÿ‡ท **Korean writing guidance** | Offered at install time, defaulting to your locale ([below](#-korean-writing-guidance)) |
65
65
 
66
66
  ## Not a router โ€” 60 seconds
67
67
 
@@ -103,7 +103,9 @@ npm i -g claude-token-saver
103
103
 
104
104
  The statusline appears at the bottom of Claude Code right away. If auto-registration was skipped (`--ignore-scripts`, sudo, sandboxed installs), run `claude-token-saver install`.
105
105
 
106
- One install sets up everything: **statusline, Skill, SessionStart hook, the ๐Ÿ…ท Harness (5 principles), and a first route-scan.** The harness is **appended** to `~/.claude/CLAUDE.md` as a marked block (your existing content is backed up and preserved) and is left alone if one is already there. Skip it with `CTS_NO_HARNESS=1 npm i -g claude-token-saver`; undo it with `claude-token-saver harness uninit --global`.
106
+ One install sets up everything: **statusline, Skill, SessionStart hook, the ๐Ÿ…ท Harness (5 principles), and a first route-scan.** The harness and the Korean writing guidance **show what they add and ask before enabling it.** The harness is **appended** to `~/.claude/CLAUDE.md` as a marked block (your existing content is backed up and preserved) and is left alone if one is already there.
107
+
108
+ Outside a terminal โ€” npm `postinstall`, CI, piped stdin โ€” the question is skipped and the old defaults apply. Use `--yes` or `--no-input` to skip it deliberately, `CTS_NO_HARNESS=1 npm i -g claude-token-saver` to skip the harness entirely, and `claude-token-saver harness uninit --global` to undo it.
107
109
 
108
110
  > โš ๏ธ Avoid `sudo` global installs โ€” the Skill lands in root's `~/.claude` instead of yours. Use nvm/fnm/Volta or `npm config set prefix ~/.npm-global`.
109
111
 
@@ -291,9 +293,31 @@ claude-token-saver korean off # off
291
293
 
292
294
  Claude Code's output styles can do the same thing, but an output style is **a single global slot**: turning it on takes that slot away from anything else and has to be configured per machine. This ships the guidance inside the package and delivers it through the SessionStart hook that is already installed, so it **applies wherever the CLI is installed and leaves the output-style slot free.** It survives `/clear`, because the hook fires again.
293
295
 
294
- Cost is **~1.5k tokens per session, injected once at session start rather than per turn**, and covered by the prompt cache from the second request on. When it is on, a `๊ฐ€` chip appears in the statusline.
296
+ Cost is **~1.5k tokens per session, injected once at session start rather than per turn**, and covered by the prompt cache from the second request on. When it is on, a `โœ๏ธ` chip appears in the statusline.
297
+
298
+ ### Before and after
299
+
300
+ From a real pipeline (yaml-sns-agent), where a Slack bot shells out to `claude -p` to investigate the repository and answers in a thread. The guidance was toggled off and then on, with the same question both times: "explain in three Korean sentences how to narrow down a render failure."
301
+
302
+ **Before** (guidance off)
303
+
304
+ > ๋ Œ๋” ์‹คํŒจ ์›์ธ ์ขํžˆ๊ธฐ 3๋‹จ๊ณ„: ๋จผ์ € `hn:still --check`๋กœ ์Šคํ‹ธ์ด ๋ช…์„ธ๋ณด๋‹ค ์ตœ์‹ ์ธ์ง€(FRESH/STALE) ํ™•์ธ โ€” STALE์ด๋ฉด ์žฌ์ƒ์„ฑ ์•ˆ ํ•ด์„œ ์˜› ์ž์‚ฐ์œผ๋กœ ๋ Œ๋” ์‹œ๋„ํ•œ ๊ฒŒ ์›์ธ. ๋‹ค์Œ caption-blocks๊ฐ€ ์ตœ์‹  ๋‚˜๋ ˆ์ด์…˜๊ณผ ๋‹จ์–ด์—ด ์ผ์น˜ํ•˜๋Š”์ง€ ์ง์ ‘ ๋Œ€์กฐ โ€” TTS ์žฌ์ƒ์„ฑ ํ›„ blocks ์žฌ์ƒ์„ฑ ๋น ์ง€๋ฉด ์ž๋ง‰ยท์Œ์„ฑ ์–ด๊ธ‹๋‚˜ ๋ Œ๋” ๊นจ์ง.
305
+
306
+ **After** (guidance on)
295
307
 
296
- **The default is decided at install time.** A Korean system locale (`ko_KR` and friends; on macOS the system setting is checked too) turns it on; anything else leaves it off, so users who never write Korean are not billed 1.5k tokens a session. **Once you have turned it on or off yourself, that choice sticks โ€” an upgrade never overrides it.** Install with `CTS_NO_KOREAN=1` to skip the automatic decision.
308
+ > ๋จผ์ € `hn:still --check` ๋Œ๋ ค ์Šคํ‹ธ์ด ๋ช…์„ธ๋ณด๋‹ค ์ตœ์‹ ์ธ์ง€ ํ™•์ธํ•œ๋‹ค. STALE์ด๋ฉด ์žฌ์ƒ์„ฑ ์•ˆ ํ•ด์„œ ์ƒ๊ธด ๋ฌธ์ œ.
309
+ >
310
+ > ๋‹ค์Œ caption-blocks๊ฐ€ captions.json ๋‹จ์–ด์—ด๊ณผ ์ผ์น˜ํ•˜๋Š”์ง€ ๋ณธ๋‹ค. ๋‚ด๋ ˆ์ด์…˜ ์žฌTTS ํ›„ blocks ์žฌ์ƒ์„ฑ ๋น ์ง€๋ฉด ์˜› ์ž๋ง‰์ด ์ƒˆ ์Œ์„ฑ ์œ„์— ๋œฌ๋‹ค.
311
+
312
+ Three things change. Clauses chained with em dashes become separate sentences, so one sentence carries one fact. Noun-stopped phrases (ํ™•์ธ, ๋Œ€์กฐ, ๋ Œ๋” ๊นจ์ง โ€” "check", "compare", "render breaks") become predicates (ํ™•์ธํ•œ๋‹ค, ๋ณธ๋‹ค, ๋œฌ๋‹ค), which makes it explicit that these are steps to take. And the particles come back where they had been dropped, so subject and object are legible on the first read.
313
+
314
+ 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
+
316
+ ### Asked at install time
317
+
318
+ The install **prints what the guidance changes, its per-session cost and its source, then asks.** A Korean system locale (`ko_KR` and friends; on macOS the system setting is checked too) makes the question default to yes; anything else defaults to no, so users who never write Korean are not billed 1.5k tokens a session. The locale is only a default, so an English-locale machine used for Korean work can still turn it on right there.
319
+
320
+ Installs with nobody attached โ€” npm `postinstall`, CI, piped stdin โ€” skip the question and apply the locale default, because a blocked prompt hangs the install. In that case, if the locale is not Korean the setting is **left undecided rather than recorded**, so a later run at a terminal still gets to ask. Use `--yes` or `--no-input` to force the non-interactive path, or `CTS_NO_KOREAN=1` to skip the feature entirely. **Once you have turned it on or off yourself, that choice sticks โ€” an upgrade never overrides it.**
297
321
 
298
322
  > **Source and license**
299
323
  > The guidance text comes from [fluent-korean](https://github.com/snflkd/fluent-korean). Copyright (c) 2026 snflkd, MIT License.
@@ -379,12 +403,17 @@ Also update `statusLine.command` in `~/.claude/settings.json` to `claude-token-s
379
403
 
380
404
  ## Release notes
381
405
 
406
+ ### v3.21.0 (2026-08-22)
407
+ - **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.
408
+ - **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.
409
+ - **The Korean-guidance statusline chip is now `โœ๏ธ`, not `๊ฐ€`** โ€” every other chip is an emoji, so a bare syllable read as a stray character rather than a status indicator.
410
+
382
411
  ### v3.20.0 (2026-08-22)
383
412
  - **The README opening is now scannable** โ€” prose replaced by a one-line summary, the screenshot, and the install command up top. The three parts (routing, harness, ratchet) are a table; how a saving is computed (before โ†’ after โ†’ gap) is a diagram. The old 30-second pitch table, which repeated all of it, now lists only what the statusline additionally catches.
384
413
 
385
414
  ### v3.19.0 (2026-08-22)
386
415
  - **Korean writing guidance** โ€” corrects how Claude writes Korean (dropped sentence parts, noun-stopped sentences, translationese, em-dash overuse), injected once per session. Claude Code's output styles occupy a single global slot and must be configured per machine; this ships the guidance in the package and delivers it through the SessionStart hook already installed, so it **applies in every project and leaves the output-style slot free.** Text vendored from [fluent-korean](https://github.com/snflkd/fluent-korean) (Copyright (c) 2026 snflkd, MIT), license included.
387
- - **Decided at install time** โ€” a Korean system locale turns it on; anything else leaves it off. Your own on/off choice is preserved, so upgrades never override it. Skip with `CTS_NO_KOREAN=1`; a `๊ฐ€` chip shows in the statusline when active.
416
+ - **Decided at install time** โ€” a Korean system locale turns it on; anything else leaves it off. Your own on/off choice is preserved, so upgrades never override it. Skip with `CTS_NO_KOREAN=1`. (From v3.21.0 the install asks instead of deciding for you.)
388
417
  - **Corrected an overstated README claim** โ€” routing savings were described as "the whole product", but the measured โˆ’18.6% comes from the harness and ratchet. The relationship between the three is now stated accurately.
389
418
 
390
419
  ### v3.18.0 (2026-08-22)
package/README.md CHANGED
@@ -102,7 +102,9 @@ npm i -g claude-token-saver
102
102
 
103
103
  ์„ค์น˜ํ•˜๋ฉด Claude Code ํ™”๋ฉด ํ•˜๋‹จ์— statusline์ด ๊ณง๋ฐ”๋กœ ๋‚˜ํƒ€๋‚ฉ๋‹ˆ๋‹ค. `--ignore-scripts` ์˜ต์…˜์ด๋‚˜ sudo ์‚ฌ์šฉ ๋“ฑ์œผ๋กœ ์ž๋™ ๋“ฑ๋ก์ด ๋˜์ง€ ์•Š์•˜๋‹ค๋ฉด `claude-token-saver install`์„ ์‹คํ–‰ํ•ด ์ง์ ‘ ๋“ฑ๋กํ•˜์‹ญ์‹œ์˜ค.
104
104
 
105
- ์„ค์น˜ ํ•œ ๋ฒˆ์œผ๋กœ **statusline๊ณผ Skill, SessionStart ํ›…, ๐Ÿ…ท Harness(5์›์น™), ์ตœ์ดˆ route-scan์ด** ๋ชจ๋‘ ์ค€๋น„๋ฉ๋‹ˆ๋‹ค. Harness๋Š” `~/.claude/CLAUDE.md`์— ํ‘œ์‹œ๊ฐ€ ๋ถ™์€ ๋ธ”๋ก์œผ๋กœ **์ถ”๊ฐ€๋˜๋ฉฐ**, ๊ธฐ์กด์— ์ž‘์„ฑํ•ด ๋‘” ๋‚ด์šฉ์€ ๋ฐฑ์—…ํ•œ ๋’ค ๊ทธ๋Œ€๋กœ ๋ณด์กดํ•ฉ๋‹ˆ๋‹ค. ์ด๋ฏธ ์„ค์ •๋˜์–ด ์žˆ๋Š” ๊ฒฝ์šฐ์—๋Š” ์•„๋ฌด๊ฒƒ๋„ ๋ฐ”๊พธ์ง€ ์•Š์Šต๋‹ˆ๋‹ค. ์ž๋™ ์„ค์ •์„ ์›ํ•˜์ง€ ์•Š์œผ๋ฉด `CTS_NO_HARNESS=1 npm i -g claude-token-saver`๋กœ ๊ฑด๋„ˆ๋›ธ ์ˆ˜ ์žˆ๊ณ , ์ด๋ฏธ ์ ์šฉํ•œ ์„ค์ •์„ ๋˜๋Œ๋ฆฌ๋ ค๋ฉด `claude-token-saver harness uninit --global`์„ ์‹คํ–‰ํ•˜์‹ญ์‹œ์˜ค.
105
+ ์„ค์น˜ ํ•œ ๋ฒˆ์œผ๋กœ **statusline๊ณผ Skill, SessionStart ํ›…, ๐Ÿ…ท Harness(5์›์น™), ์ตœ์ดˆ route-scan์ด** ๋ชจ๋‘ ์ค€๋น„๋ฉ๋‹ˆ๋‹ค. Harness์™€ ํ•œ๊ตญ์–ด ๋ฌธ์ฒด ์ง€์นจ์€ **๋ฌด์—‡์ด ์ถ”๊ฐ€๋˜๋Š”์ง€ ๋ณด์—ฌ ์ค€ ๋’ค ์ผค์ง€ ๋ฌผ์–ด๋ด…๋‹ˆ๋‹ค.** Harness๋Š” `~/.claude/CLAUDE.md`์— ํ‘œ์‹œ๊ฐ€ ๋ถ™์€ ๋ธ”๋ก์œผ๋กœ **์ถ”๊ฐ€๋˜๋ฉฐ**, ๊ธฐ์กด์— ์ž‘์„ฑํ•ด ๋‘” ๋‚ด์šฉ์€ ๋ฐฑ์—…ํ•œ ๋’ค ๊ทธ๋Œ€๋กœ ๋ณด์กดํ•ฉ๋‹ˆ๋‹ค. ์ด๋ฏธ ์„ค์ •๋˜์–ด ์žˆ๋Š” ๊ฒฝ์šฐ์—๋Š” ์•„๋ฌด๊ฒƒ๋„ ๋ฐ”๊พธ์ง€ ์•Š์Šต๋‹ˆ๋‹ค.
106
+
107
+ ํ„ฐ๋ฏธ๋„์ด ์•„๋‹Œ ํ™˜๊ฒฝ(npm์˜ `postinstall`, CI, ํŒŒ์ดํ”„ ์ž…๋ ฅ)์—์„œ๋Š” ์งˆ๋ฌธ์„ ๊ฑด๋„ˆ๋›ฐ๊ณ  ๊ธฐ์กด ๊ธฐ๋ณธ๊ฐ’์„ ์ ์šฉํ•ฉ๋‹ˆ๋‹ค. ์งˆ๋ฌธ ์—†์ด ์ง„ํ–‰ํ•˜๋ ค๋ฉด `--yes`๋‚˜ `--no-input`, ์•„์˜ˆ ๊ฑด๋„ˆ๋›ฐ๋ ค๋ฉด `CTS_NO_HARNESS=1 npm i -g claude-token-saver`๋ฅผ ์“ฐ์‹ญ์‹œ์˜ค. ์ด๋ฏธ ์ ์šฉํ•œ ์„ค์ •์„ ๋˜๋Œ๋ฆฌ๋ ค๋ฉด `claude-token-saver harness uninit --global`์„ ์‹คํ–‰ํ•˜์‹ญ์‹œ์˜ค.
106
108
 
107
109
  > โš ๏ธ sudo๋กœ ๊ธ€๋กœ๋ฒŒ ์„ค์น˜๋ฅผ ํ•˜๋ฉด Skill์ด ์‚ฌ์šฉ์ž ๊ณ„์ •์ด ์•„๋‹ˆ๋ผ root์˜ `~/.claude`์— ๋“ฑ๋ก๋˜๋Š” ํ•จ์ •์ด ์žˆ์Šต๋‹ˆ๋‹ค. nvm์ด๋‚˜ fnm, Volta๋ฅผ ์‚ฌ์šฉํ•ด ์‚ฌ์šฉ์ž ์˜์—ญ์— ์„ค์น˜ํ•˜๊ธฐ๋ฅผ ๊ถŒ์žฅํ•ฉ๋‹ˆ๋‹ค.
108
110
 
@@ -281,9 +283,31 @@ claude-token-saver korean off # ๋„๊ธฐ
281
283
 
282
284
  Claude Code์˜ output style๋กœ๋„ ๊ฐ™์€ ์ผ์„ ํ•  ์ˆ˜ ์žˆ์ง€๋งŒ, output style์€ **์ „์—ญ ์Šฌ๋กฏ ํ•˜๋‚˜**๋ผ์„œ ์ผœ๋Š” ์ˆœ๊ฐ„ ๋‹ค๋ฅธ ์Šคํƒ€์ผ์„ ๋ชป ์“ฐ๊ฒŒ ๋˜๊ณ  ๋จธ์‹ ๋งˆ๋‹ค ๋”ฐ๋กœ ์„ค์ •ํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค. ์ด ๊ธฐ๋Šฅ์€ ์ง€์นจ์„ ํŒจํ‚ค์ง€์— ๋‹ด๊ณ  ์ด๋ฏธ ์„ค์น˜๋œ SessionStart ํ›…์œผ๋กœ ์ „๋‹ฌํ•˜๋ฏ€๋กœ, **CLI๊ฐ€ ์„ค์น˜๋œ ๋ชจ๋“  ํ”„๋กœ์ ํŠธ์— ์ ์šฉ๋˜๋ฉฐ output style ์Šฌ๋กฏ์€ ๋น„์›Œ ๋‘ก๋‹ˆ๋‹ค.** `/clear` ์ดํ›„์—๋„ ํ›…์ด ๋‹ค์‹œ ์‹คํ–‰๋˜์–ด ์œ ์ง€๋ฉ๋‹ˆ๋‹ค.
283
285
 
284
- ๋น„์šฉ์€ **์„ธ์…˜๋‹น ์•ฝ 1,500 ํ† ํฐ์ด๋ฉฐ ๋งค ํ„ด์ด ์•„๋‹ˆ๋ผ ์„ธ์…˜ ์‹œ์ž‘์— ํ•œ ๋ฒˆ๋งŒ** ์ฃผ์ž…๋ฉ๋‹ˆ๋‹ค. ๋‘ ๋ฒˆ์งธ ์š”์ฒญ๋ถ€ํ„ฐ๋Š” ํ”„๋กฌํ”„ํŠธ ์บ์‹œ์— ์˜ฌ๋ผ๊ฐ€๋ฏ€๋กœ ์ถ”๊ฐ€ ๋ถ€๋‹ด์ด ๊ฑฐ์˜ ์—†์Šต๋‹ˆ๋‹ค. ์ผœ์ ธ ์žˆ์œผ๋ฉด statusline์— `๊ฐ€` ์นฉ์ด ํ‘œ์‹œ๋ฉ๋‹ˆ๋‹ค.
286
+ ๋น„์šฉ์€ **์„ธ์…˜๋‹น ์•ฝ 1,500 ํ† ํฐ์ด๋ฉฐ ๋งค ํ„ด์ด ์•„๋‹ˆ๋ผ ์„ธ์…˜ ์‹œ์ž‘์— ํ•œ ๋ฒˆ๋งŒ** ์ฃผ์ž…๋ฉ๋‹ˆ๋‹ค. ๋‘ ๋ฒˆ์งธ ์š”์ฒญ๋ถ€ํ„ฐ๋Š” ํ”„๋กฌํ”„ํŠธ ์บ์‹œ์— ์˜ฌ๋ผ๊ฐ€๋ฏ€๋กœ ์ถ”๊ฐ€ ๋ถ€๋‹ด์ด ๊ฑฐ์˜ ์—†์Šต๋‹ˆ๋‹ค. ์ผœ์ ธ ์žˆ์œผ๋ฉด statusline์— `โœ๏ธ` ์นฉ์ด ํ‘œ์‹œ๋ฉ๋‹ˆ๋‹ค.
287
+
288
+ ### ์ ์šฉ ์ „ํ›„ ๋น„๊ต
289
+
290
+ ์Šฌ๋ž™ ๋ด‡์ด `claude -p`๋กœ ์ €์žฅ์†Œ๋ฅผ ์กฐ์‚ฌํ•˜๊ณ  ๊ฒฐ๊ณผ๋ฅผ ์Šค๋ ˆ๋“œ์— ๋‹ตํ•˜๋Š” ์‹ค์ œ ํŒŒ์ดํ”„๋ผ์ธ(yaml-sns-agent)์—์„œ, ์ง€์นจ๋งŒ ๊ป๋‹ค ์ผœ๊ณ  ๊ฐ™์€ ์งˆ๋ฌธ์„ ๋˜์ง„ ๊ฒฐ๊ณผ์ž…๋‹ˆ๋‹ค. ์งˆ๋ฌธ์€ "๋ Œ๋”๊ฐ€ ์‹คํŒจํ–ˆ์„ ๋•Œ ์›์ธ์„ ์–ด๋–ป๊ฒŒ ์ขํžˆ๋Š”์ง€ ํ•œ๊ตญ์–ด 3๋ฌธ์žฅ์œผ๋กœ ์„ค๋ช…ํ•ด๋ผ"์˜€์Šต๋‹ˆ๋‹ค.
291
+
292
+ **์ด์ „** (์ง€์นจ ๊บผ์ง)
293
+
294
+ > ๋ Œ๋” ์‹คํŒจ ์›์ธ ์ขํžˆ๊ธฐ 3๋‹จ๊ณ„: ๋จผ์ € `hn:still --check`๋กœ ์Šคํ‹ธ์ด ๋ช…์„ธ๋ณด๋‹ค ์ตœ์‹ ์ธ์ง€(FRESH/STALE) ํ™•์ธ โ€” STALE์ด๋ฉด ์žฌ์ƒ์„ฑ ์•ˆ ํ•ด์„œ ์˜› ์ž์‚ฐ์œผ๋กœ ๋ Œ๋” ์‹œ๋„ํ•œ ๊ฒŒ ์›์ธ. ๋‹ค์Œ caption-blocks๊ฐ€ ์ตœ์‹  ๋‚˜๋ ˆ์ด์…˜๊ณผ ๋‹จ์–ด์—ด ์ผ์น˜ํ•˜๋Š”์ง€ ์ง์ ‘ ๋Œ€์กฐ โ€” TTS ์žฌ์ƒ์„ฑ ํ›„ blocks ์žฌ์ƒ์„ฑ ๋น ์ง€๋ฉด ์ž๋ง‰ยท์Œ์„ฑ ์–ด๊ธ‹๋‚˜ ๋ Œ๋” ๊นจ์ง.
295
+
296
+ **์ดํ›„** (์ง€์นจ ์ผœ์ง)
285
297
 
286
- **์„ค์น˜ํ•  ๋•Œ ์ž๋™์œผ๋กœ ๊ฒฐ์ •๋ฉ๋‹ˆ๋‹ค.** ์‹œ์Šคํ…œ ๋กœ์บ˜์ด ํ•œ๊ตญ์–ด์ด๋ฉด(`ko_KR` ๋“ฑ, macOS๋Š” ์‹œ์Šคํ…œ ์„ค์ •๊นŒ์ง€ ํ™•์ธ) ์„ค์น˜์™€ ๋™์‹œ์— ์ผœ์ง€๊ณ , ํ•œ๊ตญ์–ด ํ™˜๊ฒฝ์ด ์•„๋‹ˆ๋ฉด ๊บผ์ง„ ์ฑ„๋กœ ๋‘ก๋‹ˆ๋‹ค. ํ•œ๊ตญ์–ด๋ฅผ ์“ฐ์ง€ ์•Š๋Š” ์‚ฌ์šฉ์ž์—๊ฒŒ ์„ธ์…˜๋งˆ๋‹ค 1,500 ํ† ํฐ์„ ์ฒญ๊ตฌํ•˜์ง€ ์•Š๊ธฐ ์œ„ํ•œ ํŒ๋‹จ์ž…๋‹ˆ๋‹ค. **ํ•œ ๋ฒˆ์ด๋ผ๋„ ์ง์ ‘ ์ผœ๊ฑฐ๋‚˜ ๋ˆ ๋’ค์—๋Š” ๊ทธ ์„ ํƒ์„ ์œ ์ง€ํ•˜๋ฏ€๋กœ, ์—…๋ฐ์ดํŠธ ์„ค์น˜๊ฐ€ ์‚ฌ์šฉ์ž์˜ ๊ฒฐ์ •์„ ๋˜๋Œ๋ฆฌ์ง€ ์•Š์Šต๋‹ˆ๋‹ค.** ์ž๋™ ์„ค์ •์„ ์›ํ•˜์ง€ ์•Š์œผ๋ฉด `CTS_NO_KOREAN=1`์„ ๋ถ™์—ฌ ์„ค์น˜ํ•˜์‹ญ์‹œ์˜ค.
298
+ > ๋จผ์ € `hn:still --check` ๋Œ๋ ค ์Šคํ‹ธ์ด ๋ช…์„ธ๋ณด๋‹ค ์ตœ์‹ ์ธ์ง€ ํ™•์ธํ•œ๋‹ค. STALE์ด๋ฉด ์žฌ์ƒ์„ฑ ์•ˆ ํ•ด์„œ ์ƒ๊ธด ๋ฌธ์ œ.
299
+ >
300
+ > ๋‹ค์Œ caption-blocks๊ฐ€ captions.json ๋‹จ์–ด์—ด๊ณผ ์ผ์น˜ํ•˜๋Š”์ง€ ๋ณธ๋‹ค. ๋‚ด๋ ˆ์ด์…˜ ์žฌTTS ํ›„ blocks ์žฌ์ƒ์„ฑ ๋น ์ง€๋ฉด ์˜› ์ž๋ง‰์ด ์ƒˆ ์Œ์„ฑ ์œ„์— ๋œฌ๋‹ค.
301
+
302
+ ๋ฐ”๋€ ์ง€์ ์€ ์„ธ ๊ฐ€์ง€์ž…๋‹ˆ๋‹ค. ์ฒซ์งธ, ์— ๋Œ€์‹œ๋กœ ์ด์–ด ๋ถ™์ด๋˜ ์ ˆ์ด ๋งˆ์นจํ‘œ๋กœ ๋Š์–ด์ ธ ํ•œ ๋ฌธ์žฅ์ด ํ•œ ๊ฐ€์ง€ ์‚ฌ์‹ค๋งŒ ์ „๋‹ฌํ•ฉ๋‹ˆ๋‹ค. ๋‘˜์งธ, "ํ™•์ธ", "๋Œ€์กฐ", "๋ Œ๋” ๊นจ์ง" ๊ฐ™์€ ๋ช…์‚ฌํ˜• ์ข…๊ฒฐ์ด "ํ™•์ธํ•œ๋‹ค", "๋ณธ๋‹ค", "๋œฌ๋‹ค"์ฒ˜๋Ÿผ ์„œ์ˆ ์–ด๋กœ ๋ฐ”๋€Œ์–ด ๋ฌด์—‡์„ ํ•˜๋ผ๋Š” ๊ฒƒ์ธ์ง€๊ฐ€ ๋ถ„๋ช…ํ•ด์ง‘๋‹ˆ๋‹ค. ์…‹์งธ, "์ž๋ง‰ยท์Œ์„ฑ ์–ด๊ธ‹๋‚˜"์ฒ˜๋Ÿผ ์กฐ์‚ฌ๊ฐ€ ๋น ์ง„ ์ž๋ฆฌ์— ์กฐ์‚ฌ๊ฐ€ ๋Œ์•„์™€ ์–ด๋–ค ์„ฑ๋ถ„์ด ์ฃผ์–ด์ด๊ณ  ๋ชฉ์ ์–ด์ธ์ง€ ์ฝ๋Š” ์ฆ‰์‹œ ์žกํž™๋‹ˆ๋‹ค.
303
+
304
+ ๊ธฐ์ˆ ์  ๋‚ด์šฉ์€ ์–‘์ชฝ์ด ๋™์ผํ•ฉ๋‹ˆ๋‹ค. ์ง€์นจ์€ ํŒ๋‹จ์ด๋‚˜ ์ •ํ™•๋„๊ฐ€ ์•„๋‹ˆ๋ผ ๋ฌธ์žฅ์˜ ์™„์„ฑ๋„์—๋งŒ ๊ด€์—ฌํ•˜๋ฏ€๋กœ, ๋‹ต์ด ๋‹ฌ๋ผ์ง€๋Š” ๊ฒƒ์ด ์•„๋‹ˆ๋ผ ๊ฐ™์€ ๋‹ต์„ ๋‹ค์‹œ ์ฝ์ง€ ์•Š์•„๋„ ๋˜๋Š” ํ˜•ํƒœ๋กœ ๋งŒ๋“ค์–ด ์ค๋‹ˆ๋‹ค. ์Šฌ๋ž™์ฒ˜๋Ÿผ ์‚ฌ๋žŒ์ด ์Šคํฌ๋กคํ•˜๋ฉฐ ์ฝ๋Š” ์ฑ„๋„์—์„œ๋Š” ์ด ์ฐจ์ด๊ฐ€ ๋˜๋ฌป๋Š” ํšŸ์ˆ˜๋ฅผ ์ค„์ด๊ณ , ๋˜๋ฌป์ง€ ์•Š๋Š” ๋งŒํผ ํ† ํฐ๋„ ์•„๋‚๋‹ˆ๋‹ค.
305
+
306
+ ### ์„ค์น˜ํ•  ๋•Œ ๋ฌผ์–ด๋ด…๋‹ˆ๋‹ค
307
+
308
+ ์„ค์น˜ ๊ณผ์ •์—์„œ **์ง€์นจ์˜ ๋‚ด์šฉ๊ณผ ์„ธ์…˜๋‹น ๋น„์šฉ, ์ถœ์ฒ˜๋ฅผ ๋จผ์ € ๋ณด์—ฌ ์ค€ ๋‹ค์Œ ์ผค์ง€ ๋ฌผ์–ด๋ด…๋‹ˆ๋‹ค.** ์‹œ์Šคํ…œ ๋กœ์บ˜์ด ํ•œ๊ตญ์–ด์ด๋ฉด(`ko_KR` ๋“ฑ, macOS๋Š” ์‹œ์Šคํ…œ ์„ค์ •๊นŒ์ง€ ํ™•์ธ) ์งˆ๋ฌธ์˜ ๊ธฐ๋ณธ๊ฐ’์ด "์ผฌ"์ด ๋˜๊ณ , ํ•œ๊ตญ์–ด ํ™˜๊ฒฝ์ด ์•„๋‹ˆ๋ฉด ๊ธฐ๋ณธ๊ฐ’์ด "๋”"์ž…๋‹ˆ๋‹ค. ๋กœ์บ˜์€ ๋‹ต์ด ์•„๋‹ˆ๋ผ ๊ธฐ๋ณธ๊ฐ’์ผ ๋ฟ์ด๋ฏ€๋กœ ์˜์–ด ๋กœ์บ˜์—์„œ ํ•œ๊ตญ์–ด๋กœ ์ž‘์—…ํ•˜๋Š” ๊ฒฝ์šฐ์—๋„ ๊ทธ ์ž๋ฆฌ์—์„œ ์ผค ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.
309
+
310
+ npm์˜ `postinstall`์ด๋‚˜ CI์ฒ˜๋Ÿผ ์‚ฌ๋žŒ์ด ๋ถ™์–ด ์žˆ์ง€ ์•Š์€ ์„ค์น˜์—์„œ๋Š” ์งˆ๋ฌธ์„ ๊ฑด๋„ˆ๋›ฐ๊ณ  ๋กœ์บ˜ ๊ธฐ๋ณธ๊ฐ’์„ ๊ทธ๋Œ€๋กœ ์ ์šฉํ•ฉ๋‹ˆ๋‹ค. ํ”„๋กฌํ”„ํŠธ๊ฐ€ ๋ฉˆ์ถฐ ์„œ๋ฉด ์„ค์น˜ ์ž์ฒด๊ฐ€ ๊ฑธ๋ฆฌ๊ธฐ ๋•Œ๋ฌธ์ž…๋‹ˆ๋‹ค. ์ด๋•Œ ๋กœ์บ˜์ด ํ•œ๊ตญ์–ด๊ฐ€ ์•„๋‹ˆ๋ฉด **์„ค์ •์„ ์ €์žฅํ•˜์ง€ ์•Š๊ณ  ๋ฏธ๊ฒฐ์ •์œผ๋กœ ๋‚จ๊ฒจ ๋‘๋ฏ€๋กœ**, ๋‚˜์ค‘์— ํ„ฐ๋ฏธ๋„์—์„œ ๋‹ค์‹œ ์„ค์น˜ํ•˜๋ฉด ๊ทธ๋•Œ ๋ฌผ์–ด๋ด…๋‹ˆ๋‹ค. ์งˆ๋ฌธ ์—†์ด ๊ธฐ๋ณธ๊ฐ’์œผ๋กœ ๋„˜๊ธฐ๋ ค๋ฉด `--yes`๋‚˜ `--no-input`์„, ๊ธฐ๋Šฅ ์ž์ฒด๋ฅผ ๊ฑด๋„ˆ๋›ฐ๋ ค๋ฉด `CTS_NO_KOREAN=1`์„ ์“ฐ์‹ญ์‹œ์˜ค. **ํ•œ ๋ฒˆ์ด๋ผ๋„ ์ง์ ‘ ์ผœ๊ฑฐ๋‚˜ ๋ˆ ๋’ค์—๋Š” ๊ทธ ์„ ํƒ์„ ์œ ์ง€ํ•˜๋ฏ€๋กœ, ์—…๋ฐ์ดํŠธ ์„ค์น˜๊ฐ€ ์‚ฌ์šฉ์ž์˜ ๊ฒฐ์ •์„ ๋˜๋Œ๋ฆฌ์ง€ ์•Š์Šต๋‹ˆ๋‹ค.**
287
311
 
288
312
  > **์ถœ์ฒ˜์™€ ๋ผ์ด์„ ์Šค**
289
313
  > ์ง€์นจ ์›๋ฌธ์€ [fluent-korean](https://github.com/snflkd/fluent-korean)์—์„œ ๊ฐ€์ ธ์™”์Šต๋‹ˆ๋‹ค. Copyright (c) 2026 snflkd, MIT License.
@@ -345,12 +369,17 @@ npm uninstall -g claude-cache-monitor && npm i -g claude-token-saver
345
369
 
346
370
  ## ๋ฆด๋ฆฌ์Šค ๋…ธํŠธ
347
371
 
372
+ ### v3.21.0 (2026-08-22)
373
+ - **์„ค์น˜๊ฐ€ ๋ฌด์—‡์„ ์ผœ๋Š”์ง€ ๋ณด์—ฌ ์ค€ ๋’ค ๋ฌผ์–ด๋ด…๋‹ˆ๋‹ค** โ€” ์ง€๊ธˆ๊นŒ์ง€๋Š” ๐Ÿ…ท Harness 5์›์น™๊ณผ ํ•œ๊ตญ์–ด ๋ฌธ์ฒด ์ง€์นจ์„ ์„ค์น˜๊ฐ€ ์Šค์Šค๋กœ ์ผฐ๊ณ , ์‚ฌ์šฉ์ž๋Š” ๊ฒฐ๊ณผ๋งŒ ๋ณผ ์ˆ˜ ์žˆ์—ˆ์Šต๋‹ˆ๋‹ค. ์ด์ œ harness๋Š” ๋‹ค์„ฏ ์›์น™์˜ ์ œ๋ชฉ์„, ํ•œ๊ตญ์–ด ์ง€์นจ์€ ๋ฐ”๊พธ๋Š” ๋‚ด์šฉ๊ณผ ์„ธ์…˜๋‹น ๋น„์šฉ๊ณผ ์ถœ์ฒ˜๋ฅผ ๋จผ์ € ์ถœ๋ ฅํ•œ ๋‹ค์Œ ์ผค์ง€ ๋ฌป์Šต๋‹ˆ๋‹ค. ๋กœ์บ˜ ๊ฐ์ง€๋Š” ๋‹ต์ด ์•„๋‹ˆ๋ผ ์งˆ๋ฌธ์˜ ๊ธฐ๋ณธ๊ฐ’์œผ๋กœ ๋‚ด๋ ค์™”์œผ๋ฏ€๋กœ, ์˜์–ด ๋กœ์บ˜์—์„œ ํ•œ๊ตญ์–ด๋กœ ์ž‘์—…ํ•˜๋Š” ๊ฒฝ์šฐ์—๋„ ๊ทธ ์ž๋ฆฌ์—์„œ ์ผค ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.
374
+ - **์‚ฌ๋žŒ์ด ๋ถ™์–ด ์žˆ์ง€ ์•Š์€ ์„ค์น˜๋Š” ์˜ˆ์ „๋Œ€๋กœ ๋™์ž‘ํ•ฉ๋‹ˆ๋‹ค** โ€” npm์˜ `postinstall`, CI, ํŒŒ์ดํ”„ ์ž…๋ ฅ, `CTS_NO_INPUT=1`์—์„œ๋Š” ์งˆ๋ฌธ์„ ๊ฑด๋„ˆ๋›ฐ๊ณ  ๊ธฐ์กด ๊ธฐ๋ณธ๊ฐ’์„ ๊ทธ๋Œ€๋กœ ์ ์šฉํ•ฉ๋‹ˆ๋‹ค. ํ”„๋กฌํ”„ํŠธ๊ฐ€ ๋ฉˆ์ถฐ ์„œ๋ฉด ์„ค์น˜๊ฐ€ ๊ฑธ๋ฆฌ๊ธฐ ๋•Œ๋ฌธ์ž…๋‹ˆ๋‹ค. `--yes`์™€ `--no-input`์œผ๋กœ ์ง์ ‘ ๊ฐ•์ œํ•  ์ˆ˜๋„ ์žˆ์Šต๋‹ˆ๋‹ค. ๋น„๋Œ€ํ™”ํ˜•์ด๋ฉด์„œ ๋กœ์บ˜์ด ํ•œ๊ตญ์–ด๊ฐ€ ์•„๋‹Œ ๊ฒฝ์šฐ์—๋Š” ์„ค์ •์„ ์ €์žฅํ•˜์ง€ ์•Š๊ณ  ๋ฏธ๊ฒฐ์ •์œผ๋กœ ๋‚จ๊ฒจ ๋‘๋ฏ€๋กœ, ๋‚˜์ค‘์— ํ„ฐ๋ฏธ๋„์—์„œ ์„ค์น˜ํ•˜๋ฉด ๊ทธ๋•Œ ๋ฌผ์–ด๋ด…๋‹ˆ๋‹ค.
375
+ - **statusline์˜ ํ•œ๊ตญ์–ด ์ง€์นจ ์นฉ์„ `๊ฐ€`์—์„œ `โœ๏ธ`๋กœ ๋ฐ”๊ฟจ์Šต๋‹ˆ๋‹ค** โ€” ๋‹ค๋ฅธ ์นฉ์ด ๋ชจ๋‘ ์ด๋ชจ์ง€๋ผ์„œ ์Œ์ ˆ ํ•˜๋‚˜๊ฐ€ ์ƒํƒœ ํ‘œ์‹œ๊ฐ€ ์•„๋‹ˆ๋ผ ์ž˜๋ชป ์„ž์—ฌ ๋“ค์–ด๊ฐ„ ๊ธ€์ž๋กœ ๋ณด์˜€์Šต๋‹ˆ๋‹ค.
376
+
348
377
  ### v3.20.0 (2026-08-22)
349
378
  - **README ์ฒซ ํ™”๋ฉด์„ ํ›‘์–ด ์ฝ์„ ์ˆ˜ ์žˆ๊ฒŒ ๋‹ค์‹œ ์งฐ์Šต๋‹ˆ๋‹ค** โ€” ์„ค๋ช… ๋ฌธ๋‹จ์„ ๊ฑท์–ด๋‚ด๊ณ  ํ•œ ์ค„ ์š”์•ฝ, ์Šคํฌ๋ฆฐ์ƒท, ์„ค์น˜ ๋ช…๋ น์„ ์•ž์— ๋‘์—ˆ์Šต๋‹ˆ๋‹ค. ๋ผ์šฐํŒ…ยทHarnessยทratchet ์„ธ ๊ธฐ๋Šฅ์€ ํ‘œ๋กœ, ์ ˆ๊ฐ์•ก ๊ณ„์‚ฐ ๋ฐฉ์‹(๊ธฐ์ค€ ๋ชจ๋ธ โ†’ ์‹คํ–‰ ๋ชจ๋ธ โ†’ ์ฐจ์•ก)์€ ๋„์‹์œผ๋กœ ๋ฐ”๊ฟจ์Šต๋‹ˆ๋‹ค. ๋’ค์—์„œ ๊ฐ™์€ ๋‚ด์šฉ์„ ๋ฐ˜๋ณตํ•˜๋˜ "30์ดˆ ์š”์•ฝ" ํ‘œ๋Š” statusline์ด ์ถ”๊ฐ€๋กœ ์žก์•„ ์ฃผ๋Š” ํ•ญ๋ชฉ๋งŒ ๋‚จ๊ฒจ ์ •๋ฆฌํ–ˆ์Šต๋‹ˆ๋‹ค.
350
379
 
351
380
  ### v3.19.0 (2026-08-22)
352
381
  - **ํ•œ๊ตญ์–ด ๋ฌธ์ฒด ์ง€์นจ ๊ธฐ๋Šฅ์„ ์ถ”๊ฐ€ํ–ˆ์Šต๋‹ˆ๋‹ค** โ€” Claude๊ฐ€ ํ•œ๊ตญ์–ด๋ฅผ ์“ธ ๋•Œ ๋‚˜ํƒ€๋‚˜๋Š” ๋ฌธ์ฒด ๊ฒฐํ•จ(๋ฌธ์žฅ ์„ฑ๋ถ„ ์ƒ๋žต, ๋ช…์‚ฌํ˜• ์ข…๊ฒฐ, ๋ฒˆ์—ญ์ฒด, ์— ๋Œ€์‹œ ๋‚จ์šฉ)์„ ๊ต์ •ํ•˜๋Š” ์ง€์นจ์„ ์„ธ์…˜ ์‹œ์ž‘ ์‹œ ํ•œ ๋ฒˆ ์ฃผ์ž…ํ•ฉ๋‹ˆ๋‹ค. Claude Code์˜ output style์€ ์ „์—ญ ์Šฌ๋กฏ ํ•˜๋‚˜๋ฅผ ์ฐจ์ง€ํ•˜๊ณ  ๋จธ์‹ ๋งˆ๋‹ค ์„ค์ •ํ•ด์•ผ ํ•˜์ง€๋งŒ, ์ด ๊ธฐ๋Šฅ์€ ์ง€์นจ์„ ํŒจํ‚ค์ง€์— ๋‹ด๊ณ  ์ด๋ฏธ ์„ค์น˜๋œ SessionStart ํ›…์œผ๋กœ ์ „๋‹ฌํ•˜๋ฏ€๋กœ **CLI๊ฐ€ ์„ค์น˜๋œ ๋ชจ๋“  ํ”„๋กœ์ ํŠธ์— ์ ์šฉ๋˜๋ฉฐ output style ์Šฌ๋กฏ์€ ๋น„์›Œ ๋‘ก๋‹ˆ๋‹ค.** ์ง€์นจ ์›๋ฌธ์€ [fluent-korean](https://github.com/snflkd/fluent-korean)(Copyright (c) 2026 snflkd, MIT)์—์„œ ๊ฐ€์ ธ์™”๊ณ  ๋ผ์ด์„ ์Šค ์ „๋ฌธ์„ ํ•จ๊ป˜ ๋ฐฐํฌํ•ฉ๋‹ˆ๋‹ค.
353
- - **์„ค์น˜์™€ ๋™์‹œ์— ๊ฒฐ์ •๋ฉ๋‹ˆ๋‹ค** โ€” ์‹œ์Šคํ…œ ๋กœ์บ˜์ด ํ•œ๊ตญ์–ด์ด๋ฉด ์„ค์น˜ ์‹œ ์ž๋™์œผ๋กœ ์ผœ์ง€๊ณ , ์•„๋‹ˆ๋ฉด ๊บผ ๋‘ก๋‹ˆ๋‹ค. ์ง์ ‘ ์ผœ๊ฑฐ๋‚˜ ๋ˆ ๋’ค์—๋Š” ๊ทธ ์„ ํƒ์„ ์œ ์ง€ํ•˜๋ฏ€๋กœ ์—…๋ฐ์ดํŠธ๊ฐ€ ์‚ฌ์šฉ์ž์˜ ๊ฒฐ์ •์„ ๋˜๋Œ๋ฆฌ์ง€ ์•Š์Šต๋‹ˆ๋‹ค. `CTS_NO_KOREAN=1`๋กœ ๊ฑด๋„ˆ๋›ธ ์ˆ˜ ์žˆ๊ณ , ์ผœ์ ธ ์žˆ์œผ๋ฉด statusline์— `๊ฐ€` ์นฉ์ด ํ‘œ์‹œ๋ฉ๋‹ˆ๋‹ค.
382
+ - **์„ค์น˜์™€ ๋™์‹œ์— ๊ฒฐ์ •๋ฉ๋‹ˆ๋‹ค** โ€” ์‹œ์Šคํ…œ ๋กœ์บ˜์ด ํ•œ๊ตญ์–ด์ด๋ฉด ์„ค์น˜ ์‹œ ์ž๋™์œผ๋กœ ์ผœ์ง€๊ณ , ์•„๋‹ˆ๋ฉด ๊บผ ๋‘ก๋‹ˆ๋‹ค. ์ง์ ‘ ์ผœ๊ฑฐ๋‚˜ ๋ˆ ๋’ค์—๋Š” ๊ทธ ์„ ํƒ์„ ์œ ์ง€ํ•˜๋ฏ€๋กœ ์—…๋ฐ์ดํŠธ๊ฐ€ ์‚ฌ์šฉ์ž์˜ ๊ฒฐ์ •์„ ๋˜๋Œ๋ฆฌ์ง€ ์•Š์Šต๋‹ˆ๋‹ค. `CTS_NO_KOREAN=1`๋กœ ๊ฑด๋„ˆ๋›ธ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค(v3.21.0๋ถ€ํ„ฐ๋Š” ์„ค์น˜๊ฐ€ ๋ฌป์Šต๋‹ˆ๋‹ค).
354
383
  - **README์˜ ๊ณผ์žฅ๋œ ์„ค๋ช…์„ ๋ฐ”๋กœ์žก์•˜์Šต๋‹ˆ๋‹ค** โ€” ๋ผ์šฐํŒ… ์ ˆ๊ฐ์•ก์„ "์ด ๋„๊ตฌ์˜ ์ „๋ถ€"๋ผ๊ณ  ์ ์—ˆ์œผ๋‚˜, ์‹ค์ธก โˆ’18.6%๋Š” Harness์™€ ratchet์„ ๋„์ž…ํ•œ ํšจ๊ณผ์ž…๋‹ˆ๋‹ค. ์„ธ ๊ธฐ๋Šฅ์˜ ๊ด€๊ณ„๋ฅผ ์ •ํ™•ํžˆ ๋‹ค์‹œ ์ผ์Šต๋‹ˆ๋‹ค.
355
384
 
356
385
  ### v3.18.0 (2026-08-22)
package/bin/cli.js CHANGED
@@ -10,6 +10,8 @@
10
10
  * npx claude-token-saver --format csv # CSV output
11
11
  * npx claude-token-saver --project myproj # filter by project
12
12
  * npx claude-token-saver route-scan # detect recurring easy work โ†’ haiku-delegation candidates
13
+ * npx claude-token-saver install # set up skill/hooks/statusline; asks about harness + Korean guidance
14
+ * npx claude-token-saver install --yes # take the defaults without asking (same as --no-input)
13
15
  * npx claude-token-saver --install-hook # install PostToolUse hook
14
16
  * npx claude-token-saver --uninstall-hook # remove hook
15
17
  * npx claude-token-saver --hook-run # internal: called by hook
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-token-saver",
3
- "version": "3.21.0",
3
+ "version": "3.22.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": {
@@ -5,6 +5,8 @@
5
5
  * a legacy command file is removed automatically. Cross-platform.
6
6
  * claude-token-saver install # install/update the skill
7
7
  * claude-token-saver install --force # overwrite existing skill file
8
+ * claude-token-saver install --yes # take the defaults instead of asking
9
+ * claude-token-saver install --no-input # same as --yes; also implied by CTS_NO_INPUT=1
8
10
  */
9
11
 
10
12
  import { debug } from '../debug.js';
@@ -107,6 +107,18 @@ export async function run({ args, hasFlag, numArg }) {
107
107
  // what the model reads.
108
108
  console.log(` ${mr.composeRuleText(r.rule, r, lang)}`);
109
109
  });
110
+ // Runs on a model id we cannot price never reach the aggregate, so a
111
+ // rule whose tier runs entirely on such an id reads as "never fired".
112
+ // Say so here rather than leaving the zero unexplained.
113
+ const scan = rs.readRouteScan();
114
+ if (scan?.unresolvedRuns > 0) {
115
+ const ids = (scan.unresolvedModels || []).join(', ');
116
+ console.log(lang === 'ko'
117
+ ? `\nโš  ํ•ด์„๋˜์ง€ ์•Š์€ ๋ชจ๋ธ ID ๋•Œ๋ฌธ์— ์œ„์ž„ ${scan.unresolvedRuns}๊ฑด์ด ์ง‘๊ณ„์—์„œ ์ œ์™ธ๋์Šต๋‹ˆ๋‹ค${ids ? ` (${ids})` : ''}.`
118
+ + '\n profile-map.json ์˜ modelAliases ์— ํ•ด๋‹น ID ๋ฅผ ๋งคํ•‘ํ•œ ๋’ค route-scan --refresh ๋ฅผ ์‹คํ–‰ํ•˜์‹ญ์‹œ์˜ค.'
119
+ : `\nโš  ${scan.unresolvedRuns} delegated run(s) were excluded โ€” unpriceable model id${ids ? ` (${ids})` : ''}.`
120
+ + '\n Map it under modelAliases in profile-map.json, then run route-scan --refresh.');
121
+ }
110
122
  console.log(lang === 'ko'
111
123
  ? '\n์ œ๊ฑฐ: claude-token-saver route-scan rules rm <N>'
112
124
  : '\nRemove with: claude-token-saver route-scan rules rm <N>');
@@ -123,7 +123,9 @@ export function koreanStyleInjection({ cfg = loadConfig() } = {}) {
123
123
  if (!text) return null;
124
124
  return [
125
125
  '[claude-token-saver korean-style] ์ด ์„ธ์…˜์—์„œ ํ•œ๊ตญ์–ด๋ฅผ ์ถœ๋ ฅํ•  ๋•Œ๋Š” ์•„๋ž˜ ์ง€์นจ์„ ๋”ฐ๋ฅด์‹ญ์‹œ์˜ค.',
126
- '์ด ์ง€์นจ์€ ์‚ฌ์šฉ์ž๊ฐ€ claude-token-saver์— ์„ค์ •ํ•œ ๊ฒƒ์ด๋ฉฐ, ๋‹ต๋ณ€ยท๋ฌธ์„œยท์ฃผ์„์ด ์•„๋‹Œ ์‚ฐ๋ฌธ ์ „๋ฐ˜์— ์ ์šฉ๋ฉ๋‹ˆ๋‹ค.',
126
+ '์ด ์ง€์นจ์€ ์‚ฌ์šฉ์ž๊ฐ€ claude-token-saver์— ์„ค์ •ํ•œ ๊ฒƒ์ž…๋‹ˆ๋‹ค.',
127
+ '์ ์šฉ ๋Œ€์ƒ: ๋Œ€ํ™” ๋‹ต๋ณ€, ๊ทธ๋ฆฌ๊ณ  ์ƒˆ๋กœ ์ž‘์„ฑํ•˜๊ฑฐ๋‚˜ ์ˆ˜์ •ํ•˜๋Š” ๋งˆํฌ๋‹ค์šดยท๋ฌธ์„œยท๋ณด๊ณ ์„œ์˜ ํ•œ๊ตญ์–ด ์‚ฐ๋ฌธ์„ ๋ชจ๋‘ ํฌํ•จํ•ฉ๋‹ˆ๋‹ค.',
128
+ '์ ์šฉ ์˜ˆ์™ธ: ์›๋ฌธ์„ ๊ทธ๋Œ€๋กœ ์˜ฎ๊ธฐ๋Š” ์ธ์šฉ, ์ฝ”๋“œ, ์ฝ”๋“œ ์ฃผ์„, ๊ทธ๋ฆฌ๊ณ  ํ”„๋กœ์ ํŠธ์˜ ๊ธฐ์กด ํ‘œ๊ธฐ ๊ด€๋ก€๋ฅผ ๋”ฐ๋ผ์•ผ ํ•˜๋Š” ์ปค๋ฐ‹ ๋ฉ”์‹œ์ง€์™€ ๋กœ๊ทธ ๋ฌธ์ž์—ด์ž…๋‹ˆ๋‹ค.',
127
129
  `(์ถœ์ฒ˜: ${KOREAN_STYLE_SOURCE})`,
128
130
  '',
129
131
  text,
@@ -357,9 +357,13 @@ function topRole(tally) {
357
357
  * against 1,702 times as a subagent, and those 16 inferred 'main' votes beat
358
358
  * the single explicit 'haiku' one at exactly the 80% line.
359
359
  *
360
- * When neither bucket is decisive the id stays unresolved. An 'unknown' that
361
- * drops out of the aggregate beats a confident wrong answer that silently
362
- * re-tiers every run on that profile.
360
+ * When a thin explicit bucket vetoes a decisive inference, the explicit role
361
+ * is adopted ('explicit-veto'): a stated `Task(model: ...)` is the stronger
362
+ * evidence, and it is the only evidence left once the inference is rejected.
363
+ *
364
+ * When neither bucket says anything usable the id stays unresolved. An
365
+ * 'unknown' that drops out of the aggregate beats a confident wrong answer
366
+ * that silently re-tiers every run on that profile.
363
367
  */
364
368
  export function tallyVotes(votes, { minVotes = MIN_VOTES, minAgreement = MIN_AGREEMENT } = {}) {
365
369
  const learned = {};
@@ -385,6 +389,16 @@ export function tallyVotes(votes, { minVotes = MIN_VOTES, minAgreement = MIN_AGR
385
389
  // flag, so probably the session model" observations.
386
390
  role = inferred.role;
387
391
  source = 'inferred';
392
+ } else if (explicit.total > 0
393
+ && explicit.top / explicit.total >= minAgreement
394
+ && inferred.role
395
+ && !rolesAgree(explicit.role, inferred.role)) {
396
+ // The veto above is only coherent if we then believe what did the
397
+ // vetoing. Leaving the id unresolved instead drops every delegated run
398
+ // on that profile out of the aggregate, which is how a haiku profile
399
+ // that doubles as somebody's session model reported zero delegations.
400
+ role = explicit.role;
401
+ source = 'explicit-veto';
388
402
  }
389
403
 
390
404
  learned[pid] = {
package/src/route-scan.js CHANGED
@@ -280,6 +280,18 @@ export function dominantModel(counts) {
280
280
  return best;
281
281
  }
282
282
 
283
+ // A delegated run counts as failed on error DENSITY, not on the presence of a
284
+ // single is_error. Binary counting scored a 218-call run that finished its task
285
+ // identically to one that died on its first call. The floor keeps very short
286
+ // runs honest: 1 error in 3 calls is still a failure.
287
+ export const DELEGATED_ERR_DENSITY = 0.1;
288
+
289
+ export function isFailedRun(run) {
290
+ if (!run || !(run.toolErrors > 0)) return false;
291
+ const calls = run.calls > 0 ? run.calls : 1;
292
+ return run.toolErrors / calls > DELEGATED_ERR_DENSITY;
293
+ }
294
+
283
295
  export function worthDelegating(tier, rank) {
284
296
  const target = TIER_TARGET_RANK[tier];
285
297
  return target !== undefined && rank > target;
@@ -480,10 +492,12 @@ export async function runRouteScan({ days = 14 } = {}) {
480
492
  // T2 rule firing, a sonnet run a T1 one. Runs that were not a downgrade
481
493
  // (same tier or higher) carry no delegation saving and are skipped.
482
494
  const delegatedStats = new Map(); // "tier|category|project" โ†’ outcome aggregate
495
+ let unresolvedRuns = 0; // delegated runs dropped for an unpriceable model id
496
+ const unresolvedModels = new Set();
483
497
  const bumpDelegated = (key, run, saved) => {
484
498
  const d = delegatedStats.get(key) || { runs: 0, errRuns: 0, outTokens: 0, savedUsd: 0 };
485
499
  d.runs += 1;
486
- if (run.toolErrors > 0) d.errRuns += 1;
500
+ if (isFailedRun(run)) d.errRuns += 1;
487
501
  d.outTokens += run.out || 0;
488
502
  d.savedUsd += saved;
489
503
  delegatedStats.set(key, d);
@@ -537,6 +551,14 @@ export async function runRouteScan({ days = 14 } = {}) {
537
551
  }
538
552
  for (const run of runs) {
539
553
  const runTier = tierForRank(modelRank(run.model));
554
+ // Dropping unresolved model ids is deliberate (see cost.js) โ€” pricing
555
+ // a gateway id as Sonnet would poison every number here. But dropping
556
+ // them SILENTLY is what made a whole tier of rules report zero
557
+ // delegations with no way to tell why, so keep a count to surface.
558
+ if (!isRecognizedModelId(run.model)) {
559
+ unresolvedRuns += 1;
560
+ if (run.model) unresolvedModels.add(run.model);
561
+ }
540
562
  if (!runTier || !worthDelegating(runTier, mainRank)) continue;
541
563
  const saved = runSaving(run, mainModel);
542
564
  bumpDelegated(`${runTier}|${cat.id}|${projectDir}`, run, saved);
@@ -641,6 +663,8 @@ export async function runRouteScan({ days = 14 } = {}) {
641
663
  thresholds,
642
664
  candidates,
643
665
  resolved: [...resolved],
666
+ unresolvedRuns,
667
+ unresolvedModels: [...unresolvedModels].slice(0, 5),
644
668
  };
645
669
  try {
646
670
  const dir = stateDir();
@@ -55,6 +55,14 @@ const REJECTION_RE = /doesn't want to proceed|tool use was rejected|doesn't want
55
55
  // does not exist" on a Read) stay counted โ€” those are genuine difficulty signal.
56
56
  const SELF_CORRECTED_RE = /File has not been read yet|has been modified since read|String to replace not found|is not running \(status:|<tool_use_error>Blocked:/i;
57
57
 
58
+ // Environment constraints โ€” a sandbox without `curl`/`wc`, a corporate proxy
59
+ // timing a fetch out โ€” are not task difficulty either. The agent routinely
60
+ // routes around them and finishes: one 218-turn run that produced a 9,870-char
61
+ // sourced report was scored a failure on two `command not found` results.
62
+ // NARROW on purpose: only the shell's own "this binary is absent" wording and
63
+ // curl's transport-timeout exit, never a generic non-zero exit.
64
+ const ENVIRONMENT_RE = /command not found|curl: \(28\)|Operation timed out after|ETIMEDOUT|ENOTFOUND|getaddrinfo/i;
65
+
58
66
  function toolResultText(content) {
59
67
  if (typeof content === 'string') return content;
60
68
  if (!Array.isArray(content)) return '';
@@ -64,7 +72,7 @@ function toolResultText(content) {
64
72
  function isRealToolError(block) {
65
73
  if (!block || block.type !== 'tool_result' || !block.is_error) return false;
66
74
  const txt = toolResultText(block.content);
67
- return !REJECTION_RE.test(txt) && !SELF_CORRECTED_RE.test(txt);
75
+ return !REJECTION_RE.test(txt) && !SELF_CORRECTED_RE.test(txt) && !ENVIRONMENT_RE.test(txt);
68
76
  }
69
77
 
70
78
  export async function collectSessionRecords(filePath, { includeContent = true } = {}) {