context-doctor 0.22.0 → 0.23.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 +2 -15
- package/dist/autoclear.js +3 -1
- package/dist/cli.js +42 -11
- package/dist/coldresume.js +6 -0
- package/dist/mcp.js +1 -1
- package/dist/proxy.d.ts +1 -1
- package/dist/proxy.js +1 -1
- package/dist/savings.d.ts +5 -0
- package/dist/savings.js +41 -16
- package/dist/sketch.js +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -21,22 +21,9 @@ Built and maintained by [gAI Ventures](https://gai.ventures).
|
|
|
21
21
|
npx context-doctor savings
|
|
22
22
|
```
|
|
23
23
|
|
|
24
|
-
|
|
25
|
-
What context-doctor finds in your Claude Code sessions (last 30 days, 29 sessions)
|
|
26
|
-
────────────────────────────────────────────────────────────────
|
|
27
|
-
Input you were billed for $6,021 from the usage your transcripts record
|
|
28
|
-
|
|
29
|
-
1. Compact when you come back $2,492 199 returns to a session over 150k tokens
|
|
30
|
-
after the prompt cache expired. Running /compact then, net of the compaction
|
|
31
|
-
itself, would have saved this. The every-prompt hook points these moments out;
|
|
32
|
-
it works in every Claude Code surface, the desktop app included.
|
|
33
|
-
|
|
34
|
-
2. Autopilot n/a all 29 sessions ran in the desktop app, which
|
|
35
|
-
sets its own API address, so the autopilot proxy cannot sit in front of it.
|
|
36
|
-
(Had they run in the terminal or an IDE: $788, 12.9%.)
|
|
37
|
-
```
|
|
24
|
+
<img src="https://raw.githubusercontent.com/KushalP1/context-doctor/main/assets/savings.svg" alt="npx context-doctor savings: input billed, what /compact at cold resumes and autopilot would have saved" width="640">
|
|
38
25
|
|
|
39
|
-
That is the author's machine: every session there runs in the desktop app, so the first line is the one that applies. Yours is computed the same way, from your own transcripts: every session replayed request by request through the shipped code, against what you were actually billed.
|
|
26
|
+
That is the author's machine: every session there runs in the desktop app, so the first line is the one that applies. Yours is computed the same way, from your own transcripts: every session replayed request by request through the shipped code, against what you were actually billed. `context-doctor savings --share` prints a few lines with totals only (no project names or paths) if you want to post your number.
|
|
40
27
|
|
|
41
28
|
**Then install it**, whichever way suits you:
|
|
42
29
|
|
package/dist/autoclear.js
CHANGED
|
@@ -54,7 +54,9 @@ export const CLEARABLE_TOOLS = new Set([
|
|
|
54
54
|
const IMAGE_TOKENS = 1600;
|
|
55
55
|
const HOUR = 3_600_000;
|
|
56
56
|
export function clearedNote(tokens) {
|
|
57
|
-
|
|
57
|
+
// Not "run it again": a cleared Bash result may come from a command with
|
|
58
|
+
// side effects (a push, a delete), and repeating that would be wrong.
|
|
59
|
+
return `[context-doctor: old tool output cleared to keep context lean (~${tokens} tokens). The call above shows what ran; repeat it only if it is read-only, otherwise ask the user.]`;
|
|
58
60
|
}
|
|
59
61
|
function resultTokens(content, model) {
|
|
60
62
|
if (typeof content === "string")
|
package/dist/cli.js
CHANGED
|
@@ -22,7 +22,7 @@ import { runHook } from "./hook.js";
|
|
|
22
22
|
import { buildImpactReport } from "./impact.js";
|
|
23
23
|
import { measureTokenizer, renderTokenizer } from "./tokenizer-measure.js";
|
|
24
24
|
import { autopilotOff, autopilotOn, autopilotPause, autopilotPaths, autopilotStatus, DEFAULT_AUTOPILOT_PORT } from "./autopilot.js";
|
|
25
|
-
import { estimateSavings, renderSavings } from "./savings.js";
|
|
25
|
+
import { estimateSavings, renderSavings, renderShare } from "./savings.js";
|
|
26
26
|
import { renderPreferences, copyToClipboard, CHAT_PREFERENCES } from "./preferences.js";
|
|
27
27
|
import { recordLedger } from "./ledger.js";
|
|
28
28
|
import { runDoctor } from "./doctor.js";
|
|
@@ -51,9 +51,11 @@ Usage:
|
|
|
51
51
|
Claude Code, and Cursor automatically
|
|
52
52
|
context-doctor uninstall Undo install
|
|
53
53
|
context-doctor --version Print the installed version
|
|
54
|
-
context-doctor savings [--days n]
|
|
55
|
-
Claude Code sessions
|
|
56
|
-
|
|
54
|
+
context-doctor savings [--days n] [--share [--copy]]
|
|
55
|
+
What your recent Claude Code sessions cost, and what
|
|
56
|
+
/compact at cold resumes and autopilot would save,
|
|
57
|
+
replayed and priced as billed. --share: totals only,
|
|
58
|
+
to paste. Also what a bare \`context-doctor\` shows
|
|
57
59
|
context-doctor autopilot on|off|pause|resume|status
|
|
58
60
|
Every new Claude Code session goes through the
|
|
59
61
|
local proxy, which clears stale tool output only
|
|
@@ -178,19 +180,19 @@ function parseArgs(argv) {
|
|
|
178
180
|
args.strategies.push(argv[++i]);
|
|
179
181
|
break;
|
|
180
182
|
case "--keep-recent":
|
|
181
|
-
args.keepRecent =
|
|
183
|
+
args.keepRecent = numArg("--keep-recent", argv[++i], { min: 0, integer: true });
|
|
182
184
|
break;
|
|
183
185
|
case "--max-tool-tokens":
|
|
184
|
-
args.maxToolTokens =
|
|
186
|
+
args.maxToolTokens = numArg("--max-tool-tokens", argv[++i], { min: 1, integer: true });
|
|
185
187
|
break;
|
|
186
188
|
case "--port":
|
|
187
|
-
args.port =
|
|
189
|
+
args.port = numArg("--port", argv[++i], { min: 0, max: 65535, integer: true });
|
|
188
190
|
break;
|
|
189
191
|
case "--interval-ms":
|
|
190
|
-
args.intervalMs =
|
|
192
|
+
args.intervalMs = numArg("--interval-ms", argv[++i], { min: 100, integer: true });
|
|
191
193
|
break;
|
|
192
194
|
case "--limit":
|
|
193
|
-
args.limit =
|
|
195
|
+
args.limit = numArg("--limit", argv[++i], { min: 1, integer: true });
|
|
194
196
|
break;
|
|
195
197
|
case "--task":
|
|
196
198
|
args.task = argv[++i];
|
|
@@ -202,7 +204,7 @@ function parseArgs(argv) {
|
|
|
202
204
|
args.existing = argv[++i];
|
|
203
205
|
break;
|
|
204
206
|
case "--budget":
|
|
205
|
-
args.budgetUsd =
|
|
207
|
+
args.budgetUsd = numArg("--budget", argv[++i], { min: 0 });
|
|
206
208
|
break;
|
|
207
209
|
case "--dry-run":
|
|
208
210
|
args.dryRun = true;
|
|
@@ -210,6 +212,9 @@ function parseArgs(argv) {
|
|
|
210
212
|
case "--copy":
|
|
211
213
|
args.copy = true;
|
|
212
214
|
break;
|
|
215
|
+
case "--share":
|
|
216
|
+
args.share = true;
|
|
217
|
+
break;
|
|
213
218
|
case "--allow-dirty":
|
|
214
219
|
args.allowDirty = true;
|
|
215
220
|
break;
|
|
@@ -223,7 +228,7 @@ function parseArgs(argv) {
|
|
|
223
228
|
args.token = argv[++i];
|
|
224
229
|
break;
|
|
225
230
|
case "--days":
|
|
226
|
-
args.days =
|
|
231
|
+
args.days = numArg("--days", argv[++i], { min: 1 });
|
|
227
232
|
break;
|
|
228
233
|
case "--autopilot":
|
|
229
234
|
args.autopilot = true;
|
|
@@ -289,6 +294,21 @@ function packageVersion() {
|
|
|
289
294
|
return "unknown";
|
|
290
295
|
}
|
|
291
296
|
}
|
|
297
|
+
/**
|
|
298
|
+
* A numeric flag's value, or exit 1 with a message naming the flag. Number()
|
|
299
|
+
* alone turned `--days abc` into "the last NaN days" and a bad --port into a
|
|
300
|
+
* random port.
|
|
301
|
+
*/
|
|
302
|
+
function numArg(flag, raw, { min, max, integer } = {}) {
|
|
303
|
+
const n = Number(raw);
|
|
304
|
+
const ok = raw !== undefined && raw.trim() !== "" && Number.isFinite(n) && (!integer || Number.isInteger(n)) && (min === undefined || n >= min) && (max === undefined || n <= max);
|
|
305
|
+
if (!ok) {
|
|
306
|
+
const range = min !== undefined && max !== undefined ? ` between ${min} and ${max}` : min !== undefined ? ` of at least ${min}` : "";
|
|
307
|
+
console.error(`${flag} needs ${integer ? "a whole number" : "a number"}${range}; got ${raw === undefined ? "nothing" : JSON.stringify(raw)}.`);
|
|
308
|
+
process.exit(1);
|
|
309
|
+
}
|
|
310
|
+
return n;
|
|
311
|
+
}
|
|
292
312
|
/** Any Claude Code transcript on this machine? One directory listing, no parsing. */
|
|
293
313
|
function hasClaudeSessions() {
|
|
294
314
|
try {
|
|
@@ -504,6 +524,12 @@ async function main() {
|
|
|
504
524
|
const on = existsSync(autopilotPaths().config);
|
|
505
525
|
if (args.json)
|
|
506
526
|
console.log(JSON.stringify(report, null, 2));
|
|
527
|
+
else if (args.share) {
|
|
528
|
+
const text = renderShare(report);
|
|
529
|
+
console.log(text);
|
|
530
|
+
if (args.copy)
|
|
531
|
+
console.log(copyToClipboard(text) ? "\n(copied to the clipboard)" : "\n(no clipboard tool found; copy the lines above)");
|
|
532
|
+
}
|
|
507
533
|
else {
|
|
508
534
|
console.log(renderSavings(report, on));
|
|
509
535
|
if (!args.command)
|
|
@@ -582,6 +608,11 @@ async function main() {
|
|
|
582
608
|
return; // server keeps the process alive
|
|
583
609
|
}
|
|
584
610
|
if (!args.command || !args.file) {
|
|
611
|
+
const known = ["analyze", "optimize"];
|
|
612
|
+
if (args.command && !known.includes(args.command)) {
|
|
613
|
+
console.error(`Unknown command "${args.command}". Run \`context-doctor --help\` for the list.`);
|
|
614
|
+
process.exit(1);
|
|
615
|
+
}
|
|
585
616
|
console.log(HELP);
|
|
586
617
|
process.exit(args.command ? 1 : 0);
|
|
587
618
|
}
|
package/dist/coldresume.js
CHANGED
|
@@ -60,6 +60,12 @@ export function detectColdResume(path, now = Date.now(), idleMs = COLD_IDLE_MS,
|
|
|
60
60
|
catch {
|
|
61
61
|
continue;
|
|
62
62
|
}
|
|
63
|
+
// Compacted since the last reply: the old usage no longer describes the
|
|
64
|
+
// context, and there is nothing large left to warn about.
|
|
65
|
+
if (e?.type === "system" && e.subtype === "compact_boundary")
|
|
66
|
+
return undefined;
|
|
67
|
+
if (e?.isCompactSummary)
|
|
68
|
+
return undefined;
|
|
63
69
|
if (e?.type !== "assistant" || e.isSidechain)
|
|
64
70
|
continue;
|
|
65
71
|
const u = e.message?.usage;
|
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.23.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({
|
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.23.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.23.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"]);
|
package/dist/savings.d.ts
CHANGED
|
@@ -59,3 +59,8 @@ export interface SavingsReport {
|
|
|
59
59
|
export declare function replaySession(path: string, options?: AutoClearOptions, since?: number): SessionSavings | undefined;
|
|
60
60
|
export declare function estimateSavings(days?: number, options?: AutoClearOptions, paths?: string[], onProgress?: (done: number, total: number) => void): SavingsReport;
|
|
61
61
|
export declare function renderSavings(r: SavingsReport, autopilotOn?: boolean): string;
|
|
62
|
+
/**
|
|
63
|
+
* `savings --share`: a few lines people can paste or screenshot. Totals only:
|
|
64
|
+
* no project names, paths or session ids, so sharing it leaks nothing.
|
|
65
|
+
*/
|
|
66
|
+
export declare function renderShare(r: SavingsReport): string;
|
package/dist/savings.js
CHANGED
|
@@ -208,31 +208,56 @@ export function renderSavings(r, autopilotOn = false) {
|
|
|
208
208
|
}
|
|
209
209
|
const n = r.sessions.length;
|
|
210
210
|
const allDesktop = r.unreachableSessions === n;
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
lines.push(
|
|
211
|
+
const pct = (x) => (r.billedUsd > 0 ? ` (${Math.round((x / r.billedUsd) * 100)}%)` : "");
|
|
212
|
+
// Kept within 80 columns: this is most people's first screen of the tool.
|
|
213
|
+
lines.push("What context-doctor finds in your Claude Code sessions");
|
|
214
|
+
lines.push(`Last ${r.days} days, ${n} session${n === 1 ? "" : "s"}, priced at list prices`);
|
|
215
|
+
lines.push("─".repeat(60));
|
|
216
|
+
lines.push(`Input you were billed for ${usd(r.billedUsd).padStart(9)} from your transcripts' usage`);
|
|
214
217
|
lines.push("");
|
|
215
|
-
lines.push(`1. Compact when you come back
|
|
216
|
-
lines.push(
|
|
217
|
-
lines.push("
|
|
218
|
-
lines.push("
|
|
218
|
+
lines.push(`1. Compact when you come back ${usd(Math.max(0, r.coldResumeNetUsd)).padStart(9)}${pct(Math.max(0, r.coldResumeNetUsd))}`);
|
|
219
|
+
lines.push(` ${r.coldResumes} return${r.coldResumes === 1 ? "" : "s"} to a session over 150k tokens after the prompt cache`);
|
|
220
|
+
lines.push(" expired. Running /compact then, net of the compaction itself, would");
|
|
221
|
+
lines.push(" have saved this. The every-prompt hook points these moments out, in");
|
|
222
|
+
lines.push(" every Claude Code surface, the desktop app included.");
|
|
219
223
|
lines.push("");
|
|
220
224
|
if (allDesktop) {
|
|
221
|
-
lines.push(`2. Autopilot
|
|
222
|
-
lines.push(
|
|
223
|
-
lines.push(
|
|
225
|
+
lines.push(`2. Autopilot ${"n/a".padStart(9)}`);
|
|
226
|
+
lines.push(` All ${n} sessions ran in the desktop app, which sets its own API`);
|
|
227
|
+
lines.push(" address, so the autopilot proxy cannot sit in front of them. In a");
|
|
228
|
+
lines.push(` terminal or an IDE they would have saved ${usd(r.savedUsd)} (${(r.savedPct * 100).toFixed(1)}%).`);
|
|
224
229
|
}
|
|
225
230
|
else {
|
|
226
|
-
lines.push(`2. Autopilot
|
|
227
|
-
lines.push(
|
|
231
|
+
lines.push(`2. Autopilot ${usd(r.reachableUsd).padStart(9)}${pct(r.reachableUsd)}`);
|
|
232
|
+
lines.push(" Stale tool output cleared only when the cache is cold, so it never");
|
|
233
|
+
lines.push(` costs more (${r.worse} session${r.worse === 1 ? "" : "s"} made more expensive); ${tokens(r.savedTokens)} tokens not sent.`);
|
|
228
234
|
if (r.unreachableSessions > 0)
|
|
229
|
-
lines.push(`
|
|
235
|
+
lines.push(` Terminal and IDE sessions only; ${r.unreachableSessions} desktop-app session${r.unreachableSessions === 1 ? "" : "s"} not reachable.`);
|
|
230
236
|
if (!autopilotOn)
|
|
231
237
|
lines.push(" Turn it on: context-doctor autopilot on");
|
|
232
238
|
}
|
|
233
239
|
lines.push("");
|
|
234
|
-
lines.push("Replayed request by request through the shipped code and priced as the
|
|
235
|
-
lines.push("cache bills
|
|
236
|
-
lines.push("usage limit instead.");
|
|
240
|
+
lines.push("Replayed request by request through the shipped code and priced as the");
|
|
241
|
+
lines.push("prompt cache bills. On a subscription the same tokens come out of your");
|
|
242
|
+
lines.push("usage limit instead. Share yours: context-doctor savings --share");
|
|
243
|
+
return lines.join("\n");
|
|
244
|
+
}
|
|
245
|
+
/**
|
|
246
|
+
* `savings --share`: a few lines people can paste or screenshot. Totals only:
|
|
247
|
+
* no project names, paths or session ids, so sharing it leaks nothing.
|
|
248
|
+
*/
|
|
249
|
+
export function renderShare(r) {
|
|
250
|
+
if (r.sessions.length === 0)
|
|
251
|
+
return "context-doctor found no Claude Code sessions to measure yet.";
|
|
252
|
+
const lines = [
|
|
253
|
+
`My Claude Code input, last ${r.days} days: ${usd(r.billedUsd)} at list price (${r.sessions.length} sessions).`,
|
|
254
|
+
];
|
|
255
|
+
if (r.coldResumeNetUsd > 0) {
|
|
256
|
+
lines.push(`Running /compact when I came back to a big session after the cache expired would have saved ${usd(r.coldResumeNetUsd)} (${Math.round((r.coldResumeNetUsd / r.billedUsd) * 100)}%, ${r.coldResumes} times).`);
|
|
257
|
+
}
|
|
258
|
+
if (r.reachableUsd > 0) {
|
|
259
|
+
lines.push(`Clearing stale tool output only when the cache is cold would have cut another ${usd(r.reachableUsd)}, with no session made more expensive.`);
|
|
260
|
+
}
|
|
261
|
+
lines.push("Measured from my own transcripts with context-doctor: npx context-doctor savings");
|
|
237
262
|
return lines.join("\n");
|
|
238
263
|
}
|
package/dist/sketch.js
CHANGED
|
@@ -128,7 +128,7 @@ export function profileSketch(sketch) {
|
|
|
128
128
|
severity: totalTokens > 100_000 ? "high" : "warn",
|
|
129
129
|
estSavings: Math.max(0, totalTokens - afterHandoff),
|
|
130
130
|
message: `${turns} turns: every new message re-reads ~${formatTokens(totalTokens)}.`,
|
|
131
|
-
action: `Offer a handoff: write a ≤${HANDOFF_SUMMARY_TOKENS}-token summary (decisions, current state, open items, key identifiers) for the user to start a new chat with. A fresh chat re-reads ~${formatTokens(afterHandoff)} instead.`,
|
|
131
|
+
action: `Offer a handoff: write a ≤${HANDOFF_SUMMARY_TOKENS}-token summary (decisions, current state, open items, key identifiers) for the user to start a new chat with. A fresh chat re-reads ~${formatTokens(afterHandoff)} of conversation instead, plus the app's own system prompt, which every chat carries.`,
|
|
132
132
|
});
|
|
133
133
|
}
|
|
134
134
|
const contextWindow = contextWindowFor(sketch.model);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "context-doctor",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.23.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",
|