claude-token-saver 2.6.0 → 2.7.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,10 +1,10 @@
1
1
  {
2
2
  "name": "claude-token-saver",
3
- "version": "2.6.0",
3
+ "version": "2.7.1",
4
4
  "description": "Save tokens on Claude Code — spike diagnosis, 1M-context detection, TTL countdown, statusline. (formerly claude-cache-monitor)",
5
5
  "type": "module",
6
6
  "bin": {
7
- "claude-token-saver": "./bin/cli.js"
7
+ "claude-token-saver": "bin/cli.js"
8
8
  },
9
9
  "files": [
10
10
  "bin/",
@@ -31,6 +31,6 @@
31
31
  "license": "MIT",
32
32
  "repository": {
33
33
  "type": "git",
34
- "url": "https://github.com/rootstudioyaml/claude-token-saver"
34
+ "url": "git+https://github.com/rootstudioyaml/claude-token-saver.git"
35
35
  }
36
36
  }
package/src/advice.js CHANGED
@@ -59,6 +59,48 @@ export const ISSUE_MESSAGES = {
59
59
  commands: [`Press ${toggleShortcut()} to toggle on/off instantly`],
60
60
  commandsKo: [`${toggleShortcut()} 누르면 즉시 on/off 토글`],
61
61
  },
62
+ {
63
+ label: 'Cap extended-thinking budget (⚠ check /effort first)',
64
+ labelKo: '확장 사고(thinking) 예산 제한 (⚠ /effort 먼저 확인)',
65
+ commands: [
66
+ '⚠ Run `/effort` to check current level — `xhigh` is the #1 cap killer',
67
+ '/effort medium — Anthropic-official default (use this for normal coding)',
68
+ '/effort low — for simple edits, labeling, boilerplate',
69
+ '/effort xhigh — ONLY for complex architecture / multi-file refactor planning, then revert',
70
+ 'export MAX_THINKING_TOKENS=8000 # global hard cap (overrides /effort)',
71
+ ],
72
+ commandsKo: [
73
+ '⚠ `/effort`로 현재 단계 확인 — `xhigh`가 캡 소진의 1순위 원인',
74
+ '/effort medium — Anthropic 공식 기본값 (일반 코딩은 이걸로)',
75
+ '/effort low — 단순 편집·라벨링·보일러플레이트',
76
+ '/effort xhigh — 복잡한 아키텍처·다파일 리팩터 계획 한정, 끝나면 즉시 복귀',
77
+ 'export MAX_THINKING_TOKENS=8000 # 전역 하드 캡 (/effort 위에 우선 적용)',
78
+ ],
79
+ },
80
+ {
81
+ label: 'Plan mode preemptively (Shift+Tab)',
82
+ labelKo: 'Plan 모드 선제 사용 (Shift+Tab)',
83
+ commands: [
84
+ 'Plan mode forces a written plan before any edits — prevents wrong-direction rework',
85
+ 'Rework after a misread spec costs more tokens than 1 extra plan turn',
86
+ ],
87
+ commandsKo: [
88
+ 'Plan 모드는 편집 전에 계획을 쓰게 강제 — 잘못된 방향 재작업 방지',
89
+ '스펙 오독 후 재작업이 plan 1턴 추가보다 훨씬 비쌈',
90
+ ],
91
+ },
92
+ {
93
+ label: 'Compact or clear when context grows',
94
+ labelKo: '컨텍스트가 커지면 /compact 또는 /clear',
95
+ commands: [
96
+ '/compact — summarize the session history in place',
97
+ '/clear — drop history entirely at a clean task boundary',
98
+ ],
99
+ commandsKo: [
100
+ '/compact — 세션 히스토리를 그 자리에서 요약',
101
+ '/clear — 작업 분기점에서 히스토리를 통째로 비우기',
102
+ ],
103
+ },
62
104
  {
63
105
  label: '⚠ Known bug #31640',
64
106
  labelKo: '⚠ 알려진 버그 #31640',
@@ -86,14 +128,26 @@ export const ISSUE_MESSAGES = {
86
128
  {
87
129
  label: 'Avoid opening fresh sessions too often',
88
130
  labelKo: '새 세션을 너무 자주 열지 말기',
89
- commands: ['Continue the same task in the same session (context switching = cache miss)'],
90
- commandsKo: ['같은 작업은 같은 세션에서 계속 (컨텍스트 전환 = 캐시 미스)'],
131
+ commands: [
132
+ 'Continue the same task in the same session (context switching = cache miss)',
133
+ 'Resume with `claude --continue` instead of starting fresh — preserves the prefix cache',
134
+ ],
135
+ commandsKo: [
136
+ '같은 작업은 같은 세션에서 계속 (컨텍스트 전환 = 캐시 미스)',
137
+ '새로 시작 대신 `claude --continue`로 재개 — prefix 캐시 보존',
138
+ ],
91
139
  },
92
140
  {
93
141
  label: 'Stabilize the prompt prefix',
94
142
  labelKo: '프롬프트 prefix 안정화',
95
- commands: ['System prompts / tool definitions that change per request invalidate the cache every time'],
96
- commandsKo: ['요청마다 바뀌는 시스템 프롬프트 / 도구 정의는 매번 캐시를 무효화'],
143
+ commands: [
144
+ 'System prompts / tool definitions that change per request invalidate the cache every time',
145
+ 'Trim CLAUDE.md — every line ships on every turn; keep only durable rules',
146
+ ],
147
+ commandsKo: [
148
+ '요청마다 바뀌는 시스템 프롬프트 / 도구 정의는 매번 캐시를 무효화',
149
+ 'CLAUDE.md 다이어트 — 모든 줄이 매 턴 실림; 영속적인 규칙만 유지',
150
+ ],
97
151
  },
98
152
  ],
99
153
  },
@@ -142,6 +196,36 @@ export const ISSUE_MESSAGES = {
142
196
  '긴 문서/README 생성 요청은 스크립트로 옮겨 출력 축소',
143
197
  ],
144
198
  },
199
+ {
200
+ label: 'Cap extended-thinking budget (⚠ check /effort)',
201
+ labelKo: '확장 사고(thinking) 예산 제한 (⚠ /effort 확인)',
202
+ commands: [
203
+ '⚠ Check `/effort` — `xhigh` burns thinking tokens that count as output',
204
+ '/effort medium — Anthropic-official default; switch back from xhigh',
205
+ 'export MAX_THINKING_TOKENS=8000 # global hard cap',
206
+ ],
207
+ commandsKo: [
208
+ '⚠ `/effort` 확인 — `xhigh`는 thinking 토큰을 다량 소비 (출력으로 집계)',
209
+ '/effort medium — Anthropic 공식 기본값; xhigh에서 복귀',
210
+ 'export MAX_THINKING_TOKENS=8000 # 전역 하드 캡',
211
+ ],
212
+ },
213
+ {
214
+ label: 'Model matching strategy (80/15/5 rule)',
215
+ labelKo: '모델 매칭 전략 (80/15/5 비율)',
216
+ commands: [
217
+ 'Sonnet 80% — daily coding, edits, refactors',
218
+ 'Opus 15% — complex design, multi-file refactor planning, deep debugging',
219
+ 'Haiku 5% — boilerplate, labeling, summaries, subagent work',
220
+ 'Switch with `/model sonnet` / `/model opus` / `/model haiku`',
221
+ ],
222
+ commandsKo: [
223
+ 'Sonnet 80% — 일상 코딩, 편집, 리팩터링',
224
+ 'Opus 15% — 복잡한 설계, 다파일 리팩터 계획, 깊은 디버깅',
225
+ 'Haiku 5% — 보일러플레이트, 라벨링, 요약, 서브에이전트 작업',
226
+ '`/model sonnet` / `/model opus` / `/model haiku`로 전환',
227
+ ],
228
+ },
145
229
  ],
146
230
  },
147
231
  HIGH_REQUEST_COUNT: {
@@ -165,6 +249,136 @@ export const ISSUE_MESSAGES = {
165
249
  commands: ['Check that the agent is not repeating the same test/search in a loop'],
166
250
  commandsKo: ['에이전트가 동일한 테스트/검색을 루프로 반복하고 있지 않은지 확인'],
167
251
  },
252
+ {
253
+ label: 'Delegate large searches to a subagent (Sonnet by default)',
254
+ labelKo: '대형 검색은 서브에이전트로 위임 (Sonnet 기본)',
255
+ commands: [
256
+ 'Use the Task tool with the Explore subagent for repo-wide searches',
257
+ 'Pin custom subagents to Sonnet/Haiku in their frontmatter: `model: sonnet`',
258
+ 'The subagent runs in its own context — main session keeps a clean prefix',
259
+ ],
260
+ commandsKo: [
261
+ '레포 전반 검색은 Task 도구의 Explore 서브에이전트로 위임',
262
+ '커스텀 서브에이전트는 frontmatter에 `model: sonnet` 명시 (Opus 자동 상속 방지)',
263
+ '서브에이전트는 자체 컨텍스트에서 실행 — 메인 세션 prefix가 깨끗하게 유지됨',
264
+ ],
265
+ },
266
+ {
267
+ label: 'Migrate MCP servers → Skills / CLI',
268
+ labelKo: 'MCP 서버 → Skills / CLI 전환',
269
+ commands: [
270
+ '30 MCP tools ≈ 3,600 tokens loaded every turn — measured by mcp2cli (HN 146pts)',
271
+ 'Replacing rarely-used MCPs with Skills (loaded on demand) or shell CLI cuts ~96%',
272
+ 'Audit `.mcp.json` and disable everything not used weekly',
273
+ ],
274
+ commandsKo: [
275
+ 'MCP 도구 30개 ≈ 매 턴 3,600 토큰 상시 로드 (mcp2cli 측정, HN 146pts)',
276
+ '드물게 쓰는 MCP를 Skills(필요 시 로드) 또는 셸 CLI로 대체하면 ~96% 절감',
277
+ '`.mcp.json` 점검해 주간에 안 쓰는 건 모두 비활성화',
278
+ ],
279
+ },
280
+ {
281
+ label: 'Trim hooks that inject context',
282
+ labelKo: '컨텍스트 주입 훅 정리',
283
+ commands: ['Disable noisy PreToolUse/PostToolUse hooks unless they earn their tokens'],
284
+ commandsKo: ['값을 못 하는 PreToolUse/PostToolUse 훅은 비활성화'],
285
+ },
286
+ ],
287
+ },
288
+ TTL_EXPIRY_IMMINENT: {
289
+ title: 'Cache TTL is about to expire',
290
+ titleKo: '캐시 TTL 만료 임박',
291
+ explain:
292
+ 'Once the TTL window closes, the entire prefix cache is discarded and must be rebuilt ' +
293
+ 'from scratch on the next call — paying full input cost again.',
294
+ explainKo:
295
+ 'TTL 창이 닫히면 prefix 캐시 전체가 폐기되고 다음 호출에서 처음부터 재구축해야 합니다 — ' +
296
+ '전체 입력 비용이 다시 청구됩니다.',
297
+ actions: () => [
298
+ {
299
+ label: 'Send any prompt within the TTL window',
300
+ labelKo: 'TTL 창 안에 어떤 프롬프트든 보내기',
301
+ commands: [
302
+ 'Even a short "." or summary request resets the TTL clock',
303
+ 'Pro plan TTL = 5 min; Max plan TTL = 1 hour',
304
+ ],
305
+ commandsKo: [
306
+ '"." 한 글자나 짧은 요약 요청만 해도 TTL 타이머가 리셋됨',
307
+ 'Pro 플랜 TTL = 5분; Max 플랜 TTL = 1시간',
308
+ ],
309
+ },
310
+ {
311
+ label: 'Use /compact before going idle',
312
+ labelKo: '자리를 비우기 전에 /compact 실행',
313
+ commands: [
314
+ '/compact summarizes the conversation in place and re-anchors the cache',
315
+ 'The compacted summary is much smaller — next rebuild is cheaper',
316
+ ],
317
+ commandsKo: [
318
+ '/compact는 대화를 그 자리에서 요약하고 캐시를 재고정',
319
+ '요약된 컨텍스트는 훨씬 작아서 다음 재빌드 비용도 줄어듦',
320
+ ],
321
+ },
322
+ {
323
+ label: 'Upgrade to Max for 1h TTL',
324
+ labelKo: 'Max 플랜으로 업그레이드해 1시간 TTL 확보',
325
+ commands: [
326
+ 'Pro plan is capped at 5-minute TTL — any break longer than that expires the cache',
327
+ 'Max plan uses the 1-hour extended TTL bucket by default',
328
+ ],
329
+ commandsKo: [
330
+ 'Pro 플랜은 5분 TTL 고정 — 5분 이상 공백이면 캐시 만료',
331
+ 'Max 플랜은 기본적으로 1시간 TTL 버킷을 사용',
332
+ ],
333
+ },
334
+ ],
335
+ },
336
+ CONTEXT_NEAR_LIMIT: {
337
+ title: 'Context window is approaching the limit',
338
+ titleKo: '컨텍스트 창이 한계에 근접',
339
+ explain:
340
+ 'As context grows toward 200k (or 1M), each turn becomes more expensive and cache ' +
341
+ 'reuse efficiency drops. Past 200k, long-context pricing applies.',
342
+ explainKo:
343
+ '컨텍스트가 200k(또는 1M)에 가까워질수록 매 턴 비용이 높아지고 캐시 재사용 효율이 떨어집니다. ' +
344
+ '200k를 넘으면 장기 컨텍스트 요금이 적용됩니다.',
345
+ actions: () => [
346
+ {
347
+ label: '/compact at the next natural break',
348
+ labelKo: '다음 자연스러운 중단점에서 /compact',
349
+ commands: [
350
+ '/compact replaces the full conversation with a compact summary',
351
+ 'Run it before context hits the limit — not after (compaction itself costs tokens)',
352
+ ],
353
+ commandsKo: [
354
+ '/compact는 전체 대화를 압축 요약으로 교체',
355
+ '한계에 도달하기 전에 실행 — 이후엔 compact 자체도 비쌈',
356
+ ],
357
+ },
358
+ {
359
+ label: 'Re-attach only what you need after /clear',
360
+ labelKo: '/clear 후 필요한 것만 재첨부',
361
+ commands: [
362
+ '/clear resets context to zero — cheapest option when task scope has shifted',
363
+ 'Re-read only the files Claude needs right now, not everything from before',
364
+ ],
365
+ commandsKo: [
366
+ '/clear는 컨텍스트를 완전 초기화 — 작업 범위가 바뀌었을 때 가장 저렴',
367
+ '지금 필요한 파일만 다시 읽기, 이전 파일 전부 재로드 금지',
368
+ ],
369
+ },
370
+ {
371
+ label: 'Avoid 1M context unless required',
372
+ labelKo: '필요하지 않으면 1M 컨텍스트 비활성화',
373
+ commands: [
374
+ 'export CLAUDE_MODEL_CONTEXT=200000 # force 200k cap',
375
+ 'Max plan auto-promotes to 1M — disable if you don\'t need it',
376
+ ],
377
+ commandsKo: [
378
+ 'export CLAUDE_MODEL_CONTEXT=200000 # 200k로 고정',
379
+ 'Max 플랜은 자동으로 1M 승격 — 불필요하면 비활성화',
380
+ ],
381
+ },
168
382
  ],
169
383
  },
170
384
  FREQUENT_CACHE_REBUILD: {
@@ -182,6 +396,30 @@ export const ISSUE_MESSAGES = {
182
396
  commands: ['Continue one task in one Claude Code session'],
183
397
  commandsKo: ['하나의 작업은 하나의 Claude Code 세션에서 계속'],
184
398
  },
399
+ {
400
+ label: 'Use /compact at task boundaries',
401
+ labelKo: '작업 분기점에서 /compact 사용',
402
+ commands: [
403
+ '/compact summarizes prior turns in place — keeps the session alive without re-reading everything',
404
+ 'Better than starting a fresh session, which forces a full prefix rebuild',
405
+ ],
406
+ commandsKo: [
407
+ '/compact는 이전 턴을 그 자리에서 요약 — 전체 재읽기 없이 세션 유지',
408
+ '새 세션을 여는 것보다 나음 (새 세션은 prefix 전체 재구축 강제)',
409
+ ],
410
+ },
411
+ {
412
+ label: 'Avoid re-reading large files',
413
+ labelKo: '큰 파일 반복 Read 피하기',
414
+ commands: [
415
+ 'Prefer `git diff` to see what changed instead of re-reading the whole file',
416
+ 'Edit returns success silently — no need to Read after editing',
417
+ ],
418
+ commandsKo: [
419
+ '파일 전체 다시 읽기 대신 `git diff`로 변경분만 확인',
420
+ 'Edit는 성공 시 조용히 끝남 — 편집 후 Read 다시 할 필요 없음',
421
+ ],
422
+ },
185
423
  ],
186
424
  },
187
425
  };
@@ -197,28 +435,36 @@ export const ISSUE_MESSAGES = {
197
435
  */
198
436
  export const ISSUE_TIPS = {
199
437
  LARGE_INPUT_PER_REQUEST: {
200
- en: 'Disable 1M context: `export CLAUDE_CODE_DISABLE_1M_CONTEXT=1` (or ⌥P toggle)',
201
- ko: '1M 컨텍스트 끄기: `export CLAUDE_CODE_DISABLE_1M_CONTEXT=1` (또는 ⌥P 토글)',
438
+ en: 'Check `/effort` `xhigh` is the #1 cap killer; switch to `/effort medium` (or `low`); disable 1M context; `/compact` when context grows',
439
+ ko: '`/effort` 확인 `xhigh`가 캡 소진 1순위 원인, `medium`(또는 `low`)으로 복귀; 1M 컨텍스트 끄기; 컨텍스트 커지면 `/compact`',
202
440
  },
203
441
  LOW_HIT_RATE: {
204
- en: 'Continue same task in same session; keep prompt prefix stable',
205
- ko: '같은 작업은 같은 세션에서 계속; 프롬프트 prefix 안정 유지',
442
+ en: 'Continue with `claude --continue`; keep CLAUDE.md trim every line ships every turn',
443
+ ko: '`claude --continue`로 재개; CLAUDE.md 다이어트 모든 줄이 매 턴 실림',
206
444
  },
207
445
  BUCKET_5M_DOMINANT: {
208
446
  en: 'Send any prompt within 5min to keep cache warm; Max plan unlocks 1h TTL',
209
447
  ko: '5분 이내 한 번 더 보내 캐시 유지; Max 플랜은 1시간 TTL 제공',
210
448
  },
211
449
  HIGH_OUTPUT_RATIO: {
212
- en: 'Avoid full-file rewrites prefer Edit tool; move long generations to scripts',
213
- ko: '파일 전체 재작성 피하기 Edit 도구 사용; 생성은 스크립트로',
450
+ en: 'Check `/effort` (`xhigh` inflates output); prefer Edit over full rewrites; model matching: Sonnet 80% / Opus 15% / Haiku 5%',
451
+ ko: '`/effort` 확인 (`xhigh`는 출력 폭증); Edit 도구 우선 (전체 재작성 피하기); 모델 매칭: Sonnet 80% / Opus 15% / Haiku 5%',
214
452
  },
215
453
  HIGH_REQUEST_COUNT: {
216
- en: 'Bundle independent calls into one message; check for retry loops',
217
- ko: '독립 호출은 한 메시지에 묶기; 재시도 루프 점검',
454
+ en: 'Bundle calls into one message; delegate to subagents (`model: sonnet`); migrate MCP→Skills/CLI (30 tools = ~3,600 tokens/turn)',
455
+ ko: '호출은 한 메시지에 묶기; 서브에이전트(`model: sonnet`)로 위임; MCP→Skills/CLI 전환 (30 tools ≈ 매 턴 3,600 토큰)',
218
456
  },
219
457
  FREQUENT_CACHE_REBUILD: {
220
- en: 'Continue one task in one session short sessions trigger rebuild',
221
- ko: '한 작업은 한 세션에서 짧은 세션은 재빌드 유발',
458
+ en: 'Continue one task in one session; use `/compact` at boundaries; avoid re-reading large files (use `git diff`)',
459
+ ko: '한 작업은 한 세션에서; 분기점에선 `/compact`; 파일 재읽기 대신 `git diff`',
460
+ },
461
+ TTL_EXPIRY_IMMINENT: {
462
+ en: 'Send any prompt now to reset the TTL clock; or `/compact` before going idle (Pro = 5min, Max = 1h)',
463
+ ko: '지금 바로 아무 프롬프트나 보내 TTL 타이머 리셋; 또는 자리 비우기 전 `/compact` (Pro = 5분, Max = 1시간)',
464
+ },
465
+ CONTEXT_NEAR_LIMIT: {
466
+ en: '`/compact` before hitting the limit; `/clear` + re-attach only needed files; disable 1M: `export CLAUDE_MODEL_CONTEXT=200000`',
467
+ ko: '한계 도달 전 `/compact`; `/clear` 후 필요한 파일만 재첨부; 1M 끄기: `export CLAUDE_MODEL_CONTEXT=200000`',
222
468
  },
223
469
  };
224
470
 
@@ -235,6 +481,9 @@ export const CHIP_TO_CODES = {
235
481
  '⚠ Rebuild churn': ['FREQUENT_CACHE_REBUILD'],
236
482
  '⚠ Output heavy': ['HIGH_OUTPUT_RATIO'],
237
483
  '⚠ Call surge': ['HIGH_REQUEST_COUNT'],
484
+ '⏳ Cache expires': ['TTL_EXPIRY_IMMINENT'],
485
+ '⏳': ['TTL_EXPIRY_IMMINENT'],
486
+ '📦': ['CONTEXT_NEAR_LIMIT'],
238
487
  };
239
488
 
240
489
  /**
@@ -5,6 +5,22 @@ import { ISSUE_MESSAGES } from '../advice.js';
5
5
  import { formatResetIn, formatResetClock } from '../format-time.js';
6
6
  import { labelForKey } from '../window-labels.js';
7
7
 
8
+ const C = {
9
+ reset: '\x1b[0m',
10
+ bold: '\x1b[1m',
11
+ blink: '\x1b[5m',
12
+ red: '\x1b[31m',
13
+ yellow: '\x1b[33m',
14
+ cyan: '\x1b[36m',
15
+ };
16
+ // Wrap text with ANSI codes — gracefully strips to plain when NO_COLOR set.
17
+ const colorOk = !process.env.NO_COLOR;
18
+ const r = (s) => colorOk ? `${C.red}${s}${C.reset}` : s;
19
+ const rb = (s) => colorOk ? `${C.bold}${C.red}${s}${C.reset}` : s;
20
+ const rbl = (s) => colorOk ? `${C.blink}${C.bold}${C.red}${s}${C.reset}` : s;
21
+ const y = (s) => colorOk ? `${C.yellow}${s}${C.reset}` : s;
22
+ const b = (s) => colorOk ? `${C.bold}${s}${C.reset}` : s;
23
+
8
24
  function pad(str, len, align = 'left') {
9
25
  const s = String(str);
10
26
  if (align === 'right') return s.padStart(len);
@@ -66,11 +82,11 @@ function formatContextSize(n) {
66
82
 
67
83
  function renderSpikeSection(spikes, contextWindow) {
68
84
  const lines = [];
69
- lines.push(' ⚠ Token spike detected');
70
- lines.push(` ${'─'.repeat(50)}`);
85
+ lines.push(rbl(' ⚠ Token spike detected'));
86
+ lines.push(r(` ${'─'.repeat(50)}`));
71
87
  if (contextWindow && contextWindow.size === '1M') {
72
88
  lines.push(
73
- ` Context mode: 1M (max recent single-request input ${formatContextSize(contextWindow.maxContext)} tokens)`,
89
+ r(` Context mode: 1M (max recent single-request input ${formatContextSize(contextWindow.maxContext)} tokens)`),
74
90
  );
75
91
  lines.push('');
76
92
  }
@@ -78,18 +94,19 @@ function renderSpikeSection(spikes, contextWindow) {
78
94
  const m = spike.metrics;
79
95
  const ratioLabel = spike.ratio ? `${spike.ratio.toFixed(1)}× p95` : 'single-request > 250k';
80
96
  lines.push(
81
- ` • ${shortSessionId(m.sessionId)} [${m.projectDir || 'unknown'}] ` +
82
- `total input ${formatContextSize(m.totalInput)} (${ratioLabel}, ${m.requestCount} requests)`,
97
+ r(` • ${shortSessionId(m.sessionId)} [${m.projectDir || 'unknown'}] `) +
98
+ rb(`total input ${formatContextSize(m.totalInput)}`) +
99
+ r(` (${ratioLabel}, ${m.requestCount} requests)`),
83
100
  );
84
101
  if (m.maxContextPerRequest > 0) {
85
102
  lines.push(
86
- ` max single-request context: ${formatContextSize(m.maxContextPerRequest)} tokens`,
103
+ r(` max single-request context: ${formatContextSize(m.maxContextPerRequest)} tokens`),
87
104
  );
88
105
  }
89
106
  for (const issue of spike.issues) {
90
107
  const info = ISSUE_MESSAGES[issue.code];
91
108
  if (!info) continue;
92
- lines.push(` · ${info.title}`);
109
+ lines.push(r(` · ${info.title}`));
93
110
  }
94
111
  lines.push('');
95
112
  }
@@ -106,22 +123,24 @@ function renderSpikeSection(spikes, contextWindow) {
106
123
  }
107
124
  }
108
125
  if (uniqueIssues.length > 0) {
109
- lines.push(' Recommended actions');
110
- lines.push(` ${'─'.repeat(50)}`);
126
+ lines.push(rb(' Recommended actions'));
127
+ lines.push(r(` ${'─'.repeat(50)}`));
111
128
  for (const issue of uniqueIssues) {
112
129
  const info = ISSUE_MESSAGES[issue.code];
113
130
  if (!info) continue;
114
- lines.push(` ▸ ${info.title}`);
115
- lines.push(` ${info.explain}`);
131
+ lines.push(rb(` ▸ ${info.title}`));
132
+ lines.push(r(` ${info.explain}`));
116
133
  for (const action of info.actions()) {
117
- lines.push(` - ${action.label}`);
134
+ lines.push(r(` - ${action.label}`));
118
135
  for (const cmd of action.commands) {
119
- lines.push(` ${cmd}`);
136
+ lines.push(r(` ${cmd}`));
120
137
  }
121
138
  }
122
139
  lines.push('');
123
140
  }
124
141
  }
142
+ lines.push(rbl(' ▶ 상세 처치법: ') + rb('claude-token-saver last'));
143
+ lines.push('');
125
144
  return lines;
126
145
  }
127
146
 
@@ -132,8 +151,8 @@ function renderCapWarnSection(caps) {
132
151
  );
133
152
  if (warning.length === 0) return [];
134
153
  const lines = [];
135
- lines.push(' 🚨 Rate-limit cap is closing in');
136
- lines.push(` ${'─'.repeat(50)}`);
154
+ lines.push(rbl(' 🚨 Rate-limit cap is closing in'));
155
+ lines.push(r(` ${'─'.repeat(50)}`));
137
156
  for (const win of warning) {
138
157
  const label = labelForKey(win.key).long;
139
158
  const reset = formatResetIn(win.resetsAt);
@@ -142,12 +161,14 @@ function renderCapWarnSection(caps) {
142
161
  if (reset && clock) tail = `, resets in ${reset} (at ${clock})`;
143
162
  else if (reset) tail = `, resets in ${reset}`;
144
163
  else if (clock) tail = `, resets at ${clock}`;
145
- lines.push(` • ${label}: ${Math.round(win.usedPct)}% used${tail}`);
164
+ lines.push(rb(` • ${label}: ${Math.round(win.usedPct)}% used${tail}`));
146
165
  }
147
166
  lines.push('');
148
- lines.push(' Back up work before the cap hits:');
149
- lines.push(' claude-token-saver handoff');
150
- lines.push(' (writes a HANDOFF-*.md so a fresh session can pick up.)');
167
+ lines.push(r(' Back up work before the cap hits:'));
168
+ lines.push(rb(' claude-token-saver handoff'));
169
+ lines.push(r(' (writes a HANDOFF-*.md so a fresh session can pick up.)'));
170
+ lines.push('');
171
+ lines.push(rbl(' ▶ 상세 처치법: ') + rb('claude-token-saver last'));
151
172
  lines.push('');
152
173
  return lines;
153
174
  }