claude-token-saver 2.17.0 β†’ 3.0.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
@@ -55,7 +55,7 @@ The statusline appears at the bottom of Claude Code right away. If auto-registra
55
55
  | `🧠` | Cache hit rate (green at 85%+) |
56
56
  | `⏳` | Cache TTL countdown β€” send a message before expiry to keep the cache warm |
57
57
  | `✦ current` / `πŸ“… weekly` | 5-hour / 7-day rate-limit window usage + reset time |
58
- | `πŸ“¦` | Context window (1M shows red β€” a major cost driver) |
58
+ | `πŸ“¦` | 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 |
59
59
  | `πŸ’°` | Cumulative savings from prompt caching |
60
60
 
61
61
  When something is wrong, a **warning chip leads the line**:
@@ -64,7 +64,7 @@ When something is wrong, a **warning chip leads the line**:
64
64
  🚨 5H β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–“ 94% πŸ”„ 12:36 Β· πŸ…· 5/5 Β· πŸ€– Opus 4.8 Β· 🧠 Cache hit 72.1% Β· ⚠ Cache miss Β· πŸ“… weekly β–“β–‘β–‘β–‘β–‘β–‘ 12% πŸ”„ Sun 14:26 Β· πŸ“¦ Ctx 200k Β· last 1d
65
65
  ```
66
66
 
67
- Chips β€” `🚨 5H/7D NN%` (cap imminent) Β· `⚠ 1M ON` Β· `⚠ Cache miss` Β· `⚠ Input spike` Β· `⚠ Output heavy` Β· `⚠ Call surge` Β· `⚠ Rebuild churn` Β· `⚠ 5m TTL`. When both windows cross 90% at once, the sooner-resetting one is promoted to 🚨 and the other stays visible as a red segment (v2.16.0+).
67
+ Chips β€” `🚨 5H/7D NN%` (cap imminent) Β· `⚠ Ctx 200k+` (a single request actually exceeded 200k) Β· `⚠ Cache miss` Β· `⚠ Input spike` Β· `⚠ Output heavy` Β· `⚠ Call surge` Β· `⚠ Rebuild churn` Β· `⚠ 5m TTL`. When both windows cross 90% at once, the sooner-resetting one is promoted to 🚨 and the other stays visible as a red segment (v2.16.0+).
68
68
 
69
69
  ### When a chip appears
70
70
 
@@ -82,6 +82,8 @@ Run these in your shell (inside Claude Code, the `/claude-token-saver` Skill is
82
82
  | `claude-token-saver handoff` | Back work up to `HANDOFF-*.md` before a cap blocks you |
83
83
  | `claude-token-saver mode [keywords...]` | Output config (`icon`/`text`, `en`/`ko`, `1h`–`30d` window, …) |
84
84
  | `claude-token-saver harness ...` | πŸ…· Harness management (below) |
85
+ | `claude-token-saver frugon` | Export sessions β†’ [frugon](https://github.com/Rodiun/frugon)-compatible JSONL (model-routing savings analysis, below) |
86
+ | `claude-token-saver route-scan` | Detect recurring easy work on expensive models β†’ propose haiku-delegation ratchet rules (below) |
85
87
  | `claude-token-saver install` | Manually register Skill + statusline |
86
88
 
87
89
  Switch output language with `mode ko` / `mode en` (English default; statusline chips stay symbolic).
@@ -114,11 +116,15 @@ claude-token-saver harness init # this project
114
116
  claude-token-saver harness init --global # ~/.claude/CLAUDE.md β€” every project
115
117
  claude-token-saver harness check # current score (global fallback honored)
116
118
  claude-token-saver harness promote <N> --project|--global # warning #N β†’ ratchet rule (scope required)
119
+ claude-token-saver harness promote "<rule text>" --project|--global # register your own hand-written rules the same way
120
+ claude-token-saver harness pull # pull your global ratchet rules into this project (dedupes)
121
+ claude-token-saver harness pull --harness # also pull the global harness block (5 sections)
117
122
  claude-token-saver harness list / rm <N> # view / delete rules (auto .bak)
118
123
  claude-token-saver harness off | on # toggle the πŸ…· chip
119
124
  ```
120
125
 
121
126
  - `promote` **requires** `--project`/`--global` in non-TTY contexts (scripts, LLM calls) β€” a scope choice is never silently made for the caller.
127
+ - `install` / `init` never auto-inject rules into a project. When you want the rules you've accumulated globally in a new project, pull them explicitly with `harness pull` (idempotent β€” re-running adds nothing twice).
122
128
  - πŸ…·βš  runtime warnings (`ratchet?` `no-evidence` `PEV-skip`) expire after 30 minutes, subdirectory sessions match their project correctly, and PEV-skip counts only mutating tools (Edit/Write/Bash) so read-only research sessions don't trip it (v2.16.0+).
123
129
 
124
130
  <details>
@@ -133,11 +139,43 @@ The whole point of the ratchet is **one-direction accumulation**. Deleting rules
133
139
  An auto `.bak` is kept, but **the session context that earned the rule its place is not recoverable.**
134
140
  </details>
135
141
 
142
+ ## πŸ”€ frugon integration β€” "which calls could a cheaper model handle?"
143
+
144
+ claude-token-saver catches cache/context waste; [frugon](https://github.com/Rodiun/frugon) (a local LLM cost analyzer) covers **model routing** β€” finding calls that never needed your most expensive model. The `frugon` subcommand bridges the two:
145
+
146
+ ```bash
147
+ claude-token-saver frugon # last 30 days β†’ ./frugon-export.jsonl
148
+ claude-token-saver frugon --run # export + run frugon analyze immediately
149
+ claude-token-saver frugon --days 7 --project myproj --out logs.jsonl
150
+ ```
151
+
152
+ - Converts `~/.claude/projects/` transcripts into the OpenAI-compatible JSONL frugon reads. **Analysis is fully local** β€” no logs or keys leave your machine (same principle frugon holds).
153
+ - **Cache-weighted tokens (default):** frugon doesn't know about prompt caching, so raw physical tokens would overstate your spend ~10x. By default the export folds in Anthropic's cache multipliers (read 0.1x Β· 5m write 1.25x Β· 1h write 2x) so frugon's dollar figures match your real bill. Use `--raw-tokens` for physical counts.
154
+ - Preserves the signals frugon's easy/hard router reads (prompt/completion tokens, conversation depth) plus the last user prompt and reply text for `--measure` quality sampling. Strip text with `--no-content`.
155
+ - Install frugon with `pipx install frugon` (if models show as unpriced, run `frugon update`).
156
+
157
+ ## πŸ”€ route-scan β€” "this recurring task could run on haiku"
158
+
159
+ The practical follow-through of the frugon integration. It applies frugon-style difficulty analysis at the **episode (user request) level** to your session history, finds easy work your expensive model (opus/fable) keeps doing, and proposes promoting it into a haiku-subagent delegation rule. Fully local, zero token cost.
160
+
161
+ ```bash
162
+ claude-token-saver route-scan # scan (24h cache) + print candidates
163
+ claude-token-saver harness promote R1 --project # promote candidate R1 to a ratchet rule
164
+ claude-token-saver route-scan dismiss 1 # not interested β€” won't resurface
165
+ ```
166
+
167
+ How it works (session-boundary calibration, NOT a real-time router):
168
+ 1. `install` registers a SessionStart hook that injects the cached scan results as session context on startup and `/clear` (the scan itself refreshes in the background, once a day).
169
+ 2. When a recurring (β‰₯3Γ—) easy pattern exists, the statusline shows a `πŸ…·βš  route? R1` chip and Claude asks you whether to register it, and at which scope (`--project`/`--global`).
170
+ 3. Promoted rules accumulate in ratchet.md, so **from the next session on, the main model delegates that work type to a haiku subagent automatically**.
171
+
172
+ Recommended companion setup: create `model: haiku` subagents under `~/.claude/agents/` (e.g. haiku-explore / haiku-runner / haiku-translate) so the rules are immediately actionable.
173
+
136
174
  ## Spike issue codes
137
175
 
138
176
  | Code | Meaning |
139
177
  |---|---|
140
- | `LARGE_INPUT_PER_REQUEST` | single request > 250k tokens β†’ 1M context likely |
178
+ | `LARGE_INPUT_PER_REQUEST` | single request > 200k input tokens β€” per-turn re-billing and cap burn spike |
141
179
  | `LOW_HIT_RATE` | cache hit rate < 50% |
142
180
  | `BUCKET_5M_DOMINANT` | > 70% of cache writes hit the 5m bucket |
143
181
  | `HIGH_OUTPUT_RATIO` | output/input > 0.15 (output is 5Γ— input price) |
@@ -213,6 +251,22 @@ Also update `statusLine.command` in `~/.claude/settings.json` to `claude-token-s
213
251
 
214
252
  ## Release notes
215
253
 
254
+ ### v3.0.0 (2026-07-13)
255
+ - **Major bump** β€” with v2.19's frugon integration and v2.20's route-scan, the product's character shifted from "after-the-fact token monitor" to "a routing layer that pushes recurring easy work down to cheaper models", so this ships as a major. No breaking changes (every existing command and setting remains compatible).
256
+ - **route-scan promote fix** β€” `harness promote R<N> --project` now writes the rule into the `.claude/ratchet.md` of the project the candidate was **detected in** (previously it landed in whatever directory the CLI ran from). The scan stores each candidate's real session path (`projectPath`); promoting a foreign-project candidate from a pre-3.0 cache without that field is refused with a pointer to `route-scan --refresh`.
257
+ - **New `harness pull`** β€” explicitly pull your global ratchet rules into a project (`--harness` also pulls the global harness block). Dedupes by rule text, so re-running is idempotent. `install`/`init` still never auto-inject anything β€” pulling is always opt-in.
258
+
259
+ ### v2.20.0 (2026-07-13)
260
+ - **route-scan**: detect recurring easy work on expensive models β†’ `πŸ…·βš  route? R<N>` chip + SessionStart hook context injection + `harness promote R<N> --project|--global` to promote haiku-delegation ratchet rules.
261
+
262
+ ### v2.19.0 (2026-07-12)
263
+ - **frugon integration**: `claude-token-saver frugon` β€” export session transcripts as [frugon](https://github.com/Rodiun/frugon)-compatible JSONL for model-routing savings analysis (`--run` to analyze immediately; cache-weighted tokens by default).
264
+
265
+ ### v2.18.0 (2026-07-02)
266
+ - **1M-context warning re-scoped** β€” current models (Fable 5, Opus 4.6–4.8, Sonnet 5) all default to a 1M window with no long-context premium since Opus 4.7, so the "1M mode ON = expensive" framing is retired. The warning is now a **usage signal**: `⚠ 1M ON` β†’ `⚠ Ctx 200k+` (a single request actually exceeded 200k), and remediation is reordered to lead with `/compact`/`/clear` + `/effort` instead of "disable 1M". The incorrect "long-context pricing kicks in past 200k" copy is fixed.
267
+ - **πŸ“¦ segment shows live usage** β€” reads `context_window.used_percentage` from Claude Code's stdin and renders `πŸ“¦ Ctx 68% of 1M`, colored by fill (green <70 / yellow 70–89 / red 90+). Falls back to transcript-inferred size when stdin is absent (1M now yellow, not red).
268
+ - Back-compat: the legacy `⚠ 1M ON` chip and old detail strings in existing history files still resolve.
269
+
216
270
  ### v2.17.0 (2026-07-02)
217
271
  - **Fable 5 pricing tier** β€” `claude-fable-5`/`claude-mythos-5` previously fell through to the Sonnet tier ($3/$15), under-estimating costs ~3Γ—. Now priced at the real rates ($10 input / $50 output / $12.50 5m-write / $20 1h-write / $1 read).
218
272
  - README overhaul β€” top-level impact summary, segment table, harness scope-flag docs, pricing table brought current.
package/README.md CHANGED
@@ -55,7 +55,7 @@ npm i -g claude-token-saver
55
55
  | `🧠` | μΊμ‹œ 히트율 (85%+ 녹색) |
56
56
  | `⏳` | μΊμ‹œ TTL μΉ΄μš΄νŠΈλ‹€μš΄ β€” 만료 전에 λ©”μ‹œμ§€λ₯Ό 보내면 μΊμ‹œ μœ μ§€ |
57
57
  | `✦ current` / `πŸ“… weekly` | 5μ‹œκ°„ / 7일 rate-limit μœˆλ„ μ‚¬μš©λ₯  + 리셋 μ‹œκ° |
58
- | `πŸ“¦` | μ»¨ν…μŠ€νŠΈ μœˆλ„ (1M이면 λΉ¨κ°„ κ²½κ³  β€” λΉ„μš© 급증 μš”μΈ) |
58
+ | `πŸ“¦` | μ»¨ν…μŠ€νŠΈ μ‚¬μš©λ₯  (예: `Ctx 68% of 1M`) β€” μ‚¬μš©λ₯  κΈ°μ€€ λ…Ή/ν™©/적. ν˜„μž¬ λͺ¨λΈμ€ 1M이 기본·프리미엄 μ—†μŒμ΄μ§€λ§Œ, ν† ν°λŸ‰ μžμ²΄κ°€ ν„΄λ‹Ή λΉ„μš©κ³Ό 5H/7D ν•œλ„λ₯Ό νƒœμ›λ‹ˆλ‹€ |
59
59
  | `πŸ’°` | μΊμ‹œκ°€ μ ˆμ•½ν•΄μ€€ λˆ„μ  κΈˆμ•‘ |
60
60
 
61
61
  λ¬Έμ œκ°€ κ°μ§€λ˜λ©΄ **κ²½κ³  칩이 맨 μ•žμ—** λΆ™μŠ΅λ‹ˆλ‹€:
@@ -64,7 +64,7 @@ npm i -g claude-token-saver
64
64
  🚨 5H β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–“ 94% πŸ”„ 12:36 Β· πŸ…· 5/5 Β· πŸ€– Opus 4.8 Β· 🧠 Cache hit 72.1% Β· ⚠ Cache miss Β· πŸ“… weekly β–“β–‘β–‘β–‘β–‘β–‘ 12% πŸ”„ Sun 14:26 Β· πŸ“¦ Ctx 200k Β· last 1d
65
65
  ```
66
66
 
67
- μΉ© μ’…λ₯˜ β€” `🚨 5H/7D NN%`(μΊ‘ μž„λ°•) Β· `⚠ 1M ON` Β· `⚠ Cache miss` Β· `⚠ Input spike` Β· `⚠ Output heavy` Β· `⚠ Call surge` Β· `⚠ Rebuild churn` Β· `⚠ 5m TTL`. 두 μœˆλ„κ°€ λ™μ‹œμ— 90%+λ©΄ 리셋이 μž„λ°•ν•œ μͺ½μ΄ 🚨둜 승격되고 λ‚˜λ¨Έμ§€λŠ” λΉ¨κ°„ μ„Έκ·Έλ¨ΌνŠΈλ‘œ μœ μ§€λ©λ‹ˆλ‹€ (v2.16.0+).
67
+ μΉ© μ’…λ₯˜ β€” `🚨 5H/7D NN%`(μΊ‘ μž„λ°•) Β· `⚠ Ctx 200k+`(단일 μš”μ²­μ΄ μ‹€μ œλ‘œ 200k 초과) Β· `⚠ Cache miss` Β· `⚠ Input spike` Β· `⚠ Output heavy` Β· `⚠ Call surge` Β· `⚠ Rebuild churn` Β· `⚠ 5m TTL`. 두 μœˆλ„κ°€ λ™μ‹œμ— 90%+λ©΄ 리셋이 μž„λ°•ν•œ μͺ½μ΄ 🚨둜 승격되고 λ‚˜λ¨Έμ§€λŠ” λΉ¨κ°„ μ„Έκ·Έλ¨ΌνŠΈλ‘œ μœ μ§€λ©λ‹ˆλ‹€ (v2.16.0+).
68
68
 
69
69
  ### κ²½κ³  칩이 떴을 λ•Œ
70
70
 
@@ -82,6 +82,8 @@ Claude μ•ˆμ—μ„œ `/claude-token-saver` Skill을 μ‹€ν–‰ν•˜κ±°λ‚˜ μΉ© 문ꡬλ₯Ό
82
82
  | `claude-token-saver handoff` | μž‘μ—… μƒνƒœλ₯Ό `HANDOFF-*.md`둜 λ°±μ—… (μΊ‘ μž„λ°• μ‹œ) |
83
83
  | `claude-token-saver mode [keywords...]` | 좜λ ₯ μ„€μ • (`icon`/`text`, `ko`/`en`, `1h`~`30d` μœˆλ„ λ“±) |
84
84
  | `claude-token-saver harness ...` | πŸ…· Harness 관리 (μ•„λž˜ μ°Έκ³ ) |
85
+ | `claude-token-saver frugon` | μ„Έμ…˜ 기둝 β†’ [frugon](https://github.com/Rodiun/frugon) ν˜Έν™˜ JSONL 내보내기 (λͺ¨λΈ λΌμš°νŒ… 절감 뢄석, μ•„λž˜ μ°Έκ³ ) |
86
+ | `claude-token-saver route-scan` | μƒμœ„ λͺ¨λΈμ΄ 반볡 μ²˜λ¦¬ν•œ easy μž‘μ—… 감지 β†’ haiku μœ„μž„ λž«μ³‡ λ£° μ œμ•ˆ (μ•„λž˜ μ°Έκ³ ) |
85
87
  | `claude-token-saver install` | SkillΒ·statusline μˆ˜λ™ 등둝 |
86
88
 
87
89
  좜λ ₯ μ–Έμ–΄λŠ” `mode ko` / `mode en`으둜 μ „ν™˜ν•©λ‹ˆλ‹€ (κΈ°λ³Έ μ˜μ–΄, statusline 칩은 항상 기호). 전체 μ˜΅μ…˜μ€ [영문 README](./README.en.md#options) μ°Έκ³ .
@@ -95,11 +97,15 @@ claude-token-saver harness init # 이 ν”„λ‘œμ νŠΈμ— μ…‹μ—…
95
97
  claude-token-saver harness init --global # ~/.claude/CLAUDE.md β€” λͺ¨λ“  ν”„λ‘œμ νŠΈ 적용
96
98
  claude-token-saver harness check # ν˜„μž¬ 점수 (κΈ€λ‘œλ²Œ fallback 인정)
97
99
  claude-token-saver harness promote <N> --project|--global # κ²½κ³  #N β†’ ratchet λ£° (μŠ€μ½”ν”„ ν•„μˆ˜)
100
+ claude-token-saver harness promote "<λ£° ν…μŠ€νŠΈ>" --project|--global # λ‚΄κ°€ 직접 μ •μ˜ν•œ 룰도 같은 λͺ…λ ΉμœΌλ‘œ 등둝
101
+ claude-token-saver harness pull # κΈ€λ‘œλ²Œ λž«μ³‡ 룰을 이 ν”„λ‘œμ νŠΈλ‘œ κ°€μ Έμ˜€κΈ° (쀑볡 μžλ™ μŠ€ν‚΅)
102
+ claude-token-saver harness pull --harness # κΈ€λ‘œλ²Œ ν•˜λ„€μŠ€ 블둝(5개 μ„Ήμ…˜)κΉŒμ§€ ν•¨κ»˜ κ°€μ Έμ˜€κΈ°
98
103
  claude-token-saver harness list / rm <N> # λ£° 쑰회 / μ‚­μ œ (μžλ™ .bak)
99
104
  claude-token-saver harness off | on # πŸ…· ν‘œμ‹œ ν† κΈ€
100
105
  ```
101
106
 
102
107
  - `promote`λŠ” non-TTY(슀크립트·LLM 호좜)μ—μ„œ `--project`/`--global` ν”Œλž˜κ·Έκ°€ **ν•„μˆ˜** β€” μŠ€μ½”ν”„κ°€ 묻지 μ•Šκ³  κ²°μ •λ˜λŠ” 사고λ₯Ό 막기 μœ„ν•œ μ„€κ³„μž…λ‹ˆλ‹€.
108
+ - μ„€μΉ˜(`install`)λ‚˜ `init`은 ν”„λ‘œμ νŠΈμ— 룰을 μžλ™ μ£Όμž…ν•˜μ§€ μ•ŠμŠ΅λ‹ˆλ‹€. κΈ€λ‘œλ²Œμ— μŒ“μ•„λ‘” 룰을 μƒˆ ν”„λ‘œμ νŠΈμ—μ„œ μ“°κ³  싢을 λ•Œλ§Œ `harness pull`둜 λͺ…μ‹œμ μœΌλ‘œ κ°€μ Έμ˜€μ„Έμš” (μž¬μ‹€ν–‰ν•΄λ„ 쀑볡 μ—†μŒ).
103
109
  - πŸ…·βš  λŸ°νƒ€μž„ κ²½κ³ (`ratchet?` `no-evidence` `PEV-skip`)λŠ” 30λΆ„ ν›„ μžλ™ 만료되고, ν•˜μœ„ 디렉터리 μ„Έμ…˜λ„ ν”„λ‘œμ νŠΈμ— μ˜¬λ°”λ₯΄κ²Œ λ§€μΉ­λ©λ‹ˆλ‹€. PEV-skip은 λ³€κ²½μ„± 도ꡬ(Edit/Write/Bash)만 μΉ΄μš΄νŠΈν•΄ 읽기 μœ„μ£Ό μ„Έμ…˜μ—μ„œλŠ” λ°œλ™ν•˜μ§€ μ•ŠμŠ΅λ‹ˆλ‹€ (v2.16.0+).
104
110
 
105
111
  <details>
@@ -114,11 +120,43 @@ ratchet의 κ°€μΉ˜λŠ” **ν•œ λ°©ν–₯ λˆ„μ **에 μžˆμŠ΅λ‹ˆλ‹€. 룰을 κ°€λ³κ²Œ μ§€
114
120
  μ‚­μ œ μ‹œ `.bak`이 λ‚¨μ§€λ§Œ **κ·Έ 룰이 λ°•νžŒ μ„Έμ…˜ μ»¨ν…μŠ€νŠΈ(μ™œ)λŠ” λ³΅μ›λ˜μ§€ μ•ŠμŠ΅λ‹ˆλ‹€.**
115
121
  </details>
116
122
 
123
+ ## πŸ”€ frugon 연계 β€” "μ–΄λ–€ ν˜ΈμΆœμ„ μ‹Ό λͺ¨λΈλ‘œ 내릴 수 μžˆλ‚˜"
124
+
125
+ claude-token-saverκ°€ μΊμ‹œΒ·μ»¨ν…μŠ€νŠΈ λ‚­λΉ„λ₯Ό μž‘λŠ”λ‹€λ©΄, [frugon](https://github.com/Rodiun/frugon)(둜컬 LLM λΉ„μš© 뢄석기)은 **λͺ¨λΈ λΌμš°νŒ…** 절감 β€” ꡳ이 λΉ„μ‹Ό λͺ¨λΈμ΄ ν•„μš” μ—†λŠ” 호좜 μ°ΎκΈ° β€” 을 λ‹€λ£Ήλ‹ˆλ‹€. `frugon` μ„œλΈŒμ»€λ§¨λ“œκ°€ λ‘˜μ„ μ—°κ²°ν•©λ‹ˆλ‹€:
126
+
127
+ ```bash
128
+ claude-token-saver frugon # 졜근 30일 μ„Έμ…˜ β†’ ./frugon-export.jsonl
129
+ claude-token-saver frugon --run # 내보내기 + frugon analyze λ°”λ‘œ μ‹€ν–‰
130
+ claude-token-saver frugon --days 7 --project myproj --out logs.jsonl
131
+ ```
132
+
133
+ - `~/.claude/projects/`의 transcriptλ₯Ό frugon이 μ½λŠ” OpenAI ν˜Έν™˜ JSONL둜 λ³€ν™˜ν•©λ‹ˆλ‹€. **뢄석은 μ „λΆ€ 둜컬** β€” λ‘œκ·Έλ„ 킀도 λ°–μœΌλ‘œ λ‚˜κ°€μ§€ μ•ŠμŠ΅λ‹ˆλ‹€ (frugon의 원칙과 동일).
134
+ - **μΊμ‹œ 가쀑 토큰(κΈ°λ³Έ):** frugon은 ν”„λ‘¬ν”„νŠΈ 캐싱을 λͺ¨λ₯΄κΈ° λ•Œλ¬Έμ— 물리 토큰을 κ·ΈλŒ€λ‘œ μ£Όλ©΄ λΉ„μš©μ΄ ~10λ°° κ³ΌλŒ€ν‰κ°€λ©λ‹ˆλ‹€. 기본값은 μΊμ‹œ read 0.1x Β· 5m write 1.25x Β· 1h write 2xλ₯Ό μ ‘μ–΄ 넣은 유효 토큰이라 frugon의 λ‹¬λŸ¬ 견적이 μ‹€μ œ 청ꡬ앑과 μΌμΉ˜ν•©λ‹ˆλ‹€. 물리 토큰이 ν•„μš”ν•˜λ©΄ `--raw-tokens`.
135
+ - frugon의 easy/hard λΆ„λ₯˜κ°€ μ“°λŠ” μ‹ ν˜Έ(ν”„λ‘¬ν”„νŠΈΒ·μ‘λ‹΅ 토큰, λŒ€ν™” 깊이)와 `--measure` ν’ˆμ§ˆ 검증에 μ“°λŠ” λ§ˆμ§€λ§‰ μœ μ € ν”„λ‘¬ν”„νŠΈΒ·μ‘λ‹΅ ν…μŠ€νŠΈλ₯Ό λ³΄μ‘΄ν•©λ‹ˆλ‹€. ν…μŠ€νŠΈλ₯Ό λΉΌκ³  μ‹ΆμœΌλ©΄ `--no-content`.
136
+ - frugon μ„€μΉ˜: `pipx install frugon` (λͺ¨λΈμ΄ unpriced둜 λ‚˜μ˜€λ©΄ `frugon update`).
137
+
138
+ ## πŸ”€ route-scan β€” "이 반볡 μž‘μ—…, haiku둜 내렀도 λ©λ‹ˆλ‹€"
139
+
140
+ frugon μ—°κ³„μ˜ μ‹€μ „ λ²„μ „μž…λ‹ˆλ‹€. frugon식 λ‚œμ΄λ„ 뢄석을 **μ—ν”Όμ†Œλ“œ(μ‚¬μš©μž μš”μ²­) λ‹¨μœ„**둜 μ„Έμ…˜ 기둝에 μ μš©ν•΄, μƒμœ„ λͺ¨λΈ(opus/fable)이 반볡 μ²˜λ¦¬ν•΄ 온 easy μž‘μ—…μ„ μ°Ύμ•„ haiku μ„œλΈŒμ—μ΄μ „νŠΈ μœ„μž„ 룰둜 μŠΉκ²©ν•˜λ„λ‘ μ œμ•ˆν•©λ‹ˆλ‹€. μ „ κ³Όμ • 둜컬, 토큰 λΉ„μš© 0.
141
+
142
+ ```bash
143
+ claude-token-saver route-scan # μŠ€μΊ” (24h μΊμ‹œ) + 후보 좜λ ₯
144
+ claude-token-saver harness promote R1 --project # 후보 R1을 λž«μ³‡ 룰둜 등둝
145
+ claude-token-saver route-scan dismiss 1 # 관심 μ—†μœΌλ©΄ λ¬΄μ‹œ (μž¬μŠ€μΊ”μ—λ„ μ•ˆ 뜸)
146
+ ```
147
+
148
+ λ™μž‘ ꡬ쑰 (μ‹€μ‹œκ°„ λΌμš°νŒ…μ΄ μ•„λ‹ˆλΌ **μ„Έμ…˜ 경계 μΊ˜λ¦¬λΈŒλ ˆμ΄μ…˜**):
149
+ 1. `install` μ‹œ SessionStart 훅이 λ“±λ‘λ˜μ–΄, μƒˆ μ„Έμ…˜ μ‹œμž‘Β·`/clear` λ•Œ μΊμ‹œλœ μŠ€μΊ” κ²°κ³Όλ₯Ό μ„Έμ…˜ μ»¨ν…μŠ€νŠΈλ‘œ μ£Όμž…ν•©λ‹ˆλ‹€ (μŠ€μΊ” μžμ²΄λŠ” λ°±κ·ΈλΌμš΄λ“œμ—μ„œ 일 1회).
150
+ 2. 반볡(β‰₯3회) easy νŒ¨ν„΄μ΄ 있으면 statusline에 `πŸ…·βš  route? R1` 칩이 뜨고, Claudeκ°€ 등둝 여뢀와 scope(`--project`/`--global`)λ₯Ό λ¬Όμ–΄λ΄…λ‹ˆλ‹€.
151
+ 3. λ“±λ‘λœ 룰은 ratchet.md에 μŒ“μ—¬ **λ‹€μŒ μ„Έμ…˜λΆ€ν„° 메인 λͺ¨λΈμ΄ ν•΄λ‹Ή μœ ν˜•μ„ haiku μ„œλΈŒμ—μ΄μ „νŠΈλ‘œ μžλ™ μœ„μž„**ν•©λ‹ˆλ‹€.
152
+
153
+ ꢌμž₯ 사전 μ€€λΉ„: `~/.claude/agents/`에 `model: haiku` μ„œλΈŒμ—μ΄μ „νŠΈ(예: haiku-explore / haiku-runner / haiku-translate)λ₯Ό λ§Œλ“€μ–΄ 두면 룰이 λ°”λ‘œ μ‹€ν–‰ κ°€λŠ₯ν•΄μ§‘λ‹ˆλ‹€.
154
+
117
155
  ## 토큰 급증 원인 μ½”λ“œ
118
156
 
119
157
  | μ½”λ“œ | 의미 |
120
158
  |---|---|
121
- | `LARGE_INPUT_PER_REQUEST` | 단일 μš”μ²­ 250k+ β†’ 1M μ»¨ν…μŠ€νŠΈ μ˜μ‹¬ |
159
+ | `LARGE_INPUT_PER_REQUEST` | 단일 μš”μ²­ μž…λ ₯이 200k 초과 β€” ν„΄λ‹Ή μž¬κ³ΌκΈˆΒ·ν•œλ„ μ†Œλͺ¨ 급증 |
122
160
  | `LOW_HIT_RATE` | μΊμ‹œ 히트율 50% 미만 |
123
161
  | `BUCKET_5M_DOMINANT` | μΊμ‹œ μ“°κΈ°μ˜ 70%+κ°€ 5λΆ„ 버킷 (Pro ν”Œλžœ/Max λ‹€μš΄κ·Έλ ˆμ΄λ“œ) |
124
162
  | `HIGH_OUTPUT_RATIO` | 좜λ ₯/μž…λ ₯ λΉ„μœ¨ 0.15 초과 (좜λ ₯ λ‹¨κ°€λŠ” μž…λ ₯의 5λ°°) |
@@ -170,6 +208,22 @@ npm uninstall -g claude-cache-monitor && npm i -g claude-token-saver
170
208
 
171
209
  ## 릴리슀 λ…ΈνŠΈ
172
210
 
211
+ ### v3.0.0 (2026-07-13)
212
+ - **메이저 승격** β€” v2.19 frugon 연계 + v2.20 route-scan으둜 "사후 토큰 λͺ¨λ‹ˆν„°λ§ 도ꡬ"μ—μ„œ "반볡 easy μž‘μ—…μ„ μ‹Ό λͺ¨λΈλ‘œ λ‚΄λ €λ³΄λ‚΄λŠ” λΌμš°νŒ… 계측"으둜 μ œν’ˆ 성격이 λ°”λ€Œμ–΄ 메이저 버전을 μ˜¬λ¦½λ‹ˆλ‹€. Breaking changeλŠ” μ—†μŠ΅λ‹ˆλ‹€ (κΈ°μ‘΄ λͺ…λ ΉΒ·μ„€μ • μ „λΆ€ ν˜Έν™˜).
213
+ - **route-scan promote ꡐ정** β€” `harness promote R<N> --project`κ°€ 이제 후보가 **κ°μ§€λœ ν”„λ‘œμ νŠΈ**의 `.claude/ratchet.md`에 룰을 κΈ°λ‘ν•©λ‹ˆλ‹€ (μ΄μ „μ—λŠ” CLIλ₯Ό μ‹€ν–‰ν•œ 디렉터리에 κΈ°λ‘λ˜λŠ” 버그). μŠ€μΊ”μ΄ 후보에 μ‹€μ œ μ„Έμ…˜ 경둜(`projectPath`)λ₯Ό μ €μž₯ν•˜λ©°, 이 ν•„λ“œκ°€ μ—†λŠ” ꡬ버전 μΊμ‹œμ—μ„œ λ‹€λ₯Έ ν”„λ‘œμ νŠΈ 후보λ₯Ό μŠΉκ²©ν•˜λ € ν•˜λ©΄ `route-scan --refresh`λ₯Ό μ•ˆλ‚΄ν•˜κ³  μ€‘λ‹¨ν•©λ‹ˆλ‹€.
214
+ - **`harness pull` μ‹ μ„€** β€” κΈ€λ‘œλ²Œ λž«μ³‡ 룰을 ν”„λ‘œμ νŠΈλ‘œ λͺ…μ‹œμ μœΌλ‘œ κ°€μ Έμ˜΅λ‹ˆλ‹€ (`--harness`둜 κΈ€λ‘œλ²Œ ν•˜λ„€μŠ€ λΈ”λ‘κΉŒμ§€). λ£° ν…μŠ€νŠΈ κΈ°μ€€ 쀑볡 μžλ™ μŠ€ν‚΅μ΄λΌ μž¬μ‹€ν–‰ν•΄λ„ μ•ˆμ „(λ©±λ“±). μ„€μΉ˜Β·init은 계속 아무것도 μžλ™ μ£Όμž…ν•˜μ§€ μ•ŠμŠ΅λ‹ˆλ‹€ β€” κ°€μ Έμ˜€κΈ°λŠ” 항상 opt-in.
215
+
216
+ ### v2.20.0 (2026-07-13)
217
+ - **route-scan**: μƒμœ„ λͺ¨λΈμ΄ 반볡 μ²˜λ¦¬ν•œ easy μž‘μ—… 감지 β†’ `πŸ…·βš  route? R<N>` μΉ© + SessionStart ν›… μ»¨ν…μŠ€νŠΈ μ£Όμž… + `harness promote R<N> --project|--global`둜 haiku μœ„μž„ λž«μ³‡ λ£° 승격.
218
+
219
+ ### v2.19.0 (2026-07-12)
220
+ - **frugon 연계**: `claude-token-saver frugon` β€” μ„Έμ…˜ transcriptλ₯Ό [frugon](https://github.com/Rodiun/frugon) ν˜Έν™˜ JSONL둜 내보내 λͺ¨λΈ λΌμš°νŒ… 절감 뢄석 (`--run`으둜 μ¦‰μ‹œ 뢄석, μΊμ‹œ 가쀑 토큰 κΈ°λ³Έ).
221
+
222
+ ### v2.18.0 (2026-07-02)
223
+ - **1M μ»¨ν…μŠ€νŠΈ κ²½κ³  의미 μž¬μ •μ˜** β€” ν˜„μž¬ λͺ¨λΈ(Fable 5, Opus 4.6~4.8, Sonnet 5)은 μ „λΆ€ 1M μœˆλ„κ°€ 기본이고 Opus 4.7λΆ€ν„° μž₯κΈ° μ»¨ν…μŠ€νŠΈ 프리미엄도 μ—†μ–΄, "1M λͺ¨λ“œ ON = λΉ„μŒˆ" ν”„λ ˆμž„μ„ νκΈ°ν–ˆμŠ΅λ‹ˆλ‹€. κ²½κ³ λŠ” 이제 **μ‹€μ‚¬μš© μ‹ ν˜Έ**μž…λ‹ˆλ‹€: `⚠ 1M ON` β†’ `⚠ Ctx 200k+`(단일 μš”μ²­μ΄ μ‹€μ œλ‘œ 200k 초과), μ²˜λ°©λ„ "1M 끄기" μš°μ„ μ—μ„œ "`/compact`/`/clear` + `/effort` 점검" μš°μ„ μœΌλ‘œ μž¬μ •λ ¬. 잘λͺ»λœ "200k 초과 μ‹œ μž₯κΈ° μš”κΈˆ 적용" 문ꡬ μ •μ •.
224
+ - **πŸ“¦ μ„Έκ·Έλ¨ΌνŠΈκ°€ μ‹€μ‹œκ°„ μ‚¬μš©λ₯  ν‘œμ‹œ** β€” Claude Code stdin의 `context_window.used_percentage`λ₯Ό μ‚¬μš©ν•΄ `πŸ“¦ Ctx 68% of 1M` ν˜•νƒœλ‘œ λ Œλ” (μ‚¬μš©λ₯  κΈ°μ€€ λ…Ή <70 / ν™© 70–89 / 적 90+). stdin이 μ—†μœΌλ©΄ κΈ°μ‘΄ 크기 μΆ”λ‘ μœΌλ‘œ ν΄λ°±ν•˜λ˜ 1M은 λΉ¨κ°• λŒ€μ‹  λ…Έλž‘.
225
+ - ꡬ버전 νžˆμŠ€ν† λ¦¬ ν˜Έν™˜: `⚠ 1M ON` 칩·ꡬ λ””ν…ŒμΌ 문ꡬ도 계속 ν•΄μ„λ©λ‹ˆλ‹€.
226
+
173
227
  ### v2.17.0 (2026-07-02)
174
228
  - **Fable 5 가격 ν‹°μ–΄ μΆ”κ°€** β€” `claude-fable-5`/`claude-mythos-5`κ°€ Sonnet 단가($3/$15)둜 폴백돼 λΉ„μš©μ΄ ~3λ°° κ³Όμ†Œ μΆ”μ •λ˜λ˜ 문제 μˆ˜μ •. μ‹€μ œ 단가(μž…λ ₯ $10 / 좜λ ₯ $50 / μΊμ‹œμ“°κΈ° 5m $12.50Β·1h $20 / μΊμ‹œμ½κΈ° $1) 적용.
175
229
  - README μ „λ©΄ 개편 β€” μ΅œμƒμœ„ μž„νŒ©νŠΈ μš”μ•½, μ„Έκ·Έλ¨ΌνŠΈ ν‘œ, harness scope ν”Œλž˜κ·Έ λ¬Έμ„œν™”, 가격 ν…Œμ΄λΈ” μ΅œμ‹ ν™”.
package/bin/cli.js CHANGED
@@ -9,6 +9,9 @@
9
9
  * npx claude-token-saver --format json # JSON output
10
10
  * npx claude-token-saver --format csv # CSV output
11
11
  * npx claude-token-saver --project myproj # filter by project
12
+ * npx claude-token-saver route-scan # detect recurring easy work β†’ haiku-delegation candidates
13
+ * npx claude-token-saver frugon # export transcripts β†’ frugon JSONL (model-routing analysis)
14
+ * npx claude-token-saver frugon --run # export + run `frugon analyze`
12
15
  * npx claude-token-saver --install-hook # install PostToolUse hook
13
16
  * npx claude-token-saver --uninstall-hook # remove hook
14
17
  * npx claude-token-saver --hook-run # internal: called by hook
@@ -93,6 +96,24 @@ function bedrockDisplayFromId(id) {
93
96
  return `${family} ${m[2]}.${m[3]}`;
94
97
  }
95
98
 
99
+ /**
100
+ * Live context usage from Claude Code's stdin payload (`context_window`).
101
+ * More accurate than inferring from transcripts: it's the CURRENT session's
102
+ * real fill level, updated every refresh. Shape (subset):
103
+ * "context_window": { "context_window_size": 200000, "used_percentage": 68 }
104
+ */
105
+ function extractContextUsage(stdinJson) {
106
+ const cw = stdinJson && stdinJson.context_window;
107
+ if (!cw || typeof cw !== 'object') return null;
108
+ const usedPct = Number(cw.used_percentage);
109
+ const size = Number(cw.context_window_size);
110
+ if (!Number.isFinite(usedPct)) return null;
111
+ return {
112
+ usedPct,
113
+ size: Number.isFinite(size) && size > 0 ? size : null,
114
+ };
115
+ }
116
+
96
117
  function extractModel(stdinJson) {
97
118
  if (!stdinJson || !stdinJson.model) return null;
98
119
  const m = stdinJson.model;
@@ -380,6 +401,7 @@ async function main() {
380
401
  };
381
402
  const r = installAll({ force });
382
403
  print('skill', r.skill);
404
+ print('SessionStart hook (route-scan)', r.sessionStartHook);
383
405
  {
384
406
  const s = r.statusline;
385
407
  const verb = s.action === 'exists' ? 'already configured (refreshInterval=5)'
@@ -444,11 +466,173 @@ async function main() {
444
466
  return;
445
467
  }
446
468
 
469
+ // Subcommand: route-scan β€” detect recurring easy work on expensive models
470
+ // and propose model-delegation ratchet rules. Zero token cost, fully local.
471
+ // claude-token-saver route-scan # scan (24h cache) + print candidates
472
+ // claude-token-saver route-scan --refresh # force rescan
473
+ // claude-token-saver route-scan --days 30 # wider lookback
474
+ // claude-token-saver route-scan --hook # SessionStart hook mode (context injection)
475
+ // claude-token-saver route-scan dismiss <N> # mute candidate R<N>
476
+ // Promote a candidate to a ratchet rule (scope is always explicit):
477
+ // claude-token-saver harness promote R<N> --project|--global
478
+ if (args[0] === 'route-scan') {
479
+ const rs = await import('../src/route-scan.js');
480
+ const { userLanguage } = await import('../src/config.js');
481
+ const lang = userLanguage();
482
+
483
+ if (args[1] === 'dismiss') {
484
+ const n = parseInt(args[2], 10);
485
+ if (!Number.isFinite(n)) {
486
+ console.error('Usage: claude-token-saver route-scan dismiss <N> # N from `route? R<N>`');
487
+ process.exit(1);
488
+ }
489
+ const cand = rs.resolveCandidate(n);
490
+ if (!cand) {
491
+ console.error(`No route candidate R${n}. Run: claude-token-saver route-scan`);
492
+ process.exit(1);
493
+ }
494
+ console.log(lang === 'ko'
495
+ ? `R${n} λ¬΄μ‹œ 처리: ${cand.label} (${cand.project}) β€” μž¬μŠ€μΊ”μ—λ„ λ‹€μ‹œ λœ¨μ§€ μ•ŠμŠ΅λ‹ˆλ‹€.`
496
+ : `Dismissed R${n}: ${cand.label} (${cand.project}) β€” won't resurface on rescans.`);
497
+ return;
498
+ }
499
+
500
+ // --hook: SessionStart hook mode. Never scans inline (session start must
501
+ // stay fast) β€” reads the cache, kicks a detached refresh when stale, and
502
+ // prints delegation-candidate context for the new session.
503
+ if (hasFlag('--hook')) {
504
+ let cache = rs.readRouteScan();
505
+ if (!rs.isCacheFresh(cache)) {
506
+ try {
507
+ const { spawn } = await import('node:child_process');
508
+ spawn(process.execPath, [process.argv[1], 'route-scan', '--refresh', '--quiet'],
509
+ { detached: true, stdio: 'ignore' }).unref();
510
+ } catch { /* refresh is best-effort; stale cache still usable below */ }
511
+ }
512
+ const open = rs.openCandidates(cache);
513
+ if (open.length === 0) return; // silent β€” nothing to inject
514
+ const lines = [];
515
+ lines.push(`[claude-token-saver route-scan] 졜근 ${cache.days}일 μ„Έμ…˜μ—μ„œ μƒμœ„ λͺ¨λΈ(opus/fable)이 μ²˜λ¦¬ν•œ 반볡 easy μž‘μ—…μ΄ κ°μ§€λ˜μ—ˆμŠ΅λ‹ˆλ‹€:`);
516
+ for (const c of open) {
517
+ lines.push(` R${c.id} (Γ—${c.count}, ${c.project}): ${c.label} β†’ ${c.agent} μœ„μž„ ꢌμž₯ (scope μ œμ•ˆ: ${c.suggestedScope})`);
518
+ lines.push(` μ˜ˆμ‹œ: "${c.example}"`);
519
+ }
520
+ lines.push('이 νŒ¨ν„΄μ„ λž«μ³‡ 룰둜 λ“±λ‘ν•˜λ©΄ λ‹€μŒ μ„Έμ…˜λΆ€ν„° μžλ™ μœ„μž„λ©λ‹ˆλ‹€. μ μ ˆν•œ μ‹œμ μ— μ‚¬μš©μžμ—κ²Œ 등둝 여뢀와 scopeλ₯Ό λ¬Όμ–΄λ³Έ λ’€ μ‹€ν–‰ν•˜μ„Έμš”:');
521
+ lines.push(' claude-token-saver harness promote R<N> --project|--global # scopeλŠ” λ°˜λ“œμ‹œ μ‚¬μš©μžμ—κ²Œ 확인');
522
+ lines.push(' claude-token-saver route-scan dismiss <N> # μ‚¬μš©μžκ°€ μ›μΉ˜ μ•ŠμœΌλ©΄');
523
+ console.log(lines.join('\n'));
524
+ return;
525
+ }
526
+
527
+ const days = parseFloat(getArg('--days') || '14');
528
+ let cache = rs.readRouteScan();
529
+ if (hasFlag('--refresh') || !rs.isCacheFresh(cache) || (cache && cache.days !== days)) {
530
+ cache = await rs.runRouteScan({ days });
531
+ }
532
+ if (hasFlag('--quiet')) return;
533
+ if (hasFlag('--json')) {
534
+ console.log(JSON.stringify(cache, null, 2));
535
+ return;
536
+ }
537
+ const easyPct = cache.totalEpisodes ? Math.round(cache.easyEpisodes / cache.totalEpisodes * 100) : 0;
538
+ console.log(lang === 'ko'
539
+ ? `route-scan β€” 졜근 ${cache.days}일: μ—ν”Όμ†Œλ“œ ${cache.totalEpisodes}건 쀑 easy ${cache.easyEpisodes}건 (${easyPct}%) [μŠ€μΊ”: ${cache.scannedAt}]`
540
+ : `route-scan β€” last ${cache.days}d: ${cache.easyEpisodes}/${cache.totalEpisodes} episodes easy (${easyPct}%) [scanned: ${cache.scannedAt}]`);
541
+ const open = rs.openCandidates(cache);
542
+ if (open.length === 0) {
543
+ console.log(lang === 'ko'
544
+ ? 'μœ„μž„ 후보 μ—†μŒ (반볡 3회 λ―Έλ§Œμ΄κ±°λ‚˜ 이미 처리됨).'
545
+ : 'No delegation candidates (below recurrence threshold or already resolved).');
546
+ return;
547
+ }
548
+ console.log(lang === 'ko' ? '\nμœ„μž„ 후보:' : '\nDelegation candidates:');
549
+ for (const c of open) {
550
+ console.log(` R${c.id} Γ—${c.count} ${c.label} β†’ ${c.agent} [${c.project}] (scope μ œμ•ˆ: ${c.suggestedScope})`);
551
+ console.log(` μ˜ˆμ‹œ: "${c.example}"`);
552
+ console.log(` λ£°: ${c.rule}`);
553
+ }
554
+ console.log('');
555
+ console.log(lang === 'ko' ? '등둝 / λ¬΄μ‹œ:' : 'Promote / dismiss:');
556
+ console.log(' claude-token-saver harness promote R<N> --project|--global');
557
+ console.log(' claude-token-saver route-scan dismiss <N>');
558
+ return;
559
+ }
560
+
561
+ // Subcommand: frugon β€” export Claude Code transcripts to the JSONL format
562
+ // frugon (local LLM cost analyzer, github.com/Rodiun/frugon) analyzes, so
563
+ // users can see which calls could route to a cheaper model.
564
+ // claude-token-saver frugon # export last 30 days β†’ ./frugon-export.jsonl
565
+ // claude-token-saver frugon --days 7 # narrower window
566
+ // claude-token-saver frugon --project myproj # filter by project dir substring
567
+ // claude-token-saver frugon --out PATH # custom output path
568
+ // claude-token-saver frugon --raw-tokens # physical token counts (no cache weighting)
569
+ // claude-token-saver frugon --no-content # strip prompt/reply text (counts only)
570
+ // claude-token-saver frugon --run # run `frugon analyze` on the export
571
+ if (args[0] === 'frugon') {
572
+ const { exportFrugonLogs } = await import('../src/frugon-export.js');
573
+ const { userLanguage } = await import('../src/config.js');
574
+ const lang = userLanguage();
575
+ const days = parseFloat(getArg('--days') || '30');
576
+ const outPath = getArg('--out') || 'frugon-export.jsonl';
577
+ const cacheWeighted = !hasFlag('--raw-tokens');
578
+ const includeContent = !hasFlag('--no-content');
579
+ const res = await exportFrugonLogs({
580
+ days,
581
+ projectFilter: getArg('--project') || undefined,
582
+ outPath,
583
+ cacheWeighted,
584
+ includeContent,
585
+ });
586
+ if (res.records === 0) {
587
+ console.log(lang === 'ko'
588
+ ? `졜근 ${days}일 λ‚΄ μ„Έμ…˜ 기둝이 μ—†μŠ΅λ‹ˆλ‹€ (~/.claude/projects).`
589
+ : `No session records in the last ${days} days (~/.claude/projects).`);
590
+ return;
591
+ }
592
+ console.log(lang === 'ko'
593
+ ? `frugon 둜그 내보내기 μ™„λ£Œ: ${res.outPath}`
594
+ : `frugon log export complete: ${res.outPath}`);
595
+ console.log(` ${res.records} calls / ${res.sessions} sessions / last ${days}d`);
596
+ const byModel = Object.entries(res.models).sort((a, b) => b[1] - a[1]);
597
+ for (const [model, count] of byModel) console.log(` ${model}: ${count}`);
598
+ console.log(cacheWeighted
599
+ ? (lang === 'ko'
600
+ ? ' prompt_tokensλŠ” μΊμ‹œ κ°€μ€‘μΉ˜ μ μš©κ°’ (read 0.1x, 5m write 1.25x, 1h write 2x) β€” frugon λΉ„μš©μ΄ μ‹€μ œ 청ꡬ앑과 일치. ν•΄μ œ: --raw-tokens'
601
+ : ' prompt_tokens are cache-weighted (read 0.1x, 5m write 1.25x, 1h write 2x) so frugon costs match your real bill. Disable: --raw-tokens')
602
+ : (lang === 'ko'
603
+ ? ' prompt_tokensλŠ” 물리 토큰 수 (μΊμ‹œ κ°€μ€‘μΉ˜ μ—†μŒ β€” frugon λΉ„μš©μ΄ μ‹€μ œλ³΄λ‹€ 크게 λ‚˜μ˜΄)'
604
+ : ' prompt_tokens are raw physical counts (no cache weighting β€” frugon will overstate cost)'));
605
+ if (hasFlag('--run')) {
606
+ const { spawnSync } = await import('node:child_process');
607
+ console.log('');
608
+ const run = spawnSync('frugon', ['analyze', res.outPath], { stdio: 'inherit' });
609
+ if (run.error && run.error.code === 'ENOENT') {
610
+ console.error(lang === 'ko'
611
+ ? 'frugon이 PATH에 μ—†μŠ΅λ‹ˆλ‹€. μ„€μΉ˜: pipx install frugon (λ˜λŠ” pip install frugon)'
612
+ : 'frugon not found on PATH. Install: pipx install frugon (or pip install frugon)');
613
+ process.exit(1);
614
+ }
615
+ if (typeof run.status === 'number' && run.status !== 0) process.exit(run.status);
616
+ return;
617
+ }
618
+ console.log('');
619
+ console.log(lang === 'ko' ? 'λ‹€μŒ 단계:' : 'Next step:');
620
+ console.log(` frugon analyze ${res.outPath}`);
621
+ console.log(lang === 'ko'
622
+ ? ' (frugon λ―Έμ„€μΉ˜ μ‹œ: pipx install frugon β€” 뢄석은 μ „λΆ€ λ‘œμ»¬μ—μ„œ μ‹€ν–‰λ©λ‹ˆλ‹€)'
623
+ : ' (if frugon is not installed: pipx install frugon β€” analysis runs fully local)');
624
+ console.log(lang === 'ko'
625
+ ? ' (unpriced λͺ¨λΈμ΄ λ‚˜μ˜€λ©΄: frugon update 둜 κ°€κ²©ν‘œλ₯Ό κ°±μ‹ ν•˜μ„Έμš”)'
626
+ : ' (if models show as unpriced: run `frugon update` to refresh the pricing table)');
627
+ return;
628
+ }
629
+
447
630
  // Subcommand: harness β€” manage the project's CLAUDE.md harness rules.
448
631
  // claude-token-saver harness init # write CLAUDE.md (5 sections) + ratchet.md
449
632
  // claude-token-saver harness uninit # remove harness block from CLAUDE.md (backup kept)
450
633
  // claude-token-saver harness check # show πŸ…· N/5 + which sections are missing
451
634
  // claude-token-saver harness promote "<rule>" # append a rule to ratchet.md
635
+ // claude-token-saver harness pull [--harness] # copy global ratchet rules (+block) into this project
452
636
  // claude-token-saver harness off | on # toggle the statusline πŸ…· segment
453
637
  if (args[0] === 'harness') {
454
638
  const sub = args[1];
@@ -467,7 +651,7 @@ async function main() {
467
651
  }
468
652
  return dflt;
469
653
  };
470
- const { harnessInit, harnessUninit, harnessStatus, harnessPromote, harnessListRules, harnessRmRule, findProjectRoot } =
654
+ const { harnessInit, harnessUninit, harnessStatus, harnessPromote, harnessPull, harnessListRules, harnessRmRule, findProjectRoot } =
471
655
  await import('../src/harness.js');
472
656
  const { HARNESS_SECTIONS } = await import('../src/harness-templates.js');
473
657
  const { loadConfig, saveConfig } = await import('../src/config.js');
@@ -561,6 +745,28 @@ async function main() {
561
745
  }
562
746
  rule = `반볡 감지 Γ—${cand.count}: ${cand.pattern} β€” TODO: μ›μΈΒ·μ˜ˆλ°©μ±… ν•œ μ€„λ‘œ`;
563
747
  }
748
+ // R-prefixed arg β†’ route-scan delegation candidate (statusline `route? R<N>`).
749
+ // The rule text is pre-generated by the scan; promoting also resolves the
750
+ // candidate so the chip stops and rescans don't resurface it.
751
+ let routeCandidateId = null;
752
+ let routeCandidate = null;
753
+ if (/^[Rr]\d+$/.test(raw)) {
754
+ const n = parseInt(raw.slice(1), 10);
755
+ const rs = await import('../src/route-scan.js');
756
+ const cand = (rs.openCandidates(rs.readRouteScan()) || []).find((c) => c.id === n);
757
+ if (!cand) {
758
+ console.error(`No open route candidate R${n}. Run: claude-token-saver route-scan`);
759
+ process.exit(1);
760
+ }
761
+ rule = cand.rule;
762
+ routeCandidateId = n;
763
+ routeCandidate = cand;
764
+ if (!scope) {
765
+ console.error(`Route candidate R${n} requires an explicit scope (suggested: --${cand.suggestedScope}).`);
766
+ console.error('Ask the user, then pass --project or --global.');
767
+ process.exit(1);
768
+ }
769
+ }
564
770
  // Scope resolution: explicit flag wins. Otherwise prompt interactively
565
771
  // when running on a TTY; in non-TTY (CI/scripts) require an explicit
566
772
  // flag so the choice is never silently made for the caller.
@@ -586,9 +792,31 @@ async function main() {
586
792
  process.exit(1);
587
793
  }
588
794
  }
589
- const r = harnessPromote(rule, { scope });
795
+ // Route candidates were detected in a specific project's sessions β€” a
796
+ // --project rule must land in THAT project's ratchet.md, not the cwd's.
797
+ // The candidate carries the real session cwd (projectPath); older cached
798
+ // scans predate that field, so fall back to verifying the cwd matches.
799
+ let promoteRoot;
800
+ if (routeCandidate && scope === 'project') {
801
+ const rs = await import('../src/route-scan.js');
802
+ if (routeCandidate.projectPath) {
803
+ promoteRoot = findProjectRoot(routeCandidate.projectPath);
804
+ } else if (rs.mungeProjectPath(findProjectRoot()) !== routeCandidate.project) {
805
+ console.error(`Route candidate R${routeCandidateId} was detected in another project (${routeCandidate.project}),`);
806
+ console.error('but this cached scan predates project-path tracking.');
807
+ console.error('Re-scan to capture it, then promote again:');
808
+ console.error(' claude-token-saver route-scan --refresh');
809
+ process.exit(1);
810
+ }
811
+ }
812
+ const r = harnessPromote(rule, promoteRoot ? { scope, root: promoteRoot } : { scope });
590
813
  console.log(`Appended to ${r.path} [${r.scope}]:`);
591
814
  console.log(` - ${rule}`);
815
+ if (routeCandidateId !== null) {
816
+ const rs = await import('../src/route-scan.js');
817
+ rs.resolveCandidate(routeCandidateId);
818
+ console.log(`(route candidate R${routeCandidateId} resolved β€” λ‹€μŒ μ„Έμ…˜λΆ€ν„° μžλ™ μœ„μž„ 룰둜 μ μš©λ©λ‹ˆλ‹€)`);
819
+ }
592
820
  if (/^\d+$/.test(raw)) {
593
821
  console.log('\nπŸ‘‰ ratchet.mdλ₯Ό μ—΄μ–΄ TODO 뢀뢄을 μ‹€μ œ 룰둜 λ‹€λ“¬μ–΄μ£Όμ„Έμš”.');
594
822
  }
@@ -641,6 +869,26 @@ async function main() {
641
869
  return;
642
870
  }
643
871
 
872
+ if (sub === 'pull') {
873
+ // Pull the user's GLOBAL ratchet rules (and optionally the global
874
+ // harness block) into this project. Explicitly opt-in β€” install/init
875
+ // never auto-injects rules into a project.
876
+ const includeBlock = args.includes('--harness');
877
+ const r = harnessPull({ includeBlock });
878
+ console.log(`Pull global β†’ project (${r.root})`);
879
+ if (r.added.length) {
880
+ console.log(`βœ… ${r.added.length} rule(s) pulled into .claude/ratchet.md:`);
881
+ for (const t of r.added) console.log(` - ${t}`);
882
+ } else {
883
+ console.log('No new rules to pull.');
884
+ }
885
+ if (r.skippedRules) console.log(` (${r.skippedRules} already present β€” skipped)`);
886
+ for (const f of r.wrote.filter((w) => w.includes('CLAUDE.md'))) console.log(`βœ… ${f}`);
887
+ for (const f of r.skipped) console.log(` skip: ${f}`);
888
+ if (!includeBlock) console.log('\nκΈ€λ‘œλ²Œ ν•˜λ„€μŠ€ 블둝(CLAUDE.md 5개 μ„Ήμ…˜)κΉŒμ§€ κ°€μ Έμ˜€λ €λ©΄: claude-token-saver harness pull --harness');
889
+ return;
890
+ }
891
+
644
892
  if (sub === 'list' || sub === 'ls') {
645
893
  const wantGlobal = hasFlag('--global');
646
894
  const wantProject = hasFlag('--project') || !wantGlobal;
@@ -704,7 +952,7 @@ async function main() {
704
952
  }
705
953
 
706
954
  console.error(`Unknown harness subcommand: ${sub}`);
707
- console.error('Usage: claude-token-saver harness [check|init|uninit [--purge-ratchet]|promote "<rule>"|list|rm <N>|off|on]');
955
+ console.error('Usage: claude-token-saver harness [check|init|uninit [--purge-ratchet]|promote "<rule>"|pull [--harness]|list|rm <N>|off|on]');
708
956
  process.exit(1);
709
957
  }
710
958
 
@@ -894,6 +1142,7 @@ async function main() {
894
1142
  const stdinJson = readStdinJson();
895
1143
  let caps = extractCaps(stdinJson);
896
1144
  let model = extractModel(stdinJson);
1145
+ const ctxLive = extractContextUsage(stdinJson);
897
1146
  if (isStatusline && (caps || model)) {
898
1147
  try {
899
1148
  const { persistSnapshot } = await import('../src/caps-cache.js');
@@ -923,7 +1172,7 @@ async function main() {
923
1172
  if (format === 'statusline') {
924
1173
  if (contextWindow.size === '1M') {
925
1174
  spikeChip = chipForIssues([], contextWindow);
926
- chipDetail = `Context auto-promoted to 1M (max single-request ${Math.round(contextWindow.maxContext / 1000)}k tokens)`;
1175
+ chipDetail = `Single-request context exceeded 200k (max ${Math.round(contextWindow.maxContext / 1000)}k tokens)`;
927
1176
  } else {
928
1177
  const recentSession = sessions
929
1178
  .slice()
@@ -994,6 +1243,7 @@ async function main() {
994
1243
  lastActivity,
995
1244
  spikeReport,
996
1245
  contextWindow,
1246
+ ctxLive,
997
1247
  spikeChip,
998
1248
  caps,
999
1249
  model,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-token-saver",
3
- "version": "2.17.0",
3
+ "version": "3.0.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": {