@echomem/mcp 1.4.21 → 1.4.22

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 CHANGED
@@ -66,7 +66,7 @@ HUD; the granular commands below still work.
66
66
  | `npx -y @echomem/mcp@latest setup` | One-off setup without keeping a global CLI command |
67
67
  | `echomem-mcp setup [--client cursor\|windsurf\|claude-desktop\|claude-code\|codex]` | Write client config + log in |
68
68
  | `echomem-mcp setup --skip-login [--client cursor\|windsurf\|claude-desktop\|claude-code\|codex]` | Write client config without opening the browser or changing credentials |
69
- | `npx -y @echomem/mcp@latest update --all` | One-shot update: repoint detected client configs to the latest bridge, with no browser login |
69
+ | `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
70
  | `npx -y @echomem/mcp@latest update --client codex` | Update one client only |
71
71
  | `echomem-mcp setup --with-hud [--client codex]` | Write client config + log in + launch the EchoMem context HUD |
72
72
  | `echomem-mcp login` | Approve device in browser (or use `--token` / `--passphrase`) |
@@ -8,6 +8,9 @@ export const SETUP_PAGE_CLIENT_CORE = String.raw ` var params = new URLSear
8
8
  var status = document.getElementById("status");
9
9
  var report = null;
10
10
  var reportEnvelope = null;
11
+ // The local bridge is authoritative. Login never gets access to scan/import routes;
12
+ // onboarding may request the separate local-history permission.
13
+ var setupFlow = "onboarding";
11
14
  var activeScanId = "";
12
15
  var stats = null;
13
16
  var statsSlow = false;
@@ -27,6 +30,8 @@ export const SETUP_PAGE_CLIENT_CORE = String.raw ` var params = new URLSear
27
30
  var billingStatus = null;
28
31
  var billingStatusLoading = false;
29
32
  var setupPlanChoice = "";
33
+ var setupPlanPreview = "free";
34
+ var billingActivationPendingPlan = "";
30
35
  var selectedSessionKeys = Object.create(null);
31
36
  var sessionSelectionTouched = false;
32
37
  var sessionPickerQuery = "";
@@ -37,13 +42,30 @@ export const SETUP_PAGE_CLIENT_CORE = String.raw ` var params = new URLSear
37
42
  var localAuthAgeConfirmed = false;
38
43
  var statsPollStarted = false;
39
44
  var reportPollStarted = false;
45
+ var reportEstimateDeadline = 0;
46
+ var reportEstimateTimer = null;
47
+ var readyStage = "";
40
48
  var NIGHT_HOURS = { 22: 1, 23: 1, 0: 1, 1: 1, 2: 1, 3: 1 };
41
49
 
50
+ try {
51
+ var storedSetupPlanChoice = sessionStorage.getItem("echomem:setup-plan:" + nonce) || "";
52
+ if (storedSetupPlanChoice === "free" || storedSetupPlanChoice === "pro" || storedSetupPlanChoice === "power") {
53
+ setupPlanChoice = storedSetupPlanChoice;
54
+ }
55
+ } catch (_) {}
56
+
42
57
  function esc(value) {
43
58
  return String(value == null ? "" : value).replace(/[&<>"']/g, function (ch) {
44
59
  return { "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&#39;" }[ch];
45
60
  });
46
61
  }
62
+ function rememberSetupPlanChoice(choice) {
63
+ setupPlanChoice = choice === "free" || choice === "pro" || choice === "power" ? choice : "";
64
+ try {
65
+ if (setupPlanChoice) sessionStorage.setItem("echomem:setup-plan:" + nonce, setupPlanChoice);
66
+ else sessionStorage.removeItem("echomem:setup-plan:" + nonce);
67
+ } catch (_) {}
68
+ }
47
69
  function setupIcon(name) {
48
70
  var paths = {
49
71
  "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"/>',
@@ -152,14 +174,18 @@ export const SETUP_PAGE_CLIENT_CORE = String.raw ` var params = new URLSear
152
174
  return false;
153
175
  }
154
176
  function localAuthLegalHtml() {
155
- return '<div class="localAuthLegal">' +
156
- '<label><input type="checkbox" id="localAuthAge" ' + (localAuthAgeConfirmed ? "checked" : "") + ' />' +
157
- '<span>I confirm that I am at least 18 years old.</span></label>' +
158
- '<label><input type="checkbox" id="localAuthTerms" ' + (localAuthTermsAccepted ? "checked" : "") + ' />' +
159
- '<span>I have read and agree to the Echo <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>. I understand Echo uses subprocessors listed <a href="https://echoknows.com/subprocessor-list" target="_blank" rel="noopener noreferrer">here</a>.</span></label>' +
160
- '</div>';
177
+ return '<label class="localAuthConsent">' +
178
+ '<input type="checkbox" id="localAuthConsent" ' + (localAuthAgeConfirmed && localAuthTermsAccepted ? "checked" : "") + ' />' +
179
+ '<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>' +
180
+ '</label>';
161
181
  }
162
182
  function captureLocalAuthConsents() {
183
+ var consent = document.getElementById("localAuthConsent");
184
+ if (consent) {
185
+ localAuthTermsAccepted = !!consent.checked;
186
+ localAuthAgeConfirmed = !!consent.checked;
187
+ return;
188
+ }
163
189
  var terms = document.getElementById("localAuthTerms");
164
190
  var age = document.getElementById("localAuthAge");
165
191
  localAuthTermsAccepted = !!(terms && terms.checked);
@@ -185,19 +211,21 @@ export const SETUP_PAGE_CLIENT_CORE = String.raw ` var params = new URLSear
185
211
  setExtractMode(false);
186
212
  setReadyMode(true);
187
213
  setHead("Connect EchoMem", "Local login");
188
- app.className = "localAuthStage";
214
+ app.className = "localAuthStage localAuthWelcomeStage";
189
215
  app.innerHTML =
190
- '<section class="localAuthCard">' +
191
- '<div class="localAuthLead">' +
192
- '<img src="/hud-assets/echo-face-cutout.png" alt="" />' +
193
- '<p class="consentEyebrow">EchoMem account / local login</p>' +
194
- '<h2 class="siteHeadline">Sign in without leaving this device.</h2>' +
195
- '<p>Echo sends a one-time code to your email. The browser stays on localhost; your device token and encryption key are stored only in the local EchoMem keystore.</p>' +
216
+ '<section class="localAuthWelcome" aria-labelledby="localAuthWelcomeTitle">' +
217
+ '<div class="localAuthOrbScene" aria-hidden="true"><span class="localAuthOrb"></span><span class="localAuthOrbShadow"></span></div>' +
218
+ '<div class="localAuthWelcomeCopy">' +
219
+ '<h2 id="localAuthWelcomeTitle">Welcome to Echo</h2>' +
220
+ '<p>Your memories, sovereign and shared.</p>' +
196
221
  '</div>' +
197
- '<form class="localAuthForm" id="localAuthEmailForm">' +
198
- '<label class="localAuthField"><span>Email</span><input id="localAuthEmail" type="email" autocomplete="email" placeholder="you@example.com" value="' + esc(localAuthEmail) + '" /></label>' +
222
+ '<form class="localAuthWelcomeForm" id="localAuthEmailForm">' +
223
+ '<div class="localAuthEmailCapsule">' +
224
+ '<label class="srOnly" for="localAuthEmail">Email</label>' +
225
+ '<input id="localAuthEmail" type="email" autocomplete="email" placeholder="Enter your email" value="' + esc(localAuthEmail) + '" />' +
226
+ '<button id="localAuthSend" type="submit" aria-label="Send one-time code"><span aria-hidden="true">→</span></button>' +
227
+ '</div>' +
199
228
  localAuthLegalHtml() +
200
- '<button class="primary" id="localAuthSend" type="submit">Send code</button>' +
201
229
  '<p class="localAuthStatus' + (message ? " is-error" : "") + '" id="localAuthStatus" role="status" aria-live="polite">' + esc(message || "") + '</p>' +
202
230
  '</form>' +
203
231
  '</section>';
@@ -208,12 +236,37 @@ export const SETUP_PAGE_CLIENT_CORE = String.raw ` var params = new URLSear
208
236
  email.oninput = function () { localAuthEmail = String(email.value || ""); updateLocalAuthSendButton(); };
209
237
  email.focus();
210
238
  }
211
- var terms = document.getElementById("localAuthTerms");
212
- var age = document.getElementById("localAuthAge");
213
- if (terms) terms.onchange = updateLocalAuthSendButton;
214
- if (age) age.onchange = updateLocalAuthSendButton;
239
+ var consent = document.getElementById("localAuthConsent");
240
+ if (consent) consent.onchange = updateLocalAuthSendButton;
215
241
  updateLocalAuthSendButton();
216
242
  }
243
+ function renderLocalLoginComplete() {
244
+ setCityMode(false);
245
+ setScanMode(false);
246
+ setExtractMode(false);
247
+ setReadyMode(true);
248
+ setHead("EchoMem connected", "Done");
249
+ app.className = "notice";
250
+ app.innerHTML =
251
+ '<section class="localAuthCard localAuthComplete">' +
252
+ '<div class="localAuthLead">' +
253
+ '<img src="/hud-assets/echo-face-cutout.png" alt="" />' +
254
+ '<p class="consentEyebrow">This device is connected</p>' +
255
+ '<h2 class="siteHeadline">You\'re signed in.</h2>' +
256
+ '<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>' +
257
+ '</div>' +
258
+ '</section>';
259
+ }
260
+ async function finishLocalLogin() {
261
+ connected = true;
262
+ if (setupFlow === "login") {
263
+ renderLocalLoginComplete();
264
+ return;
265
+ }
266
+ rememberSetupPlanChoice("");
267
+ try { await refreshBillingStatus(); } catch (_) {}
268
+ await waitForStats();
269
+ }
217
270
  async function sendLocalOtp() {
218
271
  var email = document.getElementById("localAuthEmail");
219
272
  localAuthEmail = email && email.value ? String(email.value).trim().toLowerCase() : "";
@@ -224,7 +277,7 @@ export const SETUP_PAGE_CLIENT_CORE = String.raw ` var params = new URLSear
224
277
  return;
225
278
  }
226
279
  var button = document.getElementById("localAuthSend");
227
- if (button) { button.disabled = true; button.textContent = "Sending..."; }
280
+ if (button) { button.disabled = true; button.innerHTML = '<span class="localAuthSpinner" aria-hidden="true"></span>'; }
228
281
  setLocalAuthStatus("Sending verification code...");
229
282
  try {
230
283
  var data = await postJson("/local-auth/send-otp", {
@@ -235,7 +288,7 @@ export const SETUP_PAGE_CLIENT_CORE = String.raw ` var params = new URLSear
235
288
  localAuthEmail = data.email || localAuthEmail;
236
289
  renderLocalOtp("Code sent. Check your inbox.");
237
290
  } catch (error) {
238
- if (button) { button.disabled = false; button.textContent = "Send code"; }
291
+ if (button) { button.disabled = false; button.innerHTML = '<span aria-hidden="true">→</span>'; }
239
292
  setLocalAuthStatus(error && error.message ? error.message : "Could not send the code.", "error");
240
293
  }
241
294
  }
@@ -245,18 +298,26 @@ export const SETUP_PAGE_CLIENT_CORE = String.raw ` var params = new URLSear
245
298
  setExtractMode(false);
246
299
  setReadyMode(true);
247
300
  setHead("Check your email", "Local login");
248
- app.className = "localAuthStage";
301
+ app.className = "localAuthStage localAuthWelcomeStage localAuthOtpStage";
249
302
  app.innerHTML =
250
- '<section class="localAuthCard localAuthCardOtp">' +
251
- '<div class="localAuthLead">' +
252
- '<img src="/hud-assets/echo-face-cutout.png" alt="" />' +
253
- '<p class="consentEyebrow">Verification code</p>' +
254
- '<h2 class="siteHeadline">Enter the code sent to <strong>' + esc(localAuthEmail) + '</strong>.</h2>' +
255
- '<p>This code signs in this local EchoMem bridge only. The next step asks for your encryption passphrase.</p>' +
303
+ '<section class="localAuthWelcome localAuthOtpWelcome" aria-labelledby="localAuthOtpTitle">' +
304
+ '<div class="localAuthOrbScene" aria-hidden="true"><span class="localAuthOrb"></span><span class="localAuthOrbShadow"></span></div>' +
305
+ '<div class="localAuthWelcomeCopy">' +
306
+ '<h2 id="localAuthOtpTitle">Check your inbox</h2>' +
307
+ '<p>We sent a 6-digit code to <strong>' + esc(localAuthEmail) + '</strong></p>' +
256
308
  '</div>' +
257
- '<form class="localAuthForm" id="localAuthOtpForm">' +
258
- '<label class="localAuthField"><span>6-digit code</span><input id="localAuthOtp" type="text" inputmode="numeric" autocomplete="one-time-code" maxlength="6" placeholder="123456" /></label>' +
259
- '<div class="localAuthActions"><button class="primary" id="localAuthVerify" type="submit">Verify code</button><button class="textButton" id="localAuthBack" type="button">Use a different email</button></div>' +
309
+ '<form class="localAuthWelcomeForm localAuthOtpForm" id="localAuthOtpForm">' +
310
+ '<label class="srOnly" for="localAuthOtp0">6-digit code</label>' +
311
+ '<div class="localAuthOtpCells" aria-label="6-digit verification code">' +
312
+ '<input id="localAuthOtp0" data-otp-cell type="text" inputmode="numeric" autocomplete="one-time-code" maxlength="1" />' +
313
+ '<input data-otp-cell type="text" inputmode="numeric" maxlength="1" />' +
314
+ '<input data-otp-cell type="text" inputmode="numeric" maxlength="1" />' +
315
+ '<input data-otp-cell type="text" inputmode="numeric" maxlength="1" />' +
316
+ '<input data-otp-cell type="text" inputmode="numeric" maxlength="1" />' +
317
+ '<input data-otp-cell type="text" inputmode="numeric" maxlength="1" />' +
318
+ '</div>' +
319
+ '<button class="primary localAuthVerifyWide" id="localAuthVerify" type="submit">Verify</button>' +
320
+ '<button class="secondary localAuthBackWide" id="localAuthBack" type="button">Use another email</button>' +
260
321
  '<p class="localAuthStatus" id="localAuthStatus" role="status" aria-live="polite">' + esc(message || "") + '</p>' +
261
322
  '</form>' +
262
323
  '</section>';
@@ -264,12 +325,45 @@ export const SETUP_PAGE_CLIENT_CORE = String.raw ` var params = new URLSear
264
325
  if (form) form.onsubmit = function (event) { event.preventDefault(); void verifyLocalOtp(); };
265
326
  var back = document.getElementById("localAuthBack");
266
327
  if (back) back.onclick = function () { renderLocalLogin(""); };
267
- var otp = document.getElementById("localAuthOtp");
328
+ bindLocalOtpCells();
329
+ var otp = document.querySelector("[data-otp-cell]");
268
330
  if (otp) otp.focus();
269
331
  }
332
+ function localOtpCellValues() {
333
+ return Array.prototype.map.call(document.querySelectorAll("[data-otp-cell]"), function (node) {
334
+ return String(node.value || "").replace(/\\D/g, "").slice(0, 1);
335
+ }).join("");
336
+ }
337
+ function fillLocalOtpCells(value) {
338
+ var digits = String(value || "").replace(/\\D/g, "").slice(0, 6);
339
+ var cells = Array.prototype.slice.call(document.querySelectorAll("[data-otp-cell]"));
340
+ cells.forEach(function (cell, index) { cell.value = digits.charAt(index) || ""; });
341
+ var focusIndex = Math.min(digits.length, cells.length - 1);
342
+ if (cells[focusIndex]) cells[focusIndex].focus();
343
+ }
344
+ function bindLocalOtpCells() {
345
+ var cells = Array.prototype.slice.call(document.querySelectorAll("[data-otp-cell]"));
346
+ cells.forEach(function (cell, index) {
347
+ cell.oninput = function () {
348
+ var value = String(cell.value || "").replace(/\\D/g, "");
349
+ if (value.length > 1) { fillLocalOtpCells(value); return; }
350
+ cell.value = value;
351
+ if (value && cells[index + 1]) cells[index + 1].focus();
352
+ };
353
+ cell.onkeydown = function (event) {
354
+ if (event.key === "Backspace" && !cell.value && cells[index - 1]) cells[index - 1].focus();
355
+ };
356
+ cell.onpaste = function (event) {
357
+ var text = event.clipboardData && event.clipboardData.getData ? event.clipboardData.getData("text") : "";
358
+ if (text) {
359
+ event.preventDefault();
360
+ fillLocalOtpCells(text);
361
+ }
362
+ };
363
+ });
364
+ }
270
365
  async function verifyLocalOtp() {
271
- var otpInput = document.getElementById("localAuthOtp");
272
- var otp = otpInput && otpInput.value ? String(otpInput.value).replace(/\\D/g, "") : "";
366
+ var otp = localOtpCellValues();
273
367
  if (otp.length !== 6) { setLocalAuthStatus("Enter the 6-digit code.", "error"); return; }
274
368
  var button = document.getElementById("localAuthVerify");
275
369
  if (button) { button.disabled = true; button.textContent = "Verifying..."; }
@@ -285,8 +379,7 @@ export const SETUP_PAGE_CLIENT_CORE = String.raw ` var params = new URLSear
285
379
  renderLocalPassphrase(data.mode || "unlock", data.email || localAuthEmail);
286
380
  return;
287
381
  }
288
- connected = true;
289
- await waitForStats();
382
+ await finishLocalLogin();
290
383
  } catch (error) {
291
384
  if (button) { button.disabled = false; button.textContent = "Verify code"; }
292
385
  setLocalAuthStatus(error && error.message ? error.message : "Could not verify the code.", "error");
@@ -300,19 +393,19 @@ export const SETUP_PAGE_CLIENT_CORE = String.raw ` var params = new URLSear
300
393
  setExtractMode(false);
301
394
  setReadyMode(true);
302
395
  setHead(setupMode ? "Create encrypted vault" : "Unlock encrypted vault", "Local passphrase");
303
- app.className = "localAuthStage";
396
+ app.className = "localAuthStage localAuthWelcomeStage localAuthPassphraseStage";
304
397
  app.innerHTML =
305
- '<section class="localAuthCard localAuthCardPassphrase">' +
306
- '<div class="localAuthLead">' +
307
- '<img src="/hud-assets/echo-face-cutout.png" alt="" />' +
308
- '<p class="consentEyebrow">Encryption passphrase</p>' +
309
- '<h2 class="siteHeadline">' + (setupMode ? 'Create your local vault passphrase.' : 'Enter your vault passphrase.') + '</h2>' +
398
+ '<section class="localAuthWelcome localAuthPassphraseWelcome">' +
399
+ '<div class="localAuthOrbScene" aria-hidden="true"><span class="localAuthOrb"></span><span class="localAuthOrbShadow"></span></div>' +
400
+ '<div class="localAuthWelcomeCopy">' +
401
+ '<h2>' + (setupMode ? 'Create your local vault passphrase.' : 'Enter your vault passphrase.') + '</h2>' +
310
402
  '<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>' +
311
403
  '</div>' +
312
- '<form class="localAuthForm" id="localAuthPassphraseForm">' +
404
+ '<form class="localAuthWelcomeForm localAuthPassphraseForm" id="localAuthPassphraseForm">' +
313
405
  '<label class="localAuthField"><span>Passphrase</span><input id="localAuthPassphrase" type="password" autocomplete="' + (setupMode ? "new-password" : "current-password") + '" placeholder="Encryption passphrase" /></label>' +
314
406
  (setupMode ? '<label class="localAuthField"><span>Confirm passphrase</span><input id="localAuthPassphraseConfirm" type="password" autocomplete="new-password" placeholder="Confirm passphrase" /></label>' : '') +
315
- '<div class="localAuthActions"><button class="primary" id="localAuthUnlock" type="submit">' + (setupMode ? "Create vault" : "Unlock") + '</button><button class="textButton" id="localAuthRestart" type="button">Use a different email</button></div>' +
407
+ '<button class="primary localAuthPassphraseSubmit" id="localAuthUnlock" type="submit">' + (setupMode ? "Create vault" : "Unlock") + '</button>' +
408
+ '<button class="secondary localAuthBackWide" id="localAuthRestart" type="button">Use a different email</button>' +
316
409
  '<p class="localAuthStatus' + (message ? " is-error" : "") + '" id="localAuthStatus" role="status" aria-live="polite">' + esc(message || "") + '</p>' +
317
410
  '</form>' +
318
411
  '</section>';
@@ -335,8 +428,7 @@ export const SETUP_PAGE_CLIENT_CORE = String.raw ` var params = new URLSear
335
428
  setLocalAuthStatus(setupMode ? "Creating encrypted vault..." : "Unlocking vault...");
336
429
  try {
337
430
  await postJson("/local-auth/passphrase", { passphrase: passphrase }, 30000);
338
- connected = true;
339
- await waitForStats();
431
+ await finishLocalLogin();
340
432
  } catch (error) {
341
433
  if (button) { button.disabled = false; button.textContent = setupMode ? "Create vault" : "Unlock"; }
342
434
  var msg = error && error.message ? error.message : "Could not unlock encrypted memory.";
@@ -350,6 +442,7 @@ export const SETUP_PAGE_CLIENT_CORE = String.raw ` var params = new URLSear
350
442
  return;
351
443
  }
352
444
  if (connected) { void waitForStats(); return; }
445
+ rememberSetupPlanChoice("");
353
446
  renderLocalLogin("");
354
447
  }
355
448
  function bindConnect() {