@echomem/mcp 1.4.27 → 1.4.29

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.
@@ -5,6 +5,21 @@ const ROLLOUT_FILE_RE = /^rollout-.*\.jsonl$/;
5
5
  const UUID_RE = /([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})/i;
6
6
  const FIRST_LINE_CHUNK_BYTES = 64 * 1024;
7
7
  const MAX_FIRST_LINE_BYTES = 2 * 1024 * 1024;
8
+ function includesCompleteSessionMetadataLine(buffers) {
9
+ const text = Buffer.concat(buffers).toString("utf8");
10
+ const lines = text.split(/\r?\n/);
11
+ if (!text.endsWith("\n"))
12
+ lines.pop();
13
+ return lines.some((line) => {
14
+ try {
15
+ const row = JSON.parse(line);
16
+ return isRecord(row) && row.type === "session_meta" && isRecord(row.payload);
17
+ }
18
+ catch {
19
+ return false;
20
+ }
21
+ });
22
+ }
8
23
  function initialJsonLines(file) {
9
24
  const fd = fs.openSync(file, "r");
10
25
  try {
@@ -19,6 +34,11 @@ function initialJsonLines(file) {
19
34
  const piece = Buffer.from(chunk.subarray(0, bytesRead));
20
35
  buffers.push(piece);
21
36
  total += bytesRead;
37
+ // Normal Codex rollouts put session metadata in the first block. Stop as soon as that
38
+ // complete row is available, while preserving the old 2MB fallback for unusual/legacy
39
+ // files whose metadata appears later.
40
+ if (includesCompleteSessionMetadataLine(buffers))
41
+ break;
22
42
  }
23
43
  if (!buffers.length)
24
44
  return [];
package/dist/forensics.js CHANGED
@@ -1391,7 +1391,7 @@ export async function buildForensicReport(opts) {
1391
1391
  return band[0] + (band[1] - band[0]) * ratio;
1392
1392
  };
1393
1393
  const progress = (stage, detail, stageDone = done, stageTotal = total) => {
1394
- opts?.onProgress?.(done, total, stage, detail, overallFor(stage, stageDone, stageTotal));
1394
+ opts?.onProgress?.(done, total, stage, detail, overallFor(stage, stageDone, stageTotal), stageDone, stageTotal);
1395
1395
  };
1396
1396
  progress("reading-transcripts", "finding local Codex and Claude transcript files");
1397
1397
  const tick = (stage = "reading-transcripts") => {
package/dist/index.js CHANGED
@@ -12,7 +12,7 @@ import { contextHealthMarkdown, recomposeCapsuleMarkdown } from "./hud/api.js";
12
12
  import { createHash, randomUUID } from "node:crypto";
13
13
  import { fetchEncryptionConfig, decryptMemoryFields, verifyKeyB64, } from "./encryption.js";
14
14
  import { runCli } from "./setup.js";
15
- import { MCP_PACKAGE_VERSION, MCP_SERVER_INSTRUCTIONS } from "./package-metadata.js";
15
+ import { MCP_PACKAGE_VERSION, MCP_SERVER_INSTRUCTIONS, MEMORY_CITATION_INSTRUCTION, } from "./package-metadata.js";
16
16
  import { clearBillingAlert, writeBillingAlert } from "./billing-alert.js";
17
17
  import { checkLatestUpdateStatus, formatUpdateNotice, formatUpdateStatusText, startBackgroundUpdateCheck, } from "./update-check.js";
18
18
  const ECHO_API_BASE_URL = process.env.ECHO_API_BASE_URL || "https://echo-mem-chrome.vercel.app";
@@ -32,6 +32,9 @@ function memoryMarkdownLink(url, keys, description) {
32
32
  .replace(/\]/g, "\\]");
33
33
  return `[${label}](${url})`;
34
34
  }
35
+ function withMemoryCitationInstruction(text) {
36
+ return `${text}\n\n${MEMORY_CITATION_INSTRUCTION}`;
37
+ }
35
38
  /** Thrown when no API token is present yet — the model gets a "run login" nudge, not a hard error. */
36
39
  class NoTokenError extends Error {
37
40
  }
@@ -1446,7 +1449,12 @@ class EchoMemMCPServer {
1446
1449
  ].filter(Boolean).join("\n");
1447
1450
  })
1448
1451
  .join("\n\n");
1449
- return { content: [{ type: "text", text: `Retrieved ${memories.length} memories:\n\n${formattedResults}` }] };
1452
+ return {
1453
+ content: [{
1454
+ type: "text",
1455
+ text: withMemoryCitationInstruction(`Retrieved ${memories.length} memories:\n\n${formattedResults}`),
1456
+ }],
1457
+ };
1450
1458
  }
1451
1459
  // Fallback: untuned / time-range shape.
1452
1460
  const { success, memories, error } = result;
@@ -1463,7 +1471,12 @@ Category: ${m.category} | Object: ${m.object} | Emotion: ${m.emotion}
1463
1471
  Description: ${m.description}
1464
1472
  Details: ${m.details || "N/A"}`)
1465
1473
  .join("\n\n");
1466
- return { content: [{ type: "text", text: `Found ${memories.length} relevant memories:\n\n${formattedResults}` }] };
1474
+ return {
1475
+ content: [{
1476
+ type: "text",
1477
+ text: withMemoryCitationInstruction(`Found ${memories.length} relevant memories:\n\n${formattedResults}`),
1478
+ }],
1479
+ };
1467
1480
  }
1468
1481
  async handleSave(args, rec) {
1469
1482
  const sourceFallback = this.getMcpClientAnalytics().platform_source;
@@ -1565,7 +1578,7 @@ Details: ${m.details || "N/A"}`)
1565
1578
  content: [
1566
1579
  {
1567
1580
  type: "text",
1568
- text: `Found ${memories.length} memories between ${parsed.startDate} and ${parsed.endDate}:\n\n${formattedResults}`,
1581
+ text: withMemoryCitationInstruction(`Found ${memories.length} memories between ${parsed.startDate} and ${parsed.endDate}:\n\n${formattedResults}`),
1569
1582
  },
1570
1583
  ],
1571
1584
  };
@@ -1591,7 +1604,7 @@ Details: ${m.details || "N/A"}`)
1591
1604
  content: [
1592
1605
  {
1593
1606
  type: "text",
1594
- text: `Recalled ${memories.length} memories from context ${parsed.contextId} (deterministic full batch):\n\n${formattedResults}`,
1607
+ text: withMemoryCitationInstruction(`Recalled ${memories.length} memories from context ${parsed.contextId} (deterministic full batch):\n\n${formattedResults}`),
1595
1608
  },
1596
1609
  ],
1597
1610
  };
@@ -1636,7 +1649,10 @@ Details: ${m.details || "N/A"}`)
1636
1649
  const desc = readString(memory, "description") ?? "";
1637
1650
  const details = compactOneLine(readString(memory, "details"), 260);
1638
1651
  const id = readString(memory, "id");
1639
- lines.push(`- ${title}${id ? ` (${id})` : ""}: ${desc}${details ? ` — ${details}` : ""}`);
1652
+ const linkedTitle = id
1653
+ ? memoryMarkdownLink(personalMemoryWebUrl(id), title, desc)
1654
+ : title;
1655
+ lines.push(`- ${linkedTitle}${id ? ` (${id})` : ""}: ${desc}${details ? ` — ${details}` : ""}`);
1640
1656
  }
1641
1657
  lines.push("");
1642
1658
  }
@@ -1645,7 +1661,7 @@ Details: ${m.details || "N/A"}`)
1645
1661
  content: [
1646
1662
  {
1647
1663
  type: "text",
1648
- text: lines.join("\n"),
1664
+ text: withMemoryCitationInstruction(lines.join("\n")),
1649
1665
  },
1650
1666
  ],
1651
1667
  };
@@ -1672,7 +1688,7 @@ Details: ${m.details || "N/A"}`)
1672
1688
  content: [
1673
1689
  {
1674
1690
  type: "text",
1675
- text: `Found ${memories.length} memories matching keywords:\n\n${formattedResults}`,
1691
+ text: withMemoryCitationInstruction(`Found ${memories.length} memories matching keywords:\n\n${formattedResults}`),
1676
1692
  },
1677
1693
  ],
1678
1694
  };
@@ -1729,7 +1745,7 @@ Details: ${m.details || "N/A"}`;
1729
1745
  content: [
1730
1746
  {
1731
1747
  type: "text",
1732
- text: `Found ${memories.length} others' public memories${metadata ? ` (${metadata})` : ""}:\n\n${formattedResults}`,
1748
+ text: withMemoryCitationInstruction(`Found ${memories.length} others' public memories${metadata ? ` (${metadata})` : ""}:\n\n${formattedResults}`),
1733
1749
  },
1734
1750
  ],
1735
1751
  };
@@ -1857,7 +1873,7 @@ Details: ${m.details || "N/A"}`;
1857
1873
  : "",
1858
1874
  ].filter(Boolean).join("\n");
1859
1875
  return {
1860
- content: [{ type: "text", text }],
1876
+ content: [{ type: "text", text: withMemoryCitationInstruction(text) }],
1861
1877
  };
1862
1878
  }
1863
1879
  async handleGroupContext(args) {
package/dist/migrate.js CHANGED
@@ -186,14 +186,17 @@ export function normalizeCwd(cwd) {
186
186
  const m = cwd.match(/worktrees\/[^/]+\/(.+)$/);
187
187
  return m ? m[1] : cwd;
188
188
  }
189
- function userCreatedCodexFiles(codexRoot) {
189
+ function userCreatedCodexSessionFiles(codexRoot) {
190
190
  return discoverCodexSessionFiles({
191
191
  ...(codexRoot
192
192
  ? { roots: [{ kind: "active", path: codexRoot, priority: 0 }] }
193
193
  : {}),
194
194
  includeArchived: false,
195
195
  userInitiatedOnly: true,
196
- }).files.map((file) => file.path);
196
+ }).files;
197
+ }
198
+ function userCreatedCodexFiles(codexRoot) {
199
+ return userCreatedCodexSessionFiles(codexRoot).map((file) => file.path);
197
200
  }
198
201
  function userCreatedClaudeFiles(claudeRoot) {
199
202
  return walk(claudeRoot, (candidate) => candidate.endsWith(".jsonl"), (name) => name === "subagents" || name === "workflows").filter(isUserCreatedClaudeSessionFile);
@@ -312,20 +315,30 @@ function fastSessionInfo(file, source) {
312
315
  }
313
316
  function fastSessionEntries(opts = {}) {
314
317
  const out = [];
315
- for (const filePath of userCreatedCodexFiles(opts.codexRoot)) {
316
- const stat = statSafe(filePath);
317
- const info = fastSessionInfo(filePath, "codex");
318
- // Include if we found text OR a real session id (big sessions can have their first text turn beyond
319
- // the 1MB probe window — gating only on text dropped them entirely; exact discovery refines later).
320
- // We require a real key so the fast/exact conversationKey match (no sha16 fallback mismatch).
321
- if (info.hasTextTurn || info.hasRealKey)
322
- out.push({ filePath, source: "codex", conversationKey: info.conversationKey, size: stat.size, mtimeMs: stat.mtimeMs });
318
+ for (const file of userCreatedCodexSessionFiles(opts.codexRoot)) {
319
+ // Codex discovery has already classified the rollout as user-created and resolved its stable
320
+ // session key. Do not reread another 1MB of transcript just to rediscover the same ID.
321
+ const stableId = file.sessionKey.startsWith("session:")
322
+ ? file.sessionKey.slice("session:".length)
323
+ : null;
324
+ const fallback = stableId ? null : fastSessionInfo(file.path, "codex");
325
+ out.push({
326
+ filePath: file.path,
327
+ source: "codex",
328
+ conversationKey: stableId ? `codex:${stableId}` : fallback?.conversationKey || `codex:${sha16(file.path)}`,
329
+ size: file.size,
330
+ mtimeMs: Math.round(file.mtimeMs),
331
+ });
323
332
  }
324
333
  const claudeRoot = opts.claudeRoot ?? resolveClaudeProjectsDir();
325
334
  if (claudeRoot) {
326
335
  for (const filePath of userCreatedClaudeFiles(claudeRoot)) {
327
336
  const stat = statSafe(filePath);
328
- const info = fastSessionInfo(filePath, "claude-code");
337
+ const filenameId = path.basename(filePath, path.extname(filePath));
338
+ const filenameHasStableId = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(filenameId);
339
+ const info = filenameHasStableId
340
+ ? { conversationKey: `claude-code:${filenameId.toLowerCase()}`, hasTextTurn: true, hasRealKey: true }
341
+ : fastSessionInfo(filePath, "claude-code");
329
342
  // Same rule as codex: include on text OR a real session id so large sessions aren't undercounted,
330
343
  // while keeping the key stable (claude-code sessionId appears on every line, so hasRealKey is reliable).
331
344
  if (info.hasTextTurn || info.hasRealKey)
@@ -23,6 +23,7 @@ export const MCP_PACKAGE_DESCRIPTION = stringOrFallback(packageJson.description,
23
23
  export const MCP_PACKAGE_LABEL = `${MCP_PACKAGE_NAME}@${MCP_PACKAGE_VERSION}`;
24
24
  export const MCP_UPDATE_COMMAND = `npx -y ${MCP_PACKAGE_NAME}@latest update`;
25
25
  export const MCP_UPDATE_ALL_COMMAND = `${MCP_UPDATE_COMMAND} --all`;
26
+ 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. Use each memory key as the Markdown label and 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.';
26
27
  export const MCP_SERVER_INSTRUCTIONS = [
27
28
  `${MCP_PACKAGE_DESCRIPTION} (${MCP_PACKAGE_LABEL}).`,
28
29
  `If the user or local config expects a newer EchoMem MCP version than ${MCP_PACKAGE_VERSION}, update once with \`${MCP_UPDATE_ALL_COMMAND}\` and start a new MCP session.`,
@@ -34,6 +35,7 @@ export const MCP_SERVER_INSTRUCTIONS = [
34
35
  "After joining or when profile fields are missing, use candidate memory evidence to propose a title and responsibility summary. Ask the user to confirm that proposal together with the publication preview, then call update_group_profile and complete_group_publication.",
35
36
  "Use one canonical https://echoknows.com/memory/<memory-id> link for private, group, and friend evidence. Label it with the memory key; the site resolves the authorized representation.",
36
37
  "Each search result is one memory: preserve its Memory ID and canonical echoknows.com link when citing it.",
38
+ MEMORY_CITATION_INSTRUCTION,
37
39
  "During a publication preview, if an unflagged candidate appears sensitive, proactively ask whether the user wants to mark its exact ID for publication attention first. Explain that marking does not publish or change encryption; it means the agent will call it out and ask for detailed confirmation whenever a later publication includes it. Never auto-flag inferred sensitivity. For sensitive-topic flags, search and preview exact owned memories before confirmed flag_memories_for_publication_attention. Separate already-flagged candidates, state that nothing has been published yet, and offer to exclude them, review them separately, or first search for and mark similar sensitive owned memories.",
38
40
  "Never save an inferred group profile. Manual prepared publication requires explicit preview confirmation; flagged memories still require separate exact-memory confirmation. Never store or log an echo_grp_ invite code.",
39
41
  ].join(" ");
@@ -48,8 +48,18 @@ export const SETUP_PAGE_CLIENT_CORE = String.raw ` var params = new URLSear
48
48
  var localAuthTermsAccepted = false;
49
49
  var localAuthAgeConfirmed = false;
50
50
  var accountSwitchPending = false;
51
+ // Invalidates async work started for a previously connected account.
52
+ var accountStateEpoch = 0;
51
53
  var statsPollStarted = false;
52
54
  var reportPollStarted = false;
55
+ var reportEtaDeadline = 0;
56
+ var reportEtaDisplaySeconds = 0;
57
+ var reportEtaUpdatedAt = 0;
58
+ var reportEtaStage = "";
59
+ var reportEtaStageDone = 0;
60
+ var reportEtaStageElapsed = 0;
61
+ var reportEtaObservedRate = 0;
62
+ var reportEtaScanId = "";
53
63
  var readyStage = "";
54
64
  var NIGHT_HOURS = { 22: 1, 23: 1, 0: 1, 1: 1, 2: 1, 3: 1 };
55
65
 
@@ -292,14 +302,13 @@ export const SETUP_PAGE_CLIENT_CORE = String.raw ` var params = new URLSear
292
302
  setExtractMode(false);
293
303
  setReadyMode(true);
294
304
  setHead("EchoMem connected", "Done");
295
- app.className = "notice";
305
+ app.className = "localAuthCompleteStage";
296
306
  app.innerHTML =
297
- '<section class="localAuthCard localAuthComplete">' +
298
- '<div class="localAuthLead">' +
299
- '<img src="/hud-assets/echo-face-cutout.png" alt="" />' +
300
- '<h2 class="siteHeadline">You\'re signed in.</h2>' +
301
- '<p>Return to Terminal, or run <code>echomem-mcp init</code> to start onboarding.</p>' +
302
- '</div>' +
307
+ '<section class="localAuthComplete" aria-labelledby="localAuthCompleteTitle">' +
308
+ '<img src="/hud-assets/echo-face-cutout.png" alt="" />' +
309
+ '<h2 id="localAuthCompleteTitle">You\'re signed in.</h2>' +
310
+ '<p>Return to Terminal.</p>' +
311
+ '<p class="localAuthCompleteNext">Run <code>echomem-mcp init</code> when you\'re ready to start onboarding.</p>' +
303
312
  '</section>';
304
313
  }
305
314
  async function finishLocalLogin() {
@@ -453,8 +462,8 @@ export const SETUP_PAGE_CLIENT_CORE = String.raw ` var params = new URLSear
453
462
  (setupMode ? '<label class="localAuthField"><span>Confirm passphrase</span><input id="localAuthPassphraseConfirm" type="password" autocomplete="new-password" placeholder="Confirm passphrase" /></label>' : '') +
454
463
  '<button class="primary localAuthPassphraseSubmit" id="localAuthUnlock" type="submit">' + (setupMode ? "Create vault" : "Unlock") + '</button>' +
455
464
  (setupMode
456
- ? '<p class="localAuthVaultDeferNote">Import won’t start until you create your vault.</p>' +
457
- '<button class="localAuthTextAction" id="localAuthSkipVault" type="button">Skip for now</button>'
465
+ ? '<p class="localAuthVaultDeferNote">You can turn on encryption later.</p>' +
466
+ '<button class="localAuthTextAction" id="localAuthSkipVault" type="button">Skip encryption for now</button>'
458
467
  : '') +
459
468
  '<button class="localAuthTextAction localAuthRestartAction" id="localAuthRestart" type="button">Use a different email</button>' +
460
469
  '<p class="localAuthStatus' + (message ? " is-error" : "") + '" id="localAuthStatus" role="status" aria-live="polite">' + esc(message || "") + '</p>' +
@@ -471,23 +480,14 @@ export const SETUP_PAGE_CLIENT_CORE = String.raw ` var params = new URLSear
471
480
  }
472
481
  async function skipLocalVaultSetup() {
473
482
  var button = document.getElementById("localAuthSkipVault");
474
- if (button) { button.disabled = true; button.textContent = "Leaving setup…"; }
475
- setLocalAuthStatus("Leaving setup. Your coding history will stay on this Mac.");
483
+ if (button) { button.disabled = true; button.textContent = "Continuing…"; }
484
+ setLocalAuthStatus("Continuing without encryption…");
476
485
  try {
477
- await postJson("/skip", {});
478
- setHead("Setup paused", "Vault deferred");
479
- app.className = "localAuthStage localAuthWelcomeStage localAuthPassphraseStage";
480
- app.innerHTML =
481
- '<section class="localAuthWelcome localAuthDeferred" aria-labelledby="localAuthDeferredTitle">' +
482
- '<div class="localAuthWelcomeCopy">' +
483
- '<h2 id="localAuthDeferredTitle">Setup paused.</h2>' +
484
- '<p>Your coding history is unchanged. Run <code>echomem-mcp init</code> when you’re ready to create your vault and continue.</p>' +
485
- '</div>' +
486
- '</section>';
487
- try { window.close(); } catch (_) {}
486
+ await postJson("/local-auth/skip-encryption", {});
487
+ await finishLocalLogin();
488
488
  } catch (error) {
489
- if (button) { button.disabled = false; button.textContent = "Skip for now"; }
490
- setLocalAuthStatus(error && error.message ? error.message : "Could not leave setup safely.", "error");
489
+ if (button) { button.disabled = false; button.textContent = "Skip encryption for now"; }
490
+ setLocalAuthStatus(error && error.message ? error.message : "Could not continue without encryption.", "error");
491
491
  }
492
492
  }
493
493
  async function submitLocalPassphrase(setupMode) {