context-doctor 0.13.2 → 0.13.3

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 CHANGED
@@ -383,6 +383,8 @@ npm publish # prompts for the npm 2FA code
383
383
  git push --follow-tags
384
384
  ```
385
385
 
386
+ **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.
387
+
386
388
  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
389
 
388
390
  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/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 registered = JSON.stringify(settings.hooks?.UserPromptSubmit ?? []).includes("context-doctor");
100
- checks.push(registered
101
- ? { label: "Every-prompt hook", status: "ok", detail: "registered in ~/.claude/settings.json" }
102
- : { label: "Every-prompt hook", status: "fail", detail: "not registered — run: context-doctor install" });
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.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
- const ephemeral = selfDir.includes("_npx");
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)
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.2" }, { instructions: SERVER_INSTRUCTIONS });
40
+ const server = new McpServer({ name: "context-doctor", version: "0.13.3" }, { 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.2",
3
+ "version": "0.13.3",
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 run build",
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
  },