sprag-cli 3.40.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 (79) hide show
  1. package/LICENSE +21 -0
  2. package/README.ko.md +637 -0
  3. package/README.md +758 -0
  4. package/bin/cli.js +801 -0
  5. package/examples/statusline-command.ps1 +43 -0
  6. package/examples/statusline-command.sh +36 -0
  7. package/package.json +62 -0
  8. package/presets/cohesion/cohesion-en.md +26 -0
  9. package/presets/doc2md/convert.py +363 -0
  10. package/presets/korean-style/LICENSE-fluent-korean +21 -0
  11. package/presets/korean-style/fluent-korean.md +52 -0
  12. package/presets/korean-style/supplement.md +93 -0
  13. package/presets/model-rules.json +115 -0
  14. package/presets/ratchet-rules.json +38 -0
  15. package/src/advice.js +564 -0
  16. package/src/agents.js +52 -0
  17. package/src/brief.js +264 -0
  18. package/src/caps-cache.js +84 -0
  19. package/src/cli-args.js +51 -0
  20. package/src/cohesion.js +70 -0
  21. package/src/commands/brief.js +31 -0
  22. package/src/commands/cohesion.js +59 -0
  23. package/src/commands/compact-window.js +93 -0
  24. package/src/commands/doc2md.js +166 -0
  25. package/src/commands/feedback.js +132 -0
  26. package/src/commands/handoff.js +33 -0
  27. package/src/commands/harness.js +459 -0
  28. package/src/commands/history.js +46 -0
  29. package/src/commands/install.js +358 -0
  30. package/src/commands/korean.js +220 -0
  31. package/src/commands/last.js +151 -0
  32. package/src/commands/mode.js +46 -0
  33. package/src/commands/route-scan.js +454 -0
  34. package/src/commands/seed.js +105 -0
  35. package/src/commands/uninstall.js +42 -0
  36. package/src/commands/update-check.js +77 -0
  37. package/src/commands/upgrade.js +68 -0
  38. package/src/compact-window.js +205 -0
  39. package/src/config.js +232 -0
  40. package/src/cost.js +253 -0
  41. package/src/debug.js +29 -0
  42. package/src/demo.js +331 -0
  43. package/src/doc2md-ledger.cjs +227 -0
  44. package/src/doc2md.cjs +997 -0
  45. package/src/fig2md-runner.cjs +21 -0
  46. package/src/fig2md.cjs +191 -0
  47. package/src/first-run-note.js +63 -0
  48. package/src/format-time.js +44 -0
  49. package/src/formatters/csv.js +8 -0
  50. package/src/formatters/json.js +3 -0
  51. package/src/formatters/statusline.js +750 -0
  52. package/src/formatters/table.js +299 -0
  53. package/src/handoff.js +161 -0
  54. package/src/harness-analyzer.cjs +264 -0
  55. package/src/harness-templates.js +153 -0
  56. package/src/harness.js +613 -0
  57. package/src/history.js +383 -0
  58. package/src/hook-manager.js +96 -0
  59. package/src/hook.cjs +196 -0
  60. package/src/installer.js +614 -0
  61. package/src/korean-lint.cjs +303 -0
  62. package/src/korean-style.js +187 -0
  63. package/src/litellm-budget.js +223 -0
  64. package/src/model-alias.js +484 -0
  65. package/src/model-rules.js +527 -0
  66. package/src/month-spend.js +47 -0
  67. package/src/parser.js +330 -0
  68. package/src/paths.js +41 -0
  69. package/src/prompt.js +52 -0
  70. package/src/route-scan.js +832 -0
  71. package/src/savings-ledger.js +137 -0
  72. package/src/seed-rules.js +280 -0
  73. package/src/session-cache.js +160 -0
  74. package/src/session-records.js +188 -0
  75. package/src/stats.js +380 -0
  76. package/src/stdin-payload.js +122 -0
  77. package/src/subagent-records.js +214 -0
  78. package/src/update-check.js +201 -0
  79. package/src/window-labels.js +64 -0
package/src/advice.js ADDED
@@ -0,0 +1,564 @@
1
+ /**
2
+ * Human-readable advice for each diagnostic issue code.
3
+ * Platform-specific commands are selected from process.platform.
4
+ */
5
+
6
+ function platformKind() {
7
+ if (process.platform === 'win32') return 'win';
8
+ // WSL shows up as 'linux' but $WSL_DISTRO_NAME is set — treat as linux either way.
9
+ return 'posix';
10
+ }
11
+
12
+ function disable1mEnvSnippet() {
13
+ if (platformKind() === 'win') {
14
+ return [
15
+ 'setx CLAUDE_CODE_DISABLE_1M_CONTEXT 1',
16
+ '(PowerShell) $env:CLAUDE_CODE_DISABLE_1M_CONTEXT = "1"',
17
+ ];
18
+ }
19
+ return [
20
+ "echo 'export CLAUDE_CODE_DISABLE_1M_CONTEXT=1' >> ~/.zshrc && source ~/.zshrc",
21
+ '(bash) echo \'export CLAUDE_CODE_DISABLE_1M_CONTEXT=1\' >> ~/.bashrc && source ~/.bashrc',
22
+ ];
23
+ }
24
+
25
+ function toggleShortcut() {
26
+ return platformKind() === 'win' ? 'Alt + P' : '⌥ P (mac) / Alt + P (linux)';
27
+ }
28
+
29
+ /**
30
+ * Each entry has parallel English / Korean fields. table.js only reads the
31
+ * English fields (kept stable so the existing report layout doesn't shift).
32
+ * `last` (bin/cli.js) renders both, English first then `└ Korean` continuation
33
+ * — same bilingual style as the history.md file.
34
+ *
35
+ * `commandsKo` is optional per action: when a command is a literal code
36
+ * snippet (e.g. `export CLAUDE_CODE_DISABLE_1M_CONTEXT=1`), there's nothing
37
+ * to translate, so it's fine to omit and fall back to `commands`. When the
38
+ * command is descriptive prose, provide a Korean version.
39
+ */
40
+ export const ISSUE_MESSAGES = {
41
+ LARGE_INPUT_PER_REQUEST: {
42
+ title: 'Per-request input tokens are unusually large (context past 200k)',
43
+ titleKo: '요청당 입력 토큰이 비정상적으로 큼 (컨텍스트 200k 초과)',
44
+ // Since Opus 4.7 there is NO long-context price premium — 1M is standard
45
+ // rate and the default window on current models. The cost driver is the
46
+ // token volume itself: a 500k-token context re-reads ~500k tokens every
47
+ // turn (cache-read billed) and burns the subscription 5H/7D windows
48
+ // several times faster. That's what this warning is about.
49
+ explain:
50
+ 'Current models default to a 1M window with no long-context premium — but the token ' +
51
+ 'volume itself is the cost: every turn re-reads the whole context (billed as cache reads) ' +
52
+ 'and drains the 5H/7D rate-limit windows several times faster. Cache reuse also drops.',
53
+ explainKo:
54
+ '현재 모델은 1M 윈도가 기본이고 장기 컨텍스트 프리미엄도 없습니다 — 하지만 토큰량 자체가 비용입니다. ' +
55
+ '매 턴 컨텍스트 전체를 다시 읽고(캐시 읽기 과금) 5H/7D 한도도 몇 배 빠르게 소모됩니다. ' +
56
+ '캐시 재사용률도 떨어집니다.',
57
+ actions: () => [
58
+ {
59
+ label: 'Compact or clear when context grows (first lever)',
60
+ labelKo: '컨텍스트가 커지면 /compact 또는 /clear (1순위)',
61
+ commands: [
62
+ '/compact — summarize the session history in place',
63
+ '/clear — drop history entirely at a clean task boundary',
64
+ ],
65
+ commandsKo: [
66
+ '/compact — 세션 히스토리를 그 자리에서 요약',
67
+ '/clear — 작업 분기점에서 히스토리를 통째로 비우기',
68
+ ],
69
+ },
70
+ {
71
+ label: 'Cap extended-thinking budget (⚠ check /effort first)',
72
+ labelKo: '확장 사고(thinking) 예산 제한 (⚠ /effort 먼저 확인)',
73
+ commands: [
74
+ '⚠ Run `/effort` to check current level — `xhigh` is the #1 cap killer',
75
+ '/effort medium — Anthropic-official default (use this for normal coding)',
76
+ '/effort low — for simple edits, labeling, boilerplate',
77
+ '/effort xhigh — ONLY for complex architecture / multi-file refactor planning, then revert',
78
+ 'export MAX_THINKING_TOKENS=8000 # global hard cap (overrides /effort)',
79
+ ],
80
+ commandsKo: [
81
+ '⚠ `/effort`로 현재 단계 확인 — `xhigh`가 캡 소진의 1순위 원인',
82
+ '/effort medium — Anthropic 공식 기본값 (일반 코딩은 이걸로)',
83
+ '/effort low — 단순 편집·라벨링·보일러플레이트',
84
+ '/effort xhigh — 복잡한 아키텍처·다파일 리팩터 계획 한정, 끝나면 즉시 복귀',
85
+ 'export MAX_THINKING_TOKENS=8000 # 전역 하드 캡 (/effort 위에 우선 적용)',
86
+ ],
87
+ },
88
+ {
89
+ label: 'Plan mode preemptively (Shift+Tab)',
90
+ labelKo: 'Plan 모드 선제 사용 (Shift+Tab)',
91
+ commands: [
92
+ 'Plan mode forces a written plan before any edits — prevents wrong-direction rework',
93
+ 'Rework after a misread spec costs more tokens than 1 extra plan turn',
94
+ ],
95
+ commandsKo: [
96
+ 'Plan 모드는 편집 전에 계획을 쓰게 강제 — 잘못된 방향 재작업 방지',
97
+ '스펙 오독 후 재작업이 plan 1턴 추가보다 훨씬 비쌈',
98
+ ],
99
+ },
100
+ {
101
+ label: 'Cap the window at 200k if you never need more (optional)',
102
+ labelKo: '더 큰 윈도가 필요 없으면 200k로 제한 (선택)',
103
+ commands: disable1mEnvSnippet().concat([
104
+ `Or press ${toggleShortcut()} to toggle in-session`,
105
+ ]),
106
+ commandsKo: disable1mEnvSnippet().concat([
107
+ `또는 ${toggleShortcut()}로 세션 내 즉시 토글`,
108
+ ]),
109
+ },
110
+ {
111
+ label: '⚠ Known bug #31640',
112
+ labelKo: '⚠ 알려진 버그 #31640',
113
+ commands: [
114
+ '/model 200k selection sometimes does not stick — context stays at 1M.',
115
+ 'To force off: set the env var above and restart Claude Code.',
116
+ ],
117
+ commandsKo: [
118
+ '/model 200k 선택이 가끔 적용되지 않음 — 컨텍스트가 1M으로 유지됨.',
119
+ '강제로 끄려면 위의 환경변수를 설정하고 Claude Code를 재시작.',
120
+ ],
121
+ },
122
+ ],
123
+ },
124
+ LOW_HIT_RATE: {
125
+ title: 'Cache hit rate is low',
126
+ titleKo: '캐시 적중률이 낮음',
127
+ explain:
128
+ 'A low hit rate means the same prompt prefix is being rewritten on every call, ' +
129
+ 'inflating input cost.',
130
+ explainKo:
131
+ '적중률이 낮다는 건 동일한 프롬프트 prefix가 매 호출마다 다시 쓰이고 있다는 뜻이며, ' +
132
+ '입력 비용을 부풀립니다.',
133
+ actions: () => [
134
+ {
135
+ label: 'Avoid opening fresh sessions too often',
136
+ labelKo: '새 세션을 너무 자주 열지 말기',
137
+ commands: [
138
+ 'Continue the same task in the same session (context switching = cache miss)',
139
+ 'Resume with `claude --continue` instead of starting fresh — preserves the prefix cache',
140
+ ],
141
+ commandsKo: [
142
+ '같은 작업은 같은 세션에서 계속 (컨텍스트 전환 = 캐시 미스)',
143
+ '새로 시작 대신 `claude --continue`로 재개 — prefix 캐시 보존',
144
+ ],
145
+ },
146
+ {
147
+ label: 'Stabilize the prompt prefix',
148
+ labelKo: '프롬프트 prefix 안정화',
149
+ commands: [
150
+ 'System prompts / tool definitions that change per request invalidate the cache every time',
151
+ 'Trim CLAUDE.md — every line ships on every turn; keep only durable rules',
152
+ ],
153
+ commandsKo: [
154
+ '요청마다 바뀌는 시스템 프롬프트 / 도구 정의는 매번 캐시를 무효화',
155
+ 'CLAUDE.md 다이어트 — 모든 줄이 매 턴 실림; 영속적인 규칙만 유지',
156
+ ],
157
+ },
158
+ ],
159
+ },
160
+ BUCKET_5M_DOMINANT: {
161
+ title: 'Most cache writes are landing in the 5-minute TTL bucket',
162
+ titleKo: '캐시 쓰기 대부분이 5분 TTL 버킷에 들어가고 있음',
163
+ explain:
164
+ 'Pro plan is locked to 5m TTL. Gaps longer than 5 minutes expire the cache and force ' +
165
+ 'a costly rebuild.',
166
+ explainKo:
167
+ 'Pro 플랜은 5분 TTL로 고정됩니다. 5분을 넘는 공백마다 캐시가 만료되고 비싼 재빌드가 강제됩니다.',
168
+ actions: () => [
169
+ {
170
+ label: 'The 5-minute rule',
171
+ labelKo: '5분 규칙',
172
+ commands: [
173
+ 'Sending any prompt within 5 minutes keeps the prefix cache warm',
174
+ 'Upgrade to Max for the 1h TTL bucket on long tasks',
175
+ ],
176
+ commandsKo: [
177
+ '5분 이내 어떤 프롬프트든 보내면 prefix 캐시가 유지됨',
178
+ '긴 작업은 Max 플랜으로 업그레이드해서 1시간 TTL 버킷 사용',
179
+ ],
180
+ },
181
+ ],
182
+ },
183
+ // Same symptom as BUCKET_5M_DOMINANT, different remedy. On Bedrock/Vertex
184
+ // the 5m bucket is the only bucket, so telling the user to upgrade a
185
+ // subscription plan sends them to buy something that changes nothing.
186
+ BUCKET_5M_DOMINANT_GATEWAY: {
187
+ title: 'This gateway offers only the 5-minute TTL bucket',
188
+ titleKo: '이 게이트웨이는 5분 TTL 버킷만 제공합니다',
189
+ explain:
190
+ 'Bedrock/Vertex do not expose the 1h extended bucket, and they do not report the ' +
191
+ 'per-bucket split either, so this is inferred from the model ids in the transcript. ' +
192
+ 'No subscription plan changes it.',
193
+ explainKo:
194
+ 'Bedrock과 Vertex는 1시간 확장 버킷을 제공하지 않으며, 버킷별 분해 값도 내려보내지 않습니다. ' +
195
+ '그래서 이 판정은 트랜스크립트에 남은 모델 ID로 추정한 것입니다. 구독 플랜을 바꾸어도 해소되지 않습니다.',
196
+ actions: () => [
197
+ {
198
+ label: 'What actually helps here',
199
+ labelKo: '이 환경에서 실제로 듣는 대응',
200
+ commands: [
201
+ 'Send the next request within 5 minutes — that window is all you get',
202
+ '`/compact` before stepping away, so the rebuild after expiry costs less',
203
+ ],
204
+ commandsKo: [
205
+ '다음 요청을 5분 안에 보내십시오. 이 환경에서 주어지는 창은 그것이 전부입니다.',
206
+ '작업을 중단하기 전에 `/compact`를 실행해 만료 후의 재빌드 비용을 낮추십시오.',
207
+ ],
208
+ },
209
+ ],
210
+ },
211
+ HIGH_OUTPUT_RATIO: {
212
+ title: 'Output share is abnormally high',
213
+ titleKo: '출력 비중이 비정상적으로 높음',
214
+ explain:
215
+ 'Output tokens are 5x+ pricier than input. Check whether the agent is regenerating ' +
216
+ 'long content unnecessarily.',
217
+ explainKo:
218
+ '출력 토큰은 입력보다 5배 이상 비쌉니다. 에이전트가 불필요하게 긴 컨텐츠를 ' +
219
+ '반복 생성하고 있지 않은지 확인하세요.',
220
+ actions: () => [
221
+ {
222
+ label: 'Cap output length',
223
+ labelKo: '출력 길이 줄이기',
224
+ commands: [
225
+ 'Avoid full-file rewrites — prefer the Edit tool',
226
+ 'Move long doc/README generation requests into scripts to shrink output',
227
+ ],
228
+ commandsKo: [
229
+ '파일 전체 재작성 피하기 — Edit 도구 우선 사용',
230
+ '긴 문서/README 생성 요청은 스크립트로 옮겨 출력 축소',
231
+ ],
232
+ },
233
+ {
234
+ label: 'Cap extended-thinking budget (⚠ check /effort)',
235
+ labelKo: '확장 사고(thinking) 예산 제한 (⚠ /effort 확인)',
236
+ commands: [
237
+ '⚠ Check `/effort` — `xhigh` burns thinking tokens that count as output',
238
+ '/effort medium — Anthropic-official default; switch back from xhigh',
239
+ 'export MAX_THINKING_TOKENS=8000 # global hard cap',
240
+ ],
241
+ commandsKo: [
242
+ '⚠ `/effort` 확인 — `xhigh`는 thinking 토큰을 다량 소비 (출력으로 집계)',
243
+ '/effort medium — Anthropic 공식 기본값; xhigh에서 복귀',
244
+ 'export MAX_THINKING_TOKENS=8000 # 전역 하드 캡',
245
+ ],
246
+ },
247
+ {
248
+ label: 'Model matching strategy (80/15/5 rule)',
249
+ labelKo: '모델 매칭 전략 (80/15/5 비율)',
250
+ commands: [
251
+ 'Sonnet 80% — daily coding, edits, refactors',
252
+ 'Opus 15% — complex design, multi-file refactor planning, deep debugging',
253
+ 'Haiku 5% — boilerplate, labeling, summaries, subagent work',
254
+ 'Switch with `/model sonnet` / `/model opus` / `/model haiku`',
255
+ ],
256
+ commandsKo: [
257
+ 'Sonnet 80% — 일상 코딩, 편집, 리팩터링',
258
+ 'Opus 15% — 복잡한 설계, 다파일 리팩터 계획, 깊은 디버깅',
259
+ 'Haiku 5% — 보일러플레이트, 라벨링, 요약, 서브에이전트 작업',
260
+ '`/model sonnet` / `/model opus` / `/model haiku`로 전환',
261
+ ],
262
+ },
263
+ ],
264
+ },
265
+ HIGH_REQUEST_COUNT: {
266
+ title: 'API calls in this session are 3x+ the baseline',
267
+ titleKo: '이번 세션의 API 호출이 평소 대비 3배 이상',
268
+ explain:
269
+ 'Excessive tool calls or retry/loop patterns rebroadcast the prefix on every call, ' +
270
+ 'spiking input cost.',
271
+ explainKo:
272
+ '과도한 도구 호출 또는 재시도/루프 패턴은 매 호출마다 prefix를 재전송해 입력 비용을 폭증시킵니다.',
273
+ actions: () => [
274
+ {
275
+ label: 'Parallel / batch processing',
276
+ labelKo: '병렬 / 배치 처리',
277
+ commands: ['Bundle independent investigations into one message with multiple tool calls'],
278
+ commandsKo: ['독립적인 조사 작업은 도구 호출 여러 개를 한 메시지로 묶어서 보내기'],
279
+ },
280
+ {
281
+ label: 'Watch for loops',
282
+ labelKo: '루프 감시',
283
+ commands: ['Check that the agent is not repeating the same test/search in a loop'],
284
+ commandsKo: ['에이전트가 동일한 테스트/검색을 루프로 반복하고 있지 않은지 확인'],
285
+ },
286
+ {
287
+ label: 'Delegate large searches to a subagent (Sonnet by default)',
288
+ labelKo: '대형 검색은 서브에이전트로 위임 (Sonnet 기본)',
289
+ commands: [
290
+ 'Use the Task tool with the Explore subagent for repo-wide searches',
291
+ 'Pin custom subagents to Sonnet/Haiku in their frontmatter: `model: sonnet`',
292
+ 'The subagent runs in its own context — main session keeps a clean prefix',
293
+ ],
294
+ commandsKo: [
295
+ '레포 전반 검색은 Task 도구의 Explore 서브에이전트로 위임',
296
+ '커스텀 서브에이전트는 frontmatter에 `model: sonnet` 명시 (Opus 자동 상속 방지)',
297
+ '서브에이전트는 자체 컨텍스트에서 실행 — 메인 세션 prefix가 깨끗하게 유지됨',
298
+ ],
299
+ },
300
+ {
301
+ label: 'Migrate MCP servers → Skills / CLI',
302
+ labelKo: 'MCP 서버 → Skills / CLI 전환',
303
+ commands: [
304
+ '30 MCP tools ≈ 3,600 tokens loaded every turn — measured by mcp2cli (HN 146pts)',
305
+ 'Replacing rarely-used MCPs with Skills (loaded on demand) or shell CLI cuts ~96%',
306
+ 'Audit `.mcp.json` and disable everything not used weekly',
307
+ ],
308
+ commandsKo: [
309
+ 'MCP 도구 30개 ≈ 매 턴 3,600 토큰 상시 로드 (mcp2cli 측정, HN 146pts)',
310
+ '드물게 쓰는 MCP를 Skills(필요 시 로드) 또는 셸 CLI로 대체하면 ~96% 절감',
311
+ '`.mcp.json` 점검해 주간에 안 쓰는 건 모두 비활성화',
312
+ ],
313
+ },
314
+ {
315
+ label: 'Trim hooks that inject context',
316
+ labelKo: '컨텍스트 주입 훅 정리',
317
+ commands: ['Disable noisy PreToolUse/PostToolUse hooks unless they earn their tokens'],
318
+ commandsKo: ['값을 못 하는 PreToolUse/PostToolUse 훅은 비활성화'],
319
+ },
320
+ ],
321
+ },
322
+ TTL_EXPIRY_IMMINENT: {
323
+ title: 'Cache TTL is about to expire',
324
+ titleKo: '캐시 TTL 만료 임박',
325
+ explain:
326
+ 'Once the TTL window closes, the entire prefix cache is discarded and must be rebuilt ' +
327
+ 'from scratch on the next call — paying full input cost again.',
328
+ explainKo:
329
+ 'TTL 창이 닫히면 prefix 캐시 전체가 폐기되고 다음 호출에서 처음부터 재구축해야 합니다 — ' +
330
+ '전체 입력 비용이 다시 청구됩니다.',
331
+ actions: () => [
332
+ {
333
+ label: 'Send any prompt within the TTL window',
334
+ labelKo: 'TTL 창 안에 어떤 프롬프트든 보내기',
335
+ commands: [
336
+ 'Even a short "." or summary request resets the TTL clock',
337
+ 'Pro plan TTL = 5 min; Max plan TTL = 1 hour',
338
+ ],
339
+ commandsKo: [
340
+ '"." 한 글자나 짧은 요약 요청만 해도 TTL 타이머가 리셋됨',
341
+ 'Pro 플랜 TTL = 5분; Max 플랜 TTL = 1시간',
342
+ ],
343
+ },
344
+ {
345
+ label: 'Use /compact before going idle',
346
+ labelKo: '자리를 비우기 전에 /compact 실행',
347
+ commands: [
348
+ '/compact summarizes the conversation in place and re-anchors the cache',
349
+ 'The compacted summary is much smaller — next rebuild is cheaper',
350
+ ],
351
+ commandsKo: [
352
+ '/compact는 대화를 그 자리에서 요약하고 캐시를 재고정',
353
+ '요약된 컨텍스트는 훨씬 작아서 다음 재빌드 비용도 줄어듦',
354
+ ],
355
+ },
356
+ {
357
+ label: 'Upgrade to Max for 1h TTL',
358
+ labelKo: 'Max 플랜으로 업그레이드해 1시간 TTL 확보',
359
+ commands: [
360
+ 'Pro plan is capped at 5-minute TTL — any break longer than that expires the cache',
361
+ 'Max plan uses the 1-hour extended TTL bucket by default',
362
+ ],
363
+ commandsKo: [
364
+ 'Pro 플랜은 5분 TTL 고정 — 5분 이상 공백이면 캐시 만료',
365
+ 'Max 플랜은 기본적으로 1시간 TTL 버킷을 사용',
366
+ ],
367
+ },
368
+ ],
369
+ },
370
+ CONTEXT_NEAR_LIMIT: {
371
+ title: 'Context window is approaching the limit',
372
+ titleKo: '컨텍스트 창이 한계에 근접',
373
+ explain:
374
+ 'As context grows, each turn becomes more expensive (the whole context is re-read every ' +
375
+ 'turn) and cache reuse efficiency drops. There is no price premium past 200k on current ' +
376
+ 'models — the token volume itself is the cost, and it drains the 5H/7D caps faster.',
377
+ explainKo:
378
+ '컨텍스트가 커질수록 매 턴 비용이 높아지고(전체 컨텍스트를 매 턴 다시 읽음) 캐시 재사용 효율이 ' +
379
+ '떨어집니다. 현재 모델은 200k 초과 프리미엄이 없습니다 — 토큰량 자체가 비용이며 5H/7D 한도도 ' +
380
+ '더 빠르게 소모됩니다.',
381
+ actions: () => [
382
+ {
383
+ label: '/compact at the next natural break',
384
+ labelKo: '다음 자연스러운 중단점에서 /compact',
385
+ commands: [
386
+ '/compact replaces the full conversation with a compact summary',
387
+ 'Run it before context hits the limit — not after (compaction itself costs tokens)',
388
+ ],
389
+ commandsKo: [
390
+ '/compact는 전체 대화를 압축 요약으로 교체',
391
+ '한계에 도달하기 전에 실행 — 이후엔 compact 자체도 비쌈',
392
+ ],
393
+ },
394
+ {
395
+ label: 'Re-attach only what you need after /clear',
396
+ labelKo: '/clear 후 필요한 것만 재첨부',
397
+ commands: [
398
+ '/clear resets context to zero — cheapest option when task scope has shifted',
399
+ 'Re-read only the files Claude needs right now, not everything from before',
400
+ ],
401
+ commandsKo: [
402
+ '/clear는 컨텍스트를 완전 초기화 — 작업 범위가 바뀌었을 때 가장 저렴',
403
+ '지금 필요한 파일만 다시 읽기, 이전 파일 전부 재로드 금지',
404
+ ],
405
+ },
406
+ {
407
+ label: 'Avoid 1M context unless required',
408
+ labelKo: '필요하지 않으면 1M 컨텍스트 비활성화',
409
+ commands: [
410
+ 'export CLAUDE_MODEL_CONTEXT=200000 # force 200k cap',
411
+ 'Max plan auto-promotes to 1M — disable if you don\'t need it',
412
+ ],
413
+ commandsKo: [
414
+ 'export CLAUDE_MODEL_CONTEXT=200000 # 200k로 고정',
415
+ 'Max 플랜은 자동으로 1M 승격 — 불필요하면 비활성화',
416
+ ],
417
+ },
418
+ ],
419
+ },
420
+ FREQUENT_CACHE_REBUILD: {
421
+ title: 'Cache writes outweigh cache reads',
422
+ titleKo: '캐시 쓰기가 읽기보다 많음',
423
+ explain:
424
+ 'The cache is being created but not reused. Common when sessions are short-lived or ' +
425
+ 'restarted after TTL expiry.',
426
+ explainKo:
427
+ '캐시가 만들어지지만 재사용되지 않고 있습니다. 세션이 짧거나 TTL 만료 후 재시작될 때 흔합니다.',
428
+ actions: () => [
429
+ {
430
+ label: 'Check session continuity',
431
+ labelKo: '세션 연속성 점검',
432
+ commands: ['Continue one task in one Claude Code session'],
433
+ commandsKo: ['하나의 작업은 하나의 Claude Code 세션에서 계속'],
434
+ },
435
+ {
436
+ label: 'Use /compact at task boundaries',
437
+ labelKo: '작업 분기점에서 /compact 사용',
438
+ commands: [
439
+ '/compact summarizes prior turns in place — keeps the session alive without re-reading everything',
440
+ 'Better than starting a fresh session, which forces a full prefix rebuild',
441
+ ],
442
+ commandsKo: [
443
+ '/compact는 이전 턴을 그 자리에서 요약 — 전체 재읽기 없이 세션 유지',
444
+ '새 세션을 여는 것보다 나음 (새 세션은 prefix 전체 재구축 강제)',
445
+ ],
446
+ },
447
+ {
448
+ label: 'Avoid re-reading large files',
449
+ labelKo: '큰 파일 반복 Read 피하기',
450
+ commands: [
451
+ 'Prefer `git diff` to see what changed instead of re-reading the whole file',
452
+ 'Edit returns success silently — no need to Read after editing',
453
+ ],
454
+ commandsKo: [
455
+ '파일 전체 다시 읽기 대신 `git diff`로 변경분만 확인',
456
+ 'Edit는 성공 시 조용히 끝남 — 편집 후 Read 다시 할 필요 없음',
457
+ ],
458
+ },
459
+ ],
460
+ },
461
+ };
462
+
463
+ /**
464
+ * One-line action tips per issue code — bilingual. Used by history.js to
465
+ * inline a "what to do" hint right below each warning event in the daily
466
+ * markdown file, so the file alone is enough to answer "what was the warning,
467
+ * and how do I fix it" without re-running the tool.
468
+ *
469
+ * The full multi-step advice still lives in `ISSUE_MESSAGES[code].actions()`;
470
+ * `claude-token-saver last` prints that long form on demand.
471
+ */
472
+ export const ISSUE_TIPS = {
473
+ LARGE_INPUT_PER_REQUEST: {
474
+ en: '`/compact` or `/clear` — big contexts re-bill every turn and burn the 5H/7D caps; check `/effort` (`xhigh` is the #1 cap killer)',
475
+ ko: '`/compact` 또는 `/clear` — 큰 컨텍스트는 매 턴 재과금되고 5H/7D 한도를 태움; `/effort` 확인 (`xhigh`가 캡 소진 1순위)',
476
+ },
477
+ LOW_HIT_RATE: {
478
+ en: 'Continue with `claude --continue`; keep CLAUDE.md trim — every line ships every turn',
479
+ ko: '`claude --continue`로 재개; CLAUDE.md 다이어트 — 모든 줄이 매 턴 실림',
480
+ },
481
+ BUCKET_5M_DOMINANT: {
482
+ en: 'Send any prompt within 5min to keep cache warm; Max plan unlocks 1h TTL',
483
+ ko: '5분 이내 한 번 더 보내 캐시 유지; Max 플랜은 1시간 TTL 제공',
484
+ },
485
+ BUCKET_5M_DOMINANT_GATEWAY: {
486
+ en: 'Gateway (Bedrock/Vertex) is 5m-only — no plan changes that; send within 5min or `/compact` before idling',
487
+ ko: '게이트웨이(Bedrock·Vertex)는 5분 고정이라 플랜으로 해소되지 않음; 5분 안에 보내거나 작업 중단 전 `/compact`',
488
+ },
489
+ HIGH_OUTPUT_RATIO: {
490
+ en: 'Check `/effort` (`xhigh` inflates output); prefer Edit over full rewrites; model matching: Sonnet 80% / Opus 15% / Haiku 5%',
491
+ ko: '`/effort` 확인 (`xhigh`는 출력 폭증); Edit 도구 우선 (전체 재작성 피하기); 모델 매칭: Sonnet 80% / Opus 15% / Haiku 5%',
492
+ },
493
+ HIGH_REQUEST_COUNT: {
494
+ en: 'Bundle calls into one message; delegate to subagents (`model: sonnet`); migrate MCP→Skills/CLI (30 tools = ~3,600 tokens/turn)',
495
+ ko: '호출은 한 메시지에 묶기; 서브에이전트(`model: sonnet`)로 위임; MCP→Skills/CLI 전환 (30 tools ≈ 매 턴 3,600 토큰)',
496
+ },
497
+ FREQUENT_CACHE_REBUILD: {
498
+ en: 'Continue one task in one session; use `/compact` at boundaries; avoid re-reading large files (use `git diff`)',
499
+ ko: '한 작업은 한 세션에서; 분기점에선 `/compact`; 큰 파일 재읽기 대신 `git diff`',
500
+ },
501
+ TTL_EXPIRY_IMMINENT: {
502
+ en: 'Send any prompt now to reset the TTL clock; or `/compact` before going idle (Pro = 5min, Max = 1h)',
503
+ ko: '지금 바로 아무 프롬프트나 보내 TTL 타이머 리셋; 또는 자리 비우기 전 `/compact` (Pro = 5분, Max = 1시간)',
504
+ },
505
+ CONTEXT_NEAR_LIMIT: {
506
+ en: '`/compact` before hitting the limit; `/clear` + re-attach only needed files',
507
+ ko: '한계 도달 전 `/compact`; `/clear` 후 필요한 파일만 재첨부',
508
+ },
509
+ };
510
+
511
+ /**
512
+ * Chip text → diagnostic codes. Some chips fire without a `detail` line that
513
+ * includes the code (e.g. `⚠ 1M ON`, which only carries context info), so we
514
+ * need a fallback so history.js can still surface the right tip.
515
+ */
516
+ export const CHIP_TO_CODES = {
517
+ '⚠ Ctx 500k+': ['LARGE_INPUT_PER_REQUEST'],
518
+ // Legacy chip name (pre-v3.32, when the threshold was 200k).
519
+ '⚠ Ctx 200k+': ['LARGE_INPUT_PER_REQUEST'],
520
+ // Legacy chip name (pre-v2.18) — kept so `last`/`history` can still resolve
521
+ // codes from history files written by older versions.
522
+ '⚠ 1M ON': ['LARGE_INPUT_PER_REQUEST'],
523
+ '⚠ Cache miss': ['LOW_HIT_RATE'],
524
+ '⚠ Input spike': ['LARGE_INPUT_PER_REQUEST'],
525
+ '⚠ 5m TTL': ['BUCKET_5M_DOMINANT', 'BUCKET_5M_DOMINANT_GATEWAY'],
526
+ '⚠ Rebuild churn': ['FREQUENT_CACHE_REBUILD'],
527
+ '⚠ Output heavy': ['HIGH_OUTPUT_RATIO'],
528
+ '⚠ Call surge': ['HIGH_REQUEST_COUNT'],
529
+ '⏳ Cache expires': ['TTL_EXPIRY_IMMINENT'],
530
+ '⏳': ['TTL_EXPIRY_IMMINENT'],
531
+ '📦': ['CONTEXT_NEAR_LIMIT'],
532
+ };
533
+
534
+ /**
535
+ * Cap-warn (5h/7d >=90%) advice — bilingual. Always points at `handoff`
536
+ * because once the cap blocks you, you need a fresh session to continue.
537
+ */
538
+ export const CAP_TIPS = {
539
+ en: 'Run `claude-token-saver handoff` to back up state before the cap blocks you',
540
+ ko: '`claude-token-saver handoff` 실행해서 캡 도달 전에 상태 백업',
541
+ };
542
+
543
+ /**
544
+ * For the statusline: the single most relevant short chip (1~2 words).
545
+ * Priority reflects what a user can act on *right now*.
546
+ * Kept short and English-only — these ride along on a single-line statusline
547
+ * shown to a global audience.
548
+ */
549
+ export function chipForIssues(issues, contextWindow) {
550
+ // Fires on *actual usage* (a real request carried more than
551
+ // CONTEXT_WARN_TOKENS), not on the model merely supporting 1M. The old 200k
552
+ // line fired on a brand-new session — system prompt plus a couple of file
553
+ // reads already clears it — so it warned constantly and told nobody
554
+ // anything. 500k is where the per-turn re-billing is worth interrupting for.
555
+ if (contextWindow?.overWarn) return '⚠ Ctx 500k+';
556
+ const codes = issues.map((i) => i.code);
557
+ if (codes.includes('LARGE_INPUT_PER_REQUEST')) return '⚠ Input spike';
558
+ if (codes.includes('BUCKET_5M_DOMINANT') || codes.includes('BUCKET_5M_DOMINANT_GATEWAY')) return '⚠ 5m TTL';
559
+ if (codes.includes('LOW_HIT_RATE')) return '⚠ Cache miss';
560
+ if (codes.includes('FREQUENT_CACHE_REBUILD')) return '⚠ Rebuild churn';
561
+ if (codes.includes('HIGH_OUTPUT_RATIO')) return '⚠ Output heavy';
562
+ if (codes.includes('HIGH_REQUEST_COUNT')) return '⚠ Call surge';
563
+ return null;
564
+ }
package/src/agents.js ADDED
@@ -0,0 +1,52 @@
1
+ /**
2
+ * How a delegation target is phrased in generated rules.
3
+ *
4
+ * Rules used to name concrete subagents (`haiku-explore`, `haiku-runner`, …).
5
+ * Those live in the user's own `~/.claude/agents/` and are NOT shipped by this
6
+ * package, so on any machine that never created them the generated rule told
7
+ * the model to delegate to an agent that does not exist. The model tier is the
8
+ * part that actually saves tokens and it is universal, so `model: haiku` is the
9
+ * default phrasing — the agent name is only added when the file is really
10
+ * there, which keeps the tool-restricted preset in play for users who have it.
11
+ */
12
+
13
+ import { existsSync } from 'node:fs';
14
+ import { join } from 'node:path';
15
+ import { claudeUserDir } from './paths.js';
16
+
17
+ /** Tier each preset agent is pinned to, for the `model:` half of the phrase. */
18
+ const AGENT_MODEL = {
19
+ 'haiku-explore': 'haiku',
20
+ 'haiku-runner': 'haiku',
21
+ 'haiku-translate': 'haiku',
22
+ 'sonnet-worker': 'sonnet',
23
+ };
24
+
25
+ /** True when `<name>.md` exists in the project or user agents directory. */
26
+ export function agentExists(name, root = process.cwd()) {
27
+ if (!name) return false;
28
+ return existsSync(join(root, '.claude', 'agents', `${name}.md`)) ||
29
+ existsSync(join(claudeUserDir(), 'agents', `${name}.md`));
30
+ }
31
+
32
+ /**
33
+ * Korean phrase for a delegation target, e.g.
34
+ * agent present → `haiku-explore(model: haiku)`
35
+ * agent absent → `model: haiku`
36
+ */
37
+ export function agentPhrase(name, { model, root } = {}) {
38
+ const tier = model || AGENT_MODEL[name] || 'haiku';
39
+ return agentExists(name, root) ? `${name}(model: ${tier})` : `model: ${tier}`;
40
+ }
41
+
42
+ /**
43
+ * English phrase, including the article, e.g.
44
+ * agent present → `the haiku-explore (model: haiku) subagent`
45
+ * agent absent → `a model: haiku subagent`
46
+ */
47
+ export function agentPhraseEn(name, { model, root } = {}) {
48
+ const tier = model || AGENT_MODEL[name] || 'haiku';
49
+ return agentExists(name, root)
50
+ ? `the ${name} (model: ${tier}) subagent`
51
+ : `a model: ${tier} subagent`;
52
+ }