@echomem/mcp 1.4.20 → 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.
@@ -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 (;;) {
@@ -126,6 +113,10 @@ export const SETUP_PAGE_CLIENT_EXTRACTION = String.raw ` /* ---------- dash
126
113
  if (res.status === 200) {
127
114
  stats = await res.json();
128
115
  if (decisionMade) return;
116
+ if (connected && !billingStatus && !billingStatusLoading) {
117
+ try { await refreshBillingStatus(); } catch (_) {}
118
+ if (decisionMade) return;
119
+ }
129
120
  renderDashboard();
130
121
  if (stats && stats.partial) { await sleep(1200); continue; }
131
122
  return;
@@ -148,6 +139,27 @@ export const SETUP_PAGE_CLIENT_EXTRACTION = String.raw ` /* ---------- dash
148
139
  '<iframe class="plateGl" title="Extraction plate" src="/city/echo-extraction-plate.html?nonce=' + encodeURIComponent(nonce) + '"></iframe>' +
149
140
  '</div></div>';
150
141
  }
142
+ function renderPlanGate() {
143
+ setCityMode(false);
144
+ setExtractMode(false);
145
+ setReadyMode(true);
146
+ setHead("Choose your Echo", "Plan");
147
+ dashMounted = false;
148
+ if (readyStage === "plan" && app.className === "planGateStage" && document.getElementById("setupPlan")) {
149
+ renderSetupPlan(true);
150
+ renderAccountChoice();
151
+ return;
152
+ }
153
+ readyStage = "plan";
154
+ app.className = "planGateStage";
155
+ app.innerHTML =
156
+ '<section class="planGateDecision">' +
157
+ '<div class="setupPlanSlot" id="setupPlan"></div>' +
158
+ '<aside class="planGateAccount" aria-label="Connected account"><div class="setupAccountSlot" id="setupAccount"></div></aside>' +
159
+ '</section>';
160
+ renderSetupPlan(true);
161
+ renderAccountChoice();
162
+ }
151
163
  function renderDashboard(error) {
152
164
  setCityMode(false);
153
165
  setExtractMode(false);
@@ -172,6 +184,10 @@ export const SETUP_PAGE_CLIENT_EXTRACTION = String.raw ` /* ---------- dash
172
184
  var etaLabel = eta.estimatedLabel || migratable.estimatedLabel || "a couple of minutes";
173
185
  var pendingCodex = typeof migratable.pendingCodex === "number" ? migratable.pendingCodex : null;
174
186
  var pendingClaudeCode = typeof migratable.pendingClaudeCode === "number" ? migratable.pendingClaudeCode : null;
187
+ if (connected && !billingStatusLoading && !setupPlanConfirmed()) {
188
+ renderPlanGate();
189
+ return;
190
+ }
175
191
  setHead("Turn coding history into memory", pendingTrusted ? (knownDone ? "Done" : "Ready") : (localScanReady ? "Ready" : "Scanning"));
176
192
  var headlineHtml = !pendingTrusted
177
193
  ? "Counting your <strong>new conversations…</strong>"
@@ -183,13 +199,14 @@ export const SETUP_PAGE_CLIENT_EXTRACTION = String.raw ` /* ---------- dash
183
199
  : (knownDone
184
200
  ? (skippedActive ? number(skippedActive) + " active conversation" + (skippedActive === 1 ? " is" : "s are") + " still changing, so Echo will pick them up later." : "Your history is already in EchoMem. Nothing new to extract.")
185
201
  : "Turn them into memory your agents can recall: decisions, fixes, and where you left off. No more re-explaining your project every session.");
186
- var extractLabel = !pendingTrusted ? "Counting…" : (canExtract ? "Start extraction" : "All caught up");
202
+ var extractLabel = !pendingTrusted ? "Counting…" : (canExtract ? "Start memory extraction" : "All caught up");
187
203
  // Mount the dashboard shell ONCE so the WebGL plate iframe is never re-created across /stats polls.
188
204
  // (Rebuilding app.innerHTML on every partial-scan poll reloaded the iframe → the city flickered.)
189
205
  // Later polls only patch the text/counts in place; the iframe stays mounted.
190
206
  var live = dashMounted && document.getElementById("exHeadline");
191
207
  if (!live) {
192
208
  extractMounted = false;
209
+ readyStage = "sessions";
193
210
  app.className = "";
194
211
  // One reading line, top to bottom: the find (headline) → the value (sub) → quiet proof
195
212
  // (source strip) → the single action with its reassurances attached → optional detail.
@@ -198,7 +215,7 @@ export const SETUP_PAGE_CLIENT_EXTRACTION = String.raw ` /* ---------- dash
198
215
  // not a display-size headline.
199
216
  app.innerHTML =
200
217
  '<div id="exBanner"></div>' +
201
- '<section class="readyDecision">' +
218
+ '<section class="readyDecision extractionReady">' +
202
219
  '<p class="utilityEyebrow readyEyebrow">History import / review</p>' +
203
220
  '<div class="readyHero">' +
204
221
  '<img class="readyMascot" src="/hud-assets/echo-face-cutout.png" alt="" />' +
@@ -208,22 +225,23 @@ export const SETUP_PAGE_CLIENT_EXTRACTION = String.raw ` /* ---------- dash
208
225
  '</div>' +
209
226
  '</div>' +
210
227
  '<div class="proofStrip" id="exSources"></div>' +
211
- '<div class="setupAccountSlot" id="setupAccount"></div>' +
212
- '<div class="setupPlanSlot" id="setupPlan"></div>' +
213
- '<div class="sessionSelectionSlot" id="sessionSelection"></div>' +
214
228
  '<div class="readyGo">' +
215
229
  '<div class="actions">' +
216
230
  '<button id="migrate" class="primary"></button>' +
217
- '<button type="button" id="skipHistoryImport" class="secondary skipHistoryButton">Skip history import</button>' +
231
+ '<button type="button" id="skipHistoryImport" class="secondary skipHistoryButton">Skip for now</button>' +
218
232
  '</div>' +
219
233
  '<p class="ctaMeta" id="exEta"></p>' +
220
234
  '</div>' +
235
+ '<aside class="readySettings" aria-label="Account and plan settings">' +
236
+ '<div class="readySetting readyAccountSetting"><span class="readySettingLabel">Account</span><div class="setupAccountSlot" id="setupAccount"></div></div>' +
237
+ '<div class="readySetting readyPlanSetting"><span class="readySettingLabel">Plan</span><div class="setupPlanSlot" id="setupPlan"></div></div>' +
238
+ '</aside>' +
221
239
  '<div class="readyFoot">' +
222
240
  '<details class="readyExplain" id="readyExplain"></details>' +
223
241
  '</div>' +
224
242
  '</section>' +
225
243
  '<dialog class="sessionPicker" id="sessionPicker" aria-labelledby="sessionPickerTitle"></dialog>';
226
- document.getElementById("migrate").onclick = startMigrate;
244
+ document.getElementById("migrate").onclick = openSessionPicker;
227
245
  dashMounted = true;
228
246
  }
229
247
  document.getElementById("exBanner").innerHTML =
@@ -263,7 +281,6 @@ export const SETUP_PAGE_CLIENT_EXTRACTION = String.raw ` /* ---------- dash
263
281
  '</div>';
264
282
  renderSetupPlan(canExtract);
265
283
  renderAccountChoice();
266
- renderSessionSelection(canExtract);
267
284
  if (canExtract && !billingStatus && !billingStatusLoading) void refreshBillingStatus();
268
285
  // While still counting, hide the primary button entirely; the headline + source split
269
286
  // already say it is working, and the ready state should present one clear action.
@@ -271,9 +288,10 @@ export const SETUP_PAGE_CLIENT_EXTRACTION = String.raw ` /* ---------- dash
271
288
  var skipHistoryBtn = document.getElementById("skipHistoryImport");
272
289
  if (pendingTrusted) {
273
290
  migrateBtn.style.display = "";
291
+ if (canExtract) ensureSessionSelection();
274
292
  migrateBtn.disabled = canExtract && (!setupPlanConfirmed() || selectedSessionKeyList().length === 0);
275
- migrateBtn.innerHTML = '<span>' + esc(canExtract ? extractLabel : "Finish setup") + '</span>' + (canExtract ? setupIcon("arrow-right") : "");
276
- migrateBtn.onclick = canExtract ? startMigrate : skip;
293
+ migrateBtn.innerHTML = '<span>' + esc(canExtract ? "Review " + number(pendN) + " sessions" : "Finish setup") + '</span>' + (canExtract ? setupIcon("arrow-right") : "");
294
+ migrateBtn.onclick = canExtract ? openSessionPicker : skip;
277
295
  if (skipHistoryBtn) {
278
296
  skipHistoryBtn.style.display = canExtract ? "" : "none";
279
297
  skipHistoryBtn.onclick = skip;
@@ -286,12 +304,14 @@ export const SETUP_PAGE_CLIENT_EXTRACTION = String.raw ` /* ---------- dash
286
304
  async function startMigrate() {
287
305
  if (decisionMade) return;
288
306
  if (!setupPlanConfirmed()) {
289
- setSetupPlanStatus("Choose Free, Pro, or Power before extraction starts.");
307
+ setSetupPlanStatus("Choose Original, Pro, or Power to bring Echo home.");
290
308
  return;
291
309
  }
292
310
  var selectedKeys = selectedSessionKeyList();
293
311
  if (!selectedKeys.length) {
294
- renderSessionSelection(true, "Choose at least one conversation to extract.");
312
+ openSessionPicker();
313
+ var pickerStatus = document.getElementById("sessionPickerStatus");
314
+ if (pickerStatus) pickerStatus.textContent = "Choose at least one conversation to extract.";
295
315
  return;
296
316
  }
297
317
  stopBillingPoll();
@@ -301,21 +321,74 @@ export const SETUP_PAGE_CLIENT_EXTRACTION = String.raw ` /* ---------- dash
301
321
  setHead("Extracting", "Starting");
302
322
  app.className = "";
303
323
  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>';
324
+ var started;
304
325
  try {
305
- var started = await postJson("/migrate", { conversationKeys: selectedKeys });
306
- extractMounted = false;
307
- renderProgress(Object.assign({ status: "starting", completed: 0, failed: 0, extracted: 0 }, started));
308
- await pollProgress();
326
+ started = await postJson("/migrate", { conversationKeys: selectedKeys });
309
327
  } catch (e) {
310
328
  decisionMade = false;
311
329
  renderDashboard(routeError(e && e.message));
330
+ return;
331
+ }
332
+ extractMounted = false;
333
+ renderProgress(Object.assign({ status: "starting", completed: 0, failed: 0, extracted: 0 }, started));
334
+ await pollProgress();
335
+ }
336
+ async function resumeExistingExtraction() {
337
+ try {
338
+ var existing = await getJson("/progress");
339
+ if (!existing || existing.status === "idle") return false;
340
+ decisionMade = true;
341
+ extractionEnded = false;
342
+ extractMounted = false;
343
+ renderProgress(existing);
344
+ if (existing.status !== "completed" && existing.status !== "failed") {
345
+ void pollProgress();
346
+ }
347
+ return true;
348
+ } catch (_) {
349
+ // Stats cannot prove that no import is running. Stay in the recovery view until
350
+ // the authoritative progress endpoint answers instead of exposing a second start.
351
+ decisionMade = true;
352
+ extractionEnded = false;
353
+ extractMounted = false;
354
+ renderProgress({
355
+ status: "running",
356
+ total: 0,
357
+ completed: 0,
358
+ failed: 0,
359
+ extracted: 0,
360
+ latest: "Reconnecting to the active extraction..."
361
+ });
362
+ void pollProgress();
363
+ return true;
312
364
  }
313
365
  }
314
366
  async function pollProgress() {
315
367
  var runId = ++progressPollRunId;
368
+ var failures = 0;
316
369
  for (;;) {
317
370
  if (extractionEnded || runId !== progressPollRunId) return;
318
- var progress = await getJson("/progress");
371
+ var progress;
372
+ try {
373
+ progress = await getJson("/progress");
374
+ failures = 0;
375
+ } catch (_) {
376
+ failures++;
377
+ var reconnecting = Object.assign({
378
+ status: "running",
379
+ total: 0,
380
+ completed: 0,
381
+ failed: 0,
382
+ extracted: 0
383
+ }, lastExtractionProgress || {});
384
+ reconnecting.status = reconnecting.status === "completed" || reconnecting.status === "failed"
385
+ ? "running"
386
+ : reconnecting.status;
387
+ reconnecting.latest = "Connection interrupted. Reconnecting to the active extraction...";
388
+ renderProgress(reconnecting);
389
+ await sleep(Math.min(5000, 500 * Math.pow(2, Math.min(failures - 1, 3))));
390
+ continue;
391
+ }
319
392
  if (extractionEnded || runId !== progressPollRunId) return;
320
393
  renderProgress(progress);
321
394
  if (progress.status === "completed" || progress.status === "failed") return;
@@ -358,7 +431,6 @@ export const SETUP_PAGE_CLIENT_EXTRACTION = String.raw ` /* ---------- dash
358
431
  var memoryNoun = extracted === 1 ? "new memory" : "new memories";
359
432
  var savedMemoryLabel = extracted === 1 ? "memory saved" : "memories saved";
360
433
  var conversationNoun = completed === 1 ? "conversation" : "conversations";
361
- var timelineUrl = "https://yeahecho.com/memories/timeline";
362
434
  var stopDetail = String(progress.error) === "vault-locked"
363
435
  ? "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."
364
436
  : esc(progress.error || "Re-run extraction to finish the rest. Already-done conversations are skipped.");
@@ -371,7 +443,7 @@ export const SETUP_PAGE_CLIENT_EXTRACTION = String.raw ` /* ---------- dash
371
443
  '<div class="resultTop">' +
372
444
  '<div>' +
373
445
  (done
374
- ? '<h2 class="savedHeading siteHeadline"><span class="savedMain"><strong>' + esc(number(extracted)) + '</strong> ' + esc(savedMemoryLabel) + '</span><a class="viewEchoLink" href="' + timelineUrl + '" target="_blank" rel="noopener noreferrer">(view in yeahecho)</a></h2>' + completionNote
446
+ ? '<h2 class="savedHeading siteHeadline"><span class="savedMain"><strong>' + esc(number(extracted)) + '</strong> ' + esc(savedMemoryLabel) + '</span></h2>' + completionNote
375
447
  : '<h2 class="siteHeadline">Some conversations need <strong>another pass.</strong></h2>' +
376
448
  '<p class="exSub">' + stopDetail + '</p>' +
377
449
  '<div class="resultNumber"><strong>' + esc(number(extracted)) + '</strong><span>' + esc(memoryNoun) + '</span></div>') +
@@ -643,7 +715,7 @@ export const SETUP_PAGE_CLIENT_EXTRACTION = String.raw ` /* ---------- dash
643
715
  var confirm = dialog.querySelector("#confirmSessions");
644
716
  if (confirm) {
645
717
  confirm.disabled = selectedCount === 0;
646
- confirm.textContent = "Use " + number(selectedCount) + " selected";
718
+ confirm.textContent = "Start memory extraction";
647
719
  }
648
720
  }
649
721
  function sessionPickerLimitNotice(canDismiss) {
@@ -726,13 +798,13 @@ export const SETUP_PAGE_CLIENT_EXTRACTION = String.raw ` /* ---------- dash
726
798
  '</div>' +
727
799
  '<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>' +
728
800
  '<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>' +
729
- '<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>' +
801
+ '<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") + '>Start memory extraction</button></div></footer>' +
730
802
  '</div>';
731
803
  var close = document.getElementById("closeSessionPicker");
732
804
  var confirm = document.getElementById("confirmSessions");
733
805
  var skipImport = document.getElementById("skipSessionImport");
734
806
  if (close) close.onclick = function () { dialog.close(); renderSessionSelection(true); };
735
- if (confirm) confirm.onclick = function () { dialog.close(); renderSessionSelection(true); };
807
+ if (confirm) confirm.onclick = function () { dialog.close(); void startMigrate(); };
736
808
  if (skipImport) skipImport.onclick = skip;
737
809
  wireSessionPickerUpgrade();
738
810
  var search = document.getElementById("sessionPickerSearch");
@@ -787,6 +859,7 @@ export const SETUP_PAGE_CLIENT_EXTRACTION = String.raw ` /* ---------- dash
787
859
  var account = billingStatus && billingStatus.account;
788
860
  var name = account && account.displayName ? account.displayName : "Connected EchoMem account";
789
861
  var email = account && account.email ? account.email : "This import will be saved to the account connected on this device.";
862
+ var accountLabel = account && account.email ? account.email : name;
790
863
  var avatarUrl = account && account.avatarUrl ? String(account.avatarUrl) : "";
791
864
  var avatar = '<span class="setupAccountAvatar" aria-hidden="true">' +
792
865
  '<span>' + esc(accountInitials(account)) + '</span>' +
@@ -795,15 +868,15 @@ export const SETUP_PAGE_CLIENT_EXTRACTION = String.raw ` /* ---------- dash
795
868
  slot.innerHTML =
796
869
  '<section class="setupAccountCard" aria-label="Connected EchoMem account">' +
797
870
  avatar +
798
- '<span class="setupAccountCopy"><small>Saving to</small><strong>' + esc(name) + '</strong><span>' + esc(email) + '</span></span>' +
799
- '<button type="button" class="textButton setupAccountChange" id="changeSetupAccount">Change account</button>' +
871
+ '<span class="setupAccountCopy"><small>Logged in as</small><strong>' + esc(accountLabel) + '</strong><span>' + esc(email) + '</span></span>' +
872
+ '<button type="button" class="textButton setupAccountChange" id="changeSetupAccount">Switch</button>' +
800
873
  '</section>';
801
874
  var change = document.getElementById("changeSetupAccount");
802
875
  if (change) change.onclick = switchSetupAccount;
803
876
  }
804
877
  async function switchSetupAccount() {
805
878
  var button = document.getElementById("changeSetupAccount");
806
- if (button) { button.disabled = true; button.textContent = "Opening account chooser…"; }
879
+ if (button) { button.disabled = true; button.textContent = "Signing out..."; }
807
880
  try {
808
881
  await postJson("/logout", {}, 12000);
809
882
  stats = null;
@@ -811,15 +884,13 @@ export const SETUP_PAGE_CLIENT_EXTRACTION = String.raw ` /* ---------- dash
811
884
  connected = false;
812
885
  selectedSessionKeys = Object.create(null);
813
886
  sessionSelectionTouched = false;
814
- if (!openAuthWindow(switchAccountUrl || authUrl)) {
815
- if (button) { button.disabled = false; button.textContent = "Change account"; }
816
- showPopupFallback("Your browser blocked the account chooser. Open it below to continue.", switchAccountUrl || authUrl);
817
- return;
818
- }
819
- startConnectionPoll();
887
+ localAuthEmail = "";
888
+ localAuthTermsAccepted = false;
889
+ localAuthAgeConfirmed = false;
890
+ renderLocalLogin("");
820
891
  } catch (error) {
821
892
  if (button) { button.disabled = false; button.textContent = "Change account"; }
822
- setSetupPlanStatus(error && error.message ? error.message : "Could not open the account chooser.");
893
+ setSetupPlanStatus(error && error.message ? error.message : "Could not switch accounts.");
823
894
  }
824
895
  }
825
896
  function paidRecallPlan(plan) {
@@ -834,6 +905,112 @@ export const SETUP_PAGE_CLIENT_EXTRACTION = String.raw ` /* ---------- dash
834
905
  function setupPlanConfirmed() {
835
906
  return setupPlanChoice === "free" || paidRecallPlan(billingStatus && billingStatus.plan);
836
907
  }
908
+ function setupPlanDefinition(plan) {
909
+ if (plan === "power") return {
910
+ id: "power",
911
+ name: "Power",
912
+ homeName: "Power",
913
+ promise: "More room. More agents. Much more Echo.",
914
+ price: "$100",
915
+ cadence: "per month · cancel anytime",
916
+ sticker: "/hud-assets/echo-pricing-power-sticker.png",
917
+ stickerAlt: "Power Echo arrives with a gold key and a crew of notebook helpers.",
918
+ features: ["2,000 past chats", "agent-heavy memory", "2,000 context returns / week"]
919
+ };
920
+ if (plan === "pro") return {
921
+ id: "pro",
922
+ name: "Pro",
923
+ homeName: "Pro",
924
+ promise: "A room for the work you return to every day.",
925
+ price: "$20",
926
+ cadence: "per month · cancel anytime",
927
+ sticker: "/hud-assets/echo-pricing-pro-sticker.png",
928
+ stickerAlt: "Pro Echo organizes tabbed notebooks and a daily refresh control.",
929
+ popular: true,
930
+ features: ["500 past chats", "daily memory updates", "500 context returns / week"]
931
+ };
932
+ return {
933
+ id: "free",
934
+ name: "Free",
935
+ homeName: "Original",
936
+ promise: "A little corner is enough to start.",
937
+ price: "$0",
938
+ cadence: "free forever · no card needed",
939
+ sticker: "/hud-assets/echo-pricing-free-sticker.png",
940
+ stickerAlt: "Original Echo hugs one simple memory card.",
941
+ features: ["100 past chats", "a few new chats / week", "100 context returns / week"]
942
+ };
943
+ }
944
+ function setupDiscoveredSessionCount() {
945
+ var pending = stats && stats.migratable && stats.migratable.pending;
946
+ if (typeof pending === "number" && isFinite(pending)) return Math.max(0, pending);
947
+ var total = stats && stats.sessions && stats.sessions.total;
948
+ return typeof total === "number" && isFinite(total) ? Math.max(0, total) : 0;
949
+ }
950
+ function renderPlanHabitat() {
951
+ setupPlanPreview = setupPlanPreview === "pro" || setupPlanPreview === "power" ? setupPlanPreview : "free";
952
+ var plan = setupPlanDefinition(setupPlanPreview);
953
+ var discovered = setupDiscoveredSessionCount();
954
+ var tabs = ["free", "pro", "power"].map(function (id) {
955
+ var tab = setupPlanDefinition(id);
956
+ var selected = id === plan.id;
957
+ return '<button type="button" class="mvpPlanTab" id="planTab-' + id + '" data-plan-tab="' + id + '" role="tab" aria-selected="' + (selected ? "true" : "false") + '" aria-controls="activePlanHabitat"><span>' + esc(tab.homeName) + '</span><b>' + esc(tab.price) + '</b></button>';
958
+ }).join("");
959
+ var facts = plan.features.map(function (feature) {
960
+ return '<li>' + setupIcon("check") + '<span>' + esc(feature) + '</span></li>';
961
+ }).join("");
962
+ var chooseId = plan.id === "free" ? "chooseFreePlan" : (plan.id === "power" ? "choosePowerPlan" : "chooseProPlan");
963
+ var discovery = discovered > 0
964
+ ? '<p class="mvpPricingDiscovery">Echo found <strong>' + esc(number(discovered)) + '</strong> new coding session' + (discovered === 1 ? "" : "s") + ' on this Mac.</p>'
965
+ : "";
966
+ return '<section class="mvpPricing" aria-labelledby="setupPlanHeading">' +
967
+ '<header class="mvpPricingIntro"><span>Plans</span><h2 id="setupPlanHeading">Choose your Echo.</h2>' + discovery + '</header>' +
968
+ '<div class="mvpPlanTabs" role="tablist" aria-label="Choose an Echo plan">' + tabs + '</div>' +
969
+ '<article class="mvpPlanHabitat" id="activePlanHabitat" role="tabpanel" aria-labelledby="planTab-' + plan.id + '" data-plan="' + plan.id + '">' +
970
+ '<div class="mvpHabitatScene">' +
971
+ '<div class="mvpHabitatWindow" aria-hidden="true"></div>' +
972
+ '<div class="mvpHabitatShelf" aria-hidden="true"><i></i><i></i><i></i><i></i><i></i></div>' +
973
+ '<div class="mvpHabitatRug" aria-hidden="true"></div>' +
974
+ '<img class="mvpHabitatEcho" src="' + esc(plan.sticker) + '" alt="' + esc(plan.stickerAlt) + '" />' +
975
+ '</div>' +
976
+ '<div class="mvpHabitatDetails">' +
977
+ '<div class="mvpPlanIdentity"><span>' + esc(plan.name) + '</span>' + (plan.popular ? "<b>Echo&rsquo;s pick</b>" : "") + '</div>' +
978
+ '<h3>' + esc(plan.homeName) + ' Echo</h3>' +
979
+ '<p class="mvpPlanPromise">' + esc(plan.promise) + '</p>' +
980
+ '<div class="mvpPlanPrice"><strong>' + esc(plan.price) + '</strong><span>' + esc(plan.cadence) + '</span></div>' +
981
+ '<p class="mvpPacksLabel">Echo packs</p>' +
982
+ '<ul class="mvpPlanFacts">' + facts + '</ul>' +
983
+ '<button type="button" class="primary mvpPlanCta" id="' + chooseId + '">Take ' + esc(plan.homeName) + ' home <span aria-hidden="true">→</span></button>' +
984
+ '</div>' +
985
+ '</article>' +
986
+ '<p class="mvpPlanStatus" id="setupPlanStatus" role="status"></p>' +
987
+ '</section>';
988
+ }
989
+ function bindPlanHabitat() {
990
+ Array.prototype.forEach.call(document.querySelectorAll("[data-plan-tab]"), function (tab) {
991
+ tab.onclick = function () {
992
+ setupPlanPreview = tab.getAttribute("data-plan-tab") || "free";
993
+ var slot = document.getElementById("setupPlan");
994
+ if (slot) slot.innerHTML = renderPlanHabitat();
995
+ bindPlanHabitat();
996
+ };
997
+ });
998
+ var free = document.getElementById("chooseFreePlan");
999
+ if (free) free.onclick = function () {
1000
+ rememberSetupPlanChoice("free");
1001
+ renderDashboard();
1002
+ };
1003
+ var pro = document.getElementById("chooseProPlan");
1004
+ if (pro) pro.onclick = function () {
1005
+ rememberSetupPlanChoice("pro");
1006
+ openPaidPricing("pro");
1007
+ };
1008
+ var power = document.getElementById("choosePowerPlan");
1009
+ if (power) power.onclick = function () {
1010
+ rememberSetupPlanChoice("power");
1011
+ openPaidPricing("power");
1012
+ };
1013
+ }
837
1014
  function setSetupPlanStatus(message) {
838
1015
  var node = document.getElementById("setupPlanStatus");
839
1016
  if (node) node.textContent = message || "";
@@ -841,9 +1018,11 @@ export const SETUP_PAGE_CLIENT_EXTRACTION = String.raw ` /* ---------- dash
841
1018
  function renderSetupPlan(canExtract) {
842
1019
  var slot = document.getElementById("setupPlan");
843
1020
  if (!slot) return;
1021
+ var readySettings = slot.closest ? slot.closest(".readySettings") : null;
844
1022
  if (!canExtract) {
845
1023
  slot.innerHTML = "";
846
1024
  slot.classList.add("is-hidden");
1025
+ if (readySettings) readySettings.classList.remove("is-plan-open");
847
1026
  return;
848
1027
  }
849
1028
  slot.classList.remove("is-hidden");
@@ -851,61 +1030,70 @@ export const SETUP_PAGE_CLIENT_EXTRACTION = String.raw ` /* ---------- dash
851
1030
  var paid = paidRecallPlan(plan);
852
1031
  var migrateBtn = document.getElementById("migrate");
853
1032
  if (paid) {
1033
+ if (readyStage === "plan") {
1034
+ stopBillingPoll();
1035
+ renderDashboard();
1036
+ return;
1037
+ }
1038
+ if (readySettings) readySettings.classList.remove("is-plan-open");
1039
+ rememberSetupPlanChoice("");
854
1040
  setupPlanChoice = "paid";
855
1041
  var planLabel = plan.charAt(0).toUpperCase() + plan.slice(1);
1042
+ var checkoutJustConfirmed = !!billingActivationPendingPlan;
1043
+ billingActivationPendingPlan = "";
856
1044
  var paidHistory = billingStatus && billingStatus.historicalConversationQuota;
857
1045
  var paidUsage = paidHistory && typeof paidHistory.used === "number" && typeof paidHistory.limit === "number"
858
1046
  ? paidHistory.used + " / " + paidHistory.limit + " bulk history imports used."
859
1047
  : "Your bulk history import and weekly new-chat allowance are ready.";
860
- slot.innerHTML = '<div class="setupPlanConfirmed"><span class="setupPlanConfirmedIcon" aria-hidden="true">' + setupIcon("check") + '</span><span class="setupPlanConfirmedCopy"><strong>' + esc(planLabel) + ' is active.</strong><span>' + esc(paidUsage) + '</span></span></div>';
1048
+ if (checkoutJustConfirmed) document.title = "Echo plan active continue setup";
1049
+ slot.innerHTML = '<div class="setupPlanConfirmed' + (checkoutJustConfirmed ? ' is-checkout-confirmed' : '') + '"><span class="setupPlanConfirmedIcon" aria-hidden="true">' + setupIcon("check") + '</span><span class="setupPlanConfirmedCopy"><strong>' + (checkoutJustConfirmed ? 'Payment confirmed. ' : '') + esc(planLabel) + ' Echo</strong><span>' + esc(checkoutJustConfirmed ? "Your plan is ready. Continue below—checkout never needed access to this localhost page." : paidUsage) + '</span></span><button type="button" class="textButton" id="changeActivePlan">Change</button></div>';
1050
+ var changeActivePlan = document.getElementById("changeActivePlan");
1051
+ if (changeActivePlan) changeActivePlan.onclick = openHostedPlanOptions;
861
1052
  renderSessionSelection(true);
862
1053
  stopBillingPoll();
863
1054
  return;
864
1055
  }
865
1056
  if (setupPlanChoice === "free") {
866
- slot.innerHTML = '<div class="setupPlanConfirmed"><span class="setupPlanConfirmedIcon" aria-hidden="true">' + setupIcon("check") + '</span><span class="setupPlanConfirmedCopy"><strong>Free selected.</strong><span>100 bulk history imports, 25K new-chat tokens each week, and 10 memory searches each week.</span></span><button type="button" class="textButton" id="changeSetupPlan">Change</button></div>';
1057
+ if (readySettings) readySettings.classList.remove("is-plan-open");
1058
+ slot.innerHTML = '<div class="setupPlanConfirmed"><span class="setupPlanConfirmedIcon" aria-hidden="true">' + setupIcon("check") + '</span><span class="setupPlanConfirmedCopy"><strong>Original Echo</strong><span>100 bulk history imports, 25K new-chat tokens each week, and 10 memory searches each week.</span></span><button type="button" class="textButton" id="changeSetupPlan">Change</button></div>';
867
1059
  var changeBtn = document.getElementById("changeSetupPlan");
868
- if (changeBtn) changeBtn.onclick = function () { setupPlanChoice = ""; renderSetupPlan(true); renderSessionSelection(true); };
1060
+ if (changeBtn) changeBtn.onclick = function () {
1061
+ rememberSetupPlanChoice("");
1062
+ renderDashboard();
1063
+ };
869
1064
  renderSessionSelection(true);
870
1065
  return;
871
1066
  }
872
- var trialAvailable = !billingStatus || billingStatus.trialAvailable !== false;
873
- slot.innerHTML =
874
- '<section class="setupPlanChoice" aria-labelledby="setupPlanHeading">' +
875
- '<div class="setupPlanIntro"><h3 id="setupPlanHeading">Choose a plan before extraction starts.</h3><p>Plans set how much history you can import and how much new conversation text Echo processes each week. There is no memory-count limit.</p></div>' +
876
- '<div class="setupPlanGrid">' +
877
- '<article class="setupPlanCard">' +
878
- '<div class="setupPlanHead"><strong>Free</strong><span>$0 forever</span></div>' +
879
- '<p>Start with the essentials.</p>' +
880
- '<ul><li>' + setupIcon("check") + '<span>100 bulk history imports</span></li><li>' + setupIcon("check") + '<span>25K new-chat tokens each week</span></li><li>' + setupIcon("check") + '<span>10 memory searches each week</span></li><li>' + setupIcon("check") + '<span>No memory-count limit</span></li></ul>' +
881
- '<button type="button" class="secondary" id="chooseFreePlan">Continue with Free</button>' +
882
- '</article>' +
883
- '<article class="setupPlanCard is-pro">' +
884
- '<div class="setupPlanHead"><strong>Pro</strong><span>$40 / month</span></div>' +
885
- '<p>More recall for serious work.</p>' +
886
- '<ul><li>' + setupIcon("check") + '<span>250 bulk history imports</span></li><li>' + setupIcon("check") + '<span>200K new-chat tokens each week</span></li><li>' + setupIcon("check") + '<span>100 memory searches each week</span></li><li>' + setupIcon("check") + '<span>No memory-count limit</span></li></ul>' +
887
- '<button type="button" class="primary" id="chooseProPlan">' + (trialAvailable ? "Start 7-day trial" : "Choose Pro") + '</button>' +
888
- '<small>' + (trialAvailable ? "Card required. Cancel anytime." : "Your trial was already used.") + '</small>' +
889
- '</article>' +
890
- '<article class="setupPlanCard">' +
891
- '<div class="setupPlanHead"><strong>Power</strong><span>$100 / month</span></div>' +
892
- '<p>Higher limits for people who use Echo every day.</p>' +
893
- // Future Power feature: API and automation access.
894
- '<ul><li>' + setupIcon("check") + '<span>500 bulk history imports</span></li><li>' + setupIcon("check") + '<span>750K new-chat tokens each week</span></li><li>' + setupIcon("check") + '<span>250 memory searches each week</span></li><li>' + setupIcon("check") + '<span>No memory-count limit</span></li></ul>' +
895
- '<button type="button" class="secondary" id="choosePowerPlan">' + (trialAvailable ? "Start 7-day trial" : "Choose Power") + '</button>' +
896
- '<small>' + (trialAvailable ? "Card required. Cancel anytime." : "Your trial was already used.") + '</small>' +
897
- '</article>' +
898
- '</div>' +
899
- '<div class="setupPlanFoot"><a href="' + esc(setupPricingUrl("pro", false)) + '" target="_blank" rel="noopener noreferrer">See all plan details</a><span id="setupPlanStatus" role="status"></span></div>' +
900
- '</section>';
901
- var freeBtn = document.getElementById("chooseFreePlan");
902
- if (freeBtn) freeBtn.onclick = function () { setupPlanChoice = "free"; renderSetupPlan(true); renderSessionSelection(true); };
903
- var proBtn = document.getElementById("chooseProPlan");
904
- if (proBtn) proBtn.onclick = function () { openPaidPricing("pro"); };
905
- var powerBtn = document.getElementById("choosePowerPlan");
906
- if (powerBtn) powerBtn.onclick = function () { openPaidPricing("power"); };
1067
+ var pendingPaidPlan = setupPlanChoice === "power" || billingActivationPendingPlan === "power"
1068
+ ? "power"
1069
+ : (setupPlanChoice === "pro" || billingActivationPendingPlan === "pro" ? "pro" : "");
1070
+ if (pendingPaidPlan) {
1071
+ if (readySettings) readySettings.classList.remove("is-plan-open");
1072
+ var selectedPaidPlan = pendingPaidPlan === "power" ? "Power" : "Pro";
1073
+ slot.innerHTML =
1074
+ '<section class="setupPlanChoice setupPlanCheckout" aria-labelledby="setupPlanHeading">' +
1075
+ '<div class="setupPlanIntro"><h3 id="setupPlanHeading">' + selectedPaidPlan + ' checkout is open.</h3><p>Finish in the secure Stripe tab. This page will notice automatically when your plan becomes active.</p></div>' +
1076
+ '<button type="button" class="primary" id="continueSelectedPaidPlan">Reopen secure checkout</button>' +
1077
+ '<div class="setupPlanFoot"><button type="button" class="textButton" id="changeSelectedPaidPlan">Choose another Echo</button><span id="setupPlanStatus" role="status"></span></div>' +
1078
+ '</section>';
1079
+ var continuePaid = document.getElementById("continueSelectedPaidPlan");
1080
+ if (continuePaid) continuePaid.onclick = function () { openPaidPricing(pendingPaidPlan); };
1081
+ var changePaid = document.getElementById("changeSelectedPaidPlan");
1082
+ if (changePaid) changePaid.onclick = function () {
1083
+ stopBillingPoll();
1084
+ billingActivationPendingPlan = "";
1085
+ rememberSetupPlanChoice("");
1086
+ renderSetupPlan(true);
1087
+ renderSessionSelection(true);
1088
+ };
1089
+ if (migrateBtn) migrateBtn.disabled = true;
1090
+ renderSessionSelection(true);
1091
+ return;
1092
+ }
1093
+ if (readySettings) readySettings.classList.add("is-plan-open");
1094
+ slot.innerHTML = renderPlanHabitat();
1095
+ bindPlanHabitat();
907
1096
  if (migrateBtn) migrateBtn.disabled = true;
908
- renderSessionSelection(true);
909
1097
  }
910
1098
  async function refreshBillingStatus() {
911
1099
  if (billingStatusLoading) return;
@@ -915,9 +1103,9 @@ export const SETUP_PAGE_CLIENT_EXTRACTION = String.raw ` /* ---------- dash
915
1103
  renderSetupPlan(true);
916
1104
  renderAccountChoice();
917
1105
  renderSessionSelection(true);
918
- if (billingStatus && billingStatus.plan === "unknown") setSetupPlanStatus("Plan check unavailable. Free still works.");
1106
+ if (billingStatus && billingStatus.plan === "unknown") setSetupPlanStatus("Plan check unavailable. Original still works.");
919
1107
  } catch (_) {
920
- setSetupPlanStatus("Plan check unavailable. Free still works.");
1108
+ setSetupPlanStatus("Plan check unavailable. Original still works.");
921
1109
  } finally {
922
1110
  billingStatusLoading = false;
923
1111
  }
@@ -933,21 +1121,54 @@ export const SETUP_PAGE_CLIENT_EXTRACTION = String.raw ` /* ---------- dash
933
1121
  billingPollTimer = null;
934
1122
  window.removeEventListener("focus", refreshBillingOnFocus);
935
1123
  }
936
- function openPaidPricing(plan) {
937
- var url = setupPricingUrl(plan, true);
938
- setSetupPlanStatus("Opening secure checkout…");
1124
+ async function openHostedBilling(path, body, openingMessage, openedMessage) {
1125
+ var opened = null;
939
1126
  try {
940
- var opened = window.open(url, "_blank");
1127
+ opened = window.open("about:blank", "_blank");
941
1128
  if (!opened) {
942
- setSetupPlanStatus("Your browser blocked checkout. Allow pop-ups, then try again.");
943
- return;
1129
+ setSetupPlanStatus("Your browser blocked the secure billing page. Allow pop-ups, then try again.");
1130
+ return false;
944
1131
  }
1132
+ try {
1133
+ opened.document.title = "Opening secure Echo billing…";
1134
+ opened.document.body.innerHTML = '<p style="font:16px system-ui;padding:2rem;color:#555">Opening secure Echo billing…</p>';
1135
+ } catch (_) {}
1136
+ setSetupPlanStatus(openingMessage);
1137
+ var response = await postJson(path, body, 18000);
1138
+ if (!response || typeof response.url !== "string") throw new Error("Echo billing did not return a secure URL.");
945
1139
  try { opened.opener = null; } catch (_) {}
1140
+ opened.location.href = response.url;
1141
+ setSetupPlanStatus(openedMessage);
946
1142
  startBillingPoll();
947
- } catch (_) {
948
- setSetupPlanStatus("Could not open checkout. Allow pop-ups, then try again.");
1143
+ return true;
1144
+ } catch (error) {
1145
+ try { if (opened && !opened.closed) opened.close(); } catch (_) {}
1146
+ setSetupPlanStatus(error && error.message ? error.message : "Could not open secure Echo billing.");
1147
+ return false;
949
1148
  }
950
1149
  }
1150
+ async function openPaidPricing(plan) {
1151
+ setSetupPlanStatus("Opening secure checkout…");
1152
+ var opened = await openHostedBilling(
1153
+ "/billing-checkout",
1154
+ { plan: plan, trial: !billingStatus || billingStatus.trialAvailable !== false },
1155
+ "Creating your secure checkout…",
1156
+ "Checkout opened in a secure tab. Keep this local page open; Echo will confirm here automatically."
1157
+ );
1158
+ if (opened) {
1159
+ billingActivationPendingPlan = plan;
1160
+ document.title = "Finish checkout — Echo setup is waiting";
1161
+ renderSetupPlan(true);
1162
+ }
1163
+ }
1164
+ async function openHostedPlanOptions() {
1165
+ await openHostedBilling(
1166
+ "/billing-portal",
1167
+ {},
1168
+ "Opening secure plan management…",
1169
+ "Plan management opened in a secure tab. Echo will refresh your plan here automatically."
1170
+ );
1171
+ }
951
1172
  function tryItCard() {
952
1173
  var questions = recallQuestions();
953
1174
  var prompts = questions.map(function (question, index) {