@echomem/mcp 1.4.1 → 1.4.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.
@@ -0,0 +1,125 @@
1
+ // Context X-ray report — scans many local sessions and aggregates honest, data-backed numbers.
2
+ // Everything here is measured from the user's own logs. Pollution is a tracked lower bound; the
3
+ // Echo-usage split is observational (not a controlled A/B). No projections, no fabricated savings.
4
+ import fs from "node:fs";
5
+ import { adapters } from "./adapters.js";
6
+ import { formatTokens } from "./metric.js";
7
+ // A session "used Echo" if it called an EchoMem MCP tool (memory search / save_conversation).
8
+ const ECHO_TOOL = /memor|^save_conversation$|^echomem|^mcp__echomem/i;
9
+ export function runReport(mode, opts = {}) {
10
+ const limit = opts.limit && opts.limit > 0 ? opts.limit : 40;
11
+ const client = pickClient(mode);
12
+ const adapter = adapters[client];
13
+ const files = adapter
14
+ .findAll()
15
+ .map((f) => ({ f, m: safeMtime(f) }))
16
+ .sort((a, b) => b.m - a.m)
17
+ .slice(0, limit)
18
+ .map((x) => x.f);
19
+ const scores = [];
20
+ for (const file of files) {
21
+ try {
22
+ scores.push(adapter.score(file));
23
+ }
24
+ catch {
25
+ // Skip unreadable / partially-written sessions.
26
+ }
27
+ }
28
+ const ct = scores.map((s) => s.ctTokens || 0).filter((n) => n > 0);
29
+ const pollution = scores.map((s) => s.pollutionPct || 0);
30
+ const outAgg = {};
31
+ const toolAgg = {};
32
+ const echoPol = [];
33
+ const coldPol = [];
34
+ let reclaimableTotal = 0;
35
+ let echoSessions = 0;
36
+ for (const s of scores) {
37
+ reclaimableTotal += s.pollutionTok || 0;
38
+ for (const [k, v] of Object.entries(s.outputTokensByTool || {}))
39
+ outAgg[k] = (outAgg[k] || 0) + v;
40
+ for (const [k, v] of Object.entries(s.tools || {}))
41
+ toolAgg[k] = (toolAgg[k] || 0) + v;
42
+ const usedEcho = Object.keys(s.tools || {}).some((t) => ECHO_TOOL.test(t));
43
+ if (usedEcho) {
44
+ echoSessions += 1;
45
+ echoPol.push(s.pollutionPct || 0);
46
+ }
47
+ else {
48
+ coldPol.push(s.pollutionPct || 0);
49
+ }
50
+ }
51
+ return {
52
+ client,
53
+ sessions: files.length,
54
+ scored: scores.length,
55
+ window: { median: median(ct), p90: percentile(ct, 90), max: ct.length ? Math.max(...ct) : 0 },
56
+ reclaimableTotal,
57
+ pollutionMedian: median(pollution),
58
+ outputByTool: topEntries(outAgg, 8),
59
+ toolsTop: topEntries(toolAgg, 8),
60
+ echoSessions,
61
+ echoPollutionMedian: echoPol.length ? median(echoPol) : null,
62
+ coldPollutionMedian: coldPol.length ? median(coldPol) : null,
63
+ generatedAt: new Date().toISOString(),
64
+ };
65
+ }
66
+ export function renderReportText(r) {
67
+ const lines = [];
68
+ lines.push(`EchoMem Context X-ray — ${r.client} · ${r.scored}/${r.sessions} sessions analyzed`);
69
+ lines.push("");
70
+ lines.push("Window size");
71
+ lines.push(` median ${formatTokens(r.window.median)} · p90 ${formatTokens(r.window.p90)} · largest ${formatTokens(r.window.max)}`);
72
+ lines.push("");
73
+ lines.push("Provably reclaimable — repeated rereads (tracked lower bound)");
74
+ lines.push(` ≈ ${formatTokens(r.reclaimableTotal)} across these sessions · median ${r.pollutionMedian}% of window / session`);
75
+ lines.push("");
76
+ lines.push("Output volume by tool — what filled context (≈ tokens, cumulative)");
77
+ for (const [k, v] of r.outputByTool)
78
+ lines.push(` ${k.padEnd(30)} ${formatTokens(v)}`);
79
+ lines.push("");
80
+ lines.push("Most-used tools (calls)");
81
+ for (const [k, v] of r.toolsTop)
82
+ lines.push(` ${k.padEnd(30)} ${v}`);
83
+ lines.push("");
84
+ lines.push("Echo usage (observational — your own sessions, not a controlled A/B)");
85
+ lines.push(` ${r.echoSessions}/${r.scored} sessions called Echo (memory search / save_conversation)`);
86
+ if (r.echoPollutionMedian !== null && r.coldPollutionMedian !== null) {
87
+ lines.push(` median pollution — with Echo ${r.echoPollutionMedian}% · without ${r.coldPollutionMedian}%`);
88
+ }
89
+ lines.push("");
90
+ lines.push("Honesty: pollution = tracked repeated-read lower bound (provider eviction is unobservable).");
91
+ lines.push("Output volume is cumulative tool output seen over each session, not current-window-only.");
92
+ return lines.join("\n");
93
+ }
94
+ function pickClient(mode) {
95
+ if (mode === "codex" || mode === "claude-code" || mode === "claude-desktop")
96
+ return mode;
97
+ // auto / both → the client with the most sessions on disk
98
+ const counts = Object.keys(adapters).map((c) => [c, adapters[c].findAll().length]);
99
+ counts.sort((a, b) => b[1] - a[1]);
100
+ return counts[0] && counts[0][1] > 0 ? counts[0][0] : "codex";
101
+ }
102
+ function safeMtime(file) {
103
+ try {
104
+ return fs.statSync(file).mtimeMs;
105
+ }
106
+ catch {
107
+ return 0;
108
+ }
109
+ }
110
+ function median(xs) {
111
+ if (!xs.length)
112
+ return 0;
113
+ const a = [...xs].sort((x, y) => x - y);
114
+ const m = Math.floor(a.length / 2);
115
+ return a.length % 2 ? a[m] : Math.round((a[m - 1] + a[m]) / 2);
116
+ }
117
+ function percentile(xs, p) {
118
+ if (!xs.length)
119
+ return 0;
120
+ const a = [...xs].sort((x, y) => x - y);
121
+ return a[Math.min(a.length - 1, Math.floor((p / 100) * a.length))];
122
+ }
123
+ function topEntries(map, n) {
124
+ return Object.entries(map).filter((e) => e[1] > 0).sort((a, b) => b[1] - a[1]).slice(0, n);
125
+ }
@@ -0,0 +1,95 @@
1
+ import http from "node:http";
2
+ import fs from "node:fs";
3
+ import { fileURLToPath } from "node:url";
4
+ import { HudMonitor } from "./monitor.js";
5
+ import { HUD_HTML } from "./web.js";
6
+ import { buildCapsuleText } from "./capsule.js";
7
+ export async function createHudServer(opts = {}) {
8
+ const mode = opts.mode || "auto";
9
+ const port = opts.port ?? 17377;
10
+ const clients = new Set();
11
+ const monitor = new HudMonitor(mode, opts.pollMs ?? 750);
12
+ let latest = monitor.snapshot();
13
+ monitor.on("state", (state) => {
14
+ latest = state;
15
+ const payload = `data: ${JSON.stringify(latest)}\n\n`;
16
+ for (const res of clients)
17
+ res.write(payload);
18
+ });
19
+ const server = http.createServer((req, res) => {
20
+ const url = new URL(req.url || "/", `http://${req.headers.host || "127.0.0.1"}`);
21
+ if (serveHudAsset(url.pathname, res))
22
+ return;
23
+ if (url.pathname === "/events") {
24
+ res.writeHead(200, {
25
+ "content-type": "text/event-stream",
26
+ "cache-control": "no-cache",
27
+ connection: "keep-alive",
28
+ });
29
+ res.write(`data: ${JSON.stringify(latest)}\n\n`);
30
+ clients.add(res);
31
+ req.on("close", () => clients.delete(res));
32
+ return;
33
+ }
34
+ if (url.pathname === "/state") {
35
+ res.writeHead(200, { "content-type": "application/json" });
36
+ res.end(JSON.stringify(latest, null, 2));
37
+ return;
38
+ }
39
+ if (url.pathname === "/capsule") {
40
+ const active = latest.active;
41
+ if (!active) {
42
+ res.writeHead(200, { "content-type": "application/json" });
43
+ res.end(JSON.stringify({ ok: false, reason: "no_active_session" }));
44
+ return;
45
+ }
46
+ const text = buildCapsuleText(active);
47
+ res.writeHead(200, { "content-type": "application/json" });
48
+ res.end(JSON.stringify({ ok: true, capsule: text }));
49
+ return;
50
+ }
51
+ res.writeHead(200, { "content-type": "text/html; charset=utf-8" });
52
+ res.end(HUD_HTML);
53
+ });
54
+ await new Promise((resolve, reject) => {
55
+ server.once("error", reject);
56
+ server.listen(port, "127.0.0.1", () => {
57
+ server.off("error", reject);
58
+ resolve();
59
+ });
60
+ });
61
+ monitor.start();
62
+ const address = server.address();
63
+ const actualPort = typeof address === "object" && address ? address.port : port;
64
+ return {
65
+ port: actualPort,
66
+ url: `http://127.0.0.1:${actualPort}`,
67
+ state: () => latest,
68
+ close: () => new Promise((resolve) => {
69
+ monitor.stop();
70
+ for (const res of clients)
71
+ res.end();
72
+ server.close(() => resolve());
73
+ }),
74
+ };
75
+ }
76
+ function serveHudAsset(pathname, res) {
77
+ const assets = {
78
+ "/assets/hud/echo-face-cutout.png": "../../assets/hud/echo-face-cutout.png",
79
+ };
80
+ const asset = assets[pathname];
81
+ if (!asset)
82
+ return false;
83
+ const assetPath = fileURLToPath(new URL(asset, import.meta.url));
84
+ if (!fs.existsSync(assetPath)) {
85
+ res.writeHead(404, { "content-type": "text/plain; charset=utf-8" });
86
+ res.end("HUD asset not found");
87
+ return true;
88
+ }
89
+ res.writeHead(200, {
90
+ "content-type": "image/png",
91
+ "cache-control": "public, max-age=31536000, immutable",
92
+ });
93
+ fs.createReadStream(assetPath).pipe(res);
94
+ return true;
95
+ }