sprag-cli 3.57.0 → 3.58.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.ko.md +59 -39
- package/README.md +62 -40
- package/bin/cli.js +7 -2
- package/package.json +13 -25
- package/src/commands/install.js +24 -13
- package/src/installer.js +29 -5
- package/src/prompt.js +28 -0
- package/src/statusline-base.js +37 -0
- package/src/stdin-payload.js +18 -1
package/README.ko.md
CHANGED
|
@@ -36,50 +36,25 @@ Sprag는 Claude Code와 Codex가 남기는 세션 기록을 읽어 반복 실패
|
|
|
36
36
|
npm i -g sprag-cli # 예전 이름 claude-token-saver 와 같은 패키지입니다
|
|
37
37
|
```
|
|
38
38
|
|
|
39
|
-
##
|
|
39
|
+
## 설치하면 바로 달라지는 것
|
|
40
40
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
41
|
+
1. `npm i -g sprag-cli` 를 실행합니다. 훅과 상태줄, 추천 룰이 모든 프로젝트에
|
|
42
|
+
설정되므로 프로젝트마다 따로 설정할 것은 없습니다. 이미 쓰던 상태줄이 있으면
|
|
43
|
+
둘 다 표시할지, 교체할지, 그대로 둘지 설치 중에 묻습니다.
|
|
44
|
+
2. 아무 프로젝트에서나 Claude Code를 열고 평소처럼 일합니다.
|
|
45
|
+
3. 상태줄을 봅니다. 세션에서 쓴 금액과 아낀 금액이 나오고, 캐시나 컨텍스트,
|
|
46
|
+
사용량 한도에 문제가 생기면 경고 칩이 줄 맨 앞에 나타납니다.
|
|
46
47
|
|
|
47
|
-
```
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
sprag --agent codex --days 7
|
|
51
|
-
sprag panel --agent codex # 별도 터미널에서 실시간 패널 실행
|
|
52
|
-
sprag --statusline --agent codex --text
|
|
53
|
-
sprag doctor --agent codex # 문제가 생겼을 때만
|
|
54
|
-
sprag route-scan --agent codex
|
|
55
|
-
sprag seed --agent codex
|
|
48
|
+
```
|
|
49
|
+
🔀 Routing saved $44.67 | opus→sonnet 59× $39.75 · opus→haiku 10× $4.92
|
|
50
|
+
🧠 Cache hit 98.8% · ⏳ Cache expires 59:46 · 🅷 5/5 · 📦 Ctx 47% of 1M
|
|
56
51
|
```
|
|
57
52
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
지침 전달을 켤 수 있습니다. 상태 표시는 Sprag 터미널 상태줄과 패널을 사용하십시오.
|
|
62
|
-
Codex 기본 상태줄은 셸 명령을 실행하지 않습니다. 반복된 단순 작업은 위임 후보로,
|
|
63
|
-
반복된 도구 실패는 래칫 후보로 제안합니다. 규칙을 등록하려면 사용자의 승인이 필요하며,
|
|
64
|
-
`seed`로 호환되는 기본 규칙을 선택할 수도 있습니다.
|
|
65
|
-
`sprag route-scan savings --agent codex`는 Sprag가 위임한 실행만 집계하고,
|
|
66
|
-
일치하는 LiteLLM 단가가 있을 때 예상 차액을 계산합니다. 청구 금액을 측정한 값은 아니며,
|
|
67
|
-
단가를 확인하지 못하면 `n/a`로 표시합니다. Codex 문서 누계는 Claude 추정치와 분리합니다.
|
|
68
|
-
아래의 기존 절감 수치는 Claude Code 기록이며 Codex 벤치마크가 아닙니다.
|
|
69
|
-
[Codex 지원 범위와 제한](https://sprag.io/docs/ko/codex/)이나
|
|
70
|
-
`sprag capabilities --agent codex`에서 기능별 지원 여부를 확인하십시오.
|
|
71
|
-
|
|
72
|
-
| 기능 | Claude Code | Codex |
|
|
73
|
-
|---|---|---|
|
|
74
|
-
| 세션 지침 | `CLAUDE.md`와 훅 | `AGENTS.md`와 기본 훅 |
|
|
75
|
-
| 실시간 상태 | 기본 상태줄에 명령 등록 | 별도 패널 또는 tmux 구성 |
|
|
76
|
-
| 규칙 후보 | Claude 기록과 모델 등급 프리셋 | Codex 기록과 호환 래칫 프리셋 |
|
|
77
|
-
| 모델 위임 | Haiku·Sonnet 등급 규칙 | 공급자 모델 ID와 범위별 규칙 |
|
|
78
|
-
| 위임 원장 | Claude 실행별 예상 차액 | LiteLLM 단가와 하위 실행별 예상 차액 |
|
|
79
|
-
|
|
80
|
-
두 연동을 함께 설치할 수 있습니다. `--agent codex`를 붙이지 않으면 기존 Claude 동작을 유지합니다.
|
|
53
|
+
Sprag는 사용자가 고른 모델을 바꾸지 않습니다. 분석은 로컬 세션 기록만 읽으며,
|
|
54
|
+
추가 모델 호출이나 API 키가 필요하지 않습니다. `sprag uninstall` 은 설치 때
|
|
55
|
+
추가한 것을 모두 지우고, 함께 표시하던 기존 상태줄을 되돌려 놓습니다.
|
|
81
56
|
|
|
82
|
-
|
|
57
|
+
Codex를 쓴다면 아래 [Codex](#codex) 절을 보십시오.
|
|
83
58
|
|
|
84
59
|
## 작동 방식
|
|
85
60
|
|
|
@@ -181,6 +156,51 @@ JetBrains IDE 내장 터미널에서는 이모지 대신 한 칸 글리프로
|
|
|
181
156
|
|
|
182
157
|
래칫 규칙과 위임 규칙은 늘어나는 방식이 다릅니다. 래칫 규칙은 사람이 저지른 실수에서 나오므로 계속 늘어납니다. 위임 규칙은 멈춥니다. 기준이 알아보는 작업 유형이 정해져 있어서, 유형마다 규칙이 하나 생기면 더 승격할 것이 없습니다.
|
|
183
158
|
|
|
159
|
+
## Codex
|
|
160
|
+
|
|
161
|
+
Codex 기본 훅과 `AGENTS.md` 하네스, 범위별 래칫 규칙,
|
|
162
|
+
한국어 검사, 프롬프트의 문서 경로 자동 변환, 로컬 토큰 보고서를 사용할 수 있습니다.
|
|
163
|
+
Codex가 설치되어 있으면 `npm i -g sprag-cli` 하나로 Codex도 설정하고, 추천 룰을 묻지 않고
|
|
164
|
+
전역으로 등록합니다(단계마다 확인하려면 `sprag install --manual`). Claude Code를 건드리지 않고
|
|
165
|
+
Codex만 쓴다면 다음과 같이 설치하십시오.
|
|
166
|
+
|
|
167
|
+
```bash
|
|
168
|
+
npm i -g sprag-cli --ignore-scripts
|
|
169
|
+
sprag install --agent codex
|
|
170
|
+
sprag --agent codex --days 7
|
|
171
|
+
sprag panel --agent codex # 별도 터미널에서 실시간 패널 실행
|
|
172
|
+
sprag --statusline --agent codex --text
|
|
173
|
+
sprag doctor --agent codex # 문제가 생겼을 때만
|
|
174
|
+
sprag route-scan --agent codex
|
|
175
|
+
sprag seed --agent codex
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
설치 후 Codex의 `/hooks`에서 새 훅을 검토하고 신뢰해야 실행됩니다. Codex가 요구하는
|
|
179
|
+
절차입니다. 명령의 기본 대상은 여전히 Claude Code이며, `--agent codex`를 붙이면 Codex를 고릅니다. Codex에서도 세션 브리핑, 경고 이력, 인수인계
|
|
180
|
+
파일을 지원합니다. `sprag delegate on --agent codex`로 모델 위임과 하위 에이전트
|
|
181
|
+
지침 전달을 켤 수 있습니다. 상태 표시는 Sprag 터미널 상태줄과 패널을 사용하십시오.
|
|
182
|
+
Codex 기본 상태줄은 셸 명령을 실행하지 않습니다. 반복된 단순 작업은 위임 후보로,
|
|
183
|
+
반복된 도구 실패는 래칫 후보로 제안합니다. 규칙을 등록하려면 사용자의 승인이 필요하며,
|
|
184
|
+
`seed`로 호환되는 기본 규칙을 선택할 수도 있습니다.
|
|
185
|
+
`sprag route-scan savings --agent codex`는 Sprag가 위임한 실행만 집계하고,
|
|
186
|
+
일치하는 LiteLLM 단가가 있을 때 예상 차액을 계산합니다. 청구 금액을 측정한 값은 아니며,
|
|
187
|
+
단가를 확인하지 못하면 `n/a`로 표시합니다. Codex 문서 누계는 Claude 추정치와 분리합니다.
|
|
188
|
+
아래의 기존 절감 수치는 Claude Code 기록이며 Codex 벤치마크가 아닙니다.
|
|
189
|
+
[Codex 지원 범위와 제한](https://sprag.io/docs/ko/codex/)이나
|
|
190
|
+
`sprag capabilities --agent codex`에서 기능별 지원 여부를 확인하십시오.
|
|
191
|
+
|
|
192
|
+
| 기능 | Claude Code | Codex |
|
|
193
|
+
|---|---|---|
|
|
194
|
+
| 세션 지침 | `CLAUDE.md`와 훅 | `AGENTS.md`와 기본 훅 |
|
|
195
|
+
| 실시간 상태 | 기본 상태줄에 명령 등록 | 별도 패널 또는 tmux 구성 |
|
|
196
|
+
| 규칙 후보 | Claude 기록과 모델 등급 프리셋 | Codex 기록과 호환 래칫 프리셋 |
|
|
197
|
+
| 모델 위임 | Haiku·Sonnet 등급 규칙 | 공급자 모델 ID와 범위별 규칙 |
|
|
198
|
+
| 위임 원장 | Claude 실행별 예상 차액 | LiteLLM 단가와 하위 실행별 예상 차액 |
|
|
199
|
+
|
|
200
|
+
두 연동을 함께 설치할 수 있습니다. `--agent codex`를 붙이지 않으면 기존 Claude 동작을 유지합니다.
|
|
201
|
+
|
|
202
|
+

|
|
203
|
+
|
|
184
204
|
## 자주 쓰는 명령
|
|
185
205
|
|
|
186
206
|
| 명령 | 하는 일 |
|
package/README.md
CHANGED
|
@@ -48,51 +48,27 @@ existing credentials. The CLI requires Node.js 18+ and one TOML parser dependenc
|
|
|
48
48
|
npm i -g sprag-cli # Claude Code, plus Codex when it is installed
|
|
49
49
|
```
|
|
50
50
|
|
|
51
|
-
##
|
|
51
|
+
## After you install
|
|
52
52
|
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
53
|
+
1. Run `npm i -g sprag-cli`. Hooks, the statusline and the starter rules are set
|
|
54
|
+
up for every project; there is nothing to configure per project. If you
|
|
55
|
+
already have a statusline, the install asks whether to show both, replace
|
|
56
|
+
yours, or keep yours.
|
|
57
|
+
2. Open Claude Code in any project and work as usual.
|
|
58
|
+
3. Watch the statusline. It shows what the session spent and saved, and a
|
|
59
|
+
warning chip leads the line when the cache, the context or a rate limit
|
|
60
|
+
needs attention:
|
|
58
61
|
|
|
59
|
-
```
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
sprag --agent codex --days 7
|
|
63
|
-
sprag panel --agent codex # live companion in a separate terminal
|
|
64
|
-
sprag --statusline --agent codex --text
|
|
65
|
-
sprag doctor --agent codex # troubleshooting only
|
|
66
|
-
sprag route-scan --agent codex
|
|
67
|
-
sprag seed --agent codex
|
|
62
|
+
```
|
|
63
|
+
🔀 Routing saved $44.67 | opus→sonnet 59× $39.75 · opus→haiku 10× $4.92
|
|
64
|
+
🧠 Cache hit 98.8% · ⏳ Cache expires 59:46 · 🅷 5/5 · 📦 Ctx 47% of 1M
|
|
68
65
|
```
|
|
69
66
|
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
model routing and subagent guidance (`sprag delegate on --agent codex`). The native Codex footer
|
|
74
|
-
does not accept shell commands; Sprag provides a terminal statusline and panel.
|
|
75
|
-
Recurring simple turns become route candidates; repeated tool failures become
|
|
76
|
-
ratchet candidates. Both require your approval before a rule is registered.
|
|
77
|
-
`seed` offers compatible starter rules with separate Codex decisions.
|
|
78
|
-
`sprag route-scan savings --agent codex` records only Sprag-routed child runs
|
|
79
|
-
and estimates their price difference when matching LiteLLM prices are available.
|
|
80
|
-
It is not a billing measurement. Unknown prices remain `n/a`, and Codex document
|
|
81
|
-
counts stay separate from Claude estimates. The historical dollar figures below
|
|
82
|
-
are from Claude Code, not a Codex benchmark. See [Codex support and limits](https://sprag.io/docs/codex/), or run
|
|
83
|
-
`sprag capabilities --agent codex` for the support matrix.
|
|
84
|
-
|
|
85
|
-
| Workflow | Claude Code | Codex |
|
|
86
|
-
|---|---|---|
|
|
87
|
-
| Session instructions | `CLAUDE.md` and hooks | `AGENTS.md` and native hooks |
|
|
88
|
-
| Live telemetry | Native statusline command | Companion panel or tmux layout |
|
|
89
|
-
| Rule candidates | Claude transcripts and tier presets | Codex rollouts and compatible ratchet presets |
|
|
90
|
-
| Model routing | Haiku/Sonnet tier rules | Explicit provider model IDs and scoped rules |
|
|
91
|
-
| Routing ledger | Claude run estimates | Attributed child estimates with LiteLLM prices |
|
|
92
|
-
|
|
93
|
-
Both integrations coexist. Commands without `--agent codex` retain Claude behavior.
|
|
67
|
+
Sprag never swaps the model you picked. Analysis reads your local session
|
|
68
|
+
logs only, with no extra model calls and no API key. `sprag uninstall` removes
|
|
69
|
+
everything it added and restores a statusline it merged with.
|
|
94
70
|
|
|
95
|
-
|
|
71
|
+
Using Codex? See [Codex](#codex) below.
|
|
96
72
|
|
|
97
73
|
## How it works
|
|
98
74
|
|
|
@@ -225,6 +201,52 @@ The two counts grow differently. Ratchet rules come from mistakes a person made,
|
|
|
225
201
|
so they keep coming. Delegation rules stop: the criteria recognize a fixed set of
|
|
226
202
|
work types, and once each has a rule there is nothing left to promote.
|
|
227
203
|
|
|
204
|
+
## Codex
|
|
205
|
+
|
|
206
|
+
Native hooks, an `AGENTS.md` harness, scoped ratchet rules, Korean lint,
|
|
207
|
+
document conversion, token reports, and a session-bound companion panel.
|
|
208
|
+
`npm i -g sprag-cli` sets Codex up too when it is installed, with the rule
|
|
209
|
+
presets registered globally and nothing asked (`sprag install --manual` asks at
|
|
210
|
+
each step). For a Codex-only install that leaves Claude Code alone:
|
|
211
|
+
|
|
212
|
+
```bash
|
|
213
|
+
npm i -g sprag-cli --ignore-scripts
|
|
214
|
+
sprag install --agent codex
|
|
215
|
+
sprag --agent codex --days 7
|
|
216
|
+
sprag panel --agent codex # live companion in a separate terminal
|
|
217
|
+
sprag --statusline --agent codex --text
|
|
218
|
+
sprag doctor --agent codex # troubleshooting only
|
|
219
|
+
sprag route-scan --agent codex
|
|
220
|
+
sprag seed --agent codex
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
Review and trust the new hooks in Codex's `/hooks` menu; Codex requires that
|
|
224
|
+
step. Commands still default to Claude Code; add `--agent codex` to pick Codex.
|
|
225
|
+
Codex also supports session briefings, warning history, handoffs, and opt-in
|
|
226
|
+
model routing and subagent guidance (`sprag delegate on --agent codex`). The native Codex footer
|
|
227
|
+
does not accept shell commands; Sprag provides a terminal statusline and panel.
|
|
228
|
+
Recurring simple turns become route candidates; repeated tool failures become
|
|
229
|
+
ratchet candidates. Both require your approval before a rule is registered.
|
|
230
|
+
`seed` offers compatible starter rules with separate Codex decisions.
|
|
231
|
+
`sprag route-scan savings --agent codex` records only Sprag-routed child runs
|
|
232
|
+
and estimates their price difference when matching LiteLLM prices are available.
|
|
233
|
+
It is not a billing measurement. Unknown prices remain `n/a`, and Codex document
|
|
234
|
+
counts stay separate from Claude estimates. The historical dollar figures below
|
|
235
|
+
are from Claude Code, not a Codex benchmark. See [Codex support and limits](https://sprag.io/docs/codex/), or run
|
|
236
|
+
`sprag capabilities --agent codex` for the support matrix.
|
|
237
|
+
|
|
238
|
+
| Workflow | Claude Code | Codex |
|
|
239
|
+
|---|---|---|
|
|
240
|
+
| Session instructions | `CLAUDE.md` and hooks | `AGENTS.md` and native hooks |
|
|
241
|
+
| Live telemetry | Native statusline command | Companion panel or tmux layout |
|
|
242
|
+
| Rule candidates | Claude transcripts and tier presets | Codex rollouts and compatible ratchet presets |
|
|
243
|
+
| Model routing | Haiku/Sonnet tier rules | Explicit provider model IDs and scoped rules |
|
|
244
|
+
| Routing ledger | Claude run estimates | Attributed child estimates with LiteLLM prices |
|
|
245
|
+
|
|
246
|
+
Both integrations coexist. Commands without `--agent codex` retain Claude behavior.
|
|
247
|
+
|
|
248
|
+

|
|
249
|
+
|
|
228
250
|
## Day-to-day commands
|
|
229
251
|
|
|
230
252
|
| Command | What it does |
|
package/bin/cli.js
CHANGED
|
@@ -590,10 +590,11 @@ async function main() {
|
|
|
590
590
|
persistSnapshot({ caps, model });
|
|
591
591
|
} catch (e) { debug('caps-cache:persist', e); }
|
|
592
592
|
}
|
|
593
|
-
|
|
593
|
+
const { withBaseStatusline } = await import('../src/statusline-base.js');
|
|
594
|
+
console.log(withBaseStatusline(formatNoSession(
|
|
594
595
|
{ caps, model, effort, windowLabel, version: PKG_VERSION, update: readUpdateChip() },
|
|
595
596
|
{ color: colorOk, mode: labelMode },
|
|
596
|
-
));
|
|
597
|
+
)));
|
|
597
598
|
return;
|
|
598
599
|
}
|
|
599
600
|
// JSON mode stays JSON. A caller that asked for machine-readable output
|
|
@@ -901,6 +902,10 @@ async function main() {
|
|
|
901
902
|
output = formatReport(data, { color: tableColor() });
|
|
902
903
|
}
|
|
903
904
|
|
|
905
|
+
if (format === 'statusline') {
|
|
906
|
+
const { withBaseStatusline } = await import('../src/statusline-base.js');
|
|
907
|
+
output = withBaseStatusline(output);
|
|
908
|
+
}
|
|
904
909
|
console.log(output);
|
|
905
910
|
}
|
|
906
911
|
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "sprag-cli",
|
|
3
|
-
"version": "3.
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "3.58.0",
|
|
4
|
+
"description": "Lower your Claude Code bill without changing how you work: simple tasks go to cheaper models, documents go in as compact Markdown, and the statusline shows spend and savings live. Works with Codex too.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
7
7
|
"sprag": "bin/cli.js",
|
|
@@ -33,34 +33,22 @@
|
|
|
33
33
|
"node": ">=18"
|
|
34
34
|
},
|
|
35
35
|
"keywords": [
|
|
36
|
-
"sprag",
|
|
37
|
-
"ratchet",
|
|
38
|
-
"harness",
|
|
39
|
-
"ai-agent",
|
|
40
|
-
"claude",
|
|
41
36
|
"claude-code",
|
|
42
|
-
"
|
|
43
|
-
"openai-codex",
|
|
44
|
-
"agents-md",
|
|
37
|
+
"claude",
|
|
45
38
|
"anthropic",
|
|
46
|
-
"
|
|
47
|
-
"
|
|
48
|
-
"delegation",
|
|
49
|
-
"subagent",
|
|
50
|
-
"haiku",
|
|
51
|
-
"cost-savings",
|
|
39
|
+
"codex",
|
|
40
|
+
"token-usage",
|
|
52
41
|
"token-saver",
|
|
53
42
|
"reduce-tokens",
|
|
54
|
-
"
|
|
55
|
-
"prompt-caching",
|
|
56
|
-
"cache",
|
|
57
|
-
"statusline",
|
|
58
|
-
"cli",
|
|
59
|
-
"1m-context",
|
|
60
|
-
"claude-code-statusline",
|
|
43
|
+
"cost-savings",
|
|
61
44
|
"claude-code-cost",
|
|
62
|
-
"
|
|
63
|
-
"
|
|
45
|
+
"claude-code-statusline",
|
|
46
|
+
"statusline",
|
|
47
|
+
"model-routing",
|
|
48
|
+
"subagent",
|
|
49
|
+
"prompt-caching",
|
|
50
|
+
"haiku",
|
|
51
|
+
"cli"
|
|
64
52
|
],
|
|
65
53
|
"license": "Apache-2.0",
|
|
66
54
|
"repository": {
|
package/src/commands/install.js
CHANGED
|
@@ -11,6 +11,11 @@
|
|
|
11
11
|
* registered globally, and Codex is set up too when ~/.codex exists. `--manual`
|
|
12
12
|
* restores the step-by-step questions (they still need a terminal attached).
|
|
13
13
|
* `--yes` and `--no-input` are accepted as the explicit spelling of the default.
|
|
14
|
+
*
|
|
15
|
+
* One question survives the automatic mode: when a statusline that is not ours
|
|
16
|
+
* is already configured and a terminal is attached, the install asks whether to
|
|
17
|
+
* merge (theirs first, sprag below; default), replace, or keep. Without a
|
|
18
|
+
* terminal (postinstall, CI, pipes) it keeps theirs, as before.
|
|
14
19
|
*/
|
|
15
20
|
|
|
16
21
|
import { debug } from '../debug.js';
|
|
@@ -21,7 +26,7 @@ class SkipStep extends Error {}
|
|
|
21
26
|
export async function run({ hasFlag }) {
|
|
22
27
|
const { installAll } = await import('../installer.js');
|
|
23
28
|
const { userLanguage, languageDecided, setUserLanguage } = await import('../config.js');
|
|
24
|
-
const { canPrompt, confirm } = await import('../prompt.js');
|
|
29
|
+
const { canPrompt, confirm, choose } = await import('../prompt.js');
|
|
25
30
|
const force = hasFlag('--force');
|
|
26
31
|
// Automatic unless asked otherwise. A question per step turned the install
|
|
27
32
|
// into a dozen answers before any work, and every answer has a sensible
|
|
@@ -30,6 +35,9 @@ export async function run({ hasFlag }) {
|
|
|
30
35
|
// attached, so postinstall, CI and pipes never wait on input.
|
|
31
36
|
const manual = hasFlag('--manual') && !hasFlag('--yes') && !hasFlag('--no-input');
|
|
32
37
|
const interactive = manual && canPrompt();
|
|
38
|
+
// The foreign-statusline question is asked in the automatic install too: it
|
|
39
|
+
// is the one decision with no safe default that preserves both parties.
|
|
40
|
+
const tty = canPrompt();
|
|
33
41
|
|
|
34
42
|
// Output language, decided first because every line below it — and every
|
|
35
43
|
// briefing the hooks inject from here on — is written in it. Until now it
|
|
@@ -82,25 +90,28 @@ export async function run({ hasFlag }) {
|
|
|
82
90
|
let s = r.statusline;
|
|
83
91
|
// A different statusline is already installed. Replacing it silently
|
|
84
92
|
// would be rude and leaving it silently loses the tool's main surface,
|
|
85
|
-
// so with a human attached the install asks. Default is "
|
|
86
|
-
//
|
|
87
|
-
if (s.action === 'skipped' && s.conflict &&
|
|
93
|
+
// so with a human attached the install asks. Default is "merge": their
|
|
94
|
+
// output stays and ours goes below it, so nobody loses anything.
|
|
95
|
+
if (s.action === 'skipped' && s.conflict && tty) {
|
|
96
|
+
const { installStatusline } = await import('../installer.js');
|
|
88
97
|
console.log('');
|
|
89
98
|
console.log(lang === 'ko'
|
|
90
99
|
? ` statusline: 기존 statusline이 이미 설정되어 있습니다: ${s.existingCommand}`
|
|
91
100
|
: ` statusline: an existing statusline is already configured: ${s.existingCommand}`);
|
|
92
|
-
|
|
93
|
-
? '
|
|
94
|
-
: '
|
|
95
|
-
|
|
96
|
-
? '
|
|
97
|
-
: '
|
|
98
|
-
|
|
99
|
-
|
|
101
|
+
const pick = await choose(lang === 'ko'
|
|
102
|
+
? ' statusline을 어떻게 표시할까요?'
|
|
103
|
+
: ' How should the statusline be shown?', [
|
|
104
|
+
{ key: 'merge', label: lang === 'ko' ? '함께 표시: 기존 출력 아래에 sprag 줄을 붙입니다' : 'merge: your output first, sprag lines below' },
|
|
105
|
+
{ key: 'replace', label: lang === 'ko' ? '교체: sprag statusline만 표시하고 기존 설정은 지웁니다' : 'replace: sprag statusline only, the old one is removed' },
|
|
106
|
+
{ key: 'keep', label: lang === 'ko' ? '유지: 기존 statusline만 두고 sprag은 표시하지 않습니다' : 'keep: leave yours, no sprag statusline' },
|
|
107
|
+
], { defaultIndex: 0, defaultHint: lang === 'ko' ? '(기본값)' : '(default)' });
|
|
108
|
+
if (pick === 'merge') {
|
|
109
|
+
s = installStatusline({ merge: true });
|
|
110
|
+
} else if (pick === 'replace') {
|
|
100
111
|
s = installStatusline({ force: true });
|
|
101
112
|
} else {
|
|
102
113
|
s = { ...s, reason: lang === 'ko'
|
|
103
|
-
? '기존 statusline을 유지했습니다.
|
|
114
|
+
? '기존 statusline을 유지했습니다. 바꾸려면 `sprag install --force`'
|
|
104
115
|
: 'kept your statusline — replace later with `sprag install --force`' };
|
|
105
116
|
}
|
|
106
117
|
}
|
package/src/installer.js
CHANGED
|
@@ -357,8 +357,10 @@ export function removeLegacyCommand() {
|
|
|
357
357
|
// - No statusLine yet: insert ours with refreshInterval:STATUSLINE_REFRESH_INTERVAL.
|
|
358
358
|
// - statusLine already points at sprag: ensure that refreshInterval
|
|
359
359
|
// (this is the bit that makes the TTL countdown tick every second while idle).
|
|
360
|
-
// - statusLine points at a different command: leave it alone unless --force
|
|
361
|
-
|
|
360
|
+
// - statusLine points at a different command: leave it alone unless --force,
|
|
361
|
+
// or `merge`, which keeps their command running underneath ours (the original
|
|
362
|
+
// object is saved as config `statuslineBase` so uninstall can restore it).
|
|
363
|
+
export function installStatusline({ force = false, merge = false } = {}) {
|
|
362
364
|
const dir = claudeUserDir();
|
|
363
365
|
const file = join(dir, 'settings.json');
|
|
364
366
|
mkdirSync(dir, { recursive: true });
|
|
@@ -395,12 +397,13 @@ export function installStatusline({ force = false } = {}) {
|
|
|
395
397
|
return { path: file, action: 'updated', reason: `set refreshInterval=${STATUSLINE_REFRESH_INTERVAL}` };
|
|
396
398
|
}
|
|
397
399
|
|
|
398
|
-
if (!force) {
|
|
400
|
+
if (!force && !merge) {
|
|
399
401
|
// `conflict` lets the interactive install distinguish "someone else's
|
|
400
402
|
// statusline is here" (worth asking about) from other skip reasons
|
|
401
403
|
// (unreadable JSON), which a prompt cannot fix.
|
|
402
404
|
return { path: file, action: 'skipped', conflict: true, existingCommand: cur.command, reason: `existing statusLine command (${cur.command}) — re-run with --force to overwrite` };
|
|
403
405
|
}
|
|
406
|
+
const base = cur;
|
|
404
407
|
settings.statusLine = {
|
|
405
408
|
type: 'command',
|
|
406
409
|
command: STATUSLINE_COMMAND,
|
|
@@ -410,6 +413,17 @@ export function installStatusline({ force = false } = {}) {
|
|
|
410
413
|
const writeProblem = writeSettings(file, settings);
|
|
411
414
|
if (writeProblem) return { path: file, action: 'skipped', reason: writeProblem };
|
|
412
415
|
}
|
|
416
|
+
// Config is touched only after settings were written, so a failed write
|
|
417
|
+
// never leaves a saved base for a statusline we did not take over.
|
|
418
|
+
const cfg = loadConfig();
|
|
419
|
+
if (merge) {
|
|
420
|
+
cfg.statuslineBase = base;
|
|
421
|
+
saveConfig(cfg);
|
|
422
|
+
return { path: file, action: 'updated', reason: 'merged with previous statusLine', merged: true };
|
|
423
|
+
}
|
|
424
|
+
// Replace means the old one is gone for good; a stale base would resurrect it
|
|
425
|
+
// (or keep running it) later.
|
|
426
|
+
if (cfg.statuslineBase) { delete cfg.statuslineBase; saveConfig(cfg); }
|
|
413
427
|
return { path: file, action: 'updated', reason: 'replaced previous statusLine' };
|
|
414
428
|
}
|
|
415
429
|
|
|
@@ -910,8 +924,18 @@ export function uninstallAll({ purge = false } = {}) {
|
|
|
910
924
|
// where they put it.
|
|
911
925
|
const cur = settings.statusLine;
|
|
912
926
|
if (cur && isOurCommand(cur.command)) {
|
|
913
|
-
|
|
914
|
-
|
|
927
|
+
// A merged install saved the user's original statusline; give it back
|
|
928
|
+
// instead of leaving them with none.
|
|
929
|
+
const cfg = loadConfig();
|
|
930
|
+
if (cfg.statuslineBase) {
|
|
931
|
+
settings.statusLine = cfg.statuslineBase;
|
|
932
|
+
delete cfg.statuslineBase;
|
|
933
|
+
saveConfig(cfg);
|
|
934
|
+
result.removed.push('statusLine (restored previous)');
|
|
935
|
+
} else {
|
|
936
|
+
delete settings.statusLine;
|
|
937
|
+
result.removed.push('statusLine');
|
|
938
|
+
}
|
|
915
939
|
} else if (cur) {
|
|
916
940
|
result.kept.push(`statusLine (${cur.command || 'unrecognised'})`);
|
|
917
941
|
}
|
package/src/prompt.js
CHANGED
|
@@ -50,3 +50,31 @@ export function confirm(question, { defaultValue = true, input = process.stdin,
|
|
|
50
50
|
rl.on('close', () => { if (!answered) resolve(defaultValue); });
|
|
51
51
|
});
|
|
52
52
|
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Ask a multiple-choice question and resolve to the chosen `key`.
|
|
56
|
+
*
|
|
57
|
+
* Choices are printed as a numbered list; the answer may be the 1-based number
|
|
58
|
+
* or the key itself. Like confirm(), anything unrecognized (empty line, typo,
|
|
59
|
+
* closed stream) resolves to the default, so an unattended prompt ends with
|
|
60
|
+
* the same choice the non-interactive path would have made.
|
|
61
|
+
*/
|
|
62
|
+
export function choose(question, choices, { defaultIndex = 0, defaultHint = '(default)', input = process.stdin, output = process.stdout } = {}) {
|
|
63
|
+
const fallback = choices[defaultIndex]?.key ?? choices[0].key;
|
|
64
|
+
const lines = choices.map((c, i) => ` ${i + 1}) ${c.label}${i === defaultIndex ? ` ${defaultHint}` : ''}`);
|
|
65
|
+
return new Promise((resolve) => {
|
|
66
|
+
const rl = createInterface({ input, output });
|
|
67
|
+
let answered = false;
|
|
68
|
+
output.write(`${question}\n${lines.join('\n')}\n`);
|
|
69
|
+
rl.question(` [1-${choices.length}] `, (answer) => {
|
|
70
|
+
answered = true;
|
|
71
|
+
rl.close();
|
|
72
|
+
const a = String(answer).trim().toLowerCase();
|
|
73
|
+
const n = Number(a);
|
|
74
|
+
if (Number.isInteger(n) && n >= 1 && n <= choices.length) return resolve(choices[n - 1].key);
|
|
75
|
+
const byKey = choices.find((c) => String(c.key).toLowerCase() === a);
|
|
76
|
+
resolve(byKey ? byKey.key : fallback);
|
|
77
|
+
});
|
|
78
|
+
rl.on('close', () => { if (!answered) resolve(fallback); });
|
|
79
|
+
});
|
|
80
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* statusline-base — run the user's original statusline underneath ours.
|
|
3
|
+
*
|
|
4
|
+
* `sprag install` can merge with a statusline that was already configured: the
|
|
5
|
+
* original settings.json entry is saved as config `statuslineBase`, and each
|
|
6
|
+
* refresh prints that command's output first, sprag's lines after it.
|
|
7
|
+
* The statusline runs every few seconds, so nothing here may throw or stall.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { spawnSync } from 'node:child_process';
|
|
11
|
+
import { loadConfig } from './config.js';
|
|
12
|
+
import { readStdinRaw } from './stdin-payload.js';
|
|
13
|
+
|
|
14
|
+
// A light name check instead of importing installer.js (heavy for a hot path).
|
|
15
|
+
// If the saved base were ourselves, running it would recurse without end.
|
|
16
|
+
const OURS = /(^|[\s/\\])(sprag|claude-token-saver)(-cli)?(\.cmd|\.exe|\.ps1)?(\s|$)/i;
|
|
17
|
+
|
|
18
|
+
// A wrapper script saved as the base may itself call `sprag --statusline`;
|
|
19
|
+
// that inner run must not start the base again, so the child carries a marker.
|
|
20
|
+
const NESTED = 'SPRAG_STATUSLINE_BASE';
|
|
21
|
+
|
|
22
|
+
export function withBaseStatusline(output) {
|
|
23
|
+
if (process.env[NESTED] === '1') return output;
|
|
24
|
+
try {
|
|
25
|
+
const cmd = loadConfig().statuslineBase?.command;
|
|
26
|
+
if (typeof cmd !== 'string' || !cmd.trim() || OURS.test(cmd)) return output;
|
|
27
|
+
const opts = { input: readStdinRaw(), encoding: 'utf8', timeout: 1500, env: { ...process.env, [NESTED]: '1' } };
|
|
28
|
+
const r = process.platform === 'win32'
|
|
29
|
+
? spawnSync(cmd, { ...opts, shell: true })
|
|
30
|
+
: spawnSync('/bin/sh', ['-c', cmd], opts);
|
|
31
|
+
if (r.error || r.status !== 0) return output;
|
|
32
|
+
const base = String(r.stdout || '').replace(/[\r\n]+$/, '');
|
|
33
|
+
return base ? `${base}\n${output}` : output;
|
|
34
|
+
} catch {
|
|
35
|
+
return output;
|
|
36
|
+
}
|
|
37
|
+
}
|
package/src/stdin-payload.js
CHANGED
|
@@ -25,10 +25,27 @@ import { readFileSync } from 'node:fs';
|
|
|
25
25
|
* Anthropic adds (e.g. a Sonnet-only weekly bucket) flows through without code
|
|
26
26
|
* changes — known keys get curated labels, unknowns get derived ones.
|
|
27
27
|
*/
|
|
28
|
+
// fd 0 can only be drained once. The statusline needs the payload twice: once
|
|
29
|
+
// to parse it, and once to forward it verbatim to a wrapped base statusline
|
|
30
|
+
// command, so the raw text is read on first use and cached for the process.
|
|
31
|
+
let rawCache = null;
|
|
32
|
+
|
|
33
|
+
/** The raw stdin text, read once. '' when stdin is a TTY or empty. */
|
|
34
|
+
export function readStdinRaw() {
|
|
35
|
+
if (rawCache !== null) return rawCache;
|
|
36
|
+
if (process.stdin.isTTY) return (rawCache = '');
|
|
37
|
+
try {
|
|
38
|
+
rawCache = readFileSync(0, 'utf8') || '';
|
|
39
|
+
} catch {
|
|
40
|
+
rawCache = '';
|
|
41
|
+
}
|
|
42
|
+
return rawCache;
|
|
43
|
+
}
|
|
44
|
+
|
|
28
45
|
export function readStdinJson() {
|
|
29
46
|
if (process.stdin.isTTY) return null;
|
|
30
47
|
try {
|
|
31
|
-
const raw =
|
|
48
|
+
const raw = readStdinRaw();
|
|
32
49
|
if (!raw || !raw.trim()) return null;
|
|
33
50
|
return JSON.parse(raw);
|
|
34
51
|
} catch {
|