claude-token-saver 3.26.0 โ 3.26.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.en.md +59 -0
- package/README.md +15 -4
- package/bin/cli.js +16 -0
- package/package.json +1 -1
- package/src/commands/doc2md.js +38 -1
- package/src/doc2md.cjs +76 -1
package/README.en.md
CHANGED
|
@@ -62,6 +62,7 @@ By run (newest first):
|
|
|
62
62
|
| ๐จ **No surprise rate limits** | Warns when the 5H/7D window hits 90%; `handoff` backs up your work |
|
|
63
63
|
| ๐ง **Cache waste detection** | Hit rate, TTL, 1M-context detection โ spikes diagnosed with issue codes |
|
|
64
64
|
| ๐ฐ๐ท **Korean writing guidance** | Offered at install time, defaulting to your locale ([below](#-korean-writing-guidance)) |
|
|
65
|
+
| ๐ **Document conversion** | pptx/xlsx/pdf/docx become Markdown before the model reads them, so unreadable bytes never reach the context window ([below](#-doc2md--attached-documents-become-markdown-before-the-model-reads-them)) |
|
|
65
66
|
|
|
66
67
|
## Not a router โ 60 seconds
|
|
67
68
|
|
|
@@ -163,6 +164,9 @@ Run these in your shell (inside Claude Code, the `/claude-token-saver` Skill is
|
|
|
163
164
|
| `claude-token-saver korean on\|off\|status` | Inject Korean writing guidance at session start and install the write-time check (below) |
|
|
164
165
|
| `claude-token-saver korean lint block\|warn\|off` | How the write-time check handles findings |
|
|
165
166
|
| `claude-token-saver korean lint scope all\|prose` | Check every text file, or documents only |
|
|
167
|
+
| `claude-token-saver doc2md on\|off` | Convert attached documents to Markdown before the model reads them (below) |
|
|
168
|
+
| `claude-token-saver doc2md <file>` | Convert one file by hand. Diagnostic: it prints the refusal reason instead of swallowing it |
|
|
169
|
+
| `claude-token-saver mode ttl=5m\|1h\|auto` | Pin the cache TTL bucket. The default `auto` trusts the measured split, then falls back to gateway detection |
|
|
166
170
|
| `claude-token-saver --version` | Print the installed version |
|
|
167
171
|
| `claude-token-saver update-check` | Is a newer version out? (`--refresh` to ask now, `--dismiss` to mute this version's offer) |
|
|
168
172
|
| `claude-token-saver upgrade` | Install the latest release with the package manager that installed this copy (`--print` shows the command only) |
|
|
@@ -374,6 +378,50 @@ Installs with nobody attached โ npm `postinstall`, CI, piped stdin โ skip th
|
|
|
374
378
|
> The guidance text comes from [fluent-korean](https://github.com/snflkd/fluent-korean). Copyright (c) 2026 snflkd, MIT License.
|
|
375
379
|
> The wording is unmodified; only the output-style frontmatter was removed. The full license ships with the package at `presets/korean-style/LICENSE-fluent-korean`.
|
|
376
380
|
|
|
381
|
+
## ๐ doc2md โ attached documents become Markdown before the model reads them
|
|
382
|
+
|
|
383
|
+
`Read` a pptx, xlsx, pdf or docx and the raw bytes go into the context window, where the model cannot read them. This intercepts that `Read`, converts the file once, and hands over the Markdown instead.
|
|
384
|
+
|
|
385
|
+
**This is opt-in.** Installing the CLI does not turn it on: both commands below are required, and a registered hook with no converter behind it does nothing at all.
|
|
386
|
+
|
|
387
|
+
```bash
|
|
388
|
+
claude-token-saver doc2md install-converter # markitdown into a dedicated venv
|
|
389
|
+
claude-token-saver doc2md on # register the Read hook
|
|
390
|
+
claude-token-saver doc2md # check converter + hook registration
|
|
391
|
+
claude-token-saver doc2md report.pptx # convert by hand and see the result
|
|
392
|
+
```
|
|
393
|
+
|
|
394
|
+
The converter goes into a venv this tool owns (`<state dir>/doc2md-venv`): no system interpreter is touched, and uninstalling the CLI takes it along. An existing markitdown on `uv tool` or `PATH` is preferred over building a new one.
|
|
395
|
+
|
|
396
|
+
Conversion is [markitdown](https://github.com/microsoft/markitdown). Slide numbers, heading levels, tables, speaker notes and per-sheet headings all survive, and non-Latin text comes through intact.
|
|
397
|
+
|
|
398
|
+
Several things it deliberately does not do:
|
|
399
|
+
|
|
400
|
+
- **Images are not converted.** markitdown returns nothing for them, and OCR misread resource names in testing (`c5.xlarge` as `c.xlarge`). In a document where those names *are* the content, wrong text is worse than none. The model reads images natively anyway.
|
|
401
|
+
- **A missing converter never fails silently.** The install command is shown once, then the original `Read` proceeds untouched. Repeating the notice on every read would be its own nuisance; saying nothing is how a broken converter hides. Run `doc2md` with no arguments to see the converter and hook registration together.
|
|
402
|
+
- **Conversions never land in your project.** They go under the tool's own state directory with mode `0700`, so there is nothing to add to `.gitignore`. Filenames matching payroll/contract/secret patterns are skipped entirely.
|
|
403
|
+
- **Zip bombs are refused.** pptx/xlsx/docx are zip containers: the declared sizes are checked first, and since those are written by whoever built the file, the real decompressed bytes are counted against a ceiling too.
|
|
404
|
+
- **Spreadsheets are capped by rows, not bytes.** Conversion time tracks row count (measured: a 6.3MB PDF in 0.9s, a 5.8MB workbook in 47.75s). Past 50,000 rows only the head is converted, and **the truncation and the true row count are both stated** in what the model is told.
|
|
405
|
+
|
|
406
|
+
`claude-token-saver 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.
|
|
407
|
+
|
|
408
|
+
## ๐ Behind a gateway (Bedrock / Vertex)
|
|
409
|
+
|
|
410
|
+
A gateway reports the cache-creation total but never the 5m/1h split. That left the tool unable to tell "nothing cached yet" from "this provider does not say", and the fallback assumed an hour โ for a window that is really five minutes on Bedrock, overstating it twelvefold.
|
|
411
|
+
|
|
412
|
+
Since v3.26.0 the gateway is detected from the model ids in the transcript, which fixes:
|
|
413
|
+
|
|
414
|
+
- The countdown falls back to 5 minutes, labelled `5m?`. Three grades of certainty get three labels: measured (`5m`), inferred (`5m?`), unknown (`?`).
|
|
415
|
+
- In a 5-minute bucket the countdown colour follows absolute time rather than a percentage. 30% of five minutes is 90 seconds, and green there promised comfort that was not there.
|
|
416
|
+
- The `โ 5m TTL` warning finally reaches these users โ with different advice, since no subscription plan changes a gateway's TTL.
|
|
417
|
+
- `Extra cost if 5m-only` is only asked of sessions that have 1h writes to lose. Elsewhere the arithmetically honest `+$0` read as an endorsement of the bucket you are already stuck in.
|
|
418
|
+
- Delegated runs dropped for an unpriceable model id show as `๐ N unresolved` instead of nothing, which used to be indistinguishable from never having delegated.
|
|
419
|
+
- 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.
|
|
420
|
+
|
|
421
|
+
If the detection is wrong, pin it with `claude-token-saver mode ttl=5m` (or `ttl=1h`). An explicit value outranks the measurement.
|
|
422
|
+
|
|
423
|
+
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.
|
|
424
|
+
|
|
377
425
|
## Spike issue codes
|
|
378
426
|
|
|
379
427
|
| Code | Meaning |
|
|
@@ -454,6 +502,17 @@ Also update `statusLine.command` in `~/.claude/settings.json` to `claude-token-s
|
|
|
454
502
|
|
|
455
503
|
## Release notes
|
|
456
504
|
|
|
505
|
+
### v3.26.2 (2026-09-04)
|
|
506
|
+
- **The converter installs itself.** The old instruction was `pip install`, which asks the user to modify a system interpreter โ and if they skipped it, the hook sat registered and did nothing. `doc2md install-converter` builds a dedicated venv and puts markitdown in it.
|
|
507
|
+
- **`doc2md` status now reports hook registration too.** Showing only the converter made "hook but no converter" and "converter but no hook" look identical, and both look like a broken feature.
|
|
508
|
+
- **An unknown subcommand under `--hook` prints nothing.** A 3.25.0 global install meeting a settings.json written by 3.26.0 did not recognise `doc2md`, fell through to the default report, and pushed a full statistics table into the hook stream on every `Read`.
|
|
509
|
+
|
|
510
|
+
### v3.26.0 (2026-09-04)
|
|
511
|
+
- **Attached documents are converted to Markdown before the model reads them.** Reading a pptx/xlsx/pdf/docx put unreadable bytes into the context window. `doc2md on` registers a `Read` hook that converts the file once, caches it outside your project, and points the model at the Markdown. A missing converter is announced once and then gets out of the way, zip bombs are refused, and workbooks past 50,000 rows are converted head-first with the truncation stated. See [doc2md](#-doc2md--attached-documents-become-markdown-before-the-model-reads-them).
|
|
512
|
+
- **TTL display fixed for Bedrock/Vertex sessions.** Gateways never report the per-bucket split, and the fallback assumed an hour โ twelvefold too long for a 5-minute-only backend. The gateway is now detected from the model ids, the fallback follows that evidence, and the label reads `5m?` to mark it as inferred. Pin it manually with `mode ttl=5m` if the detection is wrong.
|
|
513
|
+
- **Delegated runs are no longer discarded in silence.** Runs excluded for an unpriceable model id surface as `๐ N unresolved` on the statusline; previously that was indistinguishable from never having delegated, so an entire tier of rules could report zero with no way to find out why. Environment variables set to a `foundation-model` ARN now resolve as well.
|
|
514
|
+
- **The Korean guidance stopped contradicting itself.** The injected scope claimed code comments while the vendored text disclaimed them twice, leaving the model nothing to decide on. The vendored wording is untouched; the block now states which side wins. The em dash in the attribution line โ a mark that guidance itself forbids โ became a colon.
|
|
515
|
+
|
|
457
516
|
### v3.25.0 (2026-09-04)
|
|
458
517
|
- **The statusline now shows which version is running** โ until now the version lived only in the table report's footer, so "which version am I on" meant running a full report. A `--version` flag was added alongside it.
|
|
459
518
|
- **A new release asks at session start** โ a statusline cannot open a dialog, so telling and asking are split. The statusline only reports (`โฌ v3.24.0 โ 3.25.0`); the SessionStart hook injects a line telling the model to ask the user whether to upgrade. On yes it runs `claude-token-saver upgrade`, which uses the package manager that installed this copy; on no, `update-check --dismiss` mutes that version until a newer one ships.
|
package/README.md
CHANGED
|
@@ -62,6 +62,7 @@ $ claude-token-saver route-scan savings # ๋ชจ๋ ๊ธ์ก์ ๋ฃฐ ๋จ์๊น์ง
|
|
|
62
62
|
| ๐จ **ํ๋ ์ด๊ณผ ์๋ฐฉ** | 5์๊ฐยท7์ผ rate-limit ์๋๊ฐ 90%์ ๋ฟ์ผ๋ฉด ๊ฒฝ๊ณ ํ๊ณ , `handoff`๋ก ์์
์ ๋ฐฑ์
ํฉ๋๋ค |
|
|
63
63
|
| ๐ง **์บ์ ๋ญ๋น ๊ฐ์ง** | ํํธ์จยทTTLยท1M ์ปจํ
์คํธ๋ฅผ ๊ฐ์งํด ํ ํฐ ๊ธ์ฆ ์์ธ์ ์ฝ๋๋ก ์ง๋จํฉ๋๋ค |
|
|
64
64
|
| ๐ฐ๐ท **ํ๊ตญ์ด ๋ฌธ์ฒด ๊ต์ ** | ํ๊ตญ์ด ํ๊ฒฝ์ด๋ฉด ์๋์ผ๋ก ์ผ์ง๋๋ค ([์๋](#-ํ๊ตญ์ด-๋ฌธ์ฒด-์ง์นจ)) |
|
|
65
|
+
| ๐ **๋ฌธ์ ์๋ ๋ณํ** | pptxยทxlsxยทpdfยทdocx ๋ฅผ ์ฝ๊ธฐ ์ ์ Markdown ์ผ๋ก ๋ฐ๊ฟ ์ฝ์ง ๋ชปํ๋ ๋ฐ์ดํธ๊ฐ ์ปจํ
์คํธ์ ์ฌ๋ผ๊ฐ์ง ์๊ฒ ํฉ๋๋ค ([์๋](#-doc2md-์ฒจ๋ถ-๋ฌธ์๋ฅผ-์ฝ๊ธฐ-์ ์-markdown-์ผ๋ก-๋ฐ๊ฟ๋๋ค)) |
|
|
65
66
|
|
|
66
67
|
## ๋ผ์ฐํฐ๊ฐ ์๋๋๋ค: 60์ด ์ค๋ช
|
|
67
68
|
|
|
@@ -371,18 +372,23 @@ npm์ `postinstall`์ด๋ CI์ฒ๋ผ ์ฌ๋์ด ๋ถ์ด ์์ง ์์ ์ค์น์์
|
|
|
371
372
|
|
|
372
373
|
pptxยทxlsxยทpdfยทdocx ๋ฅผ ๊ทธ๋๋ก `Read` ํ๋ฉด ๋ชจ๋ธ์ด ์ฝ์ง ๋ชปํ๋ ๋ฐ์ดํธ๊ฐ ์ปจํ
์คํธ์ ๊ทธ๋๋ก ์ฌ๋ผ๊ฐ๋๋ค. ์ด ๊ธฐ๋ฅ์ ๊ทธ `Read` ๋ฅผ ๊ฐ๋ก์ฑ ํ์ผ์ ํ ๋ฒ ๋ณํํด ๋๊ณ , ์๋ณธ ๋์ ๋ณํ๋ณธ์ ์ฝ๊ฒ ํฉ๋๋ค.
|
|
373
374
|
|
|
375
|
+
**์ด ๊ธฐ๋ฅ์ ์ตํธ์ธ์
๋๋ค.** ์ค์น๋ง์ผ๋ก๋ ์ผ์ง์ง ์๊ณ , ์๋ ๋ ๋ช
๋ น์ ๋ชจ๋ ์คํํด์ผ ๋์ํฉ๋๋ค. ํ
๋ง ๋ฑ๋กํ๊ณ ๋ณํ๊ธฐ๊ฐ ์์ผ๋ฉด ์๋ฌด ์ผ๋ ์ผ์ด๋์ง ์์ต๋๋ค.
|
|
376
|
+
|
|
374
377
|
```bash
|
|
375
|
-
|
|
376
|
-
claude-token-saver doc2md on
|
|
377
|
-
claude-token-saver doc2md
|
|
378
|
+
claude-token-saver doc2md install-converter # ์ ์ฉ venv์ markitdown ์ค์น
|
|
379
|
+
claude-token-saver doc2md on # Read ํ
๋ฑ๋ก
|
|
380
|
+
claude-token-saver doc2md # ๋ณํ๊ธฐยทํ
๋ฑ๋ก ์ํ ํ์ธ
|
|
381
|
+
claude-token-saver doc2md ๋ณด๊ณ ์.pptx # ์ง์ ๋ณํํด ๊ฒฐ๊ณผ ํ์ธ
|
|
378
382
|
```
|
|
379
383
|
|
|
384
|
+
๋ณํ๊ธฐ๋ ๋๊ตฌ ์ ์ฉ venv(`<์ํ ๋๋ ํฐ๋ฆฌ>/doc2md-venv`)์ ์ค์นํฉ๋๋ค. ์์คํ
ํ์ด์ฌ์ ๊ฑด๋๋ฆฌ์ง ์๊ณ , CLI๋ฅผ ์ง์ฐ๋ฉด ํจ๊ป ์ฌ๋ผ์ง๋๋ค. ์ด๋ฏธ `uv tool` ์ด๋ ๋ค๋ฅธ ๊ฒฝ๋ก์ markitdown ์ด ์์ผ๋ฉด ๊ทธ์ชฝ์ ๋จผ์ ์๋๋ค.
|
|
385
|
+
|
|
380
386
|
๋ณํ์ [markitdown](https://github.com/microsoft/markitdown)์ด ๋ด๋นํ๋ฉฐ, ์ฌ๋ผ์ด๋ ๋ฒํธ์ ์ ๋ชฉ ๊ณ์ธต, ํ, ๋ฐํ์ ๋
ธํธ, ์ํธ ๊ตฌ๋ถ์ด ๋ชจ๋ ๋จ์ต๋๋ค. ํ๊ธ๋ ๊นจ์ง์ง ์์ต๋๋ค.
|
|
381
387
|
|
|
382
388
|
๋ช ๊ฐ์ง๋ ์๋์ ์ผ๋ก ํ์ง ์์ต๋๋ค.
|
|
383
389
|
|
|
384
390
|
- **์ด๋ฏธ์ง๋ ๋ณํํ์ง ์์ต๋๋ค.** markitdown ์ด ๋น ๊ฒฐ๊ณผ๋ฅผ ๋๋ ค์ฃผ๊ณ , OCR ์ ์ค์ธก์์ ๋ฆฌ์์ค ์ด๋ฆ์ ํ๋ฆฌ๊ฒ ์ฝ์์ต๋๋ค(`c5.xlarge` ๋ฅผ `c.xlarge` ๋ก). ์ด๋ฆ ์์ฒด๊ฐ ๋ด์ฉ์ธ ๋ฌธ์์์๋ ํ
์คํธ๊ฐ ์๋๋๋ง ๋ชปํฉ๋๋ค. ๋ชจ๋ธ์ด ์ด๋ฏธ์ง๋ ์ง์ ์ฝ์ต๋๋ค.
|
|
385
|
-
-
|
|
391
|
+
- **๋ณํ๊ธฐ๊ฐ ์์ผ๋ฉด ์กฐ์ฉํ ์คํจํ์ง ์์ต๋๋ค.** ์ค์น ๋ช
๋ น์ ํ ๋ฒ ์๋ดํ ๋ค ์๋ณธ `Read` ๋ฅผ ๊ทธ๋๋ก ํต๊ณผ์ํต๋๋ค. ๋งค๋ฒ ์๋ฆฌ๋ฉด ๊ทธ๊ฒ๋๋ก ๋ฐฉํด๊ฐ ๋๊ณ , ์๋ฌด ๋ง๋ ํ์ง ์์ผ๋ฉด ๊ณ ์ฅ์ ์จ๊ธฐ๊ฒ ๋ฉ๋๋ค. `doc2md` ๋ฅผ ์ธ์ ์์ด ์คํํ๋ฉด ๋ณํ๊ธฐ์ ํ
๋ฑ๋ก ์ํ๋ฅผ ํ ๋ฒ์ ํ์ธํ ์ ์์ต๋๋ค.
|
|
386
392
|
- **๋ณํ๋ณธ์ ํ๋ก์ ํธ ์์ ๋จ๊ธฐ์ง ์์ต๋๋ค.** ๋๊ตฌ์ ์ํ ๋๋ ํฐ๋ฆฌ ์๋ ๊ถํ `0700` ์ผ๋ก ์ ์ฅํ๋ฏ๋ก `.gitignore` ์ ๋ฌด์์ ์ถ๊ฐํ ํ์๊ฐ ์์ต๋๋ค. ํ์ผ๋ช
์ด ๊ธ์ฌยท๊ณ์ฝยท๊ฐ์ธ์ ๋ณด ๊ฐ์ ํจํด์ ๊ฑธ๋ฆฌ๋ฉด ์์ ๋ณํํ์ง ์์ต๋๋ค.
|
|
387
393
|
- **์์ถ ํญํ์ ๋ง์ต๋๋ค.** pptxยทxlsxยทdocx ๋ zip ์ปจํ
์ด๋์
๋๋ค. ์ ์ธ๋ ํฌ๊ธฐ๋ฅผ ๋จผ์ ๊ฑธ๋ฌ ๋ด๊ณ , ์ ์ธ์ ์กฐ์๋ ์ ์์ผ๋ฏ๋ก ์ค์ ํด์ ๋ฐ์ดํธ๋ ์ํ๊ณผ ๋์กฐํฉ๋๋ค.
|
|
388
394
|
- **์์
์ ํ ์๋ก ์๋ฆ
๋๋ค.** ๋ณํ ์๊ฐ์ ํ์ผ ํฌ๊ธฐ๊ฐ ์๋๋ผ ํ ์๋ฅผ ๋ฐ๋ฆ
๋๋ค(์ค์ธก: PDF 6.3MB 0.9์ด, ์์
5.8MB 47.75์ด). 5๋ง ํ์ ๋์ผ๋ฉด ์๋ถ๋ถ๋ง ๋ณํํ๊ณ , **์๋๋ค๋ ์ฌ์ค๊ณผ ์ ์ฒด ํ ์๋ฅผ ์๋ด์ ํจ๊ป ์ ์ต๋๋ค.**
|
|
@@ -462,6 +468,11 @@ npm uninstall -g claude-cache-monitor && npm i -g claude-token-saver
|
|
|
462
468
|
|
|
463
469
|
## ๋ฆด๋ฆฌ์ค ๋
ธํธ
|
|
464
470
|
|
|
471
|
+
### v3.26.2 (2026-09-04)
|
|
472
|
+
- **doc2md ๋ณํ๊ธฐ๋ฅผ ๋๊ตฌ๊ฐ ์ง์ ์ค์นํฉ๋๋ค.** ์ง๊ธ๊น์ง์ ์๋ด๋ `pip install` ์ด์๋๋ฐ, ์์คํ
ํ์ด์ฌ์ ๊ฑด๋๋ฆฌ๋ผ๋ ์๊ตฌ์ธ ๋ฐ๋ค ์คํํ์ง ์์ผ๋ฉด ํ
๋ง ๋ฑ๋ก๋ ์ฑ ์๋ฌด ์ผ๋ ์ผ์ด๋์ง ์์์ต๋๋ค. `doc2md install-converter` ๊ฐ ์ ์ฉ venv ๋ฅผ ๋ง๋ค์ด markitdown ์ ๋ฃ์ต๋๋ค.
|
|
473
|
+
- **`doc2md` ์ํ ์ถ๋ ฅ์ ํ
๋ฑ๋ก ์ฌ๋ถ๋ฅผ ํจ๊ป ์ ์ต๋๋ค.** ๋ณํ๊ธฐ๋ง ์๋ ค ์ฃผ๋ฉด "ํ
์ ์๋๋ฐ ๋ณํ๊ธฐ๊ฐ ์๋ค"์ "๋ณํ๊ธฐ๋ ์๋๋ฐ ํ
์ด ์๋ค"๊ฐ ๋๊ฐ์ด ์๋ฌด ์ผ๋ ์ ํ๋ ์ํ๋ก ๋ณด์ฌ์, ์ด๋ ์ชฝ์ด ๋น ์ก๋์ง ์ ์ ์์์ต๋๋ค.
|
|
474
|
+
- **๋ชจ๋ฅด๋ ์๋ธ์ปค๋งจ๋๋ฅผ `--hook` ์ผ๋ก ๋ถ๋ฅด๋ฉด ์๋ฌด๊ฒ๋ ์ถ๋ ฅํ์ง ์์ต๋๋ค.** 3.25.0 ์ ์ญ ์ค์น๋ณธ์ด 3.26.0 ์ด ์ด `settings.json` ์ ๋ง๋๋ฉด `doc2md` ๋ฅผ ์ธ์ํ์ง ๋ชปํ๊ณ ๊ธฐ๋ณธ ๋ฆฌํฌํธ๋ก ํ๋ฌ๊ฐ, `Read` ํ ๋๋ง๋ค ํต๊ณ ํ ์ ๋ฌธ์ ํ
์คํธ๋ฆผ์ ๋ฐ์ด ๋ฃ์์ต๋๋ค.
|
|
475
|
+
|
|
465
476
|
### v3.26.0 (2026-09-04)
|
|
466
477
|
- **์ฒจ๋ถ ๋ฌธ์๋ฅผ ์ฝ๊ธฐ ์ ์ Markdown ์ผ๋ก ๋ณํํฉ๋๋ค.** pptxยทxlsxยทpdfยทdocx ๋ฅผ ๊ทธ๋๋ก `Read` ํ๋ฉด ๋ชจ๋ธ์ด ์ฝ์ง ๋ชปํ๋ ๋ฐ์ดํธ๊ฐ ์ปจํ
์คํธ์ ์ฌ๋ผ๊ฐ๋๋ค. `doc2md on` ์ผ๋ก `Read` ํ
์ ๋ฑ๋กํ๋ฉด ํ์ผ์ ํ ๋ฒ ๋ณํํด ์บ์์ ๋๊ณ ๋ณํ๋ณธ์ ์ฝ๊ฒ ํฉ๋๋ค. ๋ณํ๊ธฐ๊ฐ ์์ผ๋ฉด ์๋ด๋ฅผ ํ ๋ฒ๋ง ํ๊ณ ์๋ณธ `Read` ๋ฅผ ํต๊ณผ์ํค๋ฉฐ, ์์ถ ํญํ์ ๋ง๊ณ , 5๋ง ํ์ด ๋๋ ์์
์ ์๋ถ๋ถ๋ง ๋ณํํ ๋ค ์๋๋ค๋ ์ฌ์ค์ ํจ๊ป ์๋ฆฝ๋๋ค. ์์ธํ ๋ด์ฉ์ [doc2md](#-doc2md-์ฒจ๋ถ-๋ฌธ์๋ฅผ-์ฝ๊ธฐ-์ ์-markdown-์ผ๋ก-๋ฐ๊ฟ๋๋ค) ์ ์ ์ฐธ๊ณ ํ์ญ์์ค.
|
|
467
478
|
- **BedrockยทVertex ๊ฒฝ์ ํ๊ฒฝ์ TTL ํ์๋ฅผ ๋ฐ๋ก์ก์์ต๋๋ค.** ๊ฒ์ดํธ์จ์ด๋ ๋ฒํท๋ณ ๋ถํด ๊ฐ์ ๋ด๋ ค๋ณด๋ด์ง ์๋๋ฐ, ํ์ ๋ถ๊ฐ์ผ ๋ 1์๊ฐ์ ๊ธฐ๋ณธ๊ฐ์ผ๋ก ์ก๊ณ ์์์ต๋๋ค. 5๋ถ ๋ฒํท๋ง ์ ๊ณตํ๋ ํ๊ฒฝ์์ ๋จ์ ์๊ฐ์ด ์ต๋ 12๋ฐฐ๋ก ๋ถํ์ด ๋ณด์์ต๋๋ค. ์ด์ ๋ชจ๋ธ ID๋ก ๊ฒ์ดํธ์จ์ด๋ฅผ ๊ฐ์งํด 5๋ถ์ ๊ธฐ๋ณธ๊ฐ์ผ๋ก ์ฐ๊ณ , ๋ผ๋ฒจ์ `5m?` ๋ก ์ ์ด ์ถ์ ์์ ๋ฐํ๋๋ค. `mode ttl=5m` ์ผ๋ก ์ง์ ์ง์ ํ ์๋ ์์ต๋๋ค.
|
package/bin/cli.js
CHANGED
|
@@ -83,7 +83,23 @@ function readUpdateChip() {
|
|
|
83
83
|
}
|
|
84
84
|
}
|
|
85
85
|
|
|
86
|
+
// Subcommands this build knows how to run. Used only by the guard below.
|
|
87
|
+
const KNOWN_SUBCOMMANDS = new Set([
|
|
88
|
+
'last', 'brief', 'history', 'handoff', 'install', 'uninstall', 'mode', 'korean',
|
|
89
|
+
'doc2md', 'harness', 'route-scan', 'compact-window', 'update-check', 'upgrade',
|
|
90
|
+
]);
|
|
91
|
+
|
|
86
92
|
async function main() {
|
|
93
|
+
// A hook invocation names a subcommand and expects either silence or that
|
|
94
|
+
// subcommand's own protocol on stdout. If this build does not have the
|
|
95
|
+
// subcommand โ an older global install against a newer settings.json, which
|
|
96
|
+
// is exactly what a mid-upgrade machine looks like โ falling through to the
|
|
97
|
+
// default report would push a full table into the hook stream on every
|
|
98
|
+
// matching tool call. Say nothing instead.
|
|
99
|
+
if (hasFlag('--hook') && args[0] && !KNOWN_SUBCOMMANDS.has(args[0])) {
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
|
|
87
103
|
// Subcommand: last โ print the most recent warning + how to handle it.
|
|
88
104
|
// Designed for the auto-trigger skill so the user immediately sees
|
|
89
105
|
// "what just fired and how to fix it" without having to read the whole
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "claude-token-saver",
|
|
3
|
-
"version": "3.26.
|
|
3
|
+
"version": "3.26.2",
|
|
4
4
|
"description": "Route the easy work your expensive Claude model keeps repeating down to haiku/sonnet โ post-hoc session analysis, no realtime router, no extra LLM calls.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
package/src/commands/doc2md.js
CHANGED
|
@@ -15,6 +15,27 @@ import { join } from 'node:path';
|
|
|
15
15
|
|
|
16
16
|
const require = createRequire(import.meta.url);
|
|
17
17
|
|
|
18
|
+
/**
|
|
19
|
+
* Whether the Read hook is actually in settings.json.
|
|
20
|
+
*
|
|
21
|
+
* Status output that reports only the converter is misleading: a working
|
|
22
|
+
* converter with no hook, and a hook with no converter, both add up to
|
|
23
|
+
* "nothing happens", and the user has no way to tell which half is missing.
|
|
24
|
+
*/
|
|
25
|
+
function hookRegistered() {
|
|
26
|
+
try {
|
|
27
|
+
const { homedir } = require('node:os');
|
|
28
|
+
const settings = JSON.parse(
|
|
29
|
+
require('node:fs').readFileSync(join(homedir(), '.claude', 'settings.json'), 'utf8'),
|
|
30
|
+
);
|
|
31
|
+
return (settings?.hooks?.PreToolUse || []).some((m) =>
|
|
32
|
+
(m.hooks || []).some((h) => typeof h.command === 'string' && h.command.includes('doc2md --hook')),
|
|
33
|
+
);
|
|
34
|
+
} catch {
|
|
35
|
+
return false;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
18
39
|
export async function run({ args, hasFlag }) {
|
|
19
40
|
const doc2md = require('../doc2md.cjs');
|
|
20
41
|
const sub = args[1];
|
|
@@ -37,16 +58,30 @@ export async function run({ args, hasFlag }) {
|
|
|
37
58
|
return;
|
|
38
59
|
}
|
|
39
60
|
|
|
61
|
+
if (sub === 'install-converter') {
|
|
62
|
+
const res = doc2md.installConverter({ onProgress: (m) => console.log(` ${m}`) });
|
|
63
|
+
if (res.ok) {
|
|
64
|
+
console.log(`โ converter ready: ${res.python}`);
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
console.error(`โ ${res.reason}: ${res.detail}`);
|
|
68
|
+
process.exitCode = 1;
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
|
|
40
72
|
if (sub === 'on') {
|
|
41
73
|
const { installDoc2mdHook } = await import('../installer.js');
|
|
42
74
|
const res = installDoc2mdHook();
|
|
43
75
|
console.log(res.action === 'skipped'
|
|
44
76
|
? `โ ${res.reason}`
|
|
45
77
|
: `โ Read hook ${res.action} (${res.path})`);
|
|
78
|
+
// A registered hook with no converter behind it does nothing at all, and
|
|
79
|
+
// says nothing about it either, which reads as a broken feature. Offer the
|
|
80
|
+
// one command that closes the gap right where the gap is visible.
|
|
46
81
|
const python = doc2md.findInterpreter();
|
|
47
82
|
console.log(python
|
|
48
83
|
? ` converter: markitdown via ${python}`
|
|
49
|
-
: ` converter:
|
|
84
|
+
: ` converter: missing โ run \`${doc2md.INSTALL_HINT}\` or the hook will do nothing`);
|
|
50
85
|
return;
|
|
51
86
|
}
|
|
52
87
|
|
|
@@ -95,6 +130,8 @@ export async function run({ args, hasFlag }) {
|
|
|
95
130
|
console.log(` formats: ${doc2md.TARGET_EXTENSIONS.join(' ')}`);
|
|
96
131
|
console.log(` converter: ${python ? `markitdown via ${python}` : `not installed โ ${doc2md.INSTALL_HINT}`}`);
|
|
97
132
|
console.log(` cache: ${dir} (${cached} file(s))`);
|
|
133
|
+
console.log(` hook: ${hookRegistered() ? 'registered on Read' : 'not registered'}`);
|
|
98
134
|
console.log('');
|
|
135
|
+
if (!python) console.log(`Install the converter: ${doc2md.INSTALL_HINT}`);
|
|
99
136
|
console.log('Enable with: claude-token-saver doc2md on');
|
|
100
137
|
}
|
package/src/doc2md.cjs
CHANGED
|
@@ -158,6 +158,11 @@ function findInterpreter() {
|
|
|
158
158
|
const candidates = [];
|
|
159
159
|
if (process.env.CTS_DOC2MD_PYTHON) candidates.push(process.env.CTS_DOC2MD_PYTHON);
|
|
160
160
|
candidates.push(
|
|
161
|
+
// The tool's own venv, created by `doc2md install-converter`. First
|
|
162
|
+
// because it is the only one this tool controls: telling people to
|
|
163
|
+
// `pip install` into the system interpreter is how a token-saving CLI
|
|
164
|
+
// ends up owning a break in someone else's project.
|
|
165
|
+
managedPython(),
|
|
161
166
|
path.join(os.homedir(), '.local', 'share', 'uv', 'tools', 'markitdown', 'bin', 'python'),
|
|
162
167
|
path.join(os.homedir(), '.local', 'bin', 'markitdown-python'),
|
|
163
168
|
'python3',
|
|
@@ -178,6 +183,60 @@ function findInterpreter() {
|
|
|
178
183
|
|
|
179
184
|
const CONVERTER = path.join(__dirname, '..', 'presets', 'doc2md', 'convert.py');
|
|
180
185
|
|
|
186
|
+
/** Path to the interpreter inside the venv this tool manages. */
|
|
187
|
+
function managedPython() {
|
|
188
|
+
const dir = path.join(userDataDir(), 'doc2md-venv');
|
|
189
|
+
return process.platform === 'win32'
|
|
190
|
+
? path.join(dir, 'Scripts', 'python.exe')
|
|
191
|
+
: path.join(dir, 'bin', 'python');
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
const MARKITDOWN_SPEC = 'markitdown[pptx,pdf,xlsx,docx]';
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* Build the managed venv and install markitdown into it.
|
|
198
|
+
*
|
|
199
|
+
* Kept behind an explicit command: creating a 300MB virtualenv is not
|
|
200
|
+
* something to do because somebody opened a spreadsheet once. But once asked
|
|
201
|
+
* for, it goes somewhere this tool owns, so uninstalling the CLI takes the
|
|
202
|
+
* whole thing with it and no system interpreter is touched.
|
|
203
|
+
*/
|
|
204
|
+
function installConverter({ onProgress = () => {} } = {}) {
|
|
205
|
+
const venv = path.join(userDataDir(), 'doc2md-venv');
|
|
206
|
+
const target = managedPython();
|
|
207
|
+
|
|
208
|
+
if (!fs.existsSync(target)) {
|
|
209
|
+
onProgress(`creating ${venv}`);
|
|
210
|
+
let created = false;
|
|
211
|
+
for (const base of ['python3', 'python']) {
|
|
212
|
+
const r = spawnSync(base, ['-m', 'venv', venv], { encoding: 'utf8', timeout: 180_000 });
|
|
213
|
+
if (r.status === 0) { created = true; break; }
|
|
214
|
+
}
|
|
215
|
+
if (!created) {
|
|
216
|
+
return { ok: false, reason: 'no-python', detail: 'no python3 with the venv module on PATH' };
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
onProgress(`installing ${MARKITDOWN_SPEC}`);
|
|
221
|
+
const install = spawnSync(target, ['-m', 'pip', 'install', '--quiet', MARKITDOWN_SPEC], {
|
|
222
|
+
encoding: 'utf8',
|
|
223
|
+
timeout: 900_000,
|
|
224
|
+
});
|
|
225
|
+
if (install.status !== 0) {
|
|
226
|
+
return { ok: false, reason: 'pip-failed', detail: (install.stderr || '').slice(0, 400) };
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
// The probe is the actual acceptance test: pip can exit 0 and still leave an
|
|
230
|
+
// interpreter that cannot import what was asked for.
|
|
231
|
+
const probe = spawnSync(target, ['-c', 'import markitdown'], { timeout: 60_000, stdio: 'ignore' });
|
|
232
|
+
if (probe.status !== 0) {
|
|
233
|
+
return { ok: false, reason: 'import-failed', detail: 'installed, but markitdown does not import' };
|
|
234
|
+
}
|
|
235
|
+
interpreterCache = target;
|
|
236
|
+
clearNotice();
|
|
237
|
+
return { ok: true, python: target };
|
|
238
|
+
}
|
|
239
|
+
|
|
181
240
|
/**
|
|
182
241
|
* Convert one file. Returns `{ ok: true, cacheFile, meta }`, or
|
|
183
242
|
* `{ ok: false, reason, detail }` where reason is one of:
|
|
@@ -258,6 +317,18 @@ function noticeAlreadyShown() {
|
|
|
258
317
|
}
|
|
259
318
|
}
|
|
260
319
|
|
|
320
|
+
/**
|
|
321
|
+
* Forget that the notice was shown.
|
|
322
|
+
*
|
|
323
|
+
* Called after the converter is installed, so that if it later disappears the
|
|
324
|
+
* user is told once more instead of meeting permanent silence.
|
|
325
|
+
*/
|
|
326
|
+
function clearNotice() {
|
|
327
|
+
try {
|
|
328
|
+
fs.rmSync(noticePath(), { force: true });
|
|
329
|
+
} catch { /* nothing to forget */ }
|
|
330
|
+
}
|
|
331
|
+
|
|
261
332
|
function markNoticeShown() {
|
|
262
333
|
try {
|
|
263
334
|
fs.mkdirSync(userDataDir(), { recursive: true });
|
|
@@ -265,7 +336,7 @@ function markNoticeShown() {
|
|
|
265
336
|
} catch { /* an unwritable state dir just means the notice repeats */ }
|
|
266
337
|
}
|
|
267
338
|
|
|
268
|
-
const INSTALL_HINT = '
|
|
339
|
+
const INSTALL_HINT = 'claude-token-saver doc2md install-converter';
|
|
269
340
|
|
|
270
341
|
/**
|
|
271
342
|
* Decide what to tell Claude Code about one PreToolUse(Read) payload.
|
|
@@ -364,6 +435,10 @@ module.exports = {
|
|
|
364
435
|
TARGET_EXTENSIONS,
|
|
365
436
|
MAX_SOURCE_BYTES,
|
|
366
437
|
INSTALL_HINT,
|
|
438
|
+
MARKITDOWN_SPEC,
|
|
439
|
+
managedPython,
|
|
440
|
+
installConverter,
|
|
441
|
+
clearNotice,
|
|
367
442
|
cacheDir,
|
|
368
443
|
cachePathFor,
|
|
369
444
|
metaPathFor,
|