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 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
- pip install "markitdown[pptx,pdf,xlsx,docx]" # ๋ณ€ํ™˜๊ธฐ๋Š” ํŒŒ์ด์ฌ ํŒจํ‚ค์ง€์ž…๋‹ˆ๋‹ค
376
- claude-token-saver doc2md on # Read ํ›… ๋“ฑ๋ก
377
- claude-token-saver doc2md ๋ณด๊ณ ์„œ.pptx # ์ง์ ‘ ๋ณ€ํ™˜ํ•ด ๊ฒฐ๊ณผ ํ™•์ธ
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
- - **markitdown ์ด ์—†์œผ๋ฉด ์กฐ์šฉํžˆ ์‹คํŒจํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค.** ์„ค์น˜ ๋ช…๋ น์„ ํ•œ ๋ฒˆ ์•ˆ๋‚ดํ•œ ๋’ค ์›๋ณธ `Read` ๋ฅผ ๊ทธ๋Œ€๋กœ ํ†ต๊ณผ์‹œํ‚ต๋‹ˆ๋‹ค. ๋งค๋ฒˆ ์•Œ๋ฆฌ๋ฉด ๊ทธ๊ฒƒ๋Œ€๋กœ ๋ฐฉํ•ด๊ฐ€ ๋˜๊ณ , ์•„๋ฌด ๋ง๋„ ํ•˜์ง€ ์•Š์œผ๋ฉด ๊ณ ์žฅ์„ ์ˆจ๊ธฐ๊ฒŒ ๋ฉ๋‹ˆ๋‹ค.
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.0",
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": {
@@ -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: not installed yet โ€” ${doc2md.INSTALL_HINT}`);
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 = 'pip install "markitdown[pptx,pdf,xlsx,docx]"';
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,