tanuki-context 0.1.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,117 +1,391 @@
1
1
  # tanuki-context
2
2
 
3
- Token-cutting context pipeline as a zero-dependency MCP server.
4
- TypeScript, built with Bun, runs anywhere Node 18 runs:
3
+ [![npm](https://img.shields.io/npm/v/tanuki-context)](https://www.npmjs.com/package/tanuki-context)
4
+ Zero dependencies. 0.98 MB tarball. Runs anywhere Node >= 18 runs. MIT.
5
+
6
+ A model pays about 1 token per 4 characters to read text. A PNG page is
7
+ billed by pixel dimensions (Anthropic's 28x28-pixel patch grid), and a dense
8
+ page fits 3+ characters per token. [pxpipe](https://github.com/teamchong/pxpipe)
9
+ measured ~3.1 chars per image-token on real Claude Code traffic and built a
10
+ transparent proxy on that gap. tanuki-context is the same imaging engine with
11
+ a different contract: the model calls a tool when it wants the cut, sees
12
+ exactly what got imaged, and nothing is rewritten behind its back.
13
+
14
+ It takes bulky context (logs, docs, command output), optionally distills and
15
+ compresses it, then renders it as dense PNG pages. You get the numbers before
16
+ committing: `estimate` prices both routes and says which one wins.
17
+
18
+ ## Measured
19
+
20
+ A 200 KB slice of this machine's system journal: 1,370 lines from tailscaled,
21
+ NetworkManager, the kernel, wpa_supplicant, and friends. Not a synthetic
22
+ fixture, and not cherry-picked to repeat itself (a plain `journalctl` tail is
23
+ so repetitive that distill flattens it to 5 lines, which looks fake even
24
+ though it isn't).
25
+
26
+ | how it enters context | tokens | saved |
27
+ | -------------------------------- | -----: | --------: |
28
+ | pasted as raw text | 51,200 | 0 |
29
+ | rendered as pages (level 0) | 10,752 | **-79%** |
30
+ | distilled first, then rendered | 5,320 | **-90%** |
31
+ | distilled + codebook + tiny font | 2,352 | **-95%** |
32
+
33
+ Reproduce on your own log; every row is one command:
34
+
35
+ ```
36
+ npx tanuki-context estimate your.log 0
37
+ npx tanuki-context estimate your.log 0 --distill
38
+ npx tanuki-context estimate your.log 0 --distill --codebook --font tiny
39
+ ```
40
+
41
+ Savings depend on the content. Repetitive logs crush well. Dense unique text
42
+ still gets the flat ~3-4x from imaging alone. Small inputs technically win
43
+ too, but by so little it is rarely worth the modality switch; see
44
+ [Limits](#limits-plainly).
45
+
46
+ ## Quick start
5
47
 
6
48
  ```
7
49
  npx tanuki-context
8
50
  ```
9
51
 
10
- The original Rust implementation lives on the [`rust` branch](../../tree/rust)
11
- (single ~2.6 MB static binary, same behavior). This branch is a 1:1 port of it —
12
- byte-identical output, verified (see below). Architecture and reasoning live in
13
- [DESIGN.md](DESIGN.md).
52
+ That is the MCP stdio server. Register it in any client:
53
+
54
+ ```json
55
+ { "mcpServers": { "tanuki-context": { "command": "npx", "args": ["-y", "tanuki-context"] } } }
56
+ ```
57
+
58
+ Or poke at the CLI first. Real output, same 200 KB journal slice:
14
59
 
15
60
  ```
16
- text/logs ──► stage 0: distill ──► stage 1: ladder ──► stage 2: pxpipe imaging ──► PNG pages
17
- (dedupe ×N, keep (levels 0–4: (312-col 5×8 pages, 1568×728,
18
- errors verbatim, whitespace/prose/ pixel-priced engine name kept
19
- query slice) dense/caveman) from the original pxpipe mechanic)
61
+ npx tanuki-context estimate journal.log 0
62
+ # { "imageTokens": 10752, "verdict": "PIPELINE cheaper",
63
+ # "recommend": { "distill": true, "codebook": true, "imageTokens": 3920,
64
+ # "pages": 3, "tinyImageTokens": 2352 }, ... }
65
+ npx tanuki-context render journal.log 2 ./pages --codebook
66
+ # ./pages/page0.png ...
67
+ ```
68
+
69
+ ## Clients
70
+
71
+ **OMP (oh-my-pi)** - `~/.omp/agent/mcp.json`, or `.omp/mcp.json` in the project:
72
+
73
+ ```json
74
+ { "mcpServers": { "tanuki-context": { "command": "npx", "args": ["-y", "tanuki-context"] } } }
75
+ ```
76
+
77
+ **jcode** - `~/.jcode/mcp.json`, or `.jcode/mcp.json` in the project. jcode
78
+ speaks stdio only; `"shared": true` reuses one server across sessions:
79
+
80
+ ```json
81
+ { "mcpServers": { "tanuki-context": { "command": "npx", "args": ["-y", "tanuki-context"], "shared": true } } }
82
+ ```
83
+
84
+ **pi** - pi has no MCP layer, so this package doubles as a pi extension (the
85
+ `"pi"` manifest field in package.json):
86
+
87
+ ```
88
+ pi install npm:tanuki-context
89
+ ```
90
+
91
+ The five `tanuki_*` tools register natively; the extension spawns this
92
+ package's own stdio server per session.
93
+
94
+ **Claude Code**:
95
+
96
+ ```
97
+ claude mcp add tanuki-context -- npx -y tanuki-context
98
+ ```
99
+
100
+ **Rust instead of Node?** Same pipeline, same numbers, one 5.7 MB static
101
+ binary:
102
+
103
+ ```
104
+ cargo install --git https://github.com/Osyna/tanuki-context --branch rust
105
+ ```
106
+
107
+ Then `"command": "tanuki-context", "args": []` in any snippet above, or
108
+ `TANUKI_BIN=~/.cargo/bin/tanuki-context` for the pi extension. The two
109
+ engines are parity-tested against each other on every change; see
110
+ [Two engines](#two-engines) below.
111
+
112
+ ## How it works
113
+
114
+ ```mermaid
115
+ flowchart LR
116
+ A[text / logs] --> B{log-shaped?}
117
+ B -- yes --> C["stage 0: distill<br/>collapse repeats xN<br/>errors stay verbatim<br/>optional query slice"]
118
+ B -- no --> D
119
+ C --> D["stage 1: ladder<br/>compression level 0-4"]
120
+ D --> E["stage 2: imaging<br/>312-col pages, 5x8 cells<br/>width-trimmed"]
121
+ E --> F["PNG pages<br/>tokens = 28-px patches"]
20
122
  ```
21
123
 
22
- Remake of the [pxpipe](https://github.com/teamchong/pxpipe) MCP. Page geometry
23
- and BMP glyphs are extracted from pxpipe's generated gray atlas (Spleen 5×8 for
24
- ASCII/code, Unifont for CJK/Cyrillic/etc.), so pages are pixel-faithful to
25
- pxpipe's production renderer. **Astral planes (emoji, plane-1+ symbols) render
26
- too** from GNU `unifont_upper`, box-filtered to the same AA cells which goes
27
- beyond pxpipe (it drops astral). 92,812 codepoints total; only unassigned
28
- codepoints fall back to `▯` (counted + reported).
124
+ Three stages. The first two are optional.
125
+
126
+ **Stage 0, distill.** Built for logs. Repeated lines and repeated multi-line
127
+ blocks collapse to one exemplar plus an exact `xN` count. Near-duplicates
128
+ that differ only in timestamps, ids, or numbers fold into a template, also
129
+ counted. Error, warning, and fail lines are never touched. A `query` regex
130
+ returns only the slice you care about, with context. On the journal slice
131
+ above: 1,371 lines to 621, half the characters gone, all 318 error/warn
132
+ lines kept verbatim.
133
+
134
+ **Stage 1, the ladder.** Five text-compression levels, 0 (off) to 4 (gist
135
+ only). From level 2 up an exact-recall guard keeps code, identifiers, hashes,
136
+ paths, and indented lines verbatim, so loss is confined to prose. Honest
137
+ consequence: on technical files there is little prose to cut. This repo's
138
+ README loses 10% of its characters at level 2 and 14% at level 4; DESIGN.md
139
+ loses 6% at level 4. The ladder earns its keep on wordy prose (meeting notes,
140
+ chat transcripts, tickets); on code-heavy content it mostly just guards.
141
+ Level 1 (whitespace cleanup) is lossless and always safe.
142
+
143
+ **Stage 2, imaging.** The renderer packs text into 312-column pages of 5x8
144
+ antialiased cells (1568x728 px max; short pages get width-trimmed) and
145
+ encodes grayscale PNGs. This is where the big cut comes from, and it is flat:
146
+ raw text pays ~1 token per 4 chars, a full page carries ~28,000 chars for
147
+ 1,456 image tokens. Full Unicode, 92,812 codepoints: CJK, Cyrillic, emoji,
148
+ astral planes. Unassigned codepoints render as readable `[U+HEX]` escapes.
149
+ Nothing disappears silently.
150
+
151
+ ## Density knobs
152
+
153
+ Measured by `reference/methods-report.mjs` on three corpora; regenerate the
154
+ table with `bun reference/methods-report.mjs`. Percentages are against the
155
+ baseline renderer on the same content.
29
156
 
30
- ## Measured (same machine; node reference = pre-rewrite pxpipe MCP)
157
+ | knob | what it does | code | prose | log |
158
+ | ------------------- | ----------------------------------------------------------------------------- | ---: | ----: | ---: |
159
+ | `pack` (default on) | single-cell tabs, `⇥N` indent runs, width-trimmed pages; byte-exact | -5% | 0% | 0% |
160
+ | `codebook` | repeated tokens and path prefixes become 1-cell sigils plus a `·legend·` line | -9% | 0% | -38% |
161
+ | `font: "tiny"` | glyphs box-filtered into 4x6 cells; opt-in, see limits below | -36% | -38% | -40% |
162
+ | all three stacked | | **-45%** | **-38%** | **-62%** |
31
163
 
32
- | metric | node reference | tanuki (this branch) | tanuki (rust branch) |
33
- |---|---:|---:|---:|
34
- | distill 113 MB log | ~4 s | **3.35 s** (bun) | 3.27 s |
35
- | MCP spawn → first response | 152 ms | **86 ms** (bun) / 106 ms (node) | 3 ms |
36
- | idle server RSS | 177 MB | **50 MB** (bun) / 80 MB (node) | ~3 MB |
37
- | install | node + node_modules | **`npx tanuki-context`** (0.97 MB tarball, zero deps) | build from source |
38
- | output parity | — | **byte/pixel-identical to rust** on all fixtures | pages + tokens exact vs pxpipe |
164
+ The three corpora: `src/main.ts` (code), `DESIGN.md` (prose), a path-heavy
165
+ synthetic log. Knobs compose but do not add up linearly; `codebook` needs
166
+ repetition to bite, `pack` needs indentation.
39
167
 
40
168
  ## Tools (MCP, stdio)
41
169
 
42
- - `tanuki_render` `{ text, level?, distill?, query?, reflow?, pack?, font?, codebook? }` → PNG page blocks + breakdown
43
- - `tanuki_estimate` same args; exact page geometry, numbers only (never decompresses glyphs)
44
- - `tanuki_distill` stage 0 alone (logs stay text; error/warn lines always verbatim)
45
- - `tanuki_compress` stage 1 alone (levels 0–4; code/IDs/hashes/paths verbatim from L2 up)
46
- - `tanuki_stats` honest savings summary from `~/.pxpipe/events.jsonl` (env `TANUKI_EVENTS`)
170
+ | tool | arguments | returns |
171
+ | ----------------- | --------------------------------------------------------- | ------------------------------------------------ |
172
+ | `tanuki_render` | `text, level?, distill?, query?, pack?, font?, codebook?` | PNG page blocks + savings breakdown |
173
+ | `tanuki_estimate` | same as render | page geometry, token math, and a `recommend` field: the cheapest safe knob set, priced |
174
+ | `tanuki_distill` | `text, query?` | stage 0 alone; output stays greppable text |
175
+ | `tanuki_compress` | `text, level` | stage 1 alone |
176
+ | `tanuki_stats` | none | savings summary from the session event log |
177
+
178
+ `estimate` walks the safe knob ladder server-side (plain, codebook, distill,
179
+ both, at level 0) and returns the first rung that holds as `recommend`, with
180
+ the tiny-font price alongside. Probing those combos by hand costs about 590
181
+ tokens of tool chatter; the walk is one call.
182
+
183
+ `estimate` never touches pixel data. Call it on everything; render only when
184
+ the verdict says the pipeline wins.
185
+
186
+ ## Limits, plainly
187
+
188
+ - **Rendering is exact; reading is a model skill.** Pages are pixel-faithful
189
+ to pxpipe's production renderer, and pxpipe has receipts on how well models
190
+ read them: near-perfect on arithmetic, gist, and state, but 13/15 on exact
191
+ 12-char hex strings for claude-fable-5, and misses are silent. Their
192
+ numbers: [pxpipe benchmarks](https://github.com/teamchong/pxpipe#benchmark-results-and-receipts).
193
+ Keep byte-exact-critical values (secrets, hashes you must retype) in text.
194
+ - **`font: "tiny"` trades legibility for tokens.** We measured 99.7%
195
+ character read-back, and `M`/`H` is the confusable pair. Fine for logs and
196
+ prose; skip it when the model must transcribe identifiers exactly.
197
+ - **Levels 2-4 reword prose.** The guard keeps code and identifiers verbatim,
198
+ but prose comes back paraphrase-shaped. Level 4 is gist only. Do not use it
199
+ for anything you may need to quote.
200
+ - **Tiny wins are not worth taking.** Width-trimming makes even small pages
201
+ cheap on paper (12 short lines: 56 image tokens vs 115 as text), but a
202
+ 50-token win buys you an image the model has to read back instead of text
203
+ it can quote. The proxy gate defaults to at least 25% and 300 tokens saved
204
+ for exactly this reason; in tool mode, just leave small things alone.
205
+
206
+ ## Implicit mode (proxy)
47
207
 
48
- ### Density knobs (measured, image-tokens vs the pxpipe-faithful baseline)
208
+ If you cannot touch the client, tanuki also runs as a local middlebox, the
209
+ way pxpipe deploys. We left the proxy model early on because rewriting the
210
+ system prompt into a user turn reads exactly like a prompt injection (an
211
+ agent once flagged it as an attack, and it was right to). The middlebox
212
+ came back with rules that remove that exact failure:
49
213
 
50
- All lossless or legend-decodable, all off the parity path (`pack=false,
51
- font=normal, codebook=false` = byte-identical to pxpipe).
214
+ ```
215
+ npx tanuki-context proxy # listens on 127.0.0.1:8484
216
+ export ANTHROPIC_BASE_URL=http://127.0.0.1:8484
217
+ ```
218
+
219
+ Oversized text blocks in user messages and tool results are replaced in
220
+ place by a short visible marker plus PNG blocks, only when `estimate` says
221
+ imaging wins by a clear margin (default: at least 25% and 300 tokens). What
222
+ it never does:
223
+
224
+ - touch the system prompt or tool definitions
225
+ - move content between roles or positions
226
+ - image the latest message (you may need to quote it)
227
+ - rewrite blocks carrying `cache_control` (that would break their cache)
228
+ - rewrite anything when text is cheaper; those requests forward byte-for-byte
229
+
230
+ One thing it deliberately reuses: when the same oversized block appears
231
+ twice in one request (agents re-read files constantly), the second
232
+ byte-identical copy becomes a one-line pointer to the pages above instead
233
+ of the same pages again. Exact repeats only; a one-byte difference images
234
+ independently.
235
+
236
+ Responses stream through untouched. Savings land in `~/.pxpipe/events.jsonl`
237
+ (the same file `tanuki_stats` reads), with the baseline named: what Anthropic
238
+ billed, plus what the imaged blocks would have cost as text.
52
239
 
53
- | knob | what | code | prose | log |
54
- |---|---|---:|---:|---:|
55
- | `pack` (default on) | single-cell tabs, `⇥N` indent runs, width-trimmed pages — byte-exact | −14% | −0% | −0% |
56
- | `codebook` | repeated tokens/path prefixes → 1-cell sigils + a `·legend·` line — reversible | −19% | −0% | −37% |
57
- | `font:"tiny"` | atlas box-filtered into a 4×6 cell — experimental, 99.7% read-back accuracy | −40% | −38% | −40% |
58
- | **all three** | stacked | **−51%** | **−38%** | **−62%** |
240
+ Knobs: `--port N` `--upstream URL` `--level 0-4` `--distill` `--codebook`
241
+ `--font tiny` `--min-chars N` `--ratio X` `--min-save N` `--max-pages N`.
242
+ Defaults are conservative: level 0, nothing lossy on.
243
+
244
+ ## Claude Agent SDK
245
+
246
+ `tanuki-context/agent` wires the pipeline into agents built on the
247
+ [Claude Agent SDK](https://github.com/anthropics/claude-agent-sdk-typescript).
248
+
249
+ External (subprocess per session, zero extra dependencies):
250
+
251
+ ```ts
252
+ import { query } from "@anthropic-ai/claude-agent-sdk";
253
+ import { withTanuki } from "tanuki-context/agent";
254
+
255
+ for await (const msg of query({ prompt: task, options: withTanuki({ model: "claude-..." }) })) {
256
+ // agent now has tanuki_estimate / tanuki_render / tanuki_distill / ...
257
+ }
258
+ ```
259
+
260
+ In-process (one server instance shared by every agent in the process):
261
+
262
+ ```ts
263
+ import { tanukiSdkServer, tanukiAllowedTools, TANUKI_INSTRUCTIONS } from "tanuki-context/agent";
264
+
265
+ const tanuki = await tanukiSdkServer(); // needs the SDK + zod, which agent projects already have
266
+ const options = {
267
+ mcpServers: { tanuki },
268
+ allowedTools: tanukiAllowedTools(),
269
+ systemPrompt: { type: "preset", preset: "claude_code", append: TANUKI_INSTRUCTIONS },
270
+ };
271
+ // hand the same `options` to every agent in the team
272
+ ```
273
+
274
+ `withTanuki(options?)` merges into existing options without clobbering other
275
+ servers or tools. `TANUKI_INSTRUCTIONS` is a canned prompt block that teaches
276
+ agents the estimate-first workflow and the page decode grammar. The core
277
+ package stays zero-dependency: the SDK and zod are optional peers, touched
278
+ only inside `tanukiSdkServer()`.
279
+
280
+ Python SDK, plain dict config:
281
+
282
+ ```python
283
+ from claude_agent_sdk import ClaudeAgentOptions
284
+
285
+ options = ClaudeAgentOptions(
286
+ mcp_servers={"tanuki": {"command": "npx", "args": ["-y", "tanuki-context"]}},
287
+ allowed_tools=[f"mcp__tanuki__tanuki_{t}" for t in
288
+ ["render", "estimate", "distill", "compress", "stats"]],
289
+ )
290
+ ```
59
291
 
60
292
  ## CLI
61
293
 
62
294
  ```
63
295
  npx tanuki-context # MCP stdio server (default)
296
+ npx tanuki-context proxy [--port 8484] [--upstream URL] [knobs] # implicit middlebox
64
297
  npx tanuki-context distill <file> [query] # stats JSON to stdout
65
298
  npx tanuki-context estimate <file> [level] [--distill] [--no-pack] [--font tiny] [--codebook]
66
299
  npx tanuki-context render <file> [level] [outdir] [--no-pack] [--font tiny] [--codebook]
300
+ npx tanuki-context bench <file> <distill|pipeline> [level] [runs] # in-process timing
67
301
  ```
68
302
 
69
- ## Register (MCP)
303
+ ## Footprint
70
304
 
71
- ```json
72
- { "mcpServers": { "tanuki-context": { "command": "npx", "args": ["-y", "tanuki-context"] } } }
73
- ```
305
+ Measured 2026-07-26 on a Ryzen 7 9700X, Linux. Spawn time is the median of 5
306
+ cold starts to the first MCP response; the reference server is the original
307
+ node MCP that wraps pxpipe's library (kept in `reference/node-mcp/`).
74
308
 
75
- ## Layout
309
+ | metric | node | bun | rust | node ref (pxpipe lib) |
310
+ | ----------------------- | ------- | ------- | ------- | --------------------- |
311
+ | spawn to first response | 35 ms | 27 ms | 0.4 ms | 158 ms |
312
+ | idle server RSS | 87 MB | 50 MB | 3.8 MB | 177 MB |
313
+ | distill a 12 MB log | 0.42 s | 0.31 s | 0.28 s | - |
314
+ | install | 0.98 MB tarball, zero deps | same | 5.7 MB static binary | node_modules tree |
76
315
 
77
- | path | role |
78
- |---|---|
79
- | `src/main.ts` | MCP stdio server (hand-rolled JSON-RPC, serde_json-compatible serializer) + CLI |
80
- | `src/distill.ts` | stage 0: 3-pass log distiller (runs/blocks ×N, exact+template near-dupes, query) |
81
- | `src/ladder.ts` | stage 1: levels 0–4 with the exact-recall guard |
82
- | `src/codebook.ts` | stage 0.5: repeated tokens/path prefixes → sigils + `·legend·` (opt-in, reversible) |
83
- | `src/render.ts` | stage 2: reflow (`↵`), pack (`⇥N`/single-cell tab/width-trim), wrap, page split, AA blit, tiny 4×6 font |
84
- | `src/atlas.ts` | full-Unicode glyph atlas (92,812 cps): codepoints/wide eager, pixels lazily inflated |
85
- | `src/png.ts` | minimal grayscale PNG encoder (zlib via `node:zlib`, filter-0 rows) |
86
- | `src/stats.ts` | events.jsonl summary |
87
- | `assets/glyphs.*` | generated glyph data (35,501 codepoints, 0.4 MB packed) |
88
- | `tools/gen-glyphs.mjs` | regenerates `assets/` from pxpipe's atlas |
89
- | `reference/parity-ts.mjs` | parity harness: this port vs the rust binary — CLI JSON deep-equal, MCP replies, PNG pixels |
90
- | `reference/parity.mjs` | original harness: rust vs pxpipe |
91
- | `reference/node-mcp/` | the pre-rewrite node implementation, kept for comparison |
316
+ ## Two engines
92
317
 
93
- ## Build & test
318
+ `main` is this TypeScript package. The [`rust` branch](../../tree/rust) is
319
+ the same pipeline in Rust: same patch-grid token model, same escapes, same
320
+ glyph atlas, same proxy rules. `reference/parity-ts.mjs` holds them to
321
+ byte-identical JSON and pixel-identical PNGs on every knob combination, plus
322
+ a full MCP session including error paths. When one engine changes, the
323
+ other follows, or the parity harness fails loudly. The npm packaging and
324
+ the Agent SDK / pi glue are TypeScript-only; everything the model sees is
325
+ identical.
94
326
 
95
- Everything runs from source with Bun (`bun src/main.ts`) or as the bundled,
96
- Node-compatible single file:
327
+ The imaging engine is a remake of [pxpipe](https://github.com/teamchong/pxpipe):
328
+ page geometry and glyphs come from pxpipe's own generated atlas (Spleen 5x8
329
+ for ASCII and code, Unifont for the rest), so pages are pixel-faithful to
330
+ its production renderer, and the default path (`pack` off, normal font) is
331
+ byte-identical to it. Astral-plane coverage comes from GNU `unifont_upper`,
332
+ box-filtered to the same cells. pxpipe escapes astral codepoints as
333
+ `[U+HEX]`; tanuki renders them and reserves the escape for genuinely
334
+ unassigned codepoints.
97
335
 
98
- ```
99
- bun install --frozen-lockfile # nothing to install — zero deps; safe to skip
100
- bun run build # dist/cli.js (bun build --target=node --minify)
101
- ```
336
+ ## Repository layout
337
+
338
+ | path | role |
339
+ | ---------------------- | -------------------------------------------------------------------------------------------- |
340
+ | `src/main.ts` | MCP stdio server (hand-rolled JSON-RPC) + CLI (entry: `src/cli.ts`) |
341
+ | `src/agent.ts` | Claude Agent SDK glue: `withTanuki`, in-process `tanukiSdkServer` |
342
+ | `src/pi.ts` | pi extension: five native tools over a spawned stdio server (`TANUKI_BIN` picks the engine) |
343
+ | `src/distill.ts` | stage 0: 3-pass log distiller (runs, blocks, template near-dupes, query) |
344
+ | `src/ladder.ts` | stage 1: levels 0-4 with the exact-recall guard |
345
+ | `src/codebook.ts` | repeated tokens and path prefixes to sigils plus `·legend·` (opt-in) |
346
+ | `src/render.ts` | stage 2: reflow, pack, wrap, page split, AA blit, tiny 4x6 font |
347
+ | `src/proxy.ts` | implicit mode: local Anthropic middlebox, in-place block imaging |
348
+ | `src/atlas.ts` | glyph atlas (92,812 codepoints): metadata eager, pixels inflated lazily |
349
+ | `src/png.ts` | minimal grayscale PNG encoder (`node:zlib`, filter-0 rows) |
350
+ | `src/stats.ts` | event log summary |
351
+ | `assets/glyphs.*` | generated glyph data (0.4 MB packed) |
352
+ | `tools/gen-glyphs.mjs` | regenerates `assets/` from pxpipe's atlas |
353
+ | `reference/` | parity and benchmark harnesses (the retired node MCP lives here as an oracle) |
102
354
 
103
- Parity against the Rust implementation (build it from the `rust` branch first —
104
- `git worktree add /tmp/tanuki-rust rust && cargo build --release
105
- --manifest-path /tmp/tanuki-rust/Cargo.toml`, or set `TANUKI_BIN`):
355
+ Architecture notes and the reasoning behind each stage live in
356
+ [DESIGN.md](DESIGN.md).
357
+
358
+ ## Build
359
+
360
+ Runs from source with Bun (`bun src/cli.ts`) or as the bundled,
361
+ Node-compatible files:
106
362
 
107
363
  ```
108
- node reference/parity-ts.mjs # 76 checks: distill/estimate/render/MCP, pixel-exact PNGs
109
- TANUKI_TS="bun src/main.ts" node reference/parity-ts.mjs # same, unbundled
364
+ bun run build # dist/cli.js + dist/agent.js + dist/pi.js
365
+ bun test # 39 tests
366
+ bun run parity # TS vs rust binary, byte/pixel-exact (needs TANUKI_BIN)
110
367
  ```
111
368
 
112
- Regenerating glyphs after a pxpipe atlas rebuild (requires a pxpipe checkout
113
- with `dist/` built):
369
+ Regenerating glyphs after a pxpipe atlas rebuild (needs a pxpipe checkout
370
+ with `dist/` built; the generator fetches `unifont_upper` on first run):
114
371
 
115
372
  ```
116
373
  PXPIPE_DIST=~/Projects/pxpipe/dist node tools/gen-glyphs.mjs
117
374
  ```
375
+
376
+ ## Credits
377
+
378
+ - [pxpipe](https://github.com/teamchong/pxpipe) is where the idea and the
379
+ engine come from: an image is billed by its pixels, not by how much text
380
+ is inside it, and dense text survives the trip. tanuki-context began as a
381
+ Rust rewrite of their MCP. The page geometry and glyphs are extracted
382
+ from pxpipe's own generated atlas, the default render path is still
383
+ byte-identical to their production renderer, and their
384
+ [benchmarks](https://github.com/teamchong/pxpipe#benchmark-results-and-receipts)
385
+ are the read-back evidence this README leans on. If you want the
386
+ whole-bill transparent proxy with per-model profiles and eval receipts,
387
+ use pxpipe itself; the two compose fine.
388
+ - The bitmap fonts inside the atlas are
389
+ [Spleen](https://github.com/fcambus/spleen) 5x8 by Frederic Cambus and
390
+ [GNU Unifont](https://unifoundry.com/unifont/) (BMP coverage plus
391
+ `unifont_upper` for the astral planes).
Binary file
@@ -0,0 +1,67 @@
1
+ // Hand-maintained types for the `tanuki-context/agent` subpath (the built
2
+ // dist/agent.js is a minified bundle; keep this in sync with src/agent.ts).
3
+
4
+ export declare const TANUKI_TOOL_NAMES: readonly [
5
+ "tanuki_render",
6
+ "tanuki_estimate",
7
+ "tanuki_distill",
8
+ "tanuki_compress",
9
+ "tanuki_stats",
10
+ ];
11
+
12
+ /** Canned agent guidance: estimate first, render on a winning verdict,
13
+ * distill logs, and the page decode grammar. */
14
+ export declare const TANUKI_INSTRUCTIONS: string;
15
+
16
+ export interface StdioServerConfig {
17
+ type: "stdio";
18
+ command: string;
19
+ args: string[];
20
+ env?: Record<string, string>;
21
+ }
22
+
23
+ /** External stdio MCP server config for `options.mcpServers`. */
24
+ export declare function tanukiMcpServer(): StdioServerConfig;
25
+
26
+ /** `mcp__<key>__<tool>` names for `options.allowedTools`. */
27
+ export declare function tanukiAllowedTools(key?: string): string[];
28
+
29
+ export interface TanukiOptions {
30
+ mcpServers?: Record<string, unknown>;
31
+ allowedTools?: string[];
32
+ [k: string]: unknown;
33
+ }
34
+
35
+ /** Merge tanuki (server + allowed tools) into an Agent SDK options object. */
36
+ export declare function withTanuki<T extends TanukiOptions>(
37
+ options?: T,
38
+ opts?: { key?: string; server?: unknown },
39
+ ): T & TanukiOptions;
40
+
41
+ export interface ZodChain {
42
+ optional(): ZodChain;
43
+ int(): ZodChain;
44
+ min(n: number): ZodChain;
45
+ max(n: number): ZodChain;
46
+ describe(text: string): ZodChain;
47
+ }
48
+ export interface ZodNamespace {
49
+ string(): ZodChain;
50
+ boolean(): ZodChain;
51
+ number(): ZodChain;
52
+ enum(values: readonly [string, ...string[]]): ZodChain;
53
+ }
54
+ export interface SdkToolSpec {
55
+ name: string;
56
+ description: string;
57
+ inputSchema: Record<string, ZodChain>;
58
+ handler: (args: Record<string, unknown>) => Promise<{ content: unknown[]; isError?: boolean }>;
59
+ }
60
+
61
+ /** The five tools as SDK tool specs (schema factory + handlers). */
62
+ export declare function tanukiSdkToolSpecs(z: ZodNamespace): SdkToolSpec[];
63
+
64
+ /** In-process MCP server (no subprocess; shareable across a team of agents).
65
+ * Requires @anthropic-ai/claude-agent-sdk and zod in the host project.
66
+ * Returned value goes straight into `options.mcpServers`. */
67
+ export declare function tanukiSdkServer(): Promise<unknown>;
package/dist/agent.js ADDED
@@ -0,0 +1,96 @@
1
+ import{createRequire as $Q}from"node:module";var AJ=$Q(import.meta.url);import{existsSync as ZZ}from"node:fs";import{fileURLToPath as $Z}from"node:url";function VQ(J){if(J===32||J>=9&&J<=13)return!0;if(J<133)return!1;return J===133||J===160||J===5760||J>=8192&&J<=8202||J===8232||J===8233||J===8239||J===8287||J===12288}function YJ(J){let Q=0;for(let Z=0;Z<J.length;Z++){let $=J.charCodeAt(Z);if($>=55296&&$<=56319&&Z+1<J.length){let V=J.charCodeAt(Z+1);if(V>=56320&&V<=57343)Z++}Q++}return Q}function GQ(J,Q){let Z=0,$=0;while(Z<J.length&&$<Q.length){let V=J.codePointAt(Z),G=Q.codePointAt($);if(V!==G)return V-G;Z+=V>65535?2:1,$+=G>65535?2:1}return J.length-Z-(Q.length-$)}function i(J){let Q=new Map,Z=(B)=>{Q.set(B,(Q.get(B)??0)+1)},$=(B)=>{if(YJ(B)>=12)Z(B);if(B.includes("/")){let D=B.split("/"),O="";for(let F=0;F<D.length;F++){if(F>0)O+="/";if(O+=D[F],F>=2){let I=O+"/";if(YJ(I)>=12)Z(I)}}}},V=-1;for(let B=0;B<J.length;){let D=J.codePointAt(B),O=D>65535?2:1;if(VQ(D)){if(V>=0)$(J.slice(V,B)),V=-1}else if(V<0)V=B;B+=O}if(V>=0)$(J.slice(V));let G=[];for(let[B,D]of Q)if(D>=3){let O=YJ(B);G.push({k:B,c:D,len:O,saved:(O-1)*D})}G.sort((B,D)=>D.saved-B.saved||GQ(B.k,D.k));let Y=[];for(let B of"§¤¢£¥µ¶ª°±¬×÷ØÞßæðøþ¡¿")if(!J.includes(B))Y.push(B);let H=[],z=[];for(let{k:B,c:D,len:O}of G){if(H.length>=Y.length)break;if((O-1)*D<=O+3)continue;let F=!1;for(let R=0;R<z.length;R++){let T=z[R];if(T.startsWith(B)||B.startsWith(T)){F=!0;break}}if(F)continue;let I=Y[H.length];z.push(B),H.push({sig:I,val:B,len:O})}if(H.length===0)return{text:J,entries:0};let K=H.map((B,D)=>D);K.sort((B,D)=>H[D].len-H[B].len);let q=J;for(let B=0;B<K.length;B++){let{sig:D,val:O}=H[K[B]];q=q.replaceAll(O,D)}let X=`
2
+ ·legend· `;for(let B=0;B<H.length;B++)X+=H[B].sig+"="+H[B].val+" ";return q+=X.slice(0,-1),{text:q,entries:H.length}}import{Buffer as _J}from"node:buffer";var YQ=/\x1b\[[0-9;]*[A-Za-z]/g,HJ=/\b([0-9A-Za-z_]*(error|exception)s?|err|warn(ing)?s?|fail(s|ed|ure|ures)?|panic(s|ked)?|fatal|critical|traceback|denied|refused|timeouts?|timed.?out|assert(s|ed|ion|ions)?|segfault(s|ed)?)\b/i,HQ=/[0-9]{4}-[0-9]{2}-[0-9]{2}[T ][0-9]{2}:[0-9]{2}:[0-9]{2}([.,][0-9]+)?(Z|[+-][0-9]{2}:?[0-9]{2})?/g,zQ=/\b[0-9]{2}:[0-9]{2}:[0-9]{2}([.,][0-9]+)?\b/g,KQ=/\b[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}\b/gi,XQ=/\b[0-9a-f]{7,64}\b/gi,BQ=/\b[0-9]+(\.[0-9]+)?[ \t\u00a0]?(ms|us|µs|ns|s|m|h|%|[KMGT]i?B)?\b/gi,qQ=/[0-9a-f]/i,UQ=3,OQ=8,zJ=2,EJ=40;function DQ(J){if(!qQ.test(J))return J;let Q=J.replace(HQ,"<ts>");return Q=Q.replace(zQ,"<time>"),Q=Q.replace(KQ,"<uuid>"),Q=Q.replace(XQ,"<hex>"),Q.replace(BQ,"<n>")}function h(J){if(J===32)return!0;if(J<9)return!1;if(J<=13)return!0;if(J<133)return!1;return J===133||J===160||J===5760||J>=8192&&J<=8202||J===8232||J===8233||J===8239||J===8287||J===12288}function SJ(J){let Q=0,Z=J.length;while(Q<Z&&h(J.charCodeAt(Q)))Q++;while(Z>Q&&h(J.charCodeAt(Z-1)))Z--;return Q===0&&Z===J.length?J:J.slice(Q,Z)}function FQ(J,Q){let Z=0,$=J.length;while(Z<$&&h(J.charCodeAt(Z)))Z++;while($>Z&&h(J.charCodeAt($-1)))$--;let V=0;for(let G=Z;G<$&&V<Q;V++){let Y=J.charCodeAt(G);G+=Y>=55296&&Y<=56319&&G+1<$?2:1}return V<Q}function MQ(J){let Q=0,Z=J.length;while(Q<Z&&h(J.charCodeAt(Q)))Q++;return J.charCodeAt(Q)===91&&J.charCodeAt(Q+1)===215}function WQ(J){let Q=[],Z=-1,$=!0,V=!1,G=!1,Y=!0;for(let H=0;H<J.length;){let z=J.codePointAt(H),K=z>65535?2:1,q=h(z);if(Y){if(Y=!1,q)Q.push("<v>")}if(q){if(Z>=0)Q.push($?J.slice(Z,H):"<v>"),Z=-1;G=!0}else{if(V=!0,G=!1,Z<0)Z=H,$=!0;if($){let X=z|32;if(X<97||X>122||z>127)$=!1}}H+=K}if(Z>=0)Q.push($?J.slice(Z):"<v>");if(V&&G)Q.push("<v>");return Q.join(" ")}function CJ(J,Q){let Z=0,$=0,V=J.length;while(Z<V&&$<Q){let G=J.charCodeAt(Z);Z+=G>=55296&&G<=56319&&Z+1<V?2:1,$++}return Z>=V?J:J.slice(0,Z)}var jQ=/[.*+?^${}()|[\]\\]/g;function d(J,Q=null,Z=2){let $=Z,G=J.replace(YQ,"").split(`
3
+ `),Y=G.length,H=Y,z=Array(Y),K=Array(Y),q=0;for(let U=0;U<Y;U++){z[U]=DQ(G[U]);let M=HJ.test(G[U]);if(K[U]=M,M)q++}let X=[],B=[],D=[],O=0,F=0;while(F<Y){let U=0,M=0;for(let W=1;W<=OQ&&F+2*W<=Y;W++){if(K[F+W-1])break;let N=1;J:for(;;){let j=F+N*W;if(j+W>Y)break;for(let A=0;A<W;A++)if(K[j+A]||z[j+A]!==z[F+A])break J;N++}if(N>=UQ&&W*(N-1)>U*(M>0?M-1:0))U=W,M=N}if(U>0){for(let W=F;W<F+U;W++)X.push(G[W]),B.push(K[W]),D.push(z[W]);X.push(U===1?` [×${M} similar]`:` [×${M} similar ${U}-line blocks]`),B.push(!1),D.push(null),O++,F+=U*M}else X.push(G[F]),B.push(K[F]),D.push(z[F]),F++}let I=new Map,R=new Map,T=zJ+1,_=[],P=[],L=0,y=0;for(let U=0;U<X.length;U++){let M=X[U],W=B[U];if(W||MQ(M)||FQ(M,4)){_.push(M),P.push(W);continue}let N=D[U],j=I.get(N);if(j!==void 0){if(j.count++,j.count<=zJ)_.push(M),P.push(!1);else L++;continue}let A=WQ(N);I.set(N,{count:1,exemplar:M});let g=R.get(A);if(g!==void 0)if(g.count++,g.count<=T)_.push(M),P.push(!1);else y++;else R.set(A,{count:1,exemplar:M}),_.push(M),P.push(!1)}let v=[];for(let U of I.values())if(U.count>zJ)v.push([U.count,"exact",U.exemplar]);for(let U of R.values())if(U.count>T)v.push([U.count,"template",U.exemplar]);if(v.sort((U,M)=>M[0]-U[0]),v.length>EJ)v.length=EJ;if(L+y>0){let U=`── ${L+y} repeated lines suppressed (${L} exact ×N, ${y} same-template; first occurrences kept above) ──`;_.push(U),P.push(HJ.test(U));for(let[M,W,N]of v){let A=` ×${M}${W==="template"?" (template)":""} ${CJ(SJ(N),160)}`;_.push(A),P.push(HJ.test(A))}}let b;if(Q!=null){let U;try{U=new RegExp(Q,"i")}catch{U=new RegExp(Q.replace(jQ,"\\$&"),"i")}let M=_.length,W=new Uint8Array(M);for(let j=0;j<M;j++)if(P[j]||U.test(_[j])){let A=j>$?j-$:0,g=Math.min(j+$,M-1);W.fill(1,A,g+1)}b=[];let N=0;for(let j=0;j<M;j++)if(W[j]){if(N>0)b.push(`… ${N} lines omitted`),N=0;b.push(_[j])}else N++;if(N>0)b.push(`… ${N} lines omitted`)}else b=_;let IJ=b.join(`
4
+ `),RJ=_J.byteLength(J),PJ=_J.byteLength(IJ),GJ=(1-PJ/RJ)*100,JQ=J.length===0?0:GJ<0?-Math.round(-GJ):Math.round(GJ),QQ=v.map(([U,M,W])=>({count:U,exemplar:CJ(SJ(W),160),kind:M})),ZQ={collapsedRuns:O,importantKept:q,origChars:RJ,origLines:H,outChars:PJ,outLines:b.length,query:Q??null,savedPct:JQ,suppressedLines:L,templateSuppressed:y,topRepeats:QQ};return{distilled:IJ,stats:ZQ}}var r=[["none","none","passthrough (baseline)"],["whitespace","lossless","trailing whitespace + blank-line runs collapsed; safe for code"],["prose","light","L1 + prose lines: collapse spaces, cut redundant filler phrases (code/IDs protected)"],["dense","medium","L2 + prose: drop articles & intensifiers"],["caveman","heavy","L3 + prose: telegraphic — drop function words; gist only, NOT verbatim"]];var E=(J)=>new RegExp(`(?<![\\p{Alphabetic}\\p{Mark}\\p{Decimal_Number}\\p{Connector_Punctuation}\\p{Join_Control}])(?:${J})(?![\\p{Alphabetic}\\p{Mark}\\p{Decimal_Number}\\p{Connector_Punctuation}\\p{Join_Control}])`,"giu"),KJ=[[E("in order to"),"to"],[E("due to the fact that"),"because"],[E("at this point in time"),"now"],[E("in the event that"),"if"],[E("for the purpose of"),"for"],[E("with regard to"),"about"],[E("a large number of"),"many"],[E("it is important to note that"),""],[E("please note that"),""],[E("as a matter of fact"),""],[E("in terms of"),"for"],[E("the fact that"),"that"]],NQ=new RegExp("(?<![\\p{Alphabetic}\\p{Mark}\\p{Decimal_Number}\\p{Connector_Punctuation}\\p{Join_Control}])(?:the|an|a)[\\t\\n\\x0B\\f\\r \\u0085\\u00A0\\u1680\\u2000-\\u200A\\u2028\\u2029\\u202F\\u205F\\u3000]+","giu"),IQ=new RegExp("(?<![\\p{Alphabetic}\\p{Mark}\\p{Decimal_Number}\\p{Connector_Punctuation}\\p{Join_Control}])(?:very|really|just|actually|basically|simply|quite|rather|essentially|literally)[\\t\\n\\x0B\\f\\r \\u0085\\u00A0\\u1680\\u2000-\\u200A\\u2028\\u2029\\u202F\\u205F\\u3000]+","giu"),RQ=new RegExp("(?<![\\p{Alphabetic}\\p{Mark}\\p{Decimal_Number}\\p{Connector_Punctuation}\\p{Join_Control}])(?:is|are|was|were|am|be|been|being|do|does|did|have|has|had|will|would|shall|should|can|could|may|might|of|to|in|on|at|for|with|that|this|these|those|it|its|there|here)(?![\\p{Alphabetic}\\p{Mark}\\p{Decimal_Number}\\p{Connector_Punctuation}\\p{Join_Control}])[\\t\\n\\x0B\\f\\r \\u0085\\u00A0\\u1680\\u2000-\\u200A\\u2028\\u2029\\u202F\\u205F\\u3000]*","giu"),TJ=/ {2,}/g,PQ=new RegExp("[\\t\\n\\x0B\\f\\r \\u0085\\u00A0\\u1680\\u2000-\\u200A\\u2028\\u2029\\u202F\\u205F\\u3000]+([.,;:!?])","gu"),AQ=/\n{3,}/g;function XJ(J){if(J===32||J>=9&&J<=13)return!0;if(J<133)return!1;return J===133||J===160||J===5760||J>=8192&&J<=8202||J===8232||J===8233||J===8239||J===8287||J===12288}function _Q(J){let Q=0,Z=J.length;while(Q<Z){let $=J.codePointAt(Q);if(!XJ($))break;Q+=$>65535?2:1}while(Z>Q){let $=J.charCodeAt(Z-1);if(!XJ($))break;Z--}return Q===0&&Z===J.length?J:J.slice(Q,Z)}function EQ(J){if(J.length===0)return!1;let Q=J.charCodeAt(0);if(Q===32||Q===9)return!0;let Z=0,$=0,V=0,G=!1;for(let Y=0;Y<J.length;){let H=J.codePointAt(Y);if(Y+=H>65535?2:1,Z++,XJ(H)){V=0;continue}if(V++,V>=24)G=!0;if(H>=48&&H<=57||H>=65&&H<=90||H>=97&&H<=122)continue;switch(H){case 46:case 44:case 59:case 58:case 39:case 34:case 33:case 63:case 40:case 41:case 45:continue}$++}if($/Z>0.3)return!0;return G}function SQ(J,Q){let Z=J.replace(TJ," ");for(let V=0;V<KJ.length;V++)Z=Z.replace(KJ[V][0],KJ[V][1]);if(Q>=3)Z=Z.replace(NQ,""),Z=Z.replace(IQ,"");if(Q>=4)Z=Z.replace(RQ,"");Z=Z.replace(TJ," "),Z=_Q(Z.replace(PQ,"$1"));let $=Z.charCodeAt(0);if($>=97&&$<=122)Z=String.fromCharCode($-32)+Z.slice(1);return Z}function a(J,Q){let Z=Math.min(Q,4);if(Z===0)return{compressed:J,protectedLines:0,level:Z};let $=0,V=J.split(`
5
+ `),G=Array(V.length);for(let H=0;H<V.length;H++){let z=V[H],K=z.length;while(K>0){let X=z.charCodeAt(K-1);if(X!==32&&X!==9)break;K--}let q=K===z.length?z:z.slice(0,K);if(Z===1){G[H]=q;continue}if(EQ(q)){$++,G[H]=q;continue}G[H]=SQ(q,Z)}return{compressed:G.join(`
6
+ `).replace(AQ,`
7
+
8
+ `),protectedLines:$,level:Z}}import{readFileSync as qJ}from"node:fs";import{inflateSync as CQ}from"node:zlib";//! Full-BMP glyph atlas (Spleen 5x8 for ASCII/code + Unifont fallback),
9
+ //! extracted from pxpipe's generated gray atlas by `tools/gen-glyphs.mjs`.
10
+ //!
11
+ //! Codepoints + wide flags load eagerly (~175 KB, needed for wrap math);
12
+ //! coverage pixels stay zlib-packed on disk and decompress lazily on the
13
+ //! first blit, so `estimate` never pays for them.
14
+ var w=5,l=8,x=qJ(new URL("../assets/glyphs.cps",import.meta.url)),t=qJ(new URL("../assets/glyphs.wide",import.meta.url)),LJ=(()=>{let J=x.byteLength>>>2;if((x.byteOffset&3)===0)return new Uint32Array(x.buffer,x.byteOffset,J);return new Uint32Array(x.buffer.slice(x.byteOffset,x.byteOffset+(J<<2)))})(),TQ=(()=>{let J=new Uint32Array(t.length),Q=0;for(let Z=0;Z<t.length;Z++)J[Z]=Q,Q+=(t[Z]===1?2*w:w)*l;return J})(),BJ=null;function p(J){let Q=0,Z=LJ.length-1;while(Q<=Z){let $=Q+Z>>>1,V=LJ[$];if(V<J)Q=$+1;else if(V>J)Z=$-1;else return $}return-1}function k(J){return t[J]===1}function UJ(J){if(BJ===null)BJ=CQ(qJ(new URL("../assets/glyphs.pix.z",import.meta.url)));let Q=TQ[J],Z=k(J)?2*w:w;return BJ.subarray(Q,Q+Z*l)}function yJ(J,Q,Z){let $=UJ(J),V=k(J)?2*w:w;return LQ($,V,l,Q,Z)}function LQ(J,Q,Z,$,V){let G=new Uint8Array($*V),Y=Q/$,H=Z/V;for(let z=0;z<V;z++){let K=z*H,q=(z+1)*H;for(let X=0;X<$;X++){let B=X*Y,D=(X+1)*Y,O=0,F=0,I=Math.min(Math.ceil(q),Z);for(let R=Math.floor(K);R<I;R++){let T=Math.min(R+1,q)-Math.max(R,K);if(T<=0)continue;let _=Math.min(Math.ceil(D),Q);for(let P=Math.floor(B);P<_;P++){let L=Math.min(P+1,D)-Math.max(P,B);if(L<=0)continue;let y=L*T;O+=y*J[R*Q+P],F+=y}}G[z*$+X]=F>0?Math.min(255,Math.max(0,Math.round(O/F))):0}}return G}import{deflateSync as yQ}from"node:zlib";//! Minimal grayscale PNG encoder (bit depth 8, color type 0), mirroring
15
+ //! pxpipe's png.ts: IHDR + one IDAT (zlib) + IEND, filter byte 0 per row.
16
+ var wQ=(()=>{let J=new Uint32Array(256);for(let Q=0;Q<256;Q++){let Z=Q;for(let $=0;$<8;$++)Z=(Z&1)!==0?3988292384^Z>>>1:Z>>>1;J[Q]=Z>>>0}return J})();function OJ(J,Q,Z,$){let V=$.length;J[Q]=V>>>24&255,J[Q+1]=V>>>16&255,J[Q+2]=V>>>8&255,J[Q+3]=V&255;let G=Q+4;for(let z=0;z<4;z++)J[G+z]=Z.charCodeAt(z);J.set($,G+4);let Y=4294967295;for(let z=G;z<G+4+V;z++)Y=wQ[(Y^J[z])&255]^Y>>>8;Y=~Y>>>0;let H=G+4+V;return J[H]=Y>>>24&255,J[H+1]=Y>>>16&255,J[H+2]=Y>>>8&255,J[H+3]=Y&255,H+4}function wJ(J,Q,Z){let $=new Uint8Array(Z*(Q+1));for(let z=0,K=0,q=0;z<Z;z++)$[K++]=0,$.set(J.subarray(q,q+Q),K),K+=Q,q+=Q;let V=yQ($,{level:6}),G=new Uint8Array(33+(12+V.length)+12);G[0]=137,G[1]=80,G[2]=78,G[3]=71,G[4]=13,G[5]=10,G[6]=26,G[7]=10;let Y=new Uint8Array(13);Y[0]=Q>>>24&255,Y[1]=Q>>>16&255,Y[2]=Q>>>8&255,Y[3]=Q&255,Y[4]=Z>>>24&255,Y[5]=Z>>>16&255,Y[6]=Z>>>8&255,Y[7]=Z&255,Y[8]=8,Y[9]=0;let H=OJ(G,8,"IHDR",Y);return H=OJ(G,H,"IDAT",V),OJ(G,H,"IEND",new Uint8Array(0)),G}//! Stage 2: the `pxpipe` imaging engine, ported from pxpipe's render.ts
17
+ //! production dense path (bare 5x8 AA cell, 312 cols, 1568x728 pages).
18
+ //! Glyphs cover BMP (pxpipe's atlas: Spleen for ASCII/code, Unifont fallback)
19
+ //! PLUS the astral planes (unifont_upper, incl. emoji) — beyond pxpipe, which
20
+ //! drops astral. Only unassigned codepoints fall back to `▯` and are counted
21
+ //! as dropped.
22
+ //!
23
+ //! Two tanuki-only extensions over the faithful port, both behind knobs so the
24
+ //! `pack=false, font=Normal` path stays byte-identical to pxpipe (parity):
25
+ //! * `pack` — lossless reflow tighter than pxpipe: single-cell tabs (no
26
+ //! 4-col padding) + indent run-length (`⇥N`), plus per-page
27
+ //! width-trim so short payloads stop paying for 1568px rows.
28
+ //! * `font` — `Tiny` renders the same atlas box-filtered into a 4x6 cell
29
+ //! (390 cols x 120 rows/page), ~40% fewer image-tokens; opt-in,
30
+ //! transcription-accuracy gated.
31
+ var n=4,s=4,vQ=1568,bQ=728,e="↵",xQ="⏎",JJ="→",hQ="⇢",DJ="⇥",kQ="⇨",fQ=9647,vJ=4,mQ=3,bJ="0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz",uQ=/\n{4,}/g;function c(J){return J.length===4&&J.replace(/[A-Z]/g,(Q)=>Q.toLowerCase())==="tiny"?"tiny":"normal"}function fJ(J){let Q=J==="tiny"?4:w,Z=J==="tiny"?6:l,$=Math.floor((vQ-2*n)/Q),V=Math.floor((bQ-2*s)/Z);return{cw:Q,ch:Z,cols:$,maxLines:V,maxChars:$*V}}var gQ=(()=>{let J=new Uint8Array(128);for(let Q=0;Q<128;Q++){let Z=p(Q);J[Q]=Z>=0&&k(Z)?2:1}return J})();function FJ(J){if(J<128)return gQ[J];let Q=p(J);return Q>=0&&k(Q)?2:1}function dQ(J){let Q=0;for(let Z=0;Z<J.length;Z++){let $=J.charCodeAt(Z);if($>=55296&&$<56320&&Z+1<J.length){let V=J.charCodeAt(Z+1);if(V>=56320&&V<57344)Z++}Q++}return Q}function MJ(J){let Q=J.split(`
32
+ `);for(let Z=0;Z<Q.length;Z++){let $=Q[Z],V=$.length;while(V>0){let G=$.charCodeAt(V-1);if(G!==32&&G!==9)break;V--}if(V!==$.length)Q[Z]=$.slice(0,V)}return Q.join(`
33
+ `).replace(uQ,`
34
+
35
+
36
+ `)}function mJ(J){if(!J.includes("\t"))return J;let Q="",Z=0;for(let $ of J)if($==="\t"){let V=vJ-Z%vJ;Q+=JJ;for(let G=1;G<V;G++)Q+=" ";Z+=V}else Q+=$,Z+=FJ($.codePointAt(0));return Q}function lQ(J){let Q=J.includes("\t")?J.replaceAll("\t",JJ):J,Z=0;while(Z<Q.length&&Q.charCodeAt(Z)===32)Z++;if(Z>=mQ&&Z<bJ.length)return DJ+bJ[Z]+Q.slice(Z);return Q}function uJ(J){return J.includes(e)?J.replaceAll(e,xQ):J}function pQ(J){let Q=uJ(J);if(Q.includes(JJ))Q=Q.replaceAll(JJ,hQ);if(Q.includes(DJ))Q=Q.replaceAll(DJ,kQ);return Q}function nQ(J){let Q=MJ(J).split(`
37
+ `);for(let Z=0;Z<Q.length;Z++)Q[Z]=mJ(Q[Z]);return Q.join(e)}function cQ(J){let Q=MJ(J).split(`
38
+ `);for(let Z=0;Z<Q.length;Z++)Q[Z]=lQ(Q[Z]);return Q.join(e)}function oQ(J,Q){let Z=[],$=MJ(J);for(let V of $.split(`
39
+ `)){let G=mJ(V);if(G.length===0){Z.push("");continue}let Y="",H=0;for(let z of G){let K=FJ(z.codePointAt(0));if(H+K>Q)Z.push(Y),Y=z,H=K;else Y+=z,H+=K}if(Y.length!==0)Z.push(Y)}return Z}function iQ(J,Q,Z){let $=[],V=[],G=0;for(let Y of J){let H=dQ(Y),z=H+(V.length!==0?1:0);if(V.length!==0&&(V.length>=Q||G+z>Z))$.push(V),V=[],G=0;G+=H+(V.length!==0?1:0),V.push(Y)}if(V.length!==0)$.push(V);return $}function gJ(J){if(J<32)return!0;if(J>=127&&J<=159)return!0;if(J>=768&&J<=879)return!0;if(J===8203||J===8204||J===8205||J===8288||J===65279)return!0;if(J>=65024&&J<=65039)return!0;if(J>=917760&&J<=917999)return!0;return!1}function rQ(J){let Q=null,Z=0;for(let $ of J){let V=$.codePointAt(0);if(p(V)<0&&!gJ(V)){if(Q===null)Q=J.slice(0,Z);Q+=`[U+${V.toString(16).toUpperCase()}]`}else if(Q!==null)Q+=$;Z+=$.length}return Q??J}function dJ(J,Q,Z,$){let V=Q?Z?cQ(pQ(J)):nQ(uJ(J)):J;return iQ(oQ(rQ(V),$.cols),$.maxLines,$.maxChars)}function lJ(J,Q,Z){if(!Z)return 2*n+Q.cols*Q.cw;let $=0;for(let Y of J){let H=0;for(let z of Y)H+=FJ(z.codePointAt(0));if(H>Q.cols)H=Q.cols;if(H>$)$=H}let V=2*n+$*Q.cw,G=2*n+Q.cw;return V>G?V:G}var xJ=new Map;function hJ(J,Q,Z,$,V,G,Y){let H=p(V);if(H<0)return 0;let z=k(H),K=z?2*G.cw:G.cw,q;if(Y==="normal")q=UJ(H);else{let X=xJ.get(H);if(X===void 0)X=yJ(H,K,G.ch),xJ.set(H,X);q=X}for(let X=0;X<G.ch;X++){let B=($+X)*Q+Z,D=X*K;for(let O=0;O<K;O++){let F=q[D+O];if(F>0){let I=B+O;if(F>J[I])J[I]=F}}}return z?2:1}function aQ(J,Q,Z,$){let V=lJ(J,Q,Z),G=2*s+J.length*Q.ch,Y=new Uint8Array(V*G),H=0;for(let z=0;z<J.length;z++){let K=s+z*Q.ch,q=0;for(let X of J[z]){if(q>=Q.cols)break;let B=n+q*Q.cw,D=hJ(Y,V,B,K,X.codePointAt(0),Q,$);if(D===0){if(D=1,gJ(X.codePointAt(0)));else if(H++,X!==" ")hJ(Y,V,B,K,fQ,Q,$)}q+=D}}for(let z=0;z<Y.length;z++)Y[z]=255-Y[z];return{png:wJ(Y,V,G),width:V,height:G,dropped:H}}function WJ(J,Q,Z,$){let V=fJ($),G=dJ(J,Q,Z,V).map((K)=>aQ(K,V,Z,$)),Y=0,H=0,z=0;for(let K of G)Y+=K.width*K.height,H+=pJ(K.width,K.height),z+=K.dropped;return{pages:G,pixels:Y,tokens:H,dropped:z}}function QJ(J,Q,Z,$){let V=fJ($),G=dJ(J,Q,Z,V),Y=0,H=0;for(let z of G){let K=lJ(z,V,Z),q=2*s+z.length*V.ch;Y+=K*q,H+=pJ(K,q)}return{pages:G.length,pixels:Y,tokens:H}}var kJ=28;function pJ(J,Q){return Math.ceil(J/kJ)*Math.ceil(Q/kJ)}import{readFileSync as tQ}from"node:fs";import{join as sQ}from"node:path";//! pxpipe measurement-log summary (~/.pxpipe/events.jsonl), same math as the
40
+ //! node MCP: actual = every way input bytes get billed (input + cache reads +
41
+ //! cache creates) — ignoring cache_read would fake the savings.
42
+ class ZJ{value;constructor(J){this.value=J}}function nJ(){let J=process.env.TANUKI_EVENTS;if(J!==void 0)return J;let Q=process.env.HOME??"";return sQ(Q,".pxpipe","events.jsonl")}function f(J){return typeof J==="number"&&Number.isSafeInteger(J)&&J>=0?J:0}function eQ(J){return J<0?-Math.round(-J):Math.round(J)}function jJ(){let J=nJ(),Q;try{Q=tQ(J,"utf8")}catch{return{available:!1,note:`no ${J} yet`}}let Z=0,$=0,V=0,G=0,Y=0,H=0;for(let K of Q.split(`
43
+ `)){if(K.trim().length===0)continue;let q;try{q=JSON.parse(K)}catch{continue}Z+=1;let X=q!==null&&typeof q==="object"&&!Array.isArray(q)?q:{};if(X.compressed===!0)$+=1,V+=f(X.orig_chars),G+=f(X.image_count);Y+=f(X.baseline_tokens),H+=f(X.input_tokens)+f(X.cache_read_tokens)+f(X.cache_create_tokens)}let z=Y>0&&H>0?new ZJ(eQ((1-H/Y)*1000)/10):null;return{available:!0,requests:Z,compressedRequests:$,imagedChars:V,imagesEmitted:G,baselineTokens:Y,actualInputTokens:H,estInputSavedPct:z}}//! Implicit mode: a local Anthropic middlebox, the pxpipe deployment shape
44
+ //! without pxpipe's structural flaw. Rules that keep it injection-shaped-free:
45
+ //!
46
+ //! 1. The system prompt and tool definitions are NEVER touched.
47
+ //! 2. Nothing moves between roles or positions: an oversized text block is
48
+ //! replaced IN PLACE by a short overt marker + PNG page blocks, in the
49
+ //! same user-role message (Anthropic allows image blocks in user content
50
+ //! and inside tool_result content).
51
+ //! 3. The latest message is never imaged (the model may need to quote it).
52
+ //! 4. Blocks carrying cache_control are never touched (rewriting would
53
+ //! defeat the cache they exist for).
54
+ //! 5. Imaging only happens when `estimate` says it wins by a clear margin;
55
+ //! everything else passes through byte-for-byte.
56
+ //!
57
+ //! Responses stream through untouched; usage is scraped from the stream for
58
+ //! the ~/.pxpipe/events.jsonl savings log (same format tanuki_stats reads).
59
+ //! tanuki-context — token-cutting context pipeline.
60
+ //! pipeline: text -> distill (stage 0, logs) -> ladder level 0-4 (stage 1)
61
+ //! -> pxpipe imaging (stage 2, name kept from the original mechanic)
62
+ //!
63
+ //! Default: MCP stdio server (newline-delimited JSON-RPC 2.0).
64
+ //! CLI: tanuki-context distill <file> [query]
65
+ //! tanuki-context estimate <file> [level] [--distill]
66
+ //! tanuki-context render <file> [level] [outdir]
67
+ //! tanuki-context proxy [--port N] [--upstream URL] [knobs] (implicit mode)
68
+ var cJ="0.2.0",NJ=6;function JZ(J,Q){let Z=Math.min(J.length,Q.length),$=0;while($<Z&&J.charCodeAt($)===Q.charCodeAt($))$++;if($>=Z)return J.length-Q.length;return J.codePointAt($)-Q.codePointAt($)}function C(J,Q,Z=""){if(J===null||J===void 0)return"null";if(J instanceof ZJ){let z=J.value;return Number.isFinite(z)&&Number.isInteger(z)?z.toFixed(1):String(z)}let $=typeof J;if($==="string")return JSON.stringify(J);if($==="number"||$==="boolean")return String(J);if(Array.isArray(J)){if(J.length===0)return"[]";if(!Q){let q="[";for(let X=0;X<J.length;X++){if(X>0)q+=",";q+=C(J[X],!1)}return q+"]"}let z=Z+" ",K=`[
69
+ `;for(let q=0;q<J.length;q++){if(q>0)K+=`,
70
+ `;K+=z+C(J[q],!0,z)}return K+`
71
+ `+Z+"]"}let V=J,G=Object.keys(V).sort(JZ);if(G.length===0)return"{}";if(!Q){let z="{";for(let K=0;K<G.length;K++){if(K>0)z+=",";z+=JSON.stringify(G[K])+":"+C(V[G[K]],!1)}return z+"}"}let Y=Z+" ",H=`{
72
+ `;for(let z=0;z<G.length;z++){if(z>0)H+=`,
73
+ `;H+=Y+JSON.stringify(G[z])+": "+C(V[G[z]],!0,Y)}return H+`
74
+ `+Z+"}"}function S(J,Q){return J!==null&&typeof J==="object"&&!Array.isArray(J)?J[Q]:void 0}function m(J){return typeof J==="string"?J:null}function $J(J){return typeof J==="boolean"?J:null}function oJ(J){return typeof J==="number"&&Number.isSafeInteger(J)&&J>=0?J:null}function u(J){let Q=0;for(let Z=0;Z<J.length;Z++){Q++;let $=J.charCodeAt(Z);if($>=55296&&$<=56319&&Z+1<J.length){let V=J.charCodeAt(Z+1);if(V>=56320&&V<=57343)Z++}}return Q}function iJ(J,Q,Z,$,V){let G,Y=null;if(Z||$!==null){let K=d(J,$,2);G=K.distilled,Y=K.stats}else G=J;let H=0;if(V){let K=i(G);G=K.text,H=K.entries}let z=a(G,Q);return{stage0:Y,compressed:z.compressed,protectedLines:z.protectedLines,level:z.level,cbEntries:H}}function VJ(J){return Math.round(J/4)}function o(J,Q){if(J===0)return 0;let Z=(1-Q/J)*100;return Z<0?-Math.round(-Z):Math.round(Z)}function rJ(J){return{text:m(S(J,"text"))??"",level:(oJ(S(J,"level"))??0)%256,distill:$J(S(J,"distill"))??!1,query:m(S(J,"query")),reflow:$J(S(J,"reflow"))??!0,pack:$J(S(J,"pack"))??!0,font:m(S(J,"font"))??"normal",codebook:$J(S(J,"codebook"))??!1}}function QZ(J){let Q=d(J,null,2).distilled,Z=[[!1,J],[!0,Q]],$={distill:!1,codebook:!1,text:J,tokens:1/0,pages:0};for(let[G,Y]of Z)for(let H of[!1,!0]){let z=H?i(Y).text:Y,K=QJ(z,!0,!0,c("normal"));if(K.tokens<$.tokens)$={distill:G,codebook:H,text:z,tokens:K.tokens,pages:K.pages}}let V=QJ($.text,!0,!0,c("tiny"));return{codebook:$.codebook,distill:$.distill,imageTokens:$.tokens,pages:$.pages,tinyImageTokens:V.tokens}}function aJ(J){let Q=rJ(J),Z=iJ(Q.text,Q.level,Q.distill,Q.query,Q.codebook),$=c(Q.font),V=QJ(Z.compressed,Q.reflow,Q.pack,$),G=V.tokens,Y=u(Q.text),H=u(Z.compressed),z=VJ(Y),[K,q]=r[Z.level];return{engine:"pxpipe",level:`${Z.level} ${K}`,loss:q,distill:Z.stage0,origChars:Y,stage1Chars:H,stage1SavedPct:o(Y,H),pages:V.pages,imageTokens:G,rawTextTokens:z,totalSavedPct:o(z,G),protectedLines:Z.protectedLines,pack:Q.pack,font:$==="tiny"?"tiny":"normal",codebook:Q.codebook?Z.cbEntries:!1,verdict:G<z?"PIPELINE cheaper":"TEXT cheaper",recommend:QZ(Q.text)}}function tJ(J){let Q=rJ(J),Z=iJ(Q.text,Q.level,Q.distill,Q.query,Q.codebook),$=c(Q.font),V=WJ(Z.compressed,Q.reflow,Q.pack,$),G=V.tokens,Y=u(Q.text),H=u(Z.compressed),z=VJ(Y),[K,q]=r[Z.level],X="";if(Z.stage0!==null){let O=Z.stage0;X+=`distill: ${O.origLines} -> ${O.outLines} lines (-${O.savedPct}% chars, ${O.collapsedRuns} runs, ${O.suppressedLines} exact + ${O.templateSuppressed} template suppressed, ${O.importantKept} error/warn kept)
75
+ `}if(X+=`L${Z.level} ${K} (${q}): ${Y} chars -> ${H} chars (stage1 -${o(Y,H)}%) -> ${V.pages.length} page(s), ~${G} image-tokens
76
+ vs ~${z} text-tokens raw = TOTAL -${o(z,G)}%`,Z.protectedLines>0)X+=` · ${Z.protectedLines} lines kept verbatim`;if(V.dropped>0)X+=` · ${V.dropped} unmapped glyphs -> ▯`;if(Z.cbEntries>0)X+=` · codebook: ${Z.cbEntries} sigils (see ·legend·)`;if($==="tiny")X+=" · font: tiny 4x6";if(Q.pack)X+=" · packed (⇥N indent, → tab)";if(Q.reflow)X+=" · ↵ = newline · engine: pxpipe";let B=[{type:"text",text:X}],D=Math.min(V.pages.length,NJ);for(let O=0;O<D;O++){let F=V.pages[O].png;B.push({type:"image",data:Buffer.from(F.buffer,F.byteOffset,F.byteLength).toString("base64"),mimeType:"image/png"})}if(V.pages.length>NJ)B.push({type:"text",text:`(+${V.pages.length-NJ} more page(s))`});return B}function sJ(J){let Q=m(S(J,"text"))??"",Z=d(Q,m(S(J,"query")),2);return[{type:"text",text:C(Z.stats,!0)},{type:"text",text:Z.distilled}]}function eJ(J){let Q=m(S(J,"text"))??"",Z=(oJ(S(J,"level"))??1)%256,$=a(Q,Z),[V,G,Y]=r[$.level],H=u(Q),z=u($.compressed),K=VJ(H),q=VJ(z),X={level:`${$.level} ${V}`,loss:G,note:Y,origChars:H,outChars:z,approxOrigTokens:K,approxOutTokens:q,savedPct:o(K,q),protectedLines:$.protectedLines};return[{type:"text",text:C(X,!0)},{type:"text",text:$.compressed}]}//! Claude Agent SDK integration (`tanuki-context/agent`).
77
+ //!
78
+ //! Two flavours, both one-liners from an agent's point of view:
79
+ //!
80
+ //! external (subprocess per session, works everywhere):
81
+ //! import { query } from "@anthropic-ai/claude-agent-sdk";
82
+ //! import { withTanuki } from "tanuki-context/agent";
83
+ //! for await (const m of query({ prompt, options: withTanuki() })) { ... }
84
+ //!
85
+ //! in-process (one process shared by a whole team of agents):
86
+ //! import { tanukiSdkServer, tanukiAllowedTools, TANUKI_INSTRUCTIONS } from "tanuki-context/agent";
87
+ //! const tanuki = await tanukiSdkServer();
88
+ //! query({ prompt, options: { mcpServers: { tanuki }, allowedTools: tanukiAllowedTools() } })
89
+ //!
90
+ //! The core package stays zero-dependency: the SDK and zod are touched only
91
+ //! inside `tanukiSdkServer()` via dynamic import, and both are already present
92
+ //! in any Agent SDK project (zod is the SDK's own peer dependency).
93
+ var VZ=["tanuki_render","tanuki_estimate","tanuki_distill","tanuki_compress","tanuki_stats"],GZ=`tanuki-context turns bulky text (logs, command output, docs) into dense PNG pages that cost a fraction of the text tokens.
94
+ Workflow: call tanuki_estimate first (instant, exact, never renders pixels). Its "recommend" field already names the cheapest safe knob set, priced - do not probe combos by hand. If the verdict says "PIPELINE cheaper", call tanuki_render with the recommended knobs and use the returned pages instead of pasting the text.
95
+ For logs, pass distill:true (repeats collapse, error/warn lines stay verbatim; add query:"regex" to slice). For prose you will not quote verbatim, level 2-3 shrinks it further. codebook:true helps path-heavy logs. Never image content you must quote byte-exact at level 4 or font tiny.
96
+ Pages decode as: ↵ = newline, → = tab, ⇥N = N leading spaces, a trailing ·legend· line maps sigils back to full tokens.`;function YZ(){let J=new URL("./cli.js",import.meta.url);if(J.protocol==="file:"){let Q=$Z(J);if(ZZ(Q))return{type:"stdio",command:process.execPath,args:[Q]}}return{type:"stdio",command:"npx",args:["-y","tanuki-context"]}}function HZ(J="tanuki"){return VZ.map((Q)=>`mcp__${J}__${Q}`)}function nZ(J,Q={}){let Z=J??{},$=Q.key??"tanuki";return{...Z,mcpServers:{...Z.mcpServers??{},[$]:Q.server??YZ()},allowedTools:[...Z.allowedTools??[],...HZ($)]}}function zZ(J){let Q=J.string().describe("the bulky text to process"),Z=J.number().int().min(0).max(4).optional().describe("ladder level 0-4 (default 0)"),$={text:Q,level:Z,distill:J.boolean().optional().describe("stage 0 log distiller"),query:J.string().optional().describe("distill: keep matching lines +context"),reflow:J.boolean().optional().describe("pack short lines into full rows (default true)"),pack:J.boolean().optional().describe("indent RLE + width trim, lossless (default true)"),font:J.enum(["normal","tiny"]).optional().describe("tiny = 4x6 cells, ~40% fewer tokens, gated"),codebook:J.boolean().optional().describe("repeated tokens/paths -> sigils + legend")},V=(Y)=>({content:Y}),G=async(Y)=>{try{return V(Y())}catch(H){return{content:[{type:"text",text:`tanuki-context error: ${H instanceof Error?H.message:String(H)}`}],isError:!0}}};return[{name:"tanuki_render",description:"Render text through the pipeline (optional distill/level/codebook) into dense PNG pages. Call after tanuki_estimate says PIPELINE cheaper.",inputSchema:$,handler:(Y)=>G(()=>tJ(Y))},{name:"tanuki_estimate",description:"Exact page/token math for the same arguments as tanuki_render, without touching pixels. Instant; call this first.",inputSchema:$,handler:(Y)=>G(()=>[{type:"text",text:C(aJ(Y),!0)}])},{name:"tanuki_distill",description:"Stage 0 alone: collapse repeated log lines/blocks and template near-dupes; error/warn lines kept verbatim. Output stays greppable text.",inputSchema:{text:Q,query:$.query},handler:(Y)=>G(()=>sJ(Y))},{name:"tanuki_compress",description:"Stage 1 alone: graded text compression, levels 0-4, code/paths/hashes protected from level 2 up.",inputSchema:{text:Q,level:Z},handler:(Y)=>G(()=>eJ(Y))},{name:"tanuki_stats",description:"Session savings summary from the events log (honest denominator: input + cache reads + cache creates).",inputSchema:{},handler:()=>G(()=>[{type:"text",text:C(jJ(),!0)}])}]}async function cZ(){let J,Q;try{J=await import("@anthropic-ai/claude-agent-sdk");let $=await import("zod");Q=$.z??$}catch($){throw Error(`tanukiSdkServer() needs the host project's @anthropic-ai/claude-agent-sdk and zod (npm i @anthropic-ai/claude-agent-sdk zod). Import failed: ${$ instanceof Error?$.message:String($)}`)}let Z=zZ(Q).map(($)=>J.tool($.name,$.description,$.inputSchema,(V)=>$.handler(V)));return J.createSdkMcpServer({name:"tanuki-context",version:cJ,instructions:GZ,tools:Z})}export{nZ as withTanuki,zZ as tanukiSdkToolSpecs,cZ as tanukiSdkServer,YZ as tanukiMcpServer,HZ as tanukiAllowedTools,VZ as TANUKI_TOOL_NAMES,GZ as TANUKI_INSTRUCTIONS};
package/dist/cli.js CHANGED
@@ -1,20 +1,20 @@
1
1
  #!/usr/bin/env node
2
- import{readFileSync as sQ,mkdirSync as eQ,writeFileSync as JZ}from"node:fs";import C from"node:process";function ZQ(J){if(J===32||J>=9&&J<=13)return!0;if(J<133)return!1;return J===133||J===160||J===5760||J>=8192&&J<=8202||J===8232||J===8233||J===8239||J===8287||J===12288}function BJ(J){let Z=0;for(let Q=0;Q<J.length;Q++){let $=J.charCodeAt(Q);if($>=55296&&$<=56319&&Q+1<J.length){let V=J.charCodeAt(Q+1);if(V>=56320&&V<=57343)Q++}Z++}return Z}function $Q(J,Z){let Q=0,$=0;while(Q<J.length&&$<Z.length){let V=J.codePointAt(Q),z=Z.codePointAt($);if(V!==z)return V-z;Q+=V>65535?2:1,$+=z>65535?2:1}return J.length-Q-(Z.length-$)}function SJ(J){let Z=new Map,Q=(O)=>{Z.set(O,(Z.get(O)??0)+1)},$=(O)=>{if(BJ(O)>=12)Q(O);if(O.includes("/")){let X=O.split("/"),F="";for(let D=0;D<X.length;D++){if(D>0)F+="/";if(F+=X[D],D>=2){let R=F+"/";if(BJ(R)>=12)Q(R)}}}},V=-1;for(let O=0;O<J.length;){let X=J.codePointAt(O),F=X>65535?2:1;if(ZQ(X)){if(V>=0)$(J.slice(V,O)),V=-1}else if(V<0)V=O;O+=F}if(V>=0)$(J.slice(V));let z=[];for(let[O,X]of Z)if(X>=3){let F=BJ(O);z.push({k:O,c:X,len:F,saved:(F-1)*X})}z.sort((O,X)=>X.saved-O.saved||$Q(O.k,X.k));let Y=[];for(let O of"§¤¢£¥µ¶ª°±¬×÷ØÞßæðøþ¡¿")if(!J.includes(O))Y.push(O);let G=[],q=[];for(let{k:O,c:X,len:F}of z){if(G.length>=Y.length)break;if((F-1)*X<=F+3)continue;let D=!1;for(let P=0;P<q.length;P++){let L=q[P];if(L.startsWith(O)||O.startsWith(L)){D=!0;break}}if(D)continue;let R=Y[G.length];q.push(O),G.push({sig:R,val:O,len:F})}if(G.length===0)return{text:J,entries:0};let H=G.map((O,X)=>X);H.sort((O,X)=>G[X].len-G[O].len);let U=J;for(let O=0;O<H.length;O++){let{sig:X,val:F}=G[H[O]];U=U.replaceAll(F,X)}let B=`
3
- ·legend· `;for(let O=0;O<G.length;O++)B+=G[O].sig+"="+G[O].val+" ";return U+=B.slice(0,-1),{text:U,entries:G.length}}import{Buffer as TJ}from"node:buffer";var VQ=/\x1b\[[0-9;]*[A-Za-z]/g,OJ=/\b([0-9A-Za-z_]*(error|exception)s?|err|warn(ing)?s?|fail(s|ed|ure|ures)?|panic(s|ked)?|fatal|critical|traceback|denied|refused|timeouts?|timed.?out|assert(s|ed|ion|ions)?|segfault(s|ed)?)\b/i,zQ=/[0-9]{4}-[0-9]{2}-[0-9]{2}[T ][0-9]{2}:[0-9]{2}:[0-9]{2}([.,][0-9]+)?(Z|[+-][0-9]{2}:?[0-9]{2})?/g,GQ=/\b[0-9]{2}:[0-9]{2}:[0-9]{2}([.,][0-9]+)?\b/g,YQ=/\b[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}\b/gi,qQ=/\b[0-9a-f]{7,64}\b/gi,HQ=/\b[0-9]+(\.[0-9]+)?[ \t\u00a0]?(ms|us|µs|ns|s|m|h|%|[KMGT]i?B)?\b/gi,BQ=/[0-9a-f]/i,OQ=3,UQ=8,UJ=2,CJ=40;function XQ(J){if(!BQ.test(J))return J;let Z=J.replace(zQ,"<ts>");return Z=Z.replace(GQ,"<time>"),Z=Z.replace(YQ,"<uuid>"),Z=Z.replace(qQ,"<hex>"),Z.replace(HQ,"<n>")}function u(J){if(J===32)return!0;if(J<9)return!1;if(J<=13)return!0;if(J<133)return!1;return J===133||J===160||J===5760||J>=8192&&J<=8202||J===8232||J===8233||J===8239||J===8287||J===12288}function yJ(J){let Z=0,Q=J.length;while(Z<Q&&u(J.charCodeAt(Z)))Z++;while(Q>Z&&u(J.charCodeAt(Q-1)))Q--;return Z===0&&Q===J.length?J:J.slice(Z,Q)}function KQ(J,Z){let Q=0,$=J.length;while(Q<$&&u(J.charCodeAt(Q)))Q++;while($>Q&&u(J.charCodeAt($-1)))$--;let V=0;for(let z=Q;z<$&&V<Z;V++){let Y=J.charCodeAt(z);z+=Y>=55296&&Y<=56319&&z+1<$?2:1}return V<Z}function FQ(J){let Z=0,Q=J.length;while(Z<Q&&u(J.charCodeAt(Z)))Z++;return J.charCodeAt(Z)===91&&J.charCodeAt(Z+1)===215}function DQ(J){let Z=[],Q=-1,$=!0,V=!1,z=!1,Y=!0;for(let G=0;G<J.length;){let q=J.codePointAt(G),H=q>65535?2:1,U=u(q);if(Y){if(Y=!1,U)Z.push("<v>")}if(U){if(Q>=0)Z.push($?J.slice(Q,G):"<v>"),Q=-1;z=!0}else{if(V=!0,z=!1,Q<0)Q=G,$=!0;if($){let B=q|32;if(B<97||B>122||q>127)$=!1}}G+=H}if(Q>=0)Z.push($?J.slice(Q):"<v>");if(V&&z)Z.push("<v>");return Z.join(" ")}function LJ(J,Z){let Q=0,$=0,V=J.length;while(Q<V&&$<Z){let z=J.charCodeAt(Q);Q+=z>=55296&&z<=56319&&Q+1<V?2:1,$++}return Q>=V?J:J.slice(0,Q)}var MQ=/[.*+?^${}()|[\]\\]/g;function p(J,Z=null,Q=2){let $=Q,z=J.replace(VQ,"").split(`
4
- `),Y=z.length,G=Y,q=Array(Y),H=Array(Y),U=0;for(let K=0;K<Y;K++){q[K]=XQ(z[K]);let M=OJ.test(z[K]);if(H[K]=M,M)U++}let B=[],O=[],X=[],F=0,D=0;while(D<Y){let K=0,M=0;for(let W=1;W<=UQ&&D+2*W<=Y;W++){if(H[D+W-1])break;let N=1;J:for(;;){let j=D+N*W;if(j+W>Y)break;for(let _=0;_<W;_++)if(H[j+_]||q[j+_]!==q[D+_])break J;N++}if(N>=OQ&&W*(N-1)>K*(M>0?M-1:0))K=W,M=N}if(K>0){for(let W=D;W<D+K;W++)B.push(z[W]),O.push(H[W]),X.push(q[W]);B.push(K===1?` [×${M} similar]`:` [×${M} similar ${K}-line blocks]`),O.push(!1),X.push(null),F++,D+=K*M}else B.push(z[D]),O.push(H[D]),X.push(q[D]),D++}let R=new Map,P=new Map,L=UJ+1,S=[],A=[],b=0,w=0;for(let K=0;K<B.length;K++){let M=B[K],W=O[K];if(W||FQ(M)||KQ(M,4)){S.push(M),A.push(W);continue}let N=X[K],j=R.get(N);if(j!==void 0){if(j.count++,j.count<=UJ)S.push(M),A.push(!1);else b++;continue}let _=DQ(N);R.set(N,{count:1,exemplar:M});let l=P.get(_);if(l!==void 0)if(l.count++,l.count<=L)S.push(M),A.push(!1);else w++;else P.set(_,{count:1,exemplar:M}),S.push(M),A.push(!1)}let h=[];for(let K of R.values())if(K.count>UJ)h.push([K.count,"exact",K.exemplar]);for(let K of P.values())if(K.count>L)h.push([K.count,"template",K.exemplar]);if(h.sort((K,M)=>M[0]-K[0]),h.length>CJ)h.length=CJ;if(b+w>0){let K=`── ${b+w} repeated lines suppressed (${b} exact ×N, ${w} same-template; first occurrences kept above) ──`;S.push(K),A.push(OJ.test(K));for(let[M,W,N]of h){let _=` ×${M}${W==="template"?" (template)":""} ${LJ(yJ(N),160)}`;S.push(_),A.push(OJ.test(_))}}let x;if(Z!=null){let K;try{K=new RegExp(Z,"i")}catch{K=new RegExp(Z.replace(MQ,"\\$&"),"i")}let M=S.length,W=new Uint8Array(M);for(let j=0;j<M;j++)if(A[j]||K.test(S[j])){let _=j>$?j-$:0,l=Math.min(j+$,M-1);W.fill(1,_,l+1)}x=[];let N=0;for(let j=0;j<M;j++)if(W[j]){if(N>0)x.push(`… ${N} lines omitted`),N=0;x.push(S[j])}else N++;if(N>0)x.push(`… ${N} lines omitted`)}else x=S;let AJ=x.join(`
5
- `),_J=TJ.byteLength(J),EJ=TJ.byteLength(AJ),HJ=(1-EJ/_J)*100,eJ=J.length===0?0:HJ<0?-Math.round(-HJ):Math.round(HJ),JQ=h.map(([K,M,W])=>({count:K,exemplar:LJ(yJ(W),160),kind:M})),QQ={collapsedRuns:F,importantKept:U,origChars:_J,origLines:G,outChars:EJ,outLines:x.length,query:Z??null,savedPct:eJ,suppressedLines:b,templateSuppressed:w,topRepeats:JQ};return{distilled:AJ,stats:QQ}}var t=[["none","none","passthrough (baseline)"],["whitespace","lossless","trailing whitespace + blank-line runs collapsed; safe for code"],["prose","light","L1 + prose lines: collapse spaces, cut redundant filler phrases (code/IDs protected)"],["dense","medium","L2 + prose: drop articles & intensifiers"],["caveman","heavy","L3 + prose: telegraphic — drop function words; gist only, NOT verbatim"]];var T=(J)=>new RegExp(`(?<![\\p{Alphabetic}\\p{Mark}\\p{Decimal_Number}\\p{Connector_Punctuation}\\p{Join_Control}])(?:${J})(?![\\p{Alphabetic}\\p{Mark}\\p{Decimal_Number}\\p{Connector_Punctuation}\\p{Join_Control}])`,"giu"),XJ=[[T("in order to"),"to"],[T("due to the fact that"),"because"],[T("at this point in time"),"now"],[T("in the event that"),"if"],[T("for the purpose of"),"for"],[T("with regard to"),"about"],[T("a large number of"),"many"],[T("it is important to note that"),""],[T("please note that"),""],[T("as a matter of fact"),""],[T("in terms of"),"for"],[T("the fact that"),"that"]],WQ=new RegExp("(?<![\\p{Alphabetic}\\p{Mark}\\p{Decimal_Number}\\p{Connector_Punctuation}\\p{Join_Control}])(?:the|an|a)[\\t\\n\\x0B\\f\\r \\u0085\\u00A0\\u1680\\u2000-\\u200A\\u2028\\u2029\\u202F\\u205F\\u3000]+","giu"),jQ=new RegExp("(?<![\\p{Alphabetic}\\p{Mark}\\p{Decimal_Number}\\p{Connector_Punctuation}\\p{Join_Control}])(?:very|really|just|actually|basically|simply|quite|rather|essentially|literally)[\\t\\n\\x0B\\f\\r \\u0085\\u00A0\\u1680\\u2000-\\u200A\\u2028\\u2029\\u202F\\u205F\\u3000]+","giu"),NQ=new RegExp("(?<![\\p{Alphabetic}\\p{Mark}\\p{Decimal_Number}\\p{Connector_Punctuation}\\p{Join_Control}])(?:is|are|was|were|am|be|been|being|do|does|did|have|has|had|will|would|shall|should|can|could|may|might|of|to|in|on|at|for|with|that|this|these|those|it|its|there|here)(?![\\p{Alphabetic}\\p{Mark}\\p{Decimal_Number}\\p{Connector_Punctuation}\\p{Join_Control}])[\\t\\n\\x0B\\f\\r \\u0085\\u00A0\\u1680\\u2000-\\u200A\\u2028\\u2029\\u202F\\u205F\\u3000]*","giu"),bJ=/ {2,}/g,IQ=new RegExp("[\\t\\n\\x0B\\f\\r \\u0085\\u00A0\\u1680\\u2000-\\u200A\\u2028\\u2029\\u202F\\u205F\\u3000]+([.,;:!?])","gu"),RQ=/\n{3,}/g;function KJ(J){if(J===32||J>=9&&J<=13)return!0;if(J<133)return!1;return J===133||J===160||J===5760||J>=8192&&J<=8202||J===8232||J===8233||J===8239||J===8287||J===12288}function PQ(J){let Z=0,Q=J.length;while(Z<Q){let $=J.codePointAt(Z);if(!KJ($))break;Z+=$>65535?2:1}while(Q>Z){let $=J.charCodeAt(Q-1);if(!KJ($))break;Q--}return Z===0&&Q===J.length?J:J.slice(Z,Q)}function AQ(J){if(J.length===0)return!1;let Z=J.charCodeAt(0);if(Z===32||Z===9)return!0;let Q=0,$=0,V=0,z=!1;for(let Y=0;Y<J.length;){let G=J.codePointAt(Y);if(Y+=G>65535?2:1,Q++,KJ(G)){V=0;continue}if(V++,V>=24)z=!0;if(G>=48&&G<=57||G>=65&&G<=90||G>=97&&G<=122)continue;switch(G){case 46:case 44:case 59:case 58:case 39:case 34:case 33:case 63:case 40:case 41:case 45:continue}$++}if($/Q>0.3)return!0;return z}function _Q(J,Z){let Q=J.replace(bJ," ");for(let V=0;V<XJ.length;V++)Q=Q.replace(XJ[V][0],XJ[V][1]);if(Z>=3)Q=Q.replace(WQ,""),Q=Q.replace(jQ,"");if(Z>=4)Q=Q.replace(NQ,"");Q=Q.replace(bJ," "),Q=PQ(Q.replace(IQ,"$1"));let $=Q.charCodeAt(0);if($>=97&&$<=122)Q=String.fromCharCode($-32)+Q.slice(1);return Q}function FJ(J,Z){let Q=Math.min(Z,4);if(Q===0)return{compressed:J,protectedLines:0,level:Q};let $=0,V=J.split(`
6
- `),z=Array(V.length);for(let G=0;G<V.length;G++){let q=V[G],H=q.length;while(H>0){let B=q.charCodeAt(H-1);if(B!==32&&B!==9)break;H--}let U=H===q.length?q:q.slice(0,H);if(Q===1){z[G]=U;continue}if(AQ(U)){$++,z[G]=U;continue}z[G]=_Q(U,Q)}return{compressed:z.join(`
7
- `).replace(RQ,`
2
+ import{readFileSync as FZ,mkdirSync as MZ,writeFileSync as WZ}from"node:fs";import S from"node:process";function XQ(J){if(J===32||J>=9&&J<=13)return!0;if(J<133)return!1;return J===133||J===160||J===5760||J>=8192&&J<=8202||J===8232||J===8233||J===8239||J===8287||J===12288}function qJ(J){let Q=0;for(let Z=0;Z<J.length;Z++){let $=J.charCodeAt(Z);if($>=55296&&$<=56319&&Z+1<J.length){let V=J.charCodeAt(Z+1);if(V>=56320&&V<=57343)Z++}Q++}return Q}function BQ(J,Q){let Z=0,$=0;while(Z<J.length&&$<Q.length){let V=J.codePointAt(Z),z=Q.codePointAt($);if(V!==z)return V-z;Z+=V>65535?2:1,$+=z>65535?2:1}return J.length-Z-(Q.length-$)}function i(J){let Q=new Map,Z=(U)=>{Q.set(U,(Q.get(U)??0)+1)},$=(U)=>{if(qJ(U)>=12)Z(U);if(U.includes("/")){let O=U.split("/"),q="";for(let F=0;F<O.length;F++){if(F>0)q+="/";if(q+=O[F],F>=2){let R=q+"/";if(qJ(R)>=12)Z(R)}}}},V=-1;for(let U=0;U<J.length;){let O=J.codePointAt(U),q=O>65535?2:1;if(XQ(O)){if(V>=0)$(J.slice(V,U)),V=-1}else if(V<0)V=U;U+=q}if(V>=0)$(J.slice(V));let z=[];for(let[U,O]of Q)if(O>=3){let q=qJ(U);z.push({k:U,c:O,len:q,saved:(q-1)*O})}z.sort((U,O)=>O.saved-U.saved||BQ(U.k,O.k));let Y=[];for(let U of"§¤¢£¥µ¶ª°±¬×÷ØÞßæðøþ¡¿")if(!J.includes(U))Y.push(U);let G=[],H=[];for(let{k:U,c:O,len:q}of z){if(G.length>=Y.length)break;if((q-1)*O<=q+3)continue;let F=!1;for(let A=0;A<H.length;A++){let y=H[A];if(y.startsWith(U)||U.startsWith(y)){F=!0;break}}if(F)continue;let R=Y[G.length];H.push(U),G.push({sig:R,val:U,len:q})}if(G.length===0)return{text:J,entries:0};let K=G.map((U,O)=>O);K.sort((U,O)=>G[O].len-G[U].len);let B=J;for(let U=0;U<K.length;U++){let{sig:O,val:q}=G[K[U]];B=B.replaceAll(q,O)}let X=`
3
+ ·legend· `;for(let U=0;U<G.length;U++)X+=G[U].sig+"="+G[U].val+" ";return B+=X.slice(0,-1),{text:B,entries:G.length}}import{Buffer as wJ}from"node:buffer";var OQ=/\x1b\[[0-9;]*[A-Za-z]/g,DJ=/\b([0-9A-Za-z_]*(error|exception)s?|err|warn(ing)?s?|fail(s|ed|ure|ures)?|panic(s|ked)?|fatal|critical|traceback|denied|refused|timeouts?|timed.?out|assert(s|ed|ion|ions)?|segfault(s|ed)?)\b/i,qQ=/[0-9]{4}-[0-9]{2}-[0-9]{2}[T ][0-9]{2}:[0-9]{2}:[0-9]{2}([.,][0-9]+)?(Z|[+-][0-9]{2}:?[0-9]{2})?/g,DQ=/\b[0-9]{2}:[0-9]{2}:[0-9]{2}([.,][0-9]+)?\b/g,FQ=/\b[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}\b/gi,MQ=/\b[0-9a-f]{7,64}\b/gi,WQ=/\b[0-9]+(\.[0-9]+)?[ \t\u00a0]?(ms|us|µs|ns|s|m|h|%|[KMGT]i?B)?\b/gi,NQ=/[0-9a-f]/i,jQ=3,IQ=8,FJ=2,vJ=40;function RQ(J){if(!NQ.test(J))return J;let Q=J.replace(qQ,"<ts>");return Q=Q.replace(DQ,"<time>"),Q=Q.replace(FQ,"<uuid>"),Q=Q.replace(MQ,"<hex>"),Q.replace(WQ,"<n>")}function d(J){if(J===32)return!0;if(J<9)return!1;if(J<=13)return!0;if(J<133)return!1;return J===133||J===160||J===5760||J>=8192&&J<=8202||J===8232||J===8233||J===8239||J===8287||J===12288}function bJ(J){let Q=0,Z=J.length;while(Q<Z&&d(J.charCodeAt(Q)))Q++;while(Z>Q&&d(J.charCodeAt(Z-1)))Z--;return Q===0&&Z===J.length?J:J.slice(Q,Z)}function AQ(J,Q){let Z=0,$=J.length;while(Z<$&&d(J.charCodeAt(Z)))Z++;while($>Z&&d(J.charCodeAt($-1)))$--;let V=0;for(let z=Z;z<$&&V<Q;V++){let Y=J.charCodeAt(z);z+=Y>=55296&&Y<=56319&&z+1<$?2:1}return V<Q}function PQ(J){let Q=0,Z=J.length;while(Q<Z&&d(J.charCodeAt(Q)))Q++;return J.charCodeAt(Q)===91&&J.charCodeAt(Q+1)===215}function _Q(J){let Q=[],Z=-1,$=!0,V=!1,z=!1,Y=!0;for(let G=0;G<J.length;){let H=J.codePointAt(G),K=H>65535?2:1,B=d(H);if(Y){if(Y=!1,B)Q.push("<v>")}if(B){if(Z>=0)Q.push($?J.slice(Z,G):"<v>"),Z=-1;z=!0}else{if(V=!0,z=!1,Z<0)Z=G,$=!0;if($){let X=H|32;if(X<97||X>122||H>127)$=!1}}G+=K}if(Z>=0)Q.push($?J.slice(Z):"<v>");if(V&&z)Q.push("<v>");return Q.join(" ")}function xJ(J,Q){let Z=0,$=0,V=J.length;while(Z<V&&$<Q){let z=J.charCodeAt(Z);Z+=z>=55296&&z<=56319&&Z+1<V?2:1,$++}return Z>=V?J:J.slice(0,Z)}var SQ=/[.*+?^${}()|[\]\\]/g;function b(J,Q=null,Z=2){let $=Z,z=J.replace(OQ,"").split(`
4
+ `),Y=z.length,G=Y,H=Array(Y),K=Array(Y),B=0;for(let D=0;D<Y;D++){H[D]=RQ(z[D]);let M=DJ.test(z[D]);if(K[D]=M,M)B++}let X=[],U=[],O=[],q=0,F=0;while(F<Y){let D=0,M=0;for(let W=1;W<=IQ&&F+2*W<=Y;W++){if(K[F+W-1])break;let j=1;J:for(;;){let N=F+j*W;if(N+W>Y)break;for(let _=0;_<W;_++)if(K[N+_]||H[N+_]!==H[F+_])break J;j++}if(j>=jQ&&W*(j-1)>D*(M>0?M-1:0))D=W,M=j}if(D>0){for(let W=F;W<F+D;W++)X.push(z[W]),U.push(K[W]),O.push(H[W]);X.push(D===1?` [×${M} similar]`:` [×${M} similar ${D}-line blocks]`),U.push(!1),O.push(null),q++,F+=D*M}else X.push(z[F]),U.push(K[F]),O.push(H[F]),F++}let R=new Map,A=new Map,y=FJ+1,T=[],P=[],w=0,v=0;for(let D=0;D<X.length;D++){let M=X[D],W=U[D];if(W||PQ(M)||AQ(M,4)){T.push(M),P.push(W);continue}let j=O[D],N=R.get(j);if(N!==void 0){if(N.count++,N.count<=FJ)T.push(M),P.push(!1);else w++;continue}let _=_Q(j);R.set(j,{count:1,exemplar:M});let o=A.get(_);if(o!==void 0)if(o.count++,o.count<=y)T.push(M),P.push(!1);else v++;else A.set(_,{count:1,exemplar:M}),T.push(M),P.push(!1)}let f=[];for(let D of R.values())if(D.count>FJ)f.push([D.count,"exact",D.exemplar]);for(let D of A.values())if(D.count>y)f.push([D.count,"template",D.exemplar]);if(f.sort((D,M)=>M[0]-D[0]),f.length>vJ)f.length=vJ;if(w+v>0){let D=`── ${w+v} repeated lines suppressed (${w} exact ×N, ${v} same-template; first occurrences kept above) ──`;T.push(D),P.push(DJ.test(D));for(let[M,W,j]of f){let _=` ×${M}${W==="template"?" (template)":""} ${xJ(bJ(j),160)}`;T.push(_),P.push(DJ.test(_))}}let m;if(Q!=null){let D;try{D=new RegExp(Q,"i")}catch{D=new RegExp(Q.replace(SQ,"\\$&"),"i")}let M=T.length,W=new Uint8Array(M);for(let N=0;N<M;N++)if(P[N]||D.test(T[N])){let _=N>$?N-$:0,o=Math.min(N+$,M-1);W.fill(1,_,o+1)}m=[];let j=0;for(let N=0;N<M;N++)if(W[N]){if(j>0)m.push(`… ${j} lines omitted`),j=0;m.push(T[N])}else j++;if(j>0)m.push(`… ${j} lines omitted`)}else m=T;let CJ=m.join(`
5
+ `),LJ=wJ.byteLength(J),yJ=wJ.byteLength(CJ),OJ=(1-yJ/LJ)*100,HQ=J.length===0?0:OJ<0?-Math.round(-OJ):Math.round(OJ),KQ=f.map(([D,M,W])=>({count:D,exemplar:xJ(bJ(W),160),kind:M})),UQ={collapsedRuns:q,importantKept:B,origChars:LJ,origLines:G,outChars:yJ,outLines:m.length,query:Q??null,savedPct:HQ,suppressedLines:w,templateSuppressed:v,topRepeats:KQ};return{distilled:CJ,stats:UQ}}var ZJ=[["none","none","passthrough (baseline)"],["whitespace","lossless","trailing whitespace + blank-line runs collapsed; safe for code"],["prose","light","L1 + prose lines: collapse spaces, cut redundant filler phrases (code/IDs protected)"],["dense","medium","L2 + prose: drop articles & intensifiers"],["caveman","heavy","L3 + prose: telegraphic — drop function words; gist only, NOT verbatim"]];var C=(J)=>new RegExp(`(?<![\\p{Alphabetic}\\p{Mark}\\p{Decimal_Number}\\p{Connector_Punctuation}\\p{Join_Control}])(?:${J})(?![\\p{Alphabetic}\\p{Mark}\\p{Decimal_Number}\\p{Connector_Punctuation}\\p{Join_Control}])`,"giu"),MJ=[[C("in order to"),"to"],[C("due to the fact that"),"because"],[C("at this point in time"),"now"],[C("in the event that"),"if"],[C("for the purpose of"),"for"],[C("with regard to"),"about"],[C("a large number of"),"many"],[C("it is important to note that"),""],[C("please note that"),""],[C("as a matter of fact"),""],[C("in terms of"),"for"],[C("the fact that"),"that"]],EQ=new RegExp("(?<![\\p{Alphabetic}\\p{Mark}\\p{Decimal_Number}\\p{Connector_Punctuation}\\p{Join_Control}])(?:the|an|a)[\\t\\n\\x0B\\f\\r \\u0085\\u00A0\\u1680\\u2000-\\u200A\\u2028\\u2029\\u202F\\u205F\\u3000]+","giu"),TQ=new RegExp("(?<![\\p{Alphabetic}\\p{Mark}\\p{Decimal_Number}\\p{Connector_Punctuation}\\p{Join_Control}])(?:very|really|just|actually|basically|simply|quite|rather|essentially|literally)[\\t\\n\\x0B\\f\\r \\u0085\\u00A0\\u1680\\u2000-\\u200A\\u2028\\u2029\\u202F\\u205F\\u3000]+","giu"),CQ=new RegExp("(?<![\\p{Alphabetic}\\p{Mark}\\p{Decimal_Number}\\p{Connector_Punctuation}\\p{Join_Control}])(?:is|are|was|were|am|be|been|being|do|does|did|have|has|had|will|would|shall|should|can|could|may|might|of|to|in|on|at|for|with|that|this|these|those|it|its|there|here)(?![\\p{Alphabetic}\\p{Mark}\\p{Decimal_Number}\\p{Connector_Punctuation}\\p{Join_Control}])[\\t\\n\\x0B\\f\\r \\u0085\\u00A0\\u1680\\u2000-\\u200A\\u2028\\u2029\\u202F\\u205F\\u3000]*","giu"),kJ=/ {2,}/g,LQ=new RegExp("[\\t\\n\\x0B\\f\\r \\u0085\\u00A0\\u1680\\u2000-\\u200A\\u2028\\u2029\\u202F\\u205F\\u3000]+([.,;:!?])","gu"),yQ=/\n{3,}/g;function WJ(J){if(J===32||J>=9&&J<=13)return!0;if(J<133)return!1;return J===133||J===160||J===5760||J>=8192&&J<=8202||J===8232||J===8233||J===8239||J===8287||J===12288}function wQ(J){let Q=0,Z=J.length;while(Q<Z){let $=J.codePointAt(Q);if(!WJ($))break;Q+=$>65535?2:1}while(Z>Q){let $=J.charCodeAt(Z-1);if(!WJ($))break;Z--}return Q===0&&Z===J.length?J:J.slice(Q,Z)}function vQ(J){if(J.length===0)return!1;let Q=J.charCodeAt(0);if(Q===32||Q===9)return!0;let Z=0,$=0,V=0,z=!1;for(let Y=0;Y<J.length;){let G=J.codePointAt(Y);if(Y+=G>65535?2:1,Z++,WJ(G)){V=0;continue}if(V++,V>=24)z=!0;if(G>=48&&G<=57||G>=65&&G<=90||G>=97&&G<=122)continue;switch(G){case 46:case 44:case 59:case 58:case 39:case 34:case 33:case 63:case 40:case 41:case 45:continue}$++}if($/Z>0.3)return!0;return z}function bQ(J,Q){let Z=J.replace(kJ," ");for(let V=0;V<MJ.length;V++)Z=Z.replace(MJ[V][0],MJ[V][1]);if(Q>=3)Z=Z.replace(EQ,""),Z=Z.replace(TQ,"");if(Q>=4)Z=Z.replace(CQ,"");Z=Z.replace(kJ," "),Z=wQ(Z.replace(LQ,"$1"));let $=Z.charCodeAt(0);if($>=97&&$<=122)Z=String.fromCharCode($-32)+Z.slice(1);return Z}function r(J,Q){let Z=Math.min(Q,4);if(Z===0)return{compressed:J,protectedLines:0,level:Z};let $=0,V=J.split(`
6
+ `),z=Array(V.length);for(let G=0;G<V.length;G++){let H=V[G],K=H.length;while(K>0){let X=H.charCodeAt(K-1);if(X!==32&&X!==9)break;K--}let B=K===H.length?H:H.slice(0,K);if(Z===1){z[G]=B;continue}if(vQ(B)){$++,z[G]=B;continue}z[G]=bQ(B,Z)}return{compressed:z.join(`
7
+ `).replace(yQ,`
8
8
 
9
- `),protectedLines:$,level:Q}}import{readFileSync as MJ}from"node:fs";import{inflateSync as EQ}from"node:zlib";//! Full-BMP glyph atlas (Spleen 5x8 for ASCII/code + Unifont fallback),
9
+ `),protectedLines:$,level:Z}}import{appendFileSync as YZ,mkdirSync as HZ}from"node:fs";import{dirname as KZ}from"node:path";import eJ from"node:http";import UZ from"node:https";import{URL as XZ}from"node:url";import{readFileSync as jJ}from"node:fs";import{inflateSync as xQ}from"node:zlib";//! Full-BMP glyph atlas (Spleen 5x8 for ASCII/code + Unifont fallback),
10
10
  //! extracted from pxpipe's generated gray atlas by `tools/gen-glyphs.mjs`.
11
11
  //!
12
12
  //! Codepoints + wide flags load eagerly (~175 KB, needed for wrap math);
13
13
  //! coverage pixels stay zlib-packed on disk and decompress lazily on the
14
14
  //! first blit, so `estimate` never pays for them.
15
- var v=5,c=8,m=MJ(new URL("../assets/glyphs.cps",import.meta.url)),s=MJ(new URL("../assets/glyphs.wide",import.meta.url)),wJ=(()=>{let J=m.byteLength>>>2;if((m.byteOffset&3)===0)return new Uint32Array(m.buffer,m.byteOffset,J);return new Uint32Array(m.buffer.slice(m.byteOffset,m.byteOffset+(J<<2)))})(),SQ=(()=>{let J=new Uint32Array(s.length),Z=0;for(let Q=0;Q<s.length;Q++)J[Q]=Z,Z+=(s[Q]===1?2*v:v)*c;return J})(),DJ=null;function e(J){let Z=0,Q=wJ.length-1;while(Z<=Q){let $=Z+Q>>>1,V=wJ[$];if(V<J)Z=$+1;else if(V>J)Q=$-1;else return $}return-1}function g(J){return s[J]===1}function WJ(J){if(DJ===null)DJ=EQ(MJ(new URL("../assets/glyphs.pix.z",import.meta.url)));let Z=SQ[J],Q=g(J)?2*v:v;return DJ.subarray(Z,Z+Q*c)}function vJ(J,Z,Q){let $=WJ(J),V=g(J)?2*v:v;return TQ($,V,c,Z,Q)}function TQ(J,Z,Q,$,V){let z=new Uint8Array($*V),Y=Z/$,G=Q/V;for(let q=0;q<V;q++){let H=q*G,U=(q+1)*G;for(let B=0;B<$;B++){let O=B*Y,X=(B+1)*Y,F=0,D=0,R=Math.min(Math.ceil(U),Q);for(let P=Math.floor(H);P<R;P++){let L=Math.min(P+1,U)-Math.max(P,H);if(L<=0)continue;let S=Math.min(Math.ceil(X),Z);for(let A=Math.floor(O);A<S;A++){let b=Math.min(A+1,X)-Math.max(A,O);if(b<=0)continue;let w=b*L;F+=w*J[P*Z+A],D+=w}}z[q*$+B]=D>0?Math.min(255,Math.max(0,Math.round(F/D))):0}}return z}import{deflateSync as CQ}from"node:zlib";//! Minimal grayscale PNG encoder (bit depth 8, color type 0), mirroring
15
+ var x=5,a=8,u=jJ(new URL("../assets/glyphs.cps",import.meta.url)),$J=jJ(new URL("../assets/glyphs.wide",import.meta.url)),hJ=(()=>{let J=u.byteLength>>>2;if((u.byteOffset&3)===0)return new Uint32Array(u.buffer,u.byteOffset,J);return new Uint32Array(u.buffer.slice(u.byteOffset,u.byteOffset+(J<<2)))})(),kQ=(()=>{let J=new Uint32Array($J.length),Q=0;for(let Z=0;Z<$J.length;Z++)J[Z]=Q,Q+=($J[Z]===1?2*x:x)*a;return J})(),NJ=null;function t(J){let Q=0,Z=hJ.length-1;while(Q<=Z){let $=Q+Z>>>1,V=hJ[$];if(V<J)Q=$+1;else if(V>J)Z=$-1;else return $}return-1}function l(J){return $J[J]===1}function IJ(J){if(NJ===null)NJ=xQ(jJ(new URL("../assets/glyphs.pix.z",import.meta.url)));let Q=kQ[J],Z=l(J)?2*x:x;return NJ.subarray(Q,Q+Z*a)}function fJ(J,Q,Z){let $=IJ(J),V=l(J)?2*x:x;return hQ($,V,a,Q,Z)}function hQ(J,Q,Z,$,V){let z=new Uint8Array($*V),Y=Q/$,G=Z/V;for(let H=0;H<V;H++){let K=H*G,B=(H+1)*G;for(let X=0;X<$;X++){let U=X*Y,O=(X+1)*Y,q=0,F=0,R=Math.min(Math.ceil(B),Z);for(let A=Math.floor(K);A<R;A++){let y=Math.min(A+1,B)-Math.max(A,K);if(y<=0)continue;let T=Math.min(Math.ceil(O),Q);for(let P=Math.floor(U);P<T;P++){let w=Math.min(P+1,O)-Math.max(P,U);if(w<=0)continue;let v=w*y;q+=v*J[A*Q+P],F+=v}}z[H*$+X]=F>0?Math.min(255,Math.max(0,Math.round(q/F))):0}}return z}import{deflateSync as fQ}from"node:zlib";//! Minimal grayscale PNG encoder (bit depth 8, color type 0), mirroring
16
16
  //! pxpipe's png.ts: IHDR + one IDAT (zlib) + IEND, filter byte 0 per row.
17
- var yQ=(()=>{let J=new Uint32Array(256);for(let Z=0;Z<256;Z++){let Q=Z;for(let $=0;$<8;$++)Q=(Q&1)!==0?3988292384^Q>>>1:Q>>>1;J[Z]=Q>>>0}return J})();function jJ(J,Z,Q,$){let V=$.length;J[Z]=V>>>24&255,J[Z+1]=V>>>16&255,J[Z+2]=V>>>8&255,J[Z+3]=V&255;let z=Z+4;for(let q=0;q<4;q++)J[z+q]=Q.charCodeAt(q);J.set($,z+4);let Y=4294967295;for(let q=z;q<z+4+V;q++)Y=yQ[(Y^J[q])&255]^Y>>>8;Y=~Y>>>0;let G=z+4+V;return J[G]=Y>>>24&255,J[G+1]=Y>>>16&255,J[G+2]=Y>>>8&255,J[G+3]=Y&255,G+4}function fJ(J,Z,Q){let $=new Uint8Array(Q*(Z+1));for(let q=0,H=0,U=0;q<Q;q++)$[H++]=0,$.set(J.subarray(U,U+Z),H),H+=Z,U+=Z;let V=CQ($,{level:6}),z=new Uint8Array(33+(12+V.length)+12);z[0]=137,z[1]=80,z[2]=78,z[3]=71,z[4]=13,z[5]=10,z[6]=26,z[7]=10;let Y=new Uint8Array(13);Y[0]=Z>>>24&255,Y[1]=Z>>>16&255,Y[2]=Z>>>8&255,Y[3]=Z&255,Y[4]=Q>>>24&255,Y[5]=Q>>>16&255,Y[6]=Q>>>8&255,Y[7]=Q&255,Y[8]=8,Y[9]=0;let G=jJ(z,8,"IHDR",Y);return G=jJ(z,G,"IDAT",V),jJ(z,G,"IEND",new Uint8Array(0)),z}//! Stage 2: the `pxpipe` imaging engine, ported from pxpipe's render.ts
17
+ var mQ=(()=>{let J=new Uint32Array(256);for(let Q=0;Q<256;Q++){let Z=Q;for(let $=0;$<8;$++)Z=(Z&1)!==0?3988292384^Z>>>1:Z>>>1;J[Q]=Z>>>0}return J})();function RJ(J,Q,Z,$){let V=$.length;J[Q]=V>>>24&255,J[Q+1]=V>>>16&255,J[Q+2]=V>>>8&255,J[Q+3]=V&255;let z=Q+4;for(let H=0;H<4;H++)J[z+H]=Z.charCodeAt(H);J.set($,z+4);let Y=4294967295;for(let H=z;H<z+4+V;H++)Y=mQ[(Y^J[H])&255]^Y>>>8;Y=~Y>>>0;let G=z+4+V;return J[G]=Y>>>24&255,J[G+1]=Y>>>16&255,J[G+2]=Y>>>8&255,J[G+3]=Y&255,G+4}function mJ(J,Q,Z){let $=new Uint8Array(Z*(Q+1));for(let H=0,K=0,B=0;H<Z;H++)$[K++]=0,$.set(J.subarray(B,B+Q),K),K+=Q,B+=Q;let V=fQ($,{level:6}),z=new Uint8Array(33+(12+V.length)+12);z[0]=137,z[1]=80,z[2]=78,z[3]=71,z[4]=13,z[5]=10,z[6]=26,z[7]=10;let Y=new Uint8Array(13);Y[0]=Q>>>24&255,Y[1]=Q>>>16&255,Y[2]=Q>>>8&255,Y[3]=Q&255,Y[4]=Z>>>24&255,Y[5]=Z>>>16&255,Y[6]=Z>>>8&255,Y[7]=Z&255,Y[8]=8,Y[9]=0;let G=RJ(z,8,"IHDR",Y);return G=RJ(z,G,"IDAT",V),RJ(z,G,"IEND",new Uint8Array(0)),z}//! Stage 2: the `pxpipe` imaging engine, ported from pxpipe's render.ts
18
18
  //! production dense path (bare 5x8 AA cell, 312 cols, 1568x728 pages).
19
19
  //! Glyphs cover BMP (pxpipe's atlas: Spleen for ASCII/code, Unifont fallback)
20
20
  //! PLUS the astral planes (unifont_upper, incl. emoji) — beyond pxpipe, which
@@ -29,19 +29,40 @@ var yQ=(()=>{let J=new Uint32Array(256);for(let Z=0;Z<256;Z++){let Q=Z;for(let $
29
29
  //! * `font` — `Tiny` renders the same atlas box-filtered into a 4x6 cell
30
30
  //! (390 cols x 120 rows/page), ~40% fewer image-tokens; opt-in,
31
31
  //! transcription-accuracy gated.
32
- var n=4,JJ=4,LQ=1568,bQ=728,QJ="↵",wQ="⏎",ZJ="→",vQ="⇢",NJ="⇥",fQ="⇨",kQ=9647,kJ=4,hQ=3,hJ="0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz",xQ=/\n{4,}/g;function $J(J){return J.length===4&&J.replace(/[A-Z]/g,(Z)=>Z.toLowerCase())==="tiny"?"tiny":"normal"}function uJ(J){let Z=J==="tiny"?4:v,Q=J==="tiny"?6:c,$=Math.floor((LQ-2*n)/Z),V=Math.floor((bQ-2*JJ)/Q);return{cw:Z,ch:Q,cols:$,maxLines:V,maxChars:$*V}}var mQ=(()=>{let J=new Uint8Array(128);for(let Z=0;Z<128;Z++){let Q=e(Z);J[Z]=Q>=0&&g(Q)?2:1}return J})();function IJ(J){if(J<128)return mQ[J];let Z=e(J);return Z>=0&&g(Z)?2:1}function uQ(J){let Z=0;for(let Q=0;Q<J.length;Q++){let $=J.charCodeAt(Q);if($>=55296&&$<56320&&Q+1<J.length){let V=J.charCodeAt(Q+1);if(V>=56320&&V<57344)Q++}Z++}return Z}function RJ(J){let Z=J.split(`
33
- `);for(let Q=0;Q<Z.length;Q++){let $=Z[Q],V=$.length;while(V>0){let z=$.charCodeAt(V-1);if(z!==32&&z!==9)break;V--}if(V!==$.length)Z[Q]=$.slice(0,V)}return Z.join(`
34
- `).replace(xQ,`
32
+ var s=4,VJ=4,uQ=1568,gQ=728,zJ="↵",dQ="⏎",GJ="→",lQ="⇢",AJ="⇥",pQ="⇨",nQ=9647,uJ=4,cQ=3,gJ="0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz",oQ=/\n{4,}/g;function g(J){return J.length===4&&J.replace(/[A-Z]/g,(Q)=>Q.toLowerCase())==="tiny"?"tiny":"normal"}function nJ(J){let Q=J==="tiny"?4:x,Z=J==="tiny"?6:a,$=Math.floor((uQ-2*s)/Q),V=Math.floor((gQ-2*VJ)/Z);return{cw:Q,ch:Z,cols:$,maxLines:V,maxChars:$*V}}var iQ=(()=>{let J=new Uint8Array(128);for(let Q=0;Q<128;Q++){let Z=t(Q);J[Q]=Z>=0&&l(Z)?2:1}return J})();function PJ(J){if(J<128)return iQ[J];let Q=t(J);return Q>=0&&l(Q)?2:1}function rQ(J){let Q=0;for(let Z=0;Z<J.length;Z++){let $=J.charCodeAt(Z);if($>=55296&&$<56320&&Z+1<J.length){let V=J.charCodeAt(Z+1);if(V>=56320&&V<57344)Z++}Q++}return Q}function _J(J){let Q=J.split(`
33
+ `);for(let Z=0;Z<Q.length;Z++){let $=Q[Z],V=$.length;while(V>0){let z=$.charCodeAt(V-1);if(z!==32&&z!==9)break;V--}if(V!==$.length)Q[Z]=$.slice(0,V)}return Q.join(`
34
+ `).replace(oQ,`
35
35
 
36
36
 
37
- `)}function gJ(J){if(!J.includes("\t"))return J;let Z="",Q=0;for(let $ of J)if($==="\t"){let V=kJ-Q%kJ;Z+=ZJ;for(let z=1;z<V;z++)Z+=" ";Q+=V}else Z+=$,Q+=IJ($.codePointAt(0));return Z}function gQ(J){let Z=J.includes("\t")?J.replaceAll("\t",ZJ):J,Q=0;while(Q<Z.length&&Z.charCodeAt(Q)===32)Q++;if(Q>=hQ&&Q<hJ.length)return NJ+hJ[Q]+Z.slice(Q);return Z}function dJ(J){return J.includes(QJ)?J.replaceAll(QJ,wQ):J}function dQ(J){let Z=dJ(J);if(Z.includes(ZJ))Z=Z.replaceAll(ZJ,vQ);if(Z.includes(NJ))Z=Z.replaceAll(NJ,fQ);return Z}function lQ(J){let Z=RJ(J).split(`
38
- `);for(let Q=0;Q<Z.length;Q++)Z[Q]=gJ(Z[Q]);return Z.join(QJ)}function pQ(J){let Z=RJ(J).split(`
39
- `);for(let Q=0;Q<Z.length;Q++)Z[Q]=gQ(Z[Q]);return Z.join(QJ)}function cQ(J,Z){let Q=[],$=RJ(J);for(let V of $.split(`
40
- `)){let z=gJ(V);if(z.length===0){Q.push("");continue}let Y="",G=0;for(let q of z){let H=IJ(q.codePointAt(0));if(G+H>Z)Q.push(Y),Y=q,G=H;else Y+=q,G+=H}if(Y.length!==0)Q.push(Y)}return Q}function nQ(J,Z,Q){let $=[],V=[],z=0;for(let Y of J){let G=uQ(Y),q=G+(V.length!==0?1:0);if(V.length!==0&&(V.length>=Z||z+q>Q))$.push(V),V=[],z=0;z+=G+(V.length!==0?1:0),V.push(Y)}if(V.length!==0)$.push(V);return $}function lJ(J,Z,Q,$){let V=Z?Q?pQ(dQ(J)):lQ(dJ(J)):J;return nQ(cQ(V,$.cols),$.maxLines,$.maxChars)}function pJ(J,Z,Q){if(!Q)return 2*n+Z.cols*Z.cw;let $=0;for(let Y of J){let G=0;for(let q of Y)G+=IJ(q.codePointAt(0));if(G>Z.cols)G=Z.cols;if(G>$)$=G}let V=2*n+$*Z.cw,z=2*n+Z.cw;return V>z?V:z}var xJ=new Map;function mJ(J,Z,Q,$,V,z,Y){let G=e(V);if(G<0)return 0;let q=g(G),H=q?2*z.cw:z.cw,U;if(Y==="normal")U=WJ(G);else{let B=xJ.get(G);if(B===void 0)B=vJ(G,H,z.ch),xJ.set(G,B);U=B}for(let B=0;B<z.ch;B++){let O=($+B)*Z+Q,X=B*H;for(let F=0;F<H;F++){let D=U[X+F];if(D>0){let R=O+F;if(D>J[R])J[R]=D}}}return q?2:1}function oQ(J,Z,Q,$){let V=pJ(J,Z,Q),z=2*JJ+J.length*Z.ch,Y=new Uint8Array(V*z),G=0;for(let q=0;q<J.length;q++){let H=JJ+q*Z.ch,U=0;for(let B of J[q]){if(U>=Z.cols)break;let O=n+U*Z.cw,X=mJ(Y,V,O,H,B.codePointAt(0),Z,$);if(X===0){if(G++,B!==" ")mJ(Y,V,O,H,kQ,Z,$);X=1}U+=X}}for(let q=0;q<Y.length;q++)Y[q]=255-Y[q];return{png:fJ(Y,V,z),width:V,height:z,dropped:G}}function VJ(J,Z,Q,$){let V=uJ($),z=lJ(J,Z,Q,V).map((q)=>oQ(q,V,Q,$)),Y=0,G=0;for(let q of z)Y+=q.width*q.height,G+=q.dropped;return{pages:z,pixels:Y,dropped:G}}function cJ(J,Z,Q,$){let V=uJ($),z=lJ(J,Z,Q,V),Y=0;for(let G of z)Y+=pJ(G,V,Q)*(2*JJ+G.length*V.ch);return{pages:z.length,pixels:Y}}function o(J){return Math.round(J/750)}import{readFileSync as rQ}from"node:fs";import{join as iQ}from"node:path";//! pxpipe measurement-log summary (~/.pxpipe/events.jsonl), same math as the
37
+ `)}function cJ(J){if(!J.includes("\t"))return J;let Q="",Z=0;for(let $ of J)if($==="\t"){let V=uJ-Z%uJ;Q+=GJ;for(let z=1;z<V;z++)Q+=" ";Z+=V}else Q+=$,Z+=PJ($.codePointAt(0));return Q}function aQ(J){let Q=J.includes("\t")?J.replaceAll("\t",GJ):J,Z=0;while(Z<Q.length&&Q.charCodeAt(Z)===32)Z++;if(Z>=cQ&&Z<gJ.length)return AJ+gJ[Z]+Q.slice(Z);return Q}function oJ(J){return J.includes(zJ)?J.replaceAll(zJ,dQ):J}function tQ(J){let Q=oJ(J);if(Q.includes(GJ))Q=Q.replaceAll(GJ,lQ);if(Q.includes(AJ))Q=Q.replaceAll(AJ,pQ);return Q}function sQ(J){let Q=_J(J).split(`
38
+ `);for(let Z=0;Z<Q.length;Z++)Q[Z]=cJ(Q[Z]);return Q.join(zJ)}function eQ(J){let Q=_J(J).split(`
39
+ `);for(let Z=0;Z<Q.length;Z++)Q[Z]=aQ(Q[Z]);return Q.join(zJ)}function JZ(J,Q){let Z=[],$=_J(J);for(let V of $.split(`
40
+ `)){let z=cJ(V);if(z.length===0){Z.push("");continue}let Y="",G=0;for(let H of z){let K=PJ(H.codePointAt(0));if(G+K>Q)Z.push(Y),Y=H,G=K;else Y+=H,G+=K}if(Y.length!==0)Z.push(Y)}return Z}function QZ(J,Q,Z){let $=[],V=[],z=0;for(let Y of J){let G=rQ(Y),H=G+(V.length!==0?1:0);if(V.length!==0&&(V.length>=Q||z+H>Z))$.push(V),V=[],z=0;z+=G+(V.length!==0?1:0),V.push(Y)}if(V.length!==0)$.push(V);return $}function iJ(J){if(J<32)return!0;if(J>=127&&J<=159)return!0;if(J>=768&&J<=879)return!0;if(J===8203||J===8204||J===8205||J===8288||J===65279)return!0;if(J>=65024&&J<=65039)return!0;if(J>=917760&&J<=917999)return!0;return!1}function ZZ(J){let Q=null,Z=0;for(let $ of J){let V=$.codePointAt(0);if(t(V)<0&&!iJ(V)){if(Q===null)Q=J.slice(0,Z);Q+=`[U+${V.toString(16).toUpperCase()}]`}else if(Q!==null)Q+=$;Z+=$.length}return Q??J}function rJ(J,Q,Z,$){let V=Q?Z?eQ(tQ(J)):sQ(oJ(J)):J;return QZ(JZ(ZZ(V),$.cols),$.maxLines,$.maxChars)}function aJ(J,Q,Z){if(!Z)return 2*s+Q.cols*Q.cw;let $=0;for(let Y of J){let G=0;for(let H of Y)G+=PJ(H.codePointAt(0));if(G>Q.cols)G=Q.cols;if(G>$)$=G}let V=2*s+$*Q.cw,z=2*s+Q.cw;return V>z?V:z}var dJ=new Map;function lJ(J,Q,Z,$,V,z,Y){let G=t(V);if(G<0)return 0;let H=l(G),K=H?2*z.cw:z.cw,B;if(Y==="normal")B=IJ(G);else{let X=dJ.get(G);if(X===void 0)X=fJ(G,K,z.ch),dJ.set(G,X);B=X}for(let X=0;X<z.ch;X++){let U=($+X)*Q+Z,O=X*K;for(let q=0;q<K;q++){let F=B[O+q];if(F>0){let R=U+q;if(F>J[R])J[R]=F}}}return H?2:1}function $Z(J,Q,Z,$){let V=aJ(J,Q,Z),z=2*VJ+J.length*Q.ch,Y=new Uint8Array(V*z),G=0;for(let H=0;H<J.length;H++){let K=VJ+H*Q.ch,B=0;for(let X of J[H]){if(B>=Q.cols)break;let U=s+B*Q.cw,O=lJ(Y,V,U,K,X.codePointAt(0),Q,$);if(O===0){if(O=1,iJ(X.codePointAt(0)));else if(G++,X!==" ")lJ(Y,V,U,K,nQ,Q,$)}B+=O}}for(let H=0;H<Y.length;H++)Y[H]=255-Y[H];return{png:mJ(Y,V,z),width:V,height:z,dropped:G}}function p(J,Q,Z,$){let V=nJ($),z=rJ(J,Q,Z,V).map((K)=>$Z(K,V,Z,$)),Y=0,G=0,H=0;for(let K of z)Y+=K.width*K.height,G+=tJ(K.width,K.height),H+=K.dropped;return{pages:z,pixels:Y,tokens:G,dropped:H}}function YJ(J,Q,Z,$){let V=nJ($),z=rJ(J,Q,Z,V),Y=0,G=0;for(let H of z){let K=aJ(H,V,Z),B=2*VJ+H.length*V.ch;Y+=K*B,G+=tJ(K,B)}return{pages:z.length,pixels:Y,tokens:G}}var pJ=28;function tJ(J,Q){return Math.ceil(J/pJ)*Math.ceil(Q/pJ)}import{readFileSync as VZ}from"node:fs";import{join as zZ}from"node:path";//! pxpipe measurement-log summary (~/.pxpipe/events.jsonl), same math as the
41
41
  //! node MCP: actual = every way input bytes get billed (input + cache reads +
42
42
  //! cache creates) — ignoring cache_read would fake the savings.
43
- class r{value;constructor(J){this.value=J}}function aQ(){let J=process.env.TANUKI_EVENTS;if(J!==void 0)return J;let Z=process.env.HOME??"";return iQ(Z,".pxpipe","events.jsonl")}function d(J){return typeof J==="number"&&Number.isSafeInteger(J)&&J>=0?J:0}function tQ(J){return J<0?-Math.round(-J):Math.round(J)}function nJ(){let J=aQ(),Z;try{Z=rQ(J,"utf8")}catch{return{available:!1,note:`no ${J} yet`}}let Q=0,$=0,V=0,z=0,Y=0,G=0;for(let H of Z.split(`
44
- `)){if(H.trim().length===0)continue;let U;try{U=JSON.parse(H)}catch{continue}Q+=1;let B=U!==null&&typeof U==="object"&&!Array.isArray(U)?U:{};if(B.compressed===!0)$+=1,V+=d(B.orig_chars),z+=d(B.image_count);Y+=d(B.baseline_tokens),G+=d(B.input_tokens)+d(B.cache_read_tokens)+d(B.cache_create_tokens)}let q=Y>0&&G>0?new r(tQ((1-G/Y)*1000)/10):null;return{available:!0,requests:Q,compressedRequests:$,imagedChars:V,imagesEmitted:z,baselineTokens:Y,actualInputTokens:G,estInputSavedPct:q}}//! tanuki-context token-cutting context pipeline, all Rust.
43
+ class e{value;constructor(J){this.value=J}}function SJ(){let J=process.env.TANUKI_EVENTS;if(J!==void 0)return J;let Q=process.env.HOME??"";return zZ(Q,".pxpipe","events.jsonl")}function n(J){return typeof J==="number"&&Number.isSafeInteger(J)&&J>=0?J:0}function GZ(J){return J<0?-Math.round(-J):Math.round(J)}function sJ(){let J=SJ(),Q;try{Q=VZ(J,"utf8")}catch{return{available:!1,note:`no ${J} yet`}}let Z=0,$=0,V=0,z=0,Y=0,G=0;for(let K of Q.split(`
44
+ `)){if(K.trim().length===0)continue;let B;try{B=JSON.parse(K)}catch{continue}Z+=1;let X=B!==null&&typeof B==="object"&&!Array.isArray(B)?B:{};if(X.compressed===!0)$+=1,V+=n(X.orig_chars),z+=n(X.image_count);Y+=n(X.baseline_tokens),G+=n(X.input_tokens)+n(X.cache_read_tokens)+n(X.cache_create_tokens)}let H=Y>0&&G>0?new e(GZ((1-G/Y)*1000)/10):null;return{available:!0,requests:Z,compressedRequests:$,imagedChars:V,imagesEmitted:z,baselineTokens:Y,actualInputTokens:G,estInputSavedPct:H}}//! Implicit mode: a local Anthropic middlebox, the pxpipe deployment shape
45
+ //! without pxpipe's structural flaw. Rules that keep it injection-shaped-free:
46
+ //!
47
+ //! 1. The system prompt and tool definitions are NEVER touched.
48
+ //! 2. Nothing moves between roles or positions: an oversized text block is
49
+ //! replaced IN PLACE by a short overt marker + PNG page blocks, in the
50
+ //! same user-role message (Anthropic allows image blocks in user content
51
+ //! and inside tool_result content).
52
+ //! 3. The latest message is never imaged (the model may need to quote it).
53
+ //! 4. Blocks carrying cache_control are never touched (rewriting would
54
+ //! defeat the cache they exist for).
55
+ //! 5. Imaging only happens when `estimate` says it wins by a clear margin;
56
+ //! everything else passes through byte-for-byte.
57
+ //!
58
+ //! Responses stream through untouched; usage is scraped from the stream for
59
+ //! the ~/.pxpipe/events.jsonl savings log (same format tanuki_stats reads).
60
+ var c={level:0,distill:!1,codebook:!1,font:"normal",minChars:4000,ratio:0.75,minSave:300,maxPages:20};function EJ(J){let Q=0;for(let Z=0;Z<J.length;Z++){let $=J.charCodeAt(Z);if($<55296||$>56319)Q++}return Q}function BZ(J,Q){let Z=EJ(J);if(Z<Q.minChars)return null;let $=J;if(Q.distill)$=b($,null,2).distilled;let V=0;if(Q.codebook){let K=i($);$=K.text,V=K.entries}if(Q.level>0)$=r($,Q.level).compressed;let z=Math.round(Z/4),Y=p($,!0,!0,Q.font);if(Y.pages.length>Q.maxPages)return null;if(Y.tokens>z*Q.ratio||z-Y.tokens<Q.minSave)return null;let H=[{type:"text",text:`[tanuki-context: ${Z} chars imaged in place as ${Y.pages.length} PNG page(s), ~${Y.tokens} vs ~${z} text tokens. ↵=newline →=tab ⇥N=indent`+(V>0?`; ·legend· line maps ${V} sigils`:"")+"]"}];for(let K of Y.pages)H.push({type:"image",source:{type:"base64",media_type:"image/png",data:Buffer.from(K.png.buffer,K.png.byteOffset,K.png.byteLength).toString("base64")}});return{blocks:H,origChars:Z,pages:Y.pages.length,savedTokens:z-Y.tokens}}function HJ(J){return J!==null&&typeof J==="object"&&!Array.isArray(J)}function OZ(J,Q){let Z;try{Z=JSON.parse(J)}catch{return null}if(!HJ(Z)||!Array.isArray(Z.messages))return null;let $=0,V=0,z=0,Y=0,G=new Map,H=(K)=>{let B=G.get(K),X;if(B!==void 0){let U=EJ(K),O=`[tanuki-context: ${U} chars, byte-identical to a block imaged above (${B} PNG page(s)); not repeated]`;X={blocks:[{type:"text",text:O}],origChars:U,pages:0,savedTokens:Math.round(U/4)-Math.round(EJ(O)/4)}}else if(X=BZ(K,Q),X)G.set(K,X.pages);if(X)$++,V+=X.origChars,z+=X.pages,Y+=X.savedTokens;return X};for(let K=0;K<Z.messages.length-1;K++){let B=Z.messages[K];if(!HJ(B)||B.role!=="user")continue;if(typeof B.content==="string"){let U=H(B.content);if(U)B.content=U.blocks;continue}if(!Array.isArray(B.content))continue;let X=[];for(let U of B.content){if(!HJ(U)||U.cache_control!==void 0){X.push(U);continue}if(U.type==="text"&&typeof U.text==="string"){let O=H(U.text);if(O)X.push(...O.blocks);else X.push(U);continue}if(U.type==="tool_result"){if(typeof U.content==="string"){let O=H(U.content);if(O)U.content=O.blocks}else if(Array.isArray(U.content)){let O=[];for(let q of U.content){if(HJ(q)&&q.type==="text"&&typeof q.text==="string"&&q.cache_control===void 0){let F=H(q.text);if(F){O.push(...F.blocks);continue}}O.push(q)}U.content=O}}X.push(U)}B.content=X}if($===0)return null;return{body:JSON.stringify(Z),imagedBlocks:$,origChars:V,imageCount:z,savedTokens:Y}}function qZ(J){let Q=(Z)=>{let $=Z.exec(J);return $?Number($[1]):0};return{input:Q(/"input_tokens"\s*:\s*(\d+)/),cacheRead:Q(/"cache_read_input_tokens"\s*:\s*(\d+)/),cacheCreate:Q(/"cache_creation_input_tokens"\s*:\s*(\d+)/)}}function DZ(J){try{let Q=SJ();HZ(KZ(Q),{recursive:!0}),YZ(Q,JSON.stringify(J)+`
61
+ `)}catch{}}function JQ(J){let Q=new XZ(J.upstream),Z=Q.protocol==="https:"?UZ:eJ,$=eJ.createServer((V,z)=>{let Y=[];V.on("data",(G)=>Y.push(G)),V.on("end",()=>{let G=Buffer.concat(Y),H=V.method==="POST"&&(V.url??"").startsWith("/v1/messages")&&!(V.url??"").includes("count_tokens")&&V.headers["content-encoding"]===void 0,K=null;if(H){if(K=OZ(G.toString("utf8"),J),K)G=Buffer.from(K.body,"utf8")}let B={...V.headers};if(delete B.host,delete B.connection,B["content-length"]=String(G.length),H)delete B["accept-encoding"];let X=Z.request({protocol:Q.protocol,hostname:Q.hostname,port:Q.port||(Q.protocol==="https:"?443:80),path:V.url,method:V.method,headers:B},(U)=>{if(z.writeHead(U.statusCode??502,U.headers),H){let O=[];U.on("data",(q)=>{O.push(q),z.write(q)}),U.on("end",()=>{z.end();let q=qZ(Buffer.concat(O).toString("utf8")),F=q.input+q.cacheRead+q.cacheCreate;DZ({ts:Date.now(),tool:"proxy",compressed:K!==null,orig_chars:K?.origChars??0,image_count:K?.imageCount??0,baseline_tokens:F+(K?.savedTokens??0),input_tokens:q.input,cache_read_tokens:q.cacheRead,cache_create_tokens:q.cacheCreate})})}else U.pipe(z)});X.on("error",(U)=>{z.writeHead(502,{"content-type":"application/json"}),z.end(JSON.stringify({type:"error",error:{type:"api_error",message:`tanuki proxy: upstream unreachable (${U.message})`}}))}),X.end(G)})});return $.listen(J.port,"127.0.0.1",()=>{let V=$.address(),z=V!==null&&typeof V==="object"?V.port:J.port,Y=`level=${J.level} distill=${J.distill} codebook=${J.codebook} font=${J.font} minChars=${J.minChars} ratio=${J.ratio} minSave=${J.minSave}`;process.stderr.write(`tanuki-context proxy on http://127.0.0.1:${z} -> ${J.upstream}
62
+ ${Y}
63
+ `+` rules: system prompt & tools untouched · in-place blocks only · latest message never imaged · cache_control skipped · identical blocks imaged once
64
+ `+` point your client at it: export ANTHROPIC_BASE_URL=http://127.0.0.1:${z}
65
+ `)}),$}//! tanuki-context — token-cutting context pipeline.
45
66
  //! pipeline: text -> distill (stage 0, logs) -> ladder level 0-4 (stage 1)
46
67
  //! -> pxpipe imaging (stage 2, name kept from the original mechanic)
47
68
  //!
@@ -49,20 +70,23 @@ class r{value;constructor(J){this.value=J}}function aQ(){let J=process.env.TANUK
49
70
  //! CLI: tanuki-context distill <file> [query]
50
71
  //! tanuki-context estimate <file> [level] [--distill]
51
72
  //! tanuki-context render <file> [level] [outdir]
52
- var QZ="0.1.0",PJ=6;function ZZ(J,Z){let Q=Math.min(J.length,Z.length),$=0;while($<Q&&J.charCodeAt($)===Z.charCodeAt($))$++;if($>=Q)return J.length-Z.length;return J.codePointAt($)-Z.codePointAt($)}function E(J,Z,Q=""){if(J===null||J===void 0)return"null";if(J instanceof r){let q=J.value;return Number.isFinite(q)&&Number.isInteger(q)?q.toFixed(1):String(q)}let $=typeof J;if($==="string")return JSON.stringify(J);if($==="number"||$==="boolean")return String(J);if(Array.isArray(J)){if(J.length===0)return"[]";if(!Z){let U="[";for(let B=0;B<J.length;B++){if(B>0)U+=",";U+=E(J[B],!1)}return U+"]"}let q=Q+" ",H=`[
53
- `;for(let U=0;U<J.length;U++){if(U>0)H+=`,
54
- `;H+=q+E(J[U],!0,q)}return H+`
55
- `+Q+"]"}let V=J,z=Object.keys(V).sort(ZZ);if(z.length===0)return"{}";if(!Z){let q="{";for(let H=0;H<z.length;H++){if(H>0)q+=",";q+=JSON.stringify(z[H])+":"+E(V[z[H]],!1)}return q+"}"}let Y=Q+" ",G=`{
56
- `;for(let q=0;q<z.length;q++){if(q>0)G+=`,
57
- `;G+=Y+JSON.stringify(z[q])+": "+E(V[z[q]],!0,Y)}return G+`
58
- `+Q+"}"}function I(J,Z){return J!==null&&typeof J==="object"&&!Array.isArray(J)?J[Z]:void 0}function y(J){return typeof J==="string"?J:null}function zJ(J){return typeof J==="boolean"?J:null}function aJ(J){return typeof J==="number"&&Number.isSafeInteger(J)&&J>=0?J:null}function k(J){let Z=0;for(let Q=0;Q<J.length;Q++){Z++;let $=J.charCodeAt(Q);if($>=55296&&$<=56319&&Q+1<J.length){let V=J.charCodeAt(Q+1);if(V>=56320&&V<=57343)Q++}}return Z}function qJ(J,Z,Q,$,V){let z,Y=null;if(Q||$!==null){let H=p(J,$,2);z=H.distilled,Y=H.stats}else z=J;let G=0;if(V){let H=SJ(z);z=H.text,G=H.entries}let q=FJ(z,Z);return{stage0:Y,compressed:q.compressed,protectedLines:q.protectedLines,level:q.level,cbEntries:G}}function i(J){return Math.round(J/4)}function a(J,Z){if(J===0)return 0;let Q=(1-Z/J)*100;return Q<0?-Math.round(-Q):Math.round(Q)}function tJ(J){return{text:y(I(J,"text"))??"",level:(aJ(I(J,"level"))??0)%256,distill:zJ(I(J,"distill"))??!1,query:y(I(J,"query")),reflow:zJ(I(J,"reflow"))??!0,pack:zJ(I(J,"pack"))??!0,font:y(I(J,"font"))??"normal",codebook:zJ(I(J,"codebook"))??!1}}function sJ(J){let Z=tJ(J),Q=qJ(Z.text,Z.level,Z.distill,Z.query,Z.codebook),$=$J(Z.font),V=cJ(Q.compressed,Z.reflow,Z.pack,$),z=o(V.pixels),Y=k(Z.text),G=k(Q.compressed),q=i(Y),[H,U]=t[Q.level];return{engine:"pxpipe",level:`${Q.level} ${H}`,loss:U,distill:Q.stage0,origChars:Y,stage1Chars:G,stage1SavedPct:a(Y,G),pages:V.pages,imageTokens:z,rawTextTokens:q,totalSavedPct:a(q,z),protectedLines:Q.protectedLines,pack:Z.pack,font:$==="tiny"?"tiny":"normal",codebook:Z.codebook?Q.cbEntries:!1,verdict:z<q?"PIPELINE cheaper":"TEXT cheaper"}}function $Z(J){let Z=tJ(J),Q=qJ(Z.text,Z.level,Z.distill,Z.query,Z.codebook),$=$J(Z.font),V=VJ(Q.compressed,Z.reflow,Z.pack,$),z=o(V.pixels),Y=k(Z.text),G=k(Q.compressed),q=i(Y),[H,U]=t[Q.level],B="";if(Q.stage0!==null){let F=Q.stage0;B+=`distill: ${F.origLines} -> ${F.outLines} lines (-${F.savedPct}% chars, ${F.collapsedRuns} runs, ${F.suppressedLines} exact + ${F.templateSuppressed} template suppressed, ${F.importantKept} error/warn kept)
59
- `}if(B+=`L${Q.level} ${H} (${U}): ${Y} chars -> ${G} chars (stage1 -${a(Y,G)}%) -> ${V.pages.length} page(s), ~${z} image-tokens
60
- vs ~${q} text-tokens raw = TOTAL -${a(q,z)}%`,Q.protectedLines>0)B+=` · ${Q.protectedLines} lines kept verbatim`;if(V.dropped>0)B+=` · ${V.dropped} unmapped glyphs -> ▯`;if(Q.cbEntries>0)B+=` · codebook: ${Q.cbEntries} sigils (see ·legend·)`;if($==="tiny")B+=" · font: tiny 4x6";if(Z.pack)B+=" · packed (⇥N indent, → tab)";if(Z.reflow)B+=" · ↵ = newline · engine: pxpipe";let O=[{type:"text",text:B}],X=Math.min(V.pages.length,PJ);for(let F=0;F<X;F++){let D=V.pages[F].png;O.push({type:"image",data:Buffer.from(D.buffer,D.byteOffset,D.byteLength).toString("base64"),mimeType:"image/png"})}if(V.pages.length>PJ)O.push({type:"text",text:`(+${V.pages.length-PJ} more page(s))`});return O}function VZ(J){let Z=y(I(J,"text"))??"",Q=p(Z,y(I(J,"query")),2);return[{type:"text",text:E(Q.stats,!0)},{type:"text",text:Q.distilled}]}function zZ(J){let Z=y(I(J,"text"))??"",Q=(aJ(I(J,"level"))??1)%256,$=FJ(Z,Q),[V,z,Y]=t[$.level],G=k(Z),q=k($.compressed),H=i(G),U=i(q),B={level:`${$.level} ${V}`,loss:z,note:Y,origChars:G,outChars:q,approxOrigTokens:H,approxOutTokens:U,savedPct:a(H,U),protectedLines:$.protectedLines};return[{type:"text",text:E(B,!0)},{type:"text",text:$.compressed}]}function GZ(){let J={type:"string"},Z={type:"integer",minimum:0,maximum:4};return{tools:[{name:"tanuki_render",description:"Token-cut pipeline: optional log distillation (dedupe noise, keep errors verbatim, optional query filter), optional codebook (repeated long tokens/path prefixes -> 1-cell sigils + a ·legend· line), then a ladder level, then dense PNG page(s) via the pxpipe imaging engine. level 0 raw · 1 whitespace (lossless) · 2 prose · 3 dense · 4 caveman (gist only). From level 2 up code/IDs/hashes/paths stay verbatim. pack (default true) = lossless tight reflow (single-cell tabs, ⇥N indent runs, width-trimmed pages). font 'tiny' = 4x6 cell, ~40% fewer image-tokens (opt-in). Image tokens are pixel-priced, so every earlier cut compounds. Returns image blocks + a breakdown.",inputSchema:{type:"object",properties:{text:J,level:Z,distill:{type:"boolean"},query:{type:"string"},reflow:{type:"boolean"},pack:{type:"boolean"},font:{type:"string",enum:["normal","tiny"]},codebook:{type:"boolean"}},required:["text"]}},{name:"tanuki_estimate",description:"Estimate tokens for the pipeline (distill -> codebook -> level -> pxpipe imaging) vs sending the raw text as text. Exact page geometry, no image data returned. Compare levels/pack/font/codebook to pick a loss/size tradeoff.",inputSchema:{type:"object",properties:{text:J,level:Z,distill:{type:"boolean"},query:{type:"string"},reflow:{type:"boolean"},pack:{type:"boolean"},font:{type:"string",enum:["normal","tiny"]},codebook:{type:"boolean"}},required:["text"]}},{name:"tanuki_distill",description:"Stage 0 alone: make noisy logs/output small and readable WITHOUT imaging. Strips ANSI, collapses runs of near-identical lines/blocks into '[×N similar]', suppresses global near-dupes (exact + same-template) with exact counts, always keeps error/warn/fail lines verbatim, optional query (regex) returns only the relevant slice. Deterministic, order-preserving.",inputSchema:{type:"object",properties:{text:J,query:{type:"string"}},required:["text"]}},{name:"tanuki_compress",description:"Stage 1 alone: graded text compression for content that stays TEXT. level 0 none · 1 whitespace (lossless, safe for code) · 2 prose · 3 dense · 4 caveman (gist only). From level 2 up code/IDs/hashes/paths are preserved verbatim.",inputSchema:{type:"object",properties:{text:J,level:Z},required:["text"]}},{name:"tanuki_stats",description:"Summarize the pxpipe measurement log (~/.pxpipe/events.jsonl): requests, compression counts, honest input-token savings (input + cache reads + cache creates).",inputSchema:{type:"object",properties:{}}}]}}function YZ(J){let Z=y(I(J,"name"))??"",Q=I(J,"arguments"),$;switch(Z){case"tanuki_render":$=$Z(Q);break;case"tanuki_estimate":$=[{type:"text",text:E(sJ(Q),!0)}];break;case"tanuki_distill":$=VZ(Q);break;case"tanuki_compress":$=zZ(Q);break;case"tanuki_stats":$=[{type:"text",text:E(nJ(),!0)}];break;default:return{ok:!1,error:`unknown tool: ${Z}`}}return{ok:!0,value:{content:$}}}function oJ(J){let Z=J.endsWith("\r")?J.slice(0,-1):J;if(Z.trim().length===0)return;let Q;try{Q=JSON.parse(Z)}catch{return}let $=Q!==null&&typeof Q==="object"&&!Array.isArray(Q)&&Object.prototype.hasOwnProperty.call(Q,"id"),V=$?Q.id??null:null,z=y(I(Q,"method")),Y;switch(z){case"initialize":{let G=y(I(I(Q,"params"),"protocolVersion"))??"2025-06-18";Y={jsonrpc:"2.0",id:V,result:{protocolVersion:G,capabilities:{tools:{}},serverInfo:{name:"tanuki-context",version:QZ}}};break}case"notifications/initialized":case"notifications/cancelled":Y=void 0;break;case"ping":Y={jsonrpc:"2.0",id:V,result:{}};break;case"tools/list":Y={jsonrpc:"2.0",id:V,result:GZ()};break;case"tools/call":{let G=YZ(I(Q,"params"));Y=G.ok?{jsonrpc:"2.0",id:V,result:G.value}:{jsonrpc:"2.0",id:V,error:{code:-32602,message:G.error}};break}default:Y=$?{jsonrpc:"2.0",id:V,error:{code:-32601,message:"method not found"}}:void 0}if(Y!==void 0)C.stdout.write(E(Y,!1)+`
61
- `)}function qZ(){let J=Buffer.alloc(0);C.stdin.on("data",(Z)=>{let Q=J.length>0?Buffer.concat([J,Z]):Z,$=0,V;while((V=Q.indexOf(10,$))!==-1)oJ(Q.toString("utf8",$,V)),$=V+1;J=Q.subarray($)}),C.stdin.on("end",()=>{if(J.length>0)oJ(J.toString("utf8")),J=Buffer.alloc(0)})}function f(J){C.stderr.write(J+`
62
- `),C.exit(101)}function GJ(J){try{return sQ(J,"utf8")}catch{f("read file")}}function YJ(J,Z){if(!/^\+?[0-9]+$/.test(J))return null;let Q=BigInt(J.startsWith("+")?J.slice(1):J);return Q>Z?null:Number(Q)}var rJ=255n,iJ=0xffffffffffffffffn;function HZ(){let J=C.argv.slice(1);switch(J[1]){case"distill":{let Z=J[2]??f("usage: tanuki-context distill <file> [query]"),Q=GJ(Z),$=p(Q,J[3]??null,2);C.stdout.write(E($.stats,!1)+`
63
- `);break}case"estimate":{let Z=J[2]??f("usage: tanuki-context estimate <file> [level] [--distill] [--no-pack] [--font tiny] [--codebook]"),Q=GJ(Z),$=J.slice(3).filter((q)=>!q.startsWith("--")),V=$.length>0?YJ($[0],iJ)??0:0,z=J.indexOf("--font"),Y=z!==-1&&J[z+1]!==void 0?J[z+1]:"normal",G=sJ({text:Q,level:V,distill:J.includes("--distill"),pack:!J.includes("--no-pack"),font:Y,codebook:J.includes("--codebook")});C.stdout.write(E(G,!1)+`
64
- `);break}case"render":{let Z=J[2]??f("usage: tanuki-context render <file> [level] [outdir] [--no-pack] [--font tiny] [--codebook]"),Q=GJ(Z),$=J.slice(3).filter((X)=>!X.startsWith("--")),V=$.length>0?YJ($[0],rJ)??0:0,z=!J.includes("--no-pack"),Y=J.includes("--codebook"),G=J.indexOf("--font"),q=$J(G!==-1&&J[G+1]!==void 0?J[G+1]:"normal"),H=qJ(Q,V,!1,null,Y),U=VJ(H.compressed,!0,z,q),B=o(U.pixels);C.stdout.write(E({pages:U.pages.length,imageTokens:B,dropped:U.dropped,rawTextTokens:i(k(Q))},!1)+`
65
- `);let O=$[1];if(O!==void 0){try{eQ(O,{recursive:!0})}catch{f("mkdir")}for(let X=0;X<U.pages.length;X++)try{JZ(`${O}/page${X}.png`,U.pages[X].png)}catch{f("write png")}}break}case"bench":{let Z=J[2]??f("usage: tanuki-context bench <file> <op> [level] [runs] [--distill]"),Q=J[3]??"pipeline",$=J[4]!==void 0?YJ(J[4],rJ)??0:0,V=J[5]!==void 0?YJ(J[5],iJ)??3:3,z=J.includes("--distill"),Y=GJ(Z),G=[],q=null;for(let H=0;H<=V;H++){let U=performance.now();if(Q==="distill")q=p(Y,null,2).stats;else{let B=qJ(Y,$,z,null,!1),O=VJ(B.compressed,!0,!1,"normal");q={pages:O.pages.length,imageTokens:o(O.pixels),stage1Chars:k(B.compressed),dropped:O.dropped}}if(H>0)G.push(performance.now()-U)}if(G.sort((H,U)=>H-U),G.length===0)f("index out of bounds: the len is 0 but the index is 0");C.stdout.write(E({medianMs:new r(G[Math.floor(G.length/2)]),runs:V,result:q},!1)+`
66
- `);break}case"serve":case void 0:qZ();break;default:C.stderr.write(`unknown command: ${J[1]}
67
- usage: tanuki-context [serve|distill|estimate|render] ...
68
- `),C.exit(1)}}HZ();
73
+ //! tanuki-context proxy [--port N] [--upstream URL] [knobs] (implicit mode)
74
+ var NZ="0.2.0",TJ=6;function jZ(J,Q){let Z=Math.min(J.length,Q.length),$=0;while($<Z&&J.charCodeAt($)===Q.charCodeAt($))$++;if($>=Z)return J.length-Q.length;return J.codePointAt($)-Q.codePointAt($)}function E(J,Q,Z=""){if(J===null||J===void 0)return"null";if(J instanceof e){let H=J.value;return Number.isFinite(H)&&Number.isInteger(H)?H.toFixed(1):String(H)}let $=typeof J;if($==="string")return JSON.stringify(J);if($==="number"||$==="boolean")return String(J);if(Array.isArray(J)){if(J.length===0)return"[]";if(!Q){let B="[";for(let X=0;X<J.length;X++){if(X>0)B+=",";B+=E(J[X],!1)}return B+"]"}let H=Z+" ",K=`[
75
+ `;for(let B=0;B<J.length;B++){if(B>0)K+=`,
76
+ `;K+=H+E(J[B],!0,H)}return K+`
77
+ `+Z+"]"}let V=J,z=Object.keys(V).sort(jZ);if(z.length===0)return"{}";if(!Q){let H="{";for(let K=0;K<z.length;K++){if(K>0)H+=",";H+=JSON.stringify(z[K])+":"+E(V[z[K]],!1)}return H+"}"}let Y=Z+" ",G=`{
78
+ `;for(let H=0;H<z.length;H++){if(H>0)G+=`,
79
+ `;G+=Y+JSON.stringify(z[H])+": "+E(V[z[H]],!0,Y)}return G+`
80
+ `+Z+"}"}function I(J,Q){return J!==null&&typeof J==="object"&&!Array.isArray(J)?J[Q]:void 0}function L(J){return typeof J==="string"?J:null}function KJ(J){return typeof J==="boolean"?J:null}function VQ(J){return typeof J==="number"&&Number.isSafeInteger(J)&&J>=0?J:null}function h(J){let Q=0;for(let Z=0;Z<J.length;Z++){Q++;let $=J.charCodeAt(Z);if($>=55296&&$<=56319&&Z+1<J.length){let V=J.charCodeAt(Z+1);if(V>=56320&&V<=57343)Z++}}return Q}function BJ(J,Q,Z,$,V){let z,Y=null;if(Z||$!==null){let K=b(J,$,2);z=K.distilled,Y=K.stats}else z=J;let G=0;if(V){let K=i(z);z=K.text,G=K.entries}let H=r(z,Q);return{stage0:Y,compressed:H.compressed,protectedLines:H.protectedLines,level:H.level,cbEntries:G}}function JJ(J){return Math.round(J/4)}function QJ(J,Q){if(J===0)return 0;let Z=(1-Q/J)*100;return Z<0?-Math.round(-Z):Math.round(Z)}function zQ(J){return{text:L(I(J,"text"))??"",level:(VQ(I(J,"level"))??0)%256,distill:KJ(I(J,"distill"))??!1,query:L(I(J,"query")),reflow:KJ(I(J,"reflow"))??!0,pack:KJ(I(J,"pack"))??!0,font:L(I(J,"font"))??"normal",codebook:KJ(I(J,"codebook"))??!1}}function IZ(J){let Q=b(J,null,2).distilled,Z=[[!1,J],[!0,Q]],$={distill:!1,codebook:!1,text:J,tokens:1/0,pages:0};for(let[z,Y]of Z)for(let G of[!1,!0]){let H=G?i(Y).text:Y,K=YJ(H,!0,!0,g("normal"));if(K.tokens<$.tokens)$={distill:z,codebook:G,text:H,tokens:K.tokens,pages:K.pages}}let V=YJ($.text,!0,!0,g("tiny"));return{codebook:$.codebook,distill:$.distill,imageTokens:$.tokens,pages:$.pages,tinyImageTokens:V.tokens}}function GQ(J){let Q=zQ(J),Z=BJ(Q.text,Q.level,Q.distill,Q.query,Q.codebook),$=g(Q.font),V=YJ(Z.compressed,Q.reflow,Q.pack,$),z=V.tokens,Y=h(Q.text),G=h(Z.compressed),H=JJ(Y),[K,B]=ZJ[Z.level];return{engine:"pxpipe",level:`${Z.level} ${K}`,loss:B,distill:Z.stage0,origChars:Y,stage1Chars:G,stage1SavedPct:QJ(Y,G),pages:V.pages,imageTokens:z,rawTextTokens:H,totalSavedPct:QJ(H,z),protectedLines:Z.protectedLines,pack:Q.pack,font:$==="tiny"?"tiny":"normal",codebook:Q.codebook?Z.cbEntries:!1,verdict:z<H?"PIPELINE cheaper":"TEXT cheaper",recommend:IZ(Q.text)}}function RZ(J){let Q=zQ(J),Z=BJ(Q.text,Q.level,Q.distill,Q.query,Q.codebook),$=g(Q.font),V=p(Z.compressed,Q.reflow,Q.pack,$),z=V.tokens,Y=h(Q.text),G=h(Z.compressed),H=JJ(Y),[K,B]=ZJ[Z.level],X="";if(Z.stage0!==null){let q=Z.stage0;X+=`distill: ${q.origLines} -> ${q.outLines} lines (-${q.savedPct}% chars, ${q.collapsedRuns} runs, ${q.suppressedLines} exact + ${q.templateSuppressed} template suppressed, ${q.importantKept} error/warn kept)
81
+ `}if(X+=`L${Z.level} ${K} (${B}): ${Y} chars -> ${G} chars (stage1 -${QJ(Y,G)}%) -> ${V.pages.length} page(s), ~${z} image-tokens
82
+ vs ~${H} text-tokens raw = TOTAL -${QJ(H,z)}%`,Z.protectedLines>0)X+=` · ${Z.protectedLines} lines kept verbatim`;if(V.dropped>0)X+=` · ${V.dropped} unmapped glyphs -> ▯`;if(Z.cbEntries>0)X+=` · codebook: ${Z.cbEntries} sigils (see ·legend·)`;if($==="tiny")X+=" · font: tiny 4x6";if(Q.pack)X+=" · packed (⇥N indent, → tab)";if(Q.reflow)X+=" · ↵ = newline · engine: pxpipe";let U=[{type:"text",text:X}],O=Math.min(V.pages.length,TJ);for(let q=0;q<O;q++){let F=V.pages[q].png;U.push({type:"image",data:Buffer.from(F.buffer,F.byteOffset,F.byteLength).toString("base64"),mimeType:"image/png"})}if(V.pages.length>TJ)U.push({type:"text",text:`(+${V.pages.length-TJ} more page(s))`});return U}function AZ(J){let Q=L(I(J,"text"))??"",Z=b(Q,L(I(J,"query")),2);return[{type:"text",text:E(Z.stats,!0)},{type:"text",text:Z.distilled}]}function PZ(J){let Q=L(I(J,"text"))??"",Z=(VQ(I(J,"level"))??1)%256,$=r(Q,Z),[V,z,Y]=ZJ[$.level],G=h(Q),H=h($.compressed),K=JJ(G),B=JJ(H),X={level:`${$.level} ${V}`,loss:z,note:Y,origChars:G,outChars:H,approxOrigTokens:K,approxOutTokens:B,savedPct:QJ(K,B),protectedLines:$.protectedLines};return[{type:"text",text:E(X,!0)},{type:"text",text:$.compressed}]}function _Z(){let J={type:"string"},Q={type:"integer",minimum:0,maximum:4};return{tools:[{name:"tanuki_render",description:"Token-cut pipeline: optional log distillation (dedupe noise, keep errors verbatim, optional query filter), optional codebook (repeated long tokens/path prefixes -> 1-cell sigils + a ·legend· line), then a ladder level, then dense PNG page(s) via the pxpipe imaging engine. level 0 raw · 1 whitespace (lossless) · 2 prose · 3 dense · 4 caveman (gist only). From level 2 up code/IDs/hashes/paths stay verbatim. pack (default true) = lossless tight reflow (single-cell tabs, ⇥N indent runs, width-trimmed pages). font 'tiny' = 4x6 cell, ~40% fewer image-tokens (opt-in). Image tokens are pixel-priced, so every earlier cut compounds. Returns image blocks + a breakdown.",inputSchema:{type:"object",properties:{text:J,level:Q,distill:{type:"boolean"},query:{type:"string"},reflow:{type:"boolean"},pack:{type:"boolean"},font:{type:"string",enum:["normal","tiny"]},codebook:{type:"boolean"}},required:["text"]}},{name:"tanuki_estimate",description:"Estimate tokens for the pipeline (distill -> codebook -> level -> pxpipe imaging) vs sending the raw text as text. Exact page geometry, no image data returned. Compare levels/pack/font/codebook to pick a loss/size tradeoff. The result's 'recommend' field names the cheapest safe knob set (level 0), so one call replaces manual knob probing.",inputSchema:{type:"object",properties:{text:J,level:Q,distill:{type:"boolean"},query:{type:"string"},reflow:{type:"boolean"},pack:{type:"boolean"},font:{type:"string",enum:["normal","tiny"]},codebook:{type:"boolean"}},required:["text"]}},{name:"tanuki_distill",description:"Stage 0 alone: make noisy logs/output small and readable WITHOUT imaging. Strips ANSI, collapses runs of near-identical lines/blocks into '[×N similar]', suppresses global near-dupes (exact + same-template) with exact counts, always keeps error/warn/fail lines verbatim, optional query (regex) returns only the relevant slice. Deterministic, order-preserving.",inputSchema:{type:"object",properties:{text:J,query:{type:"string"}},required:["text"]}},{name:"tanuki_compress",description:"Stage 1 alone: graded text compression for content that stays TEXT. level 0 none · 1 whitespace (lossless, safe for code) · 2 prose · 3 dense · 4 caveman (gist only). From level 2 up code/IDs/hashes/paths are preserved verbatim.",inputSchema:{type:"object",properties:{text:J,level:Q},required:["text"]}},{name:"tanuki_stats",description:"Summarize the pxpipe measurement log (~/.pxpipe/events.jsonl): requests, compression counts, honest input-token savings (input + cache reads + cache creates).",inputSchema:{type:"object",properties:{}}}]}}function SZ(J){let Q=L(I(J,"name"))??"",Z=I(J,"arguments"),$;switch(Q){case"tanuki_render":$=RZ(Z);break;case"tanuki_estimate":$=[{type:"text",text:E(GQ(Z),!0)}];break;case"tanuki_distill":$=AZ(Z);break;case"tanuki_compress":$=PZ(Z);break;case"tanuki_stats":$=[{type:"text",text:E(sJ(),!0)}];break;default:return{ok:!1,error:`unknown tool: ${Q}`}}return{ok:!0,value:{content:$}}}function QQ(J){let Q=J.endsWith("\r")?J.slice(0,-1):J;if(Q.trim().length===0)return;let Z;try{Z=JSON.parse(Q)}catch{return}let $=Z!==null&&typeof Z==="object"&&!Array.isArray(Z)&&Object.prototype.hasOwnProperty.call(Z,"id"),V=$?Z.id??null:null,z=L(I(Z,"method")),Y;switch(z){case"initialize":{let G=L(I(I(Z,"params"),"protocolVersion"))??"2025-06-18";Y={jsonrpc:"2.0",id:V,result:{protocolVersion:G,capabilities:{tools:{}},serverInfo:{name:"tanuki-context",version:NZ}}};break}case"notifications/initialized":case"notifications/cancelled":Y=void 0;break;case"ping":Y={jsonrpc:"2.0",id:V,result:{}};break;case"tools/list":Y={jsonrpc:"2.0",id:V,result:_Z()};break;case"tools/call":{let G=SZ(I(Z,"params"));Y=G.ok?{jsonrpc:"2.0",id:V,result:G.value}:{jsonrpc:"2.0",id:V,error:{code:-32602,message:G.error}};break}default:Y=$?{jsonrpc:"2.0",id:V,error:{code:-32601,message:"method not found"}}:void 0}if(Y!==void 0)S.stdout.write(E(Y,!1)+`
83
+ `)}function EZ(){let J=Buffer.alloc(0);S.stdin.on("data",(Q)=>{let Z=J.length>0?Buffer.concat([J,Q]):Q,$=0,V;while((V=Z.indexOf(10,$))!==-1)QQ(Z.toString("utf8",$,V)),$=V+1;J=Z.subarray($)}),S.stdin.on("end",()=>{if(J.length>0)QQ(J.toString("utf8")),J=Buffer.alloc(0)})}function k(J){S.stderr.write(J+`
84
+ `),S.exit(101)}function UJ(J){try{return FZ(J,"utf8")}catch{k("read file")}}function XJ(J,Q){if(!/^\+?[0-9]+$/.test(J))return null;let Z=BigInt(J.startsWith("+")?J.slice(1):J);return Z>Q?null:Number(Z)}var ZQ=255n,$Q=0xffffffffffffffffn;function YQ(){let J=S.argv.slice(1);switch(J[1]){case"distill":{let Q=J[2]??k("usage: tanuki-context distill <file> [query]"),Z=UJ(Q),$=b(Z,J[3]??null,2);S.stdout.write(E($.stats,!1)+`
85
+ `);break}case"estimate":{let Q=J[2]??k("usage: tanuki-context estimate <file> [level] [--distill] [--no-pack] [--font tiny] [--codebook]"),Z=UJ(Q),$=J.slice(3).filter((H)=>!H.startsWith("--")),V=$.length>0?XJ($[0],$Q)??0:0,z=J.indexOf("--font"),Y=z!==-1&&J[z+1]!==void 0?J[z+1]:"normal",G=GQ({text:Z,level:V,distill:J.includes("--distill"),pack:!J.includes("--no-pack"),font:Y,codebook:J.includes("--codebook")});S.stdout.write(E(G,!1)+`
86
+ `);break}case"render":{let Q=J[2]??k("usage: tanuki-context render <file> [level] [outdir] [--no-pack] [--font tiny] [--codebook]"),Z=UJ(Q),$=J.slice(3).filter((O)=>!O.startsWith("--")),V=$.length>0?XJ($[0],ZQ)??0:0,z=!J.includes("--no-pack"),Y=J.includes("--codebook"),G=J.indexOf("--font"),H=g(G!==-1&&J[G+1]!==void 0?J[G+1]:"normal"),K=BJ(Z,V,!1,null,Y),B=p(K.compressed,!0,z,H),X=B.tokens;S.stdout.write(E({pages:B.pages.length,imageTokens:X,dropped:B.dropped,rawTextTokens:JJ(h(Z))},!1)+`
87
+ `);let U=$[1];if(U!==void 0){try{MZ(U,{recursive:!0})}catch{k("mkdir")}for(let O=0;O<B.pages.length;O++)try{WZ(`${U}/page${O}.png`,B.pages[O].png)}catch{k("write png")}}break}case"bench":{let Q=J[2]??k("usage: tanuki-context bench <file> <op> [level] [runs] [--distill]"),Z=J[3]??"pipeline",$=J[4]!==void 0?XJ(J[4],ZQ)??0:0,V=J[5]!==void 0?XJ(J[5],$Q)??3:3,z=J.includes("--distill"),Y=UJ(Q),G=[],H=null;for(let K=0;K<=V;K++){let B=performance.now();if(Z==="distill")H=b(Y,null,2).stats;else{let X=BJ(Y,$,z,null,!1),U=p(X.compressed,!0,!1,"normal");H={pages:U.pages.length,imageTokens:U.tokens,stage1Chars:h(X.compressed),dropped:U.dropped}}if(K>0)G.push(performance.now()-B)}if(G.sort((K,B)=>K-B),G.length===0)k("index out of bounds: the len is 0 but the index is 0");S.stdout.write(E({medianMs:new e(G[Math.floor(G.length/2)]),runs:V,result:H},!1)+`
88
+ `);break}case"proxy":{let Q=(V,z)=>{let Y=J.indexOf(V);if(Y===-1||J[Y+1]===void 0)return z;let G=Number(J[Y+1]);return Number.isFinite(G)?G:z},Z=J.indexOf("--upstream"),$=J.indexOf("--font");JQ({port:Q("--port",8484),upstream:Z!==-1&&J[Z+1]!==void 0?J[Z+1]:S.env.TANUKI_UPSTREAM??"https://api.anthropic.com",level:Q("--level",c.level),distill:J.includes("--distill"),codebook:J.includes("--codebook"),font:g($!==-1&&J[$+1]!==void 0?J[$+1]:"normal"),minChars:Q("--min-chars",c.minChars),ratio:Q("--ratio",c.ratio),minSave:Q("--min-save",c.minSave),maxPages:Q("--max-pages",c.maxPages)});break}case"serve":case void 0:EZ();break;default:S.stderr.write(`unknown command: ${J[1]}
89
+ usage: tanuki-context [serve|proxy|distill|estimate|render] ...
90
+ `),S.exit(1)}}//! Bin entry: keeps src/main.ts importable as a library (src/agent.ts) without
91
+ //! starting the MCP server as an import side effect.
92
+ YQ();
package/dist/pi.js ADDED
@@ -0,0 +1,15 @@
1
+ import{spawn as W}from"node:child_process";import{fileURLToPath as X}from"node:url";import{Type as q}from"typebox";//! Pi (pi-mono / oh-my-pi lineage) extension: registers the five tanuki tools.
2
+ //!
3
+ //! Engine-agnostic by design: the extension is a thin stdio JSON-RPC client to
4
+ //! a `tanuki-context` MCP server it spawns itself, so the same file serves
5
+ //! - the npm/TS engine (default: `node <this package>/dist/cli.js`), and
6
+ //! - the Rust engine (`TANUKI_BIN=/path/to/tanuki-context`).
7
+ //! One child per pi session, spawned lazily on first tool call (pi docs forbid
8
+ //! starting processes in the factory), killed on session_shutdown.
9
+ //!
10
+ //! MCP tool-result content blocks ({type:"text"|"image"}) are pi's own
11
+ //! ToolResult content shape, so results pass through untouched.
12
+ function Y(){let F=process.env.TANUKI_BIN;if(F)return{cmd:F,args:[]};return{cmd:process.execPath,args:[X(new URL("./cli.js",import.meta.url))]}}class Q{proc;buf="";nextId=1;pending=new Map;ready;constructor(){let{cmd:F,args:B}=Y();this.proc=W(F,B,{stdio:["pipe","pipe","inherit"]}),this.proc.stdout.setEncoding("utf8"),this.proc.stdout.on("data",(G)=>{this.buf+=G;let z;while((z=this.buf.indexOf(`
13
+ `))!==-1){let H=this.buf.slice(0,z).trim();if(this.buf=this.buf.slice(z+1),!H)continue;let D;try{D=JSON.parse(H)}catch{continue}let J=D.id!==void 0?this.pending.get(D.id):void 0;if(!J)continue;if(this.pending.delete(D.id),D.error)J.reject(Error(D.error.message??"tanuki-context error"));else J.resolve(D.result)}}),this.proc.on("exit",(G)=>{let z=Error(`tanuki-context server exited (code ${G})`);for(let H of this.pending.values())H.reject(z);this.pending.clear()}),this.ready=this.request("initialize",{})}request(F,B){let G=this.nextId++,{promise:z,resolve:H,reject:D}=Promise.withResolvers();if(!this.proc.stdin?.writable)return D(Error("tanuki-context server is gone")),z;return this.pending.set(G,{resolve:H,reject:D}),this.proc.stdin.write(JSON.stringify({jsonrpc:"2.0",id:G,method:F,params:B})+`
14
+ `),z}async call(F,B){return await this.ready,this.request("tools/call",{name:F,arguments:B})}kill(){this.proc.kill()}get alive(){return this.proc.exitCode===null&&!this.proc.killed}}var K=q.String({description:"The text to process"}),V=q.Optional(q.Integer({minimum:0,maximum:4,description:"0 raw · 1 whitespace · 2 prose · 3 dense · 4 caveman"})),N=q.Object({text:K,level:V,distill:q.Optional(q.Boolean()),query:q.Optional(q.String()),reflow:q.Optional(q.Boolean()),pack:q.Optional(q.Boolean()),font:q.Optional(q.String({enum:["normal","tiny"]})),codebook:q.Optional(q.Boolean())}),Z=[{name:"tanuki_render",label:"Tanuki Render",description:"Token-cut pipeline: optional log distillation (dedupe noise, keep errors verbatim, optional query filter), optional codebook (repeated long tokens/path prefixes -> 1-cell sigils + a ·legend· line), then a ladder level, then dense PNG page(s) via the pxpipe imaging engine. level 0 raw · 1 whitespace (lossless) · 2 prose · 3 dense · 4 caveman (gist only). From level 2 up code/IDs/hashes/paths stay verbatim. pack (default true) = lossless tight reflow. font 'tiny' = 4x6 cell, ~40% fewer image-tokens (opt-in). Image tokens are pixel-priced, so every earlier cut compounds. Returns image blocks + a breakdown.",parameters:N,snippet:"Render bulky text/logs as dense PNG pages that cost a fraction of the tokens"},{name:"tanuki_estimate",label:"Tanuki Estimate",description:"Estimate tokens for the pipeline (distill -> codebook -> level -> pxpipe imaging) vs sending the raw text as text. Exact page geometry, no image data returned. Compare levels/pack/font/codebook to pick a loss/size tradeoff. The result's 'recommend' field names the cheapest safe knob set (level 0), so one call replaces manual knob probing.",parameters:N,snippet:"Instant token verdict: would imaging this text beat sending it as text?"},{name:"tanuki_distill",label:"Tanuki Distill",description:"Stage 0 alone: make noisy logs/output small and readable WITHOUT imaging. Strips ANSI, collapses runs of near-identical lines/blocks into '[×N similar]', suppresses global near-dupes with exact counts, always keeps error/warn/fail lines verbatim, optional query (regex) returns only the relevant slice. Deterministic, order-preserving.",parameters:q.Object({text:K,query:q.Optional(q.String())}),snippet:"Deterministically dedupe noisy logs, keeping every error line verbatim"},{name:"tanuki_compress",label:"Tanuki Compress",description:"Stage 1 alone: graded text compression for content that stays TEXT. level 0 none · 1 whitespace (lossless, safe for code) · 2 prose · 3 dense · 4 caveman (gist only). From level 2 up code/IDs/hashes/paths are preserved verbatim.",parameters:q.Object({text:K,level:V}),snippet:"Graded text compression (lossless whitespace up to gist-only)"},{name:"tanuki_stats",label:"Tanuki Stats",description:"Summarize the pxpipe measurement log (~/.pxpipe/events.jsonl): requests, compression counts, honest input-token savings (input + cache reads + cache creates).",parameters:q.Object({}),snippet:"Session savings summary from the tanuki/pxpipe event log"}];function $(F){let B=null,G=()=>{if(!B||!B.alive)B=new Q;return B};F.on("session_shutdown",async()=>{B?.kill(),B=null});for(let z of Z)F.registerTool({name:z.name,label:z.label,description:z.description,promptSnippet:z.snippet,parameters:z.parameters,async execute(H,D){let J=await G().call(z.name,D??{}),M=(J.content??[]).map((I)=>I.type==="image"?{type:"image",data:I.data??"",mimeType:I.mimeType??"image/png"}:{type:"text",text:I.text??""});if(J.isError)throw Error(M.map((I)=>("text"in I)?I.text:"").join(`
15
+ `)||z.name+" failed");return{content:M,details:{}}}})}export{$ as default};
package/package.json CHANGED
@@ -1,12 +1,19 @@
1
1
  {
2
2
  "name": "tanuki-context",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
4
  "description": "Token-cutting context pipeline as an MCP server: distill logs, compress text, render dense PNG pages (pxpipe imaging engine). Zero dependencies.",
5
5
  "license": "MIT",
6
6
  "type": "module",
7
7
  "bin": {
8
8
  "tanuki-context": "dist/cli.js"
9
9
  },
10
+ "exports": {
11
+ "./agent": {
12
+ "types": "./dist/agent.d.ts",
13
+ "default": "./dist/agent.js"
14
+ },
15
+ "./package.json": "./package.json"
16
+ },
10
17
  "files": [
11
18
  "dist",
12
19
  "assets",
@@ -14,9 +21,32 @@
14
21
  "LICENSE"
15
22
  ],
16
23
  "scripts": {
17
- "build": "bun build src/main.ts --target=node --minify --outfile=dist/cli.js",
24
+ "build": "bun build src/cli.ts --target=node --minify --outfile=dist/cli.js && bun build src/agent.ts --target=node --minify --external @anthropic-ai/claude-agent-sdk --external zod --outfile=dist/agent.js && bun build src/pi.ts --target=node --minify --external typebox --outfile=dist/pi.js && cp types/agent.d.ts dist/agent.d.ts",
18
25
  "prepublishOnly": "bun run build",
19
- "parity": "bun reference/parity-ts.mjs"
26
+ "parity": "bun reference/parity-ts.mjs",
27
+ "test": "bun test"
28
+ },
29
+ "peerDependencies": {
30
+ "@anthropic-ai/claude-agent-sdk": ">=0.3.0",
31
+ "zod": "^3.25.0 || ^4.0.0"
32
+ },
33
+ "peerDependenciesMeta": {
34
+ "@anthropic-ai/claude-agent-sdk": {
35
+ "optional": true
36
+ },
37
+ "zod": {
38
+ "optional": true
39
+ }
40
+ },
41
+ "devDependencies": {
42
+ "@anthropic-ai/claude-agent-sdk": "^0.3.220",
43
+ "typebox": "^1.0.0",
44
+ "zod": "^4.0.0"
45
+ },
46
+ "pi": {
47
+ "extensions": [
48
+ "./dist/pi.js"
49
+ ]
20
50
  },
21
51
  "engines": {
22
52
  "node": ">=18"
@@ -31,6 +61,7 @@
31
61
  "context",
32
62
  "compression",
33
63
  "tokens",
34
- "llm"
64
+ "llm",
65
+ "pxpipe"
35
66
  ]
36
67
  }