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
@@ -0,0 +1,750 @@
1
+ /**
2
+ * Statusline formatter β€” compact single-line output for Claude Code statusline API.
3
+ * Called every ~300ms, so kept minimal and fast. ANSI color codes included by default.
4
+ *
5
+ * Example output (color):
6
+ * 🧠 97.5% Β· 1h Β· ⏱ 42:15 Β· πŸ’° $4.8K Β· 7d
7
+ *
8
+ * Disable color with NO_COLOR=1 env var or --no-color flag.
9
+ * Disable the TTL countdown with --no-timer.
10
+ *
11
+ * Usage in ~/.claude/settings.json:
12
+ * {
13
+ * "statusLine": {
14
+ * "type": "command",
15
+ * "command": "npx claude-token-saver --statusline"
16
+ * }
17
+ * }
18
+ */
19
+
20
+ import { formatResetClock } from '../format-time.js';
21
+ import { labelForKey } from '../window-labels.js';
22
+ import { harnessStatusForStatusline } from '../harness.js';
23
+ import { loadConfig } from '../config.js';
24
+ import { koreanStyleEnabled } from '../korean-style.js';
25
+
26
+ // The 8-color ANSI defaults (RED=31, GREEN=32, YELLOW=33…) read as garish
27
+ // next to each other β€” terminal palettes set them with unbalanced perceptual
28
+ // brightness, so the line ends up feeling loud. We emit a Tailwind-inspired
29
+ // muted palette via 24-bit truecolor when the terminal advertises support
30
+ // (`COLORTERM=truecolor`/`24bit`), and gracefully fall back to the legacy
31
+ // 8-color codes on terminals that don't.
32
+ //
33
+ // GREEN β†’ emerald-400 #34D399 (calm, balanced with the others)
34
+ // YELLOW β†’ amber-400 #FBBF24 (warm, not screamy)
35
+ // RED β†’ rose-400 #FB7185 (alarm without the eye-burn of pure red)
36
+ // CYAN β†’ cyan-400 #22D3EE
37
+ // MAGENTA β†’ violet-400 #A78BFA (model identity tone)
38
+ // GRAY β†’ slate-500 #64748B (recedes for the gauge track / period footer)
39
+ const TRUECOLOR =
40
+ process.env.COLORTERM === 'truecolor' || process.env.COLORTERM === '24bit';
41
+ const fg = (r, g, b, fallback) =>
42
+ TRUECOLOR ? `\x1b[38;2;${r};${g};${b}m` : fallback;
43
+
44
+ const RESET = '\x1b[0m';
45
+ const RED = fg(251, 113, 133, '\x1b[31m');
46
+ const GREEN = fg(52, 211, 153, '\x1b[32m');
47
+ const YELLOW = fg(251, 191, 36, '\x1b[33m');
48
+ const CYAN = fg(34, 211, 238, '\x1b[36m');
49
+ const MAGENTA = fg(167, 139, 250, '\x1b[35m');
50
+ const GRAY = fg(100, 116, 139, '\x1b[90m');
51
+ const BOLD = '\x1b[1m';
52
+
53
+ function formatMoney(usd) {
54
+ // Boundaries sit at 999.5/999500 so a value that would round up into the
55
+ // next band's width ($999.9 β†’ "$1000") jumps to that band's unit instead β€”
56
+ // otherwise "$1000" renders visually larger than "$1.0K".
57
+ if (usd >= 999500) return `$${(usd / 1e6).toFixed(1)}M`;
58
+ if (usd >= 999.5) return `$${(usd / 1000).toFixed(1)}K`;
59
+ if (usd >= 100) return `$${usd.toFixed(0)}`;
60
+ if (usd >= 10) return `$${usd.toFixed(1)}`;
61
+ return `$${usd.toFixed(2)}`;
62
+ }
63
+
64
+ function formatPct(v) {
65
+ return `${(v * 100).toFixed(1)}%`;
66
+ }
67
+
68
+ /**
69
+ * Render a 6-cell density-gradient gauge for `pct` (0..100). All cells share
70
+ * the same Unicode "Block Elements" density family β€” `β–ˆ` (100%) β†’ `β–“` (75%)
71
+ * → `▒` (50%) → `░` (25%) — so the fill→empty boundary reads as one smooth
72
+ * gradient instead of an awkward step.
73
+ *
74
+ * Earlier we used partial-fill glyphs (`β–β–Žβ–β–Œβ–‹β–Šβ–‰`) for sub-cell precision, but
75
+ * those have transparent halves that clash visually with the `β–‘` track next
76
+ * to them (the eye sees "solid edge | gap | dotted track" β€” three zones).
77
+ * Density chars are the same shape, just darker/lighter, so the boundary
78
+ * cell reads as a single smooth fade.
79
+ *
80
+ * Each cell is ~17% wide; the boundary cell uses 3 intermediate density steps
81
+ * for ~4% effective precision around the fill edge. Stable monospace width
82
+ * across all terminal fonts that ship Block Elements (U+2580–U+259F).
83
+ */
84
+ function gaugeBar(pct) {
85
+ const cells = 6;
86
+ const clamped = Math.max(0, Math.min(100, pct));
87
+ const filled = (clamped / 100) * cells; // e.g. 4.32 cells filled
88
+ const fullCells = Math.floor(filled);
89
+ const remainder = filled - fullCells; // 0..1 β€” fill fraction of the boundary cell
90
+ // Boundary cell: β–‘ (empty), β–’ (1/3), β–“ (2/3), or roll over to a full β–ˆ.
91
+ let partial = '';
92
+ let extra = 0;
93
+ if (remainder >= 0.83) {
94
+ extra = 1; // round up β€” fill the boundary cell completely
95
+ } else if (remainder >= 0.5) {
96
+ partial = 'β–“';
97
+ } else if (remainder >= 0.16) {
98
+ partial = 'β–’';
99
+ } // else: remainder is too small to show β€” leave the cell empty
100
+ const totalFull = Math.min(cells, fullCells + extra);
101
+ const usedCells = totalFull + (partial ? 1 : 0);
102
+ const empty = 'β–‘'.repeat(Math.max(0, cells - usedCells));
103
+ return 'β–ˆ'.repeat(totalFull) + partial + empty;
104
+ }
105
+
106
+ /**
107
+ * Format a remaining-seconds countdown as MM:SS (or H:MM when β‰₯ 1h).
108
+ */
109
+ function formatTimer(remainingSec) {
110
+ // Defensive: non-finite/NaN inputs (e.g. clock skew, stringified Date) used
111
+ // to slip through and render as "NaN:NaN" or stretched seconds. Treat any
112
+ // weird input as expired rather than rendering garbage in the statusline.
113
+ if (!Number.isFinite(remainingSec) || remainingSec <= 0) return 'EXPIRED';
114
+ const totalSec = Math.max(0, Math.floor(remainingSec));
115
+ const h = Math.floor(totalSec / 3600);
116
+ const mRaw = Math.floor((totalSec % 3600) / 60);
117
+ const sRaw = totalSec % 60;
118
+ // Clamp explicitly so a future regression in the math (or padStart no-op
119
+ // truncation) can never produce m:sss like "4:547".
120
+ const m = Math.min(59, Math.max(0, mRaw));
121
+ const s = Math.min(59, Math.max(0, sRaw));
122
+ if (h > 0) return `${h}:${String(m).padStart(2, '0')}`;
123
+ return `${m}:${String(s).padStart(2, '0')}`;
124
+ }
125
+
126
+ /**
127
+ * Pick the cap-warn chip that should surface from any of the rate-limit
128
+ * windows, or null when none are at 90%+. When multiple windows are warning,
129
+ * the one that resets sooner wins (it's the more imminent block).
130
+ */
131
+ export function pickCapWarn(caps) {
132
+ if (!caps || !Array.isArray(caps.windows)) return null;
133
+ const candidates = caps.windows
134
+ .filter((w) => Number.isFinite(w.usedPct) && w.usedPct >= 90)
135
+ .map((w) => ({ ...w, label: labelForKey(w.key).short }));
136
+ if (candidates.length === 0) return null;
137
+ candidates.sort((a, b) => {
138
+ const ar = Number.isFinite(a.resetsAt) ? a.resetsAt : Infinity;
139
+ const br = Number.isFinite(b.resetsAt) ? b.resetsAt : Infinity;
140
+ return ar - br;
141
+ });
142
+ return candidates[0];
143
+ }
144
+
145
+ /**
146
+ * Korean-style chip builder. Renders only when the session-start injection is
147
+ * enabled, so nothing changes for anyone who never asked for it.
148
+ */
149
+ function buildKoreanSeg(c, isIcon, verbose) {
150
+ try {
151
+ if (!koreanStyleEnabled()) return null;
152
+ // Deliberately quiet (gray, one glyph): this is a "yes, it is on"
153
+ // confirmation, not a warning. Without it a silently-failed hook looks
154
+ // exactly like a working one, because the style only shows up when the
155
+ // model happens to write Korean. The icon says "writing guidance", not
156
+ // "Korean" β€” the verbose label already carries the language.
157
+ if (isIcon) return `${c(GRAY)}${verbose ? '✍️ Korean style' : '✍️'}${c(RESET)}`;
158
+ return `${c(GRAY)}Korean style${c(RESET)}`;
159
+ } catch {
160
+ return null;
161
+ }
162
+ }
163
+
164
+ /**
165
+ * Version segment builder β€” "which copy of this tool am I looking at", plus
166
+ * the upgrade nudge when a newer one has been published.
167
+ *
168
+ * Two states, deliberately different in weight:
169
+ * - up to date β†’ `v3.24.0` in gray. Identity context, not news.
170
+ * - update available β†’ `⬆ v3.24.0 β†’ 3.25.0` in yellow. Same tone as the
171
+ * other "you should do something eventually" chips, never red: nothing is
172
+ * broken, and a permanently-red statusline trains the eye to ignore red.
173
+ *
174
+ * A statusline cannot open a dialog, so the *asking* happens at session start
175
+ * (see route-scan --hook, which briefs the model to offer the upgrade). This
176
+ * chip is the persistent reminder between those offers, which is why it keeps
177
+ * rendering after the user declines β€” declining hides the session-start
178
+ * question, not the fact that a new version exists.
179
+ */
180
+ function buildVersionSeg(version, update, c, isIcon, verbose) {
181
+ if (!version) return null;
182
+ if (update && update.available && update.latest) {
183
+ const body = verbose
184
+ ? `Update v${version} β†’ ${update.latest}`
185
+ : `v${version} β†’ ${update.latest}`;
186
+ return `${c(YELLOW)}${isIcon ? '⬆ ' : ''}${body}${c(RESET)}`;
187
+ }
188
+ return `${c(GRAY)}v${version}${c(RESET)}`;
189
+ }
190
+
191
+ /**
192
+ * Harness πŸ…· segment builder β€” shared by the full report and the no-session
193
+ * fallback line. Best-effort: never throws into the statusline (corrupted
194
+ * CLAUDE.md, permission issue, etc. β†’ null).
195
+ */
196
+ function buildHarnessSeg(c, isIcon) {
197
+ try {
198
+ const harnessInfo = harnessStatusForStatusline(loadConfig());
199
+ if (!harnessInfo) return null;
200
+ const icon = isIcon ? 'πŸ…·' : 'H';
201
+ if (harnessInfo.warning) {
202
+ // Warning state outranks the N/5 count β€” a runtime issue (repeated
203
+ // error / no-evidence / racing edits) is more actionable than a
204
+ // missing ratchet section. Always red so it stands out.
205
+ return `${c(RED)}${icon}⚠ ${harnessInfo.warning}${c(RESET)}`;
206
+ }
207
+ if (harnessInfo.custom) return `${c(CYAN)}${icon} custom${c(RESET)}`;
208
+ const tone = harnessInfo.configured >= harnessInfo.total ? GREEN : YELLOW;
209
+ return `${c(tone)}${icon} ${harnessInfo.configured}/${harnessInfo.total}${c(RESET)}`;
210
+ } catch {
211
+ return null;
212
+ }
213
+ }
214
+
215
+ /**
216
+ * Cap-warn chip builder β€” shared by the full report and the no-session
217
+ * fallback line. At 90%+ the user wants to know "when can I send again", so
218
+ * the wall-clock reset time rides along in the same `πŸ”„ HH:MM` shape as the
219
+ * always-on usage segments.
220
+ */
221
+ function buildCapWarnSeg(capWarn, c, isIcon) {
222
+ if (!capWarn) return null;
223
+ const pct = Math.round(capWarn.usedPct);
224
+ const clock = formatResetClock(capWarn.resetsAt);
225
+ const clockTail = clock ? ` πŸ”„ ${clock}` : '';
226
+ if (isIcon) {
227
+ // Gauge keeps shape parity with the always-on usage segment β€” the
228
+ // cap-warn is just the same gauge "filled to alarm". Visual continuity
229
+ // helps the eye understand "this is the 5H bar I was watching, just red now."
230
+ const bar = gaugeBar(pct);
231
+ return `${c(BOLD)}${c(RED)}🚨 ${capWarn.label} ${bar} ${pct}%${clockTail}${c(RESET)}`;
232
+ }
233
+ return `${c(BOLD)}${c(RED)}${capWarn.label} cap ${pct}%${clockTail}${c(RESET)}`;
234
+ }
235
+
236
+ /**
237
+ * Fallback line for when no session data exists in the analysis window.
238
+ * The stdin payload (rate limits, model) is still live in that case, and a
239
+ * 90%+ cap warning is exactly the kind of signal that must not disappear
240
+ * just because the user has been idle past the window β€” so cap-warn,
241
+ * harness, and model chips still render around the "no session data" note.
242
+ */
243
+ export function formatNoSession({ caps = null, model = null, windowLabel = '', version = '', update = null } = {}, { color = true, mode = 'icon' } = {}) {
244
+ const c = (v) => (color ? v : '');
245
+ const isIcon = mode === 'icon';
246
+ const segs = [];
247
+ const capSeg = buildCapWarnSeg(pickCapWarn(caps), c, isIcon);
248
+ if (capSeg) segs.push(capSeg);
249
+ const versionSeg = buildVersionSeg(version, update, c, isIcon, false);
250
+ if (versionSeg && update && update.available) segs.push(versionSeg);
251
+ const harnessSeg = buildHarnessSeg(c, isIcon);
252
+ if (harnessSeg) segs.push(harnessSeg);
253
+ if (typeof model === 'string' && model.length > 0) {
254
+ segs.push(isIcon ? `${c(MAGENTA)}πŸ€– ${model}${c(RESET)}` : `${c(MAGENTA)}${model}${c(RESET)}`);
255
+ }
256
+ segs.push(`${c(GRAY)}🧠 no session data${windowLabel ? ` · ${windowLabel}` : ''}${c(RESET)}`);
257
+ if (versionSeg && !(update && update.available)) segs.push(versionSeg);
258
+ return segs.join(' Β· ') + (color ? '\x1b[K' : '');
259
+ }
260
+
261
+ /**
262
+ * @param {object} data - output of main report pipeline (summary, ttl, cost, options, lastActivity)
263
+ * @param {object} [opts]
264
+ * @param {boolean} [opts.color=true] - emit ANSI escape codes
265
+ * @param {boolean} [opts.verbose=false] - longer layout with labels
266
+ * @param {boolean} [opts.timer=true] - show TTL countdown segment
267
+ * @param {'text'|'icon'} [opts.mode='text'] - label style. 'icon' uses 🧠 ⏳ πŸ’° instead of word labels.
268
+ * @param {string[]|null} [opts.segments] - whitelist of segments to render. Names: cap-warn, spike, version, harness, korean, model, hit, ttl, saved, delegated, doc2md, ctx, period, plus per-window keys (`five_hour`, `seven_day`, …). `5h`/`7d` are kept as aliases for back-compat. Null/undefined = all.
269
+ * @param {boolean} [opts.singleLine=false] - force the legacy one-line layout. By default, when the delegation ledger has lifetime savings, the routing totals lead on their own first line and everything else moves to line 2 (Claude Code renders multi-line statuslines; `--single-line` is the escape hatch for terminals that only show the first line).
270
+ */
271
+ export function formatReport(data, { color = true, verbose = false, timer = true, mode = 'text', segments = null, singleLine = false } = {}) {
272
+ const { summary, ttl, cost, options, lastActivity, contextWindow, ctxLive, spikeChip, caps, model } = data;
273
+ const { hitRate } = summary;
274
+
275
+ // Hit rate β†’ color signal
276
+ const hitColor =
277
+ hitRate >= 0.85 ? GREEN :
278
+ hitRate >= 0.70 ? YELLOW :
279
+ RED;
280
+
281
+ // TTL dominance β†’ color signal (1h = good, 5m = warning).
282
+ // The subscription plan fixes this, so the bucket rarely changes β€” it's the countdown that matters.
283
+ // When ttl.total === 0 (no cache writes observed in the window), we cannot
284
+ // infer the bucket. Default to 1h-sized countdown rather than 5m so Max
285
+ // users on idle don't see a misleading "Cache expires 5:00". The bucket
286
+ // label is shown as "?" so the uncertainty is visible.
287
+ //
288
+ // That default is exactly backwards behind a gateway. Bedrock and Vertex
289
+ // never report the per-bucket split, so ttl.total stays 0 there forever, and
290
+ // they offer only the 5m bucket: the countdown opened at 59:59 for a window
291
+ // that was really 5:00, overstating it twelvefold. So the fallback now
292
+ // follows the evidence β€” gateway seen, assume 5m; otherwise keep 1h. An
293
+ // explicit `ttlBucket` setting outranks both, so a gateway that starts
294
+ // reporting the split correctly does not need a release to be believed.
295
+ const hasTtlData = ttl.total > 0;
296
+ const override = data.ttlBucket === '5m' || data.ttlBucket === '1h' ? data.ttlBucket : null;
297
+ const is1h = override ? override === '1h' : (hasTtlData ? ttl.pct1h >= 0.5 : !ttl.gatewayObserved);
298
+ // Three grades of certainty, three labels: measured (`1h`/`5m`), inferred
299
+ // from a gateway model id (`5m?`), and unknown (`?`). Folding the middle
300
+ // case into `?` would hide a judgement the user could otherwise check.
301
+ const bucketKnown = hasTtlData || !!override;
302
+ const bucketLabel = bucketKnown
303
+ ? (is1h ? '1h' : '5m')
304
+ : (ttl.gatewayObserved ? '5m?' : '?');
305
+ const bucketColor = bucketKnown
306
+ ? (is1h ? GREEN : YELLOW)
307
+ : (ttl.gatewayObserved ? YELLOW : GRAY);
308
+ const ttlSeconds = is1h ? 3600 : 300;
309
+
310
+ const savings = cost?.savings ?? 0;
311
+
312
+ const c = (v) => (color ? v : '');
313
+ const isIcon = mode === 'icon';
314
+
315
+ // Labels per mode.
316
+ // text: "Cache hit 98.3%" | verbose: "Cache hit 98.3%"
317
+ // icon: "🧠 98.3%" | verbose: "🧠 Cache hit 98.3%"
318
+ const hitLabel = isIcon
319
+ ? (verbose ? '🧠 Cache hit' : '🧠')
320
+ : 'Cache hit';
321
+ const hitSeg = `${c(BOLD)}${hitLabel}${c(RESET)} ${c(hitColor)}${formatPct(hitRate)}${c(RESET)}`;
322
+
323
+ // text: "Cache saved $1.5K" | same in verbose
324
+ // icon: "πŸ’° $1.5K" | verbose: "πŸ’° Cache saved $1.5K"
325
+ const saveLabel = isIcon
326
+ ? (verbose ? 'πŸ’° Cache saved' : 'πŸ’°')
327
+ : 'Cache saved';
328
+ const saveSeg = `${c(CYAN)}${saveLabel}${c(RESET)} ${formatMoney(savings)}`;
329
+
330
+ // Delegation savings β€” a DIFFERENT number from "Cache saved" above, which
331
+ // covers the prompt cache only. This one is what running work on a cheaper
332
+ // tier saved, summed from the rule registry route-scan maintains. Hidden
333
+ // when zero or absent: a permanent "$0" is noise for direct-API users and
334
+ // for anyone who has not delegated yet.
335
+ // "Routing saved" says what earned the money β€” work that ran on a cheaper
336
+ // model instead of this one. It leads the line rather than trailing it
337
+ // because it is the headline number of the whole tool, not a footnote.
338
+ // text: "Routing saved $3.2" | same in verbose
339
+ // icon: "πŸ”€ $3.2" | verbose: "πŸ”€ Routing saved $3.2"
340
+ const delegationSaved = Number(data.delegationSaved) || 0;
341
+ const delegateLabel = isIcon
342
+ ? (verbose ? 'πŸ”€ Routing saved' : 'πŸ”€')
343
+ : 'Routing saved';
344
+ // Zero savings has two very different causes and, until now, one appearance:
345
+ // nothing at all. "Never delegated" and "delegated plenty, but every run was
346
+ // dropped because the gateway model id could not be resolved" looked
347
+ // identical, so users in the second case had no reason to suspect anything
348
+ // was wrong. The count gets a chip; the explanation stays in `route-scan
349
+ // rules`, where there is room for it.
350
+ const unresolvedRuns = Number(data.unresolvedRuns) || 0;
351
+ const delegateSeg = delegationSaved > 0
352
+ ? `${c(GREEN)}${delegateLabel}${c(RESET)} ${formatMoney(delegationSaved)}`
353
+ : (unresolvedRuns > 0
354
+ ? `${c(YELLOW)}πŸ”€ ${unresolvedRuns} unresolved${c(RESET)}`
355
+ : null);
356
+
357
+ // Document conversions β€” the same kind of number as "Routing saved", earned
358
+ // a different way: a document read as Markdown instead of attached whole.
359
+ // Kept as its own chip rather than folded into the routing total, because a
360
+ // single figure could not tell the reader which habit earned it.
361
+ //
362
+ // Conversions that saved nothing measurable still show as a count. For
363
+ // pptx/xlsx/docx the honest saving is zero β€” the client extracts much the
364
+ // same text β€” and a chip that disappeared on those would read as "doc2md
365
+ // did nothing" on the very formats it is the only way to open.
366
+ // icon: "πŸ“„ $0.42" verbose: "πŸ“„ Doc2md saved $0.42 Β· 12 docs"
367
+ // text: "Doc2md saved $0.42"
368
+ const doc2md = data.doc2mdTotals;
369
+ const doc2mdUsd = Number(doc2md && doc2md.total) || 0;
370
+ const doc2mdDocs = Number(doc2md && doc2md.docs) || 0;
371
+ const doc2mdLabel = isIcon
372
+ ? (verbose ? 'πŸ“„ Doc2md saved' : 'πŸ“„')
373
+ : 'Doc2md saved';
374
+ let doc2mdSeg = null;
375
+ if (doc2mdUsd > 0) {
376
+ doc2mdSeg = `${c(GREEN)}${doc2mdLabel}${c(RESET)} ${formatMoney(doc2mdUsd)}`
377
+ + (verbose ? ` ${c(GRAY)}Β· ${doc2mdDocs} docs${c(RESET)}` : '');
378
+ } else if (doc2mdDocs > 0) {
379
+ const label = isIcon ? 'πŸ“„' : 'Doc2md';
380
+ doc2mdSeg = `${c(GRAY)}${label} ${doc2mdDocs} docs${c(RESET)}`;
381
+ }
382
+
383
+ // 이번 달 μ§€μΆœ μ„Έκ·Έλ¨ΌνŠΈ. 달λ ₯ μ›”(1일 00μ‹œ κΈ°μ€€) μ§€μΆœ μΆ”μ •μΉ˜λΌμ„œ cap 이 μ—†λŠ”
384
+ // κ²Œμ΄νŠΈμ›¨μ΄ μ‚¬μš©μžμ—κ²Œλ„ 항상 μ˜λ―Έκ°€ μžˆλ‹€. 정보성 μ§€ν‘œμ΄λ―€λ‘œ νšŒμƒ‰μœΌλ‘œ,
385
+ // κ²Œμ΄μ§€λ“€ 뒀에 쑰용히 λ†“λŠ”λ‹€.
386
+ // icon: "πŸ’΅ Sep $42.1"
387
+ // text: "Sep spend $42.1"
388
+ // verbose: "Sep spend $42.1 (since Sep 1)"
389
+ const month = data.monthSpend;
390
+ let monthSeg = null;
391
+ if (month && Number(month.usd) > 0) {
392
+ const amt = formatMoney(Number(month.usd));
393
+ if (isIcon) {
394
+ monthSeg = `${c(GRAY)}πŸ’΅ ${month.label} ${amt}${c(RESET)}`;
395
+ } else if (verbose) {
396
+ monthSeg = `${c(GRAY)}${month.label} spend ${amt} (since ${month.label} 1)${c(RESET)}`;
397
+ } else {
398
+ monthSeg = `${c(GRAY)}${month.label} spend ${amt}${c(RESET)}`;
399
+ }
400
+ }
401
+
402
+ // Routing-savings headline line (multi-line layout). The lifetime sum from
403
+ // the delegation ledger β€” the number the whole tool exists to grow, so it
404
+ // gets line 1 to itself while the diagnostics move to line 2.
405
+ //
406
+ // One figure, not three. The weekly and monthly sums used to sit here too,
407
+ // but the per-model breakdown that follows is a lifetime split, and next to
408
+ // a row of rolling windows it read as a breakdown of whichever one it
409
+ // touched. A single lifetime total makes the whole line one timeframe with
410
+ // nothing to mismatch.
411
+ // icon: "πŸ”€ Routing saved $9.8 | opusβ†’haiku 2Γ— $6.4 Β· fableβ†’sonnet 1Γ— $3.4"
412
+ // text: "Routing saved $9.8 | opus→haiku 2× $6.4 · fable→sonnet 1× $3.4"
413
+ const totals = data.delegationTotals;
414
+ let totalsLine = null;
415
+ if (!singleLine && totals && Number(totals.total) > 0) {
416
+ const head = isIcon ? 'πŸ”€ Routing saved' : 'Routing saved';
417
+ // Model changes behind the total, family-level and version-free: `opus β†’
418
+ // haiku 2Γ— $0.6`. Versions bump constantly and add nothing here β€” the
419
+ // shape of the trade is the point.
420
+ //
421
+ // Every pair is listed, not a top-N: the amounts are shown next to a
422
+ // total, so a truncated list reads as "this is what the total is made of"
423
+ // and quietly misstates it. Families collapse the list on their own β€”
424
+ // there are only so many tier-to-tier moves β€” so it stays short without
425
+ // being cut.
426
+ const pairs = Array.isArray(totals.pairs) ? totals.pairs : [];
427
+ // The breakdown stays entirely gray, amounts included. Only the total is
428
+ // green: it is the headline figure, and repeating that green on every
429
+ // component would flatten the line into one loud block with nothing to
430
+ // land on first.
431
+ const pairText = pairs
432
+ .map((p) => `${c(GRAY)}${p.from}β†’${p.to} ${p.runs}Γ— ${formatMoney(p.usd)}${c(RESET)}`)
433
+ .join(` ${c(GRAY)}Β·${c(RESET)} `);
434
+ totalsLine =
435
+ `${c(GREEN)}${c(BOLD)}${head}${c(RESET)} ` +
436
+ `${c(GREEN)}${formatMoney(Number(totals.total) || 0)}${c(RESET)}` +
437
+ (pairText ? ` ${c(GRAY)}|${c(RESET)} ${pairText}` : '');
438
+ }
439
+
440
+ // Doc2md's own headline line, same anatomy as the routing one: a green
441
+ // lifetime total, then a gray per-format breakdown. Only built when there
442
+ // is money to report β€” a bare document count stays an inline chip, since a
443
+ // whole line for "3 docs" would be all frame and no figure.
444
+ // icon: "πŸ“„ Doc2md saved $1.8 | pptx 1Γ— $1.55 Β· pdf 2Γ— $0.27"
445
+ let doc2mdLine = null;
446
+ if (!singleLine && doc2mdUsd > 0) {
447
+ const head = isIcon ? 'πŸ“„ Doc2md saved' : 'Doc2md saved';
448
+ const byExt = Array.isArray(doc2md.byExt) ? doc2md.byExt : [];
449
+ // Formats that earned money show it; formats with no baseline to measure
450
+ // against are counted instead. Printing "$0.00" next to real amounts reads
451
+ // as "this format saved nothing", when what it means is that nobody has
452
+ // measured what the alternative costs. Only .xls sits there now β€” the
453
+ // pre-2007 binary, which has no container to price against.
454
+ const paid = byExt.filter((r) => r.usd > 0);
455
+ const unpriced = byExt.filter((r) => !(r.usd > 0));
456
+ const parts = paid.map((r) => `${c(GRAY)}${r.ext} ${r.docs}Γ— ${formatMoney(r.usd)}${c(RESET)}`);
457
+ if (unpriced.length) {
458
+ const counted = unpriced.map((r) => `${r.ext} ${r.docs}Γ—`).join(' Β· ');
459
+ parts.push(`${c(GRAY)}${counted} ${verbose ? '(no baseline)' : '(n/a)'}${c(RESET)}`);
460
+ }
461
+ const extText = parts.join(` ${c(GRAY)}Β·${c(RESET)} `);
462
+ doc2mdLine =
463
+ `${c(GREEN)}${c(BOLD)}${head}${c(RESET)} ` +
464
+ `${c(GREEN)}${formatMoney(doc2mdUsd)}${c(RESET)}` +
465
+ (extText ? ` ${c(GRAY)}|${c(RESET)} ${extText}` : '');
466
+ }
467
+
468
+ // Period label honors hour-precision configs (`mode 6h` β†’ "6h", `mode 1d` β†’ "1d").
469
+ // Fall back to legacy `${days}d` when callers haven't supplied a label.
470
+ const periodLabel = options.windowLabel || `${options.days}d`;
471
+ const periodSeg = verbose
472
+ ? `${c(GRAY)}last ${periodLabel}${c(RESET)}`
473
+ : `${c(GRAY)}${periodLabel}${c(RESET)}`;
474
+
475
+ // TTL countdown β€” how much time is left on the last API call's cache entry.
476
+ // Matches Anthropic's actual prompt-cache behaviour: each call starts a fresh
477
+ // TTL window, and the next call (hit) within that window resets it. So the
478
+ // countdown visibly ticks down between prompts, and "resets" happens as a
479
+ // jump back toward the bucket max the moment you send another message.
480
+ // Compact modes drop the bucket label β€” it's read as part of the clock
481
+ // ("1h 59:58" gets parsed as "1 hour 59 minutes 58 seconds"). The bucket
482
+ // is plan-determined and rarely changes, so verbose mode is where it belongs.
483
+ // text compact: "Expires 59:58"
484
+ // text verbose: "1h bucket Β· expires in 59:58"
485
+ // icon compact: "⏳ 59:58"
486
+ // icon verbose: "⏳ Expires 1h 59:58"
487
+ let ttlSeg;
488
+ if (timer && lastActivity) {
489
+ // Coerce to a numeric ms timestamp. Some upstream paths handed in a Date,
490
+ // a stringified ISO timestamp, or epoch-seconds β€” any of which silently
491
+ // produces NaN/huge values when subtracted from Date.now(), which then
492
+ // bypasses formatTimer's normal MM:SS shape.
493
+ const laMs =
494
+ typeof lastActivity === 'number'
495
+ ? (lastActivity < 1e12 ? lastActivity * 1000 : lastActivity) // seconds β†’ ms
496
+ : (lastActivity instanceof Date ? lastActivity.getTime() : Date.parse(lastActivity));
497
+ const elapsed = Number.isFinite(laMs) ? (Date.now() - laMs) / 1000 : Infinity;
498
+ // Clamp remaining into the bucket so a clock-skew or stale-state edge case
499
+ // can't display a value larger than the bucket itself.
500
+ const remaining = Math.min(ttlSeconds, ttlSeconds - elapsed);
501
+ const text = formatTimer(remaining);
502
+ const pct = remaining / ttlSeconds;
503
+ // Percentages are the wrong unit in a 5-minute bucket: 30% of it is 90
504
+ // seconds, and green there reads as comfort the user does not have. Below
505
+ // an hour the thresholds are absolute, so the color tracks whether there
506
+ // is time to finish a thought rather than a share of a short window.
507
+ const timerColor =
508
+ remaining <= 0 ? RED :
509
+ is1h
510
+ ? (pct > 0.30 ? GREEN : pct > 0.10 ? YELLOW : RED)
511
+ : (remaining > 60 ? GREEN : remaining > 30 ? YELLOW : RED);
512
+
513
+ if (isIcon && verbose) {
514
+ // Drop bucket here too β€” `⏳ Expires 1h 57:20` reads as "1h 57m 20s left"
515
+ // for the same reason the compact form did. The bucket lives in the
516
+ // text-verbose layout where the "bucket" word + `Β·` separator make it
517
+ // unambiguous.
518
+ ttlSeg = `${c(timerColor)}⏳ Cache expires ${text}${c(RESET)}`;
519
+ } else if (isIcon) {
520
+ ttlSeg = `${c(timerColor)}⏳ ${text}${c(RESET)}`;
521
+ } else if (verbose) {
522
+ ttlSeg = `${c(bucketColor)}Cache ${bucketLabel} bucket${c(RESET)} Β· ${c(timerColor)}expires in ${text}${c(RESET)}`;
523
+ } else {
524
+ ttlSeg = `${c(timerColor)}Cache expires ${text}${c(RESET)}`;
525
+ }
526
+ } else {
527
+ // No-timer fallback: only the bucket is available, so we show just that.
528
+ if (isIcon) {
529
+ const prefix = verbose ? '⏳ Cache bucket ' : '⏳ ';
530
+ ttlSeg = `${c(bucketColor)}${prefix}${bucketLabel}${c(RESET)}`;
531
+ } else if (verbose) {
532
+ ttlSeg = `${c(bucketColor)}Cache ${bucketLabel} bucket${c(RESET)}`;
533
+ } else {
534
+ ttlSeg = `${c(bucketColor)}Cache bucket ${bucketLabel}${c(RESET)}`;
535
+ }
536
+ }
537
+
538
+ // Context chip. Two data sources, best first:
539
+ //
540
+ // 1. Live fill level from Claude Code's stdin (`context_window.used_percentage`)
541
+ // β€” the current session's actual usage, refreshed every render. Rendered
542
+ // as `πŸ“¦ 68%` and colored by fill (green <70, yellow 70–89, red 90+),
543
+ // matching the cap-segment tone scale.
544
+ // 2. Fallback (table view / older Claude Code): transcript-inferred window
545
+ // size. Note the semantics: `size === '1M'` means a real request already
546
+ // carried >210k input tokens β€” actual heavy usage, not just the model
547
+ // supporting 1M. Current models are all 1M by default with no price
548
+ // premium, so this renders yellow ("your context is genuinely big"),
549
+ // not red ("expensive mode on") like it used to.
550
+ let ctxSeg = null;
551
+ if (ctxLive && Number.isFinite(ctxLive.usedPct)) {
552
+ const pct = Math.max(0, Math.round(ctxLive.usedPct));
553
+ const tone = pct >= 90 ? RED : pct >= 70 ? YELLOW : GREEN;
554
+ const sizeLabel = ctxLive.size
555
+ ? (ctxLive.size >= 900_000 ? '1M' : `${Math.round(ctxLive.size / 1000)}k`)
556
+ : null;
557
+ const longLabel = sizeLabel ? `${pct}% of ${sizeLabel}` : `${pct}%`;
558
+ if (isIcon && verbose) {
559
+ ctxSeg = `${c(tone)}πŸ“¦ Ctx ${longLabel}${c(RESET)}`;
560
+ } else if (isIcon) {
561
+ ctxSeg = `${c(tone)}πŸ“¦ ${pct}%${c(RESET)}`;
562
+ } else {
563
+ ctxSeg = `${c(tone)}Ctx ${longLabel}${c(RESET)}`;
564
+ }
565
+ } else if (contextWindow && contextWindow.size && contextWindow.size !== 'unknown') {
566
+ const label = contextWindow.size === '1M' ? '1M' : '200k';
567
+ // Yellow is reserved for the 500k warn line. A 1M window by itself is the
568
+ // default on every current model, so colouring it yellow cried wolf.
569
+ const ctxColor = contextWindow.overWarn ? YELLOW : GREEN;
570
+ if (isIcon && verbose) {
571
+ ctxSeg = `${c(ctxColor)}πŸ“¦ Ctx ${label}${c(RESET)}`;
572
+ } else if (isIcon) {
573
+ ctxSeg = `${c(ctxColor)}πŸ“¦ ${label}${c(RESET)}`;
574
+ } else {
575
+ ctxSeg = `${c(ctxColor)}Ctx ${label}${c(RESET)}`;
576
+ }
577
+ }
578
+
579
+ // Spike chip β€” one word only, keeps the statusline single-line.
580
+ const spikeSeg = spikeChip ? `${c(RED)}${spikeChip}${c(RESET)}` : null;
581
+
582
+ // Harness πŸ…· N/5 β€” project-scoped completeness of CLAUDE.md harness rules.
583
+ // Silent when the project hasn't opted in (no CLAUDE.md and no .claude/);
584
+ // otherwise renders πŸ…· 5/5 (green) / πŸ…· N/5 (yellow) so the user can spot
585
+ // a missing section at a glance and know to run `harness init`.
586
+ const harnessSeg = buildHarnessSeg(c, isIcon);
587
+
588
+ // Version / upgrade chip. Read from a cache written by a detached background
589
+ // check β€” this render path never touches the network.
590
+ const versionSeg = buildVersionSeg(options.version, data.update, c, isIcon, verbose);
591
+ const updateAvailable = !!(data.update && data.update.available);
592
+
593
+ // Korean-style chip β€” rendered only when the session-start injection is on.
594
+ const koreanSeg = buildKoreanSeg(c, isIcon, verbose);
595
+
596
+ // Model chip β€” pulled from Claude Code's stdin payload (`model.display_name`).
597
+ // Cheap identity context: useful when the user toggles between Sonnet/Opus
598
+ // mid-session and wants to confirm at a glance which one is answering.
599
+ let modelSeg = null;
600
+ if (typeof model === 'string' && model.length > 0) {
601
+ // πŸ€– + name is enough β€” the emoji disambiguates so the literal word "Model"
602
+ // is dead weight in icon mode. Text modes keep the bare name; the magenta
603
+ // tone marks it as identity context.
604
+ if (isIcon) {
605
+ modelSeg = `${c(MAGENTA)}πŸ€– ${model}${c(RESET)}`;
606
+ } else {
607
+ modelSeg = `${c(MAGENTA)}${model}${c(RESET)}`;
608
+ }
609
+ }
610
+
611
+ // Always-on usage segments β€” what /usage shows in Claude Code, mirrored
612
+ // to the statusline so the user doesn't have to slash-command for it.
613
+ // Today the stdin payload exposes the 5h ("Current session") and 7-day
614
+ // rolling ("Current week") windows; if Anthropic ships more (e.g. a
615
+ // Sonnet-only weekly), they render automatically with derived labels.
616
+ // Each renders as `{label} {pct}% Β· {countdown}`. The window promoted to the
617
+ // cap-warn chip is suppressed here to avoid duplicate noise β€” but ONLY that
618
+ // one. When several windows are at 90%+ the chip shows just the most
619
+ // imminent, so the others must keep their always-on segment (red) or they'd
620
+ // vanish from the statusline entirely at the worst possible moment.
621
+ function buildUsageSeg({ labels, info, color: tone, suppressed }) {
622
+ if (!info || !Number.isFinite(info.usedPct)) return null;
623
+ if (suppressed) return null; // cap-warn chip handles this window
624
+ const pct = Math.round(info.usedPct);
625
+ // Show only the wall-clock reset time (e.g. `πŸ”„ 21:10`). Absolute time
626
+ // doesn't tick second-by-second so the statusline reads stable, and the
627
+ // πŸ”„ icon itself separates the percent from the clock β€” no extra `Β·` needed.
628
+ const clock = formatResetClock(info.resetsAt);
629
+ const tail = clock ? ` πŸ”„ ${clock}` : '';
630
+ // `cap` reads as a rate-limit ceiling rather than a duration. Icon mode
631
+ // leans on the icon to identify the window (✦ = session/now, πŸ“… = week),
632
+ // so the 5H label is empty while the 7D label spells out "weekly". Text and
633
+ // verbose modes keep the `5H`/`7D` short label since they have no icon.
634
+ // Icon mode renders an inline β–°β–± gauge instead of the literal "cap used" β€”
635
+ // a glance at the bar conveys urgency faster than parsing a percent number,
636
+ // and the gauge stays the same width as the percent climbs.
637
+ // LiteLLM μ˜ˆμ‚° μœˆλ„μš°λŠ” νΌμ„ΌνŠΈλ§ŒμœΌλ‘œλŠ” 감이 μ•ˆ μ˜€λ―€λ‘œ(μ˜ˆμ‚° 크기λ₯Ό λͺ¨λ¦„)
638
+ // κΈˆμ•‘μ„ ν•¨κ»˜ 보여 μ€€λ‹€: `πŸ”‘ budget β–°β–± 34% $34/$100`.
639
+ const money =
640
+ Number.isFinite(info.maxBudget) && info.maxBudget > 0
641
+ ? ` ${formatMoney(Number(info.spend) || 0)}/${formatMoney(info.maxBudget)}`
642
+ : '';
643
+ if (isIcon) {
644
+ const labelPart = labels.usageLabel ? `${labels.usageLabel} ` : '';
645
+ const bar = gaugeBar(pct);
646
+ return `${c(tone)}${labels.icon} ${labelPart}${bar} ${pct}%${money}${tail}${c(RESET)}`;
647
+ }
648
+ if (verbose) {
649
+ return `${c(tone)}${labels.short} cap ${pct}% used${money}${tail}${c(RESET)}`;
650
+ }
651
+ return `${c(tone)}${labels.short} cap ${pct}%${money}${tail}${c(RESET)}`;
652
+ }
653
+ // Color tone: green <70%, yellow 70-89%, red 90+% (a 90+% window only
654
+ // renders here when a *different* window won the cap-warn chip slot).
655
+ function usageTone(info) {
656
+ if (!info || !Number.isFinite(info.usedPct)) return GRAY;
657
+ if (info.usedPct >= 90) return RED;
658
+ if (info.usedPct >= 70) return YELLOW;
659
+ return GREEN;
660
+ }
661
+ // Cap-warn chip β€” leads everything when ANY rate-limit window is at 90%+.
662
+ // It's the most actionable signal we can show: no point optimizing cache
663
+ // hits if you're about to be rate-limited anyway. The chip body matches the
664
+ // English shape `🚨 5H 94%` / `🚨 7D 92%` so history parsers can dedupe on it.
665
+ // Computed before the usage segments so they know which window it claimed.
666
+ const capWarn = pickCapWarn(caps);
667
+ const usageSegs = [];
668
+ if (caps && Array.isArray(caps.windows)) {
669
+ for (const win of caps.windows) {
670
+ const labels = labelForKey(win.key);
671
+ const seg = buildUsageSeg({
672
+ labels,
673
+ info: win,
674
+ color: usageTone(win),
675
+ suppressed: !!capWarn && capWarn.key === win.key,
676
+ });
677
+ if (seg) usageSegs.push({ key: win.key, seg });
678
+ }
679
+ }
680
+
681
+ const capWarnSeg = buildCapWarnSeg(capWarn, c, isIcon);
682
+
683
+ // Warning chip leads β€” a glance at the statusline catches "something's wrong"
684
+ // before parsing any numbers. Healthy states have no chip and look unchanged.
685
+ // Cap-warn outranks spike: an imminent rate-limit block is more urgent than
686
+ // a single spiking session.
687
+ const allow = segments && segments.length
688
+ ? new Set(segments.map((s) => s.toLowerCase()))
689
+ : null;
690
+ const want = (name) => !allow || allow.has(name);
691
+ // Legacy whitelist aliases: `5h` ↔ `five_hour`, `7d` ↔ `seven_day`. So
692
+ // existing `--segments` configs keep working after the generic refactor.
693
+ const usageWant = (key) => {
694
+ if (!allow) return true;
695
+ if (allow.has(key.toLowerCase())) return true;
696
+ if (key === 'five_hour' && allow.has('5h')) return true;
697
+ if (key === 'seven_day' && allow.has('7d')) return true;
698
+ return false;
699
+ };
700
+ const segs = [];
701
+ if (capWarnSeg && want('cap-warn')) segs.push(capWarnSeg);
702
+ if (spikeSeg && want('spike')) segs.push(spikeSeg);
703
+ // An available upgrade rides up front with the other "act on this" chips.
704
+ // When there is nothing to upgrade to, the same segment is pure identity and
705
+ // sits at the tail instead (pushed after `saved`, below).
706
+ if (versionSeg && updateAvailable && want('version')) segs.push(versionSeg);
707
+ if (harnessSeg && want('harness')) segs.push(harnessSeg);
708
+ if (koreanSeg && want('korean')) segs.push(koreanSeg);
709
+ if (modelSeg && want('model')) segs.push(modelSeg);
710
+ // Delegation savings ride up front, next to the model that would otherwise
711
+ // have done the work. "Cache saved" stays at the tail: it is a lifetime brag
712
+ // stat, while this one is the point of the tool.
713
+ // When the totals headline owns line 1, the inline session chip would
714
+ // repeat the same story on line 2 β€” drop it there.
715
+ if (delegateSeg && want('delegated') && !totalsLine) segs.push(delegateSeg);
716
+ // Only when it did not already earn a headline line above.
717
+ if (doc2mdSeg && want('doc2md') && !doc2mdLine) segs.push(doc2mdSeg);
718
+ if (want('hit')) segs.push(hitSeg);
719
+ if (want('ttl')) segs.push(ttlSeg);
720
+ for (const { key, seg } of usageSegs) {
721
+ if (usageWant(key)) segs.push(seg);
722
+ }
723
+ if (monthSeg && want('month')) segs.push(monthSeg);
724
+ if (ctxSeg && want('ctx')) segs.push(ctxSeg);
725
+ // Cache saved is the "lifetime brag" stat β€” useful but not actionable, so
726
+ // it sits near the tail. The period label closes the line as a quiet
727
+ // timeframe footer.
728
+ if (want('saved')) segs.push(saveSeg);
729
+ if (versionSeg && !updateAvailable && want('version')) segs.push(versionSeg);
730
+ if (want('period')) segs.push(periodSeg);
731
+ // Trailing erase-to-end-of-line so any leftover characters from a previous
732
+ // (longer) statusline render don't bleed into ours. \x1b[K is the standard
733
+ // "erase from cursor to EOL" CSI. Only emitted when color (i.e. ANSI) is
734
+ // allowed β€” --no-color/NO_COLOR consumers expect escape-free output.
735
+ const eol = color ? '\x1b[K' : '';
736
+ const rest = segs.join(' Β· ') + eol;
737
+ // Each savings source that earned real money gets a headline line, ordered
738
+ // biggest saver first β€” the top line is the one the eye lands on, so it
739
+ // goes to whichever habit is actually paying for the tool. The diagnostics
740
+ // line always closes.
741
+ const headlines = [];
742
+ if (totalsLine && want('delegated')) {
743
+ headlines.push({ usd: Number(totals && totals.total) || 0, line: totalsLine });
744
+ }
745
+ if (doc2mdLine && want('doc2md')) {
746
+ headlines.push({ usd: doc2mdUsd, line: doc2mdLine });
747
+ }
748
+ headlines.sort((a, b) => b.usd - a.usd);
749
+ return headlines.map((h) => h.line + eol + '\n').join('') + rest;
750
+ }