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 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.1" }, { instructions: SERVER_INSTRUCTIONS });
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(raw, index) {
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 toolCalls = raw.tool_calls;
77
- if (Array.isArray(toolCalls) && toolCalls.length > 0) {
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 rawMessages = obj.messages ?? [];
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.content) && m.content.some((b) => b?.type === "tool_use" || b?.type === "tool_result"));
131
- const parseWarning = messages.length === 0
132
- ? "This JSON has no `messages` array (and no `system`) it does not look like a conversation. Expected {\"messages\":[{\"role\":…,\"content\":…}]}."
133
- : undefined;
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 arraythis 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.1",
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",