@echomem/mcp 1.4.7 → 1.4.9
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 +35 -9
- package/assets/canonical-scorer/README.md +18 -0
- package/assets/canonical-scorer/analyze-10-problems.mjs +857 -0
- package/assets/canonical-scorer/build-session-waste-dashboard.mjs +1628 -0
- package/assets/canonical-scorer/golden_anchors.mjs +83 -0
- package/assets/canonical-scorer/optimizable_detail.mjs +633 -0
- package/assets/hud/claude.svg +1 -0
- package/assets/hud/codex.svg +1 -0
- package/assets/hud/session-viewer.html +35 -0
- package/dist/city/chaos-to-clarity-pencil.html +582 -0
- package/dist/city/echo-ai-city-only.html +1126 -109
- package/dist/city/echo-ai-city-only.template.html +1126 -109
- package/dist/city/echo-face-cutout.png +0 -0
- package/dist/city/pencil-pie-generator.html +883 -0
- package/dist/city/pencil-webgl-landscape.html +1239 -0
- package/dist/city/spatial-fan-story.html +479 -0
- package/dist/codex-session-files.js +283 -0
- package/dist/codex-sync.js +7 -2
- package/dist/context-analysis/canonical-golden.js +47 -0
- package/dist/context-analysis/claude-native-canonical.js +1193 -0
- package/dist/context-analysis/vendored-canonical.js +793 -0
- package/dist/context-analysis/workspace-report.js +1838 -0
- package/dist/context-metrics/calculate.js +56 -0
- package/dist/context-metrics/model-limits.js +26 -0
- package/dist/context-metrics/types.js +1 -0
- package/dist/forensics-10-problems.js +7 -6
- package/dist/forensics.js +863 -132
- package/dist/hud/adapters.js +8 -4
- package/dist/hud/autostart.js +66 -0
- package/dist/hud/cli.js +31 -0
- package/dist/hud/electron-main.js +182 -19
- package/dist/hud/metric.js +13 -4
- package/dist/hud/monitor.js +171 -84
- package/dist/hud/preload.cjs +3 -0
- package/dist/hud/server.js +321 -4
- package/dist/hud/web.js +880 -270
- package/dist/index.js +122 -24
- package/dist/local-data-paths.js +87 -0
- package/dist/migrate.js +55 -29
- package/dist/report.js +101 -40
- package/dist/setup-page.js +4257 -245
- package/dist/setup-preview.js +245 -0
- package/dist/setup.js +786 -75
- package/dist/v1-contract.js +20 -2
- package/package.json +6 -4
- package/templates/echomem-recall.md +2 -2
package/dist/report.js
CHANGED
|
@@ -16,10 +16,11 @@
|
|
|
16
16
|
* labeled — not a measurement on this account.
|
|
17
17
|
*/
|
|
18
18
|
import fs from "node:fs";
|
|
19
|
-
import os from "node:os";
|
|
20
19
|
import path from "node:path";
|
|
21
20
|
import { StringDecoder } from "node:string_decoder";
|
|
22
21
|
import axios from "axios";
|
|
22
|
+
import { discoverCodexSessionFiles } from "./codex-session-files.js";
|
|
23
|
+
import { resolveClaudeProjectsDir } from "./local-data-paths.js";
|
|
23
24
|
import { KeyStore } from "./keystore.js";
|
|
24
25
|
const API_BASE = (process.env.ECHO_API_BASE_URL || "https://echo-mem-chrome.vercel.app").replace(/\/$/, "");
|
|
25
26
|
// Early signal from the controlled sub-agent experiment, NOT measured on this account.
|
|
@@ -120,6 +121,14 @@ function epochMs(ts) {
|
|
|
120
121
|
const n = Date.parse(ts);
|
|
121
122
|
return Number.isFinite(n) ? n : null;
|
|
122
123
|
}
|
|
124
|
+
function localDate(ts) {
|
|
125
|
+
const ms = epochMs(ts);
|
|
126
|
+
if (ms == null)
|
|
127
|
+
return null;
|
|
128
|
+
const d = new Date(ms);
|
|
129
|
+
const pad = (value) => String(value).padStart(2, "0");
|
|
130
|
+
return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}`;
|
|
131
|
+
}
|
|
123
132
|
function durationBetween(startTs, lastTs) {
|
|
124
133
|
const start = epochMs(startTs);
|
|
125
134
|
const last = epochMs(lastTs);
|
|
@@ -194,12 +203,21 @@ export function parseCodex(file) {
|
|
|
194
203
|
let acts = 0;
|
|
195
204
|
eachLine(file, (o) => {
|
|
196
205
|
if (typeof o.timestamp === "string") {
|
|
197
|
-
|
|
206
|
+
const ms = epochMs(o.timestamp);
|
|
207
|
+
if (ms != null && (meta.first === null || ms < (epochMs(meta.first) ?? Number.POSITIVE_INFINITY)))
|
|
198
208
|
meta.first = o.timestamp;
|
|
199
|
-
meta.last
|
|
209
|
+
if (ms != null && (meta.last === null || ms > (epochMs(meta.last) ?? Number.NEGATIVE_INFINITY)))
|
|
210
|
+
meta.last = o.timestamp;
|
|
200
211
|
}
|
|
201
|
-
if (o && o.type === "session_meta" && o.payload && typeof o.payload
|
|
202
|
-
|
|
212
|
+
if (o && o.type === "session_meta" && o.payload && typeof o.payload === "object") {
|
|
213
|
+
if (typeof o.payload.cwd === "string")
|
|
214
|
+
meta.cwd = o.payload.cwd;
|
|
215
|
+
if (typeof o.payload.timestamp === "string") {
|
|
216
|
+
const launchMs = epochMs(o.payload.timestamp);
|
|
217
|
+
if (launchMs != null && (meta.first === null || launchMs < (epochMs(meta.first) ?? Number.POSITIVE_INFINITY))) {
|
|
218
|
+
meta.first = o.payload.timestamp;
|
|
219
|
+
}
|
|
220
|
+
}
|
|
203
221
|
return;
|
|
204
222
|
}
|
|
205
223
|
const p = o && typeof o.payload === "object" && o.payload ? o.payload : o;
|
|
@@ -224,8 +242,8 @@ export function parseCodex(file) {
|
|
|
224
242
|
return null;
|
|
225
243
|
return {
|
|
226
244
|
source: "codex",
|
|
227
|
-
date: meta.first
|
|
228
|
-
month: meta.first
|
|
245
|
+
date: localDate(meta.first),
|
|
246
|
+
month: localDate(meta.first)?.slice(0, 7) ?? null,
|
|
229
247
|
startTs: meta.first,
|
|
230
248
|
lastTs: meta.last,
|
|
231
249
|
durationSec: durationBetween(meta.first, meta.last),
|
|
@@ -240,9 +258,9 @@ export function parseCodex(file) {
|
|
|
240
258
|
}
|
|
241
259
|
/** Claude Code: usage is PER assistant turn (SUM); tool_use blocks live in message.content. */
|
|
242
260
|
export function parseClaude(file) {
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
let
|
|
261
|
+
const usageByRequest = new Map();
|
|
262
|
+
const seenToolUses = new Set();
|
|
263
|
+
let anonymousRequestSeq = 0;
|
|
246
264
|
let echo = 0;
|
|
247
265
|
let reads = 0;
|
|
248
266
|
let acts = 0;
|
|
@@ -253,16 +271,26 @@ export function parseClaude(file) {
|
|
|
253
271
|
durationSec: 0,
|
|
254
272
|
prevMs: null,
|
|
255
273
|
};
|
|
256
|
-
|
|
274
|
+
const requestKey = (row) => {
|
|
275
|
+
const messageId = typeof row.message?.id === "string" ? row.message.id.trim() : "";
|
|
276
|
+
if (messageId)
|
|
277
|
+
return `message:${messageId}`;
|
|
278
|
+
const requestId = typeof row.requestId === "string" ? row.requestId.trim() : "";
|
|
279
|
+
if (requestId)
|
|
280
|
+
return `request:${requestId}`;
|
|
281
|
+
const uuid = typeof row.uuid === "string" ? row.uuid.trim() : "";
|
|
282
|
+
return uuid ? `event:${uuid}` : `anonymous:${++anonymousRequestSeq}`;
|
|
283
|
+
};
|
|
257
284
|
eachLine(file, (o) => {
|
|
258
285
|
if (!meta.cwd && typeof o.cwd === "string")
|
|
259
286
|
meta.cwd = o.cwd;
|
|
260
287
|
if (typeof o.timestamp === "string") {
|
|
261
|
-
if (meta.first === null)
|
|
262
|
-
meta.first = o.timestamp;
|
|
263
|
-
meta.last = o.timestamp;
|
|
264
288
|
const ms = epochMs(o.timestamp);
|
|
265
289
|
if (ms != null) {
|
|
290
|
+
if (meta.first === null || ms < (epochMs(meta.first) ?? Number.POSITIVE_INFINITY))
|
|
291
|
+
meta.first = o.timestamp;
|
|
292
|
+
if (meta.last === null || ms > (epochMs(meta.last) ?? Number.NEGATIVE_INFINITY))
|
|
293
|
+
meta.last = o.timestamp;
|
|
266
294
|
if (meta.prevMs != null) {
|
|
267
295
|
meta.durationSec += Math.min(300, Math.max(0, Math.round((ms - meta.prevMs) / 1000)));
|
|
268
296
|
}
|
|
@@ -271,21 +299,34 @@ export function parseClaude(file) {
|
|
|
271
299
|
}
|
|
272
300
|
if (o.type !== "assistant" || !o.message)
|
|
273
301
|
return;
|
|
302
|
+
const key = requestKey(o);
|
|
274
303
|
const u = o.message.usage;
|
|
275
304
|
if (u) {
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
305
|
+
const candidate = {
|
|
306
|
+
input: Math.max(0, Number(u.input_tokens) || 0),
|
|
307
|
+
cacheRead: Math.max(0, Number(u.cache_read_input_tokens) || 0),
|
|
308
|
+
cacheCreate: Math.max(0, Number(u.cache_creation_input_tokens) || 0),
|
|
309
|
+
output: Math.max(0, Number(u.output_tokens) || 0),
|
|
310
|
+
};
|
|
311
|
+
const existing = usageByRequest.get(key);
|
|
312
|
+
if (!existing || candidate.input + candidate.cacheRead + candidate.cacheCreate > existing.input + existing.cacheRead + existing.cacheCreate) {
|
|
313
|
+
if (existing)
|
|
314
|
+
candidate.output = Math.max(candidate.output, existing.output);
|
|
315
|
+
usageByRequest.set(key, candidate);
|
|
316
|
+
}
|
|
317
|
+
else if (candidate.output > existing.output) {
|
|
318
|
+
existing.output = candidate.output;
|
|
319
|
+
}
|
|
284
320
|
}
|
|
285
321
|
const content = o.message.content;
|
|
286
322
|
if (Array.isArray(content)) {
|
|
287
|
-
for (
|
|
323
|
+
for (let index = 0; index < content.length; index += 1) {
|
|
324
|
+
const b = content[index];
|
|
288
325
|
if (b && b.type === "tool_use") {
|
|
326
|
+
const toolKey = typeof b.id === "string" && b.id ? `id:${b.id}` : `${key}:${index}:${String(b.name || "")}`;
|
|
327
|
+
if (seenToolUses.has(toolKey))
|
|
328
|
+
continue;
|
|
329
|
+
seenToolUses.add(toolKey);
|
|
289
330
|
if (String(b.name || "").toLowerCase().includes("echomem"))
|
|
290
331
|
echo++;
|
|
291
332
|
else if (classifyClaudeTool(b.name) === "read")
|
|
@@ -296,12 +337,16 @@ export function parseClaude(file) {
|
|
|
296
337
|
}
|
|
297
338
|
}
|
|
298
339
|
});
|
|
299
|
-
|
|
340
|
+
const usage = [...usageByRequest.values()];
|
|
341
|
+
const cached = usage.reduce((sum, row) => sum + row.cacheRead, 0);
|
|
342
|
+
const output = usage.reduce((sum, row) => sum + row.output, 0);
|
|
343
|
+
const total = usage.reduce((sum, row) => sum + row.input + row.cacheRead + row.cacheCreate + row.output, 0);
|
|
344
|
+
if (!usage.length || total === 0)
|
|
300
345
|
return null;
|
|
301
346
|
return {
|
|
302
347
|
source: "claude-code",
|
|
303
|
-
date: meta.first
|
|
304
|
-
month: meta.first
|
|
348
|
+
date: localDate(meta.first),
|
|
349
|
+
month: localDate(meta.first)?.slice(0, 7) ?? null,
|
|
305
350
|
startTs: meta.first,
|
|
306
351
|
lastTs: meta.last,
|
|
307
352
|
durationSec: meta.durationSec,
|
|
@@ -324,11 +369,22 @@ export function walk(dir, match, skipDir, out = []) {
|
|
|
324
369
|
}
|
|
325
370
|
for (const e of entries) {
|
|
326
371
|
const full = path.join(dir, e.name);
|
|
327
|
-
|
|
372
|
+
let stat;
|
|
373
|
+
try {
|
|
374
|
+
// Never follow entries that changed into symlinks/special files after readdir. A validated root
|
|
375
|
+
// may itself be a user-controlled symlink, but traversal stays inside regular child entries.
|
|
376
|
+
stat = fs.lstatSync(full);
|
|
377
|
+
}
|
|
378
|
+
catch {
|
|
379
|
+
continue;
|
|
380
|
+
}
|
|
381
|
+
if (stat.isSymbolicLink())
|
|
382
|
+
continue;
|
|
383
|
+
if (stat.isDirectory()) {
|
|
328
384
|
if (!skipDir(e.name))
|
|
329
385
|
walk(full, match, skipDir, out);
|
|
330
386
|
}
|
|
331
|
-
else if (match(full)) {
|
|
387
|
+
else if (stat.isFile() && match(full)) {
|
|
332
388
|
out.push(full);
|
|
333
389
|
}
|
|
334
390
|
}
|
|
@@ -336,18 +392,22 @@ export function walk(dir, match, skipDir, out = []) {
|
|
|
336
392
|
}
|
|
337
393
|
export function collect() {
|
|
338
394
|
const stats = [];
|
|
339
|
-
const
|
|
340
|
-
|
|
341
|
-
const
|
|
342
|
-
|
|
343
|
-
|
|
395
|
+
const codexDiscovery = discoverCodexSessionFiles({ includeArchived: true });
|
|
396
|
+
if (codexDiscovery.files.length) {
|
|
397
|
+
for (const { path: f } of codexDiscovery.files) {
|
|
398
|
+
const s = parseCodex(f);
|
|
399
|
+
if (s)
|
|
400
|
+
stats.push(s);
|
|
401
|
+
}
|
|
344
402
|
}
|
|
345
403
|
// Claude Code: top-level session files only (skip subagent/workflow dirs to avoid double-counting).
|
|
346
|
-
const claudeRoot =
|
|
347
|
-
|
|
348
|
-
const
|
|
349
|
-
|
|
350
|
-
|
|
404
|
+
const claudeRoot = resolveClaudeProjectsDir();
|
|
405
|
+
if (claudeRoot) {
|
|
406
|
+
for (const f of walk(claudeRoot, (p) => p.endsWith(".jsonl"), (name) => name === "subagents" || name === "workflows")) {
|
|
407
|
+
const s = parseClaude(f);
|
|
408
|
+
if (s)
|
|
409
|
+
stats.push(s);
|
|
410
|
+
}
|
|
351
411
|
}
|
|
352
412
|
return stats;
|
|
353
413
|
}
|
|
@@ -371,6 +431,7 @@ export function aggregate(stats) {
|
|
|
371
431
|
const fresh = total - cached;
|
|
372
432
|
const noncached = Math.max(0, total - cached - output);
|
|
373
433
|
const dates = stats.map((s) => s.date).filter(Boolean).sort();
|
|
434
|
+
const lastDates = stats.map((s) => localDate(s.lastTs)).filter(Boolean).sort();
|
|
374
435
|
const byMonth = new Map();
|
|
375
436
|
for (const s of stats) {
|
|
376
437
|
if (!s.month)
|
|
@@ -426,7 +487,7 @@ export function aggregate(stats) {
|
|
|
426
487
|
rereadPct: total ? Math.round((cached / total) * 100) : 0,
|
|
427
488
|
ratio: recalls > 0 ? Math.round(reads / recalls) : null,
|
|
428
489
|
first: dates[0] || null,
|
|
429
|
-
last: dates[dates.length - 1] || null,
|
|
490
|
+
last: lastDates[lastDates.length - 1] || dates[dates.length - 1] || null,
|
|
430
491
|
days: new Set(dates).size,
|
|
431
492
|
months,
|
|
432
493
|
busiestDay: { date: busiestDayEntry?.[0] || null, sessions: busiestDayEntry?.[1] || 0 },
|
|
@@ -623,7 +684,7 @@ function renderText(a, memCount, useColor) {
|
|
|
623
684
|
L(c.green(`✓ You're connected — every new session now builds on your ${memCount} memories.`));
|
|
624
685
|
}
|
|
625
686
|
else {
|
|
626
|
-
L(c.green("→ Start now (no signup wall): ") + c.bold("npm i -g @echomem/mcp@latest && echomem-mcp
|
|
687
|
+
L(c.green("→ Start now (no signup wall): ") + c.bold("npm i -g @echomem/mcp@latest && echomem-mcp init"));
|
|
627
688
|
L(c.dim(` ~1 minute. Your next coding session recalls instead of re-reading.`));
|
|
628
689
|
}
|
|
629
690
|
NL();
|