@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.
- package/README.md +1 -1
- package/assets/hud/echo-pricing-free-sticker.png +0 -0
- package/assets/hud/echo-pricing-power-sticker.png +0 -0
- package/assets/hud/echo-pricing-pro-sticker.png +0 -0
- package/assets/hud/scan-loop/frame-01-ready.png +0 -0
- package/assets/hud/scan-loop/frame-02-open.png +0 -0
- package/assets/hud/scan-loop/frame-03-pull.png +0 -0
- package/assets/hud/scan-loop/frame-04-read.png +0 -0
- package/assets/hud/scan-loop/frame-05-file.png +0 -0
- package/assets/hud/scan-loop/frame-06-close.png +0 -0
- package/dist/crypto.js +4 -0
- package/dist/encryption.js +14 -2
- package/dist/index.js +52 -117
- package/dist/migrate.js +38 -1
- package/dist/setup-page/client-core.js +286 -26
- package/dist/setup-page/client-extraction.js +322 -101
- package/dist/setup-page/client-lifecycle.js +46 -44
- package/dist/setup-page/client-report-audit.js +1 -1
- package/dist/setup-page/client-report-city.js +35 -22
- package/dist/setup-page/document.js +1 -1
- package/dist/setup-page/styles-extraction.js +455 -0
- package/dist/setup-page/styles-mvp.js +976 -0
- package/dist/setup-page/styles-website-alignment.js +237 -5
- package/dist/setup-page/styles.js +2 -0
- package/dist/setup-page.js +1 -0
- package/dist/setup-preview.js +142 -37
- package/dist/setup.js +527 -61
- package/dist/v1-contract.js +1 -11
- package/package.json +6 -2
|
@@ -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
|
-
|
|
117
|
-
|
|
118
|
-
|
|
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
|
|
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 =
|
|
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 ?
|
|
276
|
-
migrateBtn.onclick = canExtract ?
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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 = "
|
|
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") + '>
|
|
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();
|
|
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>
|
|
799
|
-
'<button type="button" class="textButton setupAccountChange" id="changeSetupAccount">
|
|
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 = "
|
|
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
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
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
|
|
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’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
|
-
|
|
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
|
-
|
|
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 () {
|
|
1060
|
+
if (changeBtn) changeBtn.onclick = function () {
|
|
1061
|
+
rememberSetupPlanChoice("");
|
|
1062
|
+
renderDashboard();
|
|
1063
|
+
};
|
|
869
1064
|
renderSessionSelection(true);
|
|
870
1065
|
return;
|
|
871
1066
|
}
|
|
872
|
-
var
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
'</
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
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.
|
|
1106
|
+
if (billingStatus && billingStatus.plan === "unknown") setSetupPlanStatus("Plan check unavailable. Original still works.");
|
|
919
1107
|
} catch (_) {
|
|
920
|
-
setSetupPlanStatus("Plan check unavailable.
|
|
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
|
|
937
|
-
var
|
|
938
|
-
setSetupPlanStatus("Opening secure checkout…");
|
|
1124
|
+
async function openHostedBilling(path, body, openingMessage, openedMessage) {
|
|
1125
|
+
var opened = null;
|
|
939
1126
|
try {
|
|
940
|
-
|
|
1127
|
+
opened = window.open("about:blank", "_blank");
|
|
941
1128
|
if (!opened) {
|
|
942
|
-
setSetupPlanStatus("Your browser blocked
|
|
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
|
-
|
|
948
|
-
|
|
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) {
|