claude-token-saver 3.42.0 โ 3.42.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.ko.md +86 -84
- package/README.md +86 -84
- package/bin/cli.js +51 -51
- package/package.json +1 -1
- package/src/advice.js +3 -3
- package/src/brief.js +2 -2
- package/src/commands/compact-window.js +9 -9
- package/src/commands/feedback.js +2 -2
- package/src/commands/install.js +7 -7
- package/src/commands/last.js +5 -5
- package/src/commands/mode.js +4 -4
- package/src/commands/route-scan.js +4 -4
- package/src/commands/seed.js +10 -10
- package/src/handoff.js +1 -1
- package/src/history.js +1 -1
- package/src/installer.js +21 -21
- package/src/korean-style.js +6 -2
- package/src/model-rules.js +2 -2
package/README.md
CHANGED
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
|
|
18
18
|
---
|
|
19
19
|
|
|
20
|
-
# Sprag (
|
|
20
|
+
# Sprag (sprag)
|
|
21
21
|
|
|
22
22
|
**Shows what it saved, on two lines.** It moves the easy work your expensive model keeps repeating onto cheaper ones, and turns documents the model cannot read into Markdown. Both figures are ledger entries rather than estimates, and whichever saved more takes the top line. Zero dependencies, one-line install.
|
|
23
23
|
|
|
@@ -25,6 +25,8 @@
|
|
|
25
25
|
|
|
26
26
|
```bash
|
|
27
27
|
npm i -g sprag-cli # or: npm i -g claude-token-saver (same package, old name)
|
|
28
|
+
# already have claude-token-saver? switching names needs a swap (same bins):
|
|
29
|
+
# npm uninstall -g claude-token-saver && npm i -g sprag-cli
|
|
28
30
|
```
|
|
29
31
|
|
|
30
32
|
Four numbers are the whole pitch.
|
|
@@ -69,14 +71,14 @@ The two savings figures are never added together, because they answer different
|
|
|
69
71
|
|
|
70
72
|
```bash
|
|
71
73
|
npm uninstall -g claude-cache-monitor # (previous-package users only)
|
|
72
|
-
npm i -g
|
|
74
|
+
npm i -g sprag-cli
|
|
73
75
|
```
|
|
74
76
|
|
|
75
|
-
The statusline appears at the bottom of Claude Code right away. If auto-registration was skipped (`--ignore-scripts`, sudo, sandboxed installs), run `
|
|
77
|
+
The statusline appears at the bottom of Claude Code right away. If auto-registration was skipped (`--ignore-scripts`, sudo, sandboxed installs), run `sprag install`.
|
|
76
78
|
|
|
77
79
|
One install sets up everything: **statusline, Skill, SessionStart hook, the ๐
ท Harness (5 principles), and a first route-scan.** The harness and the Korean writing guidance **show what they add and ask before enabling it.** The harness is **appended** to `~/.claude/CLAUDE.md` as a marked block (your existing content is backed up and preserved) and is left alone if one is already there.
|
|
78
80
|
|
|
79
|
-
Outside a terminal โ npm `postinstall`, CI, piped stdin โ the question is skipped and the old defaults apply. Use `--yes` or `--no-input` to skip it deliberately, `CTS_NO_HARNESS=1 npm i -g
|
|
81
|
+
Outside a terminal โ npm `postinstall`, CI, piped stdin โ the question is skipped and the old defaults apply. Use `--yes` or `--no-input` to skip it deliberately, `CTS_NO_HARNESS=1 npm i -g sprag-cli` to skip the harness entirely, and `sprag harness uninit --global` to undo it.
|
|
80
82
|
|
|
81
83
|
> โ ๏ธ Avoid `sudo` global installs โ the Skill lands in root's `~/.claude` instead of yours. Use nvm/fnm/Volta or `npm config set prefix ~/.npm-global`.
|
|
82
84
|
|
|
@@ -86,7 +88,7 @@ Everything that costs nothing until it is needed is on after a plain install. Th
|
|
|
86
88
|
|
|
87
89
|
| Feature | After install | How to turn it off |
|
|
88
90
|
|---|---|---|
|
|
89
|
-
| statusline (diagnostic chips, savings ledger) | on | `
|
|
91
|
+
| statusline (diagnostic chips, savings ledger) | on | `sprag uninstall` |
|
|
90
92
|
| `/claude-token-saver` Skill | on | same |
|
|
91
93
|
| SessionStart hook (route-scan refresh) | on | same |
|
|
92
94
|
| UserPromptSubmit hook (brief injection) | on | same |
|
|
@@ -117,7 +119,7 @@ Every delegated run is recorded like this:
|
|
|
117
119
|
```
|
|
118
120
|
|
|
119
121
|
```bash
|
|
120
|
-
$
|
|
122
|
+
$ sprag route-scan savings # trace every dollar back to its rule
|
|
121
123
|
|
|
122
124
|
๐ Routing saved, lifetime $2.09 (last 7d $1.40 ยท 30d $2.09)
|
|
123
125
|
|
|
@@ -164,10 +166,10 @@ Teams shipping routing products have turned the feature off for exactly this rea
|
|
|
164
166
|
So this tool never touches the main session's model. It delegates to **subagents only**, which leaves the main session's cache intact and runs the delegated work on a cheap model in its own context. That is why the savings are not cancelled out by cache loss.
|
|
165
167
|
|
|
166
168
|
```bash
|
|
167
|
-
npm i -g
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
169
|
+
npm i -g sprag-cli@latest
|
|
170
|
+
sprag route-scan # find delegation candidates in your own history (0 LLM calls)
|
|
171
|
+
sprag route-scan rules # list promoted rules ยท rm <N> to remove
|
|
172
|
+
sprag route-scan savings # audit every dollar the routing saved
|
|
171
173
|
```
|
|
172
174
|
|
|
173
175
|
Thresholds come from **your own last-14-day distribution (p25/p75)**, not someone else's benchmark.
|
|
@@ -212,7 +214,7 @@ Chips โ `๐จ 5H/7D NN%` (cap imminent) ยท `โ Ctx 500k+` (a single request a
|
|
|
212
214
|
|
|
213
215
|
### When a chip appears
|
|
214
216
|
|
|
215
|
-
Run the `/claude-token-saver` Skill inside Claude โ or just say the chip wording ("5H cap is up", "cache miss") and it auto-activates. The Skill surfaces the **root-cause code + step-by-step fix**. When a cap is imminent, run `
|
|
217
|
+
Run the `/claude-token-saver` Skill inside Claude โ or just say the chip wording ("5H cap is up", "cache miss") and it auto-activates. The Skill surfaces the **root-cause code + step-by-step fix**. When a cap is imminent, run `sprag handoff` to back up your work state to markdown and continue in a fresh session.
|
|
216
218
|
|
|
217
219
|
## Commands
|
|
218
220
|
|
|
@@ -220,27 +222,27 @@ Run these in your shell (inside Claude Code, the `/claude-token-saver` Skill is
|
|
|
220
222
|
|
|
221
223
|
| Command | What it does |
|
|
222
224
|
|---|---|
|
|
223
|
-
| `
|
|
224
|
-
| `
|
|
225
|
-
| `
|
|
226
|
-
| `
|
|
227
|
-
| `
|
|
228
|
-
| `
|
|
229
|
-
| `
|
|
230
|
-
| `
|
|
231
|
-
| `
|
|
232
|
-
| `
|
|
233
|
-
| `
|
|
234
|
-
| `
|
|
235
|
-
| `
|
|
236
|
-
| `
|
|
237
|
-
| `
|
|
238
|
-
| `
|
|
239
|
-
| `
|
|
240
|
-
| `
|
|
241
|
-
| `
|
|
242
|
-
| `
|
|
243
|
-
| `
|
|
225
|
+
| `sprag` | Last-1-day diagnostic report (`--days N` / `--hours N`) |
|
|
226
|
+
| `sprag last` | Most recent warning + remediation |
|
|
227
|
+
| `sprag history` | Last 7 days of warning transitions |
|
|
228
|
+
| `sprag handoff` | Back work up to `HANDOFF-*.md` before a cap blocks you |
|
|
229
|
+
| `sprag mode [keywords...]` | Output config (`icon`/`text`, `en`/`ko`, `1h`โ`30d` window, โฆ) |
|
|
230
|
+
| `sprag harness ...` | ๐
ท Harness management (below) |
|
|
231
|
+
| `sprag route-scan` | Detect recurring easy work on expensive models โ propose haiku-delegation ratchet rules (below) |
|
|
232
|
+
| `sprag route-scan savings` | The routing-savings ledger โ per-model-change rollup + per-run log (the evidence behind the figure) |
|
|
233
|
+
| `sprag compact-window` | Warn when a 1M-context session has no auto-compact cap โ pin 400k with `set` (below) |
|
|
234
|
+
| `sprag korean on\|off\|status` | Inject Korean writing guidance at session start and install the write-time check (below) |
|
|
235
|
+
| `sprag cohesion on\|off\|status\|show` | Inject English cohesion guidance (sentence-connection rules) at session start |
|
|
236
|
+
| `sprag korean lint block\|warn\|off` | How the write-time check handles findings |
|
|
237
|
+
| `sprag korean lint scope all\|prose` | Check every text file, or documents only |
|
|
238
|
+
| `sprag doc2md on\|off` | Convert attached documents to Markdown before the model reads them (below) |
|
|
239
|
+
| `sprag doc2md <file>` | Convert one file by hand. Diagnostic: it prints the refusal reason instead of swallowing it |
|
|
240
|
+
| `sprag mode ttl=5m\|1h\|auto` | Pin the cache TTL bucket. The default `auto` trusts the measured split, then falls back to gateway detection |
|
|
241
|
+
| `sprag --version` | Print the installed version |
|
|
242
|
+
| `sprag update-check` | Is a newer version out? (`--refresh` to ask now, `--dismiss` to mute this version's offer) |
|
|
243
|
+
| `sprag upgrade` | Install the latest release with the package manager that installed this copy (`--print` shows the command only) |
|
|
244
|
+
| `sprag install` | Manually register Skill + statusline |
|
|
245
|
+
| `sprag uninstall [--purge]` | Remove the hooks, statusline and skill it registered. Recorded savings are kept unless `--purge` is given |
|
|
244
246
|
|
|
245
247
|
The output language is decided once, at install time: a terminal install proposes the system locale and asks whether to use Korean, while an unattended install records what the locale says. Once recorded it is never asked again, not even on an upgrade. Change it later with `mode ko` / `mode en`, or pin it for a scripted install with `CTS_LANG=ko` / `CTS_LANG=en`. Statusline chips stay symbolic either way.
|
|
246
248
|
|
|
@@ -268,8 +270,8 @@ The output language is decided once, at install time: a terminal install propose
|
|
|
268
270
|
A statusline cannot open a dialog, and it re-renders every ~300ms, so it can never touch the network while drawing. The notification is therefore split in two:
|
|
269
271
|
|
|
270
272
|
- **The statusline tells you.** Up to date: a quiet gray `v3.24.0` at the tail. Newer release out: `โฌ v3.24.0 โ 3.25.0` in yellow, moved to the front. Never red โ nothing is broken.
|
|
271
|
-
- **Session start asks you.** On a new session or `/clear`, the SessionStart hook injects one line telling the model a newer version exists and to ask before installing anything. Only after you agree does it run `
|
|
272
|
-
- **Declining sticks.** `
|
|
273
|
+
- **Session start asks you.** On a new session or `/clear`, the SessionStart hook injects one line telling the model a newer version exists and to ask before installing anything. Only after you agree does it run `sprag upgrade`.
|
|
274
|
+
- **Declining sticks.** `sprag update-check --dismiss` mutes the offer for that version; the next release asks again. The statusline chip stays โ you declined the question, not the fact.
|
|
273
275
|
|
|
274
276
|
The registry lookup runs at most once every 24h in a detached background process and only ever writes a cache file (`update-check.json`) โ the same shape npm's `update-notifier` uses. A failed check still stamps its timestamp, so an offline machine backs off instead of retrying on every render. Turn checks off entirely with `CTS_NO_UPDATE_CHECK=1` or `NO_UPDATE_NOTIFIER`.
|
|
275
277
|
|
|
@@ -278,15 +280,15 @@ The registry lookup runs at most once every 24h in a detached background process
|
|
|
278
280
|
Bootstrap five engineering principles (Ratchet ยท Evidence ยท PEV ยท Structured Task ยท Default Safe Path) into `CLAUDE.md` with one command; the statusline scores it as `๐
ท 5/5`. When the same error keeps recurring, a `๐
ทโ ratchet?` nudge appears so you can promote it to a rule.
|
|
279
281
|
|
|
280
282
|
```bash
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
283
|
+
sprag harness init # this project
|
|
284
|
+
sprag harness init --global # ~/.claude/CLAUDE.md โ every project
|
|
285
|
+
sprag harness check # current score (global fallback honored)
|
|
286
|
+
sprag harness analyze # run the transcript analysis manually (no hook needed); refreshes harness-state.json
|
|
287
|
+
sprag harness promote <N> --project|--global # warning #N โ ratchet rule (scope required)
|
|
288
|
+
sprag harness promote "<rule text>" --project|--global # register your own hand-written rules the same way
|
|
289
|
+
sprag harness pull # register the package's curated ratchet rules into your global ratchet (opt-in, dedupes)
|
|
290
|
+
sprag harness list / rm <N> # view / delete rules (auto .bak)
|
|
291
|
+
sprag harness off | on # toggle the ๐
ท chip
|
|
290
292
|
```
|
|
291
293
|
|
|
292
294
|
- `promote` **requires** `--project`/`--global` in non-TTY contexts (scripts, LLM calls) โ a scope choice is never silently made for the caller.
|
|
@@ -316,11 +318,11 @@ Claude Code compacts when usage approaches `min(autoCompactWindow, model max con
|
|
|
316
318
|
**200k sessions are never warned** โ their window is already at or below 200k, so the setting cannot change anything.
|
|
317
319
|
|
|
318
320
|
```bash
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
321
|
+
sprag compact-window # status (model, window, value, source)
|
|
322
|
+
sprag compact-window set --global # pin 500k (mid-band) in ~/.claude/settings.json
|
|
323
|
+
sprag compact-window set --project # pin it in <root>/.claude/settings.json
|
|
324
|
+
sprag compact-window set --global --value 600k # explicit value (100kโ1M)
|
|
325
|
+
sprag compact-window off | on # toggle the warning
|
|
324
326
|
```
|
|
325
327
|
|
|
326
328
|
- On a 1M model with the value unset or above 700k, the statusline shows `๐
ทโ compact-window?` and the session briefing hands the model the exact registration command.
|
|
@@ -342,11 +344,11 @@ Three design pillars:
|
|
|
342
344
|
3. Promoted rules live in a tool-owned file (`.claude/ratchet-model.md`) that **refreshes itself every scan**, and a `โ rule-health` flag fires when a delegated category's error rate climbs โ rules report their own staleness
|
|
343
345
|
|
|
344
346
|
```bash
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
347
|
+
sprag route-scan # scan (24h cache) + tiered candidates
|
|
348
|
+
sprag harness promote R1 --project # promote candidate R1 to a model-fitting rule
|
|
349
|
+
sprag route-scan dismiss 1 # not interested โ won't resurface
|
|
350
|
+
sprag route-scan rules # list model-fitting rules (rm <N> to remove)
|
|
351
|
+
sprag route-scan savings # the savings ledger โ which rule moved work off which model, onto which
|
|
350
352
|
```
|
|
351
353
|
|
|
352
354
|
Dig deeper: **tier criteria & research evidence** โ [docs/TIER_CRITERIA.md](./docs/TIER_CRITERIA.md) (Korean) ยท **rule-file mechanics, scan triggers, subagent setup** โ [docs/ROUTE_SCAN.md](./docs/ROUTE_SCAN.md) (Korean + English)
|
|
@@ -387,11 +389,11 @@ The model-fitting ratchet (`ratchet-model.md`) **starts empty.** A rule exists o
|
|
|
387
389
|
**How they get registered:** in the first session after an install or upgrade, the SessionStart hook hands the pending presets to the model, which walks the user through them **one at a time**. Each answer runs one of these immediately:
|
|
388
390
|
|
|
389
391
|
```bash
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
392
|
+
sprag seed # pending presets + recorded answers
|
|
393
|
+
sprag seed accept <id> --global|--project # register one (scope required)
|
|
394
|
+
sprag seed accept all --global # when the user says "register them all"
|
|
395
|
+
sprag seed skip <id> # decline โ never offered again
|
|
396
|
+
sprag seed reset # clear the answers and offer everything again
|
|
395
397
|
```
|
|
396
398
|
|
|
397
399
|
- **Nothing is written without a yes to that specific rule.** A declined rule stays declined across upgrades; a later release only surfaces the presets it actually added.
|
|
@@ -404,10 +406,10 @@ claude-token-saver seed reset # clear the answers a
|
|
|
404
406
|
Injects guidance that corrects how Claude writes Korean (dropped sentence parts, noun-stopped sentences, translationese, em-dash overuse) **once per session.**
|
|
405
407
|
|
|
406
408
|
```bash
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
409
|
+
sprag korean on # on, for every project
|
|
410
|
+
sprag korean status # state, cost, provenance
|
|
411
|
+
sprag korean show # print the guidance itself
|
|
412
|
+
sprag korean off # off
|
|
411
413
|
```
|
|
412
414
|
|
|
413
415
|
Claude Code's output styles can do the same thing, but an output style is **a single global slot**: turning it on takes that slot away from anything else and has to be configured per machine. This ships the guidance inside the package and delivers it through the SessionStart hook that is already installed, so it **applies wherever the CLI is installed and leaves the output-style slot free.** It survives `/clear`, because the hook fires again.
|
|
@@ -442,7 +444,7 @@ It adds three layers:
|
|
|
442
444
|
- **AI-writing tics**: automatic intensifiers ("๋ค์ํ", "ํต์ฌ์ ์ธ"), signpost sentences, rhetorical question-then-answer, unconditionally upbeat endings.
|
|
443
445
|
- **Cohesion** โ how sentences connect, which no regex can check. The research finding that shapes this section: surface connectives (conjunctions, demonstratives) correlate *negatively or not at all* with judged text quality, while elaboration โ the next sentence picking up and unpacking what the previous one introduced โ is the only connection type with a positive correlation. So the guidance says: when a transition feels rough, fix the information order (given before new), don't add a connective.
|
|
444
446
|
|
|
445
|
-
**Most of the cohesion layer is not Korean-specific.** Given-before-new ordering (the "given-new contract"), one clear referent per pronoun, keeping one subject per paragraph, bridging sentences instead of leaping, and merging choppy repetitive sentences into a modifier-plus-core structure apply to English prose the same way โ the studies happen to be about Korean learners, but the principles they validate are the standard cohesion model from text linguistics. If you write English deliverables with Claude, run `
|
|
447
|
+
**Most of the cohesion layer is not Korean-specific.** Given-before-new ordering (the "given-new contract"), one clear referent per pronoun, keeping one subject per paragraph, bridging sentences instead of leaping, and merging choppy repetitive sentences into a modifier-plus-core structure apply to English prose the same way โ the studies happen to be about Korean learners, but the principles they validate are the standard cohesion model from text linguistics. If you write English deliverables with Claude, run `sprag cohesion on` โ it injects exactly those five rules as a standalone English block (~0.5k tokens per session), no Korean feature required. While `korean on` is active the block is suppressed, because the Korean supplement already carries the same rules.
|
|
446
448
|
|
|
447
449
|
A final subsection lists what must **not** be "corrected": settled domain terms, formal register, and verbatim quotations โ every lint finding is a request to confirm, not a verdict.
|
|
448
450
|
|
|
@@ -453,14 +455,14 @@ Injecting the guidance once at session start turned out to be half the job. The
|
|
|
453
455
|
From v3.24.0 `korean on` also installs a PostToolUse hook. It opens the file the model just wrote, runs the clauses a machine can decide, and hands any findings back. The file is already saved, so nothing is lost โ the model fixes it on the spot.
|
|
454
456
|
|
|
455
457
|
```bash
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
458
|
+
sprag korean lint block # default: findings are handed back as blocking feedback
|
|
459
|
+
sprag korean lint warn # print findings, do not block
|
|
460
|
+
sprag korean lint off # disable the check
|
|
459
461
|
|
|
460
|
-
|
|
461
|
-
|
|
462
|
+
sprag korean lint scope all # default: every text file the session writes
|
|
463
|
+
sprag korean lint scope prose # documents only
|
|
462
464
|
|
|
463
|
-
|
|
465
|
+
sprag korean lint docs/*.md # check files already on disk
|
|
464
466
|
```
|
|
465
467
|
|
|
466
468
|
Checked: 15 figurative phrases, translationese markers, separators (`โ`ยท`ใ
ก`ยท`|`), three or more `์` particles in one phrase, and a period after a nominal ending. Clauses that need judgement stay with the guidance text.
|
|
@@ -475,7 +477,7 @@ Alongside the writing guidance, one more line is injected: **non-ASCII strings i
|
|
|
475
477
|
|
|
476
478
|
When the model puts Korean into a Write or Edit parameter as escapes, those escapes are sometimes not decoded into code points at all: the literal text `ํ` lands in the file. The artifact carries mojibake, and the model keeps editing on top of it without noticing that what it wrote and what the file holds have diverged. Not writing escapes in the first place removes the path entirely, so the rule blocks the input instead of repairing the output.
|
|
477
479
|
|
|
478
|
-
This line lives in
|
|
480
|
+
This line lives in sprag's own framing paragraph, not in the vendored fluent-korean text. It governs encoding rather than style, and the vendored wording is kept unmodified. For the same reason it carries no exceptions, unlike the style rules that skip code and commit messages. It adds roughly 60 tokens per session.
|
|
479
481
|
|
|
480
482
|
> **Evidence**
|
|
481
483
|
> The same failure is reported against Claude Code: [#12417, unicode handling regression](https://github.com/anthropics/claude-code/issues/12417) and [#26141, Edit silently corrupting unicode](https://github.com/anthropics/claude-code/issues/26141).
|
|
@@ -507,10 +509,10 @@ Three situations, three different interception points:
|
|
|
507
509
|
That second row is measured, not assumed: a `.pdf` Read fires the hook, and a `.pptx` Read in the same session leaves no hook log entry at all.
|
|
508
510
|
|
|
509
511
|
```bash
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
512
|
+
sprag doc2md on # register the hooks (the converter installs itself)
|
|
513
|
+
sprag doc2md # check converter + hook registration
|
|
514
|
+
sprag doc2md report.pptx # convert by hand and see the result
|
|
515
|
+
sprag doc2md install-converter # only to get the install out of the way early
|
|
514
516
|
```
|
|
515
517
|
|
|
516
518
|
**The converter installs itself.** Any rollout step a person has to be told about is a step some of them skip, so the converter installs in the background the moment a document first shows up, and converts as soon as it is ready. Measured: about 30s for the first document (15s install plus markitdown's first import), then 3.7s for a new document and 0.1s on a cache hit. The `.fig` parser installs in half a second on the first Figma file.
|
|
@@ -623,7 +625,7 @@ The model is told to ask for an unlocked copy. This tool never asks for or store
|
|
|
623
625
|
- The `.fig` parser installs through `npm.cmd` via the shell, and the package spec dropped its caret (`openfig-core@0.4.x`): in cmd.exe `^` is the escape character and never reaches npm.
|
|
624
626
|
- The background install and every child process set `windowsHide`, so no console window appears in the middle of someone's prompt.
|
|
625
627
|
|
|
626
|
-
`
|
|
628
|
+
`sprag doc2md --clean` empties the conversion cache; `doc2md off` removes the hook. Removal filters for this tool's own entry, so anything else you registered under `PreToolUse` stays.
|
|
627
629
|
|
|
628
630
|
## ๐ Behind a gateway (Bedrock / Vertex)
|
|
629
631
|
|
|
@@ -638,7 +640,7 @@ Since v3.26.0 the gateway is detected from the model ids in the transcript, whic
|
|
|
638
640
|
- Delegated runs dropped for an unpriceable model id show as `๐ N unresolved` instead of nothing, which used to be indistinguishable from never having delegated.
|
|
639
641
|
- Environment variables set to a `foundation-model` ARN now resolve. An opaque `application-inference-profile` id still does not: guessing at it is how wrong prices enter the ledger.
|
|
640
642
|
|
|
641
|
-
If the detection is wrong, pin it with `
|
|
643
|
+
If the detection is wrong, pin it with `sprag mode ttl=5m` (or `ttl=1h`). An explicit value outranks the measurement.
|
|
642
644
|
|
|
643
645
|
### LiteLLM: your key budget stands in for the missing 5h/7d caps (v3.35.0)
|
|
644
646
|
|
|
@@ -648,7 +650,7 @@ Behind a LiteLLM proxy (Bedrock and friends), Claude Code's stdin never carries
|
|
|
648
650
|
- The proxy is asked via `GET /key/info` and `GET /user/info` โ only the calling key's own data. Renders read a cache file; a detached background process refreshes it every 5 minutes (same shape as the update check), so the statusline never waits on the network.
|
|
649
651
|
- Budget source priority follows real-world usage: the **team-membership budget** (`team_memberships[].spend` + its linked budget table row) first, then the key's own `max_budget`, then the internal-user budget. Verified against a Dockerized LiteLLM, including memberships whose budget diverges from the team max into a separate budget-table row.
|
|
650
652
|
- Unlimited keys (no `max_budget`) get no gauge. The `๐ต` monthly-spend segment still shows, since it comes from session logs.
|
|
651
|
-
- Inspect with `
|
|
653
|
+
- Inspect with `sprag litellm-budget` (cached) or `litellm-budget --refresh` (query now).
|
|
652
654
|
|
|
653
655
|
One related non-bug: if your session model is already sonnet, a sonnet-delegation (T1) rule can never save anything, because there is no price gap to capture. That is correct, but `route-scan rules` displayed it identically to "no delegations yet", so it now says outright that the rule does not apply at the current default model.
|
|
654
656
|
|
|
@@ -667,7 +669,7 @@ Remediation commands are OS-aware (`~/.zshrc` for macOS/Linux/WSL, `setx` for Wi
|
|
|
667
669
|
|
|
668
670
|
## Real-world impact โ before/after report
|
|
669
671
|
|
|
670
|
-

|
|
671
673
|
|
|
672
674
|
harness 5/5 + ratchet applied to the author's own Claude Code work, normalized **per user message** (cutoff 2026-05-02, Opus 4.7 pricing):
|
|
673
675
|
|
|
@@ -684,7 +686,7 @@ Same request resolved in fewer round-trips โ first-try success rate up โ the
|
|
|
684
686
|
<summary>Measurement notes โ why cache hit rate isn't included ยท sample caveats</summary>
|
|
685
687
|
|
|
686
688
|
- The author is on the Max plan (1-hour cache TTL) with hit rate already converged near ~98%, so little headroom there. **Pro-plan users (5-minute TTL)** likely see hit rate itself rise with the handoff-before-expiry workflow.
|
|
687
|
-
- Handoff-before-expiry: watch the TTL countdown, run `
|
|
689
|
+
- Handoff-before-expiry: watch the TTL countdown, run `sprag handoff` just before expiry to dump work state into a markdown brief, start a fresh cache cycle. Same flow handles the 1M warning and cap chips.
|
|
688
690
|
- โ ๏ธ POST window is only 2 days (157 msgs); statistical confidence is low, and week-to-week topic mix differs, so the tool effect isn't cleanly isolated.
|
|
689
691
|
</details>
|
|
690
692
|
|
|
@@ -721,13 +723,13 @@ Node.js โฅ 18 ยท macOS / Linux / Windows / WSL ยท **zero dependencies**.
|
|
|
721
723
|
|
|
722
724
|
**IntelliJ Claude Code plugin** โ the statusline widget fuses frames at the character level when emoji are present (`59:548` artifacts). v2.8.5+ detects `TERMINAL_EMULATOR=JetBrains-JediTerm` and falls back to text mode automatically.
|
|
723
725
|
|
|
724
|
-
**If the countdown looks frozen:** ticking while idle requires Claude Code to re-run the statusline command on a timer, controlled by `statusLine.refreshInterval` (seconds, Claude Code v2.1.97+) in `~/.claude/settings.json`. Without it the line only redraws when the conversation updates. If behavior differs per terminal, check three things: โ that machine's Claude Code is โฅ 2.1.97; โก no project `.claude/settings.json` / `settings.local.json` overrides `statusLine` without a refreshInterval; โข the statusline wrapper actually finds `
|
|
726
|
+
**If the countdown looks frozen:** ticking while idle requires Claude Code to re-run the statusline command on a timer, controlled by `statusLine.refreshInterval` (seconds, Claude Code v2.1.97+) in `~/.claude/settings.json`. Without it the line only redraws when the conversation updates. If behavior differs per terminal, check three things: โ that machine's Claude Code is โฅ 2.1.97; โก no project `.claude/settings.json` / `settings.local.json` overrides `statusLine` without a refreshInterval; โข the statusline wrapper actually finds `sprag` on PATH instead of falling back to a multi-second `npx` run on every render (typical when nvm is not loaded in non-login shells). Re-running `sprag install` restores refreshInterval=5.
|
|
725
727
|
|
|
726
728
|
**Migration from claude-cache-monitor:**
|
|
727
729
|
```bash
|
|
728
|
-
npm uninstall -g claude-cache-monitor && npm i -g
|
|
730
|
+
npm uninstall -g claude-cache-monitor && npm i -g sprag-cli
|
|
729
731
|
```
|
|
730
|
-
Also update `statusLine.command` in `~/.claude/settings.json` to `
|
|
732
|
+
Also update `statusLine.command` in `~/.claude/settings.json` to `sprag โฆ`.
|
|
731
733
|
|
|
732
734
|
**Background:** [GitHub Issue #46829](https://github.com/anthropics/claude-code/issues/46829) (cache TTL regression) ยท [HN discussion](https://news.ycombinator.com/item?id=47736476)
|
|
733
735
|
</details>
|
|
@@ -749,12 +751,12 @@ Found a bug, or want a feature? Open an issue: https://github.com/rootstudioyaml
|
|
|
749
751
|
No browser or GitHub login handy (corporate network, mid-session)? Submit straight from the terminal โ or ask Claude to do it for you:
|
|
750
752
|
|
|
751
753
|
```bash
|
|
752
|
-
|
|
754
|
+
sprag feedback "the 5m TTL chip never clears on Bedrock"
|
|
753
755
|
```
|
|
754
756
|
|
|
755
757
|
It files a GitHub issue via the `gh` CLI when one is authenticated; otherwise it submits anonymously (no login, works where github.com is blocked). Pass `--anonymous` to skip the `gh` path. Version and OS metadata are attached automatically.
|
|
756
758
|
|
|
757
|
-
When reporting a bug, please include the tool version (`
|
|
759
|
+
When reporting a bug, please include the tool version (`sprag --version`), your OS, and โ if it is a statusline or warning issue โ the statusline output or the `sprag last` result.
|
|
758
760
|
|
|
759
761
|
## License
|
|
760
762
|
|
package/bin/cli.js
CHANGED
|
@@ -1,27 +1,27 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
|
-
* claude-token-saver
|
|
4
|
+
* sprag CLI (formerly claude-token-saver / claude-cache-monitor)
|
|
5
5
|
*
|
|
6
6
|
* Usage:
|
|
7
|
-
* npx
|
|
8
|
-
* npx
|
|
9
|
-
* npx
|
|
10
|
-
* npx
|
|
11
|
-
* npx
|
|
7
|
+
* npx sprag # default report (last 30 days)
|
|
8
|
+
* npx sprag --days 7 # last 7 days
|
|
9
|
+
* npx sprag --format json # JSON output
|
|
10
|
+
* npx sprag --format csv # CSV output
|
|
11
|
+
* npx sprag --project myproj # filter by project
|
|
12
12
|
* npx sprag route-scan # detect recurring easy work โ haiku-delegation candidates
|
|
13
13
|
* npx sprag install # set up skill/hooks/statusline; asks about harness + Korean guidance
|
|
14
14
|
* npx sprag install --yes # take the defaults without asking (same as --no-input)
|
|
15
|
-
* npx
|
|
16
|
-
* npx
|
|
15
|
+
* npx sprag --install-hook # install PostToolUse hook
|
|
16
|
+
* npx sprag --uninstall-hook # remove hook
|
|
17
17
|
* npx claude-token-saver --hook-run # internal: called by hook
|
|
18
|
-
* npx
|
|
19
|
-
* npx
|
|
20
|
-
* npx
|
|
21
|
-
* npx
|
|
22
|
-
* npx
|
|
23
|
-
* npx
|
|
24
|
-
* npx
|
|
18
|
+
* npx sprag --statusline # one-line output for Claude Code statusline API
|
|
19
|
+
* npx sprag --statusline --verbose # longer labels
|
|
20
|
+
* npx sprag --statusline --no-color # strip ANSI colors
|
|
21
|
+
* npx sprag --statusline --icon # use ๐ง โณ ๐ฐ icons
|
|
22
|
+
* npx sprag --statusline --no-timer # hide the TTL countdown
|
|
23
|
+
* npx sprag --statusline --single-line # legacy 1-line layout (no routing-totals headline)
|
|
24
|
+
* npx sprag --statusline --exclude-session <path>
|
|
25
25
|
* # exclude a JSONL path from lastActivity
|
|
26
26
|
* # (or set CACHE_MONITOR_EXCLUDE_SESSION env var)
|
|
27
27
|
*/
|
|
@@ -90,32 +90,32 @@ const KNOWN_SUBCOMMANDS = new Set([
|
|
|
90
90
|
'seed', 'litellm-budget', 'feedback',
|
|
91
91
|
]);
|
|
92
92
|
|
|
93
|
-
const USAGE = `
|
|
93
|
+
const USAGE = `sprag โ Claude Code token usage, cache health, and model routing
|
|
94
94
|
|
|
95
95
|
Usage:
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
96
|
+
sprag default report (last 30 days)
|
|
97
|
+
sprag --days 7 last 7 days
|
|
98
|
+
sprag --format json JSON output
|
|
99
|
+
sprag --format csv CSV output
|
|
100
|
+
sprag --project myproj filter by project
|
|
101
101
|
sprag route-scan detect recurring easy work โ delegation candidates
|
|
102
102
|
sprag install set up skill/hooks/statusline
|
|
103
103
|
sprag install --yes take the defaults without asking
|
|
104
|
-
|
|
104
|
+
sprag uninstall remove everything install added
|
|
105
105
|
sprag harness check score the harness setup in CLAUDE.md
|
|
106
106
|
sprag harness analyze run the harness transcript analysis manually
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
107
|
+
sprag last most recent warning + how to handle it
|
|
108
|
+
sprag history recent warning transitions
|
|
109
|
+
sprag handoff write a session handoff file
|
|
110
110
|
sprag feedback "<msg>" file a bug report / feature request (no browser needed)
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
111
|
+
sprag upgrade install the latest release
|
|
112
|
+
sprag --install-hook install cache-monitor PostToolUse hook
|
|
113
|
+
sprag --uninstall-hook remove that hook
|
|
114
|
+
sprag --statusline one-line output for Claude Code statusline
|
|
115
115
|
--verbose / --no-color / --icon / --no-timer / --single-line
|
|
116
116
|
|
|
117
117
|
Run any subcommand with --help for its own options where available.
|
|
118
|
-
Bug reports & feature requests: https://github.com/rootstudioyaml/
|
|
118
|
+
Bug reports & feature requests: https://github.com/rootstudioyaml/sprag/issues
|
|
119
119
|
`;
|
|
120
120
|
|
|
121
121
|
async function main() {
|
|
@@ -140,8 +140,8 @@ async function main() {
|
|
|
140
140
|
// Designed for the auto-trigger skill so the user immediately sees
|
|
141
141
|
// "what just fired and how to fix it" without having to read the whole
|
|
142
142
|
// history file.
|
|
143
|
-
//
|
|
144
|
-
//
|
|
143
|
+
// sprag last # search last 1 day
|
|
144
|
+
// sprag last --days 7 # widen the lookback
|
|
145
145
|
if (args[0] === 'last') {
|
|
146
146
|
return (await import('../src/commands/last.js')).run({ numArg });
|
|
147
147
|
}
|
|
@@ -149,9 +149,9 @@ async function main() {
|
|
|
149
149
|
// Subcommand: history โ print recent warning transitions captured by the
|
|
150
150
|
// statusline. One markdown file per day, persisted under the platform-
|
|
151
151
|
// specific user-data dir.
|
|
152
|
-
//
|
|
153
|
-
//
|
|
154
|
-
//
|
|
152
|
+
// sprag history # last 7 days
|
|
153
|
+
// sprag history --days 30 # custom window
|
|
154
|
+
// sprag history --list # just list available dates
|
|
155
155
|
if (args[0] === 'history') {
|
|
156
156
|
return (await import('../src/commands/history.js')).run({ hasFlag, numArg });
|
|
157
157
|
}
|
|
@@ -161,8 +161,8 @@ async function main() {
|
|
|
161
161
|
// session can pick up where this one stopped. Pairs with the cap-warn chip:
|
|
162
162
|
// when statusline shows ๐จ 5H 90%+, run this to back up state before the cap
|
|
163
163
|
// hits.
|
|
164
|
-
//
|
|
165
|
-
//
|
|
164
|
+
// sprag handoff # write to cwd
|
|
165
|
+
// sprag handoff --cwd PATH # custom directory
|
|
166
166
|
// Subcommand: feedback โ file a bug report / feature request without a
|
|
167
167
|
// browser. Tries gh CLI, then an anonymous form POST, then a local save
|
|
168
168
|
// with a prefilled GitHub issue URL. See src/commands/feedback.js.
|
|
@@ -194,9 +194,9 @@ async function main() {
|
|
|
194
194
|
|
|
195
195
|
// Subcommand: mode โ persist statusline preferences so future runs pick
|
|
196
196
|
// them up without flags or wrapper edits.
|
|
197
|
-
//
|
|
198
|
-
//
|
|
199
|
-
//
|
|
197
|
+
// sprag mode # show current config
|
|
198
|
+
// sprag mode icon verbose # set icon + verbose
|
|
199
|
+
// sprag mode reset # clear back to defaults
|
|
200
200
|
if (args[0] === 'mode') {
|
|
201
201
|
return (await import('../src/commands/mode.js')).run({ args });
|
|
202
202
|
}
|
|
@@ -244,7 +244,7 @@ async function main() {
|
|
|
244
244
|
|
|
245
245
|
// Subcommand: seed โ register the bundled starter rules (model-fitting
|
|
246
246
|
// presets + curated ratchet rules), one answer at a time.
|
|
247
|
-
//
|
|
247
|
+
// sprag seed | seed accept <id> --global|--project | seed skip <id>
|
|
248
248
|
if (args[0] === 'seed') {
|
|
249
249
|
return (await import('../src/commands/seed.js')).run({ args, hasFlag });
|
|
250
250
|
}
|
|
@@ -263,10 +263,10 @@ async function main() {
|
|
|
263
263
|
// Subcommand: compact-window โ audit / pin Claude Code's autoCompactWindow.
|
|
264
264
|
// On a 1M-context model, compaction only fires near 800k unless the window is
|
|
265
265
|
// capped; 200k sessions are exempt.
|
|
266
|
-
//
|
|
267
|
-
//
|
|
268
|
-
//
|
|
269
|
-
//
|
|
266
|
+
// sprag compact-window # status
|
|
267
|
+
// sprag compact-window set --global # pin 200k (~/.claude/settings.json)
|
|
268
|
+
// sprag compact-window set --project # pin 200k (<root>/.claude/settings.json)
|
|
269
|
+
// sprag compact-window off | on # toggle the statusline warning
|
|
270
270
|
if (args[0] === 'compact-window') {
|
|
271
271
|
return (await import('../src/commands/compact-window.js')).run({ args, hasFlag });
|
|
272
272
|
}
|
|
@@ -292,8 +292,8 @@ async function main() {
|
|
|
292
292
|
}
|
|
293
293
|
|
|
294
294
|
// Subcommand: litellm-budget ยท LiteLLM ๊ฒ์ดํธ์จ์ด ํค์ max_budget/spend ์กฐํ.
|
|
295
|
-
//
|
|
296
|
-
//
|
|
295
|
+
// sprag litellm-budget # ์บ์๋ ์์ฐ ์ํ ์ถ๋ ฅ
|
|
296
|
+
// sprag litellm-budget --refresh # ์ง๊ธ ํ๋ก์์ ๋ฌผ์ด๋ด (detached ์์์ด ์ฌ์ฉ)
|
|
297
297
|
if (args[0] === 'litellm-budget') {
|
|
298
298
|
const { gatewayEnv, readBudgetState, refreshBudgetState } = await import('../src/litellm-budget.js');
|
|
299
299
|
const quiet = hasFlag('--quiet');
|
|
@@ -319,8 +319,8 @@ async function main() {
|
|
|
319
319
|
|
|
320
320
|
// Subcommand: upgrade โ run the install command that matches how this copy
|
|
321
321
|
// got here, then confirm the new version.
|
|
322
|
-
//
|
|
323
|
-
//
|
|
322
|
+
// sprag upgrade # install the latest release
|
|
323
|
+
// sprag upgrade --print # just show the command, run nothing
|
|
324
324
|
if (args[0] === 'upgrade') {
|
|
325
325
|
return (await import('../src/commands/upgrade.js')).run({
|
|
326
326
|
hasFlag,
|
|
@@ -356,7 +356,7 @@ async function main() {
|
|
|
356
356
|
// every scenario based on wall clock so a screen recorder picks them up.
|
|
357
357
|
const demoArg = getArg('--demo');
|
|
358
358
|
|
|
359
|
-
// `
|
|
359
|
+
// `sprag --demo table` (no --statusline) โ full table view
|
|
360
360
|
// with all six issue drill-downs at once, for marketing screencasts.
|
|
361
361
|
if (!isStatusline && demoArg === 'table') {
|
|
362
362
|
const { buildTableDemoData } = await import('../src/demo.js');
|
|
@@ -592,7 +592,7 @@ async function main() {
|
|
|
592
592
|
}
|
|
593
593
|
}
|
|
594
594
|
// Persist transitions to ~/.config/claude-token-saver/history/YYYY-MM-DD.md
|
|
595
|
-
// so `
|
|
595
|
+
// so `sprag history` and the auto-skill can replay them.
|
|
596
596
|
try {
|
|
597
597
|
const { recordChip, recordCapTransition } = await import('../src/history.js');
|
|
598
598
|
recordChip(spikeChip, { detail: chipDetail });
|
package/package.json
CHANGED
package/src/advice.js
CHANGED
|
@@ -467,7 +467,7 @@ export const ISSUE_MESSAGES = {
|
|
|
467
467
|
* and how do I fix it" without re-running the tool.
|
|
468
468
|
*
|
|
469
469
|
* The full multi-step advice still lives in `ISSUE_MESSAGES[code].actions()`;
|
|
470
|
-
* `
|
|
470
|
+
* `sprag last` prints that long form on demand.
|
|
471
471
|
*/
|
|
472
472
|
export const ISSUE_TIPS = {
|
|
473
473
|
LARGE_INPUT_PER_REQUEST: {
|
|
@@ -536,8 +536,8 @@ export const CHIP_TO_CODES = {
|
|
|
536
536
|
* because once the cap blocks you, you need a fresh session to continue.
|
|
537
537
|
*/
|
|
538
538
|
export const CAP_TIPS = {
|
|
539
|
-
en: 'Run `
|
|
540
|
-
ko: '`
|
|
539
|
+
en: 'Run `sprag handoff` to back up state before the cap blocks you',
|
|
540
|
+
ko: '`sprag handoff` ์คํํด์ ์บก ๋๋ฌ ์ ์ ์ํ ๋ฐฑ์
',
|
|
541
541
|
};
|
|
542
542
|
|
|
543
543
|
/**
|
package/src/brief.js
CHANGED
|
@@ -246,7 +246,7 @@ export async function runBrief({ sessionId, transcriptPath, now = Date.now() })
|
|
|
246
246
|
briefed.add(sig);
|
|
247
247
|
s.briefed = [...briefed];
|
|
248
248
|
const now = st.window ? `ํ์ฌ ${fmtK(st.window)}` : 'ํ์ฌ ๋ฏธ์ค์ ';
|
|
249
|
-
items.push(`1M ์ปจํ
์คํธ ๋ชจ๋ธ(${st.model})์ธ๋ฐ autoCompactWindow๊ฐ ${now}์
๋๋ค โ ์๋ ์์ถ์ด 80๋ง ํ ํฐ ๊ทผ์ฒ์์์ผ ๊ฑธ๋ ค ๊ทธ์ ๊น์ง ๋ชจ๋ ์์ฒญ์ด ์ ์ฒด ์ปจํ
์คํธ๋ฅผ ์ฌ๊ณผ๊ธํฉ๋๋ค. 1M์ ๋๋ฌด ํฌ๋ ${fmtK(st.recommendedMin)}~${fmtK(st.recommendedMax)} ๋ฒ์๋ฅผ ๊ถ์ฅํฉ๋๋ค(๊ทธ ๋ฒ์ ์์ด๋ฉด ๊ฒฝ๊ณ ํ์ง ์์ต๋๋ค). 1M ์ฐฝ ์์ฒด๋ ๊ทธ๋๋ก ๋๊ณ ์์ถ ์์ ๋ง ์๋น๊น๋๋ค. ๋ฑ๋ก:
|
|
249
|
+
items.push(`1M ์ปจํ
์คํธ ๋ชจ๋ธ(${st.model})์ธ๋ฐ autoCompactWindow๊ฐ ${now}์
๋๋ค โ ์๋ ์์ถ์ด 80๋ง ํ ํฐ ๊ทผ์ฒ์์์ผ ๊ฑธ๋ ค ๊ทธ์ ๊น์ง ๋ชจ๋ ์์ฒญ์ด ์ ์ฒด ์ปจํ
์คํธ๋ฅผ ์ฌ๊ณผ๊ธํฉ๋๋ค. 1M์ ๋๋ฌด ํฌ๋ ${fmtK(st.recommendedMin)}~${fmtK(st.recommendedMax)} ๋ฒ์๋ฅผ ๊ถ์ฅํฉ๋๋ค(๊ทธ ๋ฒ์ ์์ด๋ฉด ๊ฒฝ๊ณ ํ์ง ์์ต๋๋ค). 1M ์ฐฝ ์์ฒด๋ ๊ทธ๋๋ก ๋๊ณ ์์ถ ์์ ๋ง ์๋น๊น๋๋ค. ๋ฑ๋ก: sprag compact-window set --global|--project [--value ${fmtK(st.recommendedMax)}] (๊ธฐ๋ณธ ${fmtK(st.recommended)}, ์ ์ฉ ๋ฒ์๋ ์ฌ์ฉ์์๊ฒ ํ์ธ) / ๋๊ธฐ: compact-window off`);
|
|
250
250
|
}
|
|
251
251
|
}
|
|
252
252
|
} catch { /* settings unreadable โ other briefings above still apply */ }
|
|
@@ -257,7 +257,7 @@ export async function runBrief({ sessionId, transcriptPath, now = Date.now() })
|
|
|
257
257
|
|
|
258
258
|
if (items.length === 0) return null;
|
|
259
259
|
const lines = [
|
|
260
|
-
'[
|
|
260
|
+
'[sprag ๋ธ๋ฆฌํ] ์๋ ์ํ ๋ณํ๋ฅผ ์ฌ์ฉ์์๊ฒ ์๋ ค์ฃผ์ธ์. ์งํ ์ค์ธ ๋ต๋ณ ํ๋ฆ์ ๋์ง ๋ง๊ณ , ๋ต๋ณ ๋ง๋ฏธ์ `โป [sprag]` ๋ผ๋ฒจ์ ๋ฌ์ ๊ฐ ํญ๋ชฉ์ 1~2์ค๋ก ์์ฝํด ์ ๋ฌํ๋ฉด ๋ฉ๋๋ค (์ด ๋ธ๋ฆฌํ์ ํญ๋ชฉ๋น ํ ๋ฒ๋ง ์ฃผ์
๋ฉ๋๋ค):',
|
|
261
261
|
];
|
|
262
262
|
for (const it of items) lines.push(`- ${it}`);
|
|
263
263
|
return lines.join('\n');
|