@echomem/mcp 1.4.19 → 1.4.21

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.
@@ -23,8 +23,6 @@ export const SETUP_PAGE_CLIENT_CORE = String.raw ` var params = new URLSear
23
23
  var progressPollRunId = 0; // invalidates older progress loops when extraction resumes
24
24
  var dashMounted = false; // dashboard shell (incl. plate iframe) mounted once; persists across /stats polls
25
25
  var reportMounted = false; // report shell (city iframe) mounted once; loading is an overlay on it, not a separate page
26
- var authUrl = "";
27
- var switchAccountUrl = "";
28
26
  var workspacePath = "";
29
27
  var billingStatus = null;
30
28
  var billingStatusLoading = false;
@@ -34,8 +32,9 @@ export const SETUP_PAGE_CLIENT_CORE = String.raw ` var params = new URLSear
34
32
  var sessionPickerQuery = "";
35
33
  var sessionPickerSource = "all";
36
34
  var billingPollTimer = null;
37
- var authWindow = null;
38
- var connectionPollStarted = false;
35
+ var localAuthEmail = "";
36
+ var localAuthTermsAccepted = false;
37
+ var localAuthAgeConfirmed = false;
39
38
  var statsPollStarted = false;
40
39
  var reportPollStarted = false;
41
40
  var NIGHT_HOURS = { 22: 1, 23: 1, 0: 1, 1: 1, 2: 1, 3: 1 };
@@ -142,10 +141,6 @@ export const SETUP_PAGE_CLIENT_CORE = String.raw ` var params = new URLSear
142
141
  if (code === "NOT_LOGGED_IN") return "Echo on this machine is not signed in yet. Sign in again and retry.";
143
142
  return code || "Something went wrong. Retry in a moment.";
144
143
  }
145
- function closeAuthWindow() {
146
- try { if (authWindow && !authWindow.closed) authWindow.close(); } catch (_) {}
147
- authWindow = null;
148
- }
149
144
  function notifyOpenerConnected() {
150
145
  try {
151
146
  if (window.opener && window.opener !== window && !window.opener.closed) {
@@ -156,24 +151,197 @@ export const SETUP_PAGE_CLIENT_CORE = String.raw ` var params = new URLSear
156
151
  } catch (_) {}
157
152
  return false;
158
153
  }
159
- function openAuthWindow(url) {
160
- var targetUrl = url || authUrl;
161
- if (!targetUrl) return false;
154
+ 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>';
161
+ }
162
+ function captureLocalAuthConsents() {
163
+ var terms = document.getElementById("localAuthTerms");
164
+ var age = document.getElementById("localAuthAge");
165
+ localAuthTermsAccepted = !!(terms && terms.checked);
166
+ localAuthAgeConfirmed = !!(age && age.checked);
167
+ }
168
+ function setLocalAuthStatus(message, tone) {
169
+ var node = document.getElementById("localAuthStatus");
170
+ if (!node) return;
171
+ node.textContent = message || "";
172
+ node.classList.toggle("is-error", tone === "error");
173
+ }
174
+ function updateLocalAuthSendButton() {
175
+ captureLocalAuthConsents();
176
+ var email = document.getElementById("localAuthEmail");
177
+ var button = document.getElementById("localAuthSend");
178
+ if (!button) return;
179
+ var value = email && email.value ? String(email.value).trim() : "";
180
+ button.disabled = !value || !localAuthTermsAccepted || !localAuthAgeConfirmed;
181
+ }
182
+ function renderLocalLogin(message) {
183
+ setCityMode(false);
184
+ setScanMode(false);
185
+ setExtractMode(false);
186
+ setReadyMode(true);
187
+ setHead("Connect EchoMem", "Local login");
188
+ app.className = "localAuthStage";
189
+ 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>' +
196
+ '</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>' +
199
+ localAuthLegalHtml() +
200
+ '<button class="primary" id="localAuthSend" type="submit">Send code</button>' +
201
+ '<p class="localAuthStatus' + (message ? " is-error" : "") + '" id="localAuthStatus" role="status" aria-live="polite">' + esc(message || "") + '</p>' +
202
+ '</form>' +
203
+ '</section>';
204
+ var form = document.getElementById("localAuthEmailForm");
205
+ if (form) form.onsubmit = function (event) { event.preventDefault(); void sendLocalOtp(); };
206
+ var email = document.getElementById("localAuthEmail");
207
+ if (email) {
208
+ email.oninput = function () { localAuthEmail = String(email.value || ""); updateLocalAuthSendButton(); };
209
+ email.focus();
210
+ }
211
+ var terms = document.getElementById("localAuthTerms");
212
+ var age = document.getElementById("localAuthAge");
213
+ if (terms) terms.onchange = updateLocalAuthSendButton;
214
+ if (age) age.onchange = updateLocalAuthSendButton;
215
+ updateLocalAuthSendButton();
216
+ }
217
+ async function sendLocalOtp() {
218
+ var email = document.getElementById("localAuthEmail");
219
+ localAuthEmail = email && email.value ? String(email.value).trim().toLowerCase() : "";
220
+ captureLocalAuthConsents();
221
+ if (!localAuthEmail) { setLocalAuthStatus("Enter your email.", "error"); return; }
222
+ if (!localAuthAgeConfirmed || !localAuthTermsAccepted) {
223
+ setLocalAuthStatus("Confirm your age and accept Echo's terms before continuing.", "error");
224
+ return;
225
+ }
226
+ var button = document.getElementById("localAuthSend");
227
+ if (button) { button.disabled = true; button.textContent = "Sending..."; }
228
+ setLocalAuthStatus("Sending verification code...");
162
229
  try {
163
- authWindow = window.open(targetUrl, "echomem-signin-" + nonce, "popup=yes,width=560,height=760");
164
- if (!authWindow) return false;
165
- authWindow.focus();
166
- return true;
167
- } catch (_) { return false; }
168
- }
169
- function showPopupFallback(message, url) {
170
- Array.prototype.forEach.call(document.querySelectorAll("[data-signin-help]"), function (hint) {
171
- hint.textContent = message || "Your browser blocked the secure sign-in window. Use the button below, then this page will resume when sign-in returns.";
172
- });
173
- Array.prototype.forEach.call(document.querySelectorAll("[data-signin-fallback]"), function (fallback) {
174
- fallback.setAttribute("href", url || authUrl);
175
- fallback.classList.remove("hidden");
176
- });
230
+ var data = await postJson("/local-auth/send-otp", {
231
+ email: localAuthEmail,
232
+ acceptedTerms: localAuthTermsAccepted,
233
+ ageConfirmed: localAuthAgeConfirmed
234
+ }, 12000);
235
+ localAuthEmail = data.email || localAuthEmail;
236
+ renderLocalOtp("Code sent. Check your inbox.");
237
+ } catch (error) {
238
+ if (button) { button.disabled = false; button.textContent = "Send code"; }
239
+ setLocalAuthStatus(error && error.message ? error.message : "Could not send the code.", "error");
240
+ }
241
+ }
242
+ function renderLocalOtp(message) {
243
+ setCityMode(false);
244
+ setScanMode(false);
245
+ setExtractMode(false);
246
+ setReadyMode(true);
247
+ setHead("Check your email", "Local login");
248
+ app.className = "localAuthStage";
249
+ 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>' +
256
+ '</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>' +
260
+ '<p class="localAuthStatus" id="localAuthStatus" role="status" aria-live="polite">' + esc(message || "") + '</p>' +
261
+ '</form>' +
262
+ '</section>';
263
+ var form = document.getElementById("localAuthOtpForm");
264
+ if (form) form.onsubmit = function (event) { event.preventDefault(); void verifyLocalOtp(); };
265
+ var back = document.getElementById("localAuthBack");
266
+ if (back) back.onclick = function () { renderLocalLogin(""); };
267
+ var otp = document.getElementById("localAuthOtp");
268
+ if (otp) otp.focus();
269
+ }
270
+ async function verifyLocalOtp() {
271
+ var otpInput = document.getElementById("localAuthOtp");
272
+ var otp = otpInput && otpInput.value ? String(otpInput.value).replace(/\\D/g, "") : "";
273
+ if (otp.length !== 6) { setLocalAuthStatus("Enter the 6-digit code.", "error"); return; }
274
+ var button = document.getElementById("localAuthVerify");
275
+ if (button) { button.disabled = true; button.textContent = "Verifying..."; }
276
+ setLocalAuthStatus("Verifying code...");
277
+ try {
278
+ var data = await postJson("/local-auth/verify-otp", {
279
+ email: localAuthEmail,
280
+ otp: otp,
281
+ acceptedTerms: localAuthTermsAccepted,
282
+ ageConfirmed: localAuthAgeConfirmed
283
+ }, 18000);
284
+ if (data && data.stage === "passphrase") {
285
+ renderLocalPassphrase(data.mode || "unlock", data.email || localAuthEmail);
286
+ return;
287
+ }
288
+ connected = true;
289
+ await waitForStats();
290
+ } catch (error) {
291
+ if (button) { button.disabled = false; button.textContent = "Verify code"; }
292
+ setLocalAuthStatus(error && error.message ? error.message : "Could not verify the code.", "error");
293
+ }
294
+ }
295
+ function renderLocalPassphrase(mode, email, message) {
296
+ localAuthEmail = email || localAuthEmail;
297
+ var setupMode = mode === "setup";
298
+ setCityMode(false);
299
+ setScanMode(false);
300
+ setExtractMode(false);
301
+ setReadyMode(true);
302
+ setHead(setupMode ? "Create encrypted vault" : "Unlock encrypted vault", "Local passphrase");
303
+ app.className = "localAuthStage";
304
+ 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>' +
310
+ '<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
+ '</div>' +
312
+ '<form class="localAuthForm" id="localAuthPassphraseForm">' +
313
+ '<label class="localAuthField"><span>Passphrase</span><input id="localAuthPassphrase" type="password" autocomplete="' + (setupMode ? "new-password" : "current-password") + '" placeholder="Encryption passphrase" /></label>' +
314
+ (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>' +
316
+ '<p class="localAuthStatus' + (message ? " is-error" : "") + '" id="localAuthStatus" role="status" aria-live="polite">' + esc(message || "") + '</p>' +
317
+ '</form>' +
318
+ '</section>';
319
+ var form = document.getElementById("localAuthPassphraseForm");
320
+ if (form) form.onsubmit = function (event) { event.preventDefault(); void submitLocalPassphrase(setupMode); };
321
+ var restart = document.getElementById("localAuthRestart");
322
+ if (restart) restart.onclick = function () { renderLocalLogin(""); };
323
+ var pass = document.getElementById("localAuthPassphrase");
324
+ if (pass) pass.focus();
325
+ }
326
+ async function submitLocalPassphrase(setupMode) {
327
+ var pass = document.getElementById("localAuthPassphrase");
328
+ var confirm = document.getElementById("localAuthPassphraseConfirm");
329
+ var passphrase = pass && typeof pass.value === "string" ? pass.value : "";
330
+ var confirmValue = confirm && typeof confirm.value === "string" ? confirm.value : "";
331
+ if (passphrase.length < 4) { setLocalAuthStatus("Use at least 4 characters.", "error"); return; }
332
+ if (setupMode && passphrase !== confirmValue) { setLocalAuthStatus("Passphrases do not match.", "error"); return; }
333
+ var button = document.getElementById("localAuthUnlock");
334
+ if (button) { button.disabled = true; button.textContent = setupMode ? "Creating..." : "Unlocking..."; }
335
+ setLocalAuthStatus(setupMode ? "Creating encrypted vault..." : "Unlocking vault...");
336
+ try {
337
+ await postJson("/local-auth/passphrase", { passphrase: passphrase }, 30000);
338
+ connected = true;
339
+ await waitForStats();
340
+ } catch (error) {
341
+ if (button) { button.disabled = false; button.textContent = setupMode ? "Create vault" : "Unlock"; }
342
+ var msg = error && error.message ? error.message : "Could not unlock encrypted memory.";
343
+ renderLocalPassphrase(setupMode ? "setup" : "unlock", localAuthEmail, msg);
344
+ }
177
345
  }
178
346
  function onConnectClick() {
179
347
  if (!localHistoryConsentGranted) {
@@ -182,8 +350,7 @@ export const SETUP_PAGE_CLIENT_CORE = String.raw ` var params = new URLSear
182
350
  return;
183
351
  }
184
352
  if (connected) { void waitForStats(); return; }
185
- if (!openAuthWindow(authUrl)) showPopupFallback("", authUrl);
186
- startConnectionPoll();
353
+ renderLocalLogin("");
187
354
  }
188
355
  function bindConnect() {
189
356
  Array.prototype.forEach.call(document.querySelectorAll("[data-connect-echo]"), function (signin) {
@@ -93,30 +93,17 @@ export const SETUP_PAGE_CLIENT_EXTRACTION = String.raw ` /* ---------- dash
93
93
  '<div class="actions"><button id="logout" class="secondary">Retry sign out</button></div></div>';
94
94
  var lo = document.getElementById("logout"); if (lo) lo.onclick = localLogout;
95
95
  }
96
- async function waitForConnection() {
97
- if (connectionPollStarted) return;
98
- connectionPollStarted = true;
99
- try {
100
- for (;;) {
101
- if (decisionMade) return;
102
- try {
103
- var cfg = await getJson("/config");
104
- authUrl = cfg.authUrl || authUrl;
105
- switchAccountUrl = cfg.switchAccountUrl || switchAccountUrl || authUrl;
106
- if (cfg.connected) { connected = true; closeAuthWindow(); await waitForStats(); return; }
107
- } catch (_) {}
108
- await sleep(900);
109
- }
110
- } finally { connectionPollStarted = false; }
111
- }
112
- function startConnectionPoll() { void waitForConnection(); }
113
96
  async function waitForStats() {
114
97
  if (statsPollStarted) return;
115
98
  statsPollStarted = true;
116
- // Use the same one-reading-line stage as the partial scan state. This avoids a jarring
117
- // old setup shell between device approval and the ready decision.
118
- renderDashboard();
99
+ setCityMode(false);
100
+ setExtractMode(false);
101
+ setReadyMode(true);
102
+ setHead("Checking extraction", "Working");
103
+ app.className = "loading-panel";
104
+ app.innerHTML = '<div style="padding:24px"><h2>Checking for active work</h2><p class="helper">Echo is reconnecting to any extraction already running on this machine.</p></div>';
119
105
  try {
106
+ if (await resumeExistingExtraction()) return;
120
107
  var started = Date.now();
121
108
  var lastError = "";
122
109
  for (;;) {
@@ -214,6 +201,7 @@ export const SETUP_PAGE_CLIENT_EXTRACTION = String.raw ` /* ---------- dash
214
201
  '<div class="readyGo">' +
215
202
  '<div class="actions">' +
216
203
  '<button id="migrate" class="primary"></button>' +
204
+ '<button type="button" id="skipHistoryImport" class="secondary skipHistoryButton">Skip history import</button>' +
217
205
  '</div>' +
218
206
  '<p class="ctaMeta" id="exEta"></p>' +
219
207
  '</div>' +
@@ -267,13 +255,19 @@ export const SETUP_PAGE_CLIENT_EXTRACTION = String.raw ` /* ---------- dash
267
255
  // While still counting, hide the primary button entirely; the headline + source split
268
256
  // already say it is working, and the ready state should present one clear action.
269
257
  var migrateBtn = document.getElementById("migrate");
258
+ var skipHistoryBtn = document.getElementById("skipHistoryImport");
270
259
  if (pendingTrusted) {
271
260
  migrateBtn.style.display = "";
272
261
  migrateBtn.disabled = canExtract && (!setupPlanConfirmed() || selectedSessionKeyList().length === 0);
273
262
  migrateBtn.innerHTML = '<span>' + esc(canExtract ? extractLabel : "Finish setup") + '</span>' + (canExtract ? setupIcon("arrow-right") : "");
274
263
  migrateBtn.onclick = canExtract ? startMigrate : skip;
264
+ if (skipHistoryBtn) {
265
+ skipHistoryBtn.style.display = canExtract ? "" : "none";
266
+ skipHistoryBtn.onclick = skip;
267
+ }
275
268
  } else {
276
269
  migrateBtn.style.display = "none";
270
+ if (skipHistoryBtn) skipHistoryBtn.style.display = "none";
277
271
  }
278
272
  }
279
273
  async function startMigrate() {
@@ -294,21 +288,74 @@ export const SETUP_PAGE_CLIENT_EXTRACTION = String.raw ` /* ---------- dash
294
288
  setHead("Extracting", "Starting");
295
289
  app.className = "";
296
290
  app.innerHTML = '<div class="miniLoad"><h2>Starting extraction</h2><p>Echo is sizing the remaining jobs, then imports automatically. Finished work is saved as it goes.</p><div class="miniBar"><i></i></div></div>';
291
+ var started;
297
292
  try {
298
- var started = await postJson("/migrate", { conversationKeys: selectedKeys });
299
- extractMounted = false;
300
- renderProgress(Object.assign({ status: "starting", completed: 0, failed: 0, extracted: 0 }, started));
301
- await pollProgress();
293
+ started = await postJson("/migrate", { conversationKeys: selectedKeys });
302
294
  } catch (e) {
303
295
  decisionMade = false;
304
296
  renderDashboard(routeError(e && e.message));
297
+ return;
298
+ }
299
+ extractMounted = false;
300
+ renderProgress(Object.assign({ status: "starting", completed: 0, failed: 0, extracted: 0 }, started));
301
+ await pollProgress();
302
+ }
303
+ async function resumeExistingExtraction() {
304
+ try {
305
+ var existing = await getJson("/progress");
306
+ if (!existing || existing.status === "idle") return false;
307
+ decisionMade = true;
308
+ extractionEnded = false;
309
+ extractMounted = false;
310
+ renderProgress(existing);
311
+ if (existing.status !== "completed" && existing.status !== "failed") {
312
+ void pollProgress();
313
+ }
314
+ return true;
315
+ } catch (_) {
316
+ // Stats cannot prove that no import is running. Stay in the recovery view until
317
+ // the authoritative progress endpoint answers instead of exposing a second start.
318
+ decisionMade = true;
319
+ extractionEnded = false;
320
+ extractMounted = false;
321
+ renderProgress({
322
+ status: "running",
323
+ total: 0,
324
+ completed: 0,
325
+ failed: 0,
326
+ extracted: 0,
327
+ latest: "Reconnecting to the active extraction..."
328
+ });
329
+ void pollProgress();
330
+ return true;
305
331
  }
306
332
  }
307
333
  async function pollProgress() {
308
334
  var runId = ++progressPollRunId;
335
+ var failures = 0;
309
336
  for (;;) {
310
337
  if (extractionEnded || runId !== progressPollRunId) return;
311
- var progress = await getJson("/progress");
338
+ var progress;
339
+ try {
340
+ progress = await getJson("/progress");
341
+ failures = 0;
342
+ } catch (_) {
343
+ failures++;
344
+ var reconnecting = Object.assign({
345
+ status: "running",
346
+ total: 0,
347
+ completed: 0,
348
+ failed: 0,
349
+ extracted: 0
350
+ }, lastExtractionProgress || {});
351
+ reconnecting.status = reconnecting.status === "completed" || reconnecting.status === "failed"
352
+ ? "running"
353
+ : reconnecting.status;
354
+ reconnecting.latest = "Connection interrupted. Reconnecting to the active extraction...";
355
+ renderProgress(reconnecting);
356
+ await sleep(Math.min(5000, 500 * Math.pow(2, Math.min(failures - 1, 3))));
357
+ continue;
358
+ }
312
359
  if (extractionEnded || runId !== progressPollRunId) return;
313
360
  renderProgress(progress);
314
361
  if (progress.status === "completed" || progress.status === "failed") return;
@@ -352,7 +399,7 @@ export const SETUP_PAGE_CLIENT_EXTRACTION = String.raw ` /* ---------- dash
352
399
  var savedMemoryLabel = extracted === 1 ? "memory saved" : "memories saved";
353
400
  var conversationNoun = completed === 1 ? "conversation" : "conversations";
354
401
  var timelineUrl = "https://yeahecho.com/memories/timeline";
355
- var stopDetail = String(progress.error) === "key-expired"
402
+ var stopDetail = String(progress.error) === "vault-locked"
356
403
  ? "Your encryption key was not accepted. Re-run extraction; if it keeps failing, run <code>echomem-mcp unlock</code> first. Already-done conversations are skipped."
357
404
  : esc(progress.error || "Re-run extraction to finish the rest. Already-done conversations are skipped.");
358
405
  var completionNote = done && progress.latest && progress.latest !== "Import complete."
@@ -626,6 +673,7 @@ export const SETUP_PAGE_CLIENT_EXTRACTION = String.raw ` /* ---------- dash
626
673
  }
627
674
  function updateSessionPickerSelectionUi(dialog, input, limit) {
628
675
  var selectedCount = selectedSessionKeyList().length;
676
+ hideSessionPickerLimitNotice();
629
677
  var row = input && input.closest ? input.closest(".sessionPickerRow") : null;
630
678
  if (row) row.classList.toggle("is-selected", !!input.checked);
631
679
  var count = dialog.querySelector(".sessionPickerActions strong");
@@ -638,6 +686,48 @@ export const SETUP_PAGE_CLIENT_EXTRACTION = String.raw ` /* ---------- dash
638
686
  confirm.textContent = "Use " + number(selectedCount) + " selected";
639
687
  }
640
688
  }
689
+ function sessionPickerLimitNotice(canDismiss) {
690
+ var plan = String(billingStatus && billingStatus.plan || "").toLowerCase();
691
+ var linkLabel = plan === "power" || plan === "team" || plan === "enterprise"
692
+ ? "View plan options"
693
+ : "Upgrade to import more";
694
+ return '<div class="sessionPickerLimitCard">' +
695
+ '<span class="sessionPickerLimitEyebrow">Plan limit</span>' +
696
+ '<h3>Plan limit reached.</h3>' +
697
+ '<p>Upgrade your plan to select and import more conversations.</p>' +
698
+ '<div class="sessionPickerLimitActions"><a id="upgradeSessionPlan" class="button primary" href="' + esc(setupPricingUrl("pro", false)) + '" target="_blank" rel="noopener noreferrer">' + linkLabel + '</a>' +
699
+ (canDismiss ? '<button type="button" class="textButton" id="dismissSessionLimit">Keep editing</button>' : "") + '</div>' +
700
+ '</div>';
701
+ }
702
+ function wireSessionPickerUpgrade() {
703
+ var upgrade = document.getElementById("upgradeSessionPlan");
704
+ if (upgrade) upgrade.onclick = function () { startBillingPoll(); };
705
+ var dismiss = document.getElementById("dismissSessionLimit");
706
+ if (dismiss) dismiss.onclick = hideSessionPickerLimitNotice;
707
+ }
708
+ function showSessionPickerLimitNotice(canDismiss) {
709
+ var wrap = document.getElementById("sessionPickerListWrap");
710
+ var list = wrap && wrap.querySelector ? wrap.querySelector(".sessionPickerList") : null;
711
+ var overlay = document.getElementById("sessionPickerLimitOverlay");
712
+ var status = document.getElementById("sessionPickerStatus");
713
+ if (status) status.textContent = "Plan limit reached. Upgrade to import more.";
714
+ if (!wrap || !overlay) return;
715
+ overlay.innerHTML = sessionPickerLimitNotice(canDismiss);
716
+ overlay.hidden = false;
717
+ if (list) list.inert = true;
718
+ wrap.classList.add("is-limit-blocked");
719
+ wireSessionPickerUpgrade();
720
+ }
721
+ function hideSessionPickerLimitNotice() {
722
+ var wrap = document.getElementById("sessionPickerListWrap");
723
+ var list = wrap && wrap.querySelector ? wrap.querySelector(".sessionPickerList") : null;
724
+ var overlay = document.getElementById("sessionPickerLimitOverlay");
725
+ var status = document.getElementById("sessionPickerStatus");
726
+ if (status) status.textContent = "";
727
+ if (overlay) { overlay.hidden = true; overlay.innerHTML = ""; }
728
+ if (list) list.inert = false;
729
+ if (wrap) wrap.classList.remove("is-limit-blocked");
730
+ }
641
731
  function renderSessionPicker() {
642
732
  var dialog = document.getElementById("sessionPicker");
643
733
  if (!dialog) return;
@@ -674,14 +764,17 @@ export const SETUP_PAGE_CLIENT_EXTRACTION = String.raw ` /* ---------- dash
674
764
  '<div class="sessionPickerToolbar"><label class="sessionSearch"><span>Search</span><input type="search" id="sessionPickerSearch" placeholder="Title or project" value="' + esc(sessionPickerQuery) + '" /></label>' +
675
765
  '<label class="sessionSourceFilter"><span>Source</span><select id="sessionPickerSource"><option value="all"' + (sessionPickerSource === "all" ? " selected" : "") + '>All sources</option><option value="codex"' + (sessionPickerSource === "codex" ? " selected" : "") + '>Codex</option><option value="claude-code"' + (sessionPickerSource === "claude-code" ? " selected" : "") + '>Claude Code</option></select></label>' +
676
766
  '</div>' +
677
- '<div class="sessionPickerActions"><strong>' + esc(number(selected.length)) + ' / ' + esc(number(limit)) + ' selected</strong><span id="sessionPickerStatus" role="status"></span><button type="button" class="textButton" id="selectNewestSessions">Select newest</button><button type="button" class="textButton" id="clearSessions">Clear</button></div>' +
678
- '<div class="sessionPickerList">' + (rows || '<div class="sessionPickerEmpty">No conversations match these filters.</div>') + '</div>' + overflow +
679
- '<footer class="sessionPickerFoot"><p>Only selected conversations will be sent for extraction.</p><button type="button" class="primary" id="confirmSessions"' + (selected.length ? "" : " disabled") + '>Use ' + esc(number(selected.length)) + ' selected</button></footer>' +
767
+ '<div class="sessionPickerActions"><strong>' + esc(number(selected.length)) + ' / ' + esc(number(limit)) + ' selected</strong><span id="sessionPickerStatus" class="sessionPickerStatus" role="status" aria-live="polite">' + (limit === 0 ? "Plan limit reached. Upgrade to import more." : "") + '</span><button type="button" class="textButton" id="selectNewestSessions">Select newest</button><button type="button" class="textButton" id="clearSessions">Clear</button></div>' +
768
+ '<div class="sessionPickerListWrap' + (limit === 0 ? " is-limit-blocked" : "") + '" id="sessionPickerListWrap"><div class="sessionPickerList"' + (limit === 0 ? " inert" : "") + '>' + (rows || '<div class="sessionPickerEmpty">No conversations match these filters.</div>') + overflow + '</div><div class="sessionPickerLimitOverlay" id="sessionPickerLimitOverlay" role="alert"' + (limit === 0 ? "" : " hidden") + '>' + (limit === 0 ? sessionPickerLimitNotice(false) : "") + '</div></div>' +
769
+ '<footer class="sessionPickerFoot"><p>Only selected conversations will be sent for extraction.</p><div class="sessionPickerFootActions"><button type="button" class="secondary" id="skipSessionImport">Skip history import</button><button type="button" class="primary" id="confirmSessions"' + (selected.length ? "" : " disabled") + '>Use ' + esc(number(selected.length)) + ' selected</button></div></footer>' +
680
770
  '</div>';
681
771
  var close = document.getElementById("closeSessionPicker");
682
772
  var confirm = document.getElementById("confirmSessions");
773
+ var skipImport = document.getElementById("skipSessionImport");
683
774
  if (close) close.onclick = function () { dialog.close(); renderSessionSelection(true); };
684
775
  if (confirm) confirm.onclick = function () { dialog.close(); renderSessionSelection(true); };
776
+ if (skipImport) skipImport.onclick = skip;
777
+ wireSessionPickerUpgrade();
685
778
  var search = document.getElementById("sessionPickerSearch");
686
779
  if (search) search.oninput = function () {
687
780
  sessionPickerQuery = search.value || "";
@@ -697,8 +790,7 @@ export const SETUP_PAGE_CLIENT_EXTRACTION = String.raw ` /* ---------- dash
697
790
  var count = selectedSessionKeyList().length;
698
791
  if (input.checked && !selectedSessionKeys[key] && count >= limit) {
699
792
  input.checked = false;
700
- var status = document.getElementById("sessionPickerStatus");
701
- if (status) status.textContent = "Plan limit reached";
793
+ showSessionPickerLimitNotice(limit > 0);
702
794
  return;
703
795
  }
704
796
  sessionSelectionTouched = true;
@@ -751,7 +843,7 @@ export const SETUP_PAGE_CLIENT_EXTRACTION = String.raw ` /* ---------- dash
751
843
  }
752
844
  async function switchSetupAccount() {
753
845
  var button = document.getElementById("changeSetupAccount");
754
- if (button) { button.disabled = true; button.textContent = "Opening account chooser…"; }
846
+ if (button) { button.disabled = true; button.textContent = "Signing out..."; }
755
847
  try {
756
848
  await postJson("/logout", {}, 12000);
757
849
  stats = null;
@@ -759,15 +851,13 @@ export const SETUP_PAGE_CLIENT_EXTRACTION = String.raw ` /* ---------- dash
759
851
  connected = false;
760
852
  selectedSessionKeys = Object.create(null);
761
853
  sessionSelectionTouched = false;
762
- if (!openAuthWindow(switchAccountUrl || authUrl)) {
763
- if (button) { button.disabled = false; button.textContent = "Change account"; }
764
- showPopupFallback("Your browser blocked the account chooser. Open it below to continue.", switchAccountUrl || authUrl);
765
- return;
766
- }
767
- startConnectionPoll();
854
+ localAuthEmail = "";
855
+ localAuthTermsAccepted = false;
856
+ localAuthAgeConfirmed = false;
857
+ renderLocalLogin("");
768
858
  } catch (error) {
769
859
  if (button) { button.disabled = false; button.textContent = "Change account"; }
770
- setSetupPlanStatus(error && error.message ? error.message : "Could not open the account chooser.");
860
+ setSetupPlanStatus(error && error.message ? error.message : "Could not switch accounts.");
771
861
  }
772
862
  }
773
863
  function paidRecallPlan(plan) {
@@ -137,13 +137,11 @@ export const SETUP_PAGE_CLIENT_LIFECYCLE = String.raw ` /* ---------- scan-
137
137
  var data = event.data || {};
138
138
  if (data.type === "echomem:connected" && data.nonce === nonce) {
139
139
  if (!localHistoryConsentGranted) {
140
- closeAuthWindow();
141
140
  renderLocalScanConsent();
142
141
  setConsentStatus("Local history access is required before you can connect EchoMem or continue setup.", "required");
143
142
  return;
144
143
  }
145
144
  connected = true;
146
- closeAuthWindow();
147
145
  void waitForStats();
148
146
  } else if (data.type === "echomem:city-ready" && data.nonce === nonce) {
149
147
  scheduleReportToCity();
@@ -159,12 +157,10 @@ export const SETUP_PAGE_CLIENT_LIFECYCLE = String.raw ` /* ---------- scan-
159
157
  var configLoaded = false;
160
158
  var consentRequired = true;
161
159
  var consentGranted = false;
162
- // Learn the auth URL, connection state, and server-side consent gate before routing.
160
+ // Learn the connection state and server-side consent gate before routing.
163
161
  try {
164
162
  var cfg = await getJson("/config");
165
163
  configLoaded = true;
166
- authUrl = cfg.authUrl || authUrl;
167
- switchAccountUrl = cfg.switchAccountUrl || switchAccountUrl || authUrl;
168
164
  connected = !!cfg.connected;
169
165
  workspacePath = cfg.workspacePath || workspacePath;
170
166
  consentRequired = cfg.consentRequired !== false;
@@ -192,12 +192,21 @@ export const SETUP_PAGE_CLIENT_REPORT_CITY = String.raw ` /* ---------- ful
192
192
  app.className = "scanLoading";
193
193
  app.innerHTML =
194
194
  '<div class="scanLoader" aria-live="polite">' +
195
- '<p class="scanEyebrow">Local scan</p>' +
196
- '<h2>Reading your local coding history</h2>' +
197
- '<div class="scanProgress" role="progressbar" aria-label="Local scan progress" aria-valuemin="0" aria-valuemax="100" aria-valuenow="' + pct + '">' +
198
- '<i id="scan-load-fill" class="scanProgressFill" style="width:' + pct + '%"></i>' +
195
+ '<div class="scanLoaderLead">' +
196
+ '<span class="scanMascot" aria-hidden="true"><img src="/hud-assets/echo-face-cutout.png" alt="" /></span>' +
197
+ '<div class="scanLoaderCopy">' +
198
+ '<p class="scanEyebrow">Local scan</p>' +
199
+ '<h2 class="siteHeadline">Reading your <strong>local coding history</strong></h2>' +
200
+ '<p class="scanIntro">Echo is organizing your Codex and Claude Code sessions on this Mac.</p>' +
201
+ '</div>' +
202
+ '</div>' +
203
+ '<div class="scanProgressCard">' +
204
+ '<div class="scanProgress" role="progressbar" aria-label="Local scan progress" aria-valuemin="0" aria-valuemax="100" aria-valuenow="' + pct + '">' +
205
+ '<i id="scan-load-fill" class="scanProgressFill" style="width:' + pct + '%"></i>' +
206
+ '</div>' +
207
+ '<p id="scan-load-caption" class="scanCaption">' + esc(caption) + '</p>' +
199
208
  '</div>' +
200
- '<p id="scan-load-caption" class="scanCaption">' + esc(caption) + '</p>' +
209
+ '<p class="scanPrivacy"><span aria-hidden="true"></span>Transcripts stay on this device during the scan.</p>' +
201
210
  '</div>';
202
211
  }
203
212