sprag-cli 3.56.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.
Files changed (53) hide show
  1. package/README.ko.md +60 -40
  2. package/README.md +63 -41
  3. package/bin/cli.js +65 -19
  4. package/bin/postinstall.js +22 -0
  5. package/docs/CODEX.ko.md +5 -1
  6. package/docs/CODEX.md +5 -0
  7. package/package.json +14 -26
  8. package/src/agents.js +16 -2
  9. package/src/brief.js +17 -8
  10. package/src/claude-price.js +23 -2
  11. package/src/codex-brief.js +35 -2
  12. package/src/codex-delegation.js +12 -4
  13. package/src/codex-harness.js +43 -9
  14. package/src/codex-hooks.js +24 -3
  15. package/src/codex-ledger.js +61 -5
  16. package/src/codex-panel.js +42 -22
  17. package/src/codex-rule-health.js +24 -1
  18. package/src/commands/brief.js +14 -3
  19. package/src/commands/codex-delegate.js +7 -4
  20. package/src/commands/codex.js +2 -0
  21. package/src/commands/harness.js +6 -0
  22. package/src/commands/install.js +24 -13
  23. package/src/compact-window.js +12 -4
  24. package/src/config.js +3 -3
  25. package/src/cost.js +5 -2
  26. package/src/ctx-window-cache.js +61 -0
  27. package/src/delegation-guard.js +15 -9
  28. package/src/doc2md.cjs +22 -8
  29. package/src/fig2md.cjs +7 -1
  30. package/src/formatters/statusline.js +4 -4
  31. package/src/formatters/table.js +18 -10
  32. package/src/gateway-auth.js +31 -3
  33. package/src/harness.js +43 -12
  34. package/src/history.js +6 -2
  35. package/src/hook-manager.js +54 -32
  36. package/src/installer.js +315 -158
  37. package/src/korean-lint.cjs +40 -7
  38. package/src/litellm-budget.js +10 -1
  39. package/src/model-rules.js +88 -41
  40. package/src/month-spend.js +17 -11
  41. package/src/parser.js +232 -56
  42. package/src/postinstall.js +32 -0
  43. package/src/prompt.js +28 -0
  44. package/src/route-caps.js +74 -0
  45. package/src/route-inject.js +31 -7
  46. package/src/route-scan.js +78 -26
  47. package/src/savings-ledger.js +33 -5
  48. package/src/seed-rules.js +3 -4
  49. package/src/session-cache.js +35 -1
  50. package/src/state-file.js +43 -0
  51. package/src/stats.js +20 -5
  52. package/src/statusline-base.js +37 -0
  53. package/src/stdin-payload.js +36 -5
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
- ## Codex
39
+ ## 설치하면 바로 달라지는 것
40
40
 
41
- Codex 기본 훅과 `AGENTS.md` 하네스, 범위별 래칫 규칙,
42
- 한국어 검사, 프롬프트의 문서 경로 자동 변환, 로컬 토큰 보고서를 사용할 수 있습니다.
43
- Codex가 설치되어 있으면 `npm i -g sprag-cli` 하나로 Codex도 설정하고, 추천 룰을 묻지 않고
44
- 전역으로 등록합니다(단계마다 확인하려면 `sprag install --manual`). Claude Code를 건드리지 않고
45
- Codex만 쓴다면 다음과 같이 설치하십시오.
41
+ 1. `npm i -g sprag-cli` 를 실행합니다. 훅과 상태줄, 추천 룰이 모든 프로젝트에
42
+ 설정되므로 프로젝트마다 따로 설정할 것은 없습니다. 이미 쓰던 상태줄이 있으면
43
+ 둘 다 표시할지, 교체할지, 그대로 둘지 설치 중에 묻습니다.
44
+ 2. 아무 프로젝트에서나 Claude Code를 열고 평소처럼 일합니다.
45
+ 3. 상태줄을 봅니다. 세션에서 쓴 금액과 아낀 금액이 나오고, 캐시나 컨텍스트,
46
+ 사용량 한도에 문제가 생기면 경고 칩이 줄 맨 앞에 나타납니다.
46
47
 
47
- ```bash
48
- npm i -g sprag-cli --ignore-scripts
49
- sprag install --agent codex
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
- 설치 후 Codex의 `/hooks`에서 새 훅을 검토하고 신뢰해야 실행됩니다. Codex가 요구하는
59
- 절차입니다. 명령의 기본 대상은 여전히 Claude Code이며, `--agent codex`를 붙이면 Codex를 고릅니다. Codex에서도 세션 브리핑, 경고 이력, 인수인계
60
- 파일을 지원합니다. `sprag delegate on --agent codex`로 모델 위임과 하위 에이전트
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
- ![statusline 예시. 첫 줄은 라우팅 절감액, 둘째 줄은 문서 변환 절감액, 셋째 줄은 진단 칩입니다](https://raw.githubusercontent.com/rootstudioyaml/sprag/main/docs/statusline.png)
57
+ Codex를 쓴다면 아래 [Codex](#codex) 절을 보십시오.
83
58
 
84
59
  ## 작동 방식
85
60
 
@@ -181,11 +156,56 @@ 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
+ ![statusline 예시. 첫 줄은 라우팅 절감액, 둘째 줄은 문서 변환 절감액, 셋째 줄은 진단 칩입니다](https://raw.githubusercontent.com/rootstudioyaml/sprag/main/docs/statusline.png)
203
+
184
204
  ## 자주 쓰는 명령
185
205
 
186
206
  | 명령 | 하는 일 |
187
207
  |---|---|
188
- | `sprag` | 최근 1일 진단 리포트 |
208
+ | `sprag` | 최근 30일 진단 리포트 |
189
209
  | `sprag handoff` | 한도에 막히기 전에 작업 상태를 백업 |
190
210
  | `sprag route-scan` | 내 기록에서 위임 후보 찾기 (LLM 호출 0회) |
191
211
  | `sprag route-scan savings` | 라우팅 절감 원장 조회 |
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
- ## Codex
51
+ ## After you install
52
52
 
53
- Native hooks, an `AGENTS.md` harness, scoped ratchet rules, Korean lint,
54
- document conversion, token reports, and a session-bound companion panel.
55
- `npm i -g sprag-cli` sets Codex up too when it is installed, with the rule
56
- presets registered globally and nothing asked (`sprag install --manual` asks at
57
- each step). For a Codex-only install that leaves Claude Code alone:
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
- ```bash
60
- npm i -g sprag-cli --ignore-scripts
61
- sprag install --agent codex
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
- Review and trust the new hooks in Codex's `/hooks` menu; Codex requires that
71
- step. Commands still default to Claude Code; add `--agent codex` to pick Codex.
72
- Codex also supports session briefings, warning history, handoffs, and opt-in
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
- ![statusline example: routing savings on row 1, document conversion savings on row 2, diagnostics on row 3](https://raw.githubusercontent.com/rootstudioyaml/sprag/main/docs/statusline.png)
71
+ Using Codex? See [Codex](#codex) below.
96
72
 
97
73
  ## How it works
98
74
 
@@ -225,11 +201,57 @@ 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
+ ![statusline example: routing savings on row 1, document conversion savings on row 2, diagnostics on row 3](https://raw.githubusercontent.com/rootstudioyaml/sprag/main/docs/statusline.png)
249
+
228
250
  ## Day-to-day commands
229
251
 
230
252
  | Command | What it does |
231
253
  |---|---|
232
- | `sprag` | Last-1-day diagnostic report |
254
+ | `sprag` | Last-30-day diagnostic report |
233
255
  | `sprag handoff` | Back work up to markdown before a cap blocks you |
234
256
  | `sprag route-scan` | Find delegation candidates in your own history (0 LLM calls) |
235
257
  | `sprag route-scan savings` | The routing-savings ledger |
package/bin/cli.js CHANGED
@@ -36,12 +36,13 @@ import {
36
36
  extractContextUsage,
37
37
  extractEffort,
38
38
  extractModel,
39
+ extractSessionId,
39
40
  extractTranscriptPath,
40
41
  } from '../src/stdin-payload.js';
41
42
 
42
43
  import { effortChipLevel } from '../src/ultracode.js';
43
44
 
44
- import { parseAllSessions, getLastUserMessageTime } from '../src/parser.js';
45
+ import { parseAllSessions, sessionsSince, getLastUserMessageTime } from '../src/parser.js';
45
46
  import {
46
47
  dailyTrend,
47
48
  ttlBreakdown,
@@ -92,6 +93,15 @@ function readUpdateChip() {
92
93
  }
93
94
  }
94
95
 
96
+ /**
97
+ * Whether the table report may use ANSI codes. The formatter looked at NO_COLOR
98
+ * alone, so `--no-color` did nothing there and a piped report (`sprag | less`,
99
+ * a redirect to a file) carried escape codes into its output.
100
+ */
101
+ function tableColor() {
102
+ return !hasFlag('--no-color') && !process.env.NO_COLOR && !!process.stdout.isTTY;
103
+ }
104
+
95
105
  // Subcommands this build knows how to run. Used only by the guard below.
96
106
  const KNOWN_SUBCOMMANDS = new Set([
97
107
  'last', 'brief', 'history', 'handoff', 'install', 'uninstall', 'mode', 'korean', 'cohesion',
@@ -131,6 +141,16 @@ Usage:
131
141
  sprag last most recent warning + how to handle it
132
142
  sprag history recent warning transitions
133
143
  sprag handoff write a session handoff file
144
+ sprag mode [keywords] statusline preferences (icon/text, en/ko, window, ttl)
145
+ sprag seed register the bundled starter rules, one answer at a time
146
+ sprag korean on|off|status Korean writing guidance + write-time check
147
+ sprag cohesion on|off|status English sentence-connection guidance
148
+ sprag doc2md on|off|<file> convert documents to Markdown before the model reads them
149
+ sprag delegate on|off|status add bounds and context to Task/Agent delegation prompts
150
+ sprag compact-window audit or pin autoCompactWindow for 1M sessions
151
+ sprag litellm-budget LiteLLM gateway key budget and spend
152
+ sprag update-check is a newer version out?
153
+ sprag brief --hook internal: the UserPromptSubmit hook entry point
134
154
  sprag feedback "<msg>" file a bug report / feature request (no browser needed)
135
155
  sprag upgrade install the latest release
136
156
  sprag --install-hook install cache-monitor PostToolUse hook
@@ -434,7 +454,7 @@ async function main() {
434
454
  const { buildTableDemoData } = await import('../src/demo.js');
435
455
  const { formatReport } = await import('../src/formatters/table.js');
436
456
  const data = buildTableDemoData({ version: PKG_VERSION });
437
- console.log(formatReport(data));
457
+ console.log(formatReport(data, { color: tableColor() }));
438
458
  return;
439
459
  }
440
460
 
@@ -529,7 +549,10 @@ async function main() {
529
549
  const excludeSessionPath =
530
550
  getArg('--exclude-session') || process.env.CACHE_MONITOR_EXCLUDE_SESSION || undefined;
531
551
 
532
- const sessions = await parseAllSessions({ days, projectFilter });
552
+ // The statusline also reports this month's spend. Naming the month start here
553
+ // lets the one pass over the transcripts trim boundary sessions to it too.
554
+ const monthSince = isStatusline ? (await import('../src/month-spend.js')).monthStartMs() : undefined;
555
+ const sessions = await parseAllSessions({ days, projectFilter, sinceMs: monthSince });
533
556
 
534
557
  if (sessions.length === 0) {
535
558
  // Statusline must always emit a single line (no multi-line help spam every
@@ -552,16 +575,26 @@ async function main() {
552
575
  // Resolved the same way as on the populated line below, so the chip does
553
576
  // not change its mind about the level when the analysis window empties.
554
577
  const effort = effortChipLevel(extractEffort(stdinJson), extractTranscriptPath(stdinJson));
578
+ // Recorded here too: a fresh session can render before it has any request
579
+ // in the analysis window, and its first prompt is briefed from this.
580
+ const ctxLive = extractContextUsage(stdinJson);
581
+ if (ctxLive?.size) {
582
+ try {
583
+ const { recordLiveWindow } = await import('../src/ctx-window-cache.js');
584
+ recordLiveWindow(extractSessionId(stdinJson), ctxLive.size);
585
+ } catch (e) { debug('ctx-window-cache:record', e); }
586
+ }
555
587
  if (caps || model) {
556
588
  try {
557
589
  const { persistSnapshot } = await import('../src/caps-cache.js');
558
590
  persistSnapshot({ caps, model });
559
591
  } catch (e) { debug('caps-cache:persist', e); }
560
592
  }
561
- console.log(formatNoSession(
593
+ const { withBaseStatusline } = await import('../src/statusline-base.js');
594
+ console.log(withBaseStatusline(formatNoSession(
562
595
  { caps, model, effort, windowLabel, version: PKG_VERSION, update: readUpdateChip() },
563
596
  { color: colorOk, mode: labelMode },
564
- ));
597
+ )));
565
598
  return;
566
599
  }
567
600
  // JSON mode stays JSON. A caller that asked for machine-readable output
@@ -618,6 +651,16 @@ async function main() {
618
651
  ? effortChipLevel(extractEffort(stdinJson), extractTranscriptPath(stdinJson))
619
652
  : extractEffort(stdinJson);
620
653
  const ctxLive = extractContextUsage(stdinJson);
654
+ // The prompt-submit briefing has no payload field for the window, so it reads
655
+ // back what this render was told (see src/ctx-window-cache.js).
656
+ if (isStatusline && ctxLive?.size) {
657
+ try {
658
+ const { recordLiveWindow } = await import('../src/ctx-window-cache.js');
659
+ recordLiveWindow(extractSessionId(stdinJson), ctxLive.size);
660
+ } catch (e) {
661
+ debug('ctx-window-cache:record', e);
662
+ }
663
+ }
621
664
  if (isStatusline && (caps || model)) {
622
665
  try {
623
666
  const { persistSnapshot } = await import('../src/caps-cache.js');
@@ -730,28 +773,23 @@ async function main() {
730
773
  if (isStatusline) {
731
774
  try {
732
775
  const { monthSpend, monthStartMs } = await import('../src/month-spend.js');
733
- const daysNeeded = (Date.now() - monthStartMs()) / 86400000;
776
+ const since = monthStartMs();
777
+ const daysNeeded = (Date.now() - since) / 86400000;
734
778
  const monthSessions = days >= daysNeeded
735
779
  ? sessions
736
- : await parseAllSessions({ days: Math.max(1, Math.ceil(daysNeeded)), projectFilter });
737
- monthSpendInfo = monthSpend(monthSessions);
780
+ : await parseAllSessions({ days: Math.max(1, Math.ceil(daysNeeded)), projectFilter, sinceMs: since });
781
+ // 월 경계에 걸친 세션은 월초 이후 요청만 남긴다. 그러지 않으면 1일 자정을
782
+ // 넘긴 세션의 전월분이 이번 달 지출에 그대로 들어온다.
783
+ monthSpendInfo = monthSpend(await sessionsSince(monthSessions, since));
738
784
  } catch (e) {
739
785
  debug('month-spend', e);
740
786
  }
741
787
  }
742
788
 
743
- // What delegation has measurably saved, read straight from the registry
744
- // route-scan maintains. A lookup, never a scan: the statusline re-renders
745
- // every few seconds and a scan parses tens of MB of transcripts.
746
- let delegationSaved = 0;
747
- try {
748
- const { delegationSavedUsd } = await import('../src/model-rules.js');
749
- delegationSaved = delegationSavedUsd();
750
- } catch (e) {
751
- debug('model-rules:saved', e); // an unreadable registry just hides the chip
752
- }
753
789
  // Rolling week/month/lifetime totals from the delegation ledger — the
754
790
  // statusline's headline line. Falls back to null (chip hidden) on any error.
791
+ // A lookup, never a scan: the statusline re-renders every few seconds and a
792
+ // scan parses tens of MB of transcripts.
755
793
  let delegationTotals = null;
756
794
  try {
757
795
  const { delegationSavedTotals } = await import('../src/savings-ledger.js');
@@ -759,6 +797,10 @@ async function main() {
759
797
  } catch (e) {
760
798
  debug('savings-ledger:totals', e);
761
799
  }
800
+ // The single-line layout shows the same lifetime figure as the headline
801
+ // line. It used to sum the rule registry instead, a scan-window snapshot
802
+ // priced against another baseline, so one label carried two amounts.
803
+ const delegationSaved = Number(delegationTotals?.total) || 0;
762
804
  // Document conversions, same shape as the delegation totals: a lifetime sum
763
805
  // plus a document count. A lookup of a small JSON file, never a scan.
764
806
  let doc2mdTotals = null;
@@ -857,9 +899,13 @@ async function main() {
857
899
  });
858
900
  } else {
859
901
  const { formatReport } = await import('../src/formatters/table.js');
860
- output = formatReport(data);
902
+ output = formatReport(data, { color: tableColor() });
861
903
  }
862
904
 
905
+ if (format === 'statusline') {
906
+ const { withBaseStatusline } = await import('../src/statusline-base.js');
907
+ output = withBaseStatusline(output);
908
+ }
863
909
  console.log(output);
864
910
  }
865
911
 
@@ -0,0 +1,22 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * npm `postinstall` entry point: runs `sprag install` for a global install and
5
+ * says why it did not for anything else. See src/postinstall.js.
6
+ */
7
+
8
+ import { spawnSync } from 'node:child_process';
9
+ import { fileURLToPath } from 'node:url';
10
+ import { dirname, join } from 'node:path';
11
+ import { postinstallDecision } from '../src/postinstall.js';
12
+
13
+ const decision = postinstallDecision(process.env);
14
+ if (!decision.run) {
15
+ console.log(`sprag: setup skipped (${decision.reason}). Run \`sprag install\` to register the hooks and the statusline.`);
16
+ process.exit(0);
17
+ }
18
+ const cli = join(dirname(fileURLToPath(import.meta.url)), 'cli.js');
19
+ spawnSync(process.execPath, [cli, 'install'], { stdio: 'inherit' });
20
+ // A failed setup must not fail the package install: the CLI is still usable
21
+ // and `sprag install` can be run again by hand.
22
+ process.exit(0);
package/docs/CODEX.ko.md CHANGED
@@ -114,7 +114,11 @@ Codex 0.159.2에서는 `spawn_agent`가 `PreToolUse`를 거치지 않습니다(
114
114
  "사용자가 위임을 요청했다"는 뜻으로 읽게 합니다. 자식은 `fork_turns "none"`으로 뜨고, 부모는
115
115
  같은 일을 직접 하지 않고 `wait_agent`(제한 시간 120초 이상)로 자식이 끝나기를 기다립니다. 귀속은
116
116
  자식이 route id를 되받아 적는 것에 기대지 않습니다. 대신 `SubagentStart`가 대기 중인 기록(같은
117
- 부모 세션, 같은 목표 모델, 30분 이내)에 새로 뜬 자식을 매칭해 묶습니다.
117
+ 부모 세션, 같은 목표 모델, 30분 이내)에 새로 뜬 자식을 매칭해 묶습니다. 어떤 자식도 가져가지
118
+ 않은 안내는 부모 세션에 다음 프롬프트가 들어올 때 닫히므로, 나중에 다른 이유로 뜬 자식이 그
119
+ 안내의 절감으로 잡히지 않습니다. 같은 프롬프트 시점에 아직 장부에 반영되지 않은 위임 실행도
120
+ 별도 프로세스로 계산하므로, `Routing saved`는 다음 route-scan을 기다리지 않고 위임 뒤 첫
121
+ 프롬프트에서 갱신됩니다.
118
122
 
119
123
  이 안내는 부모 세션의 컨텍스트가 `codex.delegateMinContext`(기본 60000 토큰) 아래면 붙지
120
124
  않습니다. 2026-09-30 실측에서 작은 세션은 스폰·대기·검증이라는 조율 비용이 자식이 아끼는
package/docs/CODEX.md CHANGED
@@ -166,6 +166,11 @@ with `fork_turns "none"`, then wait for it (`wait_agent` with a timeout of at le
166
166
  route id back: `SubagentStart` binds the id to whichever spawned child matches
167
167
  the pending route (same parent thread, same target model, within 30 minutes),
168
168
  so a route is credited even when the child's own first message never repeats it.
169
+ A hint that no child claimed is closed when the parent's next prompt arrives, so
170
+ a child spawned later for some other reason is not credited to it. That same
171
+ prompt also prices any delegated run the ledger has not joined yet, in a
172
+ detached process, so `Routing saved` follows a delegation by one prompt instead
173
+ of waiting for the next route scan.
169
174
 
170
175
  The hint is withheld below `codex.delegateMinContext` (default 60000 input
171
176
  tokens, read from the parent's own rollout). Measured 2026-09-30: a small
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "sprag-cli",
3
- "version": "3.56.0",
4
- "description": "Cut the tokens Claude Code and Codex spend: routes simple work to cheaper models, converts documents to compact Markdown, and shows spend and savings live. Ratchet rules and Korean writing checks included.",
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",
@@ -10,7 +10,7 @@
10
10
  "scripts": {
11
11
  "deploy": "node scripts/deploy.mjs",
12
12
  "docs:statusline": "node scripts/docs-statusline.mjs",
13
- "postinstall": "node bin/cli.js install || true",
13
+ "postinstall": "node bin/postinstall.js || true",
14
14
  "publish:legacy": "node scripts/publish-legacy.mjs",
15
15
  "release:notes": "node scripts/release-notes.mjs",
16
16
  "site:docs": "node scripts/build-site-docs.mjs",
@@ -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
- "codex",
43
- "openai-codex",
44
- "agents-md",
37
+ "claude",
45
38
  "anthropic",
46
- "model-routing",
47
- "llm-router",
48
- "delegation",
49
- "subagent",
50
- "haiku",
51
- "cost-savings",
39
+ "codex",
40
+ "token-usage",
52
41
  "token-saver",
53
42
  "reduce-tokens",
54
- "token-usage",
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
- "model-fitting",
63
- "agent-harness"
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/agents.js CHANGED
@@ -20,12 +20,26 @@ const AGENT_MODEL = {
20
20
  'haiku-runner': 'haiku',
21
21
  'haiku-translate': 'haiku',
22
22
  'sonnet-worker': 'sonnet',
23
+ // A T1 rule names its target by tier alone (`agent: 'sonnet'`), with no
24
+ // preset agent behind it. Without these the phrase fell through to the
25
+ // haiku default, and a sonnet rule told the session to delegate to haiku.
26
+ sonnet: 'sonnet',
27
+ haiku: 'haiku',
23
28
  };
24
29
 
25
- /** True when `<name>.md` exists in the project or user agents directory. */
30
+ /**
31
+ * True when `<name>.md` exists in the project or user agents directory.
32
+ *
33
+ * `root: null` asks about the user directory alone. Text that every project
34
+ * reads (the global ratchet-model.md) has to be worded from what every project
35
+ * can see: checked against the current directory, the same rule read
36
+ * `haiku-explore(model: haiku)` when the scan ran in a project that defines
37
+ * the agent and `model: haiku` when it ran anywhere else, and each flip
38
+ * rewrote a file sitting in the prompt-cache prefix of every session.
39
+ */
26
40
  export function agentExists(name, root = process.cwd()) {
27
41
  if (!name) return false;
28
- return existsSync(join(root, '.claude', 'agents', `${name}.md`)) ||
42
+ return (root !== null && existsSync(join(root, '.claude', 'agents', `${name}.md`))) ||
29
43
  existsSync(join(claudeUserDir(), 'agents', `${name}.md`));
30
44
  }
31
45