claude-token-saver 3.6.4 β†’ 3.7.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
@@ -243,6 +243,9 @@ Also update `statusLine.command` in `~/.claude/settings.json` to `claude-token-s
243
243
 
244
244
  ## Release notes
245
245
 
246
+ ### v3.7.0 (2026-07-29)
247
+ - **Fixed: delegation rules named subagents that may not exist** β€” generated T2 rules hard-coded preset names like `haiku-explore` / `haiku-runner`, which live in each user's own `~/.claude/agents/` and are not shipped by this package. On a machine without them, the rule told the model to delegate to a nonexistent agent. The default phrasing is now `model: haiku` (matching T1's `model: sonnet`), and the name is added only when the agent file is really present, e.g. `haiku-explore(model: haiku)`. Project-level `.claude/agents/` counts too. `ratchet-model.md` re-renders with the new phrasing on the next `route-scan`.
248
+
246
249
  ### v3.6.4 (2026-07-29)
247
250
  - **Fixed: false `πŸ…·βš  ratchet-unloaded`** β€” the import check looked at a single CLAUDE.md. When the project file carried the harness block, only that file was inspected, so the common layout of block-in-project + `@` imports-in-global raised the warning even though the rules loaded fine. Claude Code loads both files, so the flags are now the union of the two, and `importSource` (`project`/`global`/`both`) says which file carries them. The warning fires only when neither file imports.
248
251
 
package/README.md CHANGED
@@ -200,6 +200,9 @@ npm uninstall -g claude-cache-monitor && npm i -g claude-token-saver
200
200
 
201
201
  ## 릴리슀 λ…ΈνŠΈ
202
202
 
203
+ ### v3.7.0 (2026-07-29)
204
+ - **μœ„μž„ 룰이 μ—†λŠ” μ„œλΈŒμ—μ΄μ „νŠΈλ₯Ό κ°€λ¦¬ν‚€λ˜ 문제 μˆ˜μ •** β€” μƒμ„±λ˜λŠ” T2 룰이 `haiku-explore`Β·`haiku-runner` 같은 이름을 직접 μ μ—ˆλŠ”λ°, 이 preset μ—μ΄μ „νŠΈλ“€μ€ 각자의 `~/.claude/agents/`에 μžˆλŠ” 것이라 νŒ¨ν‚€μ§€κ°€ λ°°ν¬ν•˜μ§€ μ•ŠμŠ΅λ‹ˆλ‹€. κ·Έλž˜μ„œ ν•΄λ‹Ή 파일이 μ—†λŠ” ν™˜κ²½μ—μ„œλŠ” "μ‘΄μž¬ν•˜μ§€ μ•ŠλŠ” μ—μ΄μ „νŠΈλ‘œ μœ„μž„ν•˜λΌ"λŠ” 룰이 μžλ™ μƒμ„±λμŠ΅λ‹ˆλ‹€. 이제 κΈ°λ³Έ ν‘œν˜„μ€ `model: haiku`(T1의 `model: sonnet`κ³Ό 톡일)이고, μ—μ΄μ „νŠΈ 파일이 μ‹€μ œλ‘œ μžˆμ„ λ•Œλ§Œ `haiku-explore(model: haiku)`처럼 이름을 λ³‘κΈ°ν•©λ‹ˆλ‹€. ν”„λ‘œμ νŠΈ `.claude/agents/`도 μΈμ‹ν•©λ‹ˆλ‹€. λ‹€μŒ `route-scan` λ•Œ `ratchet-model.md`κ°€ μƒˆ ν‘œν˜„μœΌλ‘œ λ‹€μ‹œ λ Œλ”λ©λ‹ˆλ‹€.
205
+
203
206
  ### v3.6.4 (2026-07-29)
204
207
  - **`πŸ…·βš  ratchet-unloaded`κ°€ 잘λͺ» 뜨던 버그 μˆ˜μ •** β€” import μ—¬λΆ€λ₯Ό CLAUDE.md ν•œ νŒŒμΌμ—μ„œλ§Œ ν™•μΈν–ˆμŠ΅λ‹ˆλ‹€. ν”„λ‘œμ νŠΈ `CLAUDE.md`에 harness 블둝이 있으면 κ·Έ 파일만 보고 νŒμ •ν–ˆκΈ° λ•Œλ¬Έμ—, `@` importκ°€ κΈ€λ‘œλ²Œ `~/.claude/CLAUDE.md`에 μžˆλŠ” ν”ν•œ μ‘°ν•©μ—μ„œλŠ” 룰이 정상 λ‘œλ“œλ˜λŠ”λ°λ„ κ²½κ³ κ°€ λ–΄μŠ΅λ‹ˆλ‹€. Claude CodeλŠ” 두 νŒŒμΌμ„ λͺ¨λ‘ λ‘œλ“œν•˜λ―€λ‘œ 이제 μ–‘μͺ½μ˜ importλ₯Ό ν•©μ³μ„œ νŒμ •ν•˜κ³ , μ–΄λŠ μͺ½μ΄ λ“€κ³  μžˆλŠ”μ§€λŠ” `importSource`(`project`/`global`/`both`)둜 μ•Œλ €μ€λ‹ˆλ‹€. 두 파일 λ‹€ importκ°€ 없을 λ•Œλ§Œ κ²½κ³ ν•©λ‹ˆλ‹€.
205
208
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-token-saver",
3
- "version": "3.6.4",
3
+ "version": "3.7.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": {
@@ -13,4 +13,4 @@
13
13
  - μžλ™ν™” 도ꡬ가 grepΒ·μ •κ·œμ‹μœΌλ‘œ μ°ΎλŠ” 마컀 ν…μŠ€νŠΈ(ν—€λ”Β·μ•΅μ»€Β·ν‚€μ›Œλ“œ 라인)λŠ” λ¦¬νŒ©ν„°λ§Β·μ••μΆ•Β·μˆ˜μ • κΈˆμ§€ β€” μ •ν™• λ§€μΉ­ κ²€μΆœμ€ ν•œ κΈ€μžλ§Œ λ°”λ€Œμ–΄λ„ 깨짐
14
14
  - μžλ™ν™” μ„€κ³„μ—μ„œ vision/screenshot 루프가 보이면 β†’ 호좜 κ°€λŠ₯ν•œ ν•¨μˆ˜Β·APIΒ·MCP toolλΆ€ν„° μ°ΎλŠ”λ‹€. ν†΅μ œ κ°€λŠ₯ν•œ λ‚΄λΆ€ λ„κ΅¬λŠ” 무쑰건 API, ν†΅μ œ λΆˆκ°€ μ™ΈλΆ€ SaaS만 vision μ°¨μ„ μ±…. μΈν„°νŽ˜μ΄μŠ€λ₯Ό λ°”κΎΌ 뒀에 λͺ¨λΈμ„ λ‚΄λ¦°λ‹€ β€” 거꾸둜 κ°€λ©΄ 토큰 폭발
15
15
  - μž…λ ₯ 데이터(ground-truth) λ³€κ²½ μ‹œ 그에 μ˜μ‘΄ν•˜λŠ” λͺ¨λ“  νŒŒμƒ μžμ‚°μ„ λκΉŒμ§€ μž¬μƒμ„±ν•œλ‹€ β€” 쀑간 단계λ₯Ό μƒλž΅ν•˜λ©΄ stale μ‚°μΆœλ¬Όμ΄ μƒˆ μž…λ ₯ μœ„μ— μ–Ήν˜€ κ²°κ³Όκ°€ 깨짐
16
- - νƒμƒ‰Β·μ‘°νšŒΒ·μƒνƒœ 확인·λͺ…λ Ή μ‹€ν–‰ 같은 λ‹¨μˆœ μš”μ²­μ€ haiku μ„œλΈŒμ—μ΄μ „νŠΈ(haiku-exploreΒ·haiku-runner λ“±)둜 μœ„μž„ν•΄ μƒμœ„ λͺ¨λΈ 토큰을 μ•„λ‚€λ‹€
16
+ - νƒμƒ‰Β·μ‘°νšŒΒ·μƒνƒœ 확인·λͺ…λ Ή μ‹€ν–‰ 같은 λ‹¨μˆœ μš”μ²­μ€ `model: haiku` μ„œλΈŒμ—μ΄μ „νŠΈλ‘œ μœ„μž„ν•΄ μƒμœ„ λͺ¨λΈ 토큰을 μ•„λ‚€λ‹€ (읽기 μ „μš© preset μ—μ΄μ „νŠΈκ°€ 이미 있으면 κ·Έκ±Έ μ“°κ³ , μ—†μœΌλ©΄ ν‹°μ–΄λ§Œ λ‚΄λ €μ„œ μœ„μž„)
package/src/agents.js ADDED
@@ -0,0 +1,52 @@
1
+ /**
2
+ * How a delegation target is phrased in generated rules.
3
+ *
4
+ * Rules used to name concrete subagents (`haiku-explore`, `haiku-runner`, …).
5
+ * Those live in the user's own `~/.claude/agents/` and are NOT shipped by this
6
+ * package, so on any machine that never created them the generated rule told
7
+ * the model to delegate to an agent that does not exist. The model tier is the
8
+ * part that actually saves tokens and it is universal, so `model: haiku` is the
9
+ * default phrasing β€” the agent name is only added when the file is really
10
+ * there, which keeps the tool-restricted preset in play for users who have it.
11
+ */
12
+
13
+ import { existsSync } from 'node:fs';
14
+ import { join } from 'node:path';
15
+ import { claudeUserDir } from './paths.js';
16
+
17
+ /** Tier each preset agent is pinned to, for the `model:` half of the phrase. */
18
+ const AGENT_MODEL = {
19
+ 'haiku-explore': 'haiku',
20
+ 'haiku-runner': 'haiku',
21
+ 'haiku-translate': 'haiku',
22
+ 'sonnet-worker': 'sonnet',
23
+ };
24
+
25
+ /** True when `<name>.md` exists in the project or user agents directory. */
26
+ export function agentExists(name, root = process.cwd()) {
27
+ if (!name) return false;
28
+ return existsSync(join(root, '.claude', 'agents', `${name}.md`)) ||
29
+ existsSync(join(claudeUserDir(), 'agents', `${name}.md`));
30
+ }
31
+
32
+ /**
33
+ * Korean phrase for a delegation target, e.g.
34
+ * agent present β†’ `haiku-explore(model: haiku)`
35
+ * agent absent β†’ `model: haiku`
36
+ */
37
+ export function agentPhrase(name, { model, root } = {}) {
38
+ const tier = model || AGENT_MODEL[name] || 'haiku';
39
+ return agentExists(name, root) ? `${name}(model: ${tier})` : `model: ${tier}`;
40
+ }
41
+
42
+ /**
43
+ * English phrase, including the article, e.g.
44
+ * agent present β†’ `the haiku-explore (model: haiku) subagent`
45
+ * agent absent β†’ `a model: haiku subagent`
46
+ */
47
+ export function agentPhraseEn(name, { model, root } = {}) {
48
+ const tier = model || AGENT_MODEL[name] || 'haiku';
49
+ return agentExists(name, root)
50
+ ? `the ${name} (model: ${tier}) subagent`
51
+ : `a model: ${tier} subagent`;
52
+ }
@@ -33,6 +33,7 @@ import { join, dirname } from 'node:path';
33
33
  import { homedir } from 'node:os';
34
34
  import { userDataDir } from './paths.js';
35
35
  import { userLanguage } from './config.js';
36
+ import { agentPhrase, agentPhraseEn } from './agents.js';
36
37
 
37
38
  // Post-promotion delegated-category error rate above this flags the rule
38
39
  // for review (rule-health). Calibrated against local T0 avg error incidence.
@@ -144,10 +145,10 @@ export function renderModelRatchet(rules, lang = userLanguage()) {
144
145
  const t1 = group.find((r) => r.tier === 'T1');
145
146
  if (t2 && t1) {
146
147
  const rule = ko
147
- ? `"${t2.label}" μœ ν˜• μš”μ²­μ€ 기본적으둜 ${t2.agent}(haiku) μ„œλΈŒμ—μ΄μ „νŠΈλ‘œ μœ„μž„ν•œλ‹€(예: "${t2.example}"). ` +
148
+ ? `"${t2.label}" μœ ν˜• μš”μ²­μ€ 기본적으둜 ${agentPhrase(t2.agent)} μ„œλΈŒμ—μ΄μ „νŠΈλ‘œ μœ„μž„ν•œλ‹€(예: "${t2.example}"). ` +
148
149
  `μ—¬λŸ¬ λ‹¨κ³„Β·μ—¬λŸ¬ 파일 μˆ˜μ •μ΄ μ–½νžŒ 쀑간 λ‚œλ„ μš”μ²­(예: "${t1.example}")은 model: sonnet μ„œλΈŒμ—μ΄μ „νŠΈλ‘œ μœ„μž„ν•œλ‹€. ` +
149
150
  `섀계 νŒλ‹¨Β·λ°°ν¬Β·μŠ€ν† μ–΄ 제좜 같은 λΉ„κ°€μ—­ μž‘μ—…μ΄ μ„žμ΄κ±°λ‚˜ μœ„μž„ 쀑 μ—λŸ¬κ°€ 반볡되면 μœ„μž„ν•˜μ§€ 말고 메인 λͺ¨λΈμ΄ 직접 μ²˜λ¦¬ν•œλ‹€`
150
- : `Delegate "${t2.labelEn || t2.label}" requests to the ${t2.agent} (haiku) subagent by default (e.g. "${t2.example}"). ` +
151
+ : `Delegate "${t2.labelEn || t2.label}" requests to ${agentPhraseEn(t2.agent)} by default (e.g. "${t2.example}"). ` +
151
152
  `Escalate moderate ones that span multiple steps or file edits (e.g. "${t1.example}") to a model: sonnet subagent. ` +
152
153
  `Do not delegate at all β€” handle it on the main model β€” when the request mixes in design judgement or irreversible work (deploy, release, store submission), or when errors repeat during delegation`;
153
154
  lines.push(`- ${rule}${healthOf(t2)}${healthOf(t1)} <!-- T2 ${statsOf(t2)} / T1 ${statsOf(t1)} -->`);
package/src/route-scan.js CHANGED
@@ -22,6 +22,7 @@ import { join } from 'node:path';
22
22
  import { userDataDir } from './paths.js';
23
23
  import { discoverSessionFiles } from './parser.js';
24
24
  import { collectSessionRecords } from './session-records.js';
25
+ import { agentPhrase, agentPhraseEn } from './agents.js';
25
26
 
26
27
  // ── Tier bands (docs/TIER_CRITERIA.md Β§3) ────────────────────────────────
27
28
  // T2 (haiku): finished in few calls, tiny output, near-zero mutation, no
@@ -373,10 +374,10 @@ export async function runRouteScan({ days = 14 } = {}) {
373
374
  // switching `language` later re-renders (and promotes) correctly without
374
375
  // waiting for a rescan.
375
376
  const ruleText = (g) => g.tier === 'T2'
376
- ? `"${g.label}" μœ ν˜•μ˜ λ‹¨μˆœ μš”μ²­(예: "${g.example}")은 ${g.agent}(haiku) μ„œλΈŒμ—μ΄μ „νŠΈλ‘œ μœ„μž„ν•œλ‹€ (섀계 νŒλ‹¨Β·λ°°ν¬Β·μŠ€ν† μ–΄ 제좜 같은 λΉ„κ°€μ—­ μž‘μ—…μ΄ μ„žμ΄λ©΄ μœ„μž„ν•˜μ§€ μ•ŠμŒ)`
377
+ ? `"${g.label}" μœ ν˜•μ˜ λ‹¨μˆœ μš”μ²­(예: "${g.example}")은 ${agentPhrase(g.agent)} μ„œλΈŒμ—μ΄μ „νŠΈλ‘œ μœ„μž„ν•œλ‹€ (섀계 νŒλ‹¨Β·λ°°ν¬Β·μŠ€ν† μ–΄ 제좜 같은 λΉ„κ°€μ—­ μž‘μ—…μ΄ μ„žμ΄λ©΄ μœ„μž„ν•˜μ§€ μ•ŠμŒ)`
377
378
  : `"${g.label}" μœ ν˜•μ˜ 쀑간 λ‚œλ„ μš”μ²­(예: "${g.example}")은 model: sonnet μ„œλΈŒμ—μ΄μ „νŠΈλ‘œ μœ„μž„ν•œλ‹€ (섀계 νŒλ‹¨Β·λΉ„κ°€μ—­ μž‘μ—…Β·λ°˜λ³΅ μ—λŸ¬ λ°œμƒ μ‹œ 메인 λͺ¨λΈμ΄ μ΄μ–΄λ°›μŒ)`;
378
379
  const ruleTextEn = (g) => g.tier === 'T2'
379
- ? `Delegate simple "${g.labelEn}" requests (e.g. "${g.example}") to the ${g.agent} (haiku) subagent β€” never when the request mixes in design judgement or irreversible work like deploy/release/submission`
380
+ ? `Delegate simple "${g.labelEn}" requests (e.g. "${g.example}") to ${agentPhraseEn(g.agent)} β€” never when the request mixes in design judgement or irreversible work like deploy/release/submission`
380
381
  : `Delegate moderate "${g.labelEn}" requests (e.g. "${g.example}") to a model: sonnet subagent β€” hand back to the main model on design judgement, irreversible work, or repeated errors`;
381
382
 
382
383
  const candidates = [...groups.values()]