@echomem/mcp 1.4.27 → 1.4.28

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/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)
@@ -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
 
@@ -453,8 +463,8 @@ export const SETUP_PAGE_CLIENT_CORE = String.raw ` var params = new URLSear
453
463
  (setupMode ? '<label class="localAuthField"><span>Confirm passphrase</span><input id="localAuthPassphraseConfirm" type="password" autocomplete="new-password" placeholder="Confirm passphrase" /></label>' : '') +
454
464
  '<button class="primary localAuthPassphraseSubmit" id="localAuthUnlock" type="submit">' + (setupMode ? "Create vault" : "Unlock") + '</button>' +
455
465
  (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>'
466
+ ? '<p class="localAuthVaultDeferNote">You can turn on encryption later.</p>' +
467
+ '<button class="localAuthTextAction" id="localAuthSkipVault" type="button">Skip encryption for now</button>'
458
468
  : '') +
459
469
  '<button class="localAuthTextAction localAuthRestartAction" id="localAuthRestart" type="button">Use a different email</button>' +
460
470
  '<p class="localAuthStatus' + (message ? " is-error" : "") + '" id="localAuthStatus" role="status" aria-live="polite">' + esc(message || "") + '</p>' +
@@ -471,23 +481,14 @@ export const SETUP_PAGE_CLIENT_CORE = String.raw ` var params = new URLSear
471
481
  }
472
482
  async function skipLocalVaultSetup() {
473
483
  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.");
484
+ if (button) { button.disabled = true; button.textContent = "Continuing…"; }
485
+ setLocalAuthStatus("Continuing without encryption…");
476
486
  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 (_) {}
487
+ await postJson("/local-auth/skip-encryption", {});
488
+ await finishLocalLogin();
488
489
  } 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");
490
+ if (button) { button.disabled = false; button.textContent = "Skip encryption for now"; }
491
+ setLocalAuthStatus(error && error.message ? error.message : "Could not continue without encryption.", "error");
491
492
  }
492
493
  }
493
494
  async function submitLocalPassphrase(setupMode) {