claude-token-saver 3.0.1 → 3.2.1
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 +34 -21
- package/README.md +34 -21
- package/bin/cli.js +108 -96
- package/package.json +1 -1
- package/src/harness-templates.js +4 -0
- package/src/harness.js +2 -1
- package/src/model-rules.js +174 -0
- package/src/route-scan.js +206 -58
- package/src/session-records.js +112 -0
- package/src/frugon-export.js +0 -211
package/README.en.md
CHANGED
|
@@ -82,7 +82,6 @@ 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
85
|
| `claude-token-saver route-scan` | Detect recurring easy work on expensive models → propose haiku-delegation ratchet rules (below) |
|
|
87
86
|
| `claude-token-saver install` | Manually register Skill + statusline |
|
|
88
87
|
|
|
@@ -138,37 +137,39 @@ The whole point of the ratchet is **one-direction accumulation**. Deleting rules
|
|
|
138
137
|
An auto `.bak` is kept, but **the session context that earned the rule its place is not recoverable.**
|
|
139
138
|
</details>
|
|
140
139
|
|
|
141
|
-
## 🔀 frugon integration — "which calls could a cheaper model handle?"
|
|
142
140
|
|
|
143
|
-
|
|
141
|
+
## 🔀 route-scan — "this recurring task could run on a cheaper tier"
|
|
144
142
|
|
|
145
|
-
|
|
146
|
-
claude-token-saver frugon # last 30 days → ./frugon-export.jsonl
|
|
147
|
-
claude-token-saver frugon --run # export + run frugon analyze immediately
|
|
148
|
-
claude-token-saver frugon --days 7 --project myproj --out logs.jsonl
|
|
149
|
-
```
|
|
150
|
-
|
|
151
|
-
- 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).
|
|
152
|
-
- **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.
|
|
153
|
-
- 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`.
|
|
154
|
-
- Install frugon with `pipx install frugon` (if models show as unpriced, run `frugon update`).
|
|
143
|
+
Analyzes your session history at the **episode (user request) level**, classifies the work your expensive model (opus/fable) keeps doing into **tiers**, and proposes delegation rules. Fully local, zero token cost. Criteria design and evidence: [docs/TIER_CRITERIA.md](./docs/TIER_CRITERIA.md).
|
|
155
144
|
|
|
156
|
-
|
|
145
|
+
- **T2 → haiku**: few calls, tiny output, near-zero mutation, no errors (lookups, pasted-screen Q&A, simple runs)
|
|
146
|
+
- **T1 → sonnet**: moderate output, few mutations, ≤1 error (build pipelines, status checks)
|
|
147
|
+
- **T0 stays**: repeated errors, heavy mutation, big output, design/analysis keywords — the session model keeps it
|
|
157
148
|
|
|
158
|
-
|
|
149
|
+
Signals: call count, output tokens, **mutating tool calls (Edit/Write/Bash)**, **tool errors**, and request text. Output thresholds are auto-calibrated from your own 14-day distribution (fixed constants drift with workload).
|
|
159
150
|
|
|
160
151
|
```bash
|
|
161
|
-
claude-token-saver route-scan # scan (24h cache) +
|
|
162
|
-
claude-token-saver harness promote R1 --project # promote candidate R1 to a
|
|
152
|
+
claude-token-saver route-scan # scan (24h cache) + tiered candidates
|
|
153
|
+
claude-token-saver harness promote R1 --project # promote candidate R1 to a model-fitting rule
|
|
163
154
|
claude-token-saver route-scan dismiss 1 # not interested — won't resurface
|
|
155
|
+
claude-token-saver route-scan rules # list model-fitting rules (rm <N> to remove)
|
|
164
156
|
```
|
|
165
157
|
|
|
158
|
+
### Model-fitting ratchet — a separate file, continuously refreshed
|
|
159
|
+
|
|
160
|
+
Promoted delegation rules never mix with hand-written ratchet rules: they live in a **separate, fully tool-owned file** — `.claude/ratchet-model.md` per project, `~/.claude/ratchet-model.md` for global scope — regenerated wholesale on every scan, and they stay alive afterward:
|
|
161
|
+
|
|
162
|
+
- **Auto-refresh**: every rescan recomputes recurrence counts and the category's error rate from fresh logs and rewrites the file. Your `ratchet.md` is never touched by stat churn, so repos that commit `.claude/` see no diff noise (`ratchet-model.md` is safe to gitignore — it's always regenerable from the registry).
|
|
163
|
+
- **rule-health**: when the delegated category's error rate exceeds 20%, the rule gets a `⚠ rule-health` flag suggesting you narrow or remove it — the "define difficulty by outcome" principle applied to rule lifecycle.
|
|
164
|
+
- Your rules are managed by `harness list/rm`; model-fitting rules by `route-scan rules [rm <N>]` — separate files, separate indexes.
|
|
165
|
+
- The CLAUDE.md ratchet section planted by `harness init` references both files, so Claude applies them together (existing users: re-run `harness init` to refresh the block).
|
|
166
|
+
|
|
166
167
|
How it works (session-boundary calibration, NOT a real-time router):
|
|
167
|
-
1. `install` registers a SessionStart hook that injects the cached scan results as session context on startup and `/clear
|
|
168
|
-
2. When a recurring (≥3×)
|
|
169
|
-
3. Promoted rules
|
|
168
|
+
1. `install` registers a SessionStart hook that injects the cached scan results as session context on startup and `/clear`. Rescans are **data-triggered, not time-triggered**: ~5MB of new transcripts since the last scan rescans immediately, a small trickle rescans daily, and no change means no rescan at all (an unchanged-input scan is deterministic). A 1-hour minimum-interval guard applies, and promoting a rule triggers one immediate refresh to establish its stat baseline.
|
|
169
|
+
2. When a recurring (≥3×) 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 make **the main model delegate that work type to a haiku/sonnet subagent automatically from the next session on**.
|
|
170
171
|
|
|
171
|
-
Recommended companion setup: create `model: haiku` subagents under `~/.claude/agents/` (e.g. haiku-explore / haiku-runner / haiku-translate) so the rules are immediately actionable.
|
|
172
|
+
Recommended companion setup: create `model: haiku` subagents under `~/.claude/agents/` (e.g. haiku-explore / haiku-runner / haiku-translate) plus a `model: sonnet` general worker so the rules are immediately actionable.
|
|
172
173
|
|
|
173
174
|
## Spike issue codes
|
|
174
175
|
|
|
@@ -250,6 +251,18 @@ Also update `statusLine.command` in `~/.claude/settings.json` to `claude-token-s
|
|
|
250
251
|
|
|
251
252
|
## Release notes
|
|
252
253
|
|
|
254
|
+
### v3.2.1 (2026-07-13)
|
|
255
|
+
- **Instant pattern analysis on first setup** — `install` (including npm postinstall) analyzes your existing session logs inline when no cache exists, so tier-delegation candidates appear from the very first Claude Code session (previously the second).
|
|
256
|
+
|
|
257
|
+
### v3.2.0 (2026-07-13)
|
|
258
|
+
- **Tier classification (T0/T1/T2)** — route-scan grows from a binary easy/other split into three tiers. New signals: mutating tool calls and tool errors; output thresholds auto-calibrate to the user's own distribution (clamped); a dedicated category for pasted screen/log Q&A; conversational episodes (<100 output tokens) excluded. Design and research evidence in `docs/TIER_CRITERIA.md`.
|
|
259
|
+
- **Model-fitting ratchet separated** — promoted delegation rules live in their own file (`.claude/ratchet-model.md` / `~/.claude/ratchet-model.md`), file-level-separated from hand-written rules, managed via `route-scan rules [rm <N>]`; the harness CLAUDE.md block references both files.
|
|
260
|
+
- **Log-driven auto-refresh + rule-health** — every rescan recomputes each registered rule's recurrence count and error rate (over delegation-shaped episodes) and rewrites the file. An error rate >20% flags the rule with `⚠ rule-health`, suggesting narrowing or removal.
|
|
261
|
+
- **Data-triggered rescans** — the fixed 24h TTL is gone; new transcript volume triggers rescans (~5MB → immediately, a trickle → daily, no change → skip, 1h minimum interval, one immediate refresh after promote).
|
|
262
|
+
|
|
263
|
+
### v3.1.0 (2026-07-13)
|
|
264
|
+
- **frugon integration removed** — the `claude-token-saver frugon` JSONL-export subcommand is gone. An external analyzer's aggregate report can't be turned into ratchet rules (condition → action), so it never fed the delegation pipeline; 3.x instead invests in **first-party tier classification over session logs**. route-scan is unaffected (the shared parser moved to `src/session-records.js`).
|
|
265
|
+
|
|
253
266
|
### v3.0.1 (2026-07-13)
|
|
254
267
|
- **`harness pull` redefined** — v3.0.0's "copy global ratchet → project" was pointless (the global ratchet already applies to every project as the upper layer of the hierarchy) and is removed. `pull` now registers the **author-curated ratchet rules** bundled with the package (`presets/ratchet-rules.md`) into your global ratchet — six general-purpose rules promoted from real recurring mistakes; opt-in and idempotent.
|
|
255
268
|
|
package/README.md
CHANGED
|
@@ -82,7 +82,6 @@ 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
85
|
| `claude-token-saver route-scan` | 상위 모델이 반복 처리한 easy 작업 감지 → haiku 위임 랫쳇 룰 제안 (아래 참고) |
|
|
87
86
|
| `claude-token-saver install` | Skill·statusline 수동 등록 |
|
|
88
87
|
|
|
@@ -119,37 +118,39 @@ ratchet의 가치는 **한 방향 누적**에 있습니다. 룰을 가볍게 지
|
|
|
119
118
|
삭제 시 `.bak`이 남지만 **그 룰이 박힌 세션 컨텍스트(왜)는 복원되지 않습니다.**
|
|
120
119
|
</details>
|
|
121
120
|
|
|
122
|
-
## 🔀 frugon 연계 — "어떤 호출을 싼 모델로 내릴 수 있나"
|
|
123
121
|
|
|
124
|
-
|
|
122
|
+
## 🔀 route-scan — "이 반복 작업, 더 싼 티어로 내려도 됩니다"
|
|
125
123
|
|
|
126
|
-
|
|
127
|
-
claude-token-saver frugon # 최근 30일 세션 → ./frugon-export.jsonl
|
|
128
|
-
claude-token-saver frugon --run # 내보내기 + frugon analyze 바로 실행
|
|
129
|
-
claude-token-saver frugon --days 7 --project myproj --out logs.jsonl
|
|
130
|
-
```
|
|
131
|
-
|
|
132
|
-
- `~/.claude/projects/`의 transcript를 frugon이 읽는 OpenAI 호환 JSONL로 변환합니다. **분석은 전부 로컬** — 로그도 키도 밖으로 나가지 않습니다 (frugon의 원칙과 동일).
|
|
133
|
-
- **캐시 가중 토큰(기본):** frugon은 프롬프트 캐싱을 모르기 때문에 물리 토큰을 그대로 주면 비용이 ~10배 과대평가됩니다. 기본값은 캐시 read 0.1x · 5m write 1.25x · 1h write 2x를 접어 넣은 유효 토큰이라 frugon의 달러 견적이 실제 청구액과 일치합니다. 물리 토큰이 필요하면 `--raw-tokens`.
|
|
134
|
-
- frugon의 easy/hard 분류가 쓰는 신호(프롬프트·응답 토큰, 대화 깊이)와 `--measure` 품질 검증에 쓰는 마지막 유저 프롬프트·응답 텍스트를 보존합니다. 텍스트를 빼고 싶으면 `--no-content`.
|
|
135
|
-
- frugon 설치: `pipx install frugon` (모델이 unpriced로 나오면 `frugon update`).
|
|
124
|
+
세션 기록을 **에피소드(사용자 요청) 단위**로 분석해 상위 모델(opus/fable)이 반복 처리해 온 작업을 **티어로 분류**하고, 위임 룰로 승격하도록 제안합니다. 전 과정 로컬, 토큰 비용 0. 기준 설계와 근거는 [docs/TIER_CRITERIA.md](./docs/TIER_CRITERIA.md) 참고.
|
|
136
125
|
|
|
137
|
-
|
|
126
|
+
- **T2 → haiku**: 적은 호출·작은 출력·변경 거의 없음·에러 0으로 끝난 요청 (탐색·조회, 붙여넣은 화면 질문, 단순 실행 등)
|
|
127
|
+
- **T1 → sonnet**: 중간 출력·변경 소수·에러 ≤1 (빌드 파이프라인, 상태 점검 등)
|
|
128
|
+
- **T0 유지**: 에러 반복, 대량 변경, 큰 출력, 설계·분석 키워드 — 세션 모델이 계속 담당
|
|
138
129
|
|
|
139
|
-
|
|
130
|
+
판정 신호는 호출 수·출력 토큰·**변경성 도구 수(Edit/Write/Bash)**·**도구 에러 수**·요청 텍스트이고, 출력 임계값은 사용자 자신의 최근 14일 분포에서 자동 보정됩니다 (고정 상수는 워크로드가 바뀌면 어긋나기 때문).
|
|
140
131
|
|
|
141
132
|
```bash
|
|
142
|
-
claude-token-saver route-scan # 스캔 (24h 캐시) + 후보 출력
|
|
143
|
-
claude-token-saver harness promote R1 --project # 후보 R1을
|
|
133
|
+
claude-token-saver route-scan # 스캔 (24h 캐시) + 티어별 후보 출력
|
|
134
|
+
claude-token-saver harness promote R1 --project # 후보 R1을 모델 피팅 룰로 등록
|
|
144
135
|
claude-token-saver route-scan dismiss 1 # 관심 없으면 무시 (재스캔에도 안 뜸)
|
|
136
|
+
claude-token-saver route-scan rules # 등록된 모델 피팅 룰 목록 (rm <N>으로 제거)
|
|
145
137
|
```
|
|
146
138
|
|
|
139
|
+
### 모델 피팅 랫쳇 — 사용자 룰과 파일부터 분리, 로그 기반 자동 갱신
|
|
140
|
+
|
|
141
|
+
승격된 위임 룰은 손으로 쓴 랫쳇 룰과 섞이지 않도록 **별도 파일**에 저장됩니다 — 프로젝트는 `.claude/ratchet-model.md`, 글로벌은 `~/.claude/ratchet-model.md`. 이 파일은 전적으로 도구 소유라 매 스캔마다 통째로 재생성되며, 이후에도 살아 움직입니다:
|
|
142
|
+
|
|
143
|
+
- **자동 갱신**: 매 스캔마다 반복 횟수·해당 유형의 에러율을 최신 로그로 다시 계산해 파일을 재작성합니다. 통계가 바뀌어도 사용자의 `ratchet.md`는 전혀 건드리지 않으므로, `.claude/`를 커밋하는 프로젝트에서도 diff 소음이 없습니다 (`ratchet-model.md`는 gitignore해도 무방 — 레지스트리에서 항상 재생성 가능).
|
|
144
|
+
- **rule-health**: 위임 대상 유형의 에러율이 20%를 넘으면 룰에 `⚠ rule-health` 경고가 붙어 조건을 좁히거나 제거하라고 알립니다 — "결과(outcome)로 난이도를 정의"하는 원칙을 룰 수명 관리에 재적용한 것.
|
|
145
|
+
- 사용자 룰은 `harness list/rm`, 모델 피팅 룰은 `route-scan rules [rm <N>]`로 각각 관리 — 서로의 파일도 인덱스도 침범하지 않습니다.
|
|
146
|
+
- `harness init`이 심는 CLAUDE.md 랫쳇 섹션이 두 파일을 모두 참조하므로 Claude가 세션에서 함께 적용합니다 (기존 사용자는 `harness init` 재실행으로 블록 갱신).
|
|
147
|
+
|
|
147
148
|
동작 구조 (실시간 라우팅이 아니라 **세션 경계 캘리브레이션**):
|
|
148
|
-
1. `install` 시 SessionStart 훅이 등록되어, 새 세션 시작·`/clear` 때 캐시된 스캔 결과를 세션 컨텍스트로
|
|
149
|
-
2. 반복(≥3회)
|
|
150
|
-
3. 등록된 룰은
|
|
149
|
+
1. `install` 시 SessionStart 훅이 등록되어, 새 세션 시작·`/clear` 때 캐시된 스캔 결과를 세션 컨텍스트로 주입합니다. 재스캔은 시간이 아니라 **데이터가 트리거**: 마지막 스캔 이후 새 transcript가 ~5MB 이상 쌓이면 즉시, 소량이면 하루 1회, 아무 변화가 없으면 아예 돌지 않습니다 (변화 없는 재스캔은 결과가 동일하므로). 최소 간격 1시간 가드 포함, 룰 등록(promote) 직후에는 통계 기준선 확보를 위해 즉시 1회.
|
|
150
|
+
2. 반복(≥3회) 패턴이 있으면 statusline에 `🅷⚠ route? R1` 칩이 뜨고, Claude가 등록 여부와 scope(`--project`/`--global`)를 물어봅니다.
|
|
151
|
+
3. 등록된 룰은 **다음 세션부터 메인 모델이 해당 유형을 haiku/sonnet 서브에이전트로 자동 위임**하게 합니다.
|
|
151
152
|
|
|
152
|
-
권장 사전 준비: `~/.claude/agents/`에 `model: haiku` 서브에이전트(예: haiku-explore / haiku-runner / haiku-translate)
|
|
153
|
+
권장 사전 준비: `~/.claude/agents/`에 `model: haiku` 서브에이전트(예: haiku-explore / haiku-runner / haiku-translate)와 `model: sonnet` 범용 서브에이전트를 만들어 두면 룰이 바로 실행 가능해집니다.
|
|
153
154
|
|
|
154
155
|
## 토큰 급증 원인 코드
|
|
155
156
|
|
|
@@ -207,6 +208,18 @@ npm uninstall -g claude-cache-monitor && npm i -g claude-token-saver
|
|
|
207
208
|
|
|
208
209
|
## 릴리스 노트
|
|
209
210
|
|
|
211
|
+
### v3.2.1 (2026-07-13)
|
|
212
|
+
- **최초 설치 시 즉시 패턴 분석** — `install`(npm postinstall 포함)이 캐시가 없으면 기존 세션 로그를 그 자리에서 분석해, 첫 Claude Code 세션부터 티어 위임 후보가 표시됩니다 (기존에는 두 번째 세션부터).
|
|
213
|
+
|
|
214
|
+
### v3.2.0 (2026-07-13)
|
|
215
|
+
- **티어 분류 (T0/T1/T2)** — route-scan이 이분법(easy/그외)에서 3티어로 진화. 신호에 변경성 도구 수·도구 에러 수 추가, 출력 임계값은 사용자 분포 기반 자동 보정(클램프 포함), 붙여넣은 화면·로그 질문 전용 카테고리 신설, 대화성 응답(출력 <100토큰) 제외. 기준 설계·리서치 근거는 `docs/TIER_CRITERIA.md`.
|
|
216
|
+
- **모델 피팅 랫쳇 분리** — 승격된 위임 룰은 별도 파일(`.claude/ratchet-model.md` / `~/.claude/ratchet-model.md`)에 저장돼 사용자 룰과 파일 단위로 분리. `route-scan rules [rm <N>]`로 관리하며, 하네스 CLAUDE.md 블록이 두 파일을 함께 참조.
|
|
217
|
+
- **로그 기반 자동 갱신 + rule-health** — 매 스캔마다 등록 룰의 반복 횟수·에러율(위임 적격 모양의 에피소드 기준)을 재계산해 파일을 재작성. 에러율 >20%면 `⚠ rule-health` 플래그로 조건 좁히기/제거를 제안.
|
|
218
|
+
- **데이터 트리거 재스캔** — 고정 24h TTL을 폐기하고 신규 transcript 양이 재스캔을 트리거 (~5MB 즉시 / 소량 일 1회 / 무변화 스킵 / 최소 간격 1h / promote 직후 즉시 1회).
|
|
219
|
+
|
|
220
|
+
### v3.1.0 (2026-07-13)
|
|
221
|
+
- **frugon 연계 제거** — `claude-token-saver frugon` 서브커맨드(JSONL 내보내기)를 삭제했습니다. 외부 분석기의 집계 리포트는 랫쳇 룰(조건→행동)로 변환할 수 없어 위임 파이프라인에 기여하지 못했고, 3.x의 방향은 **세션 로그 기반 티어 분류를 자체적으로 탄탄히** 가져가는 것입니다. route-scan은 영향 없이 그대로 동작합니다 (공용 파서는 `src/session-records.js`로 분리).
|
|
222
|
+
|
|
210
223
|
### v3.0.1 (2026-07-13)
|
|
211
224
|
- **`harness pull` 재정의** — v3.0.0의 "글로벌 랫쳇 → 프로젝트 복사"는 글로벌 랫쳇이 이미 프로젝트의 상위 계층으로 항상 적용되므로 무의미해 제거. `pull`은 이제 패키지에 동봉된 **제작자 큐레이션 랫쳇 룰**(`presets/ratchet-rules.md`)을 사용자의 글로벌 랫쳇에 등록합니다 — 실제 반복 사고에서 승격된 범용 룰 6종, opt-in·멱등.
|
|
212
225
|
|
package/bin/cli.js
CHANGED
|
@@ -10,8 +10,6 @@
|
|
|
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 frugon # export transcripts → frugon JSONL (model-routing analysis)
|
|
14
|
-
* npx claude-token-saver frugon --run # export + run `frugon analyze`
|
|
15
13
|
* npx claude-token-saver --install-hook # install PostToolUse hook
|
|
16
14
|
* npx claude-token-saver --uninstall-hook # remove hook
|
|
17
15
|
* npx claude-token-saver --hook-run # internal: called by hook
|
|
@@ -414,6 +412,25 @@ async function main() {
|
|
|
414
412
|
print('legacy /token-monitor', r.legacy);
|
|
415
413
|
console.log(' (consolidated into the skill — same workflow, triggered by intent)');
|
|
416
414
|
}
|
|
415
|
+
// First-time setup: analyze existing session logs right away so the
|
|
416
|
+
// very first session already sees delegation candidates — without this,
|
|
417
|
+
// the initial scan would only start from the first session's hook and
|
|
418
|
+
// its results would surface one session late. Runs inline (a few
|
|
419
|
+
// seconds on a typical 14-day history): a detached child can be reaped
|
|
420
|
+
// by sandboxed installers before it finishes, and postinstall carries
|
|
421
|
+
// `|| true` so a failure here never breaks the install.
|
|
422
|
+
{
|
|
423
|
+
const rs = await import('../src/route-scan.js');
|
|
424
|
+
if (!rs.readRouteScan()) {
|
|
425
|
+
try {
|
|
426
|
+
console.log('');
|
|
427
|
+
console.log(' route-scan: 기존 세션 로그의 사용 패턴을 분석하는 중...');
|
|
428
|
+
const cache = await rs.runRouteScan({ days: 14 });
|
|
429
|
+
console.log(` route-scan: 에피소드 ${cache.totalEpisodes}건 분석 완료 — 위임 후보 ${cache.candidates.length}건.`);
|
|
430
|
+
console.log(' (다음 Claude Code 세션에서 티어 위임 후보가 표시됩니다)');
|
|
431
|
+
} catch { /* hook-triggered scan covers it on first session instead */ }
|
|
432
|
+
}
|
|
433
|
+
}
|
|
417
434
|
console.log('');
|
|
418
435
|
console.log('Open Claude Code in any directory and just mention:');
|
|
419
436
|
console.log(' "cache hit rate" / "1M context" / "5H cap" — the skill auto-activates.');
|
|
@@ -480,6 +497,37 @@ async function main() {
|
|
|
480
497
|
const { userLanguage } = await import('../src/config.js');
|
|
481
498
|
const lang = userLanguage();
|
|
482
499
|
|
|
500
|
+
// route-scan rules [rm <N>] — the model-fitting rule registry (rules
|
|
501
|
+
// promoted from candidates; auto-refreshed from logs on every rescan).
|
|
502
|
+
if (args[1] === 'rules') {
|
|
503
|
+
const mr = await import('../src/model-rules.js');
|
|
504
|
+
if (args[2] === 'rm') {
|
|
505
|
+
const n = parseInt(args[3], 10);
|
|
506
|
+
const removed = Number.isFinite(n) ? mr.removeModelRule(n) : null;
|
|
507
|
+
if (!removed) {
|
|
508
|
+
console.error('Usage: claude-token-saver route-scan rules rm <N> # N from `route-scan rules`');
|
|
509
|
+
process.exit(1);
|
|
510
|
+
}
|
|
511
|
+
// A target whose last rule was removed gets its (tool-owned) file deleted.
|
|
512
|
+
mr.syncAllFiles({ previousPaths: [mr.modelRatchetPathFor(removed.scope, removed.targetRoot)] });
|
|
513
|
+
console.log(`Removed model-fitting rule #${n}: ${removed.rule}`);
|
|
514
|
+
return;
|
|
515
|
+
}
|
|
516
|
+
const { rules } = mr.loadModelRules();
|
|
517
|
+
if (rules.length === 0) {
|
|
518
|
+
console.log(lang === 'ko' ? '등록된 모델 피팅 룰 없음.' : 'No model-fitting rules registered.');
|
|
519
|
+
return;
|
|
520
|
+
}
|
|
521
|
+
console.log(lang === 'ko' ? '📐 모델 피팅 룰 (로그 기반 자동 갱신):' : '📐 Model-fitting rules (auto-refreshed from logs):');
|
|
522
|
+
rules.forEach((r, i) => {
|
|
523
|
+
const health = r.status === 'review' ? ' ⚠ rule-health' : '';
|
|
524
|
+
console.log(` #${i + 1} [${r.tier}|${r.scope}] ×${r.count || 0} err ${Math.round((r.errRate || 0) * 100)}%${health}`);
|
|
525
|
+
console.log(` ${r.rule}`);
|
|
526
|
+
});
|
|
527
|
+
console.log('\n제거: claude-token-saver route-scan rules rm <N>');
|
|
528
|
+
return;
|
|
529
|
+
}
|
|
530
|
+
|
|
483
531
|
if (args[1] === 'dismiss') {
|
|
484
532
|
const n = parseInt(args[2], 10);
|
|
485
533
|
if (!Number.isFinite(n)) {
|
|
@@ -502,7 +550,7 @@ async function main() {
|
|
|
502
550
|
// prints delegation-candidate context for the new session.
|
|
503
551
|
if (hasFlag('--hook')) {
|
|
504
552
|
let cache = rs.readRouteScan();
|
|
505
|
-
if (
|
|
553
|
+
if (await rs.shouldRescan(cache)) {
|
|
506
554
|
try {
|
|
507
555
|
const { spawn } = await import('node:child_process');
|
|
508
556
|
spawn(process.execPath, [process.argv[1], 'route-scan', '--refresh', '--quiet'],
|
|
@@ -512,9 +560,10 @@ async function main() {
|
|
|
512
560
|
const open = rs.openCandidates(cache);
|
|
513
561
|
if (open.length === 0) return; // silent — nothing to inject
|
|
514
562
|
const lines = [];
|
|
515
|
-
lines.push(`[claude-token-saver route-scan] 최근 ${cache.days}일 세션에서 상위 모델(opus/fable)이 처리한 반복
|
|
563
|
+
lines.push(`[claude-token-saver route-scan] 최근 ${cache.days}일 세션에서 상위 모델(opus/fable)이 처리한 위임 가능 반복 작업이 감지되었습니다:`);
|
|
516
564
|
for (const c of open) {
|
|
517
|
-
|
|
565
|
+
const tierNote = c.tier === 'T1' ? 'sonnet 위임(중간 난도)' : `${c.agent} 위임(경량)`;
|
|
566
|
+
lines.push(` R${c.id} [${c.tier || 'T2'}] (×${c.count}, ${c.project}): ${c.label} → ${tierNote} 권장 (scope 제안: ${c.suggestedScope})`);
|
|
518
567
|
lines.push(` 예시: "${c.example}"`);
|
|
519
568
|
}
|
|
520
569
|
lines.push('이 패턴을 랫쳇 룰로 등록하면 다음 세션부터 자동 위임됩니다. 적절한 시점에 사용자에게 등록 여부와 scope를 물어본 뒤 실행하세요:');
|
|
@@ -526,7 +575,7 @@ async function main() {
|
|
|
526
575
|
|
|
527
576
|
const days = parseFloat(getArg('--days') || '14');
|
|
528
577
|
let cache = rs.readRouteScan();
|
|
529
|
-
if (hasFlag('--refresh') ||
|
|
578
|
+
if (hasFlag('--refresh') || (cache && cache.days !== days) || await rs.shouldRescan(cache, { days })) {
|
|
530
579
|
cache = await rs.runRouteScan({ days });
|
|
531
580
|
}
|
|
532
581
|
if (hasFlag('--quiet')) return;
|
|
@@ -547,7 +596,7 @@ async function main() {
|
|
|
547
596
|
}
|
|
548
597
|
console.log(lang === 'ko' ? '\n위임 후보:' : '\nDelegation candidates:');
|
|
549
598
|
for (const c of open) {
|
|
550
|
-
console.log(` R${c.id} ×${c.count} ${c.label} → ${c.agent} [${c.project}] (scope 제안: ${c.suggestedScope})`);
|
|
599
|
+
console.log(` R${c.id} [${c.tier || 'T2'}] ×${c.count} ${c.label} → ${c.agent} [${c.project}] (scope 제안: ${c.suggestedScope})`);
|
|
551
600
|
console.log(` 예시: "${c.example}"`);
|
|
552
601
|
console.log(` 룰: ${c.rule}`);
|
|
553
602
|
}
|
|
@@ -558,75 +607,6 @@ async function main() {
|
|
|
558
607
|
return;
|
|
559
608
|
}
|
|
560
609
|
|
|
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
|
-
|
|
630
610
|
// Subcommand: harness — manage the project's CLAUDE.md harness rules.
|
|
631
611
|
// claude-token-saver harness init # write CLAUDE.md (5 sections) + ratchet.md
|
|
632
612
|
// claude-token-saver harness uninit # remove harness block from CLAUDE.md (backup kept)
|
|
@@ -792,31 +772,63 @@ async function main() {
|
|
|
792
772
|
process.exit(1);
|
|
793
773
|
}
|
|
794
774
|
}
|
|
795
|
-
// Route candidates
|
|
796
|
-
//
|
|
797
|
-
//
|
|
798
|
-
//
|
|
799
|
-
|
|
800
|
-
if (routeCandidate && scope === 'project') {
|
|
775
|
+
// Route candidates become MODEL-FITTING rules: they live in a
|
|
776
|
+
// tool-managed block (separate from user-authored ratchet rules) and
|
|
777
|
+
// keep updating from subsequent logs — recurrence counts, error rates,
|
|
778
|
+
// rule-health — on every rescan.
|
|
779
|
+
if (routeCandidate) {
|
|
801
780
|
const rs = await import('../src/route-scan.js');
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
781
|
+
const mr = await import('../src/model-rules.js');
|
|
782
|
+
// A --project rule must land in THE project the pattern was detected
|
|
783
|
+
// in, not the cwd's. Old caches without projectPath: verify cwd match.
|
|
784
|
+
let targetRoot = null;
|
|
785
|
+
if (scope === 'project') {
|
|
786
|
+
if (routeCandidate.projectPath) {
|
|
787
|
+
targetRoot = findProjectRoot(routeCandidate.projectPath);
|
|
788
|
+
} else if (rs.mungeProjectPath(findProjectRoot()) === routeCandidate.project) {
|
|
789
|
+
targetRoot = findProjectRoot();
|
|
790
|
+
} else {
|
|
791
|
+
console.error(`Route candidate R${routeCandidateId} was detected in another project (${routeCandidate.project}),`);
|
|
792
|
+
console.error('but this cached scan predates project-path tracking.');
|
|
793
|
+
console.error('Re-scan to capture it, then promote again:');
|
|
794
|
+
console.error(' claude-token-saver route-scan --refresh');
|
|
795
|
+
process.exit(1);
|
|
796
|
+
}
|
|
810
797
|
}
|
|
798
|
+
const entry = mr.addModelRule({
|
|
799
|
+
signature: routeCandidate.signature,
|
|
800
|
+
tier: routeCandidate.tier || 'T2',
|
|
801
|
+
category: routeCandidate.category,
|
|
802
|
+
label: routeCandidate.label,
|
|
803
|
+
agent: routeCandidate.agent,
|
|
804
|
+
scope,
|
|
805
|
+
targetRoot,
|
|
806
|
+
project: routeCandidate.project,
|
|
807
|
+
rule: routeCandidate.rule,
|
|
808
|
+
example: routeCandidate.example,
|
|
809
|
+
count: routeCandidate.count,
|
|
810
|
+
promotedAt: new Date().toISOString().slice(0, 10),
|
|
811
|
+
lastSeen: new Date().toISOString().slice(0, 10),
|
|
812
|
+
});
|
|
813
|
+
const written = mr.syncAllFiles();
|
|
814
|
+
rs.resolveCandidate(routeCandidateId);
|
|
815
|
+
console.log(`Model-fitting rule registered [${scope}${targetRoot ? ` → ${targetRoot}` : ''}] (tier ${entry.tier}):`);
|
|
816
|
+
console.log(` - ${entry.rule}`);
|
|
817
|
+
for (const p of written) console.log(` ratchet-model.md updated: ${p}`);
|
|
818
|
+
console.log('(route candidate R' + routeCandidateId + ' resolved — 다음 세션부터 자동 위임, 이후 스캔마다 로그 기반 갱신됩니다)');
|
|
819
|
+
console.log('룰 목록/제거: claude-token-saver route-scan rules [rm <N>]');
|
|
820
|
+
// Event-triggered refresh: establish the new rule's stat baseline
|
|
821
|
+
// right away instead of waiting for the next data-gated rescan.
|
|
822
|
+
try {
|
|
823
|
+
const { spawn } = await import('node:child_process');
|
|
824
|
+
spawn(process.execPath, [process.argv[1], 'route-scan', '--refresh', '--quiet'],
|
|
825
|
+
{ detached: true, stdio: 'ignore' }).unref();
|
|
826
|
+
} catch { /* baseline arrives on the next gated rescan instead */ }
|
|
827
|
+
return;
|
|
811
828
|
}
|
|
812
|
-
const r = harnessPromote(rule,
|
|
829
|
+
const r = harnessPromote(rule, { scope });
|
|
813
830
|
console.log(`Appended to ${r.path} [${r.scope}]:`);
|
|
814
831
|
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
|
-
}
|
|
820
832
|
if (/^\d+$/.test(raw)) {
|
|
821
833
|
console.log('\n👉 ratchet.md를 열어 TODO 부분을 실제 룰로 다듬어주세요.');
|
|
822
834
|
}
|
package/package.json
CHANGED
package/src/harness-templates.js
CHANGED
|
@@ -37,6 +37,10 @@ ${HARNESS_SECTIONS[0].heading}
|
|
|
37
37
|
non-TTY라 CLI의 readline 프롬프트가 안 뜨므로, 호출자(LLM)가 직접 묻고
|
|
38
38
|
플래그를 명시해야 함. 묻지 않고 기본값으로 등록하지 말 것.
|
|
39
39
|
- 승인된 룰은 다음 세션부터 자동 적용.
|
|
40
|
+
- **모델 피팅 랫쳇**: \`.claude/ratchet-model.md\`(프로젝트)와
|
|
41
|
+
\`~/.claude/ratchet-model.md\`(글로벌)에 있는 티어 위임 룰도 ratchet.md와
|
|
42
|
+
동일하게 따를 것. 이 파일은 claude-token-saver가 로그 기반으로 자동
|
|
43
|
+
생성·갱신하므로 직접 수정하지 말 것 (관리: \`route-scan rules\`).
|
|
40
44
|
|
|
41
45
|
${HARNESS_SECTIONS[1].heading}
|
|
42
46
|
완료 보고("다 됐어요", "테스트 통과") 시 다음 중 1개 이상을 항상 첨부:
|
package/src/harness.js
CHANGED
|
@@ -344,7 +344,8 @@ export function harnessListRules({ root = findProjectRoot(), scope = 'project' }
|
|
|
344
344
|
for (let i = 0; i < lines.length; i++) {
|
|
345
345
|
const line = lines[i];
|
|
346
346
|
// A "rule line" starts with "- " (markdown bullet). Header lines, blanks,
|
|
347
|
-
// and the "## Rules" anchor are ignored.
|
|
347
|
+
// and the "## Rules" anchor are ignored. (Model-fitting rules live in a
|
|
348
|
+
// separate tool-owned file, ratchet-model.md — never listed here.)
|
|
348
349
|
if (/^\s*-\s+/.test(line)) {
|
|
349
350
|
rules.push({ index: rules.length + 1, lineNo: i, text: line.replace(/^\s*-\s+/, '') });
|
|
350
351
|
}
|