context-doctor 0.13.2 → 0.13.4
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 +4 -0
- package/dist/cli.js +4 -1
- package/dist/doctor.js +31 -4
- package/dist/install.d.ts +16 -1
- package/dist/install.js +58 -15
- package/dist/mcp.js +1 -1
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -34,6 +34,8 @@ Findings (4)
|
|
|
34
34
|
npx context-doctor install
|
|
35
35
|
```
|
|
36
36
|
|
|
37
|
+
`install` configures every app it detects and does not stop at the first problem: a corrupt Claude Desktop config still gets you Claude Code and Cursor. It does not pretend either. Any target that failed is named with a ✗ line, the summary reads "Done with N problem(s)" instead of "Done.", and the **exit code is 1**, so dotfiles and onboarding scripts can react. A broken config file is never overwritten; fix it and re-run.
|
|
38
|
+
|
|
37
39
|
That single command is also all it takes to **set up context-doctor on anyone else's machine**. Prefer a global install, or want the unreleased `main`? Both work (Node 20+):
|
|
38
40
|
|
|
39
41
|
```bash
|
|
@@ -383,6 +385,8 @@ npm publish # prompts for the npm 2FA code
|
|
|
383
385
|
git push --follow-tags
|
|
384
386
|
```
|
|
385
387
|
|
|
388
|
+
**What the npm download number measures.** `install` writes `npx -y context-doctor-mcp` into MCP configs, and npx re-fetches the tarball whenever a new version exists. So every release is downloaded once by every active install within about a day, and the daily count is almost entirely those refreshes: on this package, release days run ~170 downloads and non-release days ~27. Read it as "size of the active installed base × number of releases", not as new users — a quiet week with no releases will look like a decline while nothing has changed. Two corollaries: the release-day figure is a live count of machines running context-doctor, and a broken release reaches all of them automatically, which is why `prepublishOnly` runs the full test suite. npm's stats also lag by several days and occasionally record a day as zero; a zero on a release day is a gap in their pipeline, not in usage.
|
|
389
|
+
|
|
386
390
|
Known gotcha: if `npm publish` fails with **`404 Not Found - PUT …/context-doctor`** on a package that clearly exists, the real cause is an **expired npm login token** — npm reports unauthenticated publishes as a 404, not a 401. Check with `npm whoami`; if that errors, run `npm login` and publish again.
|
|
387
391
|
|
|
388
392
|
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.
|
package/dist/cli.js
CHANGED
|
@@ -359,7 +359,10 @@ function main() {
|
|
|
359
359
|
return;
|
|
360
360
|
}
|
|
361
361
|
if (args.command === "install") {
|
|
362
|
-
|
|
362
|
+
// Partial success is still installed, but not silent: any failed target
|
|
363
|
+
// makes the exit code non-zero so automation can react.
|
|
364
|
+
if (runInstall().failures.length > 0)
|
|
365
|
+
process.exitCode = 1;
|
|
363
366
|
return;
|
|
364
367
|
}
|
|
365
368
|
if (args.command === "uninstall") {
|
package/dist/doctor.js
CHANGED
|
@@ -50,6 +50,23 @@ function checkMcpEntry(appName, configPath) {
|
|
|
50
50
|
return { label: appName, status: "fail", detail: `${configPath} is not valid JSON (${e.message})` };
|
|
51
51
|
}
|
|
52
52
|
}
|
|
53
|
+
/**
|
|
54
|
+
* For a hook command, the path that must exist for it to run — or null when it
|
|
55
|
+
* resolves through PATH (`node`, `npx`) and there is nothing to check here.
|
|
56
|
+
*
|
|
57
|
+
* Forms written by install: `node "<cli.js>" hook`, `"<binary>" hook`,
|
|
58
|
+
* `npx -y context-doctor hook`.
|
|
59
|
+
*/
|
|
60
|
+
function hookBinaryMissing(command) {
|
|
61
|
+
const quoted = [...command.matchAll(/"([^"]+)"/g)].map((m) => m[1]);
|
|
62
|
+
const first = command.trim().split(/\s+/)[0]?.replace(/^"|"$/g, "") ?? "";
|
|
63
|
+
const candidates = quoted.length > 0 ? quoted : /[\\/]/.test(first) ? [first] : [];
|
|
64
|
+
for (const path of candidates) {
|
|
65
|
+
if (!existsSync(path))
|
|
66
|
+
return path;
|
|
67
|
+
}
|
|
68
|
+
return null;
|
|
69
|
+
}
|
|
53
70
|
/** Spawn our own MCP server and run the initialize handshake over stdio. */
|
|
54
71
|
function checkMcpHandshake() {
|
|
55
72
|
const label = "MCP server handshake";
|
|
@@ -96,10 +113,20 @@ export async function runDoctor() {
|
|
|
96
113
|
if (existsSync(settingsPath)) {
|
|
97
114
|
try {
|
|
98
115
|
const settings = JSON.parse(readFileSync(settingsPath, "utf8"));
|
|
99
|
-
const
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
116
|
+
const entries = settings.hooks?.UserPromptSubmit ?? [];
|
|
117
|
+
const ours = entries.map((e) => e.hooks?.[0]?.command ?? "").find((c) => /context-doctor|cli\.js"?\s+hook/.test(c));
|
|
118
|
+
if (!ours) {
|
|
119
|
+
checks.push({ label: "Every-prompt hook", status: "fail", detail: "not registered — run: context-doctor install" });
|
|
120
|
+
}
|
|
121
|
+
else {
|
|
122
|
+
// "Registered" is not "working": a hook whose binary has been deleted
|
|
123
|
+
// (an npx cache sweep, a Node upgrade) fails silently on every prompt,
|
|
124
|
+
// and this check used to report it as fine.
|
|
125
|
+
const missing = hookBinaryMissing(ours);
|
|
126
|
+
checks.push(missing
|
|
127
|
+
? { label: "Every-prompt hook", status: "fail", detail: `registered, but ${missing} no longer exists — re-run: context-doctor install` }
|
|
128
|
+
: { label: "Every-prompt hook", status: "ok", detail: "registered in ~/.claude/settings.json; command resolves" });
|
|
129
|
+
}
|
|
103
130
|
}
|
|
104
131
|
catch (e) {
|
|
105
132
|
checks.push({ label: "Every-prompt hook", status: "fail", detail: `settings.json unreadable (${e.message})` });
|
package/dist/install.d.ts
CHANGED
|
@@ -20,5 +20,20 @@ export declare function npxLauncher(platformName: string): {
|
|
|
20
20
|
command: string;
|
|
21
21
|
args: string[];
|
|
22
22
|
};
|
|
23
|
-
|
|
23
|
+
/** Outcome of an install run, so the CLI can set a truthful exit code. */
|
|
24
|
+
export interface InstallResult {
|
|
25
|
+
/** Detected targets that could not be configured, with the reason. */
|
|
26
|
+
failures: string[];
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Install into every detected app.
|
|
30
|
+
*
|
|
31
|
+
* A failure in one app must not stop the others: someone with a corrupt
|
|
32
|
+
* Claude Desktop config still wants Claude Code and Cursor wired. But it
|
|
33
|
+
* must not be reported as success either — automation (dotfiles, CI,
|
|
34
|
+
* onboarding scripts) reads the exit code, and a "Done." with exit 0 over a
|
|
35
|
+
* failed target is a lie that surfaces later as "the tools never showed up".
|
|
36
|
+
* So: keep going, summarize, and return the failures for a non-zero exit.
|
|
37
|
+
*/
|
|
38
|
+
export declare function runInstall(): InstallResult;
|
|
24
39
|
export declare function runUninstall(): void;
|
package/dist/install.js
CHANGED
|
@@ -89,6 +89,13 @@ function binOnPath(name) {
|
|
|
89
89
|
for (const dir of (process.env.PATH ?? "").split(delimiter)) {
|
|
90
90
|
if (!dir)
|
|
91
91
|
continue;
|
|
92
|
+
// npx prepends its OWN cache's .bin to PATH while it runs the command. So
|
|
93
|
+
// during `npx -y context-doctor install`, the first "global binary" on
|
|
94
|
+
// PATH is inside _npx — the garbage-collected directory this lookup exists
|
|
95
|
+
// to avoid. That hole put a cache path in the hook of every user who
|
|
96
|
+
// followed the README's headline command.
|
|
97
|
+
if (isEphemeralPath(dir))
|
|
98
|
+
continue;
|
|
92
99
|
for (const ext of exts) {
|
|
93
100
|
const candidate = join(dir, name + ext);
|
|
94
101
|
if (existsSync(candidate))
|
|
@@ -97,6 +104,10 @@ function binOnPath(name) {
|
|
|
97
104
|
}
|
|
98
105
|
return null;
|
|
99
106
|
}
|
|
107
|
+
/** Paths npm may delete at any time: the npx cache and the npm cache itself. */
|
|
108
|
+
function isEphemeralPath(path) {
|
|
109
|
+
return /[\\/]_npx[\\/]/.test(path) || /[\\/]\.npm[\\/]/.test(path) || /[\\/]npm-cache[\\/]/i.test(path);
|
|
110
|
+
}
|
|
100
111
|
/**
|
|
101
112
|
* Shell command used for the Claude Code every-prompt hook.
|
|
102
113
|
*
|
|
@@ -113,8 +124,7 @@ function binOnPath(name) {
|
|
|
113
124
|
function hookCommand() {
|
|
114
125
|
const selfDir = dirname(fileURLToPath(import.meta.url));
|
|
115
126
|
const localCli = join(selfDir, "cli.js");
|
|
116
|
-
|
|
117
|
-
if (!ephemeral && existsSync(localCli))
|
|
127
|
+
if (!isEphemeralPath(selfDir + sep) && existsSync(localCli))
|
|
118
128
|
return `node "${localCli}" hook`;
|
|
119
129
|
const global = binOnPath("context-doctor");
|
|
120
130
|
if (global)
|
|
@@ -204,6 +214,16 @@ function installSkill() {
|
|
|
204
214
|
copyFileSync(skillSource, join(skillDest, "SKILL.md"));
|
|
205
215
|
return join(skillDest, "SKILL.md");
|
|
206
216
|
}
|
|
217
|
+
/**
|
|
218
|
+
* Install into every detected app.
|
|
219
|
+
*
|
|
220
|
+
* A failure in one app must not stop the others: someone with a corrupt
|
|
221
|
+
* Claude Desktop config still wants Claude Code and Cursor wired. But it
|
|
222
|
+
* must not be reported as success either — automation (dotfiles, CI,
|
|
223
|
+
* onboarding scripts) reads the exit code, and a "Done." with exit 0 over a
|
|
224
|
+
* failed target is a lie that surfaces later as "the tools never showed up".
|
|
225
|
+
* So: keep going, summarize, and return the failures for a non-zero exit.
|
|
226
|
+
*/
|
|
207
227
|
export function runInstall() {
|
|
208
228
|
const entry = serverEntry();
|
|
209
229
|
const found = targets().filter((t) => t.detect());
|
|
@@ -211,8 +231,9 @@ export function runInstall() {
|
|
|
211
231
|
console.log("No supported AI apps detected (Claude Desktop, Claude Code, Cursor).");
|
|
212
232
|
console.log("Manual setup — add to your app's MCP config:");
|
|
213
233
|
console.log(JSON.stringify({ mcpServers: { "context-doctor": entry } }, null, 2));
|
|
214
|
-
return;
|
|
234
|
+
return { failures: [] };
|
|
215
235
|
}
|
|
236
|
+
const failures = [];
|
|
216
237
|
for (const target of found) {
|
|
217
238
|
try {
|
|
218
239
|
const config = readJson(target.configPath);
|
|
@@ -224,22 +245,44 @@ export function runInstall() {
|
|
|
224
245
|
}
|
|
225
246
|
catch (e) {
|
|
226
247
|
console.error(`✗ ${target.name}: ${e.message}`);
|
|
248
|
+
failures.push(target.name);
|
|
227
249
|
}
|
|
228
250
|
}
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
251
|
+
try {
|
|
252
|
+
const skillPath = installSkill();
|
|
253
|
+
if (skillPath)
|
|
254
|
+
console.log(`✓ Agent Skill installed for Claude Code (${skillPath})`);
|
|
255
|
+
}
|
|
256
|
+
catch (e) {
|
|
257
|
+
console.error(`✗ Agent Skill: ${e.message}`);
|
|
258
|
+
failures.push("Agent Skill");
|
|
259
|
+
}
|
|
260
|
+
try {
|
|
261
|
+
const hookPath = installHook();
|
|
262
|
+
if (hookPath) {
|
|
263
|
+
console.log(`✓ Claude Code every-prompt hook installed (${hookPath}) — heavy sessions get automatic hygiene guidance`);
|
|
264
|
+
// npx resolves the package on every single prompt; a global install makes
|
|
265
|
+
// the hook a plain exec instead, which is both faster and update-proof.
|
|
266
|
+
if (hookUsesNpx()) {
|
|
267
|
+
console.log(" note: the hook falls back to npx. For a faster, permanent hook: npm i -g context-doctor && context-doctor install");
|
|
268
|
+
}
|
|
239
269
|
}
|
|
240
270
|
}
|
|
241
|
-
|
|
242
|
-
|
|
271
|
+
catch (e) {
|
|
272
|
+
// An unreadable settings.json used to escape as a stack trace and abort
|
|
273
|
+
// the run; it is a failed target like any other.
|
|
274
|
+
console.error(`✗ Claude Code every-prompt hook: ${e.message}`);
|
|
275
|
+
failures.push("Claude Code hook");
|
|
276
|
+
}
|
|
277
|
+
if (failures.length > 0) {
|
|
278
|
+
console.log(`\nDone with ${failures.length} problem(s): ${failures.join(", ")}. See the ✗ lines above.`);
|
|
279
|
+
console.log("Everything else was installed. Exit code is 1 so scripts can tell; fix the file(s) and re-run install.");
|
|
280
|
+
}
|
|
281
|
+
else {
|
|
282
|
+
console.log("\nDone. Restart the apps to pick up the new tools, then try:");
|
|
283
|
+
console.log(' "What\'s eating my context?" — or paste a conversation and ask for a profile.');
|
|
284
|
+
}
|
|
285
|
+
return { failures };
|
|
243
286
|
}
|
|
244
287
|
export function runUninstall() {
|
|
245
288
|
for (const target of targets().filter((t) => t.detect())) {
|
package/dist/mcp.js
CHANGED
|
@@ -37,7 +37,7 @@ const STRATEGY_IDS = ["dedupe", "trim-tool-results", "trim-tool-calls", "strip-b
|
|
|
37
37
|
* recommended pattern.
|
|
38
38
|
*/
|
|
39
39
|
function createServer() {
|
|
40
|
-
const server = new McpServer({ name: "context-doctor", version: "0.13.
|
|
40
|
+
const server = new McpServer({ name: "context-doctor", version: "0.13.4" }, { 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/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "context-doctor",
|
|
3
|
-
"version": "0.13.
|
|
3
|
+
"version": "0.13.4",
|
|
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",
|
|
@@ -40,7 +40,7 @@
|
|
|
40
40
|
},
|
|
41
41
|
"scripts": {
|
|
42
42
|
"build": "tsc && node -e \"const fs=require('fs');['dist/cli.js','dist/mcp.js'].forEach(f=>fs.chmodSync(f,0o755))\"",
|
|
43
|
-
"prepublishOnly": "npm
|
|
43
|
+
"prepublishOnly": "npm test",
|
|
44
44
|
"dev": "tsc --watch",
|
|
45
45
|
"test": "npm run build && node --test dist/test/smoke.test.js dist/test/proxy.test.js dist/test/proxy-abort.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 dist/test/session.test.js dist/test/accuracy.test.js dist/test/cache-stability.test.js dist/test/ledger.test.js"
|
|
46
46
|
},
|