context-doctor 0.9.0 → 0.10.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 +29 -2
- package/dist/cache.d.ts +30 -0
- package/dist/cache.js +97 -0
- package/dist/cli.js +25 -4
- package/dist/mcp.js +1 -1
- package/dist/profile.d.ts +1 -1
- package/dist/profile.js +52 -0
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -88,9 +88,10 @@ Practical upshot: a developer who only wants cheaper, faster API calls never tou
|
|
|
88
88
|
| Command | What it does |
|
|
89
89
|
|---|---|
|
|
90
90
|
| `context-doctor install` / `uninstall` | Wire (or remove) everything: MCP for Claude Desktop/Code/Cursor, the Agent Skill, the every-prompt hook |
|
|
91
|
-
| `context-doctor analyze <file>` | Profile a conversation: token breakdown, findings, cost + latency estimates |
|
|
91
|
+
| `context-doctor analyze <file>` | Profile a conversation: token breakdown, findings, cost + latency estimates. `--fail-over-budget` exits 1 on a breach, for CI |
|
|
92
92
|
| `context-doctor optimize <file>` | Apply the safe fixes; `--strategy prune-history` for consented lossy compaction |
|
|
93
|
-
| `context-doctor session [file]` | Profile a Claude Code session
|
|
93
|
+
| `context-doctor session [file]` | Profile a Claude Code session: live context, findings, **measured tokens and prompt-cache economics**. Also reads ChatGPT data exports (`conversations.json`) |
|
|
94
|
+
| `context-doctor cursor [--list]` | Profile a chat from Cursor's local history (both storage formats) |
|
|
94
95
|
| `context-doctor report` | Machine-wide impact report: exact proxy savings, hook activity, recoverable waste in recent sessions |
|
|
95
96
|
| `context-doctor proxy` | Always-on local proxy that optimizes every Anthropic/OpenAI API request in flight (`/stats` for cumulative savings) |
|
|
96
97
|
| `context-doctor watch [file]` | Live monitor of a growing session/agent trace: token/cost line per change, findings as they appear |
|
|
@@ -245,6 +246,8 @@ const { conversation, tokensBefore, tokensAfter } = optimizeConversation(chatJso
|
|
|
245
246
|
- **Oversized tool results** — the #1 context killer in agent loops
|
|
246
247
|
- **Duplicate content** — the same doc/result pasted twice
|
|
247
248
|
- **Near-duplicates** — the same doc re-pasted with different surrounding words (shingle similarity, ≥60%)
|
|
249
|
+
- **Repeated file reads** — the same file pulled in three or more times, every copy still in context
|
|
250
|
+
- **Retained error output** — stack traces and failed commands kept verbatim long after the fix landed
|
|
248
251
|
- **Repeated identical tool calls** — a signal your agent forgot earlier results
|
|
249
252
|
- **Base64 / binary blobs** in text content
|
|
250
253
|
- **Long history** past the point where models track the middle
|
|
@@ -296,6 +299,26 @@ Drop a `.contextdoctorrc` in a project (or your home directory) and context-doct
|
|
|
296
299
|
|
|
297
300
|
The nearest file wins (walking up from the working directory, then `~`). `analyze` and `session` print a budget verdict, the every-prompt hook uses `maxTokens` as its warning threshold and names the breach to the model, and `optimize`/`proxy` pick up the defaults when you do not pass flags.
|
|
298
301
|
|
|
302
|
+
## Prompt-cache economics (Claude Code sessions)
|
|
303
|
+
|
|
304
|
+
Caching is the largest lever on LLM cost, and transcripts record exactly how it went — so `session` reports it as fact rather than estimate:
|
|
305
|
+
|
|
306
|
+
```
|
|
307
|
+
Prompt cache: 95.6% of input served from cache across 1117 requests
|
|
308
|
+
read 558.6M · written 25.5M · uncached 2k
|
|
309
|
+
input cost $438.88 — caching saved $2481.89 against $2920.76 uncached (list prices)
|
|
310
|
+
```
|
|
311
|
+
|
|
312
|
+
A cache read bills at ~10% of input while a write bills at ~125%, so a session that keeps invalidating its prefix can cost *more* than one with no caching at all. context-doctor warns on the two failure modes: a **low hit rate** (something early in the prompt changes every request) and **cache churn** (writes rivalling reads).
|
|
313
|
+
|
|
314
|
+
## Enforce a budget in CI
|
|
315
|
+
|
|
316
|
+
```bash
|
|
317
|
+
npx context-doctor analyze conversation.json --fail-over-budget
|
|
318
|
+
```
|
|
319
|
+
|
|
320
|
+
Exits 1 when the `.contextdoctorrc` budget is breached, so a pull request can be gated on context size the same way it is gated on tests.
|
|
321
|
+
|
|
299
322
|
## Performance: what context-doctor itself costs
|
|
300
323
|
|
|
301
324
|
A tool that promises speed must be near-free. Measured overhead per touchpoint:
|
|
@@ -344,6 +367,10 @@ Known gotcha: if `npm publish` fails with **`404 Not Found - PUT …/context-doc
|
|
|
344
367
|
|
|
345
368
|
Also keep the MCP server version in `src/mcp.ts` in sync with `package.json`, and remember `dist/` is committed — run `npm run build` before committing so the CI dist-sync check passes.
|
|
346
369
|
|
|
370
|
+
## Contributing
|
|
371
|
+
|
|
372
|
+
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).
|
|
373
|
+
|
|
347
374
|
## License
|
|
348
375
|
|
|
349
376
|
MIT © [gAI Ventures](https://gai.ventures)
|
package/dist/cache.d.ts
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Prompt-cache analysis for Claude Code sessions.
|
|
3
|
+
*
|
|
4
|
+
* Transcripts record what the API actually charged per request — including
|
|
5
|
+
* cache reads and cache writes — so cache behaviour can be reported as fact
|
|
6
|
+
* rather than estimated. This is usually the largest single lever on cost:
|
|
7
|
+
* a cached read bills at ~10% of input, while a cache write bills at ~125%,
|
|
8
|
+
* so a session that keeps invalidating its prefix pays more than one with no
|
|
9
|
+
* caching at all.
|
|
10
|
+
*/
|
|
11
|
+
export interface CacheUsage {
|
|
12
|
+
requests: number;
|
|
13
|
+
cacheReadTokens: number;
|
|
14
|
+
cacheWriteTokens: number;
|
|
15
|
+
uncachedTokens: number;
|
|
16
|
+
/** Share of input tokens served from cache (0-1). */
|
|
17
|
+
hitRate: number;
|
|
18
|
+
model?: string;
|
|
19
|
+
/** What the input actually cost, at list prices. */
|
|
20
|
+
paidUsd?: number;
|
|
21
|
+
/** What the same input would have cost with no caching at all. */
|
|
22
|
+
uncachedUsd?: number;
|
|
23
|
+
/** paidUsd vs uncachedUsd — positive means caching is paying off. */
|
|
24
|
+
savedUsd?: number;
|
|
25
|
+
/** USD spent on cache writes; high values mean the prefix keeps changing. */
|
|
26
|
+
writeUsd?: number;
|
|
27
|
+
}
|
|
28
|
+
export declare function analyzeCacheUsage(transcriptPath: string): CacheUsage | null;
|
|
29
|
+
/** One-paragraph verdict for humans, or null when there is nothing to say. */
|
|
30
|
+
export declare function renderCacheReport(usage: CacheUsage | null): string | null;
|
package/dist/cache.js
ADDED
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Prompt-cache analysis for Claude Code sessions.
|
|
3
|
+
*
|
|
4
|
+
* Transcripts record what the API actually charged per request — including
|
|
5
|
+
* cache reads and cache writes — so cache behaviour can be reported as fact
|
|
6
|
+
* rather than estimated. This is usually the largest single lever on cost:
|
|
7
|
+
* a cached read bills at ~10% of input, while a cache write bills at ~125%,
|
|
8
|
+
* so a session that keeps invalidating its prefix pays more than one with no
|
|
9
|
+
* caching at all.
|
|
10
|
+
*/
|
|
11
|
+
import { readFileSync } from "node:fs";
|
|
12
|
+
import { pricingFor } from "./pricing.js";
|
|
13
|
+
/** Cache pricing multipliers relative to the base input rate. */
|
|
14
|
+
const CACHE_WRITE_MULTIPLIER = 1.25;
|
|
15
|
+
export function analyzeCacheUsage(transcriptPath) {
|
|
16
|
+
let raw;
|
|
17
|
+
try {
|
|
18
|
+
raw = readFileSync(transcriptPath, "utf8");
|
|
19
|
+
}
|
|
20
|
+
catch {
|
|
21
|
+
return null;
|
|
22
|
+
}
|
|
23
|
+
let requests = 0;
|
|
24
|
+
let cacheReadTokens = 0;
|
|
25
|
+
let cacheWriteTokens = 0;
|
|
26
|
+
let uncachedTokens = 0;
|
|
27
|
+
let model;
|
|
28
|
+
for (const line of raw.split("\n")) {
|
|
29
|
+
if (!line.trim())
|
|
30
|
+
continue;
|
|
31
|
+
let entry;
|
|
32
|
+
try {
|
|
33
|
+
entry = JSON.parse(line);
|
|
34
|
+
}
|
|
35
|
+
catch {
|
|
36
|
+
continue;
|
|
37
|
+
}
|
|
38
|
+
if (entry.type !== "assistant" || !entry.message)
|
|
39
|
+
continue;
|
|
40
|
+
const usage = entry.message.usage;
|
|
41
|
+
if (!usage)
|
|
42
|
+
continue;
|
|
43
|
+
requests++;
|
|
44
|
+
cacheReadTokens += usage.cache_read_input_tokens ?? 0;
|
|
45
|
+
cacheWriteTokens += usage.cache_creation_input_tokens ?? 0;
|
|
46
|
+
uncachedTokens += usage.input_tokens ?? 0;
|
|
47
|
+
if (typeof entry.message.model === "string")
|
|
48
|
+
model = entry.message.model;
|
|
49
|
+
}
|
|
50
|
+
if (requests === 0)
|
|
51
|
+
return null;
|
|
52
|
+
const totalInput = cacheReadTokens + cacheWriteTokens + uncachedTokens;
|
|
53
|
+
const usage = {
|
|
54
|
+
requests,
|
|
55
|
+
cacheReadTokens,
|
|
56
|
+
cacheWriteTokens,
|
|
57
|
+
uncachedTokens,
|
|
58
|
+
hitRate: totalInput > 0 ? cacheReadTokens / totalInput : 0,
|
|
59
|
+
model,
|
|
60
|
+
};
|
|
61
|
+
const pricing = pricingFor(model);
|
|
62
|
+
if (pricing) {
|
|
63
|
+
const perM = (tokens, rate) => (tokens / 1_000_000) * rate;
|
|
64
|
+
const readUsd = perM(cacheReadTokens, pricing.cacheReadPerM);
|
|
65
|
+
const writeUsd = perM(cacheWriteTokens, pricing.inputPerM * CACHE_WRITE_MULTIPLIER);
|
|
66
|
+
const freshUsd = perM(uncachedTokens, pricing.inputPerM);
|
|
67
|
+
usage.paidUsd = readUsd + writeUsd + freshUsd;
|
|
68
|
+
usage.uncachedUsd = perM(totalInput, pricing.inputPerM);
|
|
69
|
+
usage.savedUsd = usage.uncachedUsd - usage.paidUsd;
|
|
70
|
+
usage.writeUsd = writeUsd;
|
|
71
|
+
}
|
|
72
|
+
return usage;
|
|
73
|
+
}
|
|
74
|
+
/** One-paragraph verdict for humans, or null when there is nothing to say. */
|
|
75
|
+
export function renderCacheReport(usage) {
|
|
76
|
+
if (!usage)
|
|
77
|
+
return null;
|
|
78
|
+
const pct = (usage.hitRate * 100).toFixed(1);
|
|
79
|
+
const lines = [];
|
|
80
|
+
const fmt = (n) => (n >= 1 ? `$${n.toFixed(2)}` : `$${n.toFixed(3)}`);
|
|
81
|
+
const tok = (n) => n >= 1e6 ? `${(n / 1e6).toFixed(1)}M` : n >= 1e3 ? `${Math.round(n / 1e3)}k` : String(n);
|
|
82
|
+
lines.push(`Prompt cache: ${pct}% of input served from cache across ${usage.requests} requests`);
|
|
83
|
+
lines.push(` read ${tok(usage.cacheReadTokens)} · written ${tok(usage.cacheWriteTokens)} · uncached ${tok(usage.uncachedTokens)}`);
|
|
84
|
+
if (usage.paidUsd !== undefined && usage.uncachedUsd !== undefined && usage.savedUsd !== undefined) {
|
|
85
|
+
lines.push(` input cost ${fmt(usage.paidUsd)} — caching saved ${fmt(usage.savedUsd)} against ${fmt(usage.uncachedUsd)} uncached (list prices)`);
|
|
86
|
+
}
|
|
87
|
+
// A cache write costs 1.25x input while a read costs 0.1x, so writes that
|
|
88
|
+
// rival reads mean the prefix keeps changing and caching is losing money.
|
|
89
|
+
const writeShare = usage.cacheReadTokens > 0 ? usage.cacheWriteTokens / usage.cacheReadTokens : Infinity;
|
|
90
|
+
if (usage.hitRate < 0.5 && usage.requests > 5) {
|
|
91
|
+
lines.push(" ⚠ Low hit rate: something early in the prompt changes between requests. Keep the system prompt, tool list and reference docs byte-stable and put volatile content last.");
|
|
92
|
+
}
|
|
93
|
+
else if (writeShare > 0.5) {
|
|
94
|
+
lines.push(" ⚠ Cache churn: writes are large relative to reads, and a write costs 1.25x input against 0.1x for a read. The cached prefix is being rebuilt often.");
|
|
95
|
+
}
|
|
96
|
+
return lines.join("\n");
|
|
97
|
+
}
|
package/dist/cli.js
CHANGED
|
@@ -26,6 +26,7 @@ import { exactTokenCount } from "./exact.js";
|
|
|
26
26
|
import { checkBudget, loadConfig } from "./config.js";
|
|
27
27
|
import { startDashboard } from "./dashboard.js";
|
|
28
28
|
import { listCursorChats, parseCursorChat } from "./cursor.js";
|
|
29
|
+
import { analyzeCacheUsage, renderCacheReport } from "./cache.js";
|
|
29
30
|
const HELP = `context-doctor — profile and optimize LLM context windows
|
|
30
31
|
|
|
31
32
|
Usage:
|
|
@@ -64,6 +65,8 @@ Options:
|
|
|
64
65
|
--exact (analyze) Add an exact token count: Anthropic count-tokens API for
|
|
65
66
|
Claude models (needs ANTHROPIC_API_KEY), tiktoken for GPT (if installed)
|
|
66
67
|
--json Machine-readable output
|
|
68
|
+
--fail-over-budget (analyze/session) Exit 1 when the .contextdoctorrc budget is
|
|
69
|
+
exceeded — lets CI gate a pull request on context size
|
|
67
70
|
--out <file> (optimize) Write result to file instead of stdout
|
|
68
71
|
--strategy <id> (optimize) Strategy to run; repeatable.
|
|
69
72
|
Available: dedupe, trim-tool-results, strip-base64, prune-history
|
|
@@ -86,7 +89,7 @@ Examples:
|
|
|
86
89
|
export OPENAI_BASE_URL=http://localhost:8787/v1
|
|
87
90
|
`;
|
|
88
91
|
function parseArgs(argv) {
|
|
89
|
-
const args = { json: false, strategies: [], list: false, exact: false };
|
|
92
|
+
const args = { json: false, strategies: [], list: false, exact: false, failOverBudget: false };
|
|
90
93
|
const positional = [];
|
|
91
94
|
for (let i = 0; i < argv.length; i++) {
|
|
92
95
|
const a = argv[i];
|
|
@@ -104,6 +107,9 @@ function parseArgs(argv) {
|
|
|
104
107
|
case "--exact":
|
|
105
108
|
args.exact = true;
|
|
106
109
|
break;
|
|
110
|
+
case "--fail-over-budget":
|
|
111
|
+
args.failOverBudget = true;
|
|
112
|
+
break;
|
|
107
113
|
case "--model":
|
|
108
114
|
args.model = argv[++i];
|
|
109
115
|
break;
|
|
@@ -148,7 +154,7 @@ function parseArgs(argv) {
|
|
|
148
154
|
function printBudgetStatus(profile, loaded) {
|
|
149
155
|
const budget = loaded.config.budget;
|
|
150
156
|
if (!budget || !loaded.path)
|
|
151
|
-
return;
|
|
157
|
+
return false;
|
|
152
158
|
const verdict = checkBudget(budget, profile);
|
|
153
159
|
console.log("");
|
|
154
160
|
if (verdict.overBudget) {
|
|
@@ -159,6 +165,14 @@ function printBudgetStatus(profile, loaded) {
|
|
|
159
165
|
else {
|
|
160
166
|
console.log(`Within budget (${loaded.path}).`);
|
|
161
167
|
}
|
|
168
|
+
return verdict.overBudget;
|
|
169
|
+
}
|
|
170
|
+
/** Exit 1 when the caller asked CI to fail on a breach. */
|
|
171
|
+
function applyBudgetGate(overBudget, failOverBudget) {
|
|
172
|
+
if (overBudget && failOverBudget) {
|
|
173
|
+
console.error("context-doctor: over budget (--fail-over-budget)");
|
|
174
|
+
process.exitCode = 1;
|
|
175
|
+
}
|
|
162
176
|
}
|
|
163
177
|
function readInput(file) {
|
|
164
178
|
if (file === "-")
|
|
@@ -269,7 +283,12 @@ function main() {
|
|
|
269
283
|
"transcript does not record — so it is larger than the breakdown above, which covers\n" +
|
|
270
284
|
"conversation messages only. Findings and savings apply to the messages.");
|
|
271
285
|
}
|
|
272
|
-
|
|
286
|
+
const cache = renderCacheReport(analyzeCacheUsage(path));
|
|
287
|
+
if (cache) {
|
|
288
|
+
console.log("");
|
|
289
|
+
console.log(cache);
|
|
290
|
+
}
|
|
291
|
+
applyBudgetGate(printBudgetStatus(profile, loadConfig(process.cwd(), (m) => console.error(`context-doctor: ${m}`))), args.failOverBudget);
|
|
273
292
|
}
|
|
274
293
|
return;
|
|
275
294
|
}
|
|
@@ -322,7 +341,9 @@ function main() {
|
|
|
322
341
|
const profile = profileConversation(parseConversation(input), args.model ?? loaded.config.model);
|
|
323
342
|
console.log(args.json ? JSON.stringify(profile, null, 2) : renderProfile(profile));
|
|
324
343
|
if (!args.json)
|
|
325
|
-
printBudgetStatus(profile, loaded);
|
|
344
|
+
applyBudgetGate(printBudgetStatus(profile, loaded), args.failOverBudget);
|
|
345
|
+
else
|
|
346
|
+
applyBudgetGate(checkBudget(loaded.config.budget, profile).overBudget, args.failOverBudget);
|
|
326
347
|
if (args.exact) {
|
|
327
348
|
void exactTokenCount(input, args.model).then((exact) => {
|
|
328
349
|
if (exact.tokens !== undefined) {
|
package/dist/mcp.js
CHANGED
|
@@ -37,7 +37,7 @@ const STRATEGY_IDS = ["dedupe", "trim-tool-results", "strip-base64", "prune-hist
|
|
|
37
37
|
* recommended pattern.
|
|
38
38
|
*/
|
|
39
39
|
function createServer() {
|
|
40
|
-
const server = new McpServer({ name: "context-doctor", version: "0.
|
|
40
|
+
const server = new McpServer({ name: "context-doctor", version: "0.10.0" }, { instructions: SERVER_INSTRUCTIONS });
|
|
41
41
|
server.tool("profile_context", "Profile an LLM conversation or prompt: token breakdown by category, largest messages, and actionable findings about wasted context (duplicates, oversized tool results, base64 blobs, cache-unfriendly ordering). Accepts OpenAI/Anthropic conversation JSON or raw text. Call this immediately whenever the user asks about token usage, context size, LLM cost, or latency — and proactively offer it once a conversation grows long or accumulates large pasted content.", {
|
|
42
42
|
conversation: z.string().describe("Conversation JSON (OpenAI or Anthropic format, or bare message array) or raw prompt text"),
|
|
43
43
|
model: z.string().optional().describe("Target model name for context-window math, e.g. claude-sonnet-5 or gpt-4o"),
|
package/dist/profile.d.ts
CHANGED
|
@@ -12,7 +12,7 @@ export interface MessageProfile {
|
|
|
12
12
|
preview: string;
|
|
13
13
|
toolName?: string;
|
|
14
14
|
}
|
|
15
|
-
export type FindingId = "large_tool_result" | "duplicate_content" | "near_duplicate" | "repeated_tool_call" | "base64_blob" | "long_history" | "large_system_prompt" | "cache_ordering" | "near_window_limit";
|
|
15
|
+
export type FindingId = "large_tool_result" | "duplicate_content" | "near_duplicate" | "repeated_tool_call" | "repeated_file_read" | "retained_error_output" | "base64_blob" | "long_history" | "large_system_prompt" | "cache_ordering" | "near_window_limit";
|
|
16
16
|
export interface Finding {
|
|
17
17
|
id: FindingId;
|
|
18
18
|
severity: "info" | "warn" | "high";
|
package/dist/profile.js
CHANGED
|
@@ -194,6 +194,58 @@ export function profileConversation(conv, model) {
|
|
|
194
194
|
});
|
|
195
195
|
}
|
|
196
196
|
}
|
|
197
|
+
// -- Same file read again and again -----------------------------------------
|
|
198
|
+
// The dominant waste in agent loops: a file re-read because its earlier
|
|
199
|
+
// contents scrolled out of attention, leaving several full copies in context.
|
|
200
|
+
{
|
|
201
|
+
const readsByPath = new Map();
|
|
202
|
+
for (const p of perMessage) {
|
|
203
|
+
if (p.msg.kind !== "tool_call" || !p.msg.toolCallText)
|
|
204
|
+
continue;
|
|
205
|
+
if (!/read|open|cat|view|get_file/i.test(p.msg.toolName ?? ""))
|
|
206
|
+
continue;
|
|
207
|
+
const match = /"(?:file_path|filePath|path|file)"\s*:\s*"([^"]{3,})"/.exec(p.msg.toolCallText);
|
|
208
|
+
if (!match)
|
|
209
|
+
continue;
|
|
210
|
+
const path = match[1];
|
|
211
|
+
readsByPath.set(path, [...(readsByPath.get(path) ?? []), p.msg.index]);
|
|
212
|
+
}
|
|
213
|
+
for (const [path, indexes] of readsByPath) {
|
|
214
|
+
if (indexes.length < 3)
|
|
215
|
+
continue;
|
|
216
|
+
// Each re-read pulls the file in again; all but the last are recoverable.
|
|
217
|
+
const resultTokens = perMessage
|
|
218
|
+
.filter((p) => p.msg.kind === "tool_result" && indexes.some((i) => p.msg.index === i + 1))
|
|
219
|
+
.reduce((sum, p) => sum + p.tokens, 0);
|
|
220
|
+
const savings = Math.round((resultTokens * (indexes.length - 1)) / indexes.length);
|
|
221
|
+
findings.push({
|
|
222
|
+
id: "repeated_file_read",
|
|
223
|
+
severity: savings > 4000 ? "high" : "warn",
|
|
224
|
+
estSavings: savings,
|
|
225
|
+
message: `${path.split("/").pop()} was read ${indexes.length} times (messages #${indexes.join(", #")}), keeping ${indexes.length} copies in context.`,
|
|
226
|
+
suggestion: "Re-read a file only after it changes; otherwise refer back to the copy already in context.",
|
|
227
|
+
messages: indexes,
|
|
228
|
+
});
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
// -- Failed tool output kept verbatim ---------------------------------------
|
|
232
|
+
// Stack traces and command failures are read once and never again, yet they
|
|
233
|
+
// are often the largest blocks in an agent transcript.
|
|
234
|
+
for (const p of perMessage) {
|
|
235
|
+
if (p.msg.kind !== "tool_result" || p.tokens < 500)
|
|
236
|
+
continue;
|
|
237
|
+
const looksFailed = /Traceback \(most recent call last\)|command not found|npm error|ERR!|\bexit code [1-9]|Exception in thread|FAILED|error TS\d+/i.test(p.msg.text);
|
|
238
|
+
if (!looksFailed)
|
|
239
|
+
continue;
|
|
240
|
+
findings.push({
|
|
241
|
+
id: "retained_error_output",
|
|
242
|
+
severity: "warn",
|
|
243
|
+
estSavings: Math.round(p.tokens * 0.85),
|
|
244
|
+
message: `Message #${p.msg.index} holds ~${p.tokens} tokens of failed tool output (errors, stack trace or non-zero exit).`,
|
|
245
|
+
suggestion: "Keep the one line that identifies the failure and drop the rest — a full trace has no value once the fix is understood.",
|
|
246
|
+
messages: [p.msg.index],
|
|
247
|
+
});
|
|
248
|
+
}
|
|
197
249
|
// -- Base64 / binary blobs ---------------------------------------------------
|
|
198
250
|
for (const p of perMessage) {
|
|
199
251
|
if (BASE64_RE.test(p.msg.text)) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "context-doctor",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.10.0",
|
|
4
4
|
"description": "Profile and optimize LLM context windows. See what's eating your tokens and fix it — works with Claude, GPT, Gemini, and any MCP-capable AI app.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"llm",
|
|
@@ -42,7 +42,7 @@
|
|
|
42
42
|
"build": "tsc && node -e \"const fs=require('fs');['dist/cli.js','dist/mcp.js'].forEach(f=>fs.chmodSync(f,0o755))\"",
|
|
43
43
|
"prepublishOnly": "npm run build",
|
|
44
44
|
"dev": "tsc --watch",
|
|
45
|
-
"test": "npm run build && node --test dist/test/smoke.test.js dist/test/proxy.test.js dist/test/hook.test.js dist/test/mcp-http.test.js dist/test/doctor.test.js dist/test/watch.test.js dist/test/chatgpt-export.test.js dist/test/config.test.js dist/test/dashboard.test.js dist/test/cursor.test.js"
|
|
45
|
+
"test": "npm run build && node --test dist/test/smoke.test.js dist/test/proxy.test.js dist/test/hook.test.js dist/test/mcp-http.test.js dist/test/doctor.test.js dist/test/watch.test.js dist/test/chatgpt-export.test.js dist/test/config.test.js dist/test/dashboard.test.js dist/test/cursor.test.js dist/test/cache.test.js"
|
|
46
46
|
},
|
|
47
47
|
"dependencies": {
|
|
48
48
|
"@modelcontextprotocol/sdk": "^1.0.0",
|