context-doctor 0.12.0 → 0.12.2
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/dist/doctor.js +5 -3
- package/dist/install.d.ts +14 -0
- package/dist/install.js +17 -1
- package/dist/mcp.js +1 -1
- package/dist/optimize.js +8 -0
- package/dist/parse.js +20 -8
- package/package.json +2 -2
package/dist/doctor.js
CHANGED
|
@@ -26,8 +26,10 @@ function checkMcpEntry(appName, configPath) {
|
|
|
26
26
|
const entry = config.mcpServers?.["context-doctor"];
|
|
27
27
|
if (!entry)
|
|
28
28
|
return { label: appName, status: "fail", detail: `no context-doctor entry in ${configPath} — run: context-doctor install` };
|
|
29
|
-
// Absolute-path entries must point at a file that still exists.
|
|
30
|
-
|
|
29
|
+
// Absolute-path entries must point at a file that still exists. Launcher
|
|
30
|
+
// forms (npx, and Windows' cmd /c npx) resolve at spawn time, not now.
|
|
31
|
+
const launcher = entry.command === "npx" || entry.command === "cmd";
|
|
32
|
+
const target = launcher ? null : entry.args?.[0];
|
|
31
33
|
if (target && !existsSync(target)) {
|
|
32
34
|
return { label: appName, status: "fail", detail: `MCP entry points at missing file ${target} — re-run: context-doctor install` };
|
|
33
35
|
}
|
|
@@ -41,7 +43,7 @@ function checkMcpEntry(appName, configPath) {
|
|
|
41
43
|
detail: `MCP command ${cmd} no longer exists (a Node upgrade moves version-pinned paths) — re-run: context-doctor install`,
|
|
42
44
|
};
|
|
43
45
|
}
|
|
44
|
-
return { label: appName, status: "ok", detail: `MCP wired (${
|
|
46
|
+
return { label: appName, status: "ok", detail: `MCP wired (${launcher ? "npx, tracks npm releases" : "local build"})` };
|
|
45
47
|
}
|
|
46
48
|
catch (e) {
|
|
47
49
|
return { label: appName, status: "fail", detail: `${configPath} is not valid JSON (${e.message})` };
|
package/dist/install.d.ts
CHANGED
|
@@ -6,5 +6,19 @@
|
|
|
6
6
|
* Every config edit is a careful JSON merge with a .backup file written first.
|
|
7
7
|
* `context-doctor uninstall` reverses it.
|
|
8
8
|
*/
|
|
9
|
+
/**
|
|
10
|
+
* How to invoke the published package as an MCP server on a given platform.
|
|
11
|
+
*
|
|
12
|
+
* On Windows npx is `npx.cmd` — a batch script, not an executable. MCP clients
|
|
13
|
+
* spawn their server directly, without a shell, so a bare "npx" fails with
|
|
14
|
+
* ENOENT and the app simply shows no tools and no error. Hence the cmd /c
|
|
15
|
+
* wrapper that every working Windows MCP config uses.
|
|
16
|
+
*
|
|
17
|
+
* Exported so the platform branch is testable from any host OS.
|
|
18
|
+
*/
|
|
19
|
+
export declare function npxLauncher(platformName: string): {
|
|
20
|
+
command: string;
|
|
21
|
+
args: string[];
|
|
22
|
+
};
|
|
9
23
|
export declare function runInstall(): void;
|
|
10
24
|
export declare function runUninstall(): void;
|
package/dist/install.js
CHANGED
|
@@ -43,9 +43,25 @@ function serverEntry() {
|
|
|
43
43
|
const localMcp = join(selfDir, "mcp.js");
|
|
44
44
|
const fromPackage = selfDir.includes(`${sep}node_modules${sep}`) || selfDir.includes("_npx");
|
|
45
45
|
if (!fromPackage && existsSync(localMcp)) {
|
|
46
|
+
// `node` is node.exe on Windows — directly spawnable, no shell needed.
|
|
46
47
|
return { command: "node", args: [localMcp] };
|
|
47
48
|
}
|
|
48
|
-
return
|
|
49
|
+
return npxLauncher(platform());
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* How to invoke the published package as an MCP server on a given platform.
|
|
53
|
+
*
|
|
54
|
+
* On Windows npx is `npx.cmd` — a batch script, not an executable. MCP clients
|
|
55
|
+
* spawn their server directly, without a shell, so a bare "npx" fails with
|
|
56
|
+
* ENOENT and the app simply shows no tools and no error. Hence the cmd /c
|
|
57
|
+
* wrapper that every working Windows MCP config uses.
|
|
58
|
+
*
|
|
59
|
+
* Exported so the platform branch is testable from any host OS.
|
|
60
|
+
*/
|
|
61
|
+
export function npxLauncher(platformName) {
|
|
62
|
+
return platformName === "win32"
|
|
63
|
+
? { command: "cmd", args: ["/c", "npx", "-y", "context-doctor-mcp"] }
|
|
64
|
+
: { command: "npx", args: ["-y", "context-doctor-mcp"] };
|
|
49
65
|
}
|
|
50
66
|
function readJson(path) {
|
|
51
67
|
if (!existsSync(path))
|
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.12.
|
|
40
|
+
const server = new McpServer({ name: "context-doctor", version: "0.12.2" }, { 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/optimize.js
CHANGED
|
@@ -127,6 +127,14 @@ export function optimizeConversation(input, options = {}) {
|
|
|
127
127
|
if (!Array.isArray(messages)) {
|
|
128
128
|
throw new Error("No `messages` array found in input");
|
|
129
129
|
}
|
|
130
|
+
// Null / non-object entries occur in truncated and hand-edited files; every
|
|
131
|
+
// strategy below would throw on them. Drop them IN PLACE rather than working
|
|
132
|
+
// on a copy — prune-history splices this same array, and the returned
|
|
133
|
+
// conversation is the caller's original object.
|
|
134
|
+
for (let i = messages.length - 1; i >= 0; i--) {
|
|
135
|
+
if (!messages[i] || typeof messages[i] !== "object")
|
|
136
|
+
messages.splice(i, 1);
|
|
137
|
+
}
|
|
130
138
|
const tokensBefore = messages.reduce((s, m) => s + estimateTokens(textOf(m.content)), 0);
|
|
131
139
|
const applied = [];
|
|
132
140
|
// -- strip-base64: replace inline blobs with a placeholder --------------------
|
package/dist/parse.js
CHANGED
|
@@ -62,7 +62,10 @@ function flattenContent(content) {
|
|
|
62
62
|
}
|
|
63
63
|
return { text, hasBinary, toolName, kind, toolCallText: toolCallText || undefined };
|
|
64
64
|
}
|
|
65
|
-
function normalizeMessage(
|
|
65
|
+
function normalizeMessage(rawInput, index) {
|
|
66
|
+
// A null or non-object entry appears in truncated and hand-edited files.
|
|
67
|
+
// Treat it as an empty message rather than throwing a stack at the user.
|
|
68
|
+
const raw = rawInput && typeof rawInput === "object" ? rawInput : {};
|
|
66
69
|
const role = String(raw.role ?? "user");
|
|
67
70
|
const flat = flattenContent(raw.content);
|
|
68
71
|
let kind = flat.kind ?? (["system", "user", "assistant"].includes(role) ? role : "other");
|
|
@@ -73,8 +76,10 @@ function normalizeMessage(raw, index) {
|
|
|
73
76
|
if (role === "tool") {
|
|
74
77
|
kind = "tool_result";
|
|
75
78
|
}
|
|
76
|
-
const
|
|
77
|
-
|
|
79
|
+
const rawToolCalls = raw.tool_calls;
|
|
80
|
+
// Entries can be null or malformed in hand-edited or truncated exports.
|
|
81
|
+
const toolCalls = Array.isArray(rawToolCalls) ? rawToolCalls.filter((tc) => tc && typeof tc === "object") : undefined;
|
|
82
|
+
if (toolCalls && toolCalls.length > 0) {
|
|
78
83
|
kind = "tool_call";
|
|
79
84
|
toolName = toolCalls[0]?.function?.name ?? toolCalls[0]?.name;
|
|
80
85
|
const calls = toolCalls
|
|
@@ -118,7 +123,12 @@ export function parseConversation(input) {
|
|
|
118
123
|
};
|
|
119
124
|
}
|
|
120
125
|
const obj = data;
|
|
121
|
-
const
|
|
126
|
+
const rawField = obj.messages;
|
|
127
|
+
const rawMessages = Array.isArray(rawField)
|
|
128
|
+
? rawField
|
|
129
|
+
: [];
|
|
130
|
+
// `messages` present but not an array is a malformed file, not an empty chat.
|
|
131
|
+
const malformedMessages = rawField != null && !Array.isArray(rawField);
|
|
122
132
|
const messages = [];
|
|
123
133
|
// Anthropic keeps the system prompt outside the messages array.
|
|
124
134
|
if (obj.system != null) {
|
|
@@ -127,9 +137,11 @@ export function parseConversation(input) {
|
|
|
127
137
|
}
|
|
128
138
|
messages.push(...rawMessages.map((m, i) => normalizeMessage(m, i)));
|
|
129
139
|
const isAnthropic = obj.system != null ||
|
|
130
|
-
rawMessages.some((m) => Array.isArray(m
|
|
131
|
-
const parseWarning =
|
|
132
|
-
?
|
|
133
|
-
:
|
|
140
|
+
rawMessages.some((m) => Array.isArray(m?.content) && m.content.some((b) => b?.type === "tool_use" || b?.type === "tool_result"));
|
|
141
|
+
const parseWarning = malformedMessages
|
|
142
|
+
? `\`messages\` is a ${typeof rawField}, not an array — this file is malformed.`
|
|
143
|
+
: messages.length === 0
|
|
144
|
+
? "This JSON has no `messages` array (and no `system`) — it does not look like a conversation. Expected {\"messages\":[{\"role\":…,\"content\":…}]}."
|
|
145
|
+
: undefined;
|
|
134
146
|
return { sourceFormat: isAnthropic ? "anthropic" : "openai", parseWarning, messages };
|
|
135
147
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "context-doctor",
|
|
3
|
-
"version": "0.12.
|
|
3
|
+
"version": "0.12.2",
|
|
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 dist/test/cache.test.js dist/test/session.test.js"
|
|
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"
|
|
46
46
|
},
|
|
47
47
|
"dependencies": {
|
|
48
48
|
"@modelcontextprotocol/sdk": "^1.0.0",
|