context-doctor 0.7.0 → 0.9.0
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 +26 -6
- package/dist/cli.js +98 -11
- package/dist/config.d.ts +63 -0
- package/dist/config.js +73 -0
- package/dist/cursor.d.ts +46 -0
- package/dist/cursor.js +203 -0
- package/dist/dashboard.d.ts +45 -0
- package/dist/dashboard.js +394 -0
- package/dist/hook.js +42 -12
- package/dist/impact.js +1 -1
- package/dist/mcp.js +1 -1
- package/dist/profile.js +35 -1
- package/dist/session.d.ts +15 -0
- package/dist/session.js +21 -2
- package/package.json +3 -2
- package/dist/test/chatgpt-export.test.d.ts +0 -2
- package/dist/test/chatgpt-export.test.js +0 -45
- package/dist/test/doctor.test.d.ts +0 -2
- package/dist/test/doctor.test.js +0 -19
- package/dist/test/hook.test.d.ts +0 -5
- package/dist/test/hook.test.js +0 -54
- package/dist/test/mcp-http.test.d.ts +0 -6
- package/dist/test/mcp-http.test.js +0 -63
- package/dist/test/proxy.test.d.ts +0 -6
- package/dist/test/proxy.test.js +0 -121
- package/dist/test/smoke.test.d.ts +0 -2
- package/dist/test/smoke.test.js +0 -111
- package/dist/test/watch.test.d.ts +0 -2
- package/dist/test/watch.test.js +0 -36
package/dist/session.js
CHANGED
|
@@ -89,6 +89,10 @@ export function parseSessionFile(path) {
|
|
|
89
89
|
const messages = [];
|
|
90
90
|
let title;
|
|
91
91
|
let model;
|
|
92
|
+
/** Index in `messages` of the newest compaction summary, or -1. */
|
|
93
|
+
let lastCompactIndex = -1;
|
|
94
|
+
/** Newest API-reported input size, if the transcript carries usage. */
|
|
95
|
+
let reportedInputTokens;
|
|
92
96
|
for (const line of raw.split("\n")) {
|
|
93
97
|
if (!line.trim())
|
|
94
98
|
continue;
|
|
@@ -113,13 +117,28 @@ export function parseSessionFile(path) {
|
|
|
113
117
|
continue;
|
|
114
118
|
if (typeof message.model === "string")
|
|
115
119
|
model = message.model;
|
|
120
|
+
const usage = message.usage;
|
|
121
|
+
if (entry.type === "assistant" && usage) {
|
|
122
|
+
const total = (usage.input_tokens ?? 0) + (usage.cache_read_input_tokens ?? 0) + (usage.cache_creation_input_tokens ?? 0);
|
|
123
|
+
if (total > 0)
|
|
124
|
+
reportedInputTokens = total;
|
|
125
|
+
}
|
|
126
|
+
if (entry.isCompactSummary)
|
|
127
|
+
lastCompactIndex = messages.length;
|
|
116
128
|
messages.push({ role: message.role, content: message.content });
|
|
117
129
|
}
|
|
130
|
+
// A compaction replaces everything before it: the summary entry IS the live
|
|
131
|
+
// history from that point on. Counting the pre-compaction turns would
|
|
132
|
+
// overstate context, cost per message and window fill — sometimes hugely.
|
|
133
|
+
const compactedAway = lastCompactIndex >= 0 ? lastCompactIndex : 0;
|
|
134
|
+
const live = lastCompactIndex >= 0 ? messages.slice(lastCompactIndex) : messages;
|
|
118
135
|
return {
|
|
119
|
-
conversationJson: JSON.stringify({ messages }),
|
|
136
|
+
conversationJson: JSON.stringify({ messages: live }),
|
|
120
137
|
title,
|
|
121
138
|
model,
|
|
122
|
-
messageCount:
|
|
139
|
+
messageCount: live.length,
|
|
140
|
+
compactedAway,
|
|
141
|
+
reportedInputTokens,
|
|
123
142
|
path,
|
|
124
143
|
};
|
|
125
144
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "context-doctor",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.9.0",
|
|
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",
|
|
@@ -30,6 +30,7 @@
|
|
|
30
30
|
},
|
|
31
31
|
"files": [
|
|
32
32
|
"dist",
|
|
33
|
+
"!dist/test",
|
|
33
34
|
"skills",
|
|
34
35
|
"README.md",
|
|
35
36
|
"LICENSE"
|
|
@@ -41,7 +42,7 @@
|
|
|
41
42
|
"build": "tsc && node -e \"const fs=require('fs');['dist/cli.js','dist/mcp.js'].forEach(f=>fs.chmodSync(f,0o755))\"",
|
|
42
43
|
"prepublishOnly": "npm run build",
|
|
43
44
|
"dev": "tsc --watch",
|
|
44
|
-
"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"
|
|
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"
|
|
45
46
|
},
|
|
46
47
|
"dependencies": {
|
|
47
48
|
"@modelcontextprotocol/sdk": "^1.0.0",
|
|
@@ -1,45 +0,0 @@
|
|
|
1
|
-
/** session: ChatGPT data-export (conversations.json) parsing. */
|
|
2
|
-
import { test } from "node:test";
|
|
3
|
-
import assert from "node:assert/strict";
|
|
4
|
-
import { mkdtempSync, writeFileSync } from "node:fs";
|
|
5
|
-
import { tmpdir } from "node:os";
|
|
6
|
-
import { join } from "node:path";
|
|
7
|
-
import { parseSessionFile } from "../session.js";
|
|
8
|
-
function node(id, role, text, t) {
|
|
9
|
-
return [id, { id, message: { author: { role }, content: { content_type: "text", parts: [text] }, create_time: t } }];
|
|
10
|
-
}
|
|
11
|
-
const older = {
|
|
12
|
-
title: "Older chat",
|
|
13
|
-
update_time: 100,
|
|
14
|
-
default_model_slug: "gpt-4o",
|
|
15
|
-
mapping: Object.fromEntries([node("a", "user", "old question", 1)]),
|
|
16
|
-
};
|
|
17
|
-
const newer = {
|
|
18
|
-
title: "Trip planning",
|
|
19
|
-
update_time: 200,
|
|
20
|
-
default_model_slug: "gpt-5",
|
|
21
|
-
mapping: Object.fromEntries([
|
|
22
|
-
node("r", "system", "You are helpful.", 1),
|
|
23
|
-
node("x", "user", "Plan me a trip to Japan with a detailed itinerary please.", 2),
|
|
24
|
-
node("y", "assistant", "Day 1: Tokyo. Day 2: Kyoto. Day 3: Osaka with food tour.", 3),
|
|
25
|
-
["tool-node", { id: "tool-node", message: { author: { role: "tool" }, content: { content_type: "text", parts: ["ignored"] }, create_time: 4 } }],
|
|
26
|
-
]),
|
|
27
|
-
};
|
|
28
|
-
test("parses a ChatGPT export: newest conversation, ordered messages, model detected", () => {
|
|
29
|
-
const dir = mkdtempSync(join(tmpdir(), "ctxdoc-gpt-"));
|
|
30
|
-
const file = join(dir, "conversations.json");
|
|
31
|
-
writeFileSync(file, JSON.stringify([older, newer]));
|
|
32
|
-
const parsed = parseSessionFile(file);
|
|
33
|
-
assert.equal(parsed.title, "Trip planning");
|
|
34
|
-
assert.equal(parsed.model, "gpt-5");
|
|
35
|
-
assert.equal(parsed.messageCount, 3); // tool node excluded
|
|
36
|
-
const conv = JSON.parse(parsed.conversationJson);
|
|
37
|
-
assert.equal(conv.messages[0].role, "system");
|
|
38
|
-
assert.equal(conv.messages[1].content.includes("Japan"), true);
|
|
39
|
-
});
|
|
40
|
-
test("JSONL transcripts still parse (no regression)", () => {
|
|
41
|
-
const dir = mkdtempSync(join(tmpdir(), "ctxdoc-jsonl-"));
|
|
42
|
-
const file = join(dir, "s.jsonl");
|
|
43
|
-
writeFileSync(file, JSON.stringify({ type: "user", message: { role: "user", content: "hi" } }) + "\n");
|
|
44
|
-
assert.equal(parseSessionFile(file).messageCount, 1);
|
|
45
|
-
});
|
package/dist/test/doctor.test.js
DELETED
|
@@ -1,19 +0,0 @@
|
|
|
1
|
-
/** doctor must always produce a diagnosis and exit 0, even on a bare machine. */
|
|
2
|
-
import { test } from "node:test";
|
|
3
|
-
import assert from "node:assert/strict";
|
|
4
|
-
import { execFile } from "node:child_process";
|
|
5
|
-
import { mkdtempSync } from "node:fs";
|
|
6
|
-
import { tmpdir } from "node:os";
|
|
7
|
-
import { join, dirname } from "node:path";
|
|
8
|
-
import { fileURLToPath } from "node:url";
|
|
9
|
-
const cliPath = join(dirname(fileURLToPath(import.meta.url)), "..", "cli.js");
|
|
10
|
-
test("doctor runs, checks the MCP handshake, and exits 0", async () => {
|
|
11
|
-
const stateDir = mkdtempSync(join(tmpdir(), "ctxdoc-doctor-"));
|
|
12
|
-
const out = await new Promise((resolve, reject) => {
|
|
13
|
-
execFile(process.execPath, [cliPath, "doctor"], { env: { ...process.env, CONTEXT_DOCTOR_HOOK_STATE: join(stateDir, "state.json") }, timeout: 20000 }, (err, stdout) => (err ? reject(err) : resolve(stdout)));
|
|
14
|
-
});
|
|
15
|
-
assert.ok(out.includes("CONTEXT DOCTOR — self-check"));
|
|
16
|
-
assert.ok(out.includes("MCP server handshake"));
|
|
17
|
-
assert.ok(/✓ MCP server handshake/.test(out), "our own server must pass its own handshake");
|
|
18
|
-
assert.ok(out.includes("Ledger"));
|
|
19
|
-
});
|
package/dist/test/hook.test.d.ts
DELETED
package/dist/test/hook.test.js
DELETED
|
@@ -1,54 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Hook tests: the every-prompt Claude Code hook must stay silent on lean
|
|
3
|
-
* sessions, fire with guidance on heavy ones, and rate-limit re-fires.
|
|
4
|
-
*/
|
|
5
|
-
import { test } from "node:test";
|
|
6
|
-
import assert from "node:assert/strict";
|
|
7
|
-
import { execFile } from "node:child_process";
|
|
8
|
-
import { mkdtempSync, writeFileSync } from "node:fs";
|
|
9
|
-
import { tmpdir } from "node:os";
|
|
10
|
-
import { join, dirname } from "node:path";
|
|
11
|
-
import { fileURLToPath } from "node:url";
|
|
12
|
-
const cliPath = join(dirname(fileURLToPath(import.meta.url)), "..", "cli.js");
|
|
13
|
-
const dir = mkdtempSync(join(tmpdir(), "ctxdoc-hook-"));
|
|
14
|
-
const statePath = join(dir, "state.json");
|
|
15
|
-
function transcriptLine(role, content) {
|
|
16
|
-
return JSON.stringify({ type: role, message: { role, content } });
|
|
17
|
-
}
|
|
18
|
-
function runHook(transcriptPath, sessionId) {
|
|
19
|
-
return new Promise((resolve, reject) => {
|
|
20
|
-
const child = execFile(process.execPath, [cliPath, "hook"], { env: { ...process.env, CONTEXT_DOCTOR_HOOK_STATE: statePath } }, (err, stdout) => (err ? reject(err) : resolve(stdout)));
|
|
21
|
-
child.stdin.end(JSON.stringify({ session_id: sessionId, transcript_path: transcriptPath }));
|
|
22
|
-
});
|
|
23
|
-
}
|
|
24
|
-
// Lean session: a couple of small turns.
|
|
25
|
-
const leanPath = join(dir, "lean.jsonl");
|
|
26
|
-
writeFileSync(leanPath, [transcriptLine("user", "hi"), transcriptLine("assistant", "hello!")].join("\n"));
|
|
27
|
-
// Heavy session: ~100k tokens of transcript.
|
|
28
|
-
const heavyPath = join(dir, "heavy.jsonl");
|
|
29
|
-
const bigTurn = "We discussed the deployment pipeline and database migrations at length. ".repeat(80);
|
|
30
|
-
writeFileSync(heavyPath, Array.from({ length: 300 }, (_, i) => transcriptLine(i % 2 ? "assistant" : "user", bigTurn)).join("\n"));
|
|
31
|
-
test("hook stays silent on a lean session", async () => {
|
|
32
|
-
const out = await runHook(leanPath, "lean-session");
|
|
33
|
-
assert.equal(out.trim(), "");
|
|
34
|
-
});
|
|
35
|
-
test("hook fires with hygiene guidance on a heavy session", async () => {
|
|
36
|
-
const out = await runHook(heavyPath, "heavy-session");
|
|
37
|
-
const parsed = JSON.parse(out);
|
|
38
|
-
const ctx = parsed.hookSpecificOutput.additionalContext;
|
|
39
|
-
assert.equal(parsed.hookSpecificOutput.hookEventName, "UserPromptSubmit");
|
|
40
|
-
assert.ok(ctx.includes("<context-doctor>"));
|
|
41
|
-
assert.ok(/context is at ~\d/.test(ctx), "reports the measured size");
|
|
42
|
-
assert.ok(ctx.includes("context hygiene"));
|
|
43
|
-
});
|
|
44
|
-
test("hook rate-limits: second prompt in the same heavy session is silent", async () => {
|
|
45
|
-
const out = await runHook(heavyPath, "heavy-session");
|
|
46
|
-
assert.equal(out.trim(), "");
|
|
47
|
-
});
|
|
48
|
-
test("hook never errors on malformed input", async () => {
|
|
49
|
-
const out = await new Promise((resolve, reject) => {
|
|
50
|
-
const child = execFile(process.execPath, [cliPath, "hook"], (err, stdout) => err ? reject(err) : resolve(stdout));
|
|
51
|
-
child.stdin.end("this is not json");
|
|
52
|
-
});
|
|
53
|
-
assert.equal(out.trim(), "");
|
|
54
|
-
});
|
|
@@ -1,63 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* MCP streamable-HTTP transport: spawn `mcp.js --http`, run the initialize
|
|
3
|
-
* handshake and a tool call over plain HTTP, exactly as a URL-based client
|
|
4
|
-
* (e.g. a ChatGPT developer-mode connector) would.
|
|
5
|
-
*/
|
|
6
|
-
import { test, after } from "node:test";
|
|
7
|
-
import assert from "node:assert/strict";
|
|
8
|
-
import { spawn } from "node:child_process";
|
|
9
|
-
import { join, dirname } from "node:path";
|
|
10
|
-
import { fileURLToPath } from "node:url";
|
|
11
|
-
const mcpPath = join(dirname(fileURLToPath(import.meta.url)), "..", "mcp.js");
|
|
12
|
-
const PORT = 8898;
|
|
13
|
-
const child = spawn(process.execPath, [mcpPath, "--http", "--port", String(PORT)], { stdio: ["ignore", "ignore", "pipe"] });
|
|
14
|
-
await new Promise((resolve, reject) => {
|
|
15
|
-
const timer = setTimeout(() => reject(new Error("HTTP MCP server did not start")), 8000);
|
|
16
|
-
child.stderr.on("data", (d) => {
|
|
17
|
-
if (d.toString().includes("streamable HTTP")) {
|
|
18
|
-
clearTimeout(timer);
|
|
19
|
-
resolve();
|
|
20
|
-
}
|
|
21
|
-
});
|
|
22
|
-
});
|
|
23
|
-
after(() => child.kill());
|
|
24
|
-
async function rpc(body) {
|
|
25
|
-
const res = await fetch(`http://127.0.0.1:${PORT}/mcp`, {
|
|
26
|
-
method: "POST",
|
|
27
|
-
headers: { "content-type": "application/json", accept: "application/json, text/event-stream" },
|
|
28
|
-
body: JSON.stringify(body),
|
|
29
|
-
});
|
|
30
|
-
const text = await res.text();
|
|
31
|
-
// Streamable HTTP may answer as SSE ("data: {...}") or plain JSON.
|
|
32
|
-
const dataLine = text.split("\n").find((l) => l.startsWith("data: "));
|
|
33
|
-
return { status: res.status, json: JSON.parse(dataLine ? dataLine.slice(6) : text) };
|
|
34
|
-
}
|
|
35
|
-
test("initialize over HTTP returns server info + instructions", async () => {
|
|
36
|
-
const { status, json } = await rpc({
|
|
37
|
-
jsonrpc: "2.0",
|
|
38
|
-
id: 1,
|
|
39
|
-
method: "initialize",
|
|
40
|
-
params: { protocolVersion: "2024-11-05", capabilities: {}, clientInfo: { name: "t", version: "1" } },
|
|
41
|
-
});
|
|
42
|
-
assert.equal(status, 200);
|
|
43
|
-
assert.equal(json.result.serverInfo.name, "context-doctor");
|
|
44
|
-
assert.ok(json.result.instructions.includes("Context hygiene"));
|
|
45
|
-
});
|
|
46
|
-
test("tools/call works statelessly over HTTP", async () => {
|
|
47
|
-
const { json } = await rpc({
|
|
48
|
-
jsonrpc: "2.0",
|
|
49
|
-
id: 2,
|
|
50
|
-
method: "tools/call",
|
|
51
|
-
params: {
|
|
52
|
-
name: "profile_context",
|
|
53
|
-
arguments: { conversation: JSON.stringify({ messages: [{ role: "user", content: "hello world" }] }) },
|
|
54
|
-
},
|
|
55
|
-
});
|
|
56
|
-
assert.ok(json.result.content[0].text.includes("CONTEXT DOCTOR"));
|
|
57
|
-
});
|
|
58
|
-
test("health endpoint responds; non-POST is rejected", async () => {
|
|
59
|
-
const health = (await (await fetch(`http://127.0.0.1:${PORT}/health`)).json());
|
|
60
|
-
assert.equal(health.ok, true);
|
|
61
|
-
const get = await fetch(`http://127.0.0.1:${PORT}/mcp`);
|
|
62
|
-
assert.equal(get.status, 405);
|
|
63
|
-
});
|
package/dist/test/proxy.test.js
DELETED
|
@@ -1,121 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Proxy end-to-end test against a mock upstream: verifies in-flight
|
|
3
|
-
* optimization, tool_result preservation, header passthrough, SSE-style
|
|
4
|
-
* streaming, and the /stats endpoint.
|
|
5
|
-
*/
|
|
6
|
-
import { test, after } from "node:test";
|
|
7
|
-
import assert from "node:assert/strict";
|
|
8
|
-
import http from "node:http";
|
|
9
|
-
import { startProxy } from "../proxy.js";
|
|
10
|
-
const bigTool = "row of data | ".repeat(2000);
|
|
11
|
-
const doc = "TERMS: usage is billed monthly per seat with overage charged at cycle end. ".repeat(8);
|
|
12
|
-
const payload = JSON.stringify({
|
|
13
|
-
model: "claude-sonnet-5",
|
|
14
|
-
max_tokens: 100,
|
|
15
|
-
system: "You are helpful.",
|
|
16
|
-
messages: [
|
|
17
|
-
{ role: "user", content: "check the data\n" + doc },
|
|
18
|
-
{ role: "assistant", content: [{ type: "tool_use", id: "t1", name: "query_db", input: { q: "select *" } }] },
|
|
19
|
-
{ role: "user", content: [{ type: "tool_result", tool_use_id: "t1", content: bigTool }] },
|
|
20
|
-
{ role: "assistant", content: "Done." },
|
|
21
|
-
{ role: "user", content: "check the data\n" + doc },
|
|
22
|
-
...Array.from({ length: 7 }, (_, i) => ({ role: "user", content: `follow-up ${i}` })),
|
|
23
|
-
],
|
|
24
|
-
});
|
|
25
|
-
let received = "";
|
|
26
|
-
let receivedApiKey;
|
|
27
|
-
const upstream = http.createServer((req, res) => {
|
|
28
|
-
let body = "";
|
|
29
|
-
req.on("data", (c) => (body += c));
|
|
30
|
-
req.on("end", () => {
|
|
31
|
-
received = body;
|
|
32
|
-
receivedApiKey = req.headers["x-api-key"];
|
|
33
|
-
res.writeHead(200, { "content-type": "text/event-stream" });
|
|
34
|
-
res.write("event: message_start\ndata: {}\n\n");
|
|
35
|
-
res.write('event: message_delta\ndata: {"usage":{"input_tokens":120,"output_tokens":45}}\n\n');
|
|
36
|
-
res.write("event: message_stop\ndata: {}\n\n");
|
|
37
|
-
res.end();
|
|
38
|
-
});
|
|
39
|
-
});
|
|
40
|
-
await new Promise((r) => upstream.listen(0, r));
|
|
41
|
-
const upstreamPort = upstream.address().port;
|
|
42
|
-
const proxy = startProxy({ port: 0, anthropicUpstream: `http://localhost:${upstreamPort}` });
|
|
43
|
-
await new Promise((r) => proxy.once("listening", () => r()));
|
|
44
|
-
const proxyPort = proxy.address().port;
|
|
45
|
-
after(() => {
|
|
46
|
-
proxy.close();
|
|
47
|
-
upstream.close();
|
|
48
|
-
});
|
|
49
|
-
test("proxy optimizes in flight and passes through auth + streaming", async () => {
|
|
50
|
-
const resp = await fetch(`http://localhost:${proxyPort}/v1/messages`, {
|
|
51
|
-
method: "POST",
|
|
52
|
-
headers: { "content-type": "application/json", "x-api-key": "sk-test-not-real", "anthropic-version": "2023-06-01" },
|
|
53
|
-
body: payload,
|
|
54
|
-
});
|
|
55
|
-
const respText = await resp.text();
|
|
56
|
-
assert.ok(received.length < payload.length, "upstream received a smaller body");
|
|
57
|
-
const parsed = JSON.parse(received);
|
|
58
|
-
const toolBlock = parsed.messages[2].content[0];
|
|
59
|
-
assert.equal(toolBlock.type, "tool_result");
|
|
60
|
-
assert.equal(toolBlock.tool_use_id, "t1");
|
|
61
|
-
assert.equal(parsed.model, "claude-sonnet-5");
|
|
62
|
-
assert.equal(receivedApiKey, "sk-test-not-real");
|
|
63
|
-
assert.equal(resp.status, 200);
|
|
64
|
-
assert.ok(respText.includes("message_start") && respText.includes("message_stop"), "SSE streamed through");
|
|
65
|
-
});
|
|
66
|
-
test("/stats reports cumulative savings with dollar estimate", async () => {
|
|
67
|
-
const stats = await (await fetch(`http://localhost:${proxyPort}/stats`)).json();
|
|
68
|
-
assert.equal(stats.requests, 1);
|
|
69
|
-
assert.equal(stats.optimizedRequests, 1);
|
|
70
|
-
assert.ok(stats.tokensSaved > 1000, `saved tokens tracked (${stats.tokensSaved})`);
|
|
71
|
-
assert.ok(stats.estUsdSaved > 0, "dollar savings estimated from the request's model");
|
|
72
|
-
});
|
|
73
|
-
test("response usage is captured from the SSE stream; cache advisor fires on prefix churn", async () => {
|
|
74
|
-
// Second request with a DIFFERENT system prompt on the same model → advisory.
|
|
75
|
-
const churned = JSON.parse(payload);
|
|
76
|
-
churned.system = "You are helpful. TODAY IS A NEW DAY."; // classic cache-buster
|
|
77
|
-
churned.tools = [{ name: "t", description: "x".repeat(5000), input_schema: { type: "object" } }];
|
|
78
|
-
const first = JSON.parse(payload);
|
|
79
|
-
first.tools = churned.tools;
|
|
80
|
-
for (const body of [first, churned]) {
|
|
81
|
-
await fetch(`http://localhost:${proxyPort}/v1/messages`, {
|
|
82
|
-
method: "POST",
|
|
83
|
-
headers: { "content-type": "application/json", "x-api-key": "sk-test-not-real" },
|
|
84
|
-
body: JSON.stringify(body),
|
|
85
|
-
});
|
|
86
|
-
}
|
|
87
|
-
const stats = (await (await fetch(`http://localhost:${proxyPort}/stats`)).json());
|
|
88
|
-
// Mock upstream reports usage in its SSE close event (added below).
|
|
89
|
-
assert.ok(stats.upstreamInputTokens >= 100, `usage input captured: ${stats.upstreamInputTokens}`);
|
|
90
|
-
assert.ok(stats.upstreamOutputTokens >= 40, `usage output captured: ${stats.upstreamOutputTokens}`);
|
|
91
|
-
assert.ok(stats.advice.some((a) => a.includes("prefix changed")), `prefix-churn advisory expected, got: ${JSON.stringify(stats.advice)}`);
|
|
92
|
-
assert.ok(stats.advice.some((a) => a.includes("cache_control")), "missing-cache_control advisory expected");
|
|
93
|
-
});
|
|
94
|
-
test("per-route config: empty strategy list disables optimization for matching models", async () => {
|
|
95
|
-
const { startProxy } = await import("../proxy.js");
|
|
96
|
-
const routed = startProxy({
|
|
97
|
-
port: 0,
|
|
98
|
-
anthropicUpstream: `http://localhost:${upstreamPort}`,
|
|
99
|
-
routes: [{ modelPrefix: "claude-sonnet", strategies: [] }],
|
|
100
|
-
});
|
|
101
|
-
await new Promise((r) => routed.once("listening", () => r()));
|
|
102
|
-
const routedPort = routed.address().port;
|
|
103
|
-
try {
|
|
104
|
-
const sent = payload;
|
|
105
|
-
await fetch(`http://localhost:${routedPort}/v1/messages`, {
|
|
106
|
-
method: "POST",
|
|
107
|
-
headers: { "content-type": "application/json" },
|
|
108
|
-
body: sent,
|
|
109
|
-
});
|
|
110
|
-
assert.equal(received.length, sent.length, "route with no strategies must pass body through unmodified");
|
|
111
|
-
}
|
|
112
|
-
finally {
|
|
113
|
-
routed.close();
|
|
114
|
-
}
|
|
115
|
-
});
|
|
116
|
-
test("unsupported paths get a clear 404, health stays up", async () => {
|
|
117
|
-
const notFound = await fetch(`http://localhost:${proxyPort}/v1/nope`, { method: "POST", body: "{}" });
|
|
118
|
-
assert.equal(notFound.status, 404);
|
|
119
|
-
const health = await (await fetch(`http://localhost:${proxyPort}/health`)).json();
|
|
120
|
-
assert.equal(health.ok, true);
|
|
121
|
-
});
|
package/dist/test/smoke.test.js
DELETED
|
@@ -1,111 +0,0 @@
|
|
|
1
|
-
/** Smoke tests: parse → profile → optimize roundtrip for both provider formats. */
|
|
2
|
-
import { test } from "node:test";
|
|
3
|
-
import assert from "node:assert/strict";
|
|
4
|
-
import { parseConversation } from "../parse.js";
|
|
5
|
-
import { profileConversation } from "../profile.js";
|
|
6
|
-
import { optimizeConversation } from "../optimize.js";
|
|
7
|
-
// Large enough to clear the profiler's 2000-token oversized-tool-result threshold.
|
|
8
|
-
const bigText = "Sunny, 18C. ".repeat(900);
|
|
9
|
-
const openaiConv = JSON.stringify({
|
|
10
|
-
messages: [
|
|
11
|
-
{ role: "system", content: "You are helpful." },
|
|
12
|
-
{ role: "user", content: "weather in SF?" },
|
|
13
|
-
{ role: "assistant", content: null, tool_calls: [{ id: "c1", type: "function", function: { name: "get_weather", arguments: '{"city":"SF"}' } }] },
|
|
14
|
-
{ role: "tool", tool_call_id: "c1", content: bigText },
|
|
15
|
-
{ role: "assistant", content: "It is sunny and 18C in SF." },
|
|
16
|
-
],
|
|
17
|
-
});
|
|
18
|
-
const anthropicConv = JSON.stringify({
|
|
19
|
-
system: "You are helpful.",
|
|
20
|
-
messages: [
|
|
21
|
-
{ role: "user", content: "weather in SF?" },
|
|
22
|
-
{ role: "assistant", content: [{ type: "tool_use", id: "t1", name: "get_weather", input: { city: "SF" } }] },
|
|
23
|
-
{ role: "user", content: [{ type: "tool_result", tool_use_id: "t1", content: bigText }] },
|
|
24
|
-
{ role: "assistant", content: "Sunny and 18C." },
|
|
25
|
-
],
|
|
26
|
-
});
|
|
27
|
-
test("parses OpenAI format and classifies tool plumbing", () => {
|
|
28
|
-
const conv = parseConversation(openaiConv);
|
|
29
|
-
assert.equal(conv.sourceFormat, "openai");
|
|
30
|
-
assert.equal(conv.messages.filter((m) => m.kind === "tool_result").length, 1);
|
|
31
|
-
assert.equal(conv.messages.filter((m) => m.kind === "tool_call").length, 1);
|
|
32
|
-
});
|
|
33
|
-
test("parses Anthropic format including external system prompt", () => {
|
|
34
|
-
const conv = parseConversation(anthropicConv);
|
|
35
|
-
assert.equal(conv.sourceFormat, "anthropic");
|
|
36
|
-
assert.equal(conv.messages[0].kind, "system");
|
|
37
|
-
assert.ok(conv.messages.some((m) => m.toolName === "get_weather"));
|
|
38
|
-
});
|
|
39
|
-
test("profiler flags oversized tool results", () => {
|
|
40
|
-
const profile = profileConversation(parseConversation(openaiConv), "gpt-4o");
|
|
41
|
-
assert.ok(profile.totalTokens > 500);
|
|
42
|
-
assert.equal(profile.contextWindow, 128_000);
|
|
43
|
-
assert.ok(profile.findings.some((f) => f.id === "large_tool_result"));
|
|
44
|
-
});
|
|
45
|
-
test("optimizer trims stale tool results and reports savings", () => {
|
|
46
|
-
const result = optimizeConversation(openaiConv, { keepRecent: 1, maxToolResultTokens: 50 });
|
|
47
|
-
assert.ok(result.tokensAfter < result.tokensBefore);
|
|
48
|
-
assert.ok(result.applied.some((c) => c.strategy === "trim-tool-results"));
|
|
49
|
-
// Output must remain valid JSON with the same message count.
|
|
50
|
-
const out = result.conversation;
|
|
51
|
-
assert.equal(out.messages.length, 5);
|
|
52
|
-
});
|
|
53
|
-
test("optimizer output for Anthropic format keeps block structure valid", () => {
|
|
54
|
-
const result = optimizeConversation(anthropicConv, { keepRecent: 1, maxToolResultTokens: 50 });
|
|
55
|
-
const out = result.conversation;
|
|
56
|
-
for (const m of out.messages) {
|
|
57
|
-
assert.ok(typeof m.content === "string" || Array.isArray(m.content));
|
|
58
|
-
}
|
|
59
|
-
// The trimmed tool_result must KEEP its block type and tool_use_id — the
|
|
60
|
-
// Anthropic API rejects a tool_use with no matching tool_result.
|
|
61
|
-
const toolResultMsg = out.messages[2].content;
|
|
62
|
-
assert.equal(toolResultMsg[0].type, "tool_result");
|
|
63
|
-
assert.equal(toolResultMsg[0].tool_use_id, "t1");
|
|
64
|
-
assert.ok(toolResultMsg[0].content.length < 1000, "tool_result content was trimmed");
|
|
65
|
-
// And the tool_use block on the assistant side is untouched.
|
|
66
|
-
const assistantMsg = out.messages[1].content;
|
|
67
|
-
assert.equal(assistantMsg[0].type, "tool_use");
|
|
68
|
-
});
|
|
69
|
-
test("prune-history never leaves an orphaned tool result at the head of the tail", () => {
|
|
70
|
-
// Build a conversation where the naive prune boundary would land exactly on
|
|
71
|
-
// a tool-result message (its tool_use call falling in the pruned half).
|
|
72
|
-
const filler = "some earlier discussion that will be pruned away. ".repeat(20);
|
|
73
|
-
const conv = JSON.stringify({
|
|
74
|
-
messages: [
|
|
75
|
-
...Array.from({ length: 8 }, (_, i) => ({ role: i % 2 ? "assistant" : "user", content: `${i} ${filler}` })),
|
|
76
|
-
{ role: "assistant", content: [{ type: "tool_use", id: "tX", name: "search", input: { q: "x" } }] },
|
|
77
|
-
{ role: "user", content: [{ type: "tool_result", tool_use_id: "tX", content: "results here" }] }, // naive boundary lands HERE
|
|
78
|
-
{ role: "assistant", content: "Summary of results." },
|
|
79
|
-
{ role: "user", content: "thanks" },
|
|
80
|
-
{ role: "assistant", content: "welcome" },
|
|
81
|
-
],
|
|
82
|
-
});
|
|
83
|
-
const result = optimizeConversation(conv, { strategies: ["prune-history"], keepRecent: 4 });
|
|
84
|
-
const out = result.conversation;
|
|
85
|
-
// First kept message after the stub must NOT be a tool result.
|
|
86
|
-
const firstKept = out.messages[1].content;
|
|
87
|
-
const isToolResult = Array.isArray(firstKept) && firstKept.some((b) => b?.type === "tool_result");
|
|
88
|
-
assert.equal(isToolResult, false, "tail must not start with an orphaned tool_result");
|
|
89
|
-
assert.ok(result.applied.some((c) => c.strategy === "prune-history"), "pruning still happened");
|
|
90
|
-
});
|
|
91
|
-
test("near-duplicate detection catches same doc with different lead-ins", () => {
|
|
92
|
-
// Varied clauses (not a repeated sentence) — like a real document.
|
|
93
|
-
const doc = Array.from({ length: 30 }, (_, i) => `Clause ${i} of the pricing policy covers refund scenario ${i} where the customer holds receipt series ${i * 7} under regional rule ${i % 5}.`).join(" ");
|
|
94
|
-
const conv = JSON.stringify({
|
|
95
|
-
messages: [
|
|
96
|
-
{ role: "user", content: "Here is our policy document for you to review:\n" + doc },
|
|
97
|
-
{ role: "assistant", content: "Understood, thanks for sharing the policy." },
|
|
98
|
-
{ role: "user", content: "Sharing the policy doc again with a totally different intro so exact hashing misses it:\n" + doc },
|
|
99
|
-
],
|
|
100
|
-
});
|
|
101
|
-
const profile = profileConversation(parseConversation(conv));
|
|
102
|
-
const near = profile.findings.find((f) => f.id === "near_duplicate");
|
|
103
|
-
assert.ok(near, "near_duplicate finding expected");
|
|
104
|
-
assert.deepEqual(near.messages, [0, 2]);
|
|
105
|
-
assert.ok(near.estSavings > 50);
|
|
106
|
-
});
|
|
107
|
-
test("raw text input still profiles", () => {
|
|
108
|
-
const profile = profileConversation(parseConversation("just some prompt text"));
|
|
109
|
-
assert.equal(profile.messageCount, 1);
|
|
110
|
-
assert.ok(profile.totalTokens > 0);
|
|
111
|
-
});
|
package/dist/test/watch.test.js
DELETED
|
@@ -1,36 +0,0 @@
|
|
|
1
|
-
/** watch: emits a status line on growth, surfaces new findings once. */
|
|
2
|
-
import { test } from "node:test";
|
|
3
|
-
import assert from "node:assert/strict";
|
|
4
|
-
import { spawn } from "node:child_process";
|
|
5
|
-
import { appendFileSync, mkdtempSync, writeFileSync } from "node:fs";
|
|
6
|
-
import { tmpdir } from "node:os";
|
|
7
|
-
import { join, dirname } from "node:path";
|
|
8
|
-
import { fileURLToPath } from "node:url";
|
|
9
|
-
const cliPath = join(dirname(fileURLToPath(import.meta.url)), "..", "cli.js");
|
|
10
|
-
function line(role, content) {
|
|
11
|
-
return JSON.stringify({ type: role, message: { role, content } }) + "\n";
|
|
12
|
-
}
|
|
13
|
-
test("watch reports growth and new findings live", async () => {
|
|
14
|
-
const dir = mkdtempSync(join(tmpdir(), "ctxdoc-watch-"));
|
|
15
|
-
const file = join(dir, "trace.jsonl");
|
|
16
|
-
writeFileSync(file, line("user", "hello there"));
|
|
17
|
-
const child = spawn(process.execPath, [cliPath, "watch", file, "--interval-ms", "150"], { stdio: ["ignore", "pipe", "pipe"] });
|
|
18
|
-
let out = "";
|
|
19
|
-
child.stdout.on("data", (d) => (out += d.toString()));
|
|
20
|
-
try {
|
|
21
|
-
// First tick: initial line.
|
|
22
|
-
await new Promise((r) => setTimeout(r, 500));
|
|
23
|
-
assert.ok(/tokens/.test(out), `initial status line expected, got: ${out}`);
|
|
24
|
-
// Grow the file with an oversized tool result → new status + a finding.
|
|
25
|
-
appendFileSync(file, line("assistant", JSON.stringify([{ type: "tool_use", id: "t1", name: "search", input: {} }])) +
|
|
26
|
-
JSON.stringify({ type: "user", message: { role: "user", content: [{ type: "tool_result", tool_use_id: "t1", content: "data ".repeat(3000) }] } }) +
|
|
27
|
-
"\n");
|
|
28
|
-
await new Promise((r) => setTimeout(r, 700));
|
|
29
|
-
const statusLines = out.split("\n").filter((l) => l.includes("tokens"));
|
|
30
|
-
assert.ok(statusLines.length >= 2, `expected a second status line after growth: ${out}`);
|
|
31
|
-
assert.ok(out.includes("⚠"), `expected a finding to surface: ${out}`);
|
|
32
|
-
}
|
|
33
|
-
finally {
|
|
34
|
-
child.kill();
|
|
35
|
-
}
|
|
36
|
-
});
|