sprag-cli 3.46.0 → 3.46.2
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.ko.md +23 -31
- package/README.md +32 -185
- package/examples/statusline-command.ps1 +11 -5
- package/examples/statusline-command.sh +9 -4
- package/package.json +3 -4
- package/src/changelog.js +37 -1
- package/src/cli-name.js +25 -0
- package/src/commands/uninstall.js +5 -4
- package/src/formatters/table.js +5 -4
- package/src/hook-manager.js +3 -2
- package/src/installer.js +83 -16
- package/src/seed-rules.js +9 -8
- package/src/update-check.js +47 -7
package/README.ko.md
CHANGED
|
@@ -8,20 +8,18 @@
|
|
|
8
8
|
**AI 코딩 에이전트용 품질 래칫**<br>**같은 실수는 두 번 없습니다**
|
|
9
9
|
|
|
10
10
|
[](https://www.npmjs.com/package/sprag-cli)
|
|
11
|
-
[](https://www.npmjs.com/package/sprag-cli)
|
|
12
12
|
[](./LICENSE)
|
|
13
13
|
|
|
14
|
-
[
|
|
14
|
+
[English](https://github.com/rootstudioyaml/sprag/blob/main/README.md) · [한국어](https://github.com/rootstudioyaml/sprag/blob/main/README.ko.md)
|
|
15
15
|
|
|
16
|
-
|
|
16
|
+
[sprag.io](https://sprag.io) · [벤치마크](https://github.com/rootstudioyaml/sprag/blob/main/docs/BENCHMARK.md)
|
|
17
17
|
|
|
18
|
-
|
|
18
|
+
</div>
|
|
19
19
|
|
|
20
20
|
---
|
|
21
21
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
**AI 코딩 에이전트에 거는 품질 래칫입니다. 에이전트는 그대로 일하고, 퇴행만 막습니다.** 이름은 스프래그 클러치에서 왔습니다. 앞으로 도는 힘은 그대로 통과시키고, 역회전하는 순간 잠기는 부품입니다. Sprag는 Claude Code가 이미 남기는 세션 기록을 읽어 누적되는 이득으로 바꿉니다. 되풀이된 실패는 세션마다 올라오는 규칙이 되고, 저렴한 티어가 감당한다고 증명된 작업은 절감액 원장을 영수증 삼아 그쪽으로 위임되며, 캐시와 사용량 한도 문제는 아직 손쓸 수 있을 때 statusline에 나타납니다.
|
|
22
|
+
**에이전트는 그대로 일하고, 퇴행만 막습니다.** 이름은 스프래그 클러치에서 왔습니다. 앞으로 도는 힘은 그대로 통과시키고, 역회전하는 순간 잠기는 부품입니다. Sprag는 Claude Code가 이미 남기는 세션 기록을 읽어 누적되는 이득으로 바꿉니다. 되풀이된 실패는 세션마다 올라오는 규칙이 되고, 저렴한 티어가 감당한다고 증명된 작업은 절감액 원장을 영수증 삼아 그쪽으로 위임되며, 캐시와 사용량 한도 문제는 아직 손쓸 수 있을 때 statusline에 나타납니다.
|
|
25
23
|
|
|
26
24
|
의존성 0, API 키 불요, 데이터는 컴퓨터 밖으로 나가지 않습니다.
|
|
27
25
|
|
|
@@ -29,9 +27,9 @@
|
|
|
29
27
|
npm i -g sprag-cli # 예전 이름 claude-token-saver 와 같은 패키지입니다
|
|
30
28
|
```
|
|
31
29
|
|
|
32
|
-

|
|
33
31
|
|
|
34
|
-
##
|
|
32
|
+
## 측정 결과
|
|
35
33
|
|
|
36
34
|
| 지표 | 결과 | | 근거 |
|
|
37
35
|
|---|---|---|---|
|
|
@@ -40,25 +38,19 @@ npm i -g sprag-cli # 예전 이름 claude-token-saver 와 같은 패키지입
|
|
|
40
38
|
| 문서 토큰 | **−95.8%** · 540,429 → 22,610 | `▰▱▱▱▱▱▱▱▱▱▱▱` | [doc2md](https://github.com/rootstudioyaml/sprag/blob/main/docs/DOC2MD.md) |
|
|
41
39
|
| 메시지당 비용 | **−18.6%** · $2.345 → $1.910 | `▰▰▰▰▰▰▰▰▰▰▱▱` | [실측 리포트](#실제-효과-도입-전후-리포트) |
|
|
42
40
|
|
|
43
|
-
```text
|
|
44
|
-
정확도 총비용 (11,696문항)
|
|
45
|
-
티어 기준 라우터 59.1% ← 최고 $268 ▰▰▰▰▰▱▱▱▱▱▱▱▱▱▱
|
|
46
|
-
gpt-5 57.8% $388 ▰▰▰▰▰▰▰▰▱▱▱▱▱▱▱
|
|
47
|
-
gemini-2.5-pro 57.9% $734 ▰▰▰▰▰▰▰▰▰▰▰▰▰▰▰
|
|
48
|
-
```
|
|
49
41
|
|
|
50
|
-
라우팅 절감액은 추정치가 아니라 위임 한 건 한 건의 차액을 적은 원장입니다. `sprag route-scan savings` 가 모든 금액을 룰 단위까지 역추적해 보여 줍니다. 집계에서 무엇을 빼는지까지 [원장 문서](https://github.com/rootstudioyaml/sprag/blob/main/docs/COMMANDS.md)에 적어 두었습니다.
|
|
42
|
+
라우팅 절감액은 추정치가 아니라 위임 한 건 한 건의 차액을 적은 원장입니다. `sprag route-scan savings` 가 모든 금액을 룰 단위까지 역추적해 보여 줍니다. 집계에서 무엇을 빼는지까지 [원장 문서](https://github.com/rootstudioyaml/sprag/blob/main/docs/COMMANDS.ko.md)에 적어 두었습니다.
|
|
51
43
|
|
|
52
44
|
## 설치 한 번에 전부 들어 있고, 첫날부터 동작합니다
|
|
53
45
|
|
|
54
46
|
| | 하는 일 | 더 보기 |
|
|
55
47
|
|---|---|---|
|
|
56
|
-
| ⚙️ **래칫 규칙** | 되풀이된 실패가 한 줄짜리 규칙이 되어 매 세션에 올라옵니다. 후보는 기록에서 자동으로 찾아 줍니다. | [Harness](https://github.com/rootstudioyaml/sprag/blob/main/docs/HARNESS.md) |
|
|
48
|
+
| ⚙️ **래칫 규칙** | 되풀이된 실패가 한 줄짜리 규칙이 되어 매 세션에 올라옵니다. 후보는 기록에서 자동으로 찾아 줍니다. | [Harness](https://github.com/rootstudioyaml/sprag/blob/main/docs/HARNESS.ko.md) |
|
|
57
49
|
| 🔀 **모델 피팅** | 기록에서 뽑은 위임 규칙에 측정한 오류율과 절감액을 붙여 `ratchet-model.md`에 적습니다. | [route-scan](https://github.com/rootstudioyaml/sprag/blob/main/docs/ROUTE_SCAN.md) |
|
|
58
|
-
| 🅷 **하네스 점수** | 다섯 가지 운영 원칙을 실시간으로 점검합니다. 검증 단계를 건너뛰면 `🅷 4/5`가 먼저 알려 줍니다. | [Harness](https://github.com/rootstudioyaml/sprag/blob/main/docs/HARNESS.md) |
|
|
59
|
-
| 📊 **토큰 텔레메트리** | 캐시 적중률과 TTL, 컨텍스트 크기, 출력 급증, 두 가지 사용량 한도를 매 턴 표시합니다. | [statusline](https://github.com/rootstudioyaml/sprag/blob/main/docs/STATUSLINE.md) |
|
|
50
|
+
| 🅷 **하네스 점수** | 다섯 가지 운영 원칙을 실시간으로 점검합니다. 검증 단계를 건너뛰면 `🅷 4/5`가 먼저 알려 줍니다. | [Harness](https://github.com/rootstudioyaml/sprag/blob/main/docs/HARNESS.ko.md) |
|
|
51
|
+
| 📊 **토큰 텔레메트리** | 캐시 적중률과 TTL, 컨텍스트 크기, 출력 급증, 두 가지 사용량 한도를 매 턴 표시합니다. | [statusline](https://github.com/rootstudioyaml/sprag/blob/main/docs/STATUSLINE.ko.md) |
|
|
60
52
|
| 📄 **doc2md** | pptx와 xlsx, pdf, docx, fig를 필요할 때 Markdown 으로 바꿔 읽습니다. | [doc2md](https://github.com/rootstudioyaml/sprag/blob/main/docs/DOC2MD.md) |
|
|
61
|
-
| 🇰🇷 **문체 게이트** | 쓰는 시점에 hook으로 검사합니다. 이중 피동과 번역투, 문장 연결을 살핍니다. | [문체 지침](https://github.com/rootstudioyaml/sprag/blob/main/docs/KOREAN-STYLE.md) |
|
|
53
|
+
| 🇰🇷 **문체 게이트** | 쓰는 시점에 hook으로 검사합니다. 이중 피동과 번역투, 문장 연결을 살핍니다. | [문체 지침](https://github.com/rootstudioyaml/sprag/blob/main/docs/KOREAN-STYLE.ko.md) |
|
|
62
54
|
|
|
63
55
|
실측 −18.6%는 하네스와 래칫의 몫이고, 라우팅과 문서 변환 절감액은 그 위에 얹힙니다. 두 절감액은 성격이 달라서 한 숫자로 합치지 않습니다.
|
|
64
56
|
|
|
@@ -69,13 +61,13 @@ gemini-2.5-pro 57.9% $734 ▰▰▰▰▰▰▰▰▰▰▰
|
|
|
69
61
|
🚨 5H ▰▰▰▰▰▰▰▰▰▰▰▱ 94% 🔄 12:36 · 🅷 5/5 · 🤖 Opus 5 · 🧠 Cache hit 98.8% · ⏳ Cache expires 59:46 · 📅 weekly ▰▰▰▰▰▱▱▱▱▱▱▱ 38% · 💵 Sep $42 · 📦 Ctx 47% of 1M
|
|
70
62
|
```
|
|
71
63
|
|
|
72
|
-
문제가 생기면 경고 칩이 줄 맨 앞으로 옵니다. `🚨 5H/7D NN%`, `⚠ Ctx 500k+`, `⚠ Cache miss`, `⚠ Input spike`, `⚠ Output heavy`, `⚠ Call surge`, `⚠ Rebuild churn`, `⚠ 5m TTL` 여덟 가지이며, 칩 문구를 그대로 말하면 Skill이 원인 코드와 조치 순서를 꺼내 줍니다. 세그먼트별 의미와 색 기준은 [statusline 문서](https://github.com/rootstudioyaml/sprag/blob/main/docs/STATUSLINE.md)에 정리했습니다.
|
|
64
|
+
문제가 생기면 경고 칩이 줄 맨 앞으로 옵니다. `🚨 5H/7D NN%`, `⚠ Ctx 500k+`, `⚠ Cache miss`, `⚠ Input spike`, `⚠ Output heavy`, `⚠ Call surge`, `⚠ Rebuild churn`, `⚠ 5m TTL` 여덟 가지이며, 칩 문구를 그대로 말하면 Skill이 원인 코드와 조치 순서를 꺼내 줍니다. 세그먼트별 의미와 색 기준은 [statusline 문서](https://github.com/rootstudioyaml/sprag/blob/main/docs/STATUSLINE.ko.md)에 정리했습니다.
|
|
73
65
|
|
|
74
|
-
JetBrains IDE 내장 터미널에서는 이모지 대신 한 칸 글리프로 표시합니다(`◉ Cache hit 98.8% · ◧ Ctx 47% of 1M`). IDE 기본 폰트에 이모지 글리프가 없어서 그대로 두면 `Cache expires 4:545` 처럼 이전 프레임 문자가 남기 때문입니다. 자세한 사정과 `sprag mode narrow` · `mode icon-force` 사용법은 [statusline 문서의 라벨 모드](https://github.com/rootstudioyaml/sprag/blob/main/docs/STATUSLINE.md#라벨-모드)에 있습니다.
|
|
66
|
+
JetBrains IDE 내장 터미널에서는 이모지 대신 한 칸 글리프로 표시합니다(`◉ Cache hit 98.8% · ◧ Ctx 47% of 1M`). IDE 기본 폰트에 이모지 글리프가 없어서 그대로 두면 `Cache expires 4:545` 처럼 이전 프레임 문자가 남기 때문입니다. 자세한 사정과 `sprag mode narrow` · `mode icon-force` 사용법은 [statusline 문서의 라벨 모드](https://github.com/rootstudioyaml/sprag/blob/main/docs/STATUSLINE.ko.md#라벨-모드)에 있습니다.
|
|
75
67
|
|
|
76
68
|
## 실제 효과: 도입 전후 리포트
|
|
77
69
|
|
|
78
|
-

|
|
79
71
|
|
|
80
72
|
harness 5/5 + ratchet을 실제 적용한 전후 비교입니다 (저자 Claude Code 로그, **사용자 메시지 1건당** 정규화, 2026-05-02 기준, Opus 4.7 가격).
|
|
81
73
|
|
|
@@ -99,23 +91,23 @@ harness 5/5 + ratchet을 실제 적용한 전후 비교입니다 (저자 Claude
|
|
|
99
91
|
| `sprag harness check` | 🅷 하네스 점수 확인 |
|
|
100
92
|
| `sprag doc2md <파일>` | 문서 한 건을 Markdown 으로 변환 |
|
|
101
93
|
|
|
102
|
-
전체 명령과 옵션, 출력 언어 설정은 [명령 문서](https://github.com/rootstudioyaml/sprag/blob/main/docs/COMMANDS.md)에 있습니다.
|
|
94
|
+
전체 명령과 옵션, 출력 언어 설정은 [명령 문서](https://github.com/rootstudioyaml/sprag/blob/main/docs/COMMANDS.ko.md)에 있습니다.
|
|
103
95
|
|
|
104
96
|
## 더 깊은 내용
|
|
105
97
|
|
|
106
98
|
| 문서 | 다루는 내용 |
|
|
107
99
|
|---|---|
|
|
108
|
-
| [설치와 기본 설정](https://github.com/rootstudioyaml/sprag/blob/main/docs/INSTALL.md) | 설치 직후 켜지는 기능, 직접 켜야 하는 기능, 끄는 방법 |
|
|
109
|
-
| [statusline](https://github.com/rootstudioyaml/sprag/blob/main/docs/STATUSLINE.md) | 세그먼트 의미, 경고 칩, 토큰 급증 원인 코드, 업데이트 안내 |
|
|
110
|
-
| [명령](https://github.com/rootstudioyaml/sprag/blob/main/docs/COMMANDS.md) | 전체 서브커맨드와 CLI 옵션 |
|
|
111
|
-
| [Harness](https://github.com/rootstudioyaml/sprag/blob/main/docs/HARNESS.md) | 5원칙, 래칫 승격, compact-window 고정, seed 프리셋 |
|
|
100
|
+
| [설치와 기본 설정](https://github.com/rootstudioyaml/sprag/blob/main/docs/INSTALL.ko.md) | 설치 직후 켜지는 기능, 직접 켜야 하는 기능, 끄는 방법 |
|
|
101
|
+
| [statusline](https://github.com/rootstudioyaml/sprag/blob/main/docs/STATUSLINE.ko.md) | 세그먼트 의미, 경고 칩, 토큰 급증 원인 코드, 업데이트 안내 |
|
|
102
|
+
| [명령](https://github.com/rootstudioyaml/sprag/blob/main/docs/COMMANDS.ko.md) | 전체 서브커맨드와 CLI 옵션 |
|
|
103
|
+
| [Harness](https://github.com/rootstudioyaml/sprag/blob/main/docs/HARNESS.ko.md) | 5원칙, 래칫 승격, compact-window 고정, seed 프리셋 |
|
|
112
104
|
| [route-scan](https://github.com/rootstudioyaml/sprag/blob/main/docs/ROUTE_SCAN.md) | 티어 판정, 룰 파일 구조, 스캔 시점, 서브에이전트 설정 |
|
|
113
105
|
| [티어 판별 기준](https://github.com/rootstudioyaml/sprag/blob/main/docs/TIER_CRITERIA.md) | T0·T1·T2 를 가르는 근거와 연구 출처 |
|
|
114
106
|
| [벤치마크](https://github.com/rootstudioyaml/sprag/blob/main/docs/BENCHMARK.md) | LLMRouterBench 11,696문항 재현 결과 |
|
|
115
107
|
| [doc2md](https://github.com/rootstudioyaml/sprag/blob/main/docs/DOC2MD.md) | 변환 대상 포맷, 절감 근거, 문서 수정 절차, DRM 문서 |
|
|
116
|
-
| [한국어 문체 지침](https://github.com/rootstudioyaml/sprag/blob/main/docs/KOREAN-STYLE.md) | 주입 내용, 쓰기 시점 검사, 적용 전후 비교 |
|
|
117
|
-
| [라우터가 아닌 이유](https://github.com/rootstudioyaml/sprag/blob/main/docs/NOT-A-ROUTER.md) | 실시간 라우팅이 캐시를 깨서 비용을 키우는 구조 |
|
|
118
|
-
| [게이트웨이와 환경](https://github.com/rootstudioyaml/sprag/blob/main/docs/GATEWAYS.md) | Bedrock·Vertex·LiteLLM, 가격표, FAQ, 동작 원리 |
|
|
108
|
+
| [한국어 문체 지침](https://github.com/rootstudioyaml/sprag/blob/main/docs/KOREAN-STYLE.ko.md) | 주입 내용, 쓰기 시점 검사, 적용 전후 비교 |
|
|
109
|
+
| [라우터가 아닌 이유](https://github.com/rootstudioyaml/sprag/blob/main/docs/NOT-A-ROUTER.ko.md) | 실시간 라우팅이 캐시를 깨서 비용을 키우는 구조 |
|
|
110
|
+
| [게이트웨이와 환경](https://github.com/rootstudioyaml/sprag/blob/main/docs/GATEWAYS.ko.md) | Bedrock·Vertex·LiteLLM, 가격표, FAQ, 동작 원리 |
|
|
119
111
|
|
|
120
112
|
## 릴리스 노트
|
|
121
113
|
|
package/README.md
CHANGED
|
@@ -8,165 +8,18 @@
|
|
|
8
8
|
**A quality ratchet for AI coding agents. Mistakes never repeat.**
|
|
9
9
|
|
|
10
10
|
[](https://www.npmjs.com/package/sprag-cli)
|
|
11
|
-
[](https://www.npmjs.com/package/sprag-cli)
|
|
12
12
|
[](./LICENSE)
|
|
13
13
|
|
|
14
|
-
[
|
|
14
|
+
[English](https://github.com/rootstudioyaml/sprag/blob/main/README.md) · [한국어](https://github.com/rootstudioyaml/sprag/blob/main/README.ko.md)
|
|
15
15
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
---
|
|
19
|
-
|
|
20
|
-
<!-- ko:begin (generated from README.ko.md — edit that file, then run npm run sync:ko) -->
|
|
21
|
-
**🇰🇷 한국어 문서: [README.ko.md](https://github.com/rootstudioyaml/sprag/blob/main/README.ko.md)** (아래 토글로도 펼칠 수 있습니다.)
|
|
22
|
-
|
|
23
|
-
<details>
|
|
24
|
-
<summary><strong>🇰🇷 한국어로 보기 (클릭하면 한국어 전문이 펼쳐집니다)</strong></summary>
|
|
25
|
-
|
|
26
|
-
---
|
|
27
|
-
|
|
28
|
-
# Sprag
|
|
29
|
-
|
|
30
|
-
**AI 코딩 에이전트에 거는 품질 래칫입니다. 에이전트는 그대로 일하고, 퇴행만 막습니다.** 이름은 스프래그 클러치에서 왔습니다. 앞으로 도는 힘은 그대로 통과시키고, 역회전하는 순간 잠기는 부품입니다. Sprag는 Claude Code가 이미 남기는 세션 기록을 읽어 누적되는 이득으로 바꿉니다. 되풀이된 실패는 세션마다 올라오는 규칙이 되고, 저렴한 티어가 감당한다고 증명된 작업은 절감액 원장을 영수증 삼아 그쪽으로 위임되며, 캐시와 사용량 한도 문제는 아직 손쓸 수 있을 때 statusline에 나타납니다.
|
|
31
|
-
|
|
32
|
-
의존성 0, API 키 불요, 데이터는 컴퓨터 밖으로 나가지 않습니다.
|
|
33
|
-
|
|
34
|
-
```bash
|
|
35
|
-
npm i -g sprag-cli # 예전 이름 claude-token-saver 와 같은 패키지입니다
|
|
36
|
-
```
|
|
37
|
-
|
|
38
|
-

|
|
39
|
-
|
|
40
|
-
## 숫자 네 개
|
|
41
|
-
|
|
42
|
-
| 지표 | 결과 | | 근거 |
|
|
43
|
-
|---|---|---|---|
|
|
44
|
-
| 공개 벤치마크 정확도 | **59.1%** · 단일 최고 모델 57.9% | `▰▰▰▰▰▰▰▰▰▰▰▰` | [벤치마크](https://github.com/rootstudioyaml/sprag/blob/main/docs/BENCHMARK.md) |
|
|
45
|
-
| 같은 문항 총비용 | **$268** · gpt-5 $388 · gemini-2.5-pro $734 | `▰▰▰▰▰▱▱▱▱▱▱▱` | [벤치마크](https://github.com/rootstudioyaml/sprag/blob/main/docs/BENCHMARK.md) |
|
|
46
|
-
| 문서 토큰 | **−95.8%** · 540,429 → 22,610 | `▰▱▱▱▱▱▱▱▱▱▱▱` | [doc2md](https://github.com/rootstudioyaml/sprag/blob/main/docs/DOC2MD.md) |
|
|
47
|
-
| 메시지당 비용 | **−18.6%** · $2.345 → $1.910 | `▰▰▰▰▰▰▰▰▰▰▱▱` | [실측 리포트](#실제-효과-도입-전후-리포트) |
|
|
48
|
-
|
|
49
|
-
```text
|
|
50
|
-
정확도 총비용 (11,696문항)
|
|
51
|
-
티어 기준 라우터 59.1% ← 최고 $268 ▰▰▰▰▰▱▱▱▱▱▱▱▱▱▱
|
|
52
|
-
gpt-5 57.8% $388 ▰▰▰▰▰▰▰▰▱▱▱▱▱▱▱
|
|
53
|
-
gemini-2.5-pro 57.9% $734 ▰▰▰▰▰▰▰▰▰▰▰▰▰▰▰
|
|
54
|
-
```
|
|
55
|
-
|
|
56
|
-
라우팅 절감액은 추정치가 아니라 위임 한 건 한 건의 차액을 적은 원장입니다. `sprag route-scan savings` 가 모든 금액을 룰 단위까지 역추적해 보여 줍니다. 집계에서 무엇을 빼는지까지 [원장 문서](https://github.com/rootstudioyaml/sprag/blob/main/docs/COMMANDS.md)에 적어 두었습니다.
|
|
57
|
-
|
|
58
|
-
## 설치 한 번에 전부 들어 있고, 첫날부터 동작합니다
|
|
59
|
-
|
|
60
|
-
| | 하는 일 | 더 보기 |
|
|
61
|
-
|---|---|---|
|
|
62
|
-
| ⚙️ **래칫 규칙** | 되풀이된 실패가 한 줄짜리 규칙이 되어 매 세션에 올라옵니다. 후보는 기록에서 자동으로 찾아 줍니다. | [Harness](https://github.com/rootstudioyaml/sprag/blob/main/docs/HARNESS.md) |
|
|
63
|
-
| 🔀 **모델 피팅** | 기록에서 뽑은 위임 규칙에 측정한 오류율과 절감액을 붙여 `ratchet-model.md`에 적습니다. | [route-scan](https://github.com/rootstudioyaml/sprag/blob/main/docs/ROUTE_SCAN.md) |
|
|
64
|
-
| 🅷 **하네스 점수** | 다섯 가지 운영 원칙을 실시간으로 점검합니다. 검증 단계를 건너뛰면 `🅷 4/5`가 먼저 알려 줍니다. | [Harness](https://github.com/rootstudioyaml/sprag/blob/main/docs/HARNESS.md) |
|
|
65
|
-
| 📊 **토큰 텔레메트리** | 캐시 적중률과 TTL, 컨텍스트 크기, 출력 급증, 두 가지 사용량 한도를 매 턴 표시합니다. | [statusline](https://github.com/rootstudioyaml/sprag/blob/main/docs/STATUSLINE.md) |
|
|
66
|
-
| 📄 **doc2md** | pptx와 xlsx, pdf, docx, fig를 필요할 때 Markdown 으로 바꿔 읽습니다. | [doc2md](https://github.com/rootstudioyaml/sprag/blob/main/docs/DOC2MD.md) |
|
|
67
|
-
| 🇰🇷 **문체 게이트** | 쓰는 시점에 hook으로 검사합니다. 이중 피동과 번역투, 문장 연결을 살핍니다. | [문체 지침](https://github.com/rootstudioyaml/sprag/blob/main/docs/KOREAN-STYLE.md) |
|
|
68
|
-
|
|
69
|
-
실측 −18.6%는 하네스와 래칫의 몫이고, 라우팅과 문서 변환 절감액은 그 위에 얹힙니다. 두 절감액은 성격이 달라서 한 숫자로 합치지 않습니다.
|
|
70
|
-
|
|
71
|
-
## statusline 한 줄로 보는 상태
|
|
72
|
-
|
|
73
|
-
```
|
|
74
|
-
🔀 라우팅 절감 $2.09 | fable→sonnet 1회 $0.72 · opus→haiku 1회 $0.57
|
|
75
|
-
🚨 5H ▰▰▰▰▰▰▰▰▰▰▰▱ 94% 🔄 12:36 · 🅷 5/5 · 🤖 Opus 5 · 🧠 Cache hit 98.8% · ⏳ Cache expires 59:46 · 📅 weekly ▰▰▰▰▰▱▱▱▱▱▱▱ 38% · 💵 Sep $42 · 📦 Ctx 47% of 1M
|
|
76
|
-
```
|
|
77
|
-
|
|
78
|
-
문제가 생기면 경고 칩이 줄 맨 앞으로 옵니다. `🚨 5H/7D NN%`, `⚠ Ctx 500k+`, `⚠ Cache miss`, `⚠ Input spike`, `⚠ Output heavy`, `⚠ Call surge`, `⚠ Rebuild churn`, `⚠ 5m TTL` 여덟 가지이며, 칩 문구를 그대로 말하면 Skill이 원인 코드와 조치 순서를 꺼내 줍니다. 세그먼트별 의미와 색 기준은 [statusline 문서](https://github.com/rootstudioyaml/sprag/blob/main/docs/STATUSLINE.md)에 정리했습니다.
|
|
79
|
-
|
|
80
|
-
JetBrains IDE 내장 터미널에서는 이모지 대신 한 칸 글리프로 표시합니다(`◉ Cache hit 98.8% · ◧ Ctx 47% of 1M`). IDE 기본 폰트에 이모지 글리프가 없어서 그대로 두면 `Cache expires 4:545` 처럼 이전 프레임 문자가 남기 때문입니다. 자세한 사정과 `sprag mode narrow` · `mode icon-force` 사용법은 [statusline 문서의 라벨 모드](https://github.com/rootstudioyaml/sprag/blob/main/docs/STATUSLINE.md#라벨-모드)에 있습니다.
|
|
81
|
-
|
|
82
|
-
## 실제 효과: 도입 전후 리포트
|
|
83
|
-
|
|
84
|
-

|
|
85
|
-
|
|
86
|
-
harness 5/5 + ratchet을 실제 적용한 전후 비교입니다 (저자 Claude Code 로그, **사용자 메시지 1건당** 정규화, 2026-05-02 기준, Opus 4.7 가격).
|
|
16
|
+
[sprag.io](https://sprag.io) · [Benchmark](https://github.com/rootstudioyaml/sprag/blob/main/docs/BENCHMARK.md)
|
|
87
17
|
|
|
88
|
-
|
|
89
|
-
|---|---:|---:|---:|
|
|
90
|
-
| 메시지당 비용 | $2.345 | $1.910 | **−18.6%** |
|
|
91
|
-
| 메시지당 출력 토큰 | 7,391 | 6,052 | −18.1% |
|
|
92
|
-
| 메시지당 assistant 왕복 | 9.73 | 8.83 | −9.2% |
|
|
93
|
-
| 메시지당 도구 호출 | 5.72 | 5.25 | −8.2% |
|
|
94
|
-
|
|
95
|
-
같은 요청을 더 적은 왕복으로 끝내므로 첫 시도 적중률이 오릅니다. PEV와 Structured Task가 한 번에 가게 만든 효과로 보입니다. 표본이 2일치(157msg)라 통계적 의미는 약하고, 주별 작업 토픽 차이도 섞여 있습니다.
|
|
96
|
-
|
|
97
|
-
## 자주 쓰는 명령
|
|
98
|
-
|
|
99
|
-
| 명령 | 하는 일 |
|
|
100
|
-
|---|---|
|
|
101
|
-
| `sprag` | 최근 1일 진단 리포트 |
|
|
102
|
-
| `sprag handoff` | 한도에 막히기 전에 작업 상태를 백업 |
|
|
103
|
-
| `sprag route-scan` | 내 기록에서 위임 후보 찾기 (LLM 호출 0회) |
|
|
104
|
-
| `sprag route-scan savings` | 라우팅 절감 원장 조회 |
|
|
105
|
-
| `sprag harness check` | 🅷 하네스 점수 확인 |
|
|
106
|
-
| `sprag doc2md <파일>` | 문서 한 건을 Markdown 으로 변환 |
|
|
107
|
-
|
|
108
|
-
전체 명령과 옵션, 출력 언어 설정은 [명령 문서](https://github.com/rootstudioyaml/sprag/blob/main/docs/COMMANDS.md)에 있습니다.
|
|
109
|
-
|
|
110
|
-
## 더 깊은 내용
|
|
111
|
-
|
|
112
|
-
| 문서 | 다루는 내용 |
|
|
113
|
-
|---|---|
|
|
114
|
-
| [설치와 기본 설정](https://github.com/rootstudioyaml/sprag/blob/main/docs/INSTALL.md) | 설치 직후 켜지는 기능, 직접 켜야 하는 기능, 끄는 방법 |
|
|
115
|
-
| [statusline](https://github.com/rootstudioyaml/sprag/blob/main/docs/STATUSLINE.md) | 세그먼트 의미, 경고 칩, 토큰 급증 원인 코드, 업데이트 안내 |
|
|
116
|
-
| [명령](https://github.com/rootstudioyaml/sprag/blob/main/docs/COMMANDS.md) | 전체 서브커맨드와 CLI 옵션 |
|
|
117
|
-
| [Harness](https://github.com/rootstudioyaml/sprag/blob/main/docs/HARNESS.md) | 5원칙, 래칫 승격, compact-window 고정, seed 프리셋 |
|
|
118
|
-
| [route-scan](https://github.com/rootstudioyaml/sprag/blob/main/docs/ROUTE_SCAN.md) | 티어 판정, 룰 파일 구조, 스캔 시점, 서브에이전트 설정 |
|
|
119
|
-
| [티어 판별 기준](https://github.com/rootstudioyaml/sprag/blob/main/docs/TIER_CRITERIA.md) | T0·T1·T2 를 가르는 근거와 연구 출처 |
|
|
120
|
-
| [벤치마크](https://github.com/rootstudioyaml/sprag/blob/main/docs/BENCHMARK.md) | LLMRouterBench 11,696문항 재현 결과 |
|
|
121
|
-
| [doc2md](https://github.com/rootstudioyaml/sprag/blob/main/docs/DOC2MD.md) | 변환 대상 포맷, 절감 근거, 문서 수정 절차, DRM 문서 |
|
|
122
|
-
| [한국어 문체 지침](https://github.com/rootstudioyaml/sprag/blob/main/docs/KOREAN-STYLE.md) | 주입 내용, 쓰기 시점 검사, 적용 전후 비교 |
|
|
123
|
-
| [라우터가 아닌 이유](https://github.com/rootstudioyaml/sprag/blob/main/docs/NOT-A-ROUTER.md) | 실시간 라우팅이 캐시를 깨서 비용을 키우는 구조 |
|
|
124
|
-
| [게이트웨이와 환경](https://github.com/rootstudioyaml/sprag/blob/main/docs/GATEWAYS.md) | Bedrock·Vertex·LiteLLM, 가격표, FAQ, 동작 원리 |
|
|
125
|
-
|
|
126
|
-
## 릴리스 노트
|
|
127
|
-
|
|
128
|
-
전체 내역은 [CHANGELOG.md](./CHANGELOG.md)로 옮겼습니다. 최근 변경은 다음과 같습니다.
|
|
129
|
-
|
|
130
|
-
- **v3.39.0**: `feedback` 서브커맨드가 터미널이나 Claude 세션에서 버그 제보·기능 제안을 바로 제출합니다. gh CLI가 있으면 이슈 직접 등록, 없으면 로그인 없는 익명 제출(릴레이가 이슈로 자동 등록), 오프라인이면 로컬 저장. `install`은 기존 statusline이 있으면 조용히 건너뛰지 않고 교체 여부를 묻습니다.
|
|
131
|
-
- **v3.38.0**: `cohesion on`이 응집성 다섯 원칙을 영어 블록으로 주입합니다. 옵트인이고, korean 지침이 켜져 있으면 중복 주입을 막습니다.
|
|
132
|
-
- **v3.37.0**: 한국어 지침에 보강 지침(supplement)이 붙습니다. 번역투·상투 패턴·응집성(문장 이어짐) 조항이며, 쓰기 시점 검사에도 번역투 5종이 추가됐습니다. 실파일 255개 실측에서 오탐 1건으로 검증했습니다.
|
|
133
|
-
- **v3.35.0**: 이번 달 1일 00시 이후 지출 추정치를 `💵 Sep $42` 세그먼트로 상시 표시합니다. LiteLLM 게이트웨이 사용자는 키의 max_budget/spend 를 `🔑 budget` 게이지로 봅니다 (5h/7d cap 이 없는 Bedrock·LiteLLM 환경 대응).
|
|
134
|
-
- **v3.34.0**: seed 프리셋 제안, 설치 시 출력 언어 선택, 컨텍스트 경고 500k 상향. 상세는 CHANGELOG 참고.
|
|
135
|
-
|
|
136
|
-
## 피드백
|
|
137
|
-
|
|
138
|
-
버그 제보와 기능 제안은 GitHub 이슈로 받습니다: https://github.com/rootstudioyaml/sprag/issues
|
|
139
|
-
|
|
140
|
-
브라우저나 GitHub 로그인이 어려운 환경(사내망, 세션 도중)이라면 터미널에서 바로 제출할 수 있고, Claude에게 대신 제출해 달라고 요청해도 됩니다.
|
|
141
|
-
|
|
142
|
-
```bash
|
|
143
|
-
sprag feedback "Bedrock에서 5m TTL 칩이 사라지지 않아요"
|
|
144
|
-
```
|
|
145
|
-
|
|
146
|
-
`gh` CLI가 인증되어 있으면 GitHub 이슈로 바로 등록하고, 없으면 로그인이 필요 없는 익명 경로로 제출합니다(github.com이 막힌 사내망에서도 동작합니다). `gh` 경로를 건너뛰려면 `--anonymous`를 붙입니다. 도구 버전과 OS 정보는 자동으로 첨부됩니다.
|
|
147
|
-
|
|
148
|
-
버그를 제보하실 때는 도구 버전(`sprag --version`)과 OS를 함께 적어 주시고, statusline이나 경고 관련 문제라면 statusline 출력이나 `sprag last` 결과를 덧붙여 주시면 원인을 빨리 찾을 수 있습니다.
|
|
149
|
-
|
|
150
|
-
## 라이선스
|
|
151
|
-
|
|
152
|
-
MIT
|
|
18
|
+
</div>
|
|
153
19
|
|
|
154
20
|
---
|
|
155
21
|
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
[](https://www.youtube.com/@DeepPulseKR)
|
|
159
|
-
[](https://www.youtube.com/@DeepPulseEN)
|
|
160
|
-
[](https://rootstudioyaml.github.io/)
|
|
161
|
-
|
|
162
|
-
AI 개발 도구를 다루는 채널 **DeepPulse**에서 만들고 씁니다. 이 도구의 배경과 사용법은 [출시 영상(60초)](https://www.youtube.com/shorts/RaD8qMsPTnA)에서 볼 수 있습니다.
|
|
163
|
-
|
|
164
|
-
</details>
|
|
165
|
-
<!-- ko:end -->
|
|
166
|
-
|
|
167
|
-
# Sprag
|
|
168
|
-
|
|
169
|
-
**A quality ratchet for AI coding agents. Your agent keeps working, it just stops regressing.** Sprag is named after the sprag clutch: forward motion passes freely, backspin locks. It reads the sessions Claude Code already writes and turns them into gains that compound. Every repeated failure becomes a rule loaded at session start, work a cheaper tier has proven it can do is delegated there with a per-run savings ledger as the receipt, and cache or rate-limit trouble reaches your statusline while you can still act on it.
|
|
22
|
+
**Your agent keeps working, it just stops regressing.** Sprag is named after the sprag clutch: forward motion passes freely, backspin locks. It reads the sessions Claude Code already writes and turns them into gains that compound. Every repeated failure becomes a rule loaded at session start, work a cheaper tier has proven it can do is delegated there with a per-run savings ledger as the receipt, and cache or rate-limit trouble reaches your statusline while you can still act on it.
|
|
170
23
|
|
|
171
24
|
Zero dependencies, no API key, nothing leaves your machine.
|
|
172
25
|
|
|
@@ -174,36 +27,30 @@ Zero dependencies, no API key, nothing leaves your machine.
|
|
|
174
27
|
npm i -g sprag-cli # same package as the old claude-token-saver name
|
|
175
28
|
```
|
|
176
29
|
|
|
177
|
-

|
|
178
31
|
|
|
179
|
-
##
|
|
32
|
+
## Measured results
|
|
180
33
|
|
|
181
34
|
| Metric | Result | | Evidence |
|
|
182
35
|
|---|---|---|---|
|
|
183
|
-
| Public benchmark accuracy | **59.1%** vs 57.9% best single model | `▰▰▰▰▰▰▰▰▰▰▰▰` | [benchmark](
|
|
184
|
-
| Cost for the same queries | **$268** vs gpt-5 $388, gemini-2.5-pro $734 | `▰▰▰▰▰▱▱▱▱▱▱▱` | [benchmark](
|
|
185
|
-
| Tokens per document | **−95.8%**, 540,429 → 22,610 | `▰▱▱▱▱▱▱▱▱▱▱▱` | [doc2md](
|
|
36
|
+
| Public benchmark accuracy | **59.1%** vs 57.9% best single model | `▰▰▰▰▰▰▰▰▰▰▰▰` | [benchmark](https://github.com/rootstudioyaml/sprag/blob/main/docs/BENCHMARK.md) |
|
|
37
|
+
| Cost for the same queries | **$268** vs gpt-5 $388, gemini-2.5-pro $734 | `▰▰▰▰▰▱▱▱▱▱▱▱` | [benchmark](https://github.com/rootstudioyaml/sprag/blob/main/docs/BENCHMARK.md) |
|
|
38
|
+
| Tokens per document | **−95.8%**, 540,429 → 22,610 | `▰▱▱▱▱▱▱▱▱▱▱▱` | [doc2md](https://github.com/rootstudioyaml/sprag/blob/main/docs/DOC2MD.md) |
|
|
186
39
|
| Cost per user message | **−18.6%**, $2.345 → $1.910 | `▰▰▰▰▰▰▰▰▰▰▱▱` | [report](#real-world-impact--beforeafter-report) |
|
|
187
40
|
|
|
188
|
-
```text
|
|
189
|
-
accuracy total cost, 11,696 queries
|
|
190
|
-
tier-criteria router 59.1% ← best $268 ▰▰▰▰▰▱▱▱▱▱▱▱▱▱▱
|
|
191
|
-
gpt-5 57.8% $388 ▰▰▰▰▰▰▰▰▱▱▱▱▱▱▱
|
|
192
|
-
gemini-2.5-pro 57.9% $734 ▰▰▰▰▰▰▰▰▰▰▰▰▰▰▰
|
|
193
|
-
```
|
|
194
41
|
|
|
195
|
-
Routing savings are a ledger, not an estimate: the price difference of each delegated run, traceable back to the rule that caused it with `sprag route-scan savings`. What the ledger deliberately excludes is documented in [the command reference](
|
|
42
|
+
Routing savings are a ledger, not an estimate: the price difference of each delegated run, traceable back to the rule that caused it with `sprag route-scan savings`. What the ledger deliberately excludes is documented in [the command reference](https://github.com/rootstudioyaml/sprag/blob/main/docs/COMMANDS.md).
|
|
196
43
|
|
|
197
44
|
## Everything ships in one install, working from day one
|
|
198
45
|
|
|
199
46
|
| | What it does | More |
|
|
200
47
|
|---|---|---|
|
|
201
|
-
| ⚙️ **Ratchet rules** | Repeated failures become one-line rules loaded every session; candidates are detected from your logs. | [harness](
|
|
202
|
-
| 🔀 **Model fitting** | Log-driven delegation rules with measured error rates and savings, written to `ratchet-model.md`. | [route-scan](
|
|
203
|
-
| 🅷 **Harness score** | Five operating principles checked live. Skip the verify step and `🅷 4/5` says so before you report done. | [harness](
|
|
204
|
-
| 📊 **Token telemetry** | Cache hit rate, TTL, context size, output spikes and both rate-limit windows, every turn. | [statusline](
|
|
205
|
-
| 📄 **doc2md** | pptx, xlsx, pdf, docx and fig converted on demand instead of pasted into context. | [doc2md](
|
|
206
|
-
| 🇰🇷 **Style gates** | Write-time prose lint enforced by hook: double passives, translationese, cohesion. | [style](
|
|
48
|
+
| ⚙️ **Ratchet rules** | Repeated failures become one-line rules loaded every session; candidates are detected from your logs. | [harness](https://github.com/rootstudioyaml/sprag/blob/main/docs/HARNESS.md) |
|
|
49
|
+
| 🔀 **Model fitting** | Log-driven delegation rules with measured error rates and savings, written to `ratchet-model.md`. | [route-scan](https://github.com/rootstudioyaml/sprag/blob/main/docs/ROUTE_SCAN.md) |
|
|
50
|
+
| 🅷 **Harness score** | Five operating principles checked live. Skip the verify step and `🅷 4/5` says so before you report done. | [harness](https://github.com/rootstudioyaml/sprag/blob/main/docs/HARNESS.md) |
|
|
51
|
+
| 📊 **Token telemetry** | Cache hit rate, TTL, context size, output spikes and both rate-limit windows, every turn. | [statusline](https://github.com/rootstudioyaml/sprag/blob/main/docs/STATUSLINE.md) |
|
|
52
|
+
| 📄 **doc2md** | pptx, xlsx, pdf, docx and fig converted on demand instead of pasted into context. | [doc2md](https://github.com/rootstudioyaml/sprag/blob/main/docs/DOC2MD.md) |
|
|
53
|
+
| 🇰🇷 **Style gates** | Write-time prose lint enforced by hook: double passives, translationese, cohesion. | [style](https://github.com/rootstudioyaml/sprag/blob/main/docs/KOREAN-STYLE.md) |
|
|
207
54
|
|
|
208
55
|
The measured −18.6% comes from the harness and ratchet; routing and conversion savings sit on top of it. The two savings figures are never added together, because they measure different things.
|
|
209
56
|
|
|
@@ -214,13 +61,13 @@ The measured −18.6% comes from the harness and ratchet; routing and conversion
|
|
|
214
61
|
🚨 5H ▰▰▰▰▰▰▰▰▰▰▰▱ 94% 🔄 12:36 · 🅷 5/5 · 🤖 Opus 5 · 🧠 Cache hit 98.8% · ⏳ Cache expires 59:46 · 📅 weekly ▰▰▰▰▰▱▱▱▱▱▱▱ 38% · 💵 Sep $42 · 📦 Ctx 47% of 1M
|
|
215
62
|
```
|
|
216
63
|
|
|
217
|
-
When something is wrong the warning chip leads the line: `🚨 5H/7D NN%`, `⚠ Ctx 500k+`, `⚠ Cache miss`, `⚠ Input spike`, `⚠ Output heavy`, `⚠ Call surge`, `⚠ Rebuild churn`, `⚠ 5m TTL`. Say the chip wording inside Claude and the Skill surfaces the root-cause code and the fix. Segment-by-segment meanings live in [the statusline reference](
|
|
64
|
+
When something is wrong the warning chip leads the line: `🚨 5H/7D NN%`, `⚠ Ctx 500k+`, `⚠ Cache miss`, `⚠ Input spike`, `⚠ Output heavy`, `⚠ Call surge`, `⚠ Rebuild churn`, `⚠ 5m TTL`. Say the chip wording inside Claude and the Skill surfaces the root-cause code and the fix. Segment-by-segment meanings live in [the statusline reference](https://github.com/rootstudioyaml/sprag/blob/main/docs/STATUSLINE.md).
|
|
218
65
|
|
|
219
|
-
Inside a JetBrains IDE terminal the chips render as single-cell glyphs instead of emoji (`◉ Cache hit 98.8% · ◧ Ctx 47% of 1M`). The IDE's default font has no emoji glyphs, so leaving them in leaves debris from the previous frame behind — `Cache expires 4:545` and the like. [Label modes](
|
|
66
|
+
Inside a JetBrains IDE terminal the chips render as single-cell glyphs instead of emoji (`◉ Cache hit 98.8% · ◧ Ctx 47% of 1M`). The IDE's default font has no emoji glyphs, so leaving them in leaves debris from the previous frame behind — `Cache expires 4:545` and the like. [Label modes](https://github.com/rootstudioyaml/sprag/blob/main/docs/STATUSLINE.md#label-modes) covers the detail, plus `sprag mode narrow` and `mode icon-force`.
|
|
220
67
|
|
|
221
68
|
## Real-world impact — before/after report
|
|
222
69
|
|
|
223
|
-

|
|
224
71
|
|
|
225
72
|
Harness 5/5 + ratchet applied to the author's own Claude Code work, normalized **per user message** (cutoff 2026-05-02, Opus 4.7 pricing):
|
|
226
73
|
|
|
@@ -244,23 +91,23 @@ The same request resolves in fewer round-trips, so first-try success goes up: th
|
|
|
244
91
|
| `sprag harness check` | Current 🅷 harness score |
|
|
245
92
|
| `sprag doc2md <file>` | Convert one document to Markdown |
|
|
246
93
|
|
|
247
|
-
Every subcommand, flag and the output-language setting: [command reference](
|
|
94
|
+
Every subcommand, flag and the output-language setting: [command reference](https://github.com/rootstudioyaml/sprag/blob/main/docs/COMMANDS.md).
|
|
248
95
|
|
|
249
96
|
## Deeper reading
|
|
250
97
|
|
|
251
98
|
| Doc | Covers |
|
|
252
99
|
|---|---|
|
|
253
|
-
| [Install](
|
|
254
|
-
| [Statusline](
|
|
255
|
-
| [Commands](
|
|
256
|
-
| [Harness](
|
|
257
|
-
| [route-scan](
|
|
258
|
-
| [Tier criteria](
|
|
259
|
-
| [Benchmark](
|
|
260
|
-
| [doc2md](
|
|
261
|
-
| [Korean style](
|
|
262
|
-
| [Not a router](
|
|
263
|
-
| [Gateways & environment](
|
|
100
|
+
| [Install](https://github.com/rootstudioyaml/sprag/blob/main/docs/INSTALL.md) | What the install turns on, what stays manual, how to turn each part off |
|
|
101
|
+
| [Statusline](https://github.com/rootstudioyaml/sprag/blob/main/docs/STATUSLINE.md) | Segments, warning chips, spike issue codes, update notifications |
|
|
102
|
+
| [Commands](https://github.com/rootstudioyaml/sprag/blob/main/docs/COMMANDS.md) | Full CLI surface |
|
|
103
|
+
| [Harness](https://github.com/rootstudioyaml/sprag/blob/main/docs/HARNESS.md) | The five principles, rule promotion, compact-window pinning, seed presets |
|
|
104
|
+
| [route-scan](https://github.com/rootstudioyaml/sprag/blob/main/docs/ROUTE_SCAN.md) | Tier verdicts, rule-file mechanics, scan triggers, subagent setup |
|
|
105
|
+
| [Tier criteria](https://github.com/rootstudioyaml/sprag/blob/main/docs/TIER_CRITERIA.md) | Why T0/T1/T2 split where they do, with sources (Korean) |
|
|
106
|
+
| [Benchmark](https://github.com/rootstudioyaml/sprag/blob/main/docs/BENCHMARK.md) | The 11,696-instance LLMRouterBench run |
|
|
107
|
+
| [doc2md](https://github.com/rootstudioyaml/sprag/blob/main/docs/DOC2MD.md) | Supported formats, savings evidence, editing documents, DRM cases |
|
|
108
|
+
| [Korean style](https://github.com/rootstudioyaml/sprag/blob/main/docs/KOREAN-STYLE.md) | What gets injected, the write-time check, before/after |
|
|
109
|
+
| [Not a router](https://github.com/rootstudioyaml/sprag/blob/main/docs/NOT-A-ROUTER.md) | Why realtime routing breaks the cache and costs more |
|
|
110
|
+
| [Gateways & environment](https://github.com/rootstudioyaml/sprag/blob/main/docs/GATEWAYS.md) | Bedrock, Vertex, LiteLLM budgets, pricing table, FAQ, how it works |
|
|
264
111
|
|
|
265
112
|
## Release notes
|
|
266
113
|
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
# Claude Code statusline (Windows PowerShell version)
|
|
2
2
|
# Mirrors the POSIX sh script — prints "user@host:cwd" then appends
|
|
3
|
-
#
|
|
3
|
+
# sprag output as a second segment.
|
|
4
4
|
#
|
|
5
5
|
# Install:
|
|
6
|
-
# 1) npm install -g
|
|
6
|
+
# 1) npm install -g sprag-cli
|
|
7
7
|
# 2) Save this file as: %USERPROFILE%\.claude\statusline-command.ps1
|
|
8
8
|
# 3) In %USERPROFILE%\.claude\settings.json add:
|
|
9
9
|
# {
|
|
@@ -30,14 +30,20 @@ Write-Host -NoNewline "$esc[01;32m$env:USERNAME@$env:COMPUTERNAME$esc[00m`:$esc[
|
|
|
30
30
|
# 2) cache monitor (appended). Separator " | ". Falls back silently.
|
|
31
31
|
Write-Host -NoNewline " $esc[90m|$esc[00m "
|
|
32
32
|
|
|
33
|
-
|
|
33
|
+
# `claude-token-saver` is the older binary name, still installed by the current
|
|
34
|
+
# package; the npx fallback names `sprag-cli`, since the old package name is
|
|
35
|
+
# deprecated on npm and would fetch a version that stopped receiving releases.
|
|
36
|
+
$cacheMonitor = Get-Command sprag -ErrorAction SilentlyContinue
|
|
37
|
+
if (-not $cacheMonitor) {
|
|
38
|
+
$cacheMonitor = Get-Command claude-token-saver -ErrorAction SilentlyContinue
|
|
39
|
+
}
|
|
34
40
|
if ($cacheMonitor) {
|
|
35
41
|
try {
|
|
36
|
-
&
|
|
42
|
+
& $cacheMonitor.Name --statusline --icon 2>$null
|
|
37
43
|
} catch { }
|
|
38
44
|
} else {
|
|
39
45
|
# fallback: npx (first run downloads the package; subsequent runs are warm)
|
|
40
46
|
try {
|
|
41
|
-
& npx --yes
|
|
47
|
+
& npx --yes sprag-cli@latest --statusline --icon 2>$null
|
|
42
48
|
} catch { }
|
|
43
49
|
}
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
#!/bin/sh
|
|
2
2
|
# Claude Code statusline (POSIX sh — works on macOS, Linux, and WSL)
|
|
3
|
-
# Prints "user@host:cwd" then appends
|
|
3
|
+
# Prints "user@host:cwd" then appends sprag as a second segment.
|
|
4
4
|
#
|
|
5
5
|
# Install:
|
|
6
|
-
# 1) npm install -g
|
|
6
|
+
# 1) npm install -g sprag-cli
|
|
7
7
|
# 2) Save this file as: ~/.claude/statusline-command.sh
|
|
8
8
|
# chmod +x ~/.claude/statusline-command.sh (optional)
|
|
9
9
|
# 3) In ~/.claude/settings.json:
|
|
@@ -29,8 +29,13 @@ printf '\033[01;32m%s@%s\033[00m:\033[01;34m%s\033[00m' "$(whoami)" "$(hostname
|
|
|
29
29
|
|
|
30
30
|
# 2) cache monitor (appended). Separator " | ". Falls back silently.
|
|
31
31
|
printf ' \033[90m|\033[00m '
|
|
32
|
-
|
|
32
|
+
# `claude-token-saver` is the older binary name, still installed by the current
|
|
33
|
+
# package; the npx fallback names `sprag-cli`, since the old package name is
|
|
34
|
+
# deprecated on npm and would fetch a version that stopped receiving releases.
|
|
35
|
+
if command -v sprag >/dev/null 2>&1; then
|
|
36
|
+
sprag --statusline --icon 2>/dev/null || true
|
|
37
|
+
elif command -v claude-token-saver >/dev/null 2>&1; then
|
|
33
38
|
claude-token-saver --statusline --icon 2>/dev/null || true
|
|
34
39
|
else
|
|
35
|
-
npx --yes
|
|
40
|
+
npx --yes sprag-cli@latest --statusline --icon 2>/dev/null || true
|
|
36
41
|
fi
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "sprag-cli",
|
|
3
|
-
"version": "3.46.
|
|
3
|
+
"version": "3.46.2",
|
|
4
4
|
"description": "Sprag - a quality ratchet harness for AI coding agents: model-fitting delegation, ratchet rules, doc2md, Korean style gate. Same tool as claude-token-saver, under its new name.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -8,13 +8,12 @@
|
|
|
8
8
|
"claude-token-saver": "bin/cli.js"
|
|
9
9
|
},
|
|
10
10
|
"scripts": {
|
|
11
|
-
"
|
|
11
|
+
"deploy": "node scripts/deploy.mjs",
|
|
12
12
|
"docs:statusline": "node scripts/docs-statusline.mjs",
|
|
13
13
|
"postinstall": "node bin/cli.js install || true",
|
|
14
14
|
"publish:legacy": "node scripts/publish-legacy.mjs",
|
|
15
|
-
"sync:ko": "node scripts/sync-readme-ko.mjs",
|
|
16
15
|
"release:notes": "node scripts/release-notes.mjs",
|
|
17
|
-
"
|
|
16
|
+
"test": "node --test"
|
|
18
17
|
},
|
|
19
18
|
"files": [
|
|
20
19
|
"bin/",
|
package/src/changelog.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* changelog — read one version's section out of CHANGELOG.md
|
|
2
|
+
* changelog — read one version's section out of CHANGELOG.md, and say whether a
|
|
3
|
+
* notes draft is still waiting on a person.
|
|
3
4
|
*
|
|
4
5
|
* Separate from scripts/release-notes.mjs, which is a CLI: importing that file
|
|
5
6
|
* runs its argument parsing and exits, so the extraction it depends on cannot be
|
|
@@ -7,6 +8,12 @@
|
|
|
7
8
|
* pulled one line short drops the release's last item, and one line long carries
|
|
8
9
|
* the previous release's first, and the upgrade offer shows either without
|
|
9
10
|
* complaint.
|
|
11
|
+
*
|
|
12
|
+
* The draft marker lives here for a reason. Two scripts gate on it — the one that
|
|
13
|
+
* writes drafts and the one that deploys — and they used to carry the string
|
|
14
|
+
* separately. Renaming it in one left the other looking for text that no longer
|
|
15
|
+
* existed, and a gate that greps for an absent string does not fail: it passes,
|
|
16
|
+
* and an unreviewed draft goes out as the release. One definition, imported.
|
|
10
17
|
*/
|
|
11
18
|
|
|
12
19
|
/**
|
|
@@ -32,3 +39,32 @@ export function sectionFor(changelog, version) {
|
|
|
32
39
|
}
|
|
33
40
|
return out.join('\n').trim();
|
|
34
41
|
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* The line `release-notes.mjs` writes at the top of a fresh draft, and the line
|
|
45
|
+
* every gate refuses on. A draft still carrying it has not been through a person.
|
|
46
|
+
*/
|
|
47
|
+
export const DRAFT_MARKER =
|
|
48
|
+
'<!-- REVIEW: edit this draft, add the English half, then delete this line -->';
|
|
49
|
+
|
|
50
|
+
/** Heading that opens the Korean half of a bilingual release body. */
|
|
51
|
+
export const KO_HEADING = '## 한국어';
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* The opening of the marker, which is what the gate actually matches. Spelled out
|
|
55
|
+
* rather than sliced off `DRAFT_MARKER`, so that editing the marker's front does
|
|
56
|
+
* not silently change what is being tested for.
|
|
57
|
+
*/
|
|
58
|
+
const REVIEW_OPENING = '<!-- REVIEW:';
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Whether a draft still needs review. Matches on the `<!-- REVIEW:` opening
|
|
62
|
+
* rather than the whole line, so editing the wording of the instruction does not
|
|
63
|
+
* quietly disarm the gate.
|
|
64
|
+
*
|
|
65
|
+
* @param {string} draft - the draft file's contents
|
|
66
|
+
* @returns {boolean} true while the draft is unreviewed
|
|
67
|
+
*/
|
|
68
|
+
export function needsReview(draft) {
|
|
69
|
+
return String(draft || '').includes(REVIEW_OPENING);
|
|
70
|
+
}
|
package/src/cli-name.js
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* cli-name — the name this tool tells the user to type.
|
|
3
|
+
*
|
|
4
|
+
* One definition, because the alternative was measured: the marker string in the
|
|
5
|
+
* release scripts lived in two files, one was renamed, and the gate that grepped
|
|
6
|
+
* for the other silently stopped gating. A command name printed for the user to
|
|
7
|
+
* copy fails more quietly still — nothing errors, the user simply types a name
|
|
8
|
+
* that is not the package they installed.
|
|
9
|
+
*
|
|
10
|
+
* `sprag-cli` is the package; `claude-token-saver` is the deprecated name it
|
|
11
|
+
* shipped under, and both binaries are still in `package.json`'s `bin`, so the
|
|
12
|
+
* old name keeps working for anyone whose settings or notes carry it.
|
|
13
|
+
*
|
|
14
|
+
* NOT the same thing as the on-disk identifiers. The state directory, the harness
|
|
15
|
+
* block markers, and the skill directory are all still spelled with the old name
|
|
16
|
+
* on purpose: renaming those would orphan existing users' ledgers and stop the
|
|
17
|
+
* harness markers matching text already in their CLAUDE.md. Those are addresses,
|
|
18
|
+
* not instructions, and an address only has to be stable.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
/** The command name to print in anything a user is expected to type or read. */
|
|
22
|
+
export const CLI_NAME = 'sprag';
|
|
23
|
+
|
|
24
|
+
/** The deprecated name. Still a working binary; recognised, never printed. */
|
|
25
|
+
export const LEGACY_CLI_NAME = 'claude-token-saver';
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Subcommand: uninstall — take the integration back out of ~/.claude.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
4
|
+
* sprag uninstall # hooks, statusline, skill
|
|
5
|
+
* sprag uninstall --purge # the above plus recorded state
|
|
6
6
|
*
|
|
7
7
|
* Recorded savings are kept by default. Someone removing an integration is
|
|
8
8
|
* usually not asking to throw away the ledger that says what it saved, and
|
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
export async function run({ hasFlag, args = [] }) {
|
|
13
13
|
const { uninstallAll } = await import('../installer.js');
|
|
14
14
|
const { userLanguage } = await import('../config.js');
|
|
15
|
+
const { CLI_NAME } = await import('../cli-name.js');
|
|
15
16
|
const lang = userLanguage();
|
|
16
17
|
const purge = hasFlag('--purge') || args.includes('--purge');
|
|
17
18
|
|
|
@@ -36,7 +37,7 @@ export async function run({ hasFlag, args = [] }) {
|
|
|
36
37
|
}
|
|
37
38
|
if (!purge) {
|
|
38
39
|
console.log(lang === 'ko'
|
|
39
|
-
?
|
|
40
|
-
:
|
|
40
|
+
? ` 기록된 절감액과 설정까지 지우려면 \`${CLI_NAME} uninstall --purge\` 를 실행하십시오.`
|
|
41
|
+
: ` To remove recorded savings and settings too: \`${CLI_NAME} uninstall --purge\`.`);
|
|
41
42
|
}
|
|
42
43
|
}
|
package/src/formatters/table.js
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
* Terminal table formatter — zero dependencies.
|
|
3
3
|
*/
|
|
4
4
|
import { ISSUE_MESSAGES } from '../advice.js';
|
|
5
|
+
import { CLI_NAME } from '../cli-name.js';
|
|
5
6
|
import { formatResetIn, formatResetClock } from '../format-time.js';
|
|
6
7
|
import { labelForKey } from '../window-labels.js';
|
|
7
8
|
|
|
@@ -139,7 +140,7 @@ function renderSpikeSection(spikes, contextWindow) {
|
|
|
139
140
|
lines.push('');
|
|
140
141
|
}
|
|
141
142
|
}
|
|
142
|
-
lines.push(rbl(' ▶ 상세 처치법: ') + rb(
|
|
143
|
+
lines.push(rbl(' ▶ 상세 처치법: ') + rb(`${CLI_NAME} last`));
|
|
143
144
|
lines.push('');
|
|
144
145
|
return lines;
|
|
145
146
|
}
|
|
@@ -165,10 +166,10 @@ function renderCapWarnSection(caps) {
|
|
|
165
166
|
}
|
|
166
167
|
lines.push('');
|
|
167
168
|
lines.push(r(' Back up work before the cap hits:'));
|
|
168
|
-
lines.push(rb(
|
|
169
|
+
lines.push(rb(` ${CLI_NAME} handoff`));
|
|
169
170
|
lines.push(r(' (writes a HANDOFF-*.md so a fresh session can pick up.)'));
|
|
170
171
|
lines.push('');
|
|
171
|
-
lines.push(rbl(' ▶ 상세 처치법: ') + rb(
|
|
172
|
+
lines.push(rbl(' ▶ 상세 처치법: ') + rb(`${CLI_NAME} last`));
|
|
172
173
|
lines.push('');
|
|
173
174
|
return lines;
|
|
174
175
|
}
|
|
@@ -179,7 +180,7 @@ export function formatReport({ summary: sum, trend, ttl, anomalies, cost, option
|
|
|
179
180
|
// Header
|
|
180
181
|
lines.push('');
|
|
181
182
|
lines.push(` Claude Token Saver — Last ${options.days} day${options.days === 1 ? '' : 's'}`);
|
|
182
|
-
lines.push(` (
|
|
183
|
+
lines.push(` (${CLI_NAME} v${options.version || ''})`.trimEnd());
|
|
183
184
|
lines.push(` ${'═'.repeat(50)}`);
|
|
184
185
|
lines.push('');
|
|
185
186
|
|
package/src/hook-manager.js
CHANGED
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
import { readFile, writeFile, rm } from 'node:fs/promises';
|
|
13
13
|
import { join } from 'node:path';
|
|
14
14
|
import { homedir } from 'node:os';
|
|
15
|
+
import { CLI_NAME } from './cli-name.js';
|
|
15
16
|
|
|
16
17
|
const SETTINGS_PATH = join(homedir(), '.claude', 'settings.json');
|
|
17
18
|
// Legacy copied-file location — removed on install/uninstall so machines that
|
|
@@ -48,7 +49,7 @@ export async function installHook({ threshold = 0.7 } = {}) {
|
|
|
48
49
|
hooks: [
|
|
49
50
|
{
|
|
50
51
|
type: 'command',
|
|
51
|
-
command:
|
|
52
|
+
command: `${CLI_NAME} --hook-run --threshold ${threshold}`,
|
|
52
53
|
timeout: 10,
|
|
53
54
|
},
|
|
54
55
|
],
|
|
@@ -59,7 +60,7 @@ export async function installHook({ threshold = 0.7 } = {}) {
|
|
|
59
60
|
// Clean up the legacy copy left by older versions.
|
|
60
61
|
await rm(LEGACY_HOOK_DEST, { force: true }).catch(() => {});
|
|
61
62
|
|
|
62
|
-
console.log(
|
|
63
|
+
console.log(`✓ Hook installed (PostToolUse → ${CLI_NAME} --hook-run)`);
|
|
63
64
|
console.log(` Settings updated: ${SETTINGS_PATH}`);
|
|
64
65
|
console.log(` Threshold: ${(threshold * 100).toFixed(0)}%`);
|
|
65
66
|
console.log(` Stats file: ~/.claude/cache-stats.jsonl`);
|
package/src/installer.js
CHANGED
|
@@ -14,10 +14,64 @@
|
|
|
14
14
|
*/
|
|
15
15
|
|
|
16
16
|
import { writeFileSync, mkdirSync, existsSync, unlinkSync, readFileSync, rmSync } from 'node:fs';
|
|
17
|
+
import { CLI_NAME, LEGACY_CLI_NAME } from './cli-name.js';
|
|
17
18
|
import { join } from 'node:path';
|
|
18
19
|
import { claudeUserDir, userDataDir } from './paths.js';
|
|
19
20
|
|
|
20
|
-
|
|
21
|
+
/**
|
|
22
|
+
* The command every entry we write invokes.
|
|
23
|
+
*
|
|
24
|
+
* Both binaries ship in `package.json`'s `bin`, so either name works — but the
|
|
25
|
+
* package is `sprag-cli`, and a fresh install writing `claude-token-saver` into
|
|
26
|
+
* the user's settings puts a name they never typed in a file they read.
|
|
27
|
+
*
|
|
28
|
+
* The reverse direction is not symmetrical: entries ALREADY on disk carry the
|
|
29
|
+
* old name, so anything that recognises our own work has to accept both. That is
|
|
30
|
+
* what `isOurCommand` is for — narrowing it to the new name would make install
|
|
31
|
+
* miss an existing hook and add a second one beside it.
|
|
32
|
+
*/
|
|
33
|
+
const CLI = CLI_NAME;
|
|
34
|
+
const LEGACY_CLI = LEGACY_CLI_NAME;
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Whether a settings command belongs to this tool, under either name.
|
|
38
|
+
*
|
|
39
|
+
* The name has to sit where the executable goes, not just appear somewhere in the
|
|
40
|
+
* string. `sprag` is five characters, and `uninstall` deletes what this returns
|
|
41
|
+
* true for: a substring test claims `node ~/.claude/probe/sprag-probe.mjs` and
|
|
42
|
+
* anyone's `my-sprag-wrapper`, then removes them as ours. That is not
|
|
43
|
+
* hypothetical — the probe entry is on this machine.
|
|
44
|
+
*
|
|
45
|
+
* A wrapper is allowed in front, because the entry may legitimately be run
|
|
46
|
+
* through one: `npx sprag-cli brief --hook` and `/usr/local/bin/sprag doc2md`
|
|
47
|
+
* are both ours. So the executable is taken as the last path segment of the
|
|
48
|
+
* first token — or of the token after a runner like `npx` or `yarn dlx` — and
|
|
49
|
+
* matched whole, with `sprag-cli` accepted there since that is what npx resolves.
|
|
50
|
+
*
|
|
51
|
+
* The runner list follows `upgradeCommand`, which already prints npm, pnpm, bun
|
|
52
|
+
* and yarn instructions. Missing one is the duplicate-hook failure rather than
|
|
53
|
+
* the deletion one: an entry we do not recognise gets a second hook added beside
|
|
54
|
+
* it, and both then run on every prompt.
|
|
55
|
+
*/
|
|
56
|
+
const RUNNERS = /^(npx|bunx|pnpx)$/;
|
|
57
|
+
const RUNNER_SUBCOMMANDS = { pnpm: ['dlx', 'exec'], yarn: ['dlx', 'run'], bun: ['x', 'run'], npm: ['exec'] };
|
|
58
|
+
|
|
59
|
+
function isOurCommand(command) {
|
|
60
|
+
if (typeof command !== 'string') return false;
|
|
61
|
+
const tokens = command.trim().split(/\s+/).filter(Boolean);
|
|
62
|
+
if (!tokens.length) return false;
|
|
63
|
+
// `npx <pkg>` puts the package one token in, `yarn dlx <pkg>` two.
|
|
64
|
+
let i = 0;
|
|
65
|
+
if (RUNNERS.test(tokens[0])) i = 1;
|
|
66
|
+
else if (RUNNER_SUBCOMMANDS[tokens[0]]?.includes(tokens[1])) i = 2;
|
|
67
|
+
const token = tokens[i];
|
|
68
|
+
if (!token) return false;
|
|
69
|
+
// A path invokes the binary by its last segment; strip a Windows .cmd/.exe too.
|
|
70
|
+
const exe = token.split(/[/\\]/).pop().replace(/\.(cmd|exe|ps1)$/i, '');
|
|
71
|
+
return exe === CLI || exe === LEGACY_CLI || exe === `${CLI}-cli`;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const STATUSLINE_COMMAND = `${CLI} --statusline --icon`;
|
|
21
75
|
const STATUSLINE_REFRESH_INTERVAL = 5;
|
|
22
76
|
|
|
23
77
|
const SKILL_BODY = `---
|
|
@@ -181,7 +235,7 @@ export function installStatusline({ force = false } = {}) {
|
|
|
181
235
|
}
|
|
182
236
|
|
|
183
237
|
const cur = settings.statusLine;
|
|
184
|
-
const targetsUs = cur &&
|
|
238
|
+
const targetsUs = !!cur && isOurCommand(cur.command);
|
|
185
239
|
|
|
186
240
|
if (!cur) {
|
|
187
241
|
settings.statusLine = {
|
|
@@ -221,7 +275,7 @@ export function installStatusline({ force = false } = {}) {
|
|
|
221
275
|
// candidates as session context (startup + /clear). Idempotent: skips when a
|
|
222
276
|
// sprag route-scan hook is already present; never touches other
|
|
223
277
|
// hooks the user configured.
|
|
224
|
-
const ROUTE_SCAN_HOOK_COMMAND =
|
|
278
|
+
const ROUTE_SCAN_HOOK_COMMAND = `${CLI} route-scan --hook`;
|
|
225
279
|
|
|
226
280
|
export function installSessionStartHook() {
|
|
227
281
|
const dir = claudeUserDir();
|
|
@@ -273,7 +327,7 @@ export function installSessionStartHook() {
|
|
|
273
327
|
* produce identical output; a delegation is precisely the event that changes it,
|
|
274
328
|
* so this hook rescans on the spot. Detached, so the tool call does not wait.
|
|
275
329
|
*/
|
|
276
|
-
const ROUTE_SCAN_DELEGATED_HOOK_COMMAND =
|
|
330
|
+
const ROUTE_SCAN_DELEGATED_HOOK_COMMAND = `${CLI} route-scan --hook-delegated`;
|
|
277
331
|
|
|
278
332
|
export function installDelegationHook() {
|
|
279
333
|
const dir = claudeUserDir();
|
|
@@ -311,7 +365,7 @@ export function installDelegationHook() {
|
|
|
311
365
|
return { path: file, action: 'created' };
|
|
312
366
|
}
|
|
313
367
|
|
|
314
|
-
const BRIEF_HOOK_COMMAND =
|
|
368
|
+
const BRIEF_HOOK_COMMAND = `${CLI} brief --hook`;
|
|
315
369
|
|
|
316
370
|
export function installBriefHook() {
|
|
317
371
|
const dir = claudeUserDir();
|
|
@@ -353,7 +407,7 @@ export function installBriefHook() {
|
|
|
353
407
|
// drift surfaces only when a human reads the artifact. This hook closes that
|
|
354
408
|
// gap: it runs on the file the model just wrote, while it can still fix it.
|
|
355
409
|
// Installed by `korean on`, removed by `korean off`. Idempotent.
|
|
356
|
-
const KOREAN_LINT_HOOK_COMMAND =
|
|
410
|
+
const KOREAN_LINT_HOOK_COMMAND = `${CLI} korean --hook`;
|
|
357
411
|
// Bash belongs on this matcher because a heredoc, a `tee`, or a `sed -i` puts
|
|
358
412
|
// Korean on disk exactly like Write does. A subagent without the Write tool
|
|
359
413
|
// reaches for `cat > file` first, so leaving Bash off the list exempted every
|
|
@@ -436,8 +490,8 @@ export function removeKoreanLintHook() {
|
|
|
436
490
|
// workbook adding a minute on top. The row cap inside the converter is what
|
|
437
491
|
// actually bounds the work; the timeout is only a backstop.
|
|
438
492
|
// Installed by `doc2md on`, removed by `doc2md off`. Idempotent.
|
|
439
|
-
const DOC2MD_HOOK_COMMAND =
|
|
440
|
-
const DOC2MD_PROMPT_HOOK_COMMAND =
|
|
493
|
+
const DOC2MD_HOOK_COMMAND = `${CLI} doc2md --hook`;
|
|
494
|
+
const DOC2MD_PROMPT_HOOK_COMMAND = `${CLI} doc2md --hook-prompt`;
|
|
441
495
|
|
|
442
496
|
export function installDoc2mdHook() {
|
|
443
497
|
const dir = claudeUserDir();
|
|
@@ -576,7 +630,7 @@ export function uninstallAll({ purge = false } = {}) {
|
|
|
576
630
|
// Only our own statusline command goes. Someone else's stays exactly
|
|
577
631
|
// where they put it.
|
|
578
632
|
const cur = settings.statusLine;
|
|
579
|
-
if (cur &&
|
|
633
|
+
if (cur && isOurCommand(cur.command)) {
|
|
580
634
|
delete settings.statusLine;
|
|
581
635
|
result.removed.push('statusLine');
|
|
582
636
|
} else if (cur) {
|
|
@@ -586,15 +640,28 @@ export function uninstallAll({ purge = false } = {}) {
|
|
|
586
640
|
// Every event, every entry whose command is this CLI. Matching on the
|
|
587
641
|
// binary name rather than a list of exact commands means a hook added by
|
|
588
642
|
// an older version is still removed by a newer one.
|
|
643
|
+
//
|
|
644
|
+
// Removal is per hook, not per matcher group. Dropping whole groups was
|
|
645
|
+
// taking other tools' hooks with them whenever Claude Code had merged them
|
|
646
|
+
// into one group — which it does for entries sharing a matcher, so a user who
|
|
647
|
+
// installed us second lost the first tool's hook on uninstall. A group is
|
|
648
|
+
// deleted only once nothing of anyone else's is left in it.
|
|
649
|
+
const isOurs = (h) => isOurCommand(h?.command)
|
|
650
|
+
|| (typeof h?.command === 'string' && h.command.includes('cache-monitor-hook'));
|
|
589
651
|
for (const event of Object.keys(settings.hooks || {})) {
|
|
590
652
|
const list = settings.hooks[event];
|
|
591
653
|
if (!Array.isArray(list)) continue;
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
654
|
+
let touched = false;
|
|
655
|
+
const kept = [];
|
|
656
|
+
for (const m of list) {
|
|
657
|
+
if (!Array.isArray(m?.hooks)) { kept.push(m); continue; }
|
|
658
|
+
const hooks = m.hooks.filter((h) => !isOurs(h));
|
|
659
|
+
if (hooks.length === m.hooks.length) { kept.push(m); continue; }
|
|
660
|
+
touched = true;
|
|
661
|
+
// The group survives if it still carries someone else's hook.
|
|
662
|
+
if (hooks.length) kept.push({ ...m, hooks });
|
|
663
|
+
}
|
|
664
|
+
if (!touched) continue;
|
|
598
665
|
result.removed.push(`hooks.${event}`);
|
|
599
666
|
if (kept.length === 0) delete settings.hooks[event];
|
|
600
667
|
else settings.hooks[event] = kept;
|
|
@@ -652,7 +719,7 @@ export function migrateLegacyCacheMonitorHook() {
|
|
|
652
719
|
for (const h of m.hooks) {
|
|
653
720
|
if (typeof h?.command !== 'string' || !h.command.includes('cache-monitor-hook')) continue;
|
|
654
721
|
const th = h.command.match(/--threshold\s+([\d.]+)/);
|
|
655
|
-
h.command =
|
|
722
|
+
h.command = `${CLI} --hook-run --threshold ${th ? th[1] : '0.7'}`;
|
|
656
723
|
migrated = true;
|
|
657
724
|
}
|
|
658
725
|
}
|
package/src/seed-rules.js
CHANGED
|
@@ -24,6 +24,7 @@
|
|
|
24
24
|
*/
|
|
25
25
|
|
|
26
26
|
import { readFileSync, writeFileSync, existsSync, mkdirSync } from 'node:fs';
|
|
27
|
+
import { CLI_NAME } from './cli-name.js';
|
|
27
28
|
import { join, dirname } from 'node:path';
|
|
28
29
|
import { fileURLToPath } from 'node:url';
|
|
29
30
|
import { createHash } from 'node:crypto';
|
|
@@ -242,7 +243,7 @@ export function seedOfferBlock({ lang = userLanguage(), root = findProjectRoot()
|
|
|
242
243
|
const fixes = pending.filter((s) => s.kind === 'ratchet');
|
|
243
244
|
const lines = [];
|
|
244
245
|
if (lang === 'ko') {
|
|
245
|
-
lines.push(`[
|
|
246
|
+
lines.push(`[${CLI_NAME} seed] 아직 등록되지 않은 추천 룰이 ${pending.length}건 있습니다 (패키지 동봉 프리셋).`);
|
|
246
247
|
lines.push('**이 세션의 첫 응답에서 다른 작업보다 먼저 물어보십시오.** 사용자가 먼저 꺼낸 용건이 있으면 그 답변을 마친 직후에 이어서 묻고, 용건이 끝나기를 기다리지 마십시오. 사용자가 이 안내 문구에 대해 질문할 때까지 미루면 안 됩니다.');
|
|
247
248
|
lines.push(`첫 질문에서 다음 네 선택지를 **이 순서대로** 제시하십시오. ${pending.length}건을 한 건씩 답하는 것은 대부분의 사용자에게 과한 부담이므로, 일괄 등록이 맨 앞에 옵니다.`);
|
|
248
249
|
lines.push(' 1. 전부 글로벌로 등록 — `seed accept all --global`');
|
|
@@ -263,11 +264,11 @@ export function seedOfferBlock({ lang = userLanguage(), root = findProjectRoot()
|
|
|
263
264
|
lines.push(` · 래칫 룰 ${fixes.length}건 — 같은 실수를 반복하지 않도록 세션마다 읽히는 규칙입니다.`);
|
|
264
265
|
for (const s of fixes) lines.push(` [${s.id}] ${s.ruleText}`);
|
|
265
266
|
}
|
|
266
|
-
lines.push(
|
|
267
|
-
lines.push(
|
|
268
|
-
lines.push(
|
|
267
|
+
lines.push(` 일괄: ${CLI_NAME} seed accept all --global|--project # 적용 범위는 반드시 사용자에게 확인`);
|
|
268
|
+
lines.push(` 개별: ${CLI_NAME} seed accept <id> --global|--project`);
|
|
269
|
+
lines.push(` 거절: ${CLI_NAME} seed skip <id>|all # 다시 묻지 않습니다`);
|
|
269
270
|
} else {
|
|
270
|
-
lines.push(`[
|
|
271
|
+
lines.push(`[${CLI_NAME} seed] ${pending.length} recommended rule(s) from the bundled presets are not registered yet.`);
|
|
271
272
|
lines.push('**Ask in your very first reply of this session, before anything else.** If the user opened with their own request, answer it and then ask right away; do not wait for their task to finish, and never wait until they ask about this notice.');
|
|
272
273
|
lines.push(`Offer these four choices, **in this order**. Answering ${pending.length} rules one by one is more than most users want to do, so registering them all comes first.`);
|
|
273
274
|
lines.push(' 1. Register all, globally — `seed accept all --global`');
|
|
@@ -288,9 +289,9 @@ export function seedOfferBlock({ lang = userLanguage(), root = findProjectRoot()
|
|
|
288
289
|
lines.push(` · ${fixes.length} ratchet rule(s) — read at the start of every session so the same mistake is not repeated.`);
|
|
289
290
|
for (const s of fixes) lines.push(` [${s.id}] ${s.ruleText}`);
|
|
290
291
|
}
|
|
291
|
-
lines.push(
|
|
292
|
-
lines.push(
|
|
293
|
-
lines.push(
|
|
292
|
+
lines.push(` all: ${CLI_NAME} seed accept all --global|--project # ALWAYS confirm the scope with the user`);
|
|
293
|
+
lines.push(` one: ${CLI_NAME} seed accept <id> --global|--project`);
|
|
294
|
+
lines.push(` decline: ${CLI_NAME} seed skip <id>|all # never offered again`);
|
|
294
295
|
}
|
|
295
296
|
return lines.join('\n');
|
|
296
297
|
}
|
package/src/update-check.js
CHANGED
|
@@ -112,7 +112,9 @@ export function updateStatus(currentVersion) {
|
|
|
112
112
|
dismissed: available && s.dismissedVersion === latest,
|
|
113
113
|
stale: age >= CHECK_INTERVAL_MS,
|
|
114
114
|
// Only when they describe THIS version. A cache written for an earlier
|
|
115
|
-
// release would otherwise sell the wrong upgrade.
|
|
115
|
+
// release would otherwise sell the wrong upgrade. Language is not checked
|
|
116
|
+
// here: a mismatch is corrected by the next refresh, and showing the other
|
|
117
|
+
// language in the meantime beats showing nothing.
|
|
116
118
|
highlights: Array.isArray(s.highlights) && s.highlightsFor === latest ? s.highlights : [],
|
|
117
119
|
};
|
|
118
120
|
}
|
|
@@ -163,6 +165,28 @@ const RELEASES_API = 'https://api.github.com/repos/rootstudioyaml/sprag/releases
|
|
|
163
165
|
const MAX_HIGHLIGHTS = 3;
|
|
164
166
|
const MAX_HIGHLIGHT_LEN = 110;
|
|
165
167
|
|
|
168
|
+
/**
|
|
169
|
+
* A release body may carry both languages, English first and Korean under a
|
|
170
|
+
* heading of its own. Read only the half that matches the reader: mixing them
|
|
171
|
+
* would put the same change on two of the three lines the notice has room for,
|
|
172
|
+
* and showing English to someone who set `lang=ko` wastes the notice entirely.
|
|
173
|
+
*
|
|
174
|
+
* The Korean half starts at a heading whose text is Korean; everything before
|
|
175
|
+
* that first Korean heading is the English half. A body in one language only has
|
|
176
|
+
* no such heading, so English readers get all of it and Korean readers fall back
|
|
177
|
+
* to the same text rather than to nothing.
|
|
178
|
+
*
|
|
179
|
+
* @param {string} body
|
|
180
|
+
* @param {string} lang - 'ko' | 'en'
|
|
181
|
+
* @returns {string} the half to read
|
|
182
|
+
*/
|
|
183
|
+
function sectionForLang(body, lang) {
|
|
184
|
+
const lines = body.split(/\r?\n/);
|
|
185
|
+
const at = lines.findIndex((l) => /^#{1,6}\s/.test(l) && /[가-힣]/.test(l));
|
|
186
|
+
if (at < 0) return body;
|
|
187
|
+
return lang === 'ko' ? lines.slice(at + 1).join('\n') : lines.slice(0, at).join('\n');
|
|
188
|
+
}
|
|
189
|
+
|
|
166
190
|
/**
|
|
167
191
|
* The headline items of a release body, as short single lines.
|
|
168
192
|
*
|
|
@@ -174,10 +198,11 @@ const MAX_HIGHLIGHT_LEN = 110;
|
|
|
174
198
|
* rather than truncated into nonsense.
|
|
175
199
|
*
|
|
176
200
|
* @param {string} body - the release body as published
|
|
201
|
+
* @param {string} [lang] - 'ko' | 'en'; picks the half of a bilingual body
|
|
177
202
|
* @returns {string[]} at most MAX_HIGHLIGHTS lines, or [] when nothing fits
|
|
178
203
|
*/
|
|
179
|
-
export function releaseHighlights(body) {
|
|
180
|
-
const text = String(body || '');
|
|
204
|
+
export function releaseHighlights(body, lang = 'en') {
|
|
205
|
+
const text = sectionForLang(String(body || ''), lang);
|
|
181
206
|
if (!text.trim()) return [];
|
|
182
207
|
const out = [];
|
|
183
208
|
for (const raw of text.split(/\r?\n/)) {
|
|
@@ -246,7 +271,7 @@ export function releaseHighlights(body) {
|
|
|
246
271
|
* is worth showing with a bare version number, so a missing release, a rate
|
|
247
272
|
* limit, or an offline machine must not cost the user the notice itself.
|
|
248
273
|
*/
|
|
249
|
-
export async function fetchHighlights(version, { timeoutMs = FETCH_TIMEOUT_MS } = {}) {
|
|
274
|
+
export async function fetchHighlights(version, { timeoutMs = FETCH_TIMEOUT_MS, lang = 'en' } = {}) {
|
|
250
275
|
const controller = new AbortController();
|
|
251
276
|
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
252
277
|
try {
|
|
@@ -256,7 +281,7 @@ export async function fetchHighlights(version, { timeoutMs = FETCH_TIMEOUT_MS }
|
|
|
256
281
|
});
|
|
257
282
|
if (!res.ok) throw new Error(`releases responded ${res.status}`);
|
|
258
283
|
const body = await res.json();
|
|
259
|
-
return releaseHighlights(body && body.body);
|
|
284
|
+
return releaseHighlights(body && body.body, lang);
|
|
260
285
|
} catch (e) {
|
|
261
286
|
debug('update-check:highlights', e);
|
|
262
287
|
return [];
|
|
@@ -293,16 +318,31 @@ export async function refreshUpdateState(currentVersion) {
|
|
|
293
318
|
// What the new version adds, so the notice can answer "why would I".
|
|
294
319
|
// Only worth a second request when there is actually an upgrade to describe,
|
|
295
320
|
// and only when we do not already hold the notes for that exact version.
|
|
296
|
-
|
|
297
|
-
|
|
321
|
+
// The reader's own language: a release body carries both halves, and the
|
|
322
|
+
// notice has room for three lines in one of them.
|
|
323
|
+
let lang = 'en';
|
|
324
|
+
try {
|
|
325
|
+
const { userLanguage } = await import('./config.js');
|
|
326
|
+
lang = userLanguage();
|
|
327
|
+
} catch (e) { debug('update-check:lang', e); }
|
|
328
|
+
// Fetch when the version is new, and also when the reader switched language
|
|
329
|
+
// since these notes were cached — otherwise they keep reading the half they
|
|
330
|
+
// left behind.
|
|
331
|
+
const staleLang = prev.highlightsFor === latest && prev.highlightsLang !== lang;
|
|
332
|
+
if (isNewer(latest, currentVersion) && (prev.highlightsFor !== latest || staleLang)) {
|
|
333
|
+
const highlights = await fetchHighlights(latest, { lang });
|
|
298
334
|
if (highlights.length) {
|
|
299
335
|
next.highlights = highlights;
|
|
300
336
|
next.highlightsFor = latest;
|
|
337
|
+
// Which language these are in. Someone who switches `lang` afterwards
|
|
338
|
+
// would otherwise keep reading the notes in the language they left.
|
|
339
|
+
next.highlightsLang = lang;
|
|
301
340
|
} else {
|
|
302
341
|
// Drop notes belonging to an older version rather than showing them
|
|
303
342
|
// against this one.
|
|
304
343
|
delete next.highlights;
|
|
305
344
|
delete next.highlightsFor;
|
|
345
|
+
delete next.highlightsLang;
|
|
306
346
|
}
|
|
307
347
|
}
|
|
308
348
|
writeUpdateState(next);
|