context-doctor 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +177 -0
- package/dist/cli.d.ts +10 -0
- package/dist/cli.js +216 -0
- package/dist/index.d.ts +19 -0
- package/dist/index.js +12 -0
- package/dist/install.d.ts +10 -0
- package/dist/install.js +120 -0
- package/dist/mcp.d.ts +15 -0
- package/dist/mcp.js +92 -0
- package/dist/optimize.d.ts +36 -0
- package/dist/optimize.js +189 -0
- package/dist/parse.d.ts +31 -0
- package/dist/parse.js +118 -0
- package/dist/pricing.d.ts +26 -0
- package/dist/pricing.js +48 -0
- package/dist/profile.d.ts +55 -0
- package/dist/profile.js +197 -0
- package/dist/proxy.d.ts +31 -0
- package/dist/proxy.js +137 -0
- package/dist/report.d.ts +7 -0
- package/dist/report.js +82 -0
- package/dist/session.d.ts +27 -0
- package/dist/session.js +80 -0
- package/dist/test/proxy.test.d.ts +6 -0
- package/dist/test/proxy.test.js +77 -0
- package/dist/test/smoke.test.d.ts +2 -0
- package/dist/test/smoke.test.js +73 -0
- package/dist/tokens.d.ts +17 -0
- package/dist/tokens.js +69 -0
- package/package.json +54 -0
- package/skills/context-doctor/SKILL.md +39 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Kushal P
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
# context-doctor ๐ฉบ
|
|
2
|
+
|
|
3
|
+
**See what's eating your LLM context window โ and fix it.**
|
|
4
|
+
|
|
5
|
+
Every long-running LLM conversation slowly fills up with junk: duplicated documents, 10k-token tool outputs nobody reads again, base64 blobs, stale history. You pay for those tokens on **every single call**, and model quality drops as the window fills.
|
|
6
|
+
|
|
7
|
+
`context-doctor` is a zero-config profiler + optimizer for LLM contexts. It works with **Claude, GPT, Gemini** message formats, and plugs into **Claude Desktop, ChatGPT (developer mode), Cursor, Claude Code** โ any MCP-capable app โ or runs standalone from the terminal.
|
|
8
|
+
|
|
9
|
+
```
|
|
10
|
+
Where the tokens go
|
|
11
|
+
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
12
|
+
Tool results โโโโโโโโโโโโโโโโโโโโโโโโโโโโ 57% ~41k
|
|
13
|
+
System prompt โโโโโโโโโโโโโโโโโโโโโโโโโโโโ 21% ~15k
|
|
14
|
+
Assistant replies โโโโโโโโโโโโโโโโโโโโโโโโโโโโ 13% ~9.4k
|
|
15
|
+
User messages โโโโโโโโโโโโโโโโโโโโโโโโโโโโ 9% ~6.5k
|
|
16
|
+
|
|
17
|
+
Findings (4)
|
|
18
|
+
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
19
|
+
โ Message #12 contains a base64/binary blob (~8.2k tokens). [save ~7.4k]
|
|
20
|
+
โ Never put base64 in text content โ use the provider's file/image APIs.
|
|
21
|
+
โฒ Tool result at message #7 (web_search) is ~6.1k tokens. [save ~4.9k]
|
|
22
|
+
โ Truncate or summarize large tool outputs before they enter history.
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
## Quick start (30 seconds)
|
|
26
|
+
|
|
27
|
+
**One command sets up everything** โ detects Claude Desktop, Claude Code, and Cursor on your machine, wires in the MCP server, and installs the Agent Skill:
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
npx context-doctor install
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
Restart your apps, then just ask Claude: *"what's eating my context?"* (`npx context-doctor uninstall` reverses it.)
|
|
34
|
+
|
|
35
|
+
Or use the CLI directly, no install needed:
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
npx context-doctor analyze conversation.json --model claude-sonnet-5
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
npx context-doctor optimize conversation.json --out slimmed.json
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
Input is any of: OpenAI chat format, Anthropic messages format (with `system` and content blocks), or a bare `[{role, content}]` array. Use `-` to pipe from stdin.
|
|
46
|
+
|
|
47
|
+
Reports include **dollar and latency estimates**, not just tokens:
|
|
48
|
+
|
|
49
|
+
```
|
|
50
|
+
Cost: ~$3.30 input per call ยท ~$3302 per 1k calls ยท ~13.2s of latency per call (estimates)
|
|
51
|
+
...
|
|
52
|
+
Potential recovery: ~5.9k tokens (~73% of context) โ $17.69 per 1k calls, 0.2s faster per call
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
## Profile your actual Claude Code sessions
|
|
56
|
+
|
|
57
|
+
```bash
|
|
58
|
+
npx context-doctor session # profile your most recent session
|
|
59
|
+
npx context-doctor session --list # browse sessions
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
Parses the transcripts Claude Code writes locally and answers "where did my tokens go today?" โ it will happily tell you that one giant skill load is 67% of your context.
|
|
63
|
+
|
|
64
|
+
## Always-on: optimize every request automatically
|
|
65
|
+
|
|
66
|
+
Run the proxy and every Anthropic/OpenAI API call your apps make gets optimized in flight โ no code changes:
|
|
67
|
+
|
|
68
|
+
```bash
|
|
69
|
+
npx context-doctor proxy
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
Then point your app or SDK at it:
|
|
73
|
+
|
|
74
|
+
```bash
|
|
75
|
+
export ANTHROPIC_BASE_URL=http://localhost:8787 # Anthropic SDKs / tools
|
|
76
|
+
export OPENAI_BASE_URL=http://localhost:8787/v1 # OpenAI SDKs / tools
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
The proxy dedupes repeated content, trims stale tool results, and strips base64 blobs from the message history of each request, then forwards it to the real API. Your API key passes through in headers untouched, streaming (SSE) works unchanged, and per-request savings are logged with upstream latency:
|
|
80
|
+
|
|
81
|
+
```
|
|
82
|
+
[context-doctor] POST /v1/messages โ 200 in 842ms | optimized 7.3k โ 518 tokens (2 changes) | session total: 6.9k tokens โ $0.021 saved
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
`GET http://localhost:8787/stats` returns cumulative savings (requests, tokens, estimated USD) since launch.
|
|
86
|
+
|
|
87
|
+
Because prompt caching matches byte-identical prefixes, deterministic strategies are chosen so repeated requests stay stable โ but if you rely on aggressive cache prefixes, start with `--strategy strip-base64 --strategy dedupe` and add more as you verify.
|
|
88
|
+
|
|
89
|
+
> **Note on desktop chat apps:** Claude Desktop and the ChatGPT app talk to their own backends โ no tool can sit in that path. For those, use the MCP integration below and add a line to your custom instructions like: *"When a conversation gets long or includes large pasted content, proactively use context-doctor's profile_context tool and tell me what to trim."* The model will then invoke it on its own.
|
|
90
|
+
|
|
91
|
+
## Use it inside your AI app (MCP)
|
|
92
|
+
|
|
93
|
+
`context-doctor` ships an MCP server, so the AI itself can profile and slim context on demand.
|
|
94
|
+
|
|
95
|
+
**Claude Desktop** โ add to `claude_desktop_config.json` (Settings โ Developer โ Edit Config):
|
|
96
|
+
|
|
97
|
+
```json
|
|
98
|
+
{
|
|
99
|
+
"mcpServers": {
|
|
100
|
+
"context-doctor": {
|
|
101
|
+
"command": "npx",
|
|
102
|
+
"args": ["-y", "context-doctor-mcp"]
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
**ChatGPT desktop** (developer mode), **Cursor**, **Claude Code** (`claude mcp add context-doctor -- npx -y context-doctor-mcp`), and any other MCP client: same command, their config syntax.
|
|
109
|
+
|
|
110
|
+
Then just ask: *"profile this conversation with context-doctor"* or paste an exported chat and say *"what's eating my context?"*
|
|
111
|
+
|
|
112
|
+
### MCP tools
|
|
113
|
+
|
|
114
|
+
| Tool | What it does |
|
|
115
|
+
|---|---|
|
|
116
|
+
| `profile_context` | Token breakdown by category, largest messages, findings with estimated savings |
|
|
117
|
+
| `optimize_context` | Rewrites the conversation: dedupe, trim stale tool results, strip base64, optional history pruning |
|
|
118
|
+
| `context_best_practices` | Curated checklist, optionally specialized for Anthropic / OpenAI |
|
|
119
|
+
|
|
120
|
+
## Use it as a library
|
|
121
|
+
|
|
122
|
+
```ts
|
|
123
|
+
import { parseConversation, profileConversation, optimizeConversation } from "context-doctor";
|
|
124
|
+
|
|
125
|
+
const profile = profileConversation(parseConversation(chatJson), "claude-sonnet-5");
|
|
126
|
+
console.log(profile.totalTokens, profile.findings);
|
|
127
|
+
|
|
128
|
+
const { conversation, tokensBefore, tokensAfter } = optimizeConversation(chatJson, {
|
|
129
|
+
strategies: ["dedupe", "trim-tool-results", "strip-base64"],
|
|
130
|
+
});
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
## What it detects
|
|
134
|
+
|
|
135
|
+
- **Oversized tool results** โ the #1 context killer in agent loops
|
|
136
|
+
- **Duplicate content** โ the same doc/result pasted twice
|
|
137
|
+
- **Repeated identical tool calls** โ a signal your agent forgot earlier results
|
|
138
|
+
- **Base64 / binary blobs** in text content
|
|
139
|
+
- **Long history** past the point where models track the middle
|
|
140
|
+
- **Cache-hostile ordering** โ volatile content before stable content breaks prompt caching (Anthropic `cache_control`, OpenAI automatic prefix caching)
|
|
141
|
+
- **Window pressure** โ usage % against the target model's real context window
|
|
142
|
+
|
|
143
|
+
## What it fixes (deterministically โ no LLM calls, no API keys)
|
|
144
|
+
|
|
145
|
+
| Strategy | Lossy? | Default |
|
|
146
|
+
|---|---|---|
|
|
147
|
+
| `dedupe` โ replace repeated content with a reference | No | โ
|
|
|
148
|
+
| `trim-tool-results` โ truncate stale tool outputs | Mostly no | โ
|
|
|
149
|
+
| `strip-base64` โ remove inline binary blobs | No (for the model) | โ
|
|
|
150
|
+
| `prune-history` โ collapse old turns into a stub for summarization | Yes | opt-in |
|
|
151
|
+
|
|
152
|
+
Everything the optimizer does is inspectable: it prints exactly which messages changed and how many tokens each change saved.
|
|
153
|
+
|
|
154
|
+
**Summarization without an API key:** when `prune-history` runs through the MCP tools, context-doctor hands a digest of the pruned turns back to the model that called it (the Claude/GPT already running in your app) and asks *it* to write the replacement summary โ LLM-quality compaction, zero extra cost, no keys.
|
|
155
|
+
|
|
156
|
+
## The Agent Skill
|
|
157
|
+
|
|
158
|
+
`skills/context-doctor/SKILL.md` (installed by `npx context-doctor install`) teaches Claude to practice context hygiene proactively: summarize big tool results after consuming them, never re-paste duplicated content, keep stable content cache-friendly, and offer compaction when a session gets heavy โ so sessions get inherently leaner without you asking.
|
|
159
|
+
|
|
160
|
+
## Why token counts are "~"
|
|
161
|
+
|
|
162
|
+
Exact counts require each provider's private tokenizer. `context-doctor` uses a calibrated chars-per-token heuristic (denser for code/JSON) that lands within ~10% โ plenty accurate for finding what's heavy and measuring savings, and it keeps the tool fully offline with zero configuration.
|
|
163
|
+
|
|
164
|
+
## Roadmap
|
|
165
|
+
|
|
166
|
+
- [x] ~~Session import from Claude Code transcript formats~~ (`context-doctor session`)
|
|
167
|
+
- [x] ~~LLM summarization for prune-history~~ (host-model summarization via MCP โ no key needed)
|
|
168
|
+
- [ ] Proxy: per-route strategy config + response token accounting
|
|
169
|
+
- [ ] `context-doctor watch` โ live profiling of a running agent's JSONL trace
|
|
170
|
+
- [ ] Exact tokenizer adapters (tiktoken, Anthropic count-tokens API) as optional plugins
|
|
171
|
+
- [ ] Cursor / ChatGPT-export transcript formats for `session`
|
|
172
|
+
|
|
173
|
+
Contributions welcome โ this project is small on purpose. Open an issue before a big PR.
|
|
174
|
+
|
|
175
|
+
## License
|
|
176
|
+
|
|
177
|
+
MIT
|
package/dist/cli.d.ts
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* context-doctor CLI
|
|
4
|
+
*
|
|
5
|
+
* context-doctor analyze <file|-> [--model claude-sonnet-5] [--json]
|
|
6
|
+
* context-doctor optimize <file|-> [--out file] [--strategy s]... [--keep-recent N] [--max-tool-tokens N]
|
|
7
|
+
*
|
|
8
|
+
* `-` reads from stdin, so you can pipe: `cat chat.json | context-doctor analyze -`
|
|
9
|
+
*/
|
|
10
|
+
export {};
|
package/dist/cli.js
ADDED
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* context-doctor CLI
|
|
4
|
+
*
|
|
5
|
+
* context-doctor analyze <file|-> [--model claude-sonnet-5] [--json]
|
|
6
|
+
* context-doctor optimize <file|-> [--out file] [--strategy s]... [--keep-recent N] [--max-tool-tokens N]
|
|
7
|
+
*
|
|
8
|
+
* `-` reads from stdin, so you can pipe: `cat chat.json | context-doctor analyze -`
|
|
9
|
+
*/
|
|
10
|
+
import { readFileSync, writeFileSync } from "node:fs";
|
|
11
|
+
import process from "node:process";
|
|
12
|
+
import { parseConversation } from "./parse.js";
|
|
13
|
+
import { profileConversation } from "./profile.js";
|
|
14
|
+
import { optimizeConversation } from "./optimize.js";
|
|
15
|
+
import { renderProfile } from "./report.js";
|
|
16
|
+
import { formatTokens } from "./tokens.js";
|
|
17
|
+
import { startProxy } from "./proxy.js";
|
|
18
|
+
import { runInstall, runUninstall } from "./install.js";
|
|
19
|
+
import { listSessions, parseSessionFile } from "./session.js";
|
|
20
|
+
const HELP = `context-doctor โ profile and optimize LLM context windows
|
|
21
|
+
|
|
22
|
+
Usage:
|
|
23
|
+
context-doctor analyze <file|-> [options] Show what's eating your tokens
|
|
24
|
+
context-doctor optimize <file|-> [options] Apply safe fixes, print slimmed conversation
|
|
25
|
+
context-doctor proxy [options] Always-on: local proxy that optimizes every
|
|
26
|
+
Anthropic/OpenAI API request in flight
|
|
27
|
+
context-doctor install Wire the MCP server + skill into Claude Desktop,
|
|
28
|
+
Claude Code, and Cursor automatically
|
|
29
|
+
context-doctor uninstall Undo install
|
|
30
|
+
context-doctor session [file] Profile a Claude Code session transcript
|
|
31
|
+
(default: the most recent session; --list to browse)
|
|
32
|
+
|
|
33
|
+
Input: a conversation JSON file (OpenAI or Anthropic message format, or a bare
|
|
34
|
+
message array). Use "-" to read from stdin.
|
|
35
|
+
|
|
36
|
+
Options:
|
|
37
|
+
--model <name> Model name for window-size math (e.g. claude-sonnet-5, gpt-4o)
|
|
38
|
+
--json Machine-readable output
|
|
39
|
+
--out <file> (optimize) Write result to file instead of stdout
|
|
40
|
+
--strategy <id> (optimize) Strategy to run; repeatable.
|
|
41
|
+
Available: dedupe, trim-tool-results, strip-base64, prune-history
|
|
42
|
+
Default: dedupe, trim-tool-results, strip-base64 (lossless-ish set)
|
|
43
|
+
--keep-recent <n> (optimize) Messages at the tail to leave untouched (default 6)
|
|
44
|
+
--max-tool-tokens <n> (optimize) Token budget for trimmed tool results (default 300)
|
|
45
|
+
--port <n> (proxy) Port to listen on (default 8787)
|
|
46
|
+
--upstream-anthropic <url> (proxy) Override Anthropic upstream (testing)
|
|
47
|
+
--upstream-openai <url> (proxy) Override OpenAI upstream (testing)
|
|
48
|
+
-h, --help Show this help
|
|
49
|
+
|
|
50
|
+
Examples:
|
|
51
|
+
context-doctor analyze chat.json --model claude-sonnet-5
|
|
52
|
+
context-doctor optimize chat.json --strategy dedupe --strategy prune-history --out slim.json
|
|
53
|
+
context-doctor proxy --port 8787
|
|
54
|
+
then: export ANTHROPIC_BASE_URL=http://localhost:8787
|
|
55
|
+
export OPENAI_BASE_URL=http://localhost:8787/v1
|
|
56
|
+
`;
|
|
57
|
+
function parseArgs(argv) {
|
|
58
|
+
const args = { json: false, strategies: [], list: false };
|
|
59
|
+
const positional = [];
|
|
60
|
+
for (let i = 0; i < argv.length; i++) {
|
|
61
|
+
const a = argv[i];
|
|
62
|
+
switch (a) {
|
|
63
|
+
case "-h":
|
|
64
|
+
case "--help":
|
|
65
|
+
console.log(HELP);
|
|
66
|
+
process.exit(0);
|
|
67
|
+
case "--json":
|
|
68
|
+
args.json = true;
|
|
69
|
+
break;
|
|
70
|
+
case "--list":
|
|
71
|
+
args.list = true;
|
|
72
|
+
break;
|
|
73
|
+
case "--model":
|
|
74
|
+
args.model = argv[++i];
|
|
75
|
+
break;
|
|
76
|
+
case "--out":
|
|
77
|
+
args.out = argv[++i];
|
|
78
|
+
break;
|
|
79
|
+
case "--strategy":
|
|
80
|
+
args.strategies.push(argv[++i]);
|
|
81
|
+
break;
|
|
82
|
+
case "--keep-recent":
|
|
83
|
+
args.keepRecent = Number(argv[++i]);
|
|
84
|
+
break;
|
|
85
|
+
case "--max-tool-tokens":
|
|
86
|
+
args.maxToolTokens = Number(argv[++i]);
|
|
87
|
+
break;
|
|
88
|
+
case "--port":
|
|
89
|
+
args.port = Number(argv[++i]);
|
|
90
|
+
break;
|
|
91
|
+
case "--upstream-anthropic":
|
|
92
|
+
args.upstreamAnthropic = argv[++i];
|
|
93
|
+
break;
|
|
94
|
+
case "--upstream-openai":
|
|
95
|
+
args.upstreamOpenai = argv[++i];
|
|
96
|
+
break;
|
|
97
|
+
default: positional.push(a);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
args.command = positional[0];
|
|
101
|
+
args.file = positional[1];
|
|
102
|
+
return args;
|
|
103
|
+
}
|
|
104
|
+
function readInput(file) {
|
|
105
|
+
if (file === "-")
|
|
106
|
+
return readFileSync(0, "utf8");
|
|
107
|
+
return readFileSync(file, "utf8");
|
|
108
|
+
}
|
|
109
|
+
function main() {
|
|
110
|
+
const args = parseArgs(process.argv.slice(2));
|
|
111
|
+
if (args.command === "session") {
|
|
112
|
+
if (args.list) {
|
|
113
|
+
const sessions = listSessions();
|
|
114
|
+
if (sessions.length === 0) {
|
|
115
|
+
console.log("No Claude Code sessions found under ~/.claude/projects.");
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
for (const s of sessions) {
|
|
119
|
+
console.log(`${s.modifiedAt.toISOString().slice(0, 16)} ${(s.sizeBytes / 1024).toFixed(0).padStart(6)}KB ${s.path}`);
|
|
120
|
+
}
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
const path = args.file ?? listSessions(1)[0]?.path;
|
|
124
|
+
if (!path) {
|
|
125
|
+
console.error("No session transcript found. Pass a .jsonl path or run inside a machine with Claude Code sessions.");
|
|
126
|
+
process.exit(1);
|
|
127
|
+
}
|
|
128
|
+
let parsed;
|
|
129
|
+
try {
|
|
130
|
+
parsed = parseSessionFile(path);
|
|
131
|
+
}
|
|
132
|
+
catch (e) {
|
|
133
|
+
console.error(`Could not read session: ${e.message}`);
|
|
134
|
+
process.exit(1);
|
|
135
|
+
}
|
|
136
|
+
const profile = profileConversation(parseConversation(parsed.conversationJson), args.model ?? parsed.model);
|
|
137
|
+
if (args.json) {
|
|
138
|
+
console.log(JSON.stringify({ session: { path: parsed.path, title: parsed.title }, profile }, null, 2));
|
|
139
|
+
}
|
|
140
|
+
else {
|
|
141
|
+
console.log(`Session: ${parsed.title ?? "(untitled)"}\nFile: ${parsed.path}\n`);
|
|
142
|
+
console.log(renderProfile(profile));
|
|
143
|
+
}
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
if (args.command === "install") {
|
|
147
|
+
runInstall();
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
150
|
+
if (args.command === "uninstall") {
|
|
151
|
+
runUninstall();
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
if (args.command === "proxy") {
|
|
155
|
+
startProxy({
|
|
156
|
+
port: args.port,
|
|
157
|
+
anthropicUpstream: args.upstreamAnthropic,
|
|
158
|
+
openaiUpstream: args.upstreamOpenai,
|
|
159
|
+
strategies: args.strategies.length > 0 ? args.strategies : undefined,
|
|
160
|
+
keepRecent: args.keepRecent,
|
|
161
|
+
maxToolResultTokens: args.maxToolTokens,
|
|
162
|
+
});
|
|
163
|
+
return; // server keeps the process alive
|
|
164
|
+
}
|
|
165
|
+
if (!args.command || !args.file) {
|
|
166
|
+
console.log(HELP);
|
|
167
|
+
process.exit(args.command ? 1 : 0);
|
|
168
|
+
}
|
|
169
|
+
let input;
|
|
170
|
+
try {
|
|
171
|
+
input = readInput(args.file);
|
|
172
|
+
}
|
|
173
|
+
catch (e) {
|
|
174
|
+
console.error(`Could not read ${args.file}: ${e.message}`);
|
|
175
|
+
process.exit(1);
|
|
176
|
+
}
|
|
177
|
+
if (args.command === "analyze") {
|
|
178
|
+
const profile = profileConversation(parseConversation(input), args.model);
|
|
179
|
+
console.log(args.json ? JSON.stringify(profile, null, 2) : renderProfile(profile));
|
|
180
|
+
return;
|
|
181
|
+
}
|
|
182
|
+
if (args.command === "optimize") {
|
|
183
|
+
let result;
|
|
184
|
+
try {
|
|
185
|
+
result = optimizeConversation(input, {
|
|
186
|
+
strategies: args.strategies.length > 0 ? args.strategies : undefined,
|
|
187
|
+
keepRecent: args.keepRecent,
|
|
188
|
+
maxToolResultTokens: args.maxToolTokens,
|
|
189
|
+
});
|
|
190
|
+
}
|
|
191
|
+
catch (e) {
|
|
192
|
+
console.error(e.message);
|
|
193
|
+
process.exit(1);
|
|
194
|
+
}
|
|
195
|
+
const output = JSON.stringify(result.conversation, null, 2);
|
|
196
|
+
if (args.out) {
|
|
197
|
+
writeFileSync(args.out, output);
|
|
198
|
+
}
|
|
199
|
+
else if (args.json) {
|
|
200
|
+
console.log(JSON.stringify(result, null, 2));
|
|
201
|
+
}
|
|
202
|
+
else {
|
|
203
|
+
console.log(output);
|
|
204
|
+
}
|
|
205
|
+
const saved = result.tokensBefore - result.tokensAfter;
|
|
206
|
+
const pct = result.tokensBefore > 0 ? Math.round((saved / result.tokensBefore) * 100) : 0;
|
|
207
|
+
console.error(`\ncontext-doctor: ${formatTokens(result.tokensBefore)} โ ${formatTokens(result.tokensAfter)} tokens ` +
|
|
208
|
+
`(saved ~${formatTokens(saved)}, ${pct}%) via ${result.applied.length} change(s)` +
|
|
209
|
+
(args.out ? ` โ written to ${args.out}` : ""));
|
|
210
|
+
return;
|
|
211
|
+
}
|
|
212
|
+
console.error(`Unknown command: ${args.command}\n`);
|
|
213
|
+
console.log(HELP);
|
|
214
|
+
process.exit(1);
|
|
215
|
+
}
|
|
216
|
+
main();
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* context-doctor โ profile and optimize LLM context windows.
|
|
3
|
+
* Library entry point; see cli.ts for the CLI and mcp.ts for the MCP server.
|
|
4
|
+
*/
|
|
5
|
+
export { parseConversation } from "./parse.js";
|
|
6
|
+
export type { NormalizedConversation, NormalizedMessage, MessageKind } from "./parse.js";
|
|
7
|
+
export { profileConversation } from "./profile.js";
|
|
8
|
+
export type { ContextProfile, Finding, FindingId, MessageProfile, Category } from "./profile.js";
|
|
9
|
+
export { optimizeConversation } from "./optimize.js";
|
|
10
|
+
export type { OptimizeOptions, OptimizeResult, AppliedChange, StrategyId } from "./optimize.js";
|
|
11
|
+
export { renderProfile } from "./report.js";
|
|
12
|
+
export { startProxy } from "./proxy.js";
|
|
13
|
+
export type { ProxyOptions, ProxyStats } from "./proxy.js";
|
|
14
|
+
export { listSessions, parseSessionFile } from "./session.js";
|
|
15
|
+
export type { SessionInfo, ParsedSession } from "./session.js";
|
|
16
|
+
export { pricingFor, inputCostUsd, estimatedTtftSeconds, formatUsd } from "./pricing.js";
|
|
17
|
+
export type { ModelPricing } from "./pricing.js";
|
|
18
|
+
export { estimateTokens, contextWindowFor, providerFor, formatTokens } from "./tokens.js";
|
|
19
|
+
export type { Provider } from "./tokens.js";
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* context-doctor โ profile and optimize LLM context windows.
|
|
3
|
+
* Library entry point; see cli.ts for the CLI and mcp.ts for the MCP server.
|
|
4
|
+
*/
|
|
5
|
+
export { parseConversation } from "./parse.js";
|
|
6
|
+
export { profileConversation } from "./profile.js";
|
|
7
|
+
export { optimizeConversation } from "./optimize.js";
|
|
8
|
+
export { renderProfile } from "./report.js";
|
|
9
|
+
export { startProxy } from "./proxy.js";
|
|
10
|
+
export { listSessions, parseSessionFile } from "./session.js";
|
|
11
|
+
export { pricingFor, inputCostUsd, estimatedTtftSeconds, formatUsd } from "./pricing.js";
|
|
12
|
+
export { estimateTokens, contextWindowFor, providerFor, formatTokens } from "./tokens.js";
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* One-command setup: `context-doctor install`
|
|
3
|
+
*
|
|
4
|
+
* Detects the AI apps present on this machine and wires the context-doctor
|
|
5
|
+
* MCP server into each, plus installs the Agent Skill for Claude Code.
|
|
6
|
+
* Every config edit is a careful JSON merge with a .backup file written first.
|
|
7
|
+
* `context-doctor uninstall` reverses it.
|
|
8
|
+
*/
|
|
9
|
+
export declare function runInstall(): void;
|
|
10
|
+
export declare function runUninstall(): void;
|
package/dist/install.js
ADDED
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* One-command setup: `context-doctor install`
|
|
3
|
+
*
|
|
4
|
+
* Detects the AI apps present on this machine and wires the context-doctor
|
|
5
|
+
* MCP server into each, plus installs the Agent Skill for Claude Code.
|
|
6
|
+
* Every config edit is a careful JSON merge with a .backup file written first.
|
|
7
|
+
* `context-doctor uninstall` reverses it.
|
|
8
|
+
*/
|
|
9
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync, copyFileSync, rmSync } from "node:fs";
|
|
10
|
+
import { homedir, platform } from "node:os";
|
|
11
|
+
import { dirname, join } from "node:path";
|
|
12
|
+
import { fileURLToPath } from "node:url";
|
|
13
|
+
function claudeDesktopConfigPath() {
|
|
14
|
+
switch (platform()) {
|
|
15
|
+
case "darwin": return join(homedir(), "Library", "Application Support", "Claude", "claude_desktop_config.json");
|
|
16
|
+
case "win32": return join(process.env.APPDATA ?? join(homedir(), "AppData", "Roaming"), "Claude", "claude_desktop_config.json");
|
|
17
|
+
default: return join(homedir(), ".config", "Claude", "claude_desktop_config.json");
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
function targets() {
|
|
21
|
+
const desktop = claudeDesktopConfigPath();
|
|
22
|
+
const claudeCode = join(homedir(), ".claude.json");
|
|
23
|
+
const cursor = join(homedir(), ".cursor", "mcp.json");
|
|
24
|
+
return [
|
|
25
|
+
{ name: "Claude Desktop", configPath: desktop, detect: () => existsSync(dirname(desktop)) },
|
|
26
|
+
{ name: "Claude Code", configPath: claudeCode, detect: () => existsSync(claudeCode) || existsSync(join(homedir(), ".claude")) },
|
|
27
|
+
{ name: "Cursor", configPath: cursor, detect: () => existsSync(join(homedir(), ".cursor")) },
|
|
28
|
+
];
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* The server command to write into configs. When running from a published
|
|
32
|
+
* install, npx keeps it auto-updating; from a local checkout, point at the
|
|
33
|
+
* built file directly so it works before the package is on npm.
|
|
34
|
+
*/
|
|
35
|
+
function serverEntry() {
|
|
36
|
+
const selfDir = dirname(fileURLToPath(import.meta.url));
|
|
37
|
+
const localMcp = join(selfDir, "mcp.js");
|
|
38
|
+
const runningFromNpx = (process.env.npm_execpath ?? "").includes("npx") || selfDir.includes("_npx");
|
|
39
|
+
if (!runningFromNpx && existsSync(localMcp)) {
|
|
40
|
+
return { command: process.execPath, args: [localMcp] };
|
|
41
|
+
}
|
|
42
|
+
return { command: "npx", args: ["-y", "context-doctor-mcp"] };
|
|
43
|
+
}
|
|
44
|
+
function readJson(path) {
|
|
45
|
+
if (!existsSync(path))
|
|
46
|
+
return {};
|
|
47
|
+
try {
|
|
48
|
+
return JSON.parse(readFileSync(path, "utf8"));
|
|
49
|
+
}
|
|
50
|
+
catch (e) {
|
|
51
|
+
throw new Error(`${path} exists but is not valid JSON โ fix or remove it first (${e.message})`);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
function writeJsonWithBackup(path, data) {
|
|
55
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
56
|
+
if (existsSync(path))
|
|
57
|
+
copyFileSync(path, path + ".context-doctor.backup");
|
|
58
|
+
writeFileSync(path, JSON.stringify(data, null, 2));
|
|
59
|
+
}
|
|
60
|
+
function installSkill() {
|
|
61
|
+
const selfDir = dirname(fileURLToPath(import.meta.url));
|
|
62
|
+
// dist/install.js โ package root is one level up; skills/ ships in the package.
|
|
63
|
+
const skillSource = join(selfDir, "..", "skills", "context-doctor", "SKILL.md");
|
|
64
|
+
if (!existsSync(skillSource))
|
|
65
|
+
return null;
|
|
66
|
+
const skillDest = join(homedir(), ".claude", "skills", "context-doctor");
|
|
67
|
+
mkdirSync(skillDest, { recursive: true });
|
|
68
|
+
copyFileSync(skillSource, join(skillDest, "SKILL.md"));
|
|
69
|
+
return join(skillDest, "SKILL.md");
|
|
70
|
+
}
|
|
71
|
+
export function runInstall() {
|
|
72
|
+
const entry = serverEntry();
|
|
73
|
+
const found = targets().filter((t) => t.detect());
|
|
74
|
+
if (found.length === 0) {
|
|
75
|
+
console.log("No supported AI apps detected (Claude Desktop, Claude Code, Cursor).");
|
|
76
|
+
console.log("Manual setup โ add to your app's MCP config:");
|
|
77
|
+
console.log(JSON.stringify({ mcpServers: { "context-doctor": entry } }, null, 2));
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
for (const target of found) {
|
|
81
|
+
try {
|
|
82
|
+
const config = readJson(target.configPath);
|
|
83
|
+
config.mcpServers = config.mcpServers ?? {};
|
|
84
|
+
config.mcpServers["context-doctor"] = entry;
|
|
85
|
+
writeJsonWithBackup(target.configPath, config);
|
|
86
|
+
console.log(`โ ${target.name}: MCP server added (${target.configPath})`);
|
|
87
|
+
}
|
|
88
|
+
catch (e) {
|
|
89
|
+
console.error(`โ ${target.name}: ${e.message}`);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
const skillPath = installSkill();
|
|
93
|
+
if (skillPath)
|
|
94
|
+
console.log(`โ Agent Skill installed for Claude Code (${skillPath})`);
|
|
95
|
+
console.log("\nDone. Restart the apps to pick up the new tools, then try:");
|
|
96
|
+
console.log(' "What\'s eating my context?" โ or paste a conversation and ask for a profile.');
|
|
97
|
+
}
|
|
98
|
+
export function runUninstall() {
|
|
99
|
+
for (const target of targets().filter((t) => t.detect())) {
|
|
100
|
+
try {
|
|
101
|
+
if (!existsSync(target.configPath))
|
|
102
|
+
continue;
|
|
103
|
+
const config = readJson(target.configPath);
|
|
104
|
+
if (config.mcpServers?.["context-doctor"]) {
|
|
105
|
+
delete config.mcpServers["context-doctor"];
|
|
106
|
+
writeJsonWithBackup(target.configPath, config);
|
|
107
|
+
console.log(`โ ${target.name}: MCP server removed`);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
catch (e) {
|
|
111
|
+
console.error(`โ ${target.name}: ${e.message}`);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
const skillDir = join(homedir(), ".claude", "skills", "context-doctor");
|
|
115
|
+
if (existsSync(skillDir)) {
|
|
116
|
+
rmSync(skillDir, { recursive: true });
|
|
117
|
+
console.log("โ Agent Skill removed");
|
|
118
|
+
}
|
|
119
|
+
console.log("Done.");
|
|
120
|
+
}
|
package/dist/mcp.d.ts
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* context-doctor MCP server (stdio).
|
|
4
|
+
*
|
|
5
|
+
* Plug into Claude Desktop, ChatGPT desktop (developer mode), Cursor, or any
|
|
6
|
+
* MCP client:
|
|
7
|
+
*
|
|
8
|
+
* { "mcpServers": { "context-doctor": { "command": "npx", "args": ["-y", "context-doctor-mcp"] } } }
|
|
9
|
+
*
|
|
10
|
+
* Tools:
|
|
11
|
+
* profile_context โ analyze a conversation/prompt, report token breakdown + findings
|
|
12
|
+
* optimize_context โ apply safe strategies, return the slimmed conversation
|
|
13
|
+
* context_best_practices โ curated checklist for a given provider/use case
|
|
14
|
+
*/
|
|
15
|
+
export {};
|