@echomem/mcp 1.4.26 → 1.4.27

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.
@@ -0,0 +1,111 @@
1
+ import fs from "node:fs";
2
+ const SAVE_TOOL_RE = /^(?:mcp__[\w.-]+__)?save_conversation$/i;
3
+ const MUTATION_TOOL_RE = /(?:^|[._])(?:apply_patch|Edit|Write)$/i;
4
+ const DURABLE_COMMAND_RE = /\b(?:git\s+commit|npm\s+(?:run\s+)?(?:test|build|lint)|pnpm\s+(?:test|build|lint)|yarn\s+(?:test|build|lint)|pytest|cargo\s+test|go\s+test|deploy|publish)\b/i;
5
+ const DURABLE_LANGUAGE_RE = /\b(?:implemented|fixed|resolved|shipped|deployed|published|committed|milestone|decision|architecture|root cause|tests? pass(?:ed|ing)?|build pass(?:ed|ing)?)\b|(?:实现|修复|解决|发布|提交|里程碑|决定|架构|根因|测试通过|构建通过)/i;
6
+ const CHECKPOINT_REASON = [
7
+ "Run EchoMem's private completion checkpoint before finishing this turn.",
8
+ "Review the work that just completed. If it produced a durable decision, confirmed preference, implementation, resolved bug, commit, passing verification, release, or project milestone, call `save_conversation` now with a concise structured summary of the outcome, reasoning, relevant files, and constraints.",
9
+ "Do not save secrets or incidental chatter. If the work was not durable, do not call the tool and finish normally.",
10
+ "Private persistence happens first. For a user with a company group, call `get_group_session_sharing` near session start or after this first qualifying private save.",
11
+ "If this exact session has no decision, ask once whether memories saved from the session should be shared with the current group, then call `set_group_session_sharing` only with the explicit Yes/No answer. A Yes decision automatically syncs eligible memories after later private saves; a No decision keeps later saves private.",
12
+ "Flagged memories are withheld from automatic session sync and remain private.",
13
+ "If EchoMem reports that the encrypted vault is locked, tell the user to run `echomem-mcp unlock`; never silently skip a qualifying checkpoint.",
14
+ ].join(" ");
15
+ function currentTurnSlice(transcript) {
16
+ const lines = transcript.split(/\r?\n/);
17
+ let start = 0;
18
+ for (let index = lines.length - 1; index >= 0; index -= 1) {
19
+ const line = lines[index];
20
+ if (/"type"\s*:\s*"user_message"/.test(line)
21
+ || /"type"\s*:\s*"user"/.test(line)
22
+ || /"role"\s*:\s*"user"/.test(line)) {
23
+ start = index;
24
+ break;
25
+ }
26
+ }
27
+ return lines.slice(start).join("\n");
28
+ }
29
+ function visibleTextSize(value) {
30
+ return value
31
+ .replace(/"[^"]+"\s*:/g, "")
32
+ .replace(/[{}\[\]",\\]/g, "")
33
+ .trim()
34
+ .length;
35
+ }
36
+ function toolNamesFromTranscript(transcript) {
37
+ const names = [];
38
+ const visit = (value) => {
39
+ if (Array.isArray(value)) {
40
+ value.forEach(visit);
41
+ return;
42
+ }
43
+ if (typeof value !== "object" || value === null)
44
+ return;
45
+ const record = value;
46
+ const type = typeof record.type === "string" ? record.type : "";
47
+ if ((type === "function_call" || type === "tool_use" || type === "mcp_tool_call")
48
+ && typeof record.name === "string") {
49
+ names.push(record.name);
50
+ }
51
+ Object.values(record).forEach(visit);
52
+ };
53
+ for (const line of transcript.split(/\r?\n/)) {
54
+ try {
55
+ visit(JSON.parse(line));
56
+ }
57
+ catch {
58
+ // Hook transcripts are JSONL. Ignore a partial final line while Codex is still flushing it.
59
+ }
60
+ }
61
+ return names;
62
+ }
63
+ export function evaluateSaveCheckpoint(payload, transcript) {
64
+ if (payload.stop_hook_active === true) {
65
+ return { decision: "allow", reason: "already_checked" };
66
+ }
67
+ if (!transcript) {
68
+ return { decision: "allow", reason: "missing_transcript" };
69
+ }
70
+ const turn = currentTurnSlice(transcript);
71
+ const toolNames = toolNamesFromTranscript(turn);
72
+ if (toolNames.some((name) => SAVE_TOOL_RE.test(name))) {
73
+ return { decision: "allow", reason: "already_saved" };
74
+ }
75
+ const assistant = typeof payload.last_assistant_message === "string" ? payload.last_assistant_message : "";
76
+ const durable = toolNames.some((name) => MUTATION_TOOL_RE.test(name))
77
+ || DURABLE_COMMAND_RE.test(turn)
78
+ || DURABLE_LANGUAGE_RE.test(assistant)
79
+ || DURABLE_LANGUAGE_RE.test(turn)
80
+ || (visibleTextSize(turn) >= 1_200 && assistant.trim().length >= 240);
81
+ if (!durable) {
82
+ return { decision: "allow", reason: "not_durable" };
83
+ }
84
+ return { decision: "block", reason: CHECKPOINT_REASON };
85
+ }
86
+ export function runSaveCheckpointHook(input) {
87
+ let payload = {};
88
+ try {
89
+ const parsed = JSON.parse(input);
90
+ if (typeof parsed === "object" && parsed !== null)
91
+ payload = parsed;
92
+ }
93
+ catch {
94
+ return JSON.stringify({ continue: true });
95
+ }
96
+ const transcriptPath = typeof payload.transcript_path === "string" ? payload.transcript_path : "";
97
+ let transcript = null;
98
+ if (transcriptPath) {
99
+ try {
100
+ transcript = fs.readFileSync(transcriptPath, "utf8");
101
+ }
102
+ catch {
103
+ transcript = null;
104
+ }
105
+ }
106
+ const result = evaluateSaveCheckpoint(payload, transcript);
107
+ if (result.decision === "block") {
108
+ return JSON.stringify({ decision: "block", reason: result.reason });
109
+ }
110
+ return JSON.stringify({ continue: true });
111
+ }
@@ -32,6 +32,13 @@ export const SETUP_PAGE_CLIENT_CORE = String.raw ` var params = new URLSear
32
32
  var setupPlanChoice = "";
33
33
  var setupPlanPreview = "free";
34
34
  var billingActivationPendingPlan = "";
35
+ var pendingCheckoutSessionId = "";
36
+ var billingSyncLastAttemptAt = 0;
37
+ var billingSyncAttempts = 0;
38
+ var billingLastCheckedAt = 0;
39
+ var billingCheckMessage = "";
40
+ var firstRecallPollTimer = null;
41
+ var firstRecallPollStartedAt = 0;
35
42
  var selectedSessionKeys = Object.create(null);
36
43
  var sessionSelectionTouched = false;
37
44
  var sessionPickerQuery = "";
@@ -40,10 +47,9 @@ export const SETUP_PAGE_CLIENT_CORE = String.raw ` var params = new URLSear
40
47
  var localAuthEmail = "";
41
48
  var localAuthTermsAccepted = false;
42
49
  var localAuthAgeConfirmed = false;
50
+ var accountSwitchPending = false;
43
51
  var statsPollStarted = false;
44
52
  var reportPollStarted = false;
45
- var reportEstimateDeadline = 0;
46
- var reportEstimateTimer = null;
47
53
  var readyStage = "";
48
54
  var NIGHT_HOURS = { 22: 1, 23: 1, 0: 1, 1: 1, 2: 1, 3: 1 };
49
55
 
@@ -52,6 +58,10 @@ export const SETUP_PAGE_CLIENT_CORE = String.raw ` var params = new URLSear
52
58
  if (storedSetupPlanChoice === "free" || storedSetupPlanChoice === "pro" || storedSetupPlanChoice === "power") {
53
59
  setupPlanChoice = storedSetupPlanChoice;
54
60
  }
61
+ var storedCheckoutSessionId = sessionStorage.getItem("echomem:checkout-session:" + nonce) || "";
62
+ if (/^cs_(?:test_|live_)?[A-Za-z0-9_]+$/.test(storedCheckoutSessionId)) {
63
+ pendingCheckoutSessionId = storedCheckoutSessionId;
64
+ }
55
65
  } catch (_) {}
56
66
 
57
67
  function esc(value) {
@@ -59,6 +69,11 @@ export const SETUP_PAGE_CLIENT_CORE = String.raw ` var params = new URLSear
59
69
  return { "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&#39;" }[ch];
60
70
  });
61
71
  }
72
+ function dataHandlingLink(label) {
73
+ return '<a class="dataHandlingLink" href="https://echoknows.com/privacy-policy" target="_blank" rel="noopener noreferrer">' +
74
+ esc(label || "How Echo handles your coding history") +
75
+ '</a>';
76
+ }
62
77
  function rememberSetupPlanChoice(choice) {
63
78
  setupPlanChoice = choice === "free" || choice === "pro" || choice === "power" ? choice : "";
64
79
  try {
@@ -66,6 +81,18 @@ export const SETUP_PAGE_CLIENT_CORE = String.raw ` var params = new URLSear
66
81
  else sessionStorage.removeItem("echomem:setup-plan:" + nonce);
67
82
  } catch (_) {}
68
83
  }
84
+ function rememberCheckoutSessionId(sessionId) {
85
+ pendingCheckoutSessionId = /^cs_(?:test_|live_)?[A-Za-z0-9_]+$/.test(String(sessionId || ""))
86
+ ? String(sessionId)
87
+ : "";
88
+ try {
89
+ if (pendingCheckoutSessionId) {
90
+ sessionStorage.setItem("echomem:checkout-session:" + nonce, pendingCheckoutSessionId);
91
+ } else {
92
+ sessionStorage.removeItem("echomem:checkout-session:" + nonce);
93
+ }
94
+ } catch (_) {}
95
+ }
69
96
  function setupIcon(name) {
70
97
  var paths = {
71
98
  "shield-check": '<path d="M12 3 5 6v5c0 4.6 2.9 8.1 7 10 4.1-1.9 7-5.4 7-10V6l-7-3Z"/><path d="m8.8 12.1 2 2 4.4-4.5"/>',
@@ -80,6 +107,18 @@ export const SETUP_PAGE_CLIENT_CORE = String.raw ` var params = new URLSear
80
107
  };
81
108
  return '<svg class="setupGlyph" viewBox="0 0 24 24" fill="none" aria-hidden="true" focusable="false"><g stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round">' + (paths[name] || "") + '</g></svg>';
82
109
  }
110
+ function setupJourney(currentStep) {
111
+ var labels = ["Scan", "Report", "Connect", "Import", "Try Echo"];
112
+ var items = labels.map(function (label, index) {
113
+ var step = index + 1;
114
+ var state = step < currentStep ? " is-complete" : (step === currentStep ? " is-current" : "");
115
+ return '<li class="setupJourneyStep' + state + '"' + (step === currentStep ? ' aria-current="step"' : "") + '>' +
116
+ '<span>' + (step < currentStep ? setupIcon("check") : esc(step)) + '</span>' +
117
+ '<b>' + esc(label) + '</b>' +
118
+ '</li>';
119
+ }).join("");
120
+ return '<nav class="setupJourney" aria-label="EchoMem setup progress"><ol>' + items + '</ol></nav>';
121
+ }
83
122
  function setHead(nextTitle, nextStatus) {
84
123
  title.textContent = nextTitle;
85
124
  status.textContent = nextStatus;
@@ -179,7 +218,7 @@ export const SETUP_PAGE_CLIENT_CORE = String.raw ` var params = new URLSear
179
218
  function localAuthLegalHtml() {
180
219
  return '<label class="localAuthConsent">' +
181
220
  '<input type="checkbox" id="localAuthConsent" ' + (localAuthAgeConfirmed && localAuthTermsAccepted ? "checked" : "") + ' />' +
182
- '<span>I confirm that I am at least 18 years old and that I have read and agree to Echo\'s <a href="https://echoknows.com/terms-of-use" target="_blank" rel="noopener noreferrer">Terms of Use</a>, <a href="https://echoknows.com/memory-terms" target="_blank" rel="noopener noreferrer">Memory Usage Terms</a>, and <a href="https://echoknows.com/privacy-policy" target="_blank" rel="noopener noreferrer">Privacy Policy</a>. See our <a href="https://echoknows.com/subprocessor-list" target="_blank" rel="noopener noreferrer">Subprocessor List</a>.</span>' +
221
+ '<span>I’m at least 18 and agree to Echo\'s <a href="https://echoknows.com/terms-of-use" target="_blank" rel="noopener noreferrer">Terms</a>, <a href="https://echoknows.com/memory-terms" target="_blank" rel="noopener noreferrer">Memory Terms</a>, and <a href="https://echoknows.com/privacy-policy" target="_blank" rel="noopener noreferrer">Privacy Policy</a>. <a href="https://echoknows.com/subprocessor-list" target="_blank" rel="noopener noreferrer">Subprocessors</a></span>' +
183
222
  '</label>';
184
223
  }
185
224
  function captureLocalAuthConsents() {
@@ -209,24 +248,28 @@ export const SETUP_PAGE_CLIENT_CORE = String.raw ` var params = new URLSear
209
248
  button.disabled = !value || !localAuthTermsAccepted || !localAuthAgeConfirmed;
210
249
  }
211
250
  function renderLocalLogin(message) {
251
+ var onboardingConnect = setupFlow !== "login";
212
252
  setCityMode(false);
213
253
  setScanMode(false);
214
254
  setExtractMode(false);
215
255
  setReadyMode(true);
216
256
  setHead("Connect EchoMem", "Local login");
217
- app.className = "localAuthStage localAuthWelcomeStage";
257
+ app.className = "localAuthStage localAuthWelcomeStage localAuthEmailStage";
218
258
  app.innerHTML =
259
+ setupJourney(3) +
219
260
  '<section class="localAuthWelcome" aria-labelledby="localAuthWelcomeTitle">' +
220
261
  '<div class="localAuthOrbScene" aria-hidden="true"><span class="localAuthOrb"></span><span class="localAuthOrbShadow"></span></div>' +
221
262
  '<div class="localAuthWelcomeCopy">' +
222
- '<h2 id="localAuthWelcomeTitle">Welcome to Echo</h2>' +
223
- '<p>Your memories, sovereign and shared.</p>' +
263
+ '<h2 id="localAuthWelcomeTitle">' + (onboardingConnect ? 'Take your context with you.' : 'Welcome to Echo') + '</h2>' +
264
+ (onboardingConnect
265
+ ? '<p class="localAuthConnectPurpose">Connect this Mac to Echo to use the memories you choose across your AI tools.</p>'
266
+ : '') +
224
267
  '</div>' +
225
268
  '<form class="localAuthWelcomeForm" id="localAuthEmailForm">' +
226
269
  '<div class="localAuthEmailCapsule">' +
227
270
  '<label class="srOnly" for="localAuthEmail">Email</label>' +
228
271
  '<input id="localAuthEmail" type="email" autocomplete="email" placeholder="Enter your email" value="' + esc(localAuthEmail) + '" />' +
229
- '<button id="localAuthSend" type="submit" aria-label="Send one-time code"><span aria-hidden="true">→</span></button>' +
272
+ '<button id="localAuthSend" type="submit">Email me a code</button>' +
230
273
  '</div>' +
231
274
  localAuthLegalHtml() +
232
275
  '<p class="localAuthStatus' + (message ? " is-error" : "") + '" id="localAuthStatus" role="status" aria-live="polite">' + esc(message || "") + '</p>' +
@@ -254,9 +297,8 @@ export const SETUP_PAGE_CLIENT_CORE = String.raw ` var params = new URLSear
254
297
  '<section class="localAuthCard localAuthComplete">' +
255
298
  '<div class="localAuthLead">' +
256
299
  '<img src="/hud-assets/echo-face-cutout.png" alt="" />' +
257
- '<p class="consentEyebrow">This device is connected</p>' +
258
300
  '<h2 class="siteHeadline">You\'re signed in.</h2>' +
259
- '<p>Your local device token and encryption key are ready. Return to Terminal, or run <code>echomem-mcp init</code> when you want to start local-history onboarding.</p>' +
301
+ '<p>Return to Terminal, or run <code>echomem-mcp init</code> to start onboarding.</p>' +
260
302
  '</div>' +
261
303
  '</section>';
262
304
  }
@@ -280,7 +322,7 @@ export const SETUP_PAGE_CLIENT_CORE = String.raw ` var params = new URLSear
280
322
  return;
281
323
  }
282
324
  var button = document.getElementById("localAuthSend");
283
- if (button) { button.disabled = true; button.innerHTML = '<span class="localAuthSpinner" aria-hidden="true"></span>'; }
325
+ if (button) { button.disabled = true; button.innerHTML = '<span class="localAuthSpinner" aria-hidden="true"></span><span>Sending…</span>'; }
284
326
  setLocalAuthStatus("Sending verification code...");
285
327
  try {
286
328
  var data = await postJson("/local-auth/send-otp", {
@@ -291,7 +333,7 @@ export const SETUP_PAGE_CLIENT_CORE = String.raw ` var params = new URLSear
291
333
  localAuthEmail = data.email || localAuthEmail;
292
334
  renderLocalOtp("Code sent. Check your inbox.");
293
335
  } catch (error) {
294
- if (button) { button.disabled = false; button.innerHTML = '<span aria-hidden="true">→</span>'; }
336
+ if (button) { button.disabled = false; button.textContent = "Email me a code"; }
295
337
  setLocalAuthStatus(error && error.message ? error.message : "Could not send the code.", "error");
296
338
  }
297
339
  }
@@ -303,6 +345,7 @@ export const SETUP_PAGE_CLIENT_CORE = String.raw ` var params = new URLSear
303
345
  setHead("Check your email", "Local login");
304
346
  app.className = "localAuthStage localAuthWelcomeStage localAuthOtpStage";
305
347
  app.innerHTML =
348
+ setupJourney(3) +
306
349
  '<section class="localAuthWelcome localAuthOtpWelcome" aria-labelledby="localAuthOtpTitle">' +
307
350
  '<div class="localAuthOrbScene" aria-hidden="true"><span class="localAuthOrb"></span><span class="localAuthOrbShadow"></span></div>' +
308
351
  '<div class="localAuthWelcomeCopy">' +
@@ -398,17 +441,22 @@ export const SETUP_PAGE_CLIENT_CORE = String.raw ` var params = new URLSear
398
441
  setHead(setupMode ? "Create encrypted vault" : "Unlock encrypted vault", "Local passphrase");
399
442
  app.className = "localAuthStage localAuthWelcomeStage localAuthPassphraseStage";
400
443
  app.innerHTML =
444
+ setupJourney(3) +
401
445
  '<section class="localAuthWelcome localAuthPassphraseWelcome">' +
402
446
  '<div class="localAuthOrbScene" aria-hidden="true"><span class="localAuthOrb"></span><span class="localAuthOrbShadow"></span></div>' +
403
447
  '<div class="localAuthWelcomeCopy">' +
404
- '<h2>' + (setupMode ? 'Create your local vault passphrase.' : 'Enter your vault passphrase.') + '</h2>' +
405
- '<p>' + (setupMode ? 'Echo derives your encryption key on this device. The passphrase and key are never sent to EchoMem. If you forget it, encrypted memories cannot be recovered.' : 'Echo verifies this passphrase locally against your account encryption token. The passphrase is never sent to EchoMem.') + '</p>' +
448
+ '<h2>' + (setupMode ? 'Protect your memories with a passphrase.' : 'Enter your vault passphrase.') + '</h2>' +
449
+ '<p>' + (setupMode ? 'It encrypts memories before upload and unlocks them on your trusted devices. Echo can’t recover it, so save it in your password manager.' : 'Verified only on this device. It is never sent to EchoMem.') + '</p>' +
406
450
  '</div>' +
407
451
  '<form class="localAuthWelcomeForm localAuthPassphraseForm" id="localAuthPassphraseForm">' +
408
452
  '<label class="localAuthField"><span>Passphrase</span><input id="localAuthPassphrase" type="password" autocomplete="' + (setupMode ? "new-password" : "current-password") + '" placeholder="Encryption passphrase" /></label>' +
409
453
  (setupMode ? '<label class="localAuthField"><span>Confirm passphrase</span><input id="localAuthPassphraseConfirm" type="password" autocomplete="new-password" placeholder="Confirm passphrase" /></label>' : '') +
410
454
  '<button class="primary localAuthPassphraseSubmit" id="localAuthUnlock" type="submit">' + (setupMode ? "Create vault" : "Unlock") + '</button>' +
411
- '<button class="secondary localAuthBackWide" id="localAuthRestart" type="button">Use a different email</button>' +
455
+ (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>'
458
+ : '') +
459
+ '<button class="localAuthTextAction localAuthRestartAction" id="localAuthRestart" type="button">Use a different email</button>' +
412
460
  '<p class="localAuthStatus' + (message ? " is-error" : "") + '" id="localAuthStatus" role="status" aria-live="polite">' + esc(message || "") + '</p>' +
413
461
  '</form>' +
414
462
  '</section>';
@@ -416,9 +464,32 @@ export const SETUP_PAGE_CLIENT_CORE = String.raw ` var params = new URLSear
416
464
  if (form) form.onsubmit = function (event) { event.preventDefault(); void submitLocalPassphrase(setupMode); };
417
465
  var restart = document.getElementById("localAuthRestart");
418
466
  if (restart) restart.onclick = function () { renderLocalLogin(""); };
467
+ var skipVault = document.getElementById("localAuthSkipVault");
468
+ if (skipVault) skipVault.onclick = function () { void skipLocalVaultSetup(); };
419
469
  var pass = document.getElementById("localAuthPassphrase");
420
470
  if (pass) pass.focus();
421
471
  }
472
+ async function skipLocalVaultSetup() {
473
+ 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.");
476
+ 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 (_) {}
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");
491
+ }
492
+ }
422
493
  async function submitLocalPassphrase(setupMode) {
423
494
  var pass = document.getElementById("localAuthPassphrase");
424
495
  var confirm = document.getElementById("localAuthPassphraseConfirm");
@@ -441,7 +512,7 @@ export const SETUP_PAGE_CLIENT_CORE = String.raw ` var params = new URLSear
441
512
  function onConnectClick() {
442
513
  if (!localHistoryConsentGranted) {
443
514
  renderLocalScanConsent();
444
- setConsentStatus("Local history access is required before you can connect EchoMem or continue setup.", "required");
515
+ setConsentStatus("Allow the local scan to continue.", "required");
445
516
  return;
446
517
  }
447
518
  if (connected) { void waitForStats(); return; }