context-doctor 0.25.0 → 0.27.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
@@ -2,18 +2,27 @@
2
2
 
3
3
  [![CI](https://github.com/KushalP1/context-doctor/actions/workflows/ci.yml/badge.svg)](https://github.com/KushalP1/context-doctor/actions) [![npm](https://img.shields.io/npm/v/context-doctor)](https://www.npmjs.com/package/context-doctor) [![npm downloads](https://img.shields.io/npm/dm/context-doctor)](https://www.npmjs.com/package/context-doctor) [![license: MIT](https://img.shields.io/badge/license-MIT-blue)](./LICENSE) ![macOS | Linux | Windows](https://img.shields.io/badge/platform-macOS%20%7C%20Linux%20%7C%20Windows-lightgrey)
4
4
 
5
- **Keep every AI session's context lean, and see exactly what it costs you, without ever making it more expensive.**
5
+ **Cut your Claude Code bill, measured on your own sessions.**
6
+
7
+ ```bash
8
+ npx context-doctor savings
9
+ ```
10
+
11
+ One command shows, in dollars, what your recent sessions cost and what each lever below would have saved on them. Then the levers do it: an every-prompt hook, an auto-compact window, and a proxy that never makes a request more expensive.
6
12
 
7
13
  Long agent sessions fill up with tool output nobody reads again, and a 500k-token session re-reads all of it on every message. The most expensive moment is coming back after lunch: the prompt cache has expired, so the next message re-sends everything at full price. `context-doctor` measures this on your own sessions and acts on it at the moments that pay.
8
14
 
9
15
  - **Tells you when to `/compact`, at the moment it pays.** When you come back to a large session after the cache expired, the every-prompt hook gives the model the numbers and it offers `/compact` once. Replayed over 133 days of the author's Claude Code history: 401 such returns, and compacting then would have saved **$4,053 net at list price, about $914 a month**. It works in every Claude Code surface, including the desktop app. [What it saves →](#what-it-saves)
10
16
  - **Compacts earlier, automatically, in every Claude Code surface.** Claude Code auto-compacts near the full window (measured: ~970k tokens on a 1M model), so long sessions re-read 400k-900k tokens on every message. `context-doctor compact-window` replays your history at smaller windows and, if you choose, sets Claude Code's own `autoCompactWindow`. On the author's last 30 days: **a 400k window would have cut input cost 55%** ($6,119 to $2,757), for 15 compactions a week instead of 5. A native setting, so it works in the desktop app too. [The trade-off →](#3-compacting-earlier-automatic)
11
17
  - **Autopilot for terminal and IDE sessions and API apps:** a local proxy that clears stale tool output only when the prompt cache is cold, so it never costs more. **9.8% less input cost, no session made more expensive**, in the same replay. (The desktop app's Code tab sets its own API address, so autopilot cannot reach it; `doctor` tells you if that is your case.)
18
+ - **Keeps waste out in the first place.** `context-doctor pack` (and the `pack_context` MCP tool) puts only the chunks of a big document, log or codebase that a question needs into the context, within a token budget: 9 of 10 questions about this repo answered from 0.6% of its tokens. `context-doctor overhead` measures what every request re-reads before your message (system prompt, tools, CLAUDE.md, rules, memory) and prices each memory file per month. [How →](#before-it-enters-the-context-pack-and-overhead)
12
19
  - **Counts Claude correctly.** Current Claude models pack 2.75 characters per token, not the 4 most tools assume, so estimates built on 4 undercount Claude by about 40%. The ratios were measured from the API's own counts; `context-doctor accuracy` re-checks them on yours.
13
- - **Works where you work:** Claude Code (terminal, IDE, desktop app, plugin), Cursor, Codex, Claude Desktop chat, any Anthropic or OpenAI API app, VS Code, CI. macOS, Linux and Windows, Node 20+. Local, keyless, no telemetry, MIT.
20
+ - **Works where you work:** Claude Code (terminal, IDE, desktop app, plugin), Cursor, Codex, Gemini CLI, Claude Desktop chat, any Anthropic or OpenAI API app, VS Code, CI. macOS, Linux and Windows, Node 20+. Local, keyless, no telemetry, MIT.
14
21
 
15
22
  Built and maintained by [gAI Ventures](https://gai.ventures).
16
23
 
24
+ > **Which context-doctor is this?** Several projects share the name. This one is [`context-doctor` on npm](https://www.npmjs.com/package/context-doctor) and `io.github.KushalP1/context-doctor` in the [official MCP Registry](https://registry.modelcontextprotocol.io): the one you run as `npx context-doctor`, and the one that prices what it saves on your own history.
25
+
17
26
  ## Quick start
18
27
 
19
28
  **See what it would save you first** (no install, reads your local Claude Code history):
@@ -24,7 +33,7 @@ npx context-doctor savings
24
33
 
25
34
  <img src="https://raw.githubusercontent.com/KushalP1/context-doctor/main/assets/savings.svg" alt="npx context-doctor savings: input billed, what /compact at cold resumes, autopilot and an earlier auto-compact window would have saved" width="640">
26
35
 
27
- That is the author's machine: every session there runs in the desktop app, so the first line is the one that applies. Yours is computed the same way, from your own transcripts: every session replayed request by request through the shipped code, against what you were actually billed. `context-doctor savings --share` prints a few lines with totals only (no project names or paths) if you want to post your number.
36
+ That is the author's machine: every session there runs in the desktop app, which autopilot cannot reach, so lines 1 and 3 are the ones that apply. Yours is computed the same way, from your own transcripts: every session replayed request by request through the shipped code, against what you were actually billed. `context-doctor savings --share` prints a few lines with totals only (no project names or paths) if you want to post your number.
28
37
 
29
38
  **Then install it**, whichever way suits you:
30
39
 
@@ -105,17 +114,60 @@ On a Claude subscription you do not pay list price; the same tokens come out of
105
114
  |---|---|---|
106
115
  | **Claude Code** in the terminal, VS Code or JetBrains | **Autopilot** clears stale tool output (cold cache only, never more expensive). **Hook** on every prompt: real context size, largest waste, and a `/compact` offer when you return to a large session after the cache expired. **`compact-window`**, if you set one |
107
116
  | **Claude Code** in the desktop app's Code tab | **Hook**, as above, including the cold-resume `/compact` offer, shown to you as well as the model. **`compact-window`**, if you set one: Claude Code compacts earlier by itself. Not autopilot: the app sets its own API address and ignores the one in settings | Install via npm or as a **plugin** (`/plugin marketplace add KushalP1/context-doctor`). Status bar context meter, `/context-doctor:savings`, `session`, `watch`, `report`, dashboard |
108
- | **Cursor** (agent) | Cursor runs Claude Code's hooks, so the same every-prompt check fires inside Cursor | MCP tools, editor status bar extension, `cursor` profiler. With your own OpenAI key, autopilot too via a tokened tunnel ([how](#putting-the-proxy-on-a-public-url-cursor-with-your-own-openai-key-remote-apps)) |
117
+ | **Cursor** (agent) | A native **after-tool-call hook** (`~/.cursor/hooks.json`): in a heavy chat the agent gets the same hygiene guidance and the largest waste, once per 40% of growth. (Cursor's prompt hook can only allow or block a prompt, so this is the event where guidance reaches the model) | MCP tools, editor status bar extension, `cursor` profiler. With your own OpenAI key, autopilot too via a tokened tunnel ([how](#putting-the-proxy-on-a-public-url-cursor-with-your-own-openai-key-remote-apps)) |
109
118
  | **Codex** (ChatGPT app's Codex tab, IDE extension, CLI) | Every-prompt hook with the API's own token counts | MCP tools, skill, `session` reads Codex rollouts. On an API key, autopilot too (`OPENAI_BASE_URL`) |
110
- | **Your own apps on the Anthropic or OpenAI API** | Autopilot on `/v1/messages`, `/v1/chat/completions` and `/v1/responses` (`ANTHROPIC_BASE_URL` / `OPENAI_BASE_URL`), or the full optimizing proxy | Exact usage and cache hit rates in `/stats`, prompt-cache placement advice |
111
- | **Claude Desktop chat** | Standing context rules in every chat; one cheap `profile_context` call the model makes past ~30 turns or on any cost question | One-click `.mcpb` install, `context_checkup` prompt |
119
+ | **Gemini CLI** | Every-prompt hook (`BeforeAgent`) with the API's own token counts. On an API key, autopilot too: `GOOGLE_GEMINI_BASE_URL=http://127.0.0.1:8787` (Google sign-in traffic goes elsewhere) | MCP tools, `session` and `watch` read its chats, `overhead` sizes GEMINI.md |
120
+ | **Your own apps on the Anthropic, OpenAI or Gemini API** | Autopilot on `/v1/messages`, `/v1/chat/completions`, `/v1/responses` and Gemini's `generateContent` (`ANTHROPIC_BASE_URL` / `OPENAI_BASE_URL` / `GOOGLE_GEMINI_BASE_URL`), or the full optimizing proxy; no proxy possible (serverless, edge)? `withContextDoctor(client)` runs autopilot inside the SDK client | Exact usage and cache hit rates in `/stats`, prompt-cache placement advice |
121
+ | **Claude Desktop chat** | Standing context rules in every chat; one cheap `profile_context` call the model makes past ~30 turns or on any cost question; `pack_context` instead of reading a big local file whole | One-click `.mcpb` install, `context_checkup` prompt |
112
122
  | **claude.ai, ChatGPT, the phone apps** | Your account's standing preferences (`context-doctor instructions --copy`) | Profile an exported chat with `analyze` |
113
123
  | **CI** | `analyze --fail-over-budget` fails a build whose prompts outgrow a budget | `.contextdoctorrc` budgets and presets |
114
124
 
115
125
  Not claimed, because no process on your machine sends those requests: trimming inside Claude Desktop chat, claude.ai, ChatGPT, Cursor's own subscription models, or Codex signed in with ChatGPT. Those get the rules and the measurements above, not autopilot.
116
126
 
127
+ ## Before it enters the context: pack and overhead
128
+
129
+ Everything above slims context that is already there. Two commands stop it getting in.
130
+
131
+ **`pack`: only the parts of a document a question needs.** A 60k-token manual pasted to answer one question is paid on that message and re-read on every message after it. `pack` splits files and folders along their own structure (markdown headings, top-level code declarations, paragraphs; one-line logs and minified files are cut too), ranks the chunks against your question with BM25, and returns the best ones that fit a budget. Every chunk carries an id and its line range, and the result names the next-best ids, so asking for more is one call. Without a question you get an outline to pick from.
132
+
133
+ ```bash
134
+ context-doctor pack docs/ README.md --query "how do I rotate the signing key" --max-tokens 3000
135
+ context-doctor pack big-manual.md # outline: id, lines, tokens, heading per chunk
136
+ context-doctor pack big-manual.md --ids big-manual.md#12,big-manual.md#13
137
+ cat server.log | context-doctor pack - -q "timeout upstream"
138
+ ```
139
+
140
+ ```
141
+ $ context-doctor pack README.md src -q "how is the ledger kept from growing" --max-tokens 2000
142
+ PACKED ~1.9k of ~315k tokens (1%) from 80 files: 7 of 1026 chunks for "how is the ledger kept from growing", budget 2.0k.
143
+
144
+ ── src/ledger.ts#6 · L108-128 · export function recordLedger(entry: Omit<LedgerEntry, "ts">): void { · ~404
145
+ ...
146
+ ── src/test/ledger.test.ts#1 · L1-22 · The ledger is capped, and everything it feeds is a LIFETIME total. · ~409
147
+ ...
148
+ Not included: 1019 chunk(s). Next best: src/watch.ts#1 (...); src/test/compactwindow.test.ts#4 (...)
149
+ ```
150
+
151
+ In Claude Desktop, Claude Code, Cursor and Codex the same thing is the `pack_context` MCP tool, and the standing instructions tell the model to use it when it needs to answer from a large file it is not editing. Ranking is lexical, offline and keyless; it misses pure paraphrase, which is what the outline is for. If you run [Ollama](https://ollama.com) with an embedding model (`ollama pull nomic-embed-text`), `--semantic` (or `semantic: true` on the MCP tool) also ranks by meaning and merges the two rankings; chunk vectors are cached, so repeat packs are fast. Without one it says so and stays lexical. Measured on this repository (README plus source, ~305k tokens): 9 of 10 factual questions were answered from a 2k-token pack. PDFs, Word (`.docx`, `.doc`, `.rtf`), PowerPoint and ODT files are read with tools the OS already has (`pdftotext` where installed, PDFKit and `textutil` on macOS, `unzip` elsewhere); when none is available, pack says what to install. Over HTTP (the ChatGPT connector mode) the tool reads no files, only text you pass.
152
+
153
+ **`overhead`: what every request re-reads before your message.** The system prompt, tool and MCP schemas, skills and your memory files ride on every request: cached, so each read is cheap, but read on every request of every session and written again at full price after each cold start. Transcripts never show them. `overhead` measures the fixed part from the first request of each recent Claude Code session (the API's own count, minus your first message), lists every memory file each agent loads in the current directory (Claude Code's CLAUDE.md with its `@imports`, rules and auto memory; Codex's AGENTS.md; Cursor's always-applied rules; GEMINI.md), and prices each per month from your own request and cold-start counts.
154
+
155
+ ```
156
+ Claude Code, last 30 days: 71 sessions start at a median ~54k tokens (p90 ~62k) before the first message.
157
+ Memory files loaded here are ~3.4k of that; the rest is Claude Code's system prompt, tools, skills and MCP schemas.
158
+ At your usage (14,104 requests, 249 cold starts, claude-fable-5-1 list prices) every 1k tokens of it costs ~$16.97 a month;
159
+ ```
160
+
161
+ Findings point at files over 2k tokens, paragraphs loaded twice from two files, and long code blocks that could live in a file the agent opens when it needs them.
162
+
163
+ To act on a finding, `context-doctor overhead split <file>` plans the fix: sections over ~400 tokens (or mostly code) move word for word to `<name>.reference.md`, each run of them leaving one pointer the agent follows when a task needs it. Lists of links are indexes and stay, and an auto-memory `MEMORY.md` keeps only its index, as Claude Code intends. Nothing is written until you add `--write`, which backs up the original first. On a copy of the author's auto-memory index: ~3.4k to ~1.7k tokens on every request, about $28 a month, nothing lost.
164
+
165
+ MCP servers are part of the same overhead: each one's tool definitions ride on every request. `overhead` counts calls per server from your transcripts, including servers the app or its connectors provide, and `overhead --mcp` launches each configured server once to size its definitions, flagging the ones you never call with what they cost a month. When Claude Code's tool search is on (the report says how often it was used), definitions are deferred and an idle server costs about its tool names instead.
166
+
117
167
  ## What's new
118
168
 
169
+ - **0.27 Keep waste out**: `pack` and the `pack_context` MCP tool put only the chunks of big files (text, code, PDF, Word, PowerPoint) a question needs into the context, within a budget; `overhead` measures and prices the front matter every request re-reads (system prompt, tools, CLAUDE.md, rules, memory, AGENTS.md, Cursor rules, GEMINI.md). MCP servers sized and their use counted; `overhead split` turns a heavy memory file into a lean one plus a reference file. `withContextDoctor(client)` runs autopilot inside an Anthropic or OpenAI SDK client, for apps with no proxy. Gemini joins: `install` wires Gemini CLI's MCP server and every-prompt hook, `session` and `watch` read its chats, and the proxy and autopilot carry Google's Gemini API. A GitHub Action (and `context-doctor ci`) reports memory-file growth on every pull request and gates on a budget. Re-verified on every surface: 229 tests on macOS, Linux and Windows, plus MCP, OpenAI and Gemini end-to-end smoke runs, now in CI, MCP over stdio and HTTP, the OpenAI proxy, and a live `doctor`.
170
+ - **0.26 Checked on every platform, three fixes**: tested end to end in Claude Code (terminal, desktop app, plugin from GitHub), Claude Desktop, Cursor, Codex on GPT-5.5, VS Code, the MCP server on every launch path, and the proxy on OpenAI's APIs. Found and fixed: Cursor's agent never received the hook's guidance (Cursor runs Claude Code's hook where output cannot add context), so `install` now adds a native Cursor hook; Codex blocked the MCP tools behind an approval it never grants in `codex exec`, so the tools are now marked read-only; autopilot skipped Codex's newest tools (`exec`, `wait`) and some of Cursor's. Also: a history imported into Codex no longer reads as "2136% of the window", and the chat-app settings paths are current.
119
171
  - **0.25 Compact earlier, by itself**: the `/compact` offer at cold resumes was followed 1 time in 33 on the author's machine, so two changes. The hook now also shows you the notice (it went only to the model, which rarely raised it), with the dollar cost of the message you just sent. And `context-doctor compact-window` measures and sets Claude Code's own auto-compact window, which works without anyone acting on advice, in every surface including the desktop app: 55% less input cost at 400k on the author's last 30 days. `savings` shows it as a third lever.
120
172
  - **0.20 Autopilot**: stale tool output cleared from every Claude Code request, only when the prompt cache is cold, so it cannot cost more (measured: 9.8% less input cost, ~$1,080 a month on the author's usage, no session worse); runs as a login service on macOS, Linux and Windows; now also for GPT via OpenAI's Chat Completions and Responses APIs.
121
173
  - **0.24 A full audit, fixed**: the session parser now rebuilds API messages from Claude Code's one-row-per-block transcripts (it had split each reply into several messages, breaking tool pairing in exported conversations and inflating profiles); the proxy forwards every non-conversation endpoint and never re-encodes bodies it does not rewrite (model listing and file uploads failed or were corrupted through it); `report` and the dashboard no longer count compaction as tokens context-doctor saved, and MCP sketches no longer inflate them; `watch` follows plain conversation files; MCP inputs are bounded. 0.23: four dependency vulnerabilities patched, no false `/compact` alarm after compacting, `savings --share`.
@@ -143,9 +195,10 @@ One run of `context-doctor install` writes these (each config edit makes a `.bac
143
195
  2. **Claude Code config** (`~/.claude.json`) — registers the MCP server
144
196
  3. **Cursor config** (`~/.cursor/mcp.json`) — registers the MCP server
145
197
  4. **Agent Skill** → `~/.claude/skills/context-doctor/` — context-hygiene playbook for Claude Code
146
- 5. **Every-prompt hook** → `~/.claude/settings.json` — the per-query context check for Claude Code (Cursor runs it too)
147
- 6. **Codex**, when present: MCP server in `~/.codex/config.toml`, the hook in `~/.codex/hooks.json`, the skill in `~/.codex/skills/`
148
- 7. With `--statusline`: live context size, cache share and cost in Claude Code's status bar
198
+ 5. **Every-prompt hook** → `~/.claude/settings.json` — the per-query context check for Claude Code
199
+ 6. **Cursor hook**, when Cursor is present → `~/.cursor/hooks.json` (`postToolUse`) — the same check for Cursor's agent, after each tool call
200
+ 7. **Codex**, when present: MCP server in `~/.codex/config.toml`, the hook in `~/.codex/hooks.json`, the skill in `~/.codex/skills/`
201
+ 8. With `--statusline`: live context size, cache share and cost in Claude Code's status bar
149
202
 
150
203
  `context-doctor autopilot on` is separate and opt-in: it adds the background proxy service and one line (`env.ANTHROPIC_BASE_URL`) to `~/.claude/settings.json`, after the proxy has answered a health check.
151
204
 
@@ -189,6 +242,10 @@ Practical upshot: a developer who only wants cheaper, faster API calls never tou
189
242
  | `context-doctor mcp [--http]` | The MCP server as a subcommand, for clients and registries that launch `npx -y context-doctor mcp` |
190
243
  | `context-doctor autopilot on\|off\|pause\|resume\|status` | Every new Claude Code session goes through the local proxy, which clears stale tool output only when the prompt cache is cold: measured 9.8% less input cost, no session worse |
191
244
  | `context-doctor instructions [--copy]` | The ~180-token standing rules (~120 on GPT) for claude.ai / ChatGPT preferences, for web and phones where no server runs |
245
+ | `context-doctor pack <files\|dirs\|-> --query "…"` | Only the chunks of big documents, logs or code a question needs, ranked and fit to `--max-tokens` (default 4000). No query: an outline; `--ids a#2,b#5` picks chunks from it. Offline, no key |
246
+ | `context-doctor overhead [--days n] [--mcp]` | What every request re-reads before your message: measured first-request size, each memory file (CLAUDE.md and imports, rules, auto memory, AGENTS.md, Cursor rules, GEMINI.md) priced per month, MCP calls per server; `--mcp` sizes each configured server's tool definitions and flags unused ones |
247
+ | `context-doctor overhead split <file> [--write]` | Move a memory file's big or code-heavy sections word for word to `<name>.reference.md`, leaving pointers; indexes stay. A plan until `--write`, which backs up first |
248
+ | `context-doctor ci [--base <ref>] [--max-tokens n] [--max-increase n]` | Memory files (CLAUDE.md, AGENTS.md, GEMINI.md, rules, their imports) at HEAD vs a base commit; exit 1 over budget. What the GitHub Action runs |
192
249
  | `context-doctor analyze <file>` | Profile a conversation: token breakdown, findings, cost + latency estimates. `--fail-over-budget` exits 1 on a breach, for CI |
193
250
  | `context-doctor optimize <file>` | Apply the safe fixes; add `--strategy trim-tool-calls` for big inline file writes, `--strategy prune-history` for consented lossy compaction |
194
251
  | `context-doctor session [file]` | Profile a Claude Code session: live context, findings, **measured tokens and prompt-cache economics**, **where the wall clock went** per tool, and **what its subagents cost** (their own windows, your bill; never in the parent's profile). Also reads ChatGPT data exports (`conversations.json`) |
@@ -329,15 +386,15 @@ Your API key still rides in the request headers, as before. The token protects t
329
386
  | Surface | Runs by itself | What that means |
330
387
  |---|---|---|
331
388
  | **Claude Code** | Yes: hook on every prompt, status line on every refresh | Past ~80k tokens the model receives hygiene guidance naming the largest waste; compaction is offered. Measured: 115 automatic checks, 48 warnings, across 32 sessions on one machine |
332
- | **Cursor** | **Yes**, since 0.15: Cursor loads Claude Code's hook config (`~/.claude/settings.json`) and runs the same hook on every agent prompt, passing its own transcript. Output is accepted through Cursor's Claude-compat layer | Same guidance as Claude Code, inside Cursor's agent, for everyone who ran `install`. Before 0.15 the hook fired but could not read Cursor's transcript format, so it said nothing |
389
+ | **Cursor** | **Yes**, since 0.26: `install` adds a native `postToolUse` hook to `~/.cursor/hooks.json`; Cursor passes it the agent transcript, and its `additional_context` reaches the model | In a heavy chat, the agent gets hygiene guidance and the largest recoverable waste, once per 40% of growth. Cursor's transcript records tool calls but not their output, and keeps turns Cursor has since summarized, so the note gives the chat's accumulated size, with no window share or price. Corrected in 0.26: Cursor also runs Claude Code's hook, but as `beforeSubmitPrompt`, whose output can only allow or block the prompt, so from 0.15 to 0.25 the guidance never reached Cursor's model |
333
390
  | **API traffic through the proxy** | Yes: every request rewritten in flight | Fewer tokens, guaranteed, model not consulted |
334
391
  | **Claude Desktop** | The standing instruction in every chat (we confirmed in the app bundle that Desktop's `LocalMcpServerManager` reads it), a one-click `context_checkup` prompt, and since 0.17 a `profile_context` the model can actually afford to call from chat | Until 0.17 the tool wanted the whole conversation as its argument, so calling it from chat meant re-typing 50k tokens; nobody did, and Desktop's log showed zero calls in a month. Now the model passes a ~120-token **sketch** (turn count, the large or repeated blocks) and gets a sized estimate, findings and the fix to apply. Still a nudge, not a hook: Desktop chat has no hook API and no transcript on disk |
335
- | **Codex (OpenAI): ChatGPT.app's Codex tab, the Codex IDE extension, the `codex` CLI** | **Yes**, since 0.16: `install` writes the hook to `~/.codex/hooks.json`, the MCP server to `~/.codex/config.toml`, and the skill to `~/.codex/skills/`. Codex uses Claude Code's hook contract almost verbatim and passes its own rollout transcript, which carries the API's real usage figures | Same guidance as Claude Code, from measured tokens. One extra step, Codex's rule not ours: a new hook runs only after you trust it once (type `/hooks` in Codex). `session` and `session --list` read Codex rollouts too |
392
+ | **Codex (OpenAI): ChatGPT.app's Codex tab, the Codex IDE extension, the `codex` CLI** | **Yes**, since 0.16: `install` writes the hook to `~/.codex/hooks.json`, the MCP server to `~/.codex/config.toml`, and the skill to `~/.codex/skills/`. Codex uses Claude Code's hook contract almost verbatim and passes its own rollout transcript, which carries the API's real usage figures | Same guidance as Claude Code, from measured tokens. One extra step, Codex's rule not ours: a new hook runs only after you trust it once (type `/hooks` in Codex). The MCP tools are marked read-only, so Codex runs them without an approval prompt, `codex exec` included. `session` and `session --list` read Codex rollouts too |
336
393
  | **ChatGPT chat UI** | No | No MCP, no hooks, no data path in the chat product itself. Use Codex, or a developer-mode connector at a URL you host |
337
394
 
338
395
  So "every chat inherently better" is true for Claude Code, Cursor, Codex and the proxy; for Claude Desktop it is "the rules ride in every chat and the checkup is one cheap tool call away"; and not a claim we make for the ChatGPT chat UI.
339
396
 
340
- **Where there is no hook and no MCP at all** (claude.ai on the web, the Claude and ChatGPT phone apps, plain ChatGPT): the app's per-account preferences are read on every turn, which is the closest those surfaces have to a hook. `context-doctor instructions --copy` puts the ~180-token rules on your clipboard and tells you where to paste them (claude.ai Settings > Profile; ChatGPT Settings > Personalization > Custom instructions).
397
+ **Where there is no hook and no MCP at all** (claude.ai on the web, the Claude and ChatGPT phone apps, plain ChatGPT): the app's per-account preferences are read on every turn, which is the closest those surfaces have to a hook. `context-doctor instructions --copy` puts the ~180-token rules on your clipboard and tells you where to paste them (claude.ai Settings > General; ChatGPT Settings > Personalization > Custom instructions, "What traits should ChatGPT have?").
341
398
 
342
399
  **Do you need to configure anything by hand? Usually no:**
343
400
 
@@ -345,7 +402,7 @@ So "every chat inherently better" is true for Claude Code, Cursor, Codex and the
345
402
  |---|---|
346
403
  | Claude Desktop | `npx context-doctor install` writes the config — just restart the app. Or one click: download `context-doctor-<version>.mcpb` from the [latest release](https://github.com/KushalP1/context-doctor/releases) and open it (Settings > Extensions). The bundle runs on Desktop's own Node, no npm needed |
347
404
  | Claude Code | Same command — MCP + skill + every-prompt hook, all automatic |
348
- | Cursor | Same command — writes `~/.cursor/mcp.json`; the every-prompt hook is picked up from Claude Code's config, which Cursor reads |
405
+ | Cursor | Same command — writes `~/.cursor/mcp.json` and the after-tool-call hook in `~/.cursor/hooks.json`. `context-doctor doctor` checks both |
349
406
  | Codex (OpenAI) | Same command — `~/.codex/config.toml`, `~/.codex/hooks.json`, `~/.codex/skills/`. Then, once, `/hooks` in Codex to trust the hook |
350
407
  | ChatGPT (developer mode) | **Manual + a reachable URL** — ChatGPT connects to servers over the internet, never local commands. Run `context-doctor-mcp --http` on a host/tunnel, then add the URL as a connector. Normal ChatGPT (no dev mode) has no MCP — use the CLI |
351
408
 
@@ -369,7 +426,7 @@ For any other MCP client, the server entry is:
369
426
  3. That call is cheap on purpose. Claude cannot export a Desktop chat, so it passes a **sketch**: how many turns, which blocks are large, repeated, stale or images, with one size hint each (~120 tokens). The server sizes it (usually within ±20%, measured; see "Why token counts are ~"), prices the per-turn re-read (on a subscription that is what spends your usage limit), and returns ranked findings with the action for each: "summarize *the nginx config* into the points still needed", "refer to *test output* by name", "offer a 300-token handoff summary for a fresh chat". The reply ends with an instruction to apply the top one, not just suggest it.
370
427
  4. One click instead of asking: the `context_checkup` prompt in the **+** menu sends that request for you.
371
428
  5. Say *"optimize it"* on an exported conversation and Claude applies the safe fixes; if you agree to pruning old history, **Claude itself writes the replacement summary** (that's the no-API-key summarization).
372
- 6. For the same rules on your phone and on claude.ai, where no MCP server runs: `context-doctor instructions --copy`, then paste into Settings > Profile > personal preferences.
429
+ 6. For the same rules on your phone and on claude.ai, where no MCP server runs: `context-doctor instructions --copy`, then paste into Settings > General > personal preferences.
373
430
 
374
431
  What this does not do: read the chat behind Claude's back or trim it for you. Desktop chat has no hook API and no transcript on disk (checked in the app bundle, v2.2553). The model does the trimming, when the rules and the checkup tell it to.
375
432
 
@@ -379,13 +436,13 @@ ChatGPT's MCP support differs fundamentally from Claude Desktop's: **it never sp
379
436
 
380
437
  1. **Normal ChatGPT (no developer mode): no MCP at all.** context-doctor still helps via the CLI: export the conversation and run `npx context-doctor analyze chat.json --model gpt-5` / `optimize` — no account settings required.
381
438
  2. **ChatGPT developer mode**: run our HTTP transport somewhere reachable — `context-doctor-mcp --http --port 8808` on a small host (bind `--host 0.0.0.0` there), or expose your machine temporarily with a tunnel (`ngrok http 8808`). Then Settings → Connectors → Advanced → Developer mode → add connector with URL `https://<your-host>/mcp`.
382
- 3. Once connected, GPT gets the same three tools with the same trigger guidance: ask *"what's eating my context?"* → it calls `profile_context`; *"optimize it"* works the same, including GPT writing the pruning summary itself.
439
+ 3. Once connected, GPT gets the same three tools, each marked read-only (they only read their input and return text), with the same trigger guidance: ask *"what's eating my context?"* → it calls `profile_context`; *"optimize it"* works the same, including GPT writing the pruning summary itself.
383
440
 
384
441
  Security note for step 2: the HTTP endpoint is unauthenticated — put it behind your tunnel's auth or a reverse proxy if it stays up long-term.
385
442
 
386
443
  ### claude.ai on the web and the phone apps
387
444
 
388
- Your local MCP server can't reach the website, but the behavior can. Two options: `context-doctor instructions --copy` and paste into Settings > Profile > personal preferences (applies everywhere you are signed in, phone included), or upload `skills/context-doctor/SKILL.md` under Settings → Capabilities → Skills. Either way web and mobile conversations gain the same standing habits: summarize, don't re-quote, offer a handoff when heavy.
445
+ Your local MCP server can't reach the website, but the behavior can. Two options: `context-doctor instructions --copy` and paste into Settings > General > personal preferences (applies everywhere you are signed in, phone included), or upload `skills/context-doctor/SKILL.md` under Settings → Capabilities → Skills. Either way web and mobile conversations gain the same standing habits: summarize, don't re-quote, offer a handoff when heavy.
389
446
 
390
447
  ### MCP tools
391
448
 
@@ -393,6 +450,7 @@ Your local MCP server can't reach the website, but the behavior can. Two options
393
450
  |---|---|
394
451
  | `profile_context` | Token breakdown by category, largest messages, findings with estimated savings. Takes either `conversation` (full JSON or text) or `sketch` (turns + large/repeated blocks, for chat apps) |
395
452
  | `optimize_context` | Rewrites the conversation: dedupe, trim stale tool results, strip base64, optional history pruning |
453
+ | `pack_context` | Only the chunks of large local files or folders (or passed `text`) that a question needs, within `max_tokens`; outline without a query; `ids` to fetch more. Reads no files over HTTP |
396
454
  | `context_best_practices` | Curated checklist, optionally specialized for Anthropic / OpenAI |
397
455
 
398
456
  ## Use it as a library
@@ -406,8 +464,24 @@ console.log(profile.totalTokens, profile.findings);
406
464
  const { conversation, tokensBefore, tokensAfter } = optimizeConversation(chatJson, {
407
465
  strategies: ["dedupe", "trim-tool-results", "strip-base64"],
408
466
  });
467
+
468
+ // Autopilot without a proxy: wrap the SDK client (Anthropic or OpenAI)
469
+ import Anthropic from "@anthropic-ai/sdk";
470
+ import { withContextDoctor, contextDoctorStats } from "context-doctor";
471
+ const client = withContextDoctor(new Anthropic()); // use exactly as before
472
+ await client.messages.create({ model, max_tokens, messages });
473
+ console.log(contextDoctorStats(client)); // { requests, changed, tokensRemoved }
474
+
475
+ // RAG-style context packing: chunk, rank, fit a budget (no embeddings, no key)
476
+ import { packContext, renderPack } from "context-doctor";
477
+ const packed = packContext([{ name: "manual.md", text: manual }], { query: "rotate the signing key", budget: 3000 });
478
+ console.log(packed.selected.map((c) => `${c.id} L${c.startLine}-${c.endLine}`), renderPack(packed));
409
479
  ```
410
480
 
481
+ ### Autopilot inside your app: `withContextDoctor`
482
+
483
+ For apps that cannot send traffic through a local proxy (serverless and edge functions, managed hosts), wrap the SDK client. `messages.create` / `stream`, `chat.completions.create` / `stream` / `parse` and `responses.create` / `stream` / `parse` (beta namespaces too) go through the same autopilot as the proxy: stale tool output is cleared only when the prompt cache is cold. Each request is cloned before it changes, so the history your app holds is never rewritten; only the copy on the wire is slimmer. State lives in the process: one long-lived process per conversation stream behaves like the proxy, and a conversation the process has not seen counts as warm, so a fresh serverless instance never clears blind (pass `statePath` on shared storage if many instances serve the same conversations). Tested with the real `@anthropic-ai/sdk` and `openai` packages.
484
+
411
485
  ## Subagents: their own windows, your bill
412
486
 
413
487
  A subagent has its own context window, so its tokens are correctly absent from the parent's profile. They are not absent from the bill. Claude Code writes each one to `<session>/subagents/agent-<id>.jsonl`, and `session` now reads them:
@@ -454,7 +528,7 @@ ctx 801k/1.0M ▮▮▮▮▮▮▮▮░░ 80% ⚠ · cache 100% · $12.34
454
528
 
455
529
  Live context against the model's window, a warning mark from 70%, the share served from cache, and the session's cost. It reads the size from the status payload when Claude Code provides it, and otherwise from the last 256KB of the transcript (about 1ms on a 20MB file; 80ms end to end including Node startup). It is opt-in and polite: there is only one status line, so it never overwrites one you already have, and `uninstall` removes only its own. Any failure prints nothing rather than an error.
456
530
 
457
- This is the "editor status bar" roadmap item, delivered for the editor most users of this tool are actually in. A VS Code / Cursor extension for the same number remains future work.
531
+ The same number in VS Code and Cursor's status bar: see [the editor extension](#the-same-number-inside-vs-code-and-cursor).
458
532
 
459
533
  ## Does a smaller context actually help? Measure it
460
534
 
@@ -571,6 +645,29 @@ A cache read bills at ~10% of input while a write bills at ~125%, so a session t
571
645
 
572
646
  ## Enforce a budget in CI
573
647
 
648
+ **Memory files on every pull request.** CLAUDE.md, AGENTS.md, GEMINI.md and rules are re-read by every request of everyone working in the repository, so their growth deserves review. The repository is also a GitHub Action:
649
+
650
+ ```yaml
651
+ # .github/workflows/context.yml
652
+ on: pull_request
653
+ permissions:
654
+ contents: read
655
+ pull-requests: write # for the comment
656
+ jobs:
657
+ memory:
658
+ runs-on: ubuntu-latest
659
+ steps:
660
+ - uses: actions/checkout@v4
661
+ - uses: KushalP1/context-doctor@v0.27.0
662
+ with:
663
+ max-increase: "2000" # fail a PR that adds more than 2k tokens
664
+ requests-per-day: "5000" # optional: price the change
665
+ ```
666
+
667
+ It compares the memory files and their `@imports` at the PR head with the base branch, writes the table to the job summary, keeps one PR comment up to date, and fails over budget. Locally: `context-doctor ci --base origin/main`.
668
+
669
+ **Conversation fixtures.**
670
+
574
671
  ```bash
575
672
  npx context-doctor analyze conversation.json --fail-over-budget
576
673
  ```
@@ -626,7 +723,7 @@ One honest caveat worth knowing: a transcript stores the conversation, **not** t
626
723
 
627
724
  ## Roadmap
628
725
 
629
- See [ROADMAP.md](./ROADMAP.md) for the full plan with rationale. Headlines: **v0.5** trust & automation (tag-based publishing, `doctor` self-check, live `watch`), **v0.6** accuracy (exact tokenizers, semantic dedupe, more session formats), **v0.7** proxy pro (response accounting, prompt-cache advisor), **v1.0** budgets + local dashboard. Non-goals, permanently: cloud services, telemetry, silent history rewriting, mandatory API keys.
726
+ See [ROADMAP.md](./ROADMAP.md) for the full plan, the per-surface coverage table and the measurements behind every shipped item. Next, in order of expected saving: the MCP schema tax per server in `overhead`; a Python package for RAG pipelines. Non-goals, permanently: cloud services, telemetry, silent history rewriting, mandatory API keys.
630
727
 
631
728
  Contributions welcome — this project is small on purpose. Open an issue before a big PR.
632
729
 
@@ -663,6 +760,18 @@ Also keep the MCP server version in `src/mcp.ts` in sync with `package.json`, an
663
760
 
664
761
  Issues and PRs welcome — see [CONTRIBUTING.md](./CONTRIBUTING.md) for the six rules that keep this tool trustworthy (no API keys, nothing leaves the machine, no silent data loss, measurements not guesses, the hot path stays cheap, tests with every change) and a list of good first issues. What is planned next lives in [ROADMAP.md](./ROADMAP.md).
665
762
 
763
+ Two end-to-end checks beyond `npm test`, both run against a throwaway `HOME`:
764
+
765
+ ```bash
766
+ HOME=$(mktemp -d) node scripts/smoke-mcp.mjs node dist/mcp.js
767
+ ```
768
+
769
+ ```bash
770
+ HOME=$(mktemp -d) node scripts/smoke-openai-proxy.mjs
771
+ ```
772
+
773
+ The first connects to the MCP server the way a client does (it takes any launch command, `npx -y context-doctor mcp` included, or `--http <url>`), lists the tools and prompt, calls every tool and checks that bad input is rejected. The second runs the proxy against a local mock of OpenAI: streaming, the Responses API, usage capture and autopilot.
774
+
666
775
  ## License
667
776
 
668
777
  MIT © [gAI Ventures](https://gai.ventures)
@@ -53,6 +53,13 @@ export interface AutoClearOptions {
53
53
  paybackSafety?: number;
54
54
  /** Where cleared ids persist so a restart does not un-clear (and re-bill) them. */
55
55
  statePath?: string;
56
+ /**
57
+ * Treat a conversation this instance has never seen as warm-cached (so
58
+ * nothing is cleared on it). The proxy sees every request and leaves this
59
+ * off; an in-process wrapper in a fresh process cannot know whether another
60
+ * process just sent the conversation, so it turns this on.
61
+ */
62
+ unseenIsWarm?: boolean;
56
63
  }
57
64
  export interface AutoClearResult {
58
65
  changed: boolean;
@@ -81,7 +88,7 @@ interface Found {
81
88
  * as Codex with an API key sends).
82
89
  */
83
90
  interface View {
84
- format: "anthropic" | "openai-chat" | "openai-responses";
91
+ format: "anthropic" | "openai-chat" | "openai-responses" | "gemini";
85
92
  units: Array<Record<string, unknown>>;
86
93
  toolName: Map<string, string>;
87
94
  results: Array<Omit<Found, "tokens">>;
@@ -112,7 +119,7 @@ export declare class AutoClearer {
112
119
  * Rewrite `body` in place: an Anthropic Messages, OpenAI Chat Completions or
113
120
  * OpenAI Responses request. Never throws; anything unrecognised is left alone.
114
121
  */
115
- apply(body: Record<string, unknown>, now?: number): AutoClearResult;
122
+ apply(body: Record<string, unknown>, now?: number, modelHint?: string): AutoClearResult;
116
123
  /**
117
124
  * Warm cache: clearing rewrites everything after the first cleared result
118
125
  * at the write rate (1.25x instead of 0.1x: 1.15x extra, once), and saves
package/dist/autoclear.js CHANGED
@@ -45,10 +45,13 @@ import { estimateTokens } from "./tokens.js";
45
45
  export const CLEARABLE_TOOLS = new Set([
46
46
  // Claude Code
47
47
  "Read", "Bash", "BashOutput", "Grep", "Glob", "LS", "WebFetch", "WebSearch", "NotebookRead",
48
- // Codex and other OpenAI-API agents
49
- "shell", "exec_command", "local_shell", "read_file", "list_dir", "grep_search", "file_search", "web_search",
48
+ // Codex and other OpenAI-API agents. Codex's code mode runs every tool from
49
+ // one `exec` cell and polls long commands with `wait` (as Bash/BashOutput).
50
+ "shell", "exec_command", "local_shell", "read_file", "list_dir", "grep_search", "file_search", "web_search", "exec", "wait",
51
+ // Gemini CLI
52
+ "read_many_files", "run_shell_command", "glob", "search_file_content", "list_directory", "web_fetch", "google_web_search",
50
53
  // Cursor's agent
51
- "Shell", "ReadFile", "rg", "run_terminal_cmd", "codebase_search",
54
+ "Shell", "ReadFile", "rg", "run_terminal_cmd", "codebase_search", "SemanticSearch", "ReadLints", "AwaitShell",
52
55
  ]);
53
56
  /** Tokens billed for one image inside a tool result (a full-size screenshot is ~1.6k on Claude). */
54
57
  const IMAGE_TOKENS = 1600;
@@ -83,6 +86,9 @@ function resultTokens(content, model) {
83
86
  * the request sets prompt_cache_retention "24h".
84
87
  */
85
88
  export function requestTtlMs(body) {
89
+ // Gemini: implicit caching's lifetime is not published, explicit caches default to an hour.
90
+ if (Array.isArray(body.contents))
91
+ return HOUR;
86
92
  const openai = Array.isArray(body.input) || (Array.isArray(body.messages) && isOpenAIChat(body.messages));
87
93
  if (openai)
88
94
  return body.prompt_cache_retention === "24h" ? 24 * HOUR : HOUR;
@@ -96,6 +102,37 @@ export function viewOf(body) {
96
102
  const toolName = new Map();
97
103
  const results = [];
98
104
  let firstToolId = "";
105
+ // Gemini generateContent: contents[].parts[] with functionCall / functionResponse.
106
+ // Ids are optional there, so a missing one is derived from its position
107
+ // (stable: history only grows) and scoped to the conversation, because the
108
+ // cleared set is keyed by id and positions repeat across conversations.
109
+ if (Array.isArray(body.contents)) {
110
+ const units = body.contents;
111
+ const firstUser = units.find((u) => u?.role === "user")?.parts;
112
+ const scope = createHash("sha1").update(JSON.stringify(firstUser ?? "").slice(0, 4000)).digest("hex").slice(0, 10);
113
+ units.forEach((u, i) => {
114
+ if (!Array.isArray(u?.parts))
115
+ return;
116
+ const parts = u.parts;
117
+ parts.forEach((p, j) => {
118
+ const call = p?.functionCall;
119
+ const resp = p?.functionResponse;
120
+ if (call && typeof call === "object") {
121
+ const id = typeof call.id === "string" && call.id ? call.id : `gemini:${scope}:${i}:${j}`;
122
+ toolName.set(id, String(call.name ?? ""));
123
+ firstToolId ||= id;
124
+ }
125
+ else if (resp && typeof resp === "object") {
126
+ const id = typeof resp.id === "string" && resp.id ? resp.id : `gemini:${scope}:${i}:${j}`;
127
+ // The response names its tool, so it classifies on its own.
128
+ if (!toolName.has(id))
129
+ toolName.set(id, String(resp.name ?? ""));
130
+ results.push({ unit: i, id, content: resp.response, set: (note) => { parts[j] = { ...p, functionResponse: { ...resp, response: { output: note } } }; } });
131
+ }
132
+ });
133
+ });
134
+ return { format: "gemini", units, toolName, results, firstUser, firstToolId, ttlMs: requestTtlMs(body) };
135
+ }
99
136
  if (Array.isArray(body.input)) {
100
137
  const units = body.input;
101
138
  units.forEach((it, i) => {
@@ -170,6 +207,7 @@ export class AutoClearer {
170
207
  keepRecent: options.keepRecent ?? 3,
171
208
  minClearTokens: options.minClearTokens ?? 20_000,
172
209
  paybackSafety: options.paybackSafety ?? 0,
210
+ unseenIsWarm: options.unseenIsWarm ?? false,
173
211
  statePath: options.statePath,
174
212
  };
175
213
  this.load();
@@ -178,16 +216,17 @@ export class AutoClearer {
178
216
  * Rewrite `body` in place: an Anthropic Messages, OpenAI Chat Completions or
179
217
  * OpenAI Responses request. Never throws; anything unrecognised is left alone.
180
218
  */
181
- apply(body, now = Date.now()) {
219
+ apply(body, now = Date.now(), modelHint) {
182
220
  const none = (reason, cold = false) => ({ changed: false, newlyCleared: 0, tokensRemoved: 0, cold, reason, firstChanged: -1 });
183
221
  try {
184
222
  const view = viewOf(body);
185
223
  if (!view)
186
224
  return none("no messages");
187
- const model = typeof body.model === "string" ? body.model : undefined;
225
+ // Gemini puts the model in the URL, not the body; the proxy passes it.
226
+ const model = typeof body.model === "string" ? body.model : modelHint;
188
227
  const key = conversationKey(view);
189
228
  const conv = this.convs.get(key);
190
- const cold = !conv || now - conv.last > view.ttlMs;
229
+ const cold = conv ? now - conv.last > view.ttlMs : !this.opts.unseenIsWarm;
191
230
  this.convs.set(key, { last: now, requests: (conv?.requests ?? 0) + 1 });
192
231
  if (this.convs.size > 2000)
193
232
  this.convs.delete(this.convs.keys().next().value);
package/dist/ci.d.ts ADDED
@@ -0,0 +1,39 @@
1
+ /**
2
+ * `context-doctor ci`: the memory-file check for pull requests.
3
+ *
4
+ * CLAUDE.md, AGENTS.md, GEMINI.md and always-applied rules are re-read on
5
+ * every request of every session by everyone who works in the repository, so
6
+ * a PR that grows them by 2k tokens is a standing cost nobody reviews. This
7
+ * compares those files (and what they @import) at HEAD against a base commit,
8
+ * prints a table, and exits 1 over a budget, so CI can comment and gate.
9
+ *
10
+ * Base contents come from `git show <base>:<path>`; nothing is checked out.
11
+ */
12
+ export interface CiFile {
13
+ path: string;
14
+ agent: string;
15
+ base: number;
16
+ head: number;
17
+ /** Loaded through an @import rather than directly. */
18
+ imported?: boolean;
19
+ }
20
+ export interface CiReport {
21
+ base?: string;
22
+ files: CiFile[];
23
+ baseTotal: number;
24
+ headTotal: number;
25
+ breaches: string[];
26
+ }
27
+ export declare function ciReport(opts?: {
28
+ cwd?: string;
29
+ base?: string;
30
+ maxTokens?: number;
31
+ maxIncrease?: number;
32
+ }): CiReport;
33
+ /** Markdown for a PR comment or a job summary. */
34
+ export declare function renderCiMarkdown(r: CiReport, opts?: {
35
+ model?: string;
36
+ requestsPerDay?: number;
37
+ }): string;
38
+ /** Plain-text rendering for a terminal. */
39
+ export declare function renderCiText(r: CiReport): string;
package/dist/ci.js ADDED
@@ -0,0 +1,144 @@
1
+ /**
2
+ * `context-doctor ci`: the memory-file check for pull requests.
3
+ *
4
+ * CLAUDE.md, AGENTS.md, GEMINI.md and always-applied rules are re-read on
5
+ * every request of every session by everyone who works in the repository, so
6
+ * a PR that grows them by 2k tokens is a standing cost nobody reviews. This
7
+ * compares those files (and what they @import) at HEAD against a base commit,
8
+ * prints a table, and exits 1 over a budget, so CI can comment and gate.
9
+ *
10
+ * Base contents come from `git show <base>:<path>`; nothing is checked out.
11
+ */
12
+ import { execFileSync } from "node:child_process";
13
+ import { existsSync, readFileSync } from "node:fs";
14
+ import { join, posix } from "node:path";
15
+ import { estimateTokens, formatTokens } from "./tokens.js";
16
+ import { formatUsd, pricingFor } from "./pricing.js";
17
+ const CLAUDE = "claude-opus-5";
18
+ /** Repo-relative paths (posix) that agents load on every request. */
19
+ const MEMORY_PATTERNS = [
20
+ [/(^|\/)CLAUDE(\.local)?\.md$/, "Claude Code"],
21
+ [/(^|\/)\.claude\/CLAUDE\.md$/, "Claude Code"],
22
+ [/(^|\/)\.claude\/rules\/.+\.md$/, "Claude Code"],
23
+ [/(^|\/)AGENTS(\.override)?\.md$/, "Codex"],
24
+ [/(^|\/)GEMINI\.md$/, "Gemini CLI"],
25
+ [/(^|\/)\.cursorrules$/, "Cursor"],
26
+ [/(^|\/)\.cursor\/rules\/.+\.mdc?$/, "Cursor"],
27
+ ];
28
+ function git(args, cwd) {
29
+ try {
30
+ return execFileSync("git", args, { cwd, encoding: "utf8", stdio: ["ignore", "pipe", "ignore"], maxBuffer: 64 * 1024 * 1024 });
31
+ }
32
+ catch {
33
+ return undefined;
34
+ }
35
+ }
36
+ function agentFor(path) {
37
+ return MEMORY_PATTERNS.find(([re]) => re.test(path))?.[1];
38
+ }
39
+ /** Cursor rules count only when always applied; the rest load on demand. */
40
+ function counts(path, text) {
41
+ return !/\.mdc?$/.test(path) || !path.includes(".cursor/rules/") || /^alwaysApply:\s*true/m.test(text);
42
+ }
43
+ /** `@path` imports (Claude Code), resolved relative to the importing file, repo-relative. */
44
+ function importsOf(text, from) {
45
+ const out = [];
46
+ let fence = false;
47
+ for (const line of text.split("\n")) {
48
+ if (/^\s*(```|~~~)/.test(line))
49
+ fence = !fence;
50
+ if (fence)
51
+ continue;
52
+ for (const m of line.replace(/`[^`]*`/g, "").matchAll(/(?:^|\s)@((?:\.{0,2}\/|[\w.-]+\/)[^\s)]+|[\w.-]+\.\w+)/g)) {
53
+ if (m[1].startsWith("/") || m[1].startsWith("~"))
54
+ continue; // outside the repo
55
+ out.push(posix.normalize(posix.join(posix.dirname(from), m[1])));
56
+ }
57
+ }
58
+ return out.filter((p) => !p.startsWith(".."));
59
+ }
60
+ /** Token totals per memory file, with a reader for one side (HEAD from disk, base from git). */
61
+ function measure(paths, read) {
62
+ const out = new Map();
63
+ const visit = (p, imported, depth) => {
64
+ if (out.has(p))
65
+ return;
66
+ const text = read(p);
67
+ if (text === undefined || !counts(p, text))
68
+ return;
69
+ out.set(p, { tokens: estimateTokens(text, CLAUDE), imported });
70
+ // Only Claude Code resolves @imports; an imported file can import further.
71
+ if (depth < 4 && (imported || agentFor(p) === "Claude Code"))
72
+ for (const i of importsOf(text, p))
73
+ visit(i, true, depth + 1);
74
+ };
75
+ for (const p of paths)
76
+ visit(p, false, 0);
77
+ return out;
78
+ }
79
+ export function ciReport(opts = {}) {
80
+ const cwd = opts.cwd ?? process.cwd();
81
+ const root = git(["rev-parse", "--show-toplevel"], cwd)?.trim() ?? cwd;
82
+ const headList = (git(["ls-files", "--cached", "--others", "--exclude-standard"], root) ?? "").split("\n").filter(Boolean);
83
+ const baseList = opts.base ? (git(["ls-tree", "-r", "--name-only", opts.base], root) ?? "").split("\n").filter(Boolean) : [];
84
+ const head = measure(headList.filter((p) => agentFor(p)), (p) => (existsSync(join(root, p)) ? readFileSync(join(root, p), "utf8") : undefined));
85
+ const base = opts.base ? measure(baseList.filter((p) => agentFor(p)), (p) => git(["show", `${opts.base}:${p}`], root)) : new Map();
86
+ const files = [...new Set([...head.keys(), ...base.keys()])].sort().map((p) => ({
87
+ path: p,
88
+ agent: agentFor(p) ?? "Claude Code",
89
+ base: base.get(p)?.tokens ?? 0,
90
+ head: head.get(p)?.tokens ?? 0,
91
+ imported: (head.get(p) ?? base.get(p))?.imported || undefined,
92
+ }));
93
+ const baseTotal = files.reduce((s, f) => s + f.base, 0);
94
+ const headTotal = files.reduce((s, f) => s + f.head, 0);
95
+ const breaches = [];
96
+ if (opts.maxTokens !== undefined && headTotal > opts.maxTokens)
97
+ breaches.push(`memory files total ~${formatTokens(headTotal)} tokens, over the ${formatTokens(opts.maxTokens)} budget`);
98
+ if (opts.base && opts.maxIncrease !== undefined && headTotal - baseTotal > opts.maxIncrease) {
99
+ breaches.push(`this change adds ~${formatTokens(headTotal - baseTotal)} tokens, over the ${formatTokens(opts.maxIncrease)} allowed increase`);
100
+ }
101
+ return { base: opts.base, files, baseTotal, headTotal, breaches };
102
+ }
103
+ const signed = (n) => (n > 0 ? `+${formatTokens(n)}` : n < 0 ? `−${formatTokens(-n)}` : "0");
104
+ /** Markdown for a PR comment or a job summary. */
105
+ export function renderCiMarkdown(r, opts = {}) {
106
+ // The marker lets the GitHub Action find and update its own comment.
107
+ const out = ["<!-- context-doctor -->", "### context-doctor: what every agent request re-reads", ""];
108
+ if (r.files.length === 0) {
109
+ out.push("No agent memory files (CLAUDE.md, AGENTS.md, GEMINI.md, rules) in this repository.");
110
+ return out.join("\n");
111
+ }
112
+ const changed = r.base ? r.files.filter((f) => f.base !== f.head) : r.files;
113
+ const delta = r.headTotal - r.baseTotal;
114
+ out.push(r.base
115
+ ? `Memory files: **~${formatTokens(r.baseTotal)} → ~${formatTokens(r.headTotal)} tokens (${signed(delta)})**, loaded into every request of every session in this repo.`
116
+ : `Memory files: **~${formatTokens(r.headTotal)} tokens**, loaded into every request of every session in this repo.`);
117
+ const price = pricingFor(opts.model ?? "claude-sonnet-5");
118
+ if (price && opts.requestsPerDay && r.base && delta !== 0) {
119
+ // Cache reads on warm requests; this is the floor of the cost, cold starts add to it.
120
+ const usd = ((delta * price.cacheReadPerM) / 1e6) * opts.requestsPerDay * 30;
121
+ out.push(`At ${opts.requestsPerDay.toLocaleString("en-US")} requests a day on ${opts.model ?? "claude-sonnet-5"} (cached reads), that is ${usd >= 0 ? "+" : "−"}${formatUsd(Math.abs(usd))} a month.`);
122
+ }
123
+ out.push("");
124
+ if (changed.length) {
125
+ out.push(r.base ? "| File | Agent | Base | This PR | Change |" : "| File | Agent | Tokens |", r.base ? "|---|---|---:|---:|---:|" : "|---|---|---:|");
126
+ for (const f of changed) {
127
+ const name = `\`${f.path}\`${f.imported ? " (imported)" : ""}`;
128
+ out.push(r.base ? `| ${name} | ${f.agent} | ${f.base ? "~" + formatTokens(f.base) : "—"} | ${f.head ? "~" + formatTokens(f.head) : "—"} | ${signed(f.head - f.base)} |` : `| ${name} | ${f.agent} | ~${formatTokens(f.head)} |`);
129
+ }
130
+ }
131
+ else {
132
+ out.push("No memory file changed in this PR.");
133
+ }
134
+ if (r.breaches.length)
135
+ out.push("", ...r.breaches.map((b) => `**Over budget:** ${b}.`));
136
+ const big = r.files.filter((f) => f.head >= 2000);
137
+ if (big.length)
138
+ out.push("", `Large files: ${big.map((f) => `\`${f.path}\``).join(", ")}. \`npx context-doctor overhead split <file>\` moves reference sections out, word for word.`);
139
+ return out.join("\n");
140
+ }
141
+ /** Plain-text rendering for a terminal. */
142
+ export function renderCiText(r) {
143
+ return renderCiMarkdown(r).replace("<!-- context-doctor -->\n", "").replace(/^### /m, "").replace(/\*\*/g, "").replace(/`/g, "");
144
+ }