context-doctor 0.25.0 → 0.26.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 +26 -12
- package/dist/autoclear.js +4 -3
- package/dist/doctor.js +18 -0
- package/dist/hook.d.ts +2 -1
- package/dist/hook.js +43 -10
- package/dist/install.d.ts +10 -0
- package/dist/install.js +56 -0
- package/dist/mcp.js +10 -4
- package/dist/preferences.js +2 -2
- package/dist/proxy.d.ts +1 -1
- package/dist/proxy.js +4 -2
- package/dist/tokens.d.ts +2 -0
- package/dist/tokens.js +2 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -105,7 +105,7 @@ On a Claude subscription you do not pay list price; the same tokens come out of
|
|
|
105
105
|
|---|---|---|
|
|
106
106
|
| **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
107
|
| **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) |
|
|
108
|
+
| **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
109
|
| **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
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
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 |
|
|
@@ -116,6 +116,7 @@ Not claimed, because no process on your machine sends those requests: trimming i
|
|
|
116
116
|
|
|
117
117
|
## What's new
|
|
118
118
|
|
|
119
|
+
- **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
120
|
- **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
121
|
- **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
122
|
- **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 +144,10 @@ One run of `context-doctor install` writes these (each config edit makes a `.bac
|
|
|
143
144
|
2. **Claude Code config** (`~/.claude.json`) — registers the MCP server
|
|
144
145
|
3. **Cursor config** (`~/.cursor/mcp.json`) — registers the MCP server
|
|
145
146
|
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
|
|
147
|
-
6. **
|
|
148
|
-
7.
|
|
147
|
+
5. **Every-prompt hook** → `~/.claude/settings.json` — the per-query context check for Claude Code
|
|
148
|
+
6. **Cursor hook**, when Cursor is present → `~/.cursor/hooks.json` (`postToolUse`) — the same check for Cursor's agent, after each tool call
|
|
149
|
+
7. **Codex**, when present: MCP server in `~/.codex/config.toml`, the hook in `~/.codex/hooks.json`, the skill in `~/.codex/skills/`
|
|
150
|
+
8. With `--statusline`: live context size, cache share and cost in Claude Code's status bar
|
|
149
151
|
|
|
150
152
|
`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
153
|
|
|
@@ -329,15 +331,15 @@ Your API key still rides in the request headers, as before. The token protects t
|
|
|
329
331
|
| Surface | Runs by itself | What that means |
|
|
330
332
|
|---|---|---|
|
|
331
333
|
| **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.
|
|
334
|
+
| **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
335
|
| **API traffic through the proxy** | Yes: every request rewritten in flight | Fewer tokens, guaranteed, model not consulted |
|
|
334
336
|
| **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 |
|
|
337
|
+
| **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
338
|
| **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
339
|
|
|
338
340
|
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
341
|
|
|
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 >
|
|
342
|
+
**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
343
|
|
|
342
344
|
**Do you need to configure anything by hand? Usually no:**
|
|
343
345
|
|
|
@@ -345,7 +347,7 @@ So "every chat inherently better" is true for Claude Code, Cursor, Codex and the
|
|
|
345
347
|
|---|---|
|
|
346
348
|
| 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
349
|
| Claude Code | Same command — MCP + skill + every-prompt hook, all automatic |
|
|
348
|
-
| Cursor | Same command — writes `~/.cursor/mcp.json
|
|
350
|
+
| Cursor | Same command — writes `~/.cursor/mcp.json` and the after-tool-call hook in `~/.cursor/hooks.json`. `context-doctor doctor` checks both |
|
|
349
351
|
| Codex (OpenAI) | Same command — `~/.codex/config.toml`, `~/.codex/hooks.json`, `~/.codex/skills/`. Then, once, `/hooks` in Codex to trust the hook |
|
|
350
352
|
| 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
353
|
|
|
@@ -369,7 +371,7 @@ For any other MCP client, the server entry is:
|
|
|
369
371
|
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
372
|
4. One click instead of asking: the `context_checkup` prompt in the **+** menu sends that request for you.
|
|
371
373
|
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 >
|
|
374
|
+
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
375
|
|
|
374
376
|
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
377
|
|
|
@@ -379,13 +381,13 @@ ChatGPT's MCP support differs fundamentally from Claude Desktop's: **it never sp
|
|
|
379
381
|
|
|
380
382
|
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
383
|
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.
|
|
384
|
+
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
385
|
|
|
384
386
|
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
387
|
|
|
386
388
|
### claude.ai on the web and the phone apps
|
|
387
389
|
|
|
388
|
-
Your local MCP server can't reach the website, but the behavior can. Two options: `context-doctor instructions --copy` and paste into Settings >
|
|
390
|
+
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
391
|
|
|
390
392
|
### MCP tools
|
|
391
393
|
|
|
@@ -454,7 +456,7 @@ ctx 801k/1.0M ▮▮▮▮▮▮▮▮░░ 80% ⚠ · cache 100% · $12.34
|
|
|
454
456
|
|
|
455
457
|
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
458
|
|
|
457
|
-
|
|
459
|
+
The same number in VS Code and Cursor's status bar: see [the editor extension](#the-same-number-inside-vs-code-and-cursor).
|
|
458
460
|
|
|
459
461
|
## Does a smaller context actually help? Measure it
|
|
460
462
|
|
|
@@ -663,6 +665,18 @@ Also keep the MCP server version in `src/mcp.ts` in sync with `package.json`, an
|
|
|
663
665
|
|
|
664
666
|
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
667
|
|
|
668
|
+
Two end-to-end checks beyond `npm test`, both run against a throwaway `HOME`:
|
|
669
|
+
|
|
670
|
+
```bash
|
|
671
|
+
HOME=$(mktemp -d) node scripts/smoke-mcp.mjs node dist/mcp.js
|
|
672
|
+
```
|
|
673
|
+
|
|
674
|
+
```bash
|
|
675
|
+
HOME=$(mktemp -d) node scripts/smoke-openai-proxy.mjs
|
|
676
|
+
```
|
|
677
|
+
|
|
678
|
+
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.
|
|
679
|
+
|
|
666
680
|
## License
|
|
667
681
|
|
|
668
682
|
MIT © [gAI Ventures](https://gai.ventures)
|
package/dist/autoclear.js
CHANGED
|
@@ -45,10 +45,11 @@ 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
|
-
|
|
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",
|
|
50
51
|
// Cursor's agent
|
|
51
|
-
"Shell", "ReadFile", "rg", "run_terminal_cmd", "codebase_search",
|
|
52
|
+
"Shell", "ReadFile", "rg", "run_terminal_cmd", "codebase_search", "SemanticSearch", "ReadLints", "AwaitShell",
|
|
52
53
|
]);
|
|
53
54
|
/** Tokens billed for one image inside a tool result (a full-size screenshot is ~1.6k on Claude). */
|
|
54
55
|
const IMAGE_TOKENS = 1600;
|
package/dist/doctor.js
CHANGED
|
@@ -165,6 +165,24 @@ export async function runDoctor() {
|
|
|
165
165
|
else {
|
|
166
166
|
checks.push({ label: "Codex", status: "skip", detail: "not detected (ChatGPT's Codex agent, IDE extension or CLI)" });
|
|
167
167
|
}
|
|
168
|
+
// Cursor: its agent gets the guidance through a native postToolUse hook
|
|
169
|
+
// (the Claude Code hook it also runs cannot add context there).
|
|
170
|
+
if (existsSync(join(homedir(), ".cursor"))) {
|
|
171
|
+
const hooksPath = join(homedir(), ".cursor", "hooks.json");
|
|
172
|
+
try {
|
|
173
|
+
const hooks = existsSync(hooksPath) ? JSON.parse(readFileSync(hooksPath, "utf8")) : {};
|
|
174
|
+
const ours = (hooks.hooks?.postToolUse ?? []).map((e) => e.command ?? "").find((c) => /(cli\.js|context-doctor(\.cmd|\.exe|\.bat)?)"?\s+hook\s*$/.test(c));
|
|
175
|
+
const missing = ours ? hookBinaryMissing(ours) : undefined;
|
|
176
|
+
checks.push(!ours
|
|
177
|
+
? { label: "Cursor hook", status: "fail", detail: "not registered — run: context-doctor install" }
|
|
178
|
+
: missing
|
|
179
|
+
? { label: "Cursor hook", status: "fail", detail: `registered, but ${missing} no longer exists — re-run: context-doctor install` }
|
|
180
|
+
: { label: "Cursor hook", status: "ok", detail: "registered in ~/.cursor/hooks.json (after each tool call in heavy chats)" });
|
|
181
|
+
}
|
|
182
|
+
catch (e) {
|
|
183
|
+
checks.push({ label: "Cursor hook", status: "fail", detail: `~/.cursor/hooks.json unreadable (${e.message})` });
|
|
184
|
+
}
|
|
185
|
+
}
|
|
168
186
|
// Status line (opt-in, so absence is a note, not a failure)
|
|
169
187
|
if (existsSync(settingsPath)) {
|
|
170
188
|
try {
|
package/dist/hook.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Claude Code UserPromptSubmit hook: runs on EVERY query in Claude Code
|
|
2
|
+
* Claude Code UserPromptSubmit hook: runs on EVERY query in Claude Code (and
|
|
3
|
+
* Codex, same format), and after every tool call in Cursor (postToolUse).
|
|
3
4
|
*
|
|
4
5
|
* Claude Code pipes hook input as JSON on stdin ({session_id, transcript_path,
|
|
5
6
|
* prompt, ...}). We profile the session transcript; when the context is lean
|
package/dist/hook.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Claude Code UserPromptSubmit hook: runs on EVERY query in Claude Code
|
|
2
|
+
* Claude Code UserPromptSubmit hook: runs on EVERY query in Claude Code (and
|
|
3
|
+
* Codex, same format), and after every tool call in Cursor (postToolUse).
|
|
3
4
|
*
|
|
4
5
|
* Claude Code pipes hook input as JSON on stdin ({session_id, transcript_path,
|
|
5
6
|
* prompt, ...}). We profile the session transcript; when the context is lean
|
|
@@ -18,7 +19,7 @@ import { recordLedger, statePath } from "./ledger.js";
|
|
|
18
19
|
import { parseConversation } from "./parse.js";
|
|
19
20
|
import { profileConversation } from "./profile.js";
|
|
20
21
|
import { parseSessionFile } from "./session.js";
|
|
21
|
-
import { formatTokens, CHARS_PER_TOKEN } from "./tokens.js";
|
|
22
|
+
import { formatTokens, CHARS_PER_TOKEN, LARGEST_WINDOW } from "./tokens.js";
|
|
22
23
|
import { ensureProxyUp } from "./autopilot.js";
|
|
23
24
|
import { detectColdResume, renderColdResume, renderColdResumeNotice } from "./coldresume.js";
|
|
24
25
|
import { formatUsd, pricingFor } from "./pricing.js";
|
|
@@ -145,13 +146,23 @@ export async function runHook() {
|
|
|
145
146
|
}
|
|
146
147
|
const input = JSON.parse(await readStdin());
|
|
147
148
|
await heal;
|
|
149
|
+
// Cursor runs this hook twice over: Claude Code's UserPromptSubmit as its
|
|
150
|
+
// beforeSubmitPrompt (which cannot add context, so there is nothing to do),
|
|
151
|
+
// and natively after each tool call, where `additional_context` reaches
|
|
152
|
+
// the model. Codex and Claude Code send UserPromptSubmit.
|
|
153
|
+
// Cursor marks every hook input with cursor_version; any of its events but
|
|
154
|
+
// postToolUse would only spend the once-per-growth warning on output Cursor
|
|
155
|
+
// drops.
|
|
156
|
+
const cursor = input.hook_event_name === "postToolUse";
|
|
157
|
+
if (input.hook_event_name === "beforeSubmitPrompt" || (input.cursor_version && !cursor))
|
|
158
|
+
return;
|
|
148
159
|
const transcriptPath = input.transcript_path;
|
|
149
160
|
if (!transcriptPath || !existsSync(transcriptPath))
|
|
150
161
|
return;
|
|
151
162
|
// Fast path 1: a small transcript cannot exceed the threshold — exit on a
|
|
152
163
|
// single stat() without reading the file. This is the every-prompt cost
|
|
153
164
|
// for lean sessions: ~1ms.
|
|
154
|
-
const { config } = loadConfig(input.cwd ?? process.cwd());
|
|
165
|
+
const { config } = loadConfig(input.cwd ?? input.workspace_roots?.[0] ?? process.cwd());
|
|
155
166
|
const threshold = warnThreshold(config.budget?.maxTokens);
|
|
156
167
|
const sizeBytes = statSync(transcriptPath).size;
|
|
157
168
|
if (sizeBytes < minBytesForWarn(threshold))
|
|
@@ -159,7 +170,7 @@ export async function runHook() {
|
|
|
159
170
|
// Fast path 2: growth gate BEFORE parsing. If the file hasn't grown ~40%
|
|
160
171
|
// since the last full parse, nothing new can trigger — exit without the
|
|
161
172
|
// expensive read. Heavy-but-quiet sessions cost one stat + tiny state read.
|
|
162
|
-
const sessionId = input.session_id ?? transcriptPath;
|
|
173
|
+
const sessionId = input.session_id ?? input.conversation_id ?? transcriptPath;
|
|
163
174
|
const prev = readSessionState(sessionId);
|
|
164
175
|
// Cold resume: back on a large session after the cache expired. Checked
|
|
165
176
|
// before the growth gate, because a return after idle involves no growth.
|
|
@@ -177,12 +188,19 @@ export async function runHook() {
|
|
|
177
188
|
const emit = (lines) => {
|
|
178
189
|
if (lines.length === 0 && !notice)
|
|
179
190
|
return;
|
|
191
|
+
const block = `<context-doctor>\n${lines.join("\n")}\n</context-doctor>`;
|
|
192
|
+
if (cursor) {
|
|
193
|
+
// Cursor's postToolUse has no user-facing field, so only the model's note goes out.
|
|
194
|
+
if (lines.length > 0)
|
|
195
|
+
console.log(JSON.stringify({ additional_context: block }));
|
|
196
|
+
return;
|
|
197
|
+
}
|
|
180
198
|
const out = {};
|
|
181
199
|
// systemMessage is shown to the user in the app; additionalContext goes to the model.
|
|
182
200
|
if (notice)
|
|
183
201
|
out.systemMessage = notice;
|
|
184
202
|
if (lines.length > 0)
|
|
185
|
-
out.hookSpecificOutput = { hookEventName: "UserPromptSubmit", additionalContext:
|
|
203
|
+
out.hookSpecificOutput = { hookEventName: "UserPromptSubmit", additionalContext: block };
|
|
186
204
|
console.log(JSON.stringify(out));
|
|
187
205
|
};
|
|
188
206
|
if (prev.b > 0 && sizeBytes < prev.b * REGROWTH_FACTOR) {
|
|
@@ -195,7 +213,8 @@ export async function runHook() {
|
|
|
195
213
|
const parsed = parseSessionFile(transcriptPath);
|
|
196
214
|
if (parsed.messageCount === 0)
|
|
197
215
|
return;
|
|
198
|
-
const
|
|
216
|
+
const model = parsed.model ?? input.model;
|
|
217
|
+
const profile = profileConversation(parseConversation(parsed.conversationJson), model);
|
|
199
218
|
// Prefer the API's own figure when the transcript carries it: it includes
|
|
200
219
|
// the system prompt and tool schemas the transcript omits, so it is the
|
|
201
220
|
// real context size rather than a message-only estimate.
|
|
@@ -208,15 +227,27 @@ export async function runHook() {
|
|
|
208
227
|
emit(notes);
|
|
209
228
|
return;
|
|
210
229
|
}
|
|
211
|
-
|
|
230
|
+
// An estimate past the window (no API figure, e.g. a history imported into
|
|
231
|
+
// Codex) is not the live context: the app must already be trimming it.
|
|
232
|
+
const overWindow = !cursor && parsed.reportedInputTokens === undefined && liveTokens > (profile.contextWindow ?? LARGEST_WINDOW);
|
|
233
|
+
const windowPct = !cursor && !overWindow && profile.contextWindow ? (liveTokens / profile.contextWindow) * 100 : undefined;
|
|
212
234
|
// Agent sessions run on the prompt cache: a message normally re-reads the
|
|
213
235
|
// context at the cached rate (0.1x) and pays the full rate only when the
|
|
214
236
|
// cache has expired. Quoting the uncached figure alone overstated the
|
|
215
237
|
// per-message cost tenfold (fixed in 0.22).
|
|
216
|
-
const pricing = pricingFor(
|
|
238
|
+
const pricing = pricingFor(model);
|
|
217
239
|
const cachedUsd = pricing ? (liveTokens * pricing.cacheReadPerM) / 1e6 : undefined;
|
|
218
240
|
const coldUsd = pricing ? (liveTokens * pricing.inputPerM * 1.25) / 1e6 : undefined;
|
|
219
|
-
|
|
241
|
+
// Cursor's agent transcript records tool calls but not their output, and
|
|
242
|
+
// keeps turns Cursor has since summarized, so it measures how much the chat
|
|
243
|
+
// has accumulated, not the live context: no window share or price for it.
|
|
244
|
+
const lines = cursor ? [
|
|
245
|
+
`This chat has accumulated ~${formatTokens(liveTokens)} tokens of messages and tool calls (tool output not counted; Cursor summarizes older turns itself, so the live context may differ).`,
|
|
246
|
+
"Practice context hygiene from here on: summarize large tool results instead of keeping them verbatim, reference earlier content rather than re-reading or re-quoting it, and keep responses lean.",
|
|
247
|
+
] : overWindow ? [
|
|
248
|
+
`This session's transcript holds ~${formatTokens(liveTokens)} tokens, more than ${profile.contextWindow ? `the model's ~${formatTokens(profile.contextWindow)}` : "any current model's"} window, so the app is already summarizing or dropping older turns; what the model sees is a part of it.`,
|
|
249
|
+
"Practice context hygiene from here on: summarize large tool results instead of keeping them verbatim, reference earlier content rather than re-reading or re-quoting it, and keep responses lean.",
|
|
250
|
+
] : [
|
|
220
251
|
`This session's context is at ~${formatTokens(liveTokens)} tokens` +
|
|
221
252
|
(windowPct !== undefined ? ` (${windowPct.toFixed(0)}% of the window)` : "") +
|
|
222
253
|
(cachedUsd !== undefined ? `: each message re-reads it for ~${formatUsd(cachedUsd)} from the prompt cache, ~${formatUsd(coldUsd)} when the cache has expired` : "") +
|
|
@@ -233,7 +264,9 @@ export async function runHook() {
|
|
|
233
264
|
lines.push(`Largest recoverable waste: ${topFinding.message} (${topFinding.suggestion})`);
|
|
234
265
|
}
|
|
235
266
|
if (liveTokens > threshold * 2) {
|
|
236
|
-
lines.push(
|
|
267
|
+
lines.push(cursor
|
|
268
|
+
? "If the task changes, suggest the user start a new chat from a short summary of this one."
|
|
269
|
+
: "If it fits the flow, offer the user a compaction of the older history.");
|
|
237
270
|
}
|
|
238
271
|
emit([...notes, ...lines]);
|
|
239
272
|
}
|
package/dist/install.d.ts
CHANGED
|
@@ -31,6 +31,16 @@ export declare function isEphemeralPath(path: string): boolean;
|
|
|
31
31
|
* ours. Returns what happened so install can print the truth.
|
|
32
32
|
*/
|
|
33
33
|
export declare function installStatusLine(): "installed" | "already" | "kept-foreign" | "no-claude-code";
|
|
34
|
+
/**
|
|
35
|
+
* Cursor runs Claude Code's UserPromptSubmit hook as its own
|
|
36
|
+
* beforeSubmitPrompt, which can only allow or block a prompt: what the hook
|
|
37
|
+
* says never reaches Cursor's model. Cursor's postToolUse can add context
|
|
38
|
+
* (`additional_context`), and its input carries the agent transcript, so the
|
|
39
|
+
* check runs there, in Cursor's own ~/.cursor/hooks.json. It fires after every
|
|
40
|
+
* tool call, which the hook's stat-only fast path and regrowth gate make cheap.
|
|
41
|
+
*/
|
|
42
|
+
export declare function cursorHooksPath(): string;
|
|
43
|
+
export declare function installCursorHook(): string | null;
|
|
34
44
|
export declare function codexDir(): string;
|
|
35
45
|
/**
|
|
36
46
|
* Add or replace our [mcp_servers.context-doctor] table in config.toml without
|
package/dist/install.js
CHANGED
|
@@ -237,6 +237,53 @@ function installHookInto(settingsPath, gateDir) {
|
|
|
237
237
|
function uninstallHook() {
|
|
238
238
|
uninstallHookFrom(join(homedir(), ".claude", "settings.json"), "Claude Code every-prompt hook");
|
|
239
239
|
uninstallHookFrom(join(homedir(), ".codex", "hooks.json"), "Codex every-prompt hook");
|
|
240
|
+
uninstallCursorHook();
|
|
241
|
+
}
|
|
242
|
+
/**
|
|
243
|
+
* Cursor runs Claude Code's UserPromptSubmit hook as its own
|
|
244
|
+
* beforeSubmitPrompt, which can only allow or block a prompt: what the hook
|
|
245
|
+
* says never reaches Cursor's model. Cursor's postToolUse can add context
|
|
246
|
+
* (`additional_context`), and its input carries the agent transcript, so the
|
|
247
|
+
* check runs there, in Cursor's own ~/.cursor/hooks.json. It fires after every
|
|
248
|
+
* tool call, which the hook's stat-only fast path and regrowth gate make cheap.
|
|
249
|
+
*/
|
|
250
|
+
export function cursorHooksPath() {
|
|
251
|
+
return join(homedir(), ".cursor", "hooks.json");
|
|
252
|
+
}
|
|
253
|
+
const isOurCursorEntry = (e) => /(cli\.js|context-doctor(\.cmd|\.exe|\.bat)?)"?\s+hook\s*$/.test(String(e?.command ?? ""));
|
|
254
|
+
export function installCursorHook() {
|
|
255
|
+
if (!existsSync(join(homedir(), ".cursor")))
|
|
256
|
+
return null;
|
|
257
|
+
const path = cursorHooksPath();
|
|
258
|
+
const config = readJson(path);
|
|
259
|
+
config.version = config.version ?? 1;
|
|
260
|
+
config.hooks = config.hooks ?? {};
|
|
261
|
+
const entries = config.hooks.postToolUse ?? [];
|
|
262
|
+
const want = hookCommand();
|
|
263
|
+
const ours = entries.filter(isOurCursorEntry);
|
|
264
|
+
if (ours.length === 1 && ours[0].command === want)
|
|
265
|
+
return path; // already correct
|
|
266
|
+
config.hooks.postToolUse = [...entries.filter((e) => !isOurCursorEntry(e)), { command: want, timeout: 10 }];
|
|
267
|
+
writeJsonWithBackup(path, config);
|
|
268
|
+
return path;
|
|
269
|
+
}
|
|
270
|
+
function uninstallCursorHook() {
|
|
271
|
+
const path = cursorHooksPath();
|
|
272
|
+
if (!existsSync(path))
|
|
273
|
+
return;
|
|
274
|
+
const config = readJson(path);
|
|
275
|
+
const entries = config.hooks?.postToolUse;
|
|
276
|
+
if (!entries)
|
|
277
|
+
return;
|
|
278
|
+
const filtered = entries.filter((e) => !isOurCursorEntry(e));
|
|
279
|
+
if (filtered.length === entries.length)
|
|
280
|
+
return;
|
|
281
|
+
if (filtered.length === 0)
|
|
282
|
+
delete config.hooks.postToolUse;
|
|
283
|
+
else
|
|
284
|
+
config.hooks.postToolUse = filtered;
|
|
285
|
+
writeJsonWithBackup(path, config);
|
|
286
|
+
console.log("✓ Cursor after-tool-call hook removed");
|
|
240
287
|
}
|
|
241
288
|
function uninstallHookFrom(settingsPath, label) {
|
|
242
289
|
if (!existsSync(settingsPath))
|
|
@@ -440,6 +487,15 @@ export function runInstall(options = {}) {
|
|
|
440
487
|
console.error(`✗ Claude Code every-prompt hook: ${e.message}`);
|
|
441
488
|
failures.push("Claude Code hook");
|
|
442
489
|
}
|
|
490
|
+
try {
|
|
491
|
+
const cursorHook = installCursorHook();
|
|
492
|
+
if (cursorHook)
|
|
493
|
+
console.log(`✓ Cursor after-tool-call hook installed (${cursorHook}) — Cursor's agent gets the same guidance in heavy sessions`);
|
|
494
|
+
}
|
|
495
|
+
catch (e) {
|
|
496
|
+
console.error(`✗ Cursor hook: ${e.message}`);
|
|
497
|
+
failures.push("Cursor hook");
|
|
498
|
+
}
|
|
443
499
|
installCodex(entry, failures);
|
|
444
500
|
if (options.statusLine) {
|
|
445
501
|
try {
|
package/dist/mcp.js
CHANGED
|
@@ -49,7 +49,7 @@ const STRATEGY_IDS = ["dedupe", "trim-tool-results", "trim-tool-calls", "strip-b
|
|
|
49
49
|
* recommended pattern.
|
|
50
50
|
*/
|
|
51
51
|
function createServer() {
|
|
52
|
-
const server = new McpServer({ name: "context-doctor", version: "0.
|
|
52
|
+
const server = new McpServer({ name: "context-doctor", version: "0.26.0" }, { instructions: SERVER_INSTRUCTIONS });
|
|
53
53
|
server.tool("profile_context", "Profile an LLM conversation or prompt: token breakdown, largest blocks, and actionable findings about wasted context (duplicates, oversized pastes or tool results, base64 blobs, long history). Two inputs, pass ONE: `conversation` (full OpenAI/Anthropic JSON or raw text, for agents, files and proxies) or `sketch` (for chat apps such as Claude Desktop or ChatGPT where you cannot export the conversation: the turn count plus the few blocks that matter, ~120 tokens to write). Call it whenever the user asks about token usage, context size, cost, speed or limits, and on your own once the conversation passes ~30 turns or holds 3+ large pastes. Act on the top finding in your reply.", {
|
|
54
54
|
conversation: z.string().optional().describe("Conversation JSON (OpenAI or Anthropic format, or bare message array) or raw prompt text. Omit in chat apps and pass `sketch`."),
|
|
55
55
|
sketch: z.object({
|
|
@@ -68,7 +68,9 @@ function createServer() {
|
|
|
68
68
|
})).max(500).describe("Only the blocks over ~500 tokens, repeated, or images. Plain turns need not be listed."),
|
|
69
69
|
}).optional().describe("Coarse description of the conversation for chat apps. One size hint per block: lines for code, words for prose, chars (or tokens) for logs and tool output, which vary most per line."),
|
|
70
70
|
model: z.string().optional().describe("Target model name for context-window math, e.g. claude-sonnet-5 or gpt-4o"),
|
|
71
|
-
},
|
|
71
|
+
},
|
|
72
|
+
// Pure: reads its input and returns text. Clients (Codex, ChatGPT) may skip approval for read-only tools.
|
|
73
|
+
{ title: "Profile context", readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false }, async ({ conversation, sketch, model }) => {
|
|
72
74
|
if (sketch) {
|
|
73
75
|
return { content: [{ type: "text", text: runSketch({ ...sketch, model: sketch.model ?? model }) }] };
|
|
74
76
|
}
|
|
@@ -87,7 +89,9 @@ function createServer() {
|
|
|
87
89
|
.describe("Strategies to apply. Default: dedupe, trim-tool-results, strip-base64. Add trim-tool-calls to shrink big inline file writes, or prune-history for lossy compaction of old turns."),
|
|
88
90
|
keep_recent: z.number().int().nonnegative().optional().describe("Messages at the tail to leave untouched (default 6; 0 = none)"),
|
|
89
91
|
max_tool_result_tokens: z.number().int().positive().optional().describe("Token budget for trimmed tool results (default 300)"),
|
|
90
|
-
},
|
|
92
|
+
},
|
|
93
|
+
// Pure: reads its input and returns text. Clients (Codex, ChatGPT) may skip approval for read-only tools.
|
|
94
|
+
{ title: "Optimize context", readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false }, async ({ conversation, strategies, keep_recent, max_tool_result_tokens }) => {
|
|
91
95
|
const result = optimizeConversation(conversation, {
|
|
92
96
|
strategies: strategies,
|
|
93
97
|
keepRecent: keep_recent,
|
|
@@ -148,7 +152,9 @@ function createServer() {
|
|
|
148
152
|
}));
|
|
149
153
|
server.tool("context_best_practices", "Get a curated checklist of context-management best practices, optionally specialized for a provider (anthropic, openai).", {
|
|
150
154
|
provider: z.enum(["general", "anthropic", "openai"]).optional().describe("Provider to specialize tips for (default: general)"),
|
|
151
|
-
},
|
|
155
|
+
},
|
|
156
|
+
// Pure: reads its input and returns text. Clients (Codex, ChatGPT) may skip approval for read-only tools.
|
|
157
|
+
{ title: "Context best practices", readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false }, async ({ provider }) => {
|
|
152
158
|
const tips = [...BEST_PRACTICES.general, ...(provider && provider !== "general" ? BEST_PRACTICES[provider] : [])];
|
|
153
159
|
return { content: [{ type: "text", text: tips.map((t, i) => `${i + 1}. ${t}`).join("\n") }] };
|
|
154
160
|
});
|
package/dist/preferences.js
CHANGED
|
@@ -16,8 +16,8 @@ export const CHAT_PREFERENCES = [
|
|
|
16
16
|
].join(" ");
|
|
17
17
|
/** Where to paste, per app. Paths are the UI labels, not URLs, so they survive redesigns. */
|
|
18
18
|
export const PREFERENCE_TARGETS = [
|
|
19
|
-
"claude.ai / Claude Desktop / Claude mobile: Settings >
|
|
20
|
-
"ChatGPT: Settings > Personalization > Custom instructions > \"
|
|
19
|
+
"claude.ai / Claude Desktop / Claude mobile: Settings > General > \"What personal preferences should Claude consider in responses?\"",
|
|
20
|
+
"ChatGPT: Settings > Personalization > Custom instructions > \"What traits should ChatGPT have?\"",
|
|
21
21
|
];
|
|
22
22
|
/** Copy text to the system clipboard; false when no clipboard tool is available. */
|
|
23
23
|
export function copyToClipboard(text, platform = process.platform) {
|
package/dist/proxy.d.ts
CHANGED
|
@@ -47,7 +47,7 @@ export interface ProxyOptions extends OptimizeOptions {
|
|
|
47
47
|
openaiUpstream?: string;
|
|
48
48
|
}
|
|
49
49
|
/** Reported by /health so `autopilot status` can tell an outdated service from a current one. */
|
|
50
|
-
export declare const PROXY_VERSION = "0.
|
|
50
|
+
export declare const PROXY_VERSION = "0.26.0";
|
|
51
51
|
/**
|
|
52
52
|
* Remove a leading `/t/<token>` from a request path, or return undefined when
|
|
53
53
|
* the prefix is absent or the token differs. The comparison is constant time
|
package/dist/proxy.js
CHANGED
|
@@ -20,7 +20,7 @@ import { formatTokens, CHARS_PER_TOKEN, providerFor } from "./tokens.js";
|
|
|
20
20
|
import { formatUsd, inputCostUsd, pricingFor } from "./pricing.js";
|
|
21
21
|
import { recordLedger } from "./ledger.js";
|
|
22
22
|
/** Reported by /health so `autopilot status` can tell an outdated service from a current one. */
|
|
23
|
-
export const PROXY_VERSION = "0.
|
|
23
|
+
export const PROXY_VERSION = "0.26.0";
|
|
24
24
|
/** Connection-level headers that must not be forwarded. */
|
|
25
25
|
const SKIP_REQUEST_HEADERS = new Set(["host", "content-length", "connection", "transfer-encoding", "accept-encoding", "expect"]);
|
|
26
26
|
const SKIP_RESPONSE_HEADERS = new Set(["content-length", "content-encoding", "transfer-encoding", "connection"]);
|
|
@@ -384,7 +384,9 @@ export function startProxy(opts = {}) {
|
|
|
384
384
|
console.error(`context-doctor proxy listening on http://${host}:${port}`);
|
|
385
385
|
console.error(` Anthropic apps/SDKs: export ANTHROPIC_BASE_URL=http://localhost:${port}${prefix}`);
|
|
386
386
|
console.error(` OpenAI apps/SDKs: export OPENAI_BASE_URL=http://localhost:${port}${prefix}/v1`);
|
|
387
|
-
console.error(
|
|
387
|
+
console.error(opts.autopilot
|
|
388
|
+
? ` Autopilot: stale re-runnable tool output is cleared only when the prompt cache is cold; savings are logged here.`
|
|
389
|
+
: ` Every request's context is optimized in flight; savings are logged here.`);
|
|
388
390
|
console.error(` Cumulative savings: http://localhost:${port}${prefix}/stats`);
|
|
389
391
|
if (opts.token) {
|
|
390
392
|
console.error(` Token required: every path except /health must start with /t/<token>/.`);
|
package/dist/tokens.d.ts
CHANGED
|
@@ -9,6 +9,8 @@
|
|
|
9
9
|
* fully offline.
|
|
10
10
|
*/
|
|
11
11
|
export type Provider = "anthropic" | "openai" | "google" | "generic";
|
|
12
|
+
/** The largest window any model above has: no live context can exceed it. */
|
|
13
|
+
export declare const LARGEST_WINDOW: number;
|
|
12
14
|
export declare function contextWindowFor(model?: string): number | undefined;
|
|
13
15
|
export declare function providerFor(model?: string): Provider;
|
|
14
16
|
/**
|
package/dist/tokens.js
CHANGED
|
@@ -22,6 +22,8 @@ const MODEL_WINDOWS = [
|
|
|
22
22
|
[/llama.*3/i, 128_000],
|
|
23
23
|
[/mistral|mixtral/i, 32_000],
|
|
24
24
|
];
|
|
25
|
+
/** The largest window any model above has: no live context can exceed it. */
|
|
26
|
+
export const LARGEST_WINDOW = Math.max(...MODEL_WINDOWS.map(([, w]) => w));
|
|
25
27
|
export function contextWindowFor(model) {
|
|
26
28
|
if (!model)
|
|
27
29
|
return undefined;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "context-doctor",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.26.0",
|
|
4
4
|
"description": "See what your Claude Code sessions cost and cut it: offers /compact when you return to a large session after the cache expired (every surface, desktop app included), and a cache-safe autopilot for terminal, IDE and API traffic. Token profiler, hooks, MCP tools and a Claude Code plugin.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"agent",
|