@echomem/mcp 1.4.34 → 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 +27 -8
- package/dist/package-metadata.js +10 -3
- package/dist/save-checkpoint-hook.js +1 -1
- 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 +132 -79
- package/dist/v1-contract.js +6 -6
- package/package.json +11 -6
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;
|
package/dist/index.js
CHANGED
|
@@ -18,6 +18,13 @@ import { checkLatestUpdateStatus, formatUpdateNotice, formatUpdateStatusText, st
|
|
|
18
18
|
const ECHO_API_BASE_URL = process.env.ECHO_API_BASE_URL || "https://echo-mem-chrome.vercel.app";
|
|
19
19
|
const ECHO_PRICING_URL = process.env.ECHO_PRICING_URL || "https://echoknows.com/account";
|
|
20
20
|
const ECHO_MEMORY_WEB_URL = (process.env.ECHO_MEMORY_WEB_URL || "https://echoknows.com/memory").replace(/\/$/, "");
|
|
21
|
+
const DESKTOP_MANAGED = process.env.ECHO_DESKTOP_MANAGED === "1";
|
|
22
|
+
const CONNECT_DEVICE_INSTRUCTION = DESKTOP_MANAGED
|
|
23
|
+
? "Open Echo Desktop, sign in, and choose Connect MCP, then retry this action."
|
|
24
|
+
: "Run `echomem-mcp login` in a terminal to reconnect this device, then retry this action.";
|
|
25
|
+
const UNLOCK_VAULT_INSTRUCTION = DESKTOP_MANAGED
|
|
26
|
+
? "Open Echo Desktop and unlock the vault there. Keep the passphrase out of chat."
|
|
27
|
+
: "Open Terminal and run `echomem-mcp unlock` yourself. Do not have the agent run this interactive command and do not send your passphrase in chat.";
|
|
21
28
|
function memoryWebUrl(memoryId) {
|
|
22
29
|
return `${ECHO_MEMORY_WEB_URL}/${encodeURIComponent(memoryId)}`;
|
|
23
30
|
}
|
|
@@ -156,7 +163,7 @@ function formatReconnectRequiredResult(error) {
|
|
|
156
163
|
return null;
|
|
157
164
|
return [
|
|
158
165
|
"🔌 EchoMem's saved login is no longer accepted (Unauthorized, HTTP 401).",
|
|
159
|
-
|
|
166
|
+
`Action required from the user: ${CONNECT_DEVICE_INSTRUCTION}`,
|
|
160
167
|
"No editor restart is needed. Keep all credentials out of chat.",
|
|
161
168
|
].join("\n");
|
|
162
169
|
}
|
|
@@ -1157,9 +1164,11 @@ class EchoMemMCPServer {
|
|
|
1157
1164
|
});
|
|
1158
1165
|
this.client = new EchoMemApiClient(store);
|
|
1159
1166
|
this.events = new EventLogger({ session_id: this.client.getSessionId(), app_version: SERVER_VERSION });
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1167
|
+
if (!DESKTOP_MANAGED) {
|
|
1168
|
+
startBackgroundUpdateCheck((status) => {
|
|
1169
|
+
this.updateStatus = status;
|
|
1170
|
+
});
|
|
1171
|
+
}
|
|
1163
1172
|
this.setupToolHandlers();
|
|
1164
1173
|
this.server.onerror = (error) => console.error("[MCP Error]", error);
|
|
1165
1174
|
process.on("SIGINT", async () => {
|
|
@@ -1207,7 +1216,7 @@ class EchoMemMCPServer {
|
|
|
1207
1216
|
]);
|
|
1208
1217
|
this.mapInjected = !!map;
|
|
1209
1218
|
this.groupMapInjected = !!groupMap;
|
|
1210
|
-
const updateNotice = formatUpdateNotice(this.updateStatus);
|
|
1219
|
+
const updateNotice = DESKTOP_MANAGED ? undefined : formatUpdateNotice(this.updateStatus);
|
|
1211
1220
|
return { tools: listToolSpecs({ map, groupMap, updateNotice }) };
|
|
1212
1221
|
});
|
|
1213
1222
|
this.server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
@@ -1243,6 +1252,14 @@ class EchoMemMCPServer {
|
|
|
1243
1252
|
return { content: [{ type: "text", text: await buildReportText(false) }] };
|
|
1244
1253
|
}
|
|
1245
1254
|
if (canonicalName === canonicalToolNames.updateStatus) {
|
|
1255
|
+
if (DESKTOP_MANAGED) {
|
|
1256
|
+
return {
|
|
1257
|
+
content: [{
|
|
1258
|
+
type: "text",
|
|
1259
|
+
text: `Echo Desktop manages this MCP runtime (${MCP_PACKAGE_VERSION}). Install app updates from Echo Desktop, then start a new agent session.`,
|
|
1260
|
+
}],
|
|
1261
|
+
};
|
|
1262
|
+
}
|
|
1246
1263
|
const force = isRecord(request.params.arguments) && request.params.arguments.force === true;
|
|
1247
1264
|
const status = await checkLatestUpdateStatus({ force });
|
|
1248
1265
|
this.updateStatus = status;
|
|
@@ -1361,7 +1378,7 @@ class EchoMemMCPServer {
|
|
|
1361
1378
|
content: [
|
|
1362
1379
|
{
|
|
1363
1380
|
type: "text",
|
|
1364
|
-
text:
|
|
1381
|
+
text: `🔌 EchoMem isn't connected yet. ${CONNECT_DEVICE_INSTRUCTION} No editor restart is needed.`,
|
|
1365
1382
|
},
|
|
1366
1383
|
],
|
|
1367
1384
|
};
|
|
@@ -1375,8 +1392,10 @@ class EchoMemMCPServer {
|
|
|
1375
1392
|
text: [
|
|
1376
1393
|
"🔒 EchoMem vault is locked.",
|
|
1377
1394
|
"This encrypted account has no usable local decryption key. Once unlocked, this trusted device stays unlocked until you explicitly lock it or log out.",
|
|
1378
|
-
|
|
1379
|
-
|
|
1395
|
+
`Action required from the user: ${UNLOCK_VAULT_INSTRUCTION}`,
|
|
1396
|
+
...(DESKTOP_MANAGED ? [] : [
|
|
1397
|
+
"At `Vault passphrase (typing is hidden):`, type the passphrase and press Return. No characters will appear while you type; that is expected.",
|
|
1398
|
+
]),
|
|
1380
1399
|
"After the success message, retry this EchoMem action in the current session — no editor restart is needed.",
|
|
1381
1400
|
].join("\n"),
|
|
1382
1401
|
},
|
package/dist/package-metadata.js
CHANGED
|
@@ -21,15 +21,22 @@ export const MCP_PACKAGE_NAME = stringOrFallback(packageJson.name, FALLBACK_PACK
|
|
|
21
21
|
export const MCP_PACKAGE_VERSION = stringOrFallback(packageJson.version, FALLBACK_PACKAGE.version);
|
|
22
22
|
export const MCP_PACKAGE_DESCRIPTION = stringOrFallback(packageJson.description, FALLBACK_PACKAGE.description);
|
|
23
23
|
export const MCP_PACKAGE_LABEL = `${MCP_PACKAGE_NAME}@${MCP_PACKAGE_VERSION}`;
|
|
24
|
+
export const MCP_DESKTOP_MANAGED = process.env.ECHO_DESKTOP_MANAGED === "1";
|
|
24
25
|
export const MCP_UPDATE_COMMAND = `npx -y ${MCP_PACKAGE_NAME}@latest update`;
|
|
25
26
|
export const MCP_UPDATE_ALL_COMMAND = `${MCP_UPDATE_COMMAND} --all`;
|
|
27
|
+
export const MCP_VAULT_UNLOCK_INSTRUCTION = MCP_DESKTOP_MANAGED
|
|
28
|
+
? "open Echo Desktop and unlock the vault there"
|
|
29
|
+
: "run `echomem-mcp unlock` locally";
|
|
30
|
+
const MCP_UPDATE_INSTRUCTION = MCP_DESKTOP_MANAGED
|
|
31
|
+
? "Echo Desktop manages this MCP runtime; install an Echo Desktop update when one is offered"
|
|
32
|
+
: `update once with \`${MCP_UPDATE_ALL_COMMAND}\``;
|
|
26
33
|
export const MEMORY_CITATION_INSTRUCTION = 'If the user-facing answer materially relies on one or more EchoMem memories, end it with a compact "EchoMem sources:" list containing only the memories actually used. For memories owned by teammates or accepted friends, call record_memory_citations immediately before the final answer with those exact Memory IDs. Do not cite memories that were merely retrieved. Use each memory key as the Markdown label and its canonical https://echoknows.com/memory/<memory-id> URL. Omit the sources section and citation receipt when no memory informed the answer.';
|
|
27
34
|
export const SAVED_MEMORY_RECEIPT_INSTRUCTION = 'After save_conversation succeeds and returns one or more saved memory IDs, make the save visible in the final user-facing answer with a compact "EchoMem saved:" list containing every memory created by that call. Use each memory key as the Markdown label and its canonical https://echoknows.com/memory/<memory-id> URL. This save receipt is separate from "EchoMem sources:" and does not imply that the newly saved memories informed the answer.';
|
|
28
35
|
export const MCP_SERVER_INSTRUCTIONS = [
|
|
29
36
|
`${MCP_PACKAGE_DESCRIPTION} (${MCP_PACKAGE_LABEL}).`,
|
|
30
|
-
`If this bridge is stale,
|
|
37
|
+
`If this bridge is stale, ${MCP_UPDATE_INSTRUCTION} and start a new MCP session; never auto-update at startup.`,
|
|
31
38
|
"Before re-deriving prior decisions or preferences, use search_memories.",
|
|
32
|
-
|
|
39
|
+
`Before the final response for a durable decision, implementation, fix, commit, passing verification, release, or milestone, call save_conversation. Skip secrets and trivial work. If the encrypted vault is locked, tell the user to ${MCP_VAULT_UNLOCK_INSTRUCTION}.`,
|
|
33
40
|
"After a successful save, show every memory created by that call in a compact EchoMem saved: list with canonical links; this is separate from \"EchoMem sources:\".",
|
|
34
41
|
"For company groups, call request_group_session_sharing near conversation start or after a qualifying save. It renders a native choice on clients with MCP elicitation and returns a text fallback otherwise. Omit groupSharingScopeId only on the first call, then reuse the returned scope only in this conversation. Each group needs an explicit choice; decline, cancel, or silence stays unset. Never infer consent. Flagged memories stay private.",
|
|
35
42
|
"EchoMem credential identity is authoritative over Claude profiles, host accounts, git identity, or inference. Use get_group_context before group-orientation answers and never re-filter owners returned by search_others_memories.",
|
|
@@ -38,5 +45,5 @@ export const MCP_SERVER_INSTRUCTIONS = [
|
|
|
38
45
|
"Group profiles, conversation sharing, publication, sensitive-memory flags, and deletion require explicit user confirmation. Never store or log an echo_grp_ invite code.",
|
|
39
46
|
].join(" ");
|
|
40
47
|
export function withMcpVersion(description) {
|
|
41
|
-
return `${description}\n\nEchoMem MCP bridge: ${MCP_PACKAGE_LABEL}. If this version is stale,
|
|
48
|
+
return `${description}\n\nEchoMem MCP bridge: ${MCP_PACKAGE_LABEL}. If this version is stale, ${MCP_UPDATE_INSTRUCTION}, then start a new MCP session. Do not run updates repeatedly or on every startup.`;
|
|
42
49
|
}
|
|
@@ -10,7 +10,7 @@ const CHECKPOINT_REASON = [
|
|
|
10
10
|
"Private persistence happens first. For a user with a company group, call `request_group_session_sharing` near conversation start or after a qualifying private save.",
|
|
11
11
|
"On the first request_group_session_sharing call in a conversation, omit groupSharingScopeId so EchoMem mints a fresh opaque scope. Reuse that exact scope only in this conversation for later get/request/set/save calls, and never persist it as memory. If multiple groups are returned, reuse the same scope and call once per groupId. Supported clients render a native Share with team / Keep private choice; if the tool returns a text fallback, relay its exact question and call set_group_session_sharing only after an explicit Yes/No. Decline, cancel, or silence leaves that group's state unset. A Share decision syncs eligible memories from later saves carrying the same scope to that group; saves automatically sync to every approved group. Keep private keeps them private for that group.",
|
|
12
12
|
"Flagged memories are withheld from automatic conversation sync and remain private.",
|
|
13
|
-
"If EchoMem reports that the encrypted vault is locked, tell the user to
|
|
13
|
+
"If EchoMem reports that the encrypted vault is locked, tell the user to open Echo Desktop and unlock the vault there; on a headless system, use `echomem-mcp unlock`. Never silently skip a qualifying checkpoint.",
|
|
14
14
|
].join(" ");
|
|
15
15
|
function currentTurnSlice(transcript) {
|
|
16
16
|
const lines = transcript.split(/\r?\n/);
|
|
@@ -38,7 +38,13 @@ export const SETUP_PAGE_CLIENT_EXTRACTION = String.raw ` /* ---------- dash
|
|
|
38
38
|
}
|
|
39
39
|
return envelope;
|
|
40
40
|
}
|
|
41
|
-
if (
|
|
41
|
+
if (
|
|
42
|
+
!gs ||
|
|
43
|
+
!Number.isFinite(sessions) || sessions <= 0 ||
|
|
44
|
+
!Number.isFinite(canonicalSessions) || canonicalSessions <= 0 || canonicalSessions > sessions ||
|
|
45
|
+
!Number.isFinite(skipped) || skipped < 0 || canonicalSessions + skipped !== sessions ||
|
|
46
|
+
!Number.isFinite(canonicalInput) || canonicalInput <= 0
|
|
47
|
+
) {
|
|
42
48
|
throw new Error("Canonical analysis is incomplete or does not match the provider session cohort.");
|
|
43
49
|
}
|
|
44
50
|
if (!Array.isArray(r.repos) || r.repos.length === 0) {
|
|
@@ -52,15 +58,19 @@ export const SETUP_PAGE_CLIENT_EXTRACTION = String.raw ` /* ---------- dash
|
|
|
52
58
|
report = null;
|
|
53
59
|
reportEnvelope = null;
|
|
54
60
|
resetReportSurface();
|
|
61
|
+
var canContinue = String(code || "").indexOf("REPORT_") === 0 || code === "RENDER_FAILED";
|
|
55
62
|
setHead("We couldn't finish this scan", "Needs attention");
|
|
56
63
|
app.className = "reportMessageStage";
|
|
57
64
|
app.innerHTML =
|
|
58
65
|
'<section class="reportMessage" data-report-state="failed">' +
|
|
59
66
|
'<h2>We couldn’t finish this scan.</h2>' +
|
|
60
67
|
'<p>Your coding history is unchanged.</p>' +
|
|
61
|
-
|
|
68
|
+
(canContinue
|
|
69
|
+
? '<p>This optional report can be retried later.</p><div class="actions"><button type="button" class="primary" data-connect-echo>Continue without report</button></div>'
|
|
70
|
+
: '<p>Close this tab and run <code>echomem-mcp init</code> again.</p>') +
|
|
62
71
|
'<details class="reportTechnical"><summary>Technical details</summary><code>' + esc(code || "REPORT_FAILED") + '</code></details>' +
|
|
63
72
|
'</section>';
|
|
73
|
+
if (canContinue) bindConnect();
|
|
64
74
|
}
|
|
65
75
|
function renderEmptyReport() {
|
|
66
76
|
report = null;
|
|
@@ -71,8 +81,10 @@ export const SETUP_PAGE_CLIENT_EXTRACTION = String.raw ` /* ---------- dash
|
|
|
71
81
|
app.innerHTML =
|
|
72
82
|
'<section class="reportMessage" data-report-state="empty">' +
|
|
73
83
|
'<h2>No coding history found.</h2>' +
|
|
74
|
-
'<p>
|
|
84
|
+
'<p>You can continue now and import local coding history later.</p>' +
|
|
85
|
+
'<div class="actions"><button type="button" class="primary" data-connect-echo>Continue setup</button></div>' +
|
|
75
86
|
'</section>';
|
|
87
|
+
bindConnect();
|
|
76
88
|
}
|
|
77
89
|
function renderBridgeIssue() {
|
|
78
90
|
renderReportIssue("BRIDGE_UNREACHABLE", "The local bridge stopped answering before your report was ready. Your terminal may have closed or the process may have stopped.");
|
|
@@ -203,6 +215,9 @@ export const SETUP_PAGE_CLIENT_EXTRACTION = String.raw ` /* ---------- dash
|
|
|
203
215
|
var hasPendingCount = typeof pending === "number";
|
|
204
216
|
var discovery = stats && stats.discovery ? stats.discovery : {};
|
|
205
217
|
var isPartial = !!(stats && stats.partial);
|
|
218
|
+
var optionalDiagnostics = stats && stats.optionalDiagnostics ? stats.optionalDiagnostics : {};
|
|
219
|
+
var optionalStatsDegraded = optionalDiagnostics.degraded === true;
|
|
220
|
+
var degradedWithoutCounts = optionalStatsDegraded && optionalDiagnostics.countsTrusted !== true;
|
|
206
221
|
var localScanReady = !isPartial || discovery.phase === "exact" || discovery.phase === "full";
|
|
207
222
|
var skippedActive = typeof migratable.skippedActive === "number" ? migratable.skippedActive : 0;
|
|
208
223
|
var sessions = stats && stats.sessions ? stats.sessions : {};
|
|
@@ -210,8 +225,8 @@ export const SETUP_PAGE_CLIENT_EXTRACTION = String.raw ` /* ---------- dash
|
|
|
210
225
|
// already imported elsewhere, so it over-counts (e.g. 10). The count is only trustworthy after the
|
|
211
226
|
// account check (phase "account"/"exact"/"full"), which corrects it (e.g. 3). Until then we show a
|
|
212
227
|
// "counting" state with no number, so the user never sees the count jump down.
|
|
213
|
-
var pendingTrusted = hasPendingCount && !!discovery.phase && discovery.phase !== "quick";
|
|
214
|
-
var knownDone = pendingTrusted && pending === 0;
|
|
228
|
+
var pendingTrusted = !degradedWithoutCounts && hasPendingCount && !!discovery.phase && discovery.phase !== "quick";
|
|
229
|
+
var knownDone = pendingTrusted && pending === 0 && !optionalStatsDegraded;
|
|
215
230
|
var canExtract = pendingTrusted && pending > 0;
|
|
216
231
|
var pendN = hasPendingCount ? pending : 0;
|
|
217
232
|
var pendingCodex = typeof migratable.pendingCodex === "number" ? migratable.pendingCodex : null;
|
|
@@ -227,15 +242,19 @@ export const SETUP_PAGE_CLIENT_EXTRACTION = String.raw ` /* ---------- dash
|
|
|
227
242
|
var currentPlanLabel = paidRecallPlan(currentPlan)
|
|
228
243
|
? currentPlan.charAt(0).toUpperCase() + currentPlan.slice(1) + " Echo"
|
|
229
244
|
: "Original Echo";
|
|
230
|
-
setHead("Turn coding history into memory", pendingTrusted ? (knownDone ? "Done" : "Ready") : (localScanReady ? "Ready" : "Scanning"));
|
|
231
|
-
var headlineHtml =
|
|
245
|
+
setHead("Turn coding history into memory", degradedWithoutCounts ? "Ready" : (pendingTrusted ? (knownDone ? "Done" : "Ready") : (localScanReady ? "Ready" : "Scanning")));
|
|
246
|
+
var headlineHtml = degradedWithoutCounts
|
|
247
|
+
? "Local history counting <strong>couldn’t finish.</strong>"
|
|
248
|
+
: !pendingTrusted
|
|
232
249
|
? "Counting your <strong>new conversations…</strong>"
|
|
233
250
|
: (knownDone
|
|
234
251
|
? "You are <strong>all caught up.</strong>"
|
|
235
252
|
: (planLimited
|
|
236
253
|
? "<strong>" + esc(number(pendN)) + " sessions</strong> found."
|
|
237
254
|
: "<strong>" + esc(number(pendN)) + " sessions</strong> are ready to review."));
|
|
238
|
-
var sub =
|
|
255
|
+
var sub = degradedWithoutCounts
|
|
256
|
+
? "You can finish setup now. Your conversations stay on this Mac, and you can retry the history import later."
|
|
257
|
+
: !pendingTrusted
|
|
239
258
|
? "Echo is matching your local history against what is already in memory."
|
|
240
259
|
: (knownDone
|
|
241
260
|
? (skippedActive ? number(skippedActive) + " active conversation" + (skippedActive === 1 ? " is" : "s are") + " still changing, so Echo will pick them up later." : "Your history is already in EchoMem. Nothing new to extract.")
|
|
@@ -287,6 +306,7 @@ export const SETUP_PAGE_CLIENT_EXTRACTION = String.raw ` /* ---------- dash
|
|
|
287
306
|
}
|
|
288
307
|
document.getElementById("exBanner").innerHTML =
|
|
289
308
|
(statsSlow && !stats ? '<div class="warning">Local counts are taking longer than expected. You can still ask Echo to extract anything unprocessed.</div>' : '') +
|
|
309
|
+
(optionalStatsDegraded && !degradedWithoutCounts ? '<div class="warning">The optional usage summary was skipped. Your conversation list is still ready.</div>' : '') +
|
|
290
310
|
(error ? '<div class="error">' + esc(error) + '</div>' : '');
|
|
291
311
|
document.getElementById("exHeadline").innerHTML = headlineHtml;
|
|
292
312
|
document.getElementById("exSub").textContent = sub;
|
|
@@ -297,7 +317,9 @@ export const SETUP_PAGE_CLIENT_EXTRACTION = String.raw ` /* ---------- dash
|
|
|
297
317
|
var asset = id === "claude-desktop" ? "claude" : "codex";
|
|
298
318
|
return '<span class="pfIcon"><img src="/hud-assets/' + asset + '.svg" alt="" onerror="this.style.display="none";this.nextElementSibling.style.display="grid";" /><span class="pfFallback">' + fallback + '</span></span>';
|
|
299
319
|
};
|
|
300
|
-
document.getElementById("exSources").innerHTML =
|
|
320
|
+
document.getElementById("exSources").innerHTML = degradedWithoutCounts
|
|
321
|
+
? '<span class="pfNote">Optional local-history count skipped.</span>'
|
|
322
|
+
: !pendingTrusted
|
|
301
323
|
? '<span class="pfNote">Scanning local history…</span>'
|
|
302
324
|
: (canExtract
|
|
303
325
|
? '<span class="pf">' + srcIcon("codex", "CX") + '<strong>' + esc(number(codexN)) + '</strong> Codex</span>' +
|
|
@@ -306,7 +328,9 @@ export const SETUP_PAGE_CLIENT_EXTRACTION = String.raw ` /* ---------- dash
|
|
|
306
328
|
'<span class="pfNote">found on this Mac</span>'
|
|
307
329
|
: "");
|
|
308
330
|
// Reassurances live at the moment of commitment — right under the button.
|
|
309
|
-
document.getElementById("exEta").innerHTML =
|
|
331
|
+
document.getElementById("exEta").innerHTML = degradedWithoutCounts
|
|
332
|
+
? 'Setup can continue without this optional count.'
|
|
333
|
+
: pendingTrusted
|
|
310
334
|
? (canExtract
|
|
311
335
|
? ''
|
|
312
336
|
: 'Nothing new to extract right now.')
|
|
@@ -317,7 +341,7 @@ export const SETUP_PAGE_CLIENT_EXTRACTION = String.raw ` /* ---------- dash
|
|
|
317
341
|
// While still counting, hide the primary button entirely; the headline + source split
|
|
318
342
|
// already say it is working, and the ready state should present one clear action.
|
|
319
343
|
var migrateBtn = document.getElementById("migrate");
|
|
320
|
-
if (pendingTrusted) {
|
|
344
|
+
if (pendingTrusted || degradedWithoutCounts) {
|
|
321
345
|
migrateBtn.style.display = "";
|
|
322
346
|
var candidatesReady = !canExtract || candidateSessions().length > 0;
|
|
323
347
|
if (canExtract && candidatesReady) ensureSessionSelection();
|
package/dist/setup-page.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { renderSetupPageDocument } from "./setup-page/document.js";
|
|
2
2
|
import { renderSetupPreviewBootstrap } from "./setup-preview.js";
|
|
3
|
-
export { SETUP_PREVIEW_STATES } from "./setup-preview.js";
|
|
3
|
+
export { SETUP_PREVIEW_REPORT, SETUP_PREVIEW_STATES } from "./setup-preview.js";
|
|
4
4
|
/**
|
|
5
5
|
* Setup page entry point. The implementation is split by product phase under ./setup-page/:
|
|
6
6
|
* core utilities, local report, post-auth extraction, and lifecycle polling.
|
package/dist/setup-preview.js
CHANGED
|
@@ -3,8 +3,11 @@ export const SETUP_PREVIEW_STATES = [
|
|
|
3
3
|
"consent-required",
|
|
4
4
|
"scan",
|
|
5
5
|
"scan-error",
|
|
6
|
+
"bridge-error",
|
|
6
7
|
"report",
|
|
7
8
|
"extract-counting",
|
|
9
|
+
"extract-degraded",
|
|
10
|
+
"extract-degraded-exact",
|
|
8
11
|
"extract-ready",
|
|
9
12
|
"extract-free-selected",
|
|
10
13
|
"extract-free-trial-used",
|
|
@@ -35,7 +38,7 @@ export const SETUP_PREVIEW_STATES = [
|
|
|
35
38
|
export function parseSetupPreviewState(value) {
|
|
36
39
|
return SETUP_PREVIEW_STATES.includes(value) ? value : null;
|
|
37
40
|
}
|
|
38
|
-
const
|
|
41
|
+
export const SETUP_PREVIEW_REPORT = {
|
|
39
42
|
schemaVersion: 1,
|
|
40
43
|
dataOrigin: "design-preview",
|
|
41
44
|
generatedFrom: [],
|
|
@@ -307,8 +310,12 @@ export function renderSetupPreviewBootstrap(state) {
|
|
|
307
310
|
return `${watermark}
|
|
308
311
|
renderReportIssue("REPORT_CANONICAL_INVALID", "Preview-only canonical reconciliation failure.");`;
|
|
309
312
|
}
|
|
313
|
+
if (state === "bridge-error") {
|
|
314
|
+
return `${watermark}
|
|
315
|
+
renderBridgeIssue();`;
|
|
316
|
+
}
|
|
310
317
|
if (state === "report") {
|
|
311
|
-
const reportJson = JSON.stringify(
|
|
318
|
+
const reportJson = JSON.stringify(SETUP_PREVIEW_REPORT);
|
|
312
319
|
return `${watermark}
|
|
313
320
|
localHistoryConsentGranted = true;
|
|
314
321
|
report = ${reportJson};
|
|
@@ -320,6 +327,22 @@ export function renderSetupPreviewBootstrap(state) {
|
|
|
320
327
|
stats = null;
|
|
321
328
|
renderDashboard();`;
|
|
322
329
|
}
|
|
330
|
+
if (state === "extract-degraded")
|
|
331
|
+
return `${watermark}${extractionPreviewBootstrap({
|
|
332
|
+
plan: "free", paid: false, trialAvailable: true, trialUsed: false,
|
|
333
|
+
quotaLimit: 100, quotaRemaining: 100, candidateCount: 7, selectFree: true,
|
|
334
|
+
})}
|
|
335
|
+
stats.discovery = { phase: "quick", exact: false };
|
|
336
|
+
stats.optionalDiagnostics = { degraded: true, reason: "EXACT_DISCOVERY_FAILED", countsTrusted: false };
|
|
337
|
+
renderDashboard();`;
|
|
338
|
+
if (state === "extract-degraded-exact")
|
|
339
|
+
return `${watermark}${extractionPreviewBootstrap({
|
|
340
|
+
plan: "free", paid: false, trialAvailable: true, trialUsed: false,
|
|
341
|
+
quotaLimit: 100, quotaRemaining: 100, candidateCount: 7, selectFree: true,
|
|
342
|
+
})}
|
|
343
|
+
stats.discovery = { phase: "exact", exact: true };
|
|
344
|
+
stats.optionalDiagnostics = { degraded: true, reason: "FULL_STATS_FAILED", countsTrusted: true };
|
|
345
|
+
renderDashboard();`;
|
|
323
346
|
if (state === "extract-ready")
|
|
324
347
|
return `${watermark}${extractionPreviewBootstrap({
|
|
325
348
|
plan: "free", paid: false, trialAvailable: true, trialUsed: false,
|
package/dist/setup.js
CHANGED
|
@@ -31,7 +31,7 @@ import { syncCodexUsage } from "./codex-sync.js";
|
|
|
31
31
|
import { renderSetupPage } from "./setup-page.js";
|
|
32
32
|
import { parseSetupPreviewState } from "./setup-preview.js";
|
|
33
33
|
import { repoLabel, validateForensicReportForSetup } from "./forensics.js";
|
|
34
|
-
import {
|
|
34
|
+
import { installSaveCheckpointHooks } from "./hud/hooks.js";
|
|
35
35
|
import { MCP_PACKAGE_LABEL, MCP_PACKAGE_NAME, MCP_PACKAGE_VERSION, MCP_UPDATE_ALL_COMMAND, MCP_UPDATE_COMMAND } from "./package-metadata.js";
|
|
36
36
|
import { checkLatestUpdateStatus, compareSemver, readCachedUpdateStatus } from "./update-check.js";
|
|
37
37
|
// The setup dashboard, account login, and encryption passphrase entry are all served by this
|
|
@@ -392,7 +392,7 @@ function echomemGuidanceBlock() {
|
|
|
392
392
|
"- Use EchoMem's `echomem-*` skills and MCP tools as the default memory provider. Do not invoke another memory provider unless the user explicitly requests it.",
|
|
393
393
|
"- Before re-deriving past decisions, project context, or anything the user likely settled before: call `search_memories` first.",
|
|
394
394
|
'- If the final user-facing answer materially relies on one or more EchoMem memories, end it with a compact `EchoMem sources:` list containing only the memories actually used. Link each memory key to its canonical `https://echoknows.com/memory/<memory-id>` URL. Do not cite memories that were merely retrieved, and omit the section when no memory informed the answer.',
|
|
395
|
-
"- Before the final response for a task that produced a durable decision, confirmed preference, implementation, resolved bug, commit, passing verification, release, or project milestone: call `save_conversation`. This private checkpoint does not require a separate user request. Do not save secrets, credentials, incidental chatter, or trivial read-only work. If a qualifying save fails because the encrypted vault is locked, tell the user to
|
|
395
|
+
"- Before the final response for a task that produced a durable decision, confirmed preference, implementation, resolved bug, commit, passing verification, release, or project milestone: call `save_conversation`. This private checkpoint does not require a separate user request. Do not save secrets, credentials, incidental chatter, or trivial read-only work. If a qualifying save fails because the encrypted vault is locked, tell the user to open Echo Desktop and unlock the vault there; on a headless system, use `echomem-mcp unlock`. Never silently skip it.",
|
|
396
396
|
'- After `save_conversation` succeeds and returns one or more saved memory IDs, make the save visible in the final user-facing answer with a compact `EchoMem saved:` list containing every memory created by that call. Link each memory key to its canonical `https://echoknows.com/memory/<memory-id>` URL. This save receipt is separate from `EchoMem sources:` and does not imply the newly saved memories informed the answer.',
|
|
397
397
|
"- For a user with a company group, call `request_group_session_sharing` near conversation start or after a qualifying private save. On the first call in a conversation, omit `groupSharingScopeId` so EchoMem mints a fresh opaque scope. Reuse that exact scope only in this conversation for later get/request/set/save calls, and never persist it as memory. If multiple groups are returned, reuse the same scope and call once per `groupId`. Supported clients render a native Share with team / Keep private choice. If the tool returns a text fallback, relay its exact question and call `set_group_session_sharing` only after an explicit Yes/No. Decline, cancel, or silence leaves that group's state unset; never infer an answer. Saves sync eligible memories to every approved group; a No keeps them private for that group.",
|
|
398
398
|
"- If the user pastes a session carryover/checkpoint: it may reference `get_checkpoint_by_context` — use it to pull the checkpoint/decision trail when you need more than the snapshot.",
|
|
@@ -403,14 +403,12 @@ function echomemGuidanceBlock() {
|
|
|
403
403
|
"- Group sharing is scoped to an opaque id carried only in the current conversation, not to the MCP transport session. Membership is rechecked for each sync. Flagged memories are withheld from automatic conversation sync and remain private.",
|
|
404
404
|
"- If a user asks to create a group, call `create_memory_group`; if they ask for a code to share, call `create_group_invite` and return the secret invite code only to that user. Never save the invite code to memory or include it in logs, analytics, summaries, or unrelated output.",
|
|
405
405
|
"- If a user supplies an `echo_grp_...` code and explicitly asks to join, call `join_memory_group`. Joining never authorizes publishing by itself and must not move a user out of another group. After joining, continue into the profile-and-publication preview instead of leaving title or responsibility blank.",
|
|
406
|
-
"- For requests such as “prepare my recent work memories,” “upload work from this ticket,” or “publish work since my last sync,” call `prepare_group_publication` first. This is a no-publication preview. For encrypted accounts, tell the user to
|
|
406
|
+
"- For requests such as “prepare my recent work memories,” “upload work from this ticket,” or “publish work since my last sync,” call `prepare_group_publication` first. This is a no-publication preview. For encrypted accounts, tell the user to open Echo Desktop and unlock the vault there if the tool reports that the key is required.",
|
|
407
407
|
"- After preparing, select only exact candidate memory IDs that match the user's stated scope and exclude already-published or exact-content duplicates. Use the candidate evidence to draft a concise title and responsibility summary for the current member, but label both as proposals rather than facts.",
|
|
408
408
|
"- Use one canonical evidence link for every memory: preserve the Memory ID and link to `https://echoknows.com/memory/<memory-id>`. The site resolves the authorized representation: an owner is sent to their private timeline, while current group/friend access opens an authorized publication snapshot or public memory. The visible Markdown label should use the memory key, not the raw URL or UUID.",
|
|
409
409
|
"- If the user asks to flag memories about a sensitive topic, search their own memories first, show the exact matches with owner-only personal links, and ask them to confirm. Only then call `flag_memories_for_publication_attention`; flagging does not publish, decrypt, change visibility, or retract an existing group snapshot.",
|
|
410
410
|
"- Present the proposed title/responsibility and the memory publication preview together and ask for explicit confirmation. Never save an inferred profile or publish memories before confirmation. If an unflagged candidate appears sensitive, proactively ask whether the user wants to mark its exact ID for publication attention first. Explain naturally: marking does not publish or change encryption; it means you will call it out and ask for detailed confirmation whenever a later publication includes it. Never auto-flag based on agent inference. Show already-flagged candidates in a separate warning, state that nothing has been published yet, and offer three choices: exclude them, review them separately, or first search for and mark similar sensitive owned memories for publication attention.",
|
|
411
411
|
"- On confirmation, call `update_group_profile` with the confirmed title, responsibility summary, and `confirmed: true`, then call `complete_group_publication` with the exact `scanId`, selected memory IDs, and `confirmed: true`. If a flagged memory is selected, require separate explicit acknowledgement and pass its exact ID in `acknowledgedFlaggedMemoryIds`. If the user edits either proposal, use their wording. An explicit request to join and upload still requires this preview and confirmation.",
|
|
412
|
-
"- If the user asks to show, reopen, restart, or bring back the EchoMem HUD (the context-health overlay), run the shell command `echomem-hud app --client auto`.",
|
|
413
|
-
"- If the user wants the HUD to come back after a computer restart, run the shell command `echomem-hud autostart on --client auto`.",
|
|
414
412
|
AGENTS_MD_END,
|
|
415
413
|
].join("\n");
|
|
416
414
|
}
|
|
@@ -1312,20 +1310,99 @@ function forensicStageLabel(stage) {
|
|
|
1312
1310
|
/** Build the local forensic "Context Doctor" report on a worker thread so the multi-file scan never
|
|
1313
1311
|
* blocks the bridge's event loop (the freeze postmortem: any unbounded sync work on this path is risky). */
|
|
1314
1312
|
export function buildForensicReportOffThread(onProgress, options = {}) {
|
|
1313
|
+
let lastProgress = null;
|
|
1314
|
+
const recordProgress = (progress) => {
|
|
1315
|
+
lastProgress = progress;
|
|
1316
|
+
onProgress?.(progress);
|
|
1317
|
+
};
|
|
1318
|
+
return runForensicReportWorker(recordProgress, options).catch(async (primaryError) => {
|
|
1319
|
+
if (options.failOpen === false)
|
|
1320
|
+
throw primaryError;
|
|
1321
|
+
const failureCode = errorCode(primaryError) || "REPORT_BUILD_FAILED";
|
|
1322
|
+
console.error(`[echomem] local scan degraded after ${failureCode}; continuing without local-history analysis`);
|
|
1323
|
+
onProgress?.({
|
|
1324
|
+
done: lastProgress?.done || 0,
|
|
1325
|
+
total: lastProgress?.total || 0,
|
|
1326
|
+
stage: "finalizing-report",
|
|
1327
|
+
detail: "finishing setup without optional local-history analysis",
|
|
1328
|
+
overall: 0.99,
|
|
1329
|
+
stageDone: 0,
|
|
1330
|
+
stageTotal: 0,
|
|
1331
|
+
});
|
|
1332
|
+
return runForensicReportWorker(undefined, {
|
|
1333
|
+
timeoutMs: 30_000,
|
|
1334
|
+
maxOldGenerationSizeMb: Math.max(64, options.maxOldGenerationSizeMb || 0),
|
|
1335
|
+
}, [], failureCode);
|
|
1336
|
+
});
|
|
1337
|
+
}
|
|
1338
|
+
function errorCode(error) {
|
|
1339
|
+
return error && typeof error === "object" && "code" in error
|
|
1340
|
+
? String(error.code || "")
|
|
1341
|
+
: "";
|
|
1342
|
+
}
|
|
1343
|
+
/** Turn an optional onboarding-stats failure into a terminal, non-polling payload.
|
|
1344
|
+
*
|
|
1345
|
+
* This must only be used for dashboard enrichment. Authentication, consent, vault access, and an
|
|
1346
|
+
* import the user explicitly started retain their normal hard-failure behavior. The reason is a
|
|
1347
|
+
* stable internal code rather than an exception message, so local paths or conversation details
|
|
1348
|
+
* can never cross the localhost bridge by accident. */
|
|
1349
|
+
export function completeOptionalStatsPayload(payload, reason, countsTrusted) {
|
|
1350
|
+
const fallback = {
|
|
1351
|
+
schemaVersion: 1,
|
|
1352
|
+
generatedFrom: ["~/.codex/sessions", "~/.claude/projects"],
|
|
1353
|
+
llmCallsUsed: 0,
|
|
1354
|
+
transcriptsUploaded: false,
|
|
1355
|
+
sessions: { total: 0, codex: 0, claudeCode: 0 },
|
|
1356
|
+
migratable: { pending: 0, alreadyMigrated: 0 },
|
|
1357
|
+
memoriesCaptured: null,
|
|
1358
|
+
};
|
|
1359
|
+
const completed = payload && typeof payload === "object" && !Array.isArray(payload)
|
|
1360
|
+
? { ...payload }
|
|
1361
|
+
: fallback;
|
|
1362
|
+
delete completed.partial;
|
|
1363
|
+
completed.optionalDiagnostics = {
|
|
1364
|
+
degraded: true,
|
|
1365
|
+
reason: /^[A-Z0-9_]+$/.test(reason) ? reason : "OPTIONAL_STATS_FAILED",
|
|
1366
|
+
countsTrusted,
|
|
1367
|
+
};
|
|
1368
|
+
return completed;
|
|
1369
|
+
}
|
|
1370
|
+
function runForensicReportWorker(onProgress, options, sources, degradedReason) {
|
|
1315
1371
|
const forensicsUrl = runtimeModuleUrl("forensics");
|
|
1372
|
+
const serializedSources = sources === undefined ? "undefined" : JSON.stringify(sources);
|
|
1373
|
+
const serializedDegradedReason = JSON.stringify(degradedReason || "");
|
|
1316
1374
|
const code = `
|
|
1317
1375
|
import { parentPort } from "node:worker_threads";
|
|
1318
|
-
import { buildForensicReport } from ${JSON.stringify(forensicsUrl)};
|
|
1376
|
+
import { buildForensicReport, validateForensicReportForSetup } from ${JSON.stringify(forensicsUrl)};
|
|
1319
1377
|
try {
|
|
1320
1378
|
const report = await buildForensicReport({
|
|
1379
|
+
sources: ${serializedSources},
|
|
1321
1380
|
includeLegacyGoldenStandard: false,
|
|
1322
1381
|
onProgress: (done, total, stage, detail, overall, stageDone, stageTotal) => parentPort?.postMessage({
|
|
1323
1382
|
progress: { done, total, stage, detail, overall, stageDone, stageTotal },
|
|
1324
1383
|
}),
|
|
1325
1384
|
});
|
|
1385
|
+
const degradedReason = ${serializedDegradedReason};
|
|
1386
|
+
if (degradedReason) {
|
|
1387
|
+
report.scanDiagnostics = {
|
|
1388
|
+
degraded: true,
|
|
1389
|
+
reason: degradedReason,
|
|
1390
|
+
skippedSources: ["codex", "claude"],
|
|
1391
|
+
};
|
|
1392
|
+
}
|
|
1393
|
+
const validation = validateForensicReportForSetup(report);
|
|
1394
|
+
if (!validation.ok) {
|
|
1395
|
+
const error = new Error(validation.message);
|
|
1396
|
+
error.code = validation.code;
|
|
1397
|
+
throw error;
|
|
1398
|
+
}
|
|
1326
1399
|
parentPort?.postMessage({ ok: true, report });
|
|
1327
1400
|
} catch (error) {
|
|
1328
|
-
parentPort?.postMessage({
|
|
1401
|
+
parentPort?.postMessage({
|
|
1402
|
+
ok: false,
|
|
1403
|
+
message: error instanceof Error ? error.message : String(error),
|
|
1404
|
+
code: error && typeof error === "object" && "code" in error ? String(error.code || "") : "",
|
|
1405
|
+
});
|
|
1329
1406
|
}
|
|
1330
1407
|
`;
|
|
1331
1408
|
const requestedHeapMb = options.maxOldGenerationSizeMb;
|
|
@@ -1369,7 +1446,10 @@ export function buildForensicReportOffThread(onProgress, options = {}) {
|
|
|
1369
1446
|
finish({ ok: true, report: msg.report });
|
|
1370
1447
|
return;
|
|
1371
1448
|
}
|
|
1372
|
-
|
|
1449
|
+
const error = new Error(typeof msg.message === "string" ? msg.message : "Local forensic report failed");
|
|
1450
|
+
if (typeof msg.code === "string" && msg.code)
|
|
1451
|
+
error.code = msg.code;
|
|
1452
|
+
finish({ ok: false, error });
|
|
1373
1453
|
});
|
|
1374
1454
|
worker.once("error", (error) => {
|
|
1375
1455
|
finish({ ok: false, error });
|
|
@@ -2622,19 +2702,20 @@ async function cmdSetup(flags) {
|
|
|
2622
2702
|
else {
|
|
2623
2703
|
await cmdLogin(flags);
|
|
2624
2704
|
}
|
|
2625
|
-
if (flags["with-hud"])
|
|
2626
|
-
|
|
2705
|
+
if (flags["with-hud"]) {
|
|
2706
|
+
console.log("ℹ️ The standalone EchoMem HUD has been retired. Echo Desktop now owns setup and status.");
|
|
2707
|
+
}
|
|
2627
2708
|
}
|
|
2628
2709
|
/**
|
|
2629
2710
|
* `echomem-mcp init` — the one-command install. Configures EVERY coding agent installed on this
|
|
2630
2711
|
* machine (Codex + Claude Code + Claude Desktop, not just auto-detected ones), installs EchoMem's
|
|
2631
|
-
* Codex skills
|
|
2712
|
+
* Codex skills and writes the AGENTS.md memory guidance. One browser
|
|
2632
2713
|
* bridge then runs permission → report → login → plan if needed → extraction in that order.
|
|
2633
2714
|
* `setup`/`login`/`update` remain granular primitives; init picks the full product defaults.
|
|
2634
2715
|
*/
|
|
2635
2716
|
async function cmdInit(flags) {
|
|
2636
|
-
console.log("Setting up EchoMem — shared memory for all your coding agents
|
|
2637
|
-
// 1. Configure every installed agent + write AGENTS.md. Hold login
|
|
2717
|
+
console.log("Setting up EchoMem — shared memory for all your coding agents.\n");
|
|
2718
|
+
// 1. Configure every installed agent + write AGENTS.md. Hold login so we control ordering.
|
|
2638
2719
|
await cmdSetup({
|
|
2639
2720
|
...flags,
|
|
2640
2721
|
all: true,
|
|
@@ -2643,10 +2724,7 @@ async function cmdInit(flags) {
|
|
|
2643
2724
|
"init-quiet": true,
|
|
2644
2725
|
"install-save-hooks": flags["no-save-hooks"] !== true,
|
|
2645
2726
|
});
|
|
2646
|
-
// 2.
|
|
2647
|
-
if (!flags["no-hud"])
|
|
2648
|
-
await cmdSetupHud(flags);
|
|
2649
|
-
// 3. Start one ordered onboarding bridge. A fresh device logs in only after consent + report.
|
|
2727
|
+
// 2. Start one ordered onboarding bridge. A fresh device logs in only after consent + report.
|
|
2650
2728
|
console.log("");
|
|
2651
2729
|
if (!flags["skip-login"] && !flags["no-login"] && !await cmdOnboarding(flags)) {
|
|
2652
2730
|
console.log("\nEchoMem is configured, but onboarding did not finish. Run `echomem-mcp init` again when you are ready.");
|
|
@@ -2655,13 +2733,7 @@ async function cmdInit(flags) {
|
|
|
2655
2733
|
console.log("");
|
|
2656
2734
|
console.log("🎉 EchoMem is ready.");
|
|
2657
2735
|
console.log(" • MCP memory is configured for every coding agent installed on this machine.");
|
|
2658
|
-
|
|
2659
|
-
console.log(' • The context HUD is running (top-right). Right-click it → "Show after restart" to keep it,');
|
|
2660
|
-
console.log(' or just tell your agent "open the EchoMem HUD" anytime (it runs: echomem-hud app).');
|
|
2661
|
-
}
|
|
2662
|
-
else {
|
|
2663
|
-
console.log(' • Start the context HUD anytime with: echomem-hud app');
|
|
2664
|
-
}
|
|
2736
|
+
console.log(" • Echo Desktop shows connection status and manages this device credential.");
|
|
2665
2737
|
console.log(' • Try it now: ask your agent — "search my EchoMem for what I\'ve been working on and recap it."');
|
|
2666
2738
|
}
|
|
2667
2739
|
/**
|
|
@@ -2744,55 +2816,6 @@ function selectSetupTargets(requested, all) {
|
|
|
2744
2816
|
}
|
|
2745
2817
|
return requested ? knownClients().filter((client) => client.id === requested) : detectClients();
|
|
2746
2818
|
}
|
|
2747
|
-
async function cmdSetupHud(flags) {
|
|
2748
|
-
const client = parseHudClient(flags["hud-client"] || "auto");
|
|
2749
|
-
const hudCli = resolveHudCliPath();
|
|
2750
|
-
console.log("");
|
|
2751
|
-
console.log(`✅ EchoMem HUD available: ${process.execPath} ${hudCli}`);
|
|
2752
|
-
if (flags["install-hud-hooks"]) {
|
|
2753
|
-
const paths = installHooks(client === "claude-desktop" ? "auto" : client);
|
|
2754
|
-
console.log(`✅ Installed EchoMem HUD hook support:\n${paths.map((p) => ` - ${p}`).join("\n")}`);
|
|
2755
|
-
console.log(" Codex users: run /hooks in a new Codex session to review and trust changed hooks.");
|
|
2756
|
-
}
|
|
2757
|
-
else {
|
|
2758
|
-
console.log("ℹ️ HUD hooks not installed. Add --install-hud-hooks if you want lifecycle wakeups.");
|
|
2759
|
-
}
|
|
2760
|
-
if (!flags["no-launch-hud"]) {
|
|
2761
|
-
try {
|
|
2762
|
-
spawn(process.execPath, [hudCli, "app", "--client", client], { stdio: "ignore", detached: true }).unref();
|
|
2763
|
-
console.log("✅ Launched EchoMem HUD app.");
|
|
2764
|
-
}
|
|
2765
|
-
catch {
|
|
2766
|
-
console.log(`ℹ️ Could not auto-launch HUD. Run: echomem-hud app --client ${client}`);
|
|
2767
|
-
}
|
|
2768
|
-
}
|
|
2769
|
-
else {
|
|
2770
|
-
console.log(`Run the HUD later with: echomem-hud app --client ${client}`);
|
|
2771
|
-
}
|
|
2772
|
-
}
|
|
2773
|
-
function resolveHudCliPath() {
|
|
2774
|
-
const entry = fs.realpathSync(process.argv[1] || "");
|
|
2775
|
-
const compiledEntry = compiledDistPathForSource(entry);
|
|
2776
|
-
if (compiledEntry) {
|
|
2777
|
-
const compiledHud = path.join(path.dirname(compiledEntry), "hud", "cli.js");
|
|
2778
|
-
if (fs.existsSync(compiledHud))
|
|
2779
|
-
return compiledHud;
|
|
2780
|
-
}
|
|
2781
|
-
const base = path.dirname(entry);
|
|
2782
|
-
const candidate = path.join(base, "hud", "cli.js");
|
|
2783
|
-
if (fs.existsSync(candidate))
|
|
2784
|
-
return candidate;
|
|
2785
|
-
const sourceCandidate = path.join(base, "hud", "cli.ts");
|
|
2786
|
-
if (fs.existsSync(sourceCandidate)) {
|
|
2787
|
-
throw new Error("The local HUD CLI is not built. Run npm --prefix packages/mcp-server run build and retry.");
|
|
2788
|
-
}
|
|
2789
|
-
return candidate;
|
|
2790
|
-
}
|
|
2791
|
-
function parseHudClient(value) {
|
|
2792
|
-
return value === "codex" || value === "claude-code" || value === "claude-desktop" || value === "both" || value === "auto"
|
|
2793
|
-
? value
|
|
2794
|
-
: "auto";
|
|
2795
|
-
}
|
|
2796
2819
|
function localBridgeOptions(flags) {
|
|
2797
2820
|
const port = typeof flags["dev-port"] === "string" ? Number(flags["dev-port"]) : undefined;
|
|
2798
2821
|
if (port !== undefined && (!Number.isInteger(port) || port < 1024 || port > 65535)) {
|
|
@@ -3065,7 +3088,23 @@ async function cmdOnboarding(flags) {
|
|
|
3065
3088
|
extracted: 0,
|
|
3066
3089
|
});
|
|
3067
3090
|
};
|
|
3068
|
-
const
|
|
3091
|
+
const publishOptionalStatsFallback = (generation, reason, countsTrusted) => {
|
|
3092
|
+
if (generation !== refreshGeneration)
|
|
3093
|
+
return;
|
|
3094
|
+
stats = completeOptionalStatsPayload(stats, reason, countsTrusted);
|
|
3095
|
+
srv.setStats(stats);
|
|
3096
|
+
const pending = countsTrusted ? latestPendingEstimate : 0;
|
|
3097
|
+
srv.setProgress({
|
|
3098
|
+
status: "idle",
|
|
3099
|
+
total: pending,
|
|
3100
|
+
completed: 0,
|
|
3101
|
+
running: 0,
|
|
3102
|
+
queued: pending,
|
|
3103
|
+
failed: 0,
|
|
3104
|
+
extracted: 0,
|
|
3105
|
+
});
|
|
3106
|
+
};
|
|
3107
|
+
const refreshLocalStatsForTokenCore = async (activeToken) => {
|
|
3069
3108
|
const generation = ++refreshGeneration;
|
|
3070
3109
|
lastProcessedImportKeys = null; // shared with /migrate so it can assemble only the pending sessions
|
|
3071
3110
|
let importStatusUnavailable = false;
|
|
@@ -3270,15 +3309,31 @@ async function cmdOnboarding(flags) {
|
|
|
3270
3309
|
return;
|
|
3271
3310
|
stats = fullPayload;
|
|
3272
3311
|
srv.setStats(fullPayload);
|
|
3273
|
-
})()
|
|
3312
|
+
})().catch((e) => {
|
|
3313
|
+
if (generation !== refreshGeneration)
|
|
3314
|
+
return;
|
|
3315
|
+
console.error(`[echomem] optional full local-history stats unavailable; continuing (${errorCode(e) || "FULL_STATS_FAILED"})`);
|
|
3316
|
+
publishOptionalStatsFallback(generation, "FULL_STATS_FAILED", true);
|
|
3317
|
+
});
|
|
3274
3318
|
return initialExact;
|
|
3275
3319
|
}).catch((e) => {
|
|
3276
3320
|
if (generation === refreshGeneration) {
|
|
3277
|
-
console.error(`
|
|
3321
|
+
console.error(`[echomem] optional exact local-history discovery unavailable; continuing (${errorCode(e) || "EXACT_DISCOVERY_FAILED"})`);
|
|
3322
|
+
publishOptionalStatsFallback(generation, "EXACT_DISCOVERY_FAILED", false);
|
|
3278
3323
|
}
|
|
3279
3324
|
return disc;
|
|
3280
3325
|
});
|
|
3281
3326
|
};
|
|
3327
|
+
const refreshLocalStatsForToken = async (activeToken) => {
|
|
3328
|
+
const expectedGeneration = refreshGeneration + 1;
|
|
3329
|
+
try {
|
|
3330
|
+
await refreshLocalStatsForTokenCore(activeToken);
|
|
3331
|
+
}
|
|
3332
|
+
catch (e) {
|
|
3333
|
+
console.error(`[echomem] optional local-history stats unavailable; continuing (${errorCode(e) || "OPTIONAL_STATS_FAILED"})`);
|
|
3334
|
+
publishOptionalStatsFallback(expectedGeneration, "OPTIONAL_STATS_FAILED", false);
|
|
3335
|
+
}
|
|
3336
|
+
};
|
|
3282
3337
|
srv.setLogoutHandler(resetLocalLoginState);
|
|
3283
3338
|
srv.setTokenRefreshHandler(async ({ token: nextToken, key: nextKey }) => {
|
|
3284
3339
|
if (!await verifyAndPrint({ token: nextToken, key: nextKey }))
|
|
@@ -3714,14 +3769,13 @@ function cmdLogout() {
|
|
|
3714
3769
|
const HELP = `EchoMem MCP — local memory bridge
|
|
3715
3770
|
|
|
3716
3771
|
Usage:
|
|
3717
|
-
echomem-mcp init
|
|
3772
|
+
echomem-mcp init Legacy/headless setup: configure agents + login + local-history onboarding
|
|
3718
3773
|
echomem-mcp Run the MCP server (stdio; default — used by your editor)
|
|
3719
3774
|
echomem-mcp setup [--client X] Detect editor, write its MCP config, then connect this device
|
|
3720
3775
|
echomem-mcp setup --skip-login Write MCP config without opening login/browser
|
|
3721
3776
|
echomem-mcp setup --no-codex-skills Skip installing the bundled EchoMem Codex skills
|
|
3722
3777
|
echomem-mcp update --all Install this bridge durably + repoint detected clients; no login/browser
|
|
3723
3778
|
echomem-mcp update --client X Repoint one MCP client; no login/browser
|
|
3724
|
-
echomem-mcp setup --with-hud Configure MCP, then launch the EchoMem context HUD
|
|
3725
3779
|
echomem-mcp login Connect this device only; never scans or imports local history
|
|
3726
3780
|
echomem-mcp login --force Reconnect this device with a different account
|
|
3727
3781
|
echomem-mcp unlock Privately unlock the vault on this trusted device
|
|
@@ -3744,7 +3798,6 @@ Manual / headless:
|
|
|
3744
3798
|
${MCP_UPDATE_ALL_COMMAND} # one-shot latest update for detected clients, no browser login
|
|
3745
3799
|
${MCP_UPDATE_COMMAND} --client codex # update one client only
|
|
3746
3800
|
echomem-mcp setup --dev /abs/path/dist/index.js # point clients at a local checkout
|
|
3747
|
-
echomem-mcp setup --with-hud --install-hud-hooks --install-save-hooks --client codex [--hud-client auto]
|
|
3748
3801
|
echomem-mcp setup --install-save-hooks --all Install proactive private-save completion checks
|
|
3749
3802
|
echomem-mcp sync-usage --days 7 --limit 50 --dry-run
|
|
3750
3803
|
|
package/dist/v1-contract.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
|
-
import { MEMORY_CITATION_INSTRUCTION, SAVED_MEMORY_RECEIPT_INSTRUCTION, withMcpVersion, } from "./package-metadata.js";
|
|
2
|
+
import { MCP_DESKTOP_MANAGED, MCP_VAULT_UNLOCK_INSTRUCTION, MEMORY_CITATION_INSTRUCTION, SAVED_MEMORY_RECEIPT_INSTRUCTION, withMcpVersion, } from "./package-metadata.js";
|
|
3
3
|
export const canonicalToolNames = {
|
|
4
4
|
search: "search_memories",
|
|
5
5
|
save: "save_conversation",
|
|
@@ -388,7 +388,7 @@ export function listToolSpecs(opts = {}) {
|
|
|
388
388
|
},
|
|
389
389
|
{
|
|
390
390
|
name: canonicalToolNames.save,
|
|
391
|
-
description: `Save durable knowledge from this conversation into the user's private EchoMem (durable memories are extracted automatically). Call before the final response when work produced a durable decision, confirmed preference, implementation, resolved bug, commit, passing verification, release, or project milestone; this private checkpoint does not require a separate user request. Omit secrets, incidental chatter, and trivial read-only work. If the encrypted vault is locked, tell the user to
|
|
391
|
+
description: `Save durable knowledge from this conversation into the user's private EchoMem (durable memories are extracted automatically). Call before the final response when work produced a durable decision, confirmed preference, implementation, resolved bug, commit, passing verification, release, or project milestone; this private checkpoint does not require a separate user request. Omit secrets, incidental chatter, and trivial read-only work. If the encrypted vault is locked, tell the user to ${MCP_VAULT_UNLOCK_INSTRUCTION} and never silently skip a qualifying checkpoint. Private persistence happens first. For group sharing, reuse the exact groupSharingScopeId returned by get_group_session_sharing, request_group_session_sharing, or an earlier save in this conversation. Never reuse it in another conversation or save it as memory. Each group has an independent decision under the same conversation scope; eligible memories sync automatically to every approved group, while flagged memories stay private. If a selected group has no decision yet, call request_group_session_sharing so supported hosts render a choice UI; its fallback tells you when a text Yes/No prompt is required. Silence leaves consent unset; never infer the answer. New extraction input uses the plan's weekly processing allowance; if the limit is reached, nothing is saved. passthrough=true stores the text verbatim as a session capsule. ${SAVED_MEMORY_RECEIPT_INSTRUCTION}`,
|
|
392
392
|
inputSchema: {
|
|
393
393
|
type: "object",
|
|
394
394
|
properties: {
|
|
@@ -909,11 +909,11 @@ export function listToolSpecs(opts = {}) {
|
|
|
909
909
|
},
|
|
910
910
|
{
|
|
911
911
|
name: canonicalToolNames.checkpointByContext,
|
|
912
|
-
description: withMcpVersion(`Rebuild a clean-context checkpoint / decision log from one EchoMem contextId. ${recallPlanNote} Use this when the user or EchoMem
|
|
912
|
+
description: withMcpVersion(`Rebuild a clean-context checkpoint / decision log from one EchoMem contextId. ${recallPlanNote} Use this when the user or EchoMem returns a contextId for a renewed coding session and you need the session handoff, not a raw memory dump. It deterministically fetches that context and formats it as orientation state: decisions, carryover, constraints, and checkpoints. ${memoryCitationInstruction} Current time: ${currentTime}.`),
|
|
913
913
|
inputSchema: {
|
|
914
914
|
type: "object",
|
|
915
915
|
properties: {
|
|
916
|
-
contextId: { type: "string", description: "The EchoMem contextId
|
|
916
|
+
contextId: { type: "string", description: "The EchoMem contextId returned by save_conversation or Renew session." },
|
|
917
917
|
limit: { type: "number", default: 100 },
|
|
918
918
|
triggerMessage: {
|
|
919
919
|
type: "string",
|
|
@@ -931,7 +931,7 @@ export function listToolSpecs(opts = {}) {
|
|
|
931
931
|
},
|
|
932
932
|
{
|
|
933
933
|
name: canonicalToolNames.updateStatus,
|
|
934
|
-
description: `Check whether this installed EchoMem MCP bridge is behind the latest
|
|
934
|
+
description: `Check whether this installed EchoMem MCP bridge is behind the latest version. Works without login, uploads no user transcript, and normal background checks are cached. ${MCP_DESKTOP_MANAGED ? "Echo Desktop owns updates for this runtime; direct the user back to the desktop app." : "If it reports an update, tell the user and offer to run the returned update command."} After updating, the user must start a new agent/MCP session.${updateSection}`,
|
|
935
935
|
inputSchema: {
|
|
936
936
|
type: "object",
|
|
937
937
|
properties: {
|
|
@@ -945,7 +945,7 @@ export function listToolSpecs(opts = {}) {
|
|
|
945
945
|
},
|
|
946
946
|
{
|
|
947
947
|
name: canonicalToolNames.contextHealth,
|
|
948
|
-
description: "Show the current local Codex/Claude context-health score as markdown: clean percentage, tracked lower-bound dead-weight, redundant reads, source client, and token-count source. Use when the user asks about
|
|
948
|
+
description: "Show the current local Codex/Claude context-health score as markdown: clean percentage, tracked lower-bound dead-weight, redundant reads, source client, and token-count source. Use when the user asks about dirty context, context pollution, or whether cleanup is worth it. This on-demand tool reads local agent logs only and needs no login.",
|
|
949
949
|
inputSchema: {
|
|
950
950
|
type: "object",
|
|
951
951
|
properties: {
|
package/package.json
CHANGED
|
@@ -1,13 +1,16 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@echomem/mcp",
|
|
3
|
-
"version": "1.4.
|
|
4
|
-
"description": "EchoMem MCP bridge: cloud-first memory tools
|
|
3
|
+
"version": "1.4.35",
|
|
4
|
+
"description": "EchoMem MCP bridge: cloud-first memory tools and the Agent Doctor workspace forensics report (cost ledger + 3D repo city)",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"type": "module",
|
|
7
|
+
"echoDesktop": {
|
|
8
|
+
"minimumVersion": "1.0.0",
|
|
9
|
+
"runtimeLayout": 1
|
|
10
|
+
},
|
|
7
11
|
"bin": {
|
|
8
12
|
"mcp": "dist/index.js",
|
|
9
|
-
"echomem-mcp": "dist/index.js"
|
|
10
|
-
"echomem-hud": "dist/hud/cli.js"
|
|
13
|
+
"echomem-mcp": "dist/index.js"
|
|
11
14
|
},
|
|
12
15
|
"files": [
|
|
13
16
|
"dist",
|
|
@@ -23,22 +26,24 @@
|
|
|
23
26
|
"smoke": "node smoke.mjs",
|
|
24
27
|
"preview:extraction": "npm run build && node scripts/preview-extraction.mjs",
|
|
25
28
|
"stress:long-history": "npm run build && node scripts/stress-long-history.mjs",
|
|
29
|
+
"stress:long-claude-history": "npm run build && node scripts/stress-long-claude-history.mjs",
|
|
26
30
|
"test:artifact": "npm run build && node test/package-artifact.test.mjs",
|
|
27
31
|
"test:registry": "node test/registry-artifact.test.mjs",
|
|
28
32
|
"test:registry-ui": "npm run build && node test/registry-ui.test.mjs",
|
|
29
33
|
"test:ui": "npm run build && node test/setup-ui.test.mjs",
|
|
34
|
+
"test:onboarding-resilience": "npm run build && node test/onboarding-resilience.test.mjs",
|
|
30
35
|
"test:billing-ui": "npm run build && node test/setup-ui.test.mjs billing",
|
|
31
|
-
"test": "npm run build && node test/local-data-paths.test.mjs && node test/crypto.test.mjs && node test/integration.test.mjs && node test/local-auth.test.mjs && node test/retrieval-only.test.mjs && node test/no-restart.test.mjs && node test/report.test.mjs && node test/forensics.test.mjs && node test/canonical-golden.test.mjs && node test/tools.test.mjs && node test/group-map.test.mjs && node test/update-check.test.mjs && node test/delete.test.mjs && node test/low-touch-tools.test.mjs && node test/migrate.test.mjs && node test/restart-recovery.test.mjs && node test/
|
|
36
|
+
"test": "npm run build && node test/local-data-paths.test.mjs && node test/crypto.test.mjs && node test/integration.test.mjs && node test/onboarding-resilience.test.mjs && node test/local-auth.test.mjs && node test/retrieval-only.test.mjs && node test/no-restart.test.mjs && node test/report.test.mjs && node test/forensics.test.mjs && node test/canonical-golden.test.mjs && node test/tools.test.mjs && node test/group-map.test.mjs && node test/update-check.test.mjs && node test/delete.test.mjs && node test/low-touch-tools.test.mjs && node test/migrate.test.mjs && node test/restart-recovery.test.mjs && node test/save-checkpoint-hook.test.mjs",
|
|
32
37
|
"prepack": "npm run build && node scripts/bundle-city.mjs"
|
|
33
38
|
},
|
|
34
39
|
"dependencies": {
|
|
35
40
|
"@modelcontextprotocol/sdk": "^1.0.1",
|
|
36
41
|
"axios": "^1.6.8",
|
|
37
|
-
"electron": "41.7.1",
|
|
38
42
|
"zod": "^3.22.4"
|
|
39
43
|
},
|
|
40
44
|
"devDependencies": {
|
|
41
45
|
"@types/node": "^20.11.0",
|
|
46
|
+
"electron": "41.7.1",
|
|
42
47
|
"tsx": "^4.22.4",
|
|
43
48
|
"typescript": "^5.3.3"
|
|
44
49
|
}
|