@echomem/mcp 1.4.34 → 1.4.36

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.
@@ -38,7 +38,13 @@ export const SETUP_PAGE_CLIENT_EXTRACTION = String.raw ` /* ---------- dash
38
38
  }
39
39
  return envelope;
40
40
  }
41
- if (!gs || !Number.isFinite(sessions) || sessions <= 0 || canonicalSessions !== sessions || canonicalInput <= 0 || skipped !== 0) {
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
- '<p>Close this tab and run <code>echomem-mcp init</code> again.</p>' +
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>Start a Codex or Claude Code session, then run <code>echomem-mcp init</code> again.</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 = !pendingTrusted
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 = !pendingTrusted
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=&quot;none&quot;;this.nextElementSibling.style.display=&quot;grid&quot;;" /><span class="pfFallback">' + fallback + '</span></span>';
299
319
  };
300
- document.getElementById("exSources").innerHTML = !pendingTrusted
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&hellip;</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 = pendingTrusted
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();
@@ -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.
@@ -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 previewReport = {
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(previewReport);
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 { installHooks, installSaveCheckpointHooks } from "./hud/hooks.js";
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 run `echomem-mcp unlock`; never silently skip it.",
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 run `echomem-mcp unlock` locally if the tool reports that the key is required.",
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({ ok: false, message: error instanceof Error ? error.message : String(error) });
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
- finish({ ok: false, error: new Error(typeof msg.message === "string" ? msg.message : "Local forensic report failed") });
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
- await cmdSetupHud(flags);
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, writes the AGENTS.md memory guidance, and launches the context HUD. One browser
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, plus the live context HUD.\n");
2637
- // 1. Configure every installed agent + write AGENTS.md. Hold login + HUD so we control ordering.
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. Bring the HUD up NOW (non-blocking) so everything is already running while onboarding proceeds.
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
- if (!flags["no-hud"]) {
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 refreshLocalStatsForToken = async (activeToken) => {
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(`Could not finish exact local extraction estimate: ${e instanceof Error ? e.message : String(e)}`);
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 One command: configure agents + HUD + login + local-history onboarding
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