context-doctor 0.12.1 → 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/mcp.js +1 -1
- package/dist/optimize.js +8 -0
- package/dist/parse.js +20 -8
- package/package.json +2 -2
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",
|