@echomem/mcp 1.4.33 → 1.4.35
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 +12 -45
- package/dist/context-analysis/claude-native-canonical.js +87 -29
- package/dist/forensics.js +12 -9
- package/dist/index.js +177 -24
- package/dist/package-metadata.js +17 -14
- package/dist/save-checkpoint-hook.js +3 -3
- package/dist/setup-page/client-extraction.js +35 -11
- package/dist/setup-page.js +1 -1
- package/dist/setup-preview.js +25 -2
- package/dist/setup.js +136 -81
- package/dist/v1-contract.js +155 -10
- package/package.json +11 -6
- package/templates/codex-skills/echomem-save/SKILL.md +9 -9
- package/templates/echomem-recall.md +9 -9
package/README.md
CHANGED
|
@@ -37,38 +37,26 @@ legacy EchoMem-synthesized recall answer.
|
|
|
37
37
|
|
|
38
38
|
## Quick start (recommended)
|
|
39
39
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
40
|
+
Install Echo Desktop, sign in once, then open **Connect MCP** and choose **Connect Echo**. The app
|
|
41
|
+
ships and installs the MCP runtime, configures the detected Codex and Claude hosts, mirrors the same
|
|
42
|
+
device credential used by the desktop app, and verifies the MCP handshake. No terminal login is
|
|
43
|
+
required.
|
|
44
|
+
|
|
45
|
+
The standalone HUD has been retired; Echo Desktop is the persistent setup and status surface. The
|
|
46
|
+
bridge still reads local agent logs for on-demand context-health tools, but it no longer publishes or
|
|
47
|
+
auto-launches an Electron overlay.
|
|
45
48
|
|
|
46
|
-
|
|
47
|
-
`npx` run resolves the bridge into a throwaway `_npx/<hash>` cache dir that npm later garbage-collects,
|
|
48
|
-
which would break the MCP server after the fact (`setup` now refuses to pin such a path and falls back
|
|
49
|
-
to the global install, but installing globally avoids the issue entirely). The context HUD's
|
|
50
|
-
launch-at-login also runs from the installed path. `init` is the flagship one-liner — it wraps `setup --all --with-hud`: it writes each
|
|
51
|
-
installed agent's MCP config (with **no secret** in it — credentials live in
|
|
52
|
-
`~/.echomem/credentials.json`, mode 0600), adds the EchoMem memory guidance to their global
|
|
53
|
-
`AGENTS.md` / `CLAUDE.md`, installs first-party `echomem-search`, `echomem-save`,
|
|
54
|
-
`echomem-forget`, and `echomem-login` skills for Codex, opens the browser to approve the device
|
|
55
|
-
(and unlock the vault for encrypted accounts), then launches the context HUD. EchoMem updates only
|
|
56
|
-
its own skill folders; other memory-provider skills are detected and reported but never modified.
|
|
57
|
-
Reload your editors and you're done.
|
|
58
|
-
|
|
59
|
-
Prefer to keep it minimal? `echomem-mcp setup` configures only the auto-detected editor and skips the
|
|
60
|
-
HUD; the granular commands below still work.
|
|
49
|
+
For headless systems and development, the granular CLI commands remain available:
|
|
61
50
|
|
|
62
51
|
| Command | What it does |
|
|
63
52
|
|---|---|
|
|
64
|
-
| `npm i -g @echomem/mcp@latest && echomem-mcp init` |
|
|
53
|
+
| `npm i -g @echomem/mcp@latest && echomem-mcp init` | Legacy/headless setup for installed agents + login |
|
|
65
54
|
| `npm i -g @echomem/mcp@latest && echomem-mcp setup` | Install the CLI globally and configure just the detected editor |
|
|
66
55
|
| `npx -y @echomem/mcp@latest setup` | One-off setup without keeping a global CLI command |
|
|
67
56
|
| `echomem-mcp setup [--client cursor\|windsurf\|claude-desktop\|claude-code\|codex]` | Write client config + log in |
|
|
68
57
|
| `echomem-mcp setup --skip-login [--client cursor\|windsurf\|claude-desktop\|claude-code\|codex]` | Write client config without opening the browser or changing credentials |
|
|
69
58
|
| `npx -y @echomem/mcp@latest update --all` | One-shot update: install the latest bridge durably and repoint detected client configs, with no browser login |
|
|
70
59
|
| `npx -y @echomem/mcp@latest update --client codex` | Update one client only |
|
|
71
|
-
| `echomem-mcp setup --with-hud [--client codex]` | Write client config + log in + launch the EchoMem context HUD |
|
|
72
60
|
| `echomem-mcp login` | Approve device in browser (or use `--token` / `--passphrase`) |
|
|
73
61
|
| `echomem-mcp unlock` | Privately unlock the vault on this trusted device |
|
|
74
62
|
| `echomem-mcp lock` | Remove the local vault key while keeping the device login |
|
|
@@ -82,29 +70,8 @@ checks npm for a newer published bridge using a cached, non-blocking check. Agen
|
|
|
82
70
|
`npx -y @echomem/mcp@latest update --all` if the user agrees. The bridge does not auto-update on
|
|
83
71
|
every MCP startup.
|
|
84
72
|
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
The package also ships `echomem-hud`, a local context-health sidecar for Codex and Claude. It reads
|
|
88
|
-
local agent logs, computes a tracked lower-bound "clean vs dirty" context score, and renders a small
|
|
89
|
-
HUD without adding tokens to the agent conversation.
|
|
90
|
-
|
|
91
|
-
```bash
|
|
92
|
-
echomem-hud app # Electron floating HUD
|
|
93
|
-
echomem-hud serve # Browser fallback at http://127.0.0.1:17377
|
|
94
|
-
echomem-hud summary --json # Machine-readable current score
|
|
95
|
-
echomem-hud status # Show detected Codex/Claude sources
|
|
96
|
-
```
|
|
97
|
-
|
|
98
|
-
Supported local sources:
|
|
99
|
-
- Codex: `~/.codex/sessions/**/rollout-*.jsonl`
|
|
100
|
-
- Claude Code: `~/.claude/echo-ctx/*.json` first, then local transcripts when present
|
|
101
|
-
- Claude desktop agent/Cowork: local agent-mode transcripts under Application Support
|
|
102
|
-
|
|
103
|
-
The v1 metric counts `range_redundant` reads only and reports pollution as `tracked dead-weight ≥`
|
|
104
|
-
because exact context composition and provider eviction are not observable.
|
|
105
|
-
|
|
106
|
-
MCP fallback: agents can call `echo_context_health` to get the same score as markdown in-chat. This
|
|
107
|
-
is on-demand; the passive HUD remains a separate local process.
|
|
73
|
+
Agents can still call `echo_context_health` for an on-demand local context-health report. It reads
|
|
74
|
+
the local Codex/Claude logs and does not require a separate process or desktop overlay.
|
|
108
75
|
|
|
109
76
|
## Agent Doctor — local workspace forensics (new in 1.4.9)
|
|
110
77
|
|
|
@@ -7,6 +7,14 @@ import { walk } from "../report.js";
|
|
|
7
7
|
import { isStrongPositiveFeedback, } from "./workspace-report.js";
|
|
8
8
|
const OUTPUT_TOKEN_CAP = 12_000;
|
|
9
9
|
const IMAGE_TOKENS = 4_000;
|
|
10
|
+
// Request-by-request retained-context scoring grows faster than the raw JSONL. Keep onboarding
|
|
11
|
+
// predictable by analyzing a deterministic head/middle/tail sample from oversized sessions. The
|
|
12
|
+
// source transcript is untouched and remains available to the separate import pipeline.
|
|
13
|
+
const CLAUDE_MAX_TURNS_PER_SESSION = 1_000;
|
|
14
|
+
const CLAUDE_HEAD_TURNS = 200;
|
|
15
|
+
const CLAUDE_TAIL_TURNS = 500;
|
|
16
|
+
const CLAUDE_MIDDLE_WINDOWS = 3;
|
|
17
|
+
const CLAUDE_MIDDLE_WINDOW_TURNS = 100;
|
|
10
18
|
const PROBLEM_META = {
|
|
11
19
|
P01: { label: "Outdated Images & Screenshots", bucket: "dead", category: "Runtime Bug", confidence: "high" },
|
|
12
20
|
P02: { label: "Ignored User Instructions", bucket: "refind", category: "Model Behavior", confidence: "medium" },
|
|
@@ -350,26 +358,31 @@ function resultItem(args) {
|
|
|
350
358
|
}
|
|
351
359
|
function scoreClaudeNativeSession(parsed) {
|
|
352
360
|
classifyItems(parsed.items);
|
|
353
|
-
const
|
|
361
|
+
const terminalRequestByTurn = new Map();
|
|
354
362
|
for (const request of parsed.requests) {
|
|
355
363
|
const rawBuckets = { keep_oh: 0, keep_prod: 0, opt_dup: 0, opt_refind: 0, opt_dead: 0 };
|
|
356
364
|
const buckets = { duplicate: 0, refind: 0, dead: 0, unattributed: 0 };
|
|
357
365
|
const problemStats = new Map();
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
366
|
+
// Episode rendering only needs totals by episode/problem/bucket. Aggregating here avoids
|
|
367
|
+
// retaining one occurrence for every prior item on every request (quadratic heap growth).
|
|
368
|
+
const occurrenceMap = new Map();
|
|
369
|
+
const priorItems = [];
|
|
370
|
+
let usefulItemTokens = 0;
|
|
371
|
+
let wasteItemTokens = 0;
|
|
372
|
+
for (const item of parsed.items) {
|
|
373
|
+
if (item.requestSeq >= request.seq)
|
|
374
|
+
continue;
|
|
362
375
|
const classification = classForRequest(item, request);
|
|
363
|
-
|
|
364
|
-
|
|
376
|
+
if (!classification)
|
|
377
|
+
continue;
|
|
378
|
+
priorItems.push({ item, classification });
|
|
379
|
+
if (classification.kind === "useful")
|
|
380
|
+
usefulItemTokens += item.tokens;
|
|
381
|
+
else
|
|
382
|
+
wasteItemTokens += item.tokens;
|
|
383
|
+
}
|
|
365
384
|
const overhead = Math.min(request.inputTokens, parsed.overheadTokens);
|
|
366
385
|
const productBudget = Math.max(0, request.inputTokens - overhead);
|
|
367
|
-
const usefulItemTokens = priorItems
|
|
368
|
-
.filter(({ classification }) => classification.kind === "useful")
|
|
369
|
-
.reduce((sum, { item }) => sum + item.tokens, 0);
|
|
370
|
-
const wasteItemTokens = priorItems
|
|
371
|
-
.filter(({ classification }) => classification.kind === "waste")
|
|
372
|
-
.reduce((sum, { item }) => sum + item.tokens, 0);
|
|
373
386
|
const totalObserved = usefulItemTokens + wasteItemTokens;
|
|
374
387
|
const scale = totalObserved > productBudget && totalObserved > 0 ? productBudget / totalObserved : 1;
|
|
375
388
|
const keepProd = usefulItemTokens * scale;
|
|
@@ -413,7 +426,7 @@ function scoreClaudeNativeSession(parsed) {
|
|
|
413
426
|
});
|
|
414
427
|
}
|
|
415
428
|
problemStats.set(classification.problemId, stat);
|
|
416
|
-
|
|
429
|
+
recordNativeOccurrence(occurrenceMap, {
|
|
417
430
|
episode: request.episode,
|
|
418
431
|
problemId: classification.problemId,
|
|
419
432
|
bucket: classification.bucket,
|
|
@@ -437,7 +450,7 @@ function scoreClaudeNativeSession(parsed) {
|
|
|
437
450
|
problemStats,
|
|
438
451
|
buckets,
|
|
439
452
|
rawBuckets,
|
|
440
|
-
occurrences,
|
|
453
|
+
occurrences: occurrenceMap,
|
|
441
454
|
requestUsefulTokens: rawUseful,
|
|
442
455
|
requestWasteTokens: rawResidue,
|
|
443
456
|
});
|
|
@@ -466,7 +479,7 @@ function scoreClaudeNativeSession(parsed) {
|
|
|
466
479
|
turnWasteTokens: Math.round(rawResidue),
|
|
467
480
|
}];
|
|
468
481
|
}
|
|
469
|
-
|
|
482
|
+
terminalRequestByTurn.set(request.turn, {
|
|
470
483
|
request,
|
|
471
484
|
officialInputTokens: request.inputTokens,
|
|
472
485
|
usefulTokens: rawUseful,
|
|
@@ -477,10 +490,11 @@ function scoreClaudeNativeSession(parsed) {
|
|
|
477
490
|
buckets,
|
|
478
491
|
rawBuckets,
|
|
479
492
|
problems: problemStats,
|
|
480
|
-
occurrences,
|
|
493
|
+
occurrences: [...occurrenceMap.values()],
|
|
481
494
|
});
|
|
482
495
|
}
|
|
483
|
-
const selectedRows =
|
|
496
|
+
const selectedRows = [...terminalRequestByTurn.values()]
|
|
497
|
+
.sort((left, right) => left.request.turn - right.request.turn || left.request.seq - right.request.seq);
|
|
484
498
|
const selected = mergeRequestAccountingRows(selectedRows);
|
|
485
499
|
const zeroBuckets = { duplicate: 0, refind: 0, dead: 0, unattributed: 0 };
|
|
486
500
|
const zeroRawBuckets = { keep_oh: 0, keep_prod: 0, opt_dup: 0, opt_refind: 0, opt_dead: 0 };
|
|
@@ -537,7 +551,7 @@ function allocateResidualToProblemSignals(args) {
|
|
|
537
551
|
});
|
|
538
552
|
}
|
|
539
553
|
args.problemStats.set(problemId, stat);
|
|
540
|
-
args.occurrences
|
|
554
|
+
recordNativeOccurrence(args.occurrences, {
|
|
541
555
|
episode: args.request.episode,
|
|
542
556
|
problemId,
|
|
543
557
|
bucket,
|
|
@@ -548,15 +562,15 @@ function allocateResidualToProblemSignals(args) {
|
|
|
548
562
|
}
|
|
549
563
|
return allocated;
|
|
550
564
|
}
|
|
551
|
-
function
|
|
552
|
-
const
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
565
|
+
function recordNativeOccurrence(occurrences, occurrence) {
|
|
566
|
+
const key = `${occurrence.episode}:${occurrence.problemId}:${occurrence.bucket}`;
|
|
567
|
+
const existing = occurrences.get(key);
|
|
568
|
+
if (existing) {
|
|
569
|
+
existing.tokens += occurrence.tokens;
|
|
570
|
+
existing.turn = Math.max(existing.turn, occurrence.turn);
|
|
571
|
+
return;
|
|
558
572
|
}
|
|
559
|
-
|
|
573
|
+
occurrences.set(key, { ...occurrence });
|
|
560
574
|
}
|
|
561
575
|
function mergeRequestAccountingRows(rows) {
|
|
562
576
|
const buckets = { duplicate: 0, refind: 0, dead: 0, unattributed: 0 };
|
|
@@ -1023,7 +1037,52 @@ function internalBucket(bucket) {
|
|
|
1023
1037
|
return "opt_dead";
|
|
1024
1038
|
}
|
|
1025
1039
|
function readRows(file) {
|
|
1040
|
+
const totalTurns = countClaudeNativeTurns(file);
|
|
1041
|
+
const ranges = claudeTurnSampleRanges(totalTurns);
|
|
1026
1042
|
const rows = [];
|
|
1043
|
+
let turn = 0;
|
|
1044
|
+
forEachJsonRow(file, (row) => {
|
|
1045
|
+
if (stringValue(row.type) === "user" && !isClaudeToolResultUser(row))
|
|
1046
|
+
turn += 1;
|
|
1047
|
+
if (turn === 0 || turnInClaudeRanges(turn, ranges))
|
|
1048
|
+
rows.push(row);
|
|
1049
|
+
});
|
|
1050
|
+
return rows;
|
|
1051
|
+
}
|
|
1052
|
+
function countClaudeNativeTurns(file) {
|
|
1053
|
+
let turns = 0;
|
|
1054
|
+
forEachJsonRow(file, (row) => {
|
|
1055
|
+
if (stringValue(row.type) === "user" && !isClaudeToolResultUser(row))
|
|
1056
|
+
turns += 1;
|
|
1057
|
+
});
|
|
1058
|
+
return turns;
|
|
1059
|
+
}
|
|
1060
|
+
export function claudeTurnSampleRanges(totalTurns) {
|
|
1061
|
+
if (totalTurns <= CLAUDE_MAX_TURNS_PER_SESSION)
|
|
1062
|
+
return totalTurns > 0 ? [[1, totalTurns]] : [];
|
|
1063
|
+
const tailStart = totalTurns - CLAUDE_TAIL_TURNS + 1;
|
|
1064
|
+
const ranges = [
|
|
1065
|
+
[1, CLAUDE_HEAD_TURNS],
|
|
1066
|
+
[tailStart, totalTurns],
|
|
1067
|
+
];
|
|
1068
|
+
const middleStart = CLAUDE_HEAD_TURNS + 1;
|
|
1069
|
+
const middleEnd = tailStart - 1;
|
|
1070
|
+
const middleSpan = Math.max(0, middleEnd - middleStart + 1);
|
|
1071
|
+
for (let index = 0; index < CLAUDE_MIDDLE_WINDOWS; index += 1) {
|
|
1072
|
+
const segmentStart = middleStart + Math.floor((middleSpan * index) / CLAUDE_MIDDLE_WINDOWS);
|
|
1073
|
+
const segmentEnd = middleStart + Math.floor((middleSpan * (index + 1)) / CLAUDE_MIDDLE_WINDOWS) - 1;
|
|
1074
|
+
const segmentTurns = Math.max(0, segmentEnd - segmentStart + 1);
|
|
1075
|
+
const windowTurns = Math.min(CLAUDE_MIDDLE_WINDOW_TURNS, segmentTurns);
|
|
1076
|
+
const start = segmentStart + Math.floor((segmentTurns - windowTurns) / 2);
|
|
1077
|
+
if (windowTurns > 0)
|
|
1078
|
+
ranges.push([start, start + windowTurns - 1]);
|
|
1079
|
+
}
|
|
1080
|
+
return ranges.sort((left, right) => left[0] - right[0]);
|
|
1081
|
+
}
|
|
1082
|
+
function turnInClaudeRanges(turn, ranges) {
|
|
1083
|
+
return ranges.some(([start, end]) => turn >= start && turn <= end);
|
|
1084
|
+
}
|
|
1085
|
+
function forEachJsonRow(file, visit) {
|
|
1027
1086
|
const fd = fs.openSync(file, "r");
|
|
1028
1087
|
const decoder = new StringDecoder("utf8");
|
|
1029
1088
|
const buffer = Buffer.allocUnsafe(1 << 20);
|
|
@@ -1034,7 +1093,7 @@ function readRows(file) {
|
|
|
1034
1093
|
try {
|
|
1035
1094
|
const parsed = JSON.parse(line);
|
|
1036
1095
|
if (isRecord(parsed))
|
|
1037
|
-
|
|
1096
|
+
visit(parsed);
|
|
1038
1097
|
}
|
|
1039
1098
|
catch {
|
|
1040
1099
|
// Claude Code may leave an active final JSONL line partial.
|
|
@@ -1064,7 +1123,6 @@ function readRows(file) {
|
|
|
1064
1123
|
finally {
|
|
1065
1124
|
fs.closeSync(fd);
|
|
1066
1125
|
}
|
|
1067
|
-
return rows;
|
|
1068
1126
|
}
|
|
1069
1127
|
function isClaudeToolResultUser(record) {
|
|
1070
1128
|
const content = recordValue(record.message).content;
|
package/dist/forensics.js
CHANGED
|
@@ -1158,9 +1158,9 @@ function invalidSetupReport(code, message) {
|
|
|
1158
1158
|
/**
|
|
1159
1159
|
* Fail-closed boundary between the local scanner and setup UI.
|
|
1160
1160
|
*
|
|
1161
|
-
* The setup page must never infer that a fixture,
|
|
1162
|
-
* ledger is the user's report. This validator checks
|
|
1163
|
-
* the UI relies on before any absolute token number is rendered.
|
|
1161
|
+
* The setup page must never infer that a fixture, unreconciled scan, or internally inconsistent
|
|
1162
|
+
* token ledger is the user's report. This validator checks provenance and the cross-ledger
|
|
1163
|
+
* identities the UI relies on before any absolute token number is rendered.
|
|
1164
1164
|
*/
|
|
1165
1165
|
export function validateForensicReportForSetup(value) {
|
|
1166
1166
|
const report = recordValue(value);
|
|
@@ -1268,6 +1268,7 @@ export function validateForensicReportForSetup(value) {
|
|
|
1268
1268
|
return invalidSetupReport("REPORT_COST_INVALID", "Per-workspace costs do not reconcile with total spend.");
|
|
1269
1269
|
}
|
|
1270
1270
|
const diagnostics = canonical.diagnostics === undefined ? null : recordValue(canonical.diagnostics);
|
|
1271
|
+
let skippedCanonicalSessions = 0;
|
|
1271
1272
|
if (canonical.diagnostics !== undefined && !diagnostics) {
|
|
1272
1273
|
return invalidSetupReport("REPORT_MALFORMED", "Canonical scan diagnostics are malformed.");
|
|
1273
1274
|
}
|
|
@@ -1277,12 +1278,15 @@ export function validateForensicReportForSetup(value) {
|
|
|
1277
1278
|
if (skippedSessions === null || !Array.isArray(errors)) {
|
|
1278
1279
|
return invalidSetupReport("REPORT_MALFORMED", "Canonical scan diagnostics are malformed.");
|
|
1279
1280
|
}
|
|
1280
|
-
if (skippedSessions
|
|
1281
|
-
return invalidSetupReport("
|
|
1281
|
+
if (skippedSessions !== errors.length) {
|
|
1282
|
+
return invalidSetupReport("REPORT_MALFORMED", "Canonical scan diagnostics do not reconcile.");
|
|
1282
1283
|
}
|
|
1284
|
+
skippedCanonicalSessions = skippedSessions;
|
|
1283
1285
|
}
|
|
1284
|
-
if (
|
|
1285
|
-
|
|
1286
|
+
if (canonicalSessionCount > sessionCount ||
|
|
1287
|
+
canonicalSessionCount + skippedCanonicalSessions !== sessionCount ||
|
|
1288
|
+
(sessionCount > 0 && canonicalSessionCount === 0)) {
|
|
1289
|
+
return invalidSetupReport("REPORT_COHORT_MISMATCH", "Canonical analysis did not retain a usable local-session cohort.");
|
|
1286
1290
|
}
|
|
1287
1291
|
if (canonicalUseful + canonicalWaste !== canonicalInput) {
|
|
1288
1292
|
return invalidSetupReport("REPORT_CANONICAL_INVALID", "Canonical useful and waste totals do not reconcile.");
|
|
@@ -1517,8 +1521,7 @@ export async function buildForensicReport(opts) {
|
|
|
1517
1521
|
}
|
|
1518
1522
|
if (report.canonicalGoldenStandard &&
|
|
1519
1523
|
!("error" in report.canonicalGoldenStandard) &&
|
|
1520
|
-
|
|
1521
|
-
report.scale.sessionCount === report.canonicalGoldenStandard.summary.sessionsAnalyzed) {
|
|
1524
|
+
report.canonicalGoldenStandard.summary.sessionsAnalyzed > 0) {
|
|
1522
1525
|
const projection = projectCanonicalWasteToBilledInput(Number(report.scale.totalInputTokens || 0), report.canonicalGoldenStandard.summary);
|
|
1523
1526
|
if (projection)
|
|
1524
1527
|
report.billedWasteProjection = projection;
|