@echomem/mcp 1.4.26 → 1.4.27
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/context-analysis/vendored-canonical.js +54 -11
- package/dist/forensics.js +24 -4
- package/dist/hud/cli.js +18 -1
- package/dist/hud/hooks.js +59 -0
- package/dist/index.js +111 -16
- package/dist/package-metadata.js +5 -3
- package/dist/save-checkpoint-hook.js +111 -0
- package/dist/setup-page/client-core.js +86 -15
- package/dist/setup-page/client-extraction.js +313 -145
- package/dist/setup-page/client-lifecycle.js +10 -11
- package/dist/setup-page/client-report-audit.js +3 -2
- package/dist/setup-page/client-report-city.js +36 -40
- package/dist/setup-page/styles-foundation.js +3 -1
- package/dist/setup-page/styles-mvp.js +568 -150
- package/dist/setup-preview.js +46 -6
- package/dist/setup.js +143 -10
- package/dist/v1-contract.js +39 -6
- package/package.json +3 -2
- package/templates/codex-skills/echomem-save/SKILL.md +10 -0
- package/templates/echomem-recall.md +10 -2
|
@@ -48,30 +48,31 @@ export const SETUP_PAGE_CLIENT_EXTRACTION = String.raw ` /* ---------- dash
|
|
|
48
48
|
return envelope;
|
|
49
49
|
}
|
|
50
50
|
function renderReportIssue(code, message) {
|
|
51
|
+
void message;
|
|
51
52
|
report = null;
|
|
52
53
|
reportEnvelope = null;
|
|
53
54
|
resetReportSurface();
|
|
54
|
-
setHead("
|
|
55
|
-
app.className = "
|
|
55
|
+
setHead("We couldn't finish this scan", "Needs attention");
|
|
56
|
+
app.className = "reportMessageStage";
|
|
56
57
|
app.innerHTML =
|
|
57
|
-
'<
|
|
58
|
-
'<h2>
|
|
59
|
-
'<p
|
|
60
|
-
'<
|
|
61
|
-
|
|
58
|
+
'<section class="reportMessage" data-report-state="failed">' +
|
|
59
|
+
'<h2>We couldn’t finish this scan.</h2>' +
|
|
60
|
+
'<p>Your coding history is unchanged.</p>' +
|
|
61
|
+
'<p>Close this tab and run <code>echomem-mcp init</code> again.</p>' +
|
|
62
|
+
'<details class="reportTechnical"><summary>Technical details</summary><code>' + esc(code || "REPORT_FAILED") + '</code></details>' +
|
|
63
|
+
'</section>';
|
|
62
64
|
}
|
|
63
65
|
function renderEmptyReport() {
|
|
64
66
|
report = null;
|
|
65
67
|
reportEnvelope = null;
|
|
66
68
|
resetReportSurface();
|
|
67
69
|
setHead("No local coding history found", "Local scan complete");
|
|
68
|
-
app.className = "
|
|
70
|
+
app.className = "reportMessageStage";
|
|
69
71
|
app.innerHTML =
|
|
70
|
-
'<
|
|
71
|
-
'<h2>
|
|
72
|
-
'<p
|
|
73
|
-
|
|
74
|
-
'</div>';
|
|
72
|
+
'<section class="reportMessage" data-report-state="empty">' +
|
|
73
|
+
'<h2>No coding history found.</h2>' +
|
|
74
|
+
'<p>Start a Codex or Claude Code session, then run <code>echomem-mcp init</code> again.</p>' +
|
|
75
|
+
'</section>';
|
|
75
76
|
}
|
|
76
77
|
function renderBridgeIssue() {
|
|
77
78
|
renderReportIssue("BRIDGE_UNREACHABLE", "The local bridge stopped answering before your report was ready. Your terminal may have closed or the process may have stopped.");
|
|
@@ -153,6 +154,7 @@ export const SETUP_PAGE_CLIENT_EXTRACTION = String.raw ` /* ---------- dash
|
|
|
153
154
|
readyStage = "plan";
|
|
154
155
|
app.className = "planGateStage";
|
|
155
156
|
app.innerHTML =
|
|
157
|
+
setupJourney(3) +
|
|
156
158
|
'<section class="planGateDecision">' +
|
|
157
159
|
'<div class="setupPlanSlot" id="setupPlan"></div>' +
|
|
158
160
|
'<aside class="planGateAccount" aria-label="Connected account"><div class="setupAccountSlot" id="setupAccount"></div></aside>' +
|
|
@@ -193,12 +195,12 @@ export const SETUP_PAGE_CLIENT_EXTRACTION = String.raw ` /* ---------- dash
|
|
|
193
195
|
? "Counting your <strong>new conversations…</strong>"
|
|
194
196
|
: (knownDone
|
|
195
197
|
? "You are <strong>all caught up.</strong>"
|
|
196
|
-
: "
|
|
198
|
+
: "<strong>" + esc(number(pendN)) + " sessions</strong> are ready to review.");
|
|
197
199
|
var sub = !pendingTrusted
|
|
198
200
|
? "Echo is matching your local history against what is already in memory."
|
|
199
201
|
: (knownDone
|
|
200
202
|
? (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.")
|
|
201
|
-
: "
|
|
203
|
+
: "Nothing uploads until you confirm.");
|
|
202
204
|
var extractLabel = !pendingTrusted ? "Counting…" : (canExtract ? "Start memory extraction" : "All caught up");
|
|
203
205
|
// Mount the dashboard shell ONCE so the WebGL plate iframe is never re-created across /stats polls.
|
|
204
206
|
// (Rebuilding app.innerHTML on every partial-scan poll reloaded the iframe → the city flickered.)
|
|
@@ -214,9 +216,9 @@ export const SETUP_PAGE_CLIENT_EXTRACTION = String.raw ` /* ---------- dash
|
|
|
214
216
|
// one-line strip, not stat cards; the ETA is a commitment-moment note under the button,
|
|
215
217
|
// not a display-size headline.
|
|
216
218
|
app.innerHTML =
|
|
219
|
+
setupJourney(4) +
|
|
217
220
|
'<div id="exBanner"></div>' +
|
|
218
221
|
'<section class="readyDecision extractionReady">' +
|
|
219
|
-
'<p class="utilityEyebrow readyEyebrow">History import / review</p>' +
|
|
220
222
|
'<div class="readyHero">' +
|
|
221
223
|
'<img class="readyMascot" src="/hud-assets/echo-face-cutout.png" alt="" />' +
|
|
222
224
|
'<div class="exCopy">' +
|
|
@@ -225,10 +227,10 @@ export const SETUP_PAGE_CLIENT_EXTRACTION = String.raw ` /* ---------- dash
|
|
|
225
227
|
'</div>' +
|
|
226
228
|
'</div>' +
|
|
227
229
|
'<div class="proofStrip" id="exSources"></div>' +
|
|
230
|
+
'<div class="billingCommitmentSlot" id="billingCommitment"></div>' +
|
|
228
231
|
'<div class="readyGo">' +
|
|
229
232
|
'<div class="actions">' +
|
|
230
233
|
'<button id="migrate" class="primary"></button>' +
|
|
231
|
-
'<button type="button" id="skipHistoryImport" class="secondary skipHistoryButton">Skip for now</button>' +
|
|
232
234
|
'</div>' +
|
|
233
235
|
'<p class="ctaMeta" id="exEta"></p>' +
|
|
234
236
|
'</div>' +
|
|
@@ -236,9 +238,6 @@ export const SETUP_PAGE_CLIENT_EXTRACTION = String.raw ` /* ---------- dash
|
|
|
236
238
|
'<div class="readySetting readyAccountSetting"><span class="readySettingLabel">Account</span><div class="setupAccountSlot" id="setupAccount"></div></div>' +
|
|
237
239
|
'<div class="readySetting readyPlanSetting"><span class="readySettingLabel">Plan</span><div class="setupPlanSlot" id="setupPlan"></div></div>' +
|
|
238
240
|
'</aside>' +
|
|
239
|
-
'<div class="readyFoot">' +
|
|
240
|
-
'<details class="readyExplain" id="readyExplain"></details>' +
|
|
241
|
-
'</div>' +
|
|
242
241
|
'</section>' +
|
|
243
242
|
'<dialog class="sessionPicker" id="sessionPicker" aria-labelledby="sessionPickerTitle"></dialog>';
|
|
244
243
|
document.getElementById("migrate").onclick = openSessionPicker;
|
|
@@ -251,7 +250,6 @@ export const SETUP_PAGE_CLIENT_EXTRACTION = String.raw ` /* ---------- dash
|
|
|
251
250
|
document.getElementById("exSub").textContent = sub;
|
|
252
251
|
var codexN = pendingTrusted && pendingCodex !== null ? pendingCodex : (sessions.codex || 0);
|
|
253
252
|
var claudeN = pendingTrusted && pendingClaudeCode !== null ? pendingClaudeCode : (sessions.claudeCode || 0);
|
|
254
|
-
var alreadyN = Math.max(0, (sessions.total || 0) - pendN);
|
|
255
253
|
// Proof strip: one quiet line of evidence that the scan is real. Hidden when there is nothing to show.
|
|
256
254
|
var srcIcon = function (id, fallback) {
|
|
257
255
|
var asset = id === "claude-desktop" ? "claude" : "codex";
|
|
@@ -268,59 +266,54 @@ export const SETUP_PAGE_CLIENT_EXTRACTION = String.raw ` /* ---------- dash
|
|
|
268
266
|
// Reassurances live at the moment of commitment — right under the button.
|
|
269
267
|
document.getElementById("exEta").innerHTML = pendingTrusted
|
|
270
268
|
? (canExtract
|
|
271
|
-
? '<span class="cm">≈ ' + esc(etaLabel) + '</span
|
|
269
|
+
? '<span class="cm">≈ ' + esc(etaLabel) + '</span>'
|
|
272
270
|
: 'Nothing new to extract right now.')
|
|
273
271
|
: 'Echo is checking local history against saved memory…';
|
|
274
|
-
document.getElementById("readyExplain").innerHTML =
|
|
275
|
-
'<summary>What exactly happens?</summary>' +
|
|
276
|
-
'<p>A coding session is a local Codex or Claude Code transcript from this Mac. Echo extracts compact memories from it, not a full chat replay.</p>' +
|
|
277
|
-
'<div class="readySteps">' +
|
|
278
|
-
'<div class="readyStep"><strong>Read locally</strong><span>Echo scans session logs on this device.</span></div>' +
|
|
279
|
-
'<div class="readyStep"><strong>Skip repeats</strong><span>Anything already in memory is ignored.</span></div>' +
|
|
280
|
-
'<div class="readyStep"><strong>Save memory</strong><span>Decisions, fixes, context, and next steps become searchable.</span></div>' +
|
|
281
|
-
'</div>';
|
|
282
272
|
renderSetupPlan(canExtract);
|
|
283
273
|
renderAccountChoice();
|
|
284
274
|
if (canExtract && !billingStatus && !billingStatusLoading) void refreshBillingStatus();
|
|
285
275
|
// While still counting, hide the primary button entirely; the headline + source split
|
|
286
276
|
// already say it is working, and the ready state should present one clear action.
|
|
287
277
|
var migrateBtn = document.getElementById("migrate");
|
|
288
|
-
var skipHistoryBtn = document.getElementById("skipHistoryImport");
|
|
289
278
|
if (pendingTrusted) {
|
|
290
279
|
migrateBtn.style.display = "";
|
|
291
280
|
if (canExtract) ensureSessionSelection();
|
|
292
281
|
migrateBtn.disabled = canExtract && (!setupPlanConfirmed() || selectedSessionKeyList().length === 0);
|
|
293
282
|
migrateBtn.innerHTML = '<span>' + esc(canExtract ? "Review " + number(pendN) + " sessions" : "Finish setup") + '</span>' + (canExtract ? setupIcon("arrow-right") : "");
|
|
294
283
|
migrateBtn.onclick = canExtract ? openSessionPicker : skip;
|
|
295
|
-
if (skipHistoryBtn) {
|
|
296
|
-
skipHistoryBtn.style.display = canExtract ? "" : "none";
|
|
297
|
-
skipHistoryBtn.onclick = skip;
|
|
298
|
-
}
|
|
299
284
|
} else {
|
|
300
285
|
migrateBtn.style.display = "none";
|
|
301
|
-
if (skipHistoryBtn) skipHistoryBtn.style.display = "none";
|
|
302
286
|
}
|
|
303
287
|
}
|
|
288
|
+
// The picker's confirm button checks this BEFORE closing the dialog. Closing first turned a
|
|
289
|
+
// blocked start into a silent bounce back to the dashboard — or all the way to the plan gate,
|
|
290
|
+
// because a billing refresh can flip setupPlanConfirmed() while the picker is open.
|
|
291
|
+
function migrateBlockedReason() {
|
|
292
|
+
if (!setupPlanConfirmed()) return "Choose Original, Pro, or Power to bring Echo home.";
|
|
293
|
+
if (!selectedSessionKeyList().length) return "Choose at least one conversation to extract.";
|
|
294
|
+
return "";
|
|
295
|
+
}
|
|
296
|
+
function setSessionPickerStatus(message) {
|
|
297
|
+
var status = document.getElementById("sessionPickerStatus");
|
|
298
|
+
if (status) status.textContent = message;
|
|
299
|
+
}
|
|
304
300
|
async function startMigrate() {
|
|
305
301
|
if (decisionMade) return;
|
|
306
|
-
|
|
307
|
-
|
|
302
|
+
var blocked = migrateBlockedReason();
|
|
303
|
+
if (blocked) {
|
|
304
|
+
if (!setupPlanConfirmed()) setSetupPlanStatus(blocked);
|
|
305
|
+
else openSessionPicker();
|
|
306
|
+
setSessionPickerStatus(blocked);
|
|
308
307
|
return;
|
|
309
308
|
}
|
|
310
309
|
var selectedKeys = selectedSessionKeyList();
|
|
311
|
-
if (!selectedKeys.length) {
|
|
312
|
-
openSessionPicker();
|
|
313
|
-
var pickerStatus = document.getElementById("sessionPickerStatus");
|
|
314
|
-
if (pickerStatus) pickerStatus.textContent = "Choose at least one conversation to extract.";
|
|
315
|
-
return;
|
|
316
|
-
}
|
|
317
310
|
stopBillingPoll();
|
|
318
311
|
decisionMade = true;
|
|
319
312
|
extractionEnded = false;
|
|
320
313
|
setCityMode(false);
|
|
321
314
|
setHead("Extracting", "Starting");
|
|
322
315
|
app.className = "";
|
|
323
|
-
app.innerHTML = '<div class="miniLoad"><h2>Starting extraction
|
|
316
|
+
app.innerHTML = '<div class="miniLoad"><h2>Starting extraction…</h2><div class="miniBar"><i></i></div></div>';
|
|
324
317
|
var started;
|
|
325
318
|
try {
|
|
326
319
|
started = await postJson("/migrate", { conversationKeys: selectedKeys });
|
|
@@ -414,11 +407,14 @@ export const SETUP_PAGE_CLIENT_EXTRACTION = String.raw ` /* ---------- dash
|
|
|
414
407
|
// each poll only rebuilds the text column.
|
|
415
408
|
if (!extractMounted) {
|
|
416
409
|
app.innerHTML =
|
|
410
|
+
'<div id="setupJourneySlot">' + setupJourney(4) + '</div>' +
|
|
417
411
|
'<section class="exHero"><div class="exCopy" id="exText"></div>' +
|
|
418
412
|
plateBlock() +
|
|
419
413
|
'</section>';
|
|
420
414
|
extractMounted = true;
|
|
421
415
|
}
|
|
416
|
+
var journeySlot = document.getElementById("setupJourneySlot");
|
|
417
|
+
if (journeySlot) journeySlot.innerHTML = setupJourney(done ? 5 : 4);
|
|
422
418
|
var shell = app.querySelector(".exHero");
|
|
423
419
|
if (shell) {
|
|
424
420
|
shell.classList.toggle("exHeroResult", done || stopped);
|
|
@@ -439,27 +435,18 @@ export const SETUP_PAGE_CLIENT_EXTRACTION = String.raw ` /* ---------- dash
|
|
|
439
435
|
: "";
|
|
440
436
|
t.innerHTML =
|
|
441
437
|
'<div class="resultStage ' + (done ? "ok" : "warn") + '">' +
|
|
442
|
-
(done ? '<p class="utilityEyebrow">Memory extraction / complete</p>' : '<p class="utilityEyebrow">Memory extraction / stopped</p>') +
|
|
443
438
|
'<div class="resultTop">' +
|
|
444
439
|
'<div>' +
|
|
445
440
|
(done
|
|
446
441
|
? '<h2 class="savedHeading siteHeadline"><span class="savedMain"><strong>' + esc(number(extracted)) + '</strong> ' + esc(savedMemoryLabel) + '</span></h2>' + completionNote
|
|
447
442
|
: '<h2 class="siteHeadline">Some conversations need <strong>another pass.</strong></h2>' +
|
|
448
|
-
'<p class="exSub">' + stopDetail + '</p>' +
|
|
449
|
-
'<div class="resultNumber"><strong>' + esc(number(extracted)) + '</strong><span>' + esc(memoryNoun) + '</span></div>') +
|
|
443
|
+
'<p class="exSub">' + stopDetail + '</p>') +
|
|
450
444
|
'</div>' +
|
|
451
445
|
'</div>' +
|
|
452
446
|
(done ? tryItCard() : "") +
|
|
453
|
-
(done ? "" :
|
|
454
|
-
'<div class="memoryReceipt">' +
|
|
455
|
-
'<p class="receiptK">What just happened</p>' +
|
|
456
|
-
'<div class="finishStats">' +
|
|
457
|
-
'<div class="finishStat"><strong>' + esc(number(completed)) + '</strong><span>' + esc(conversationNoun) + ' extracted</span></div>' +
|
|
458
|
-
'<div class="finishStat"><strong>' + esc(number(extracted)) + '</strong><span>memories ready</span></div>' +
|
|
459
|
-
'</div>' +
|
|
460
|
-
'</div>') +
|
|
447
|
+
(done ? "" : '<p class="resultSummary">' + esc(number(extracted)) + ' ' + esc(memoryNoun) + ' saved · ' + esc(number(completed)) + ' ' + esc(conversationNoun) + ' processed</p>') +
|
|
461
448
|
'</div>' +
|
|
462
|
-
|
|
449
|
+
"";
|
|
463
450
|
wireTryButtons();
|
|
464
451
|
return;
|
|
465
452
|
}
|
|
@@ -480,10 +467,9 @@ export const SETUP_PAGE_CLIENT_EXTRACTION = String.raw ` /* ---------- dash
|
|
|
480
467
|
if (completed >= 5 && elapsed > 20) remainSec = (total - completed) / (completed / elapsed);
|
|
481
468
|
else remainSec = Math.max(0, (exDeadline - now) / 1000);
|
|
482
469
|
if (now > exDeadline) {
|
|
483
|
-
overdueHtml = '<div class="exOverdue"><strong>Taking longer than expected
|
|
484
|
-
'Everything already processed is saved. Ending now is safe; the next run resumes from the remaining conversations.</div>';
|
|
470
|
+
overdueHtml = '<div class="exOverdue"><strong>Taking longer than expected.</strong> Saved work is safe.</div>';
|
|
485
471
|
} else {
|
|
486
|
-
countdownHtml = '<p class="extractFacts"><strong>' + esc(fmtCountdown(remainSec)) + '</strong> left · saved as it goes
|
|
472
|
+
countdownHtml = '<p class="extractFacts"><strong>' + esc(fmtCountdown(remainSec)) + '</strong> left · saved as it goes</p>';
|
|
487
473
|
}
|
|
488
474
|
}
|
|
489
475
|
var recoveryNotice = extractionRecoveryMessage
|
|
@@ -491,15 +477,13 @@ export const SETUP_PAGE_CLIENT_EXTRACTION = String.raw ` /* ---------- dash
|
|
|
491
477
|
: "";
|
|
492
478
|
t.innerHTML = recoveryNotice +
|
|
493
479
|
'<div class="workbench">' +
|
|
494
|
-
'<p class="utilityEyebrow">Memory extraction / live</p>' +
|
|
495
480
|
'<h2 class="siteHeadline">' + (preparing ? "Setting up the <strong>import.</strong>" : "Building your <strong>memory.</strong>") + '</h2>' +
|
|
496
481
|
'<div class="exMeter"><strong>' + (preparing ? "Local" : esc(number(pct)) + "%") + '</strong><span>' + (preparing ? "sizing sessions" : (total ? esc(number(completed)) + " of " + esc(number(total)) + " conversations" : "starting")) + '</span></div>' +
|
|
497
482
|
'<div class="track' + (preparing ? " indef" : "") + '"><div class="fill"' + (preparing ? "" : ' style="width:' + pct + '%"') + '></div></div>' +
|
|
498
|
-
'<p class="exSub">' + esc(number(extracted)) + ' ' + (extracted === 1 ? "memory" : "memories") +
|
|
499
|
-
'<
|
|
483
|
+
'<p class="exSub">' + esc(number(extracted)) + ' ' + (extracted === 1 ? "memory saved" : "memories saved") + (checkingEdgeCases ? ' · finishing checks' : "") + '.</p>' +
|
|
484
|
+
(String(statusLine).toLowerCase().indexOf("reconnecting") >= 0 ? '<p class="exActivity">' + esc(statusLine) + '</p>' : '') +
|
|
500
485
|
countdownHtml + overdueHtml +
|
|
501
|
-
'<div class="extractEndRow"><button type="button" class="secondary" id="endExtraction">End for now</button
|
|
502
|
-
'<p class="exHint">You can step away from this page. Echo saves each finished conversation as it goes.</p>' +
|
|
486
|
+
'<div class="extractEndRow"><button type="button" class="secondary" id="endExtraction">End for now</button></div>' +
|
|
503
487
|
'</div>';
|
|
504
488
|
var endBtn = document.getElementById("endExtraction");
|
|
505
489
|
if (endBtn) endBtn.onclick = endExtraction;
|
|
@@ -602,21 +586,50 @@ export const SETUP_PAGE_CLIENT_EXTRACTION = String.raw ` /* ---------- dash
|
|
|
602
586
|
}
|
|
603
587
|
async function tryInCodex(prompt) {
|
|
604
588
|
var copied = await copyRecallPrompt(prompt);
|
|
605
|
-
setLaunchStatus(copied ? "
|
|
589
|
+
setLaunchStatus(copied ? "Opening Codex with your first recall…" : "Opening Codex with your first recall…");
|
|
590
|
+
startFirstRecallWatch();
|
|
606
591
|
window.location.href = codexDeepLink(prompt);
|
|
607
592
|
}
|
|
608
593
|
async function openClaudeDesktop(prompt) {
|
|
609
594
|
var copied = await copyRecallPrompt(prompt);
|
|
610
|
-
setLaunchStatus(copied ? "
|
|
595
|
+
setLaunchStatus(copied ? "Question copied. Opening Claude Desktop for your first recall…" : "Opening Claude Desktop for your first recall…");
|
|
596
|
+
startFirstRecallWatch();
|
|
611
597
|
try {
|
|
612
598
|
var res = await postJson("/launch-agent", { agent: "claude-desktop" }, 8000);
|
|
613
|
-
setLaunchStatus(res && res.message ? res.message : "Question copied.
|
|
599
|
+
setLaunchStatus(res && res.message ? res.message + " Send it there; this page will confirm when Echo remembers." : "Question copied. Send it in Claude Desktop; this page will confirm when Echo remembers.");
|
|
614
600
|
} catch (error) {
|
|
615
601
|
var msg = error && error.message ? error.message : "Could not auto-open Claude.";
|
|
616
602
|
if (msg.indexOf("prompt") === -1 && msg.indexOf("Prompt") === -1) msg += " The question is copied - open Claude Desktop and paste it.";
|
|
617
603
|
setLaunchStatus(msg);
|
|
618
604
|
}
|
|
619
605
|
}
|
|
606
|
+
function stopFirstRecallWatch() {
|
|
607
|
+
if (firstRecallPollTimer) window.clearInterval(firstRecallPollTimer);
|
|
608
|
+
firstRecallPollTimer = null;
|
|
609
|
+
}
|
|
610
|
+
async function checkFirstRecallActivation() {
|
|
611
|
+
if (!firstRecallPollStartedAt || Date.now() - firstRecallPollStartedAt > 120000) {
|
|
612
|
+
stopFirstRecallWatch();
|
|
613
|
+
return;
|
|
614
|
+
}
|
|
615
|
+
try {
|
|
616
|
+
var latest = await getJson("/billing-status");
|
|
617
|
+
if (latest && latest.activation && latest.activation.hasSearchedMemory === true) {
|
|
618
|
+
stopFirstRecallWatch();
|
|
619
|
+
document.title = "Echo remembered it — setup complete";
|
|
620
|
+
var card = document.querySelector(".tryCard");
|
|
621
|
+
if (card) card.classList.add("is-recalled");
|
|
622
|
+
setLaunchStatus("Echo remembered it. Setup complete.");
|
|
623
|
+
}
|
|
624
|
+
} catch (_) {}
|
|
625
|
+
}
|
|
626
|
+
function startFirstRecallWatch() {
|
|
627
|
+
firstRecallPollStartedAt = Date.now();
|
|
628
|
+
stopFirstRecallWatch();
|
|
629
|
+
firstRecallPollTimer = window.setInterval(function () {
|
|
630
|
+
void checkFirstRecallActivation();
|
|
631
|
+
}, 2500);
|
|
632
|
+
}
|
|
620
633
|
function candidateSessions() {
|
|
621
634
|
return stats && Array.isArray(stats.candidateSessions) ? stats.candidateSessions : [];
|
|
622
635
|
}
|
|
@@ -792,20 +805,23 @@ export const SETUP_PAGE_CLIENT_EXTRACTION = String.raw ` /* ---------- dash
|
|
|
792
805
|
: "";
|
|
793
806
|
dialog.innerHTML =
|
|
794
807
|
'<div class="sessionPickerShell">' +
|
|
795
|
-
'<header class="sessionPickerHead"><div><
|
|
808
|
+
'<header class="sessionPickerHead"><div><h2 id="sessionPickerTitle">Choose conversations</h2><p>Up to ' + esc(number(limit)) + ' · newest selected first</p></div><button type="button" class="sessionPickerClose" id="closeSessionPicker" aria-label="Close">×</button></header>' +
|
|
796
809
|
'<div class="sessionPickerToolbar"><label class="sessionSearch"><span>Search</span><input type="search" id="sessionPickerSearch" placeholder="Title or project" value="' + esc(sessionPickerQuery) + '" /></label>' +
|
|
797
810
|
'<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>' +
|
|
798
811
|
'</div>' +
|
|
799
812
|
'<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>' +
|
|
800
813
|
'<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>' +
|
|
801
|
-
'<footer class="sessionPickerFoot"><
|
|
814
|
+
'<footer class="sessionPickerFoot"><div class="sessionPickerFootActions"><button type="button" class="primary" id="confirmSessions"' + (selected.length ? "" : " disabled") + '>Start memory extraction</button></div></footer>' +
|
|
802
815
|
'</div>';
|
|
803
816
|
var close = document.getElementById("closeSessionPicker");
|
|
804
817
|
var confirm = document.getElementById("confirmSessions");
|
|
805
|
-
var skipImport = document.getElementById("skipSessionImport");
|
|
806
818
|
if (close) close.onclick = function () { dialog.close(); renderSessionSelection(true); };
|
|
807
|
-
if (confirm) confirm.onclick = function () {
|
|
808
|
-
|
|
819
|
+
if (confirm) confirm.onclick = function () {
|
|
820
|
+
var blocked = migrateBlockedReason();
|
|
821
|
+
if (blocked) { setSessionPickerStatus(blocked); return; }
|
|
822
|
+
dialog.close();
|
|
823
|
+
void startMigrate();
|
|
824
|
+
};
|
|
809
825
|
wireSessionPickerUpgrade();
|
|
810
826
|
var search = document.getElementById("sessionPickerSearch");
|
|
811
827
|
if (search) search.oninput = function () {
|
|
@@ -872,13 +888,42 @@ export const SETUP_PAGE_CLIENT_EXTRACTION = String.raw ` /* ---------- dash
|
|
|
872
888
|
'<button type="button" class="textButton setupAccountChange" id="changeSetupAccount">Switch</button>' +
|
|
873
889
|
'</section>';
|
|
874
890
|
var change = document.getElementById("changeSetupAccount");
|
|
875
|
-
if (change) change.onclick =
|
|
891
|
+
if (change) change.onclick = renderAccountSwitchConfirm;
|
|
892
|
+
}
|
|
893
|
+
function renderAccountSwitchConfirm() {
|
|
894
|
+
var account = billingStatus && billingStatus.account;
|
|
895
|
+
var accountLabel = account && (account.email || account.displayName)
|
|
896
|
+
? (account.email || account.displayName)
|
|
897
|
+
: "the connected account";
|
|
898
|
+
accountSwitchPending = true;
|
|
899
|
+
setCityMode(false);
|
|
900
|
+
setExtractMode(false);
|
|
901
|
+
setReadyMode(true);
|
|
902
|
+
setHead("Switch EchoMem account", "Confirm");
|
|
903
|
+
app.className = "accountSwitchStage";
|
|
904
|
+
app.innerHTML =
|
|
905
|
+
setupJourney(4) +
|
|
906
|
+
'<section class="accountSwitchConfirm" aria-labelledby="accountSwitchTitle">' +
|
|
907
|
+
'<button type="button" class="flowBack" id="cancelAccountSwitch">← Back</button>' +
|
|
908
|
+
'<h2 id="accountSwitchTitle">Switch account?</h2>' +
|
|
909
|
+
'<p><strong>' + esc(accountLabel) + '</strong> stays connected until you continue.</p>' +
|
|
910
|
+
'<button type="button" class="primary" id="confirmAccountSwitch">Sign out & switch</button>' +
|
|
911
|
+
'<p class="localAuthStatus" id="localAuthStatus" role="status" aria-live="polite"></p>' +
|
|
912
|
+
'</section>';
|
|
913
|
+
var cancel = document.getElementById("cancelAccountSwitch");
|
|
914
|
+
if (cancel) cancel.onclick = function () {
|
|
915
|
+
accountSwitchPending = false;
|
|
916
|
+
renderDashboard();
|
|
917
|
+
};
|
|
918
|
+
var confirm = document.getElementById("confirmAccountSwitch");
|
|
919
|
+
if (confirm) confirm.onclick = function () { void switchSetupAccount(); };
|
|
876
920
|
}
|
|
877
921
|
async function switchSetupAccount() {
|
|
878
|
-
var button = document.getElementById("
|
|
922
|
+
var button = document.getElementById("confirmAccountSwitch");
|
|
879
923
|
if (button) { button.disabled = true; button.textContent = "Signing out..."; }
|
|
880
924
|
try {
|
|
881
925
|
await postJson("/logout", {}, 12000);
|
|
926
|
+
accountSwitchPending = false;
|
|
882
927
|
stats = null;
|
|
883
928
|
billingStatus = null;
|
|
884
929
|
connected = false;
|
|
@@ -889,13 +934,89 @@ export const SETUP_PAGE_CLIENT_EXTRACTION = String.raw ` /* ---------- dash
|
|
|
889
934
|
localAuthAgeConfirmed = false;
|
|
890
935
|
renderLocalLogin("");
|
|
891
936
|
} catch (error) {
|
|
892
|
-
if (button) { button.disabled = false; button.textContent = "
|
|
893
|
-
|
|
937
|
+
if (button) { button.disabled = false; button.textContent = "Sign out & switch"; }
|
|
938
|
+
setLocalAuthStatus(error && error.message ? error.message : "Could not switch accounts.", "error");
|
|
894
939
|
}
|
|
895
940
|
}
|
|
896
941
|
function paidRecallPlan(plan) {
|
|
897
942
|
return ["pro", "power", "team", "enterprise"].indexOf(String(plan || "").toLowerCase()) >= 0;
|
|
898
943
|
}
|
|
944
|
+
function setupPlanPrice(plan) {
|
|
945
|
+
return plan === "power" ? "$100" : "$20";
|
|
946
|
+
}
|
|
947
|
+
function formatBillingDate(value) {
|
|
948
|
+
if (typeof value !== "string" || !value) return "";
|
|
949
|
+
var parsed = new Date(value);
|
|
950
|
+
if (!isFinite(parsed.getTime())) return "";
|
|
951
|
+
try {
|
|
952
|
+
return new Intl.DateTimeFormat(undefined, {
|
|
953
|
+
year: "numeric",
|
|
954
|
+
month: "long",
|
|
955
|
+
day: "numeric"
|
|
956
|
+
}).format(parsed);
|
|
957
|
+
} catch (_) {
|
|
958
|
+
return parsed.toISOString().slice(0, 10);
|
|
959
|
+
}
|
|
960
|
+
}
|
|
961
|
+
function trialChargeDateText() {
|
|
962
|
+
var chargeAt = new Date(Date.now() + 14 * 24 * 60 * 60 * 1000);
|
|
963
|
+
try {
|
|
964
|
+
return new Intl.DateTimeFormat(undefined, {
|
|
965
|
+
year: "numeric",
|
|
966
|
+
month: "long",
|
|
967
|
+
day: "numeric"
|
|
968
|
+
}).format(chargeAt);
|
|
969
|
+
} catch (_) {
|
|
970
|
+
return chargeAt.toISOString().slice(0, 10);
|
|
971
|
+
}
|
|
972
|
+
}
|
|
973
|
+
function paidPlanLifecycleText(plan) {
|
|
974
|
+
var state = String(billingStatus && billingStatus.subscriptionStatus || "").toLowerCase();
|
|
975
|
+
var trialEnd = formatBillingDate(billingStatus && billingStatus.trialEndedAt);
|
|
976
|
+
var periodEnd = formatBillingDate(billingStatus && billingStatus.currentPeriodEnd);
|
|
977
|
+
if (billingStatus && billingStatus.cancelAtPeriodEnd === true) {
|
|
978
|
+
var accessEnd = state === "trialing" ? (trialEnd || periodEnd) : periodEnd;
|
|
979
|
+
return accessEnd
|
|
980
|
+
? "Canceled — active until " + accessEnd + ". It will not renew."
|
|
981
|
+
: "Canceled — active until the end of this billing period. It will not renew.";
|
|
982
|
+
}
|
|
983
|
+
if (state === "trialing") {
|
|
984
|
+
return trialEnd
|
|
985
|
+
? "Trial active until " + trialEnd + ". Then " + setupPlanPrice(plan) + "/month unless canceled."
|
|
986
|
+
: "Trial active. Then " + setupPlanPrice(plan) + "/month unless canceled.";
|
|
987
|
+
}
|
|
988
|
+
if (state === "active" && periodEnd) return "Active — renews on " + periodEnd + ".";
|
|
989
|
+
return "";
|
|
990
|
+
}
|
|
991
|
+
function renderBillingCommitment(plan) {
|
|
992
|
+
var slot = document.getElementById("billingCommitment");
|
|
993
|
+
if (!slot) return;
|
|
994
|
+
if (!paidRecallPlan(plan)) {
|
|
995
|
+
slot.innerHTML = "";
|
|
996
|
+
slot.classList.remove("is-visible");
|
|
997
|
+
return;
|
|
998
|
+
}
|
|
999
|
+
var planLabel = plan.charAt(0).toUpperCase() + plan.slice(1);
|
|
1000
|
+
var state = String(billingStatus && billingStatus.subscriptionStatus || "").toLowerCase();
|
|
1001
|
+
var canceled = !!(billingStatus && billingStatus.cancelAtPeriodEnd === true);
|
|
1002
|
+
var statusLabel = canceled ? "Canceled" : (state === "trialing" ? "Trial active" : "Active");
|
|
1003
|
+
var lifecycle = paidPlanLifecycleText(plan);
|
|
1004
|
+
var quota = billingStatus && billingStatus.historicalConversationQuota;
|
|
1005
|
+
var limit = quota && typeof quota.limit === "number" ? quota.limit : (plan === "power" ? 2000 : 500);
|
|
1006
|
+
var moneyFact = canceled
|
|
1007
|
+
? "No future charge"
|
|
1008
|
+
: (state === "trialing" ? setupPlanPrice(plan) + "/month after trial" : setupPlanPrice(plan) + "/month");
|
|
1009
|
+
slot.classList.add("is-visible");
|
|
1010
|
+
slot.innerHTML =
|
|
1011
|
+
'<section class="billingCommitment' + (canceled ? ' is-canceled' : '') + '" aria-label="Current plan commitment">' +
|
|
1012
|
+
'<div class="billingCommitmentIdentity"><span>' + esc(statusLabel) + '</span><strong>' + esc(planLabel) + '</strong></div>' +
|
|
1013
|
+
'<div class="billingCommitmentFacts">' +
|
|
1014
|
+
'<span><b>' + esc(number(limit)) + '</b> coding-session imports</span>' +
|
|
1015
|
+
'<span><b>' + esc(moneyFact) + '</b></span>' +
|
|
1016
|
+
'</div>' +
|
|
1017
|
+
'<p>' + esc(lifecycle || "Your paid plan is active.") + '</p>' +
|
|
1018
|
+
'</section>';
|
|
1019
|
+
}
|
|
899
1020
|
function setupPricingUrl(plan, startTrial) {
|
|
900
1021
|
var paidPlan = plan === "power" ? "power" : "pro";
|
|
901
1022
|
return (billingStatus && billingStatus.pricingUrl)
|
|
@@ -906,16 +1027,21 @@ export const SETUP_PAGE_CLIENT_EXTRACTION = String.raw ` /* ---------- dash
|
|
|
906
1027
|
return setupPlanChoice === "free" || paidRecallPlan(billingStatus && billingStatus.plan);
|
|
907
1028
|
}
|
|
908
1029
|
function setupPlanDefinition(plan) {
|
|
1030
|
+
var trialAvailable = !billingStatus || billingStatus.trialAvailable !== false;
|
|
909
1031
|
if (plan === "power") return {
|
|
910
1032
|
id: "power",
|
|
911
1033
|
name: "Power",
|
|
912
1034
|
homeName: "Power",
|
|
913
1035
|
promise: "More room. More agents. Much more Echo.",
|
|
914
1036
|
price: "$100",
|
|
915
|
-
cadence: "per month · 14-day trial",
|
|
1037
|
+
cadence: trialAvailable ? "per month · 14-day trial" : "per month · billed immediately",
|
|
916
1038
|
sticker: "/hud-assets/echo-pricing-power-sticker.png",
|
|
917
1039
|
stickerAlt: "Power Echo arrives with a gold key and a crew of notebook helpers.",
|
|
918
|
-
features: [
|
|
1040
|
+
features: [
|
|
1041
|
+
"2,000 coding sessions",
|
|
1042
|
+
"agent-heavy memory",
|
|
1043
|
+
"2,000 memory recalls / week"
|
|
1044
|
+
]
|
|
919
1045
|
};
|
|
920
1046
|
if (plan === "pro") return {
|
|
921
1047
|
id: "pro",
|
|
@@ -923,11 +1049,15 @@ export const SETUP_PAGE_CLIENT_EXTRACTION = String.raw ` /* ---------- dash
|
|
|
923
1049
|
homeName: "Pro",
|
|
924
1050
|
promise: "A room for the work you return to every day.",
|
|
925
1051
|
price: "$20",
|
|
926
|
-
cadence: "per month · 14-day trial",
|
|
1052
|
+
cadence: trialAvailable ? "per month · 14-day trial" : "per month · billed immediately",
|
|
927
1053
|
sticker: "/hud-assets/echo-pricing-pro-sticker.png",
|
|
928
1054
|
stickerAlt: "Pro Echo organizes tabbed notebooks and a daily refresh control.",
|
|
929
1055
|
popular: true,
|
|
930
|
-
features: [
|
|
1056
|
+
features: [
|
|
1057
|
+
"500 coding sessions",
|
|
1058
|
+
"daily memory updates",
|
|
1059
|
+
"500 memory recalls / week"
|
|
1060
|
+
]
|
|
931
1061
|
};
|
|
932
1062
|
return {
|
|
933
1063
|
id: "free",
|
|
@@ -938,19 +1068,12 @@ export const SETUP_PAGE_CLIENT_EXTRACTION = String.raw ` /* ---------- dash
|
|
|
938
1068
|
cadence: "free forever · no card needed",
|
|
939
1069
|
sticker: "/hud-assets/echo-pricing-free-sticker.png",
|
|
940
1070
|
stickerAlt: "Original Echo hugs one simple memory card.",
|
|
941
|
-
features: ["100
|
|
1071
|
+
features: ["100 coding sessions", "a few new sessions / week", "100 memory recalls / week"]
|
|
942
1072
|
};
|
|
943
1073
|
}
|
|
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
1074
|
function renderPlanHabitat() {
|
|
951
1075
|
setupPlanPreview = setupPlanPreview === "pro" || setupPlanPreview === "power" ? setupPlanPreview : "free";
|
|
952
1076
|
var plan = setupPlanDefinition(setupPlanPreview);
|
|
953
|
-
var discovered = setupDiscoveredSessionCount();
|
|
954
1077
|
var tabs = ["free", "pro", "power"].map(function (id) {
|
|
955
1078
|
var tab = setupPlanDefinition(id);
|
|
956
1079
|
var selected = id === plan.id;
|
|
@@ -960,12 +1083,13 @@ export const SETUP_PAGE_CLIENT_EXTRACTION = String.raw ` /* ---------- dash
|
|
|
960
1083
|
return '<li>' + setupIcon("check") + '<span>' + esc(feature) + '</span></li>';
|
|
961
1084
|
}).join("");
|
|
962
1085
|
var chooseId = plan.id === "free" ? "chooseFreePlan" : (plan.id === "power" ? "choosePowerPlan" : "chooseProPlan");
|
|
963
|
-
var chooseLabel = plan.id === "free"
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
1086
|
+
var chooseLabel = plan.id === "free"
|
|
1087
|
+
? "Continue with " + plan.homeName + " — Free"
|
|
1088
|
+
: ((!billingStatus || billingStatus.trialAvailable !== false)
|
|
1089
|
+
? "Start " + plan.homeName + " trial — $0 today"
|
|
1090
|
+
: "Subscribe to " + plan.homeName);
|
|
967
1091
|
return '<section class="mvpPricing" aria-labelledby="setupPlanHeading">' +
|
|
968
|
-
'<header class="mvpPricingIntro"><
|
|
1092
|
+
'<header class="mvpPricingIntro"><h2 id="setupPlanHeading">Choose your Echo.</h2></header>' +
|
|
969
1093
|
'<div class="mvpPlanTabs" role="tablist" aria-label="Choose an Echo plan">' + tabs + '</div>' +
|
|
970
1094
|
'<article class="mvpPlanHabitat" id="activePlanHabitat" role="tabpanel" aria-labelledby="planTab-' + plan.id + '" data-plan="' + plan.id + '">' +
|
|
971
1095
|
'<div class="mvpHabitatScene">' +
|
|
@@ -977,9 +1101,7 @@ export const SETUP_PAGE_CLIENT_EXTRACTION = String.raw ` /* ---------- dash
|
|
|
977
1101
|
'<div class="mvpHabitatDetails">' +
|
|
978
1102
|
'<div class="mvpPlanIdentity"><span>' + esc(plan.name) + '</span>' + (plan.popular ? "<b>Echo’s pick</b>" : "") + '</div>' +
|
|
979
1103
|
'<h3>' + esc(plan.homeName) + ' Echo</h3>' +
|
|
980
|
-
'<p class="mvpPlanPromise">' + esc(plan.promise) + '</p>' +
|
|
981
1104
|
'<div class="mvpPlanPrice"><strong>' + esc(plan.price) + '</strong><span>' + esc(plan.cadence) + '</span></div>' +
|
|
982
|
-
'<p class="mvpPacksLabel">Echo packs</p>' +
|
|
983
1105
|
'<ul class="mvpPlanFacts">' + facts + '</ul>' +
|
|
984
1106
|
'<button type="button" class="primary mvpPlanCta" id="' + chooseId + '">' + esc(chooseLabel) + ' <span aria-hidden="true">→</span></button>' +
|
|
985
1107
|
'</div>' +
|
|
@@ -1024,6 +1146,7 @@ export const SETUP_PAGE_CLIENT_EXTRACTION = String.raw ` /* ---------- dash
|
|
|
1024
1146
|
slot.innerHTML = "";
|
|
1025
1147
|
slot.classList.add("is-hidden");
|
|
1026
1148
|
if (readySettings) readySettings.classList.remove("is-plan-open", "is-checkout-pending");
|
|
1149
|
+
renderBillingCommitment("");
|
|
1027
1150
|
return;
|
|
1028
1151
|
}
|
|
1029
1152
|
slot.classList.remove("is-hidden");
|
|
@@ -1042,12 +1165,19 @@ export const SETUP_PAGE_CLIENT_EXTRACTION = String.raw ` /* ---------- dash
|
|
|
1042
1165
|
var planLabel = plan.charAt(0).toUpperCase() + plan.slice(1);
|
|
1043
1166
|
var checkoutJustConfirmed = !!billingActivationPendingPlan;
|
|
1044
1167
|
billingActivationPendingPlan = "";
|
|
1168
|
+
rememberCheckoutSessionId("");
|
|
1169
|
+
billingSyncAttempts = 0;
|
|
1045
1170
|
var paidHistory = billingStatus && billingStatus.historicalConversationQuota;
|
|
1046
1171
|
var paidUsage = paidHistory && typeof paidHistory.used === "number" && typeof paidHistory.limit === "number"
|
|
1047
|
-
? paidHistory.used + " / " + paidHistory.limit + "
|
|
1048
|
-
: "Your
|
|
1172
|
+
? paidHistory.used + " / " + paidHistory.limit + " coding sessions imported."
|
|
1173
|
+
: "Your coding-session import and weekly memory allowance are ready.";
|
|
1174
|
+
var lifecycleText = paidPlanLifecycleText(plan);
|
|
1175
|
+
var paidDetail = lifecycleText
|
|
1176
|
+
? lifecycleText + (checkoutJustConfirmed ? "" : " " + paidUsage)
|
|
1177
|
+
: (checkoutJustConfirmed ? "Continue below." : paidUsage);
|
|
1049
1178
|
if (checkoutJustConfirmed) document.title = "Echo plan active — continue setup";
|
|
1050
|
-
slot.innerHTML = '<div class="setupPlanConfirmed' + (checkoutJustConfirmed ? ' is-checkout-confirmed' : '') + '"><span class="setupPlanConfirmedIcon" aria-hidden="true">' + setupIcon("check") + '</span><span class="setupPlanConfirmedCopy"><strong>' + (checkoutJustConfirmed ? 'Your Echo is ready. ' : '') + esc(planLabel) + ' Echo</strong><span>' + esc(
|
|
1179
|
+
slot.innerHTML = '<div class="setupPlanConfirmed' + (checkoutJustConfirmed ? ' is-checkout-confirmed' : '') + (lifecycleText ? ' has-lifecycle' : '') + '"><span class="setupPlanConfirmedIcon" aria-hidden="true">' + setupIcon("check") + '</span><span class="setupPlanConfirmedCopy"><strong>' + (checkoutJustConfirmed ? 'Your Echo is ready. ' : '') + esc(planLabel) + ' Echo</strong><span>' + esc(paidDetail) + '</span></span><button type="button" class="textButton" id="changeActivePlan">Change</button></div>';
|
|
1180
|
+
renderBillingCommitment(plan);
|
|
1051
1181
|
var changeActivePlan = document.getElementById("changeActivePlan");
|
|
1052
1182
|
if (changeActivePlan) changeActivePlan.onclick = openHostedPlanOptions;
|
|
1053
1183
|
renderSessionSelection(true);
|
|
@@ -1056,7 +1186,8 @@ export const SETUP_PAGE_CLIENT_EXTRACTION = String.raw ` /* ---------- dash
|
|
|
1056
1186
|
}
|
|
1057
1187
|
if (setupPlanChoice === "free") {
|
|
1058
1188
|
if (readySettings) readySettings.classList.remove("is-plan-open", "is-checkout-pending");
|
|
1059
|
-
slot.innerHTML = '<div class="setupPlanConfirmed"><span class="setupPlanConfirmedIcon" aria-hidden="true">' + setupIcon("check") + '</span><span class="setupPlanConfirmedCopy"><strong>Original Echo</strong><span>100
|
|
1189
|
+
slot.innerHTML = '<div class="setupPlanConfirmed"><span class="setupPlanConfirmedIcon" aria-hidden="true">' + setupIcon("check") + '</span><span class="setupPlanConfirmedCopy"><strong>Original Echo</strong><span>100 coding sessions and 100 memory recalls each week.</span></span><button type="button" class="textButton" id="changeSetupPlan">Change</button></div>';
|
|
1190
|
+
renderBillingCommitment("");
|
|
1060
1191
|
var changeBtn = document.getElementById("changeSetupPlan");
|
|
1061
1192
|
if (changeBtn) changeBtn.onclick = function () {
|
|
1062
1193
|
rememberSetupPlanChoice("");
|
|
@@ -1069,17 +1200,36 @@ export const SETUP_PAGE_CLIENT_EXTRACTION = String.raw ` /* ---------- dash
|
|
|
1069
1200
|
? "power"
|
|
1070
1201
|
: (setupPlanChoice === "pro" || billingActivationPendingPlan === "pro" ? "pro" : "");
|
|
1071
1202
|
if (pendingPaidPlan) {
|
|
1203
|
+
renderBillingCommitment("");
|
|
1072
1204
|
if (readySettings) {
|
|
1073
1205
|
readySettings.classList.remove("is-plan-open");
|
|
1074
1206
|
readySettings.classList.add("is-checkout-pending");
|
|
1075
1207
|
}
|
|
1076
1208
|
var selectedPaidPlan = pendingPaidPlan === "power" ? "Power" : "Pro";
|
|
1209
|
+
var pendingHasTrial = !billingStatus || billingStatus.trialAvailable !== false;
|
|
1210
|
+
var pendingPrice = pendingPaidPlan === "power" ? "$100" : "$20";
|
|
1211
|
+
var pendingLimit = pendingPaidPlan === "power" ? "2,000" : "500";
|
|
1212
|
+
var pendingStatus = billingCheckMessage || (billingLastCheckedAt
|
|
1213
|
+
? "Checking automatically every few seconds · Last checked just now"
|
|
1214
|
+
: "Checking automatically every few seconds");
|
|
1077
1215
|
slot.innerHTML =
|
|
1078
1216
|
'<section class="setupPlanChoice setupPlanCheckout" aria-labelledby="setupPlanHeading">' +
|
|
1079
|
-
'<div class="setupPlanIntro"><h3 id="setupPlanHeading">
|
|
1080
|
-
'<
|
|
1081
|
-
|
|
1217
|
+
'<div class="setupPlanIntro"><h3 id="setupPlanHeading">' + esc(pendingHasTrial ? selectedPaidPlan + " trial — $0 today" : selectedPaidPlan + " subscription") + '</h3><p>Finish in Stripe. This page updates automatically.</p></div>' +
|
|
1218
|
+
'<dl class="checkoutCommitment">' +
|
|
1219
|
+
'<div><dt>Today</dt><dd>' + esc(pendingHasTrial ? "$0" : pendingPrice) + '</dd></div>' +
|
|
1220
|
+
'<div><dt>History import</dt><dd>' + esc(pendingLimit) + ' sessions</dd></div>' +
|
|
1221
|
+
'<div><dt>' + esc(pendingHasTrial ? "First charge" : "Billing") + '</dt><dd>' + esc(pendingHasTrial ? pendingPrice + " on " + trialChargeDateText() : pendingPrice + "/month now") + '</dd></div>' +
|
|
1222
|
+
'</dl>' +
|
|
1223
|
+
'<div class="checkoutActions"><button type="button" class="primary" id="continueSelectedPaidPlan">Reopen Stripe checkout</button><button type="button" class="secondary" id="checkPaymentStatus">Check payment status</button></div>' +
|
|
1224
|
+
'<p class="checkoutPollStatus" id="setupPlanStatus" role="status" aria-live="polite">' + esc(pendingStatus) + '</p>' +
|
|
1225
|
+
'<div class="setupPlanFoot"><button type="button" class="textButton" id="changeSelectedPaidPlan">Choose another plan</button></div>' +
|
|
1082
1226
|
'</section>';
|
|
1227
|
+
var checkPaid = document.getElementById("checkPaymentStatus");
|
|
1228
|
+
if (checkPaid) checkPaid.onclick = function () {
|
|
1229
|
+
billingCheckMessage = "Checking payment status…";
|
|
1230
|
+
renderSetupPlan(true);
|
|
1231
|
+
void refreshBillingStatus();
|
|
1232
|
+
};
|
|
1083
1233
|
var continuePaid = document.getElementById("continueSelectedPaidPlan");
|
|
1084
1234
|
if (continuePaid) continuePaid.onclick = function () { openPaidPricing(pendingPaidPlan); };
|
|
1085
1235
|
var changePaid = document.getElementById("changeSelectedPaidPlan");
|
|
@@ -1099,19 +1249,43 @@ export const SETUP_PAGE_CLIENT_EXTRACTION = String.raw ` /* ---------- dash
|
|
|
1099
1249
|
readySettings.classList.add("is-plan-open");
|
|
1100
1250
|
}
|
|
1101
1251
|
slot.innerHTML = renderPlanHabitat();
|
|
1252
|
+
renderBillingCommitment("");
|
|
1102
1253
|
bindPlanHabitat();
|
|
1103
1254
|
if (migrateBtn) migrateBtn.disabled = true;
|
|
1104
1255
|
}
|
|
1256
|
+
async function maybeSyncPendingCheckout() {
|
|
1257
|
+
if (!pendingCheckoutSessionId || billingSyncAttempts >= 12) return;
|
|
1258
|
+
var now = Date.now();
|
|
1259
|
+
if (billingSyncLastAttemptAt && now - billingSyncLastAttemptAt < 5000) return;
|
|
1260
|
+
billingSyncLastAttemptAt = now;
|
|
1261
|
+
billingSyncAttempts++;
|
|
1262
|
+
try {
|
|
1263
|
+
var result = await postJson(
|
|
1264
|
+
"/billing-checkout-sync",
|
|
1265
|
+
{ sessionId: pendingCheckoutSessionId },
|
|
1266
|
+
18000
|
|
1267
|
+
);
|
|
1268
|
+
if (result && result.ok === true) rememberCheckoutSessionId("");
|
|
1269
|
+
} catch (_) {
|
|
1270
|
+
// A 409 is expected while the hosted Checkout session is still open.
|
|
1271
|
+
// Webhooks remain primary; this bounded fallback repairs delayed delivery.
|
|
1272
|
+
}
|
|
1273
|
+
}
|
|
1105
1274
|
async function refreshBillingStatus() {
|
|
1106
1275
|
if (billingStatusLoading) return;
|
|
1107
1276
|
billingStatusLoading = true;
|
|
1108
1277
|
try {
|
|
1278
|
+
await maybeSyncPendingCheckout();
|
|
1109
1279
|
billingStatus = await getJson("/billing-status");
|
|
1280
|
+
billingLastCheckedAt = Date.now();
|
|
1281
|
+
billingCheckMessage = "";
|
|
1110
1282
|
renderSetupPlan(true);
|
|
1111
1283
|
renderAccountChoice();
|
|
1112
1284
|
renderSessionSelection(true);
|
|
1113
1285
|
if (billingStatus && billingStatus.plan === "unknown") setSetupPlanStatus("Plan check unavailable. Original still works.");
|
|
1114
1286
|
} catch (_) {
|
|
1287
|
+
billingLastCheckedAt = Date.now();
|
|
1288
|
+
billingCheckMessage = "Could not check payment yet. Echo will keep trying automatically.";
|
|
1115
1289
|
setSetupPlanStatus("Plan check unavailable. Original still works.");
|
|
1116
1290
|
} finally {
|
|
1117
1291
|
billingStatusLoading = false;
|
|
@@ -1147,23 +1321,28 @@ export const SETUP_PAGE_CLIENT_EXTRACTION = String.raw ` /* ---------- dash
|
|
|
1147
1321
|
opened.location.href = response.url;
|
|
1148
1322
|
setSetupPlanStatus(openedMessage);
|
|
1149
1323
|
startBillingPoll();
|
|
1150
|
-
return
|
|
1324
|
+
return response;
|
|
1151
1325
|
} catch (error) {
|
|
1152
1326
|
try { if (opened && !opened.closed) opened.close(); } catch (_) {}
|
|
1153
1327
|
setSetupPlanStatus(error && error.message ? error.message : "Could not open secure Echo billing.");
|
|
1154
|
-
return
|
|
1328
|
+
return null;
|
|
1155
1329
|
}
|
|
1156
1330
|
}
|
|
1157
1331
|
async function openPaidPricing(plan) {
|
|
1158
1332
|
setSetupPlanStatus("Opening secure checkout…");
|
|
1159
|
-
var
|
|
1333
|
+
var checkout = await openHostedBilling(
|
|
1160
1334
|
"/billing-checkout",
|
|
1161
1335
|
{ plan: plan, trial: !billingStatus || billingStatus.trialAvailable !== false },
|
|
1162
1336
|
"Creating your secure checkout…",
|
|
1163
1337
|
"Checkout opened in a secure tab. Keep this local page open; Echo will confirm here automatically."
|
|
1164
1338
|
);
|
|
1165
|
-
if (
|
|
1339
|
+
if (checkout) {
|
|
1166
1340
|
billingActivationPendingPlan = plan;
|
|
1341
|
+
if (typeof checkout.sessionId === "string") {
|
|
1342
|
+
rememberCheckoutSessionId(checkout.sessionId);
|
|
1343
|
+
billingSyncAttempts = 0;
|
|
1344
|
+
billingSyncLastAttemptAt = 0;
|
|
1345
|
+
}
|
|
1167
1346
|
document.title = "Finish checkout — Echo setup is waiting";
|
|
1168
1347
|
renderSetupPlan(true);
|
|
1169
1348
|
}
|
|
@@ -1178,25 +1357,17 @@ export const SETUP_PAGE_CLIENT_EXTRACTION = String.raw ` /* ---------- dash
|
|
|
1178
1357
|
}
|
|
1179
1358
|
function tryItCard() {
|
|
1180
1359
|
var questions = recallQuestions();
|
|
1181
|
-
var
|
|
1182
|
-
var numberLabel = String(index + 1).padStart(2, "0");
|
|
1183
|
-
return '<article class="recallPrompt" data-recall-prompt="' + esc(question) + '">' +
|
|
1184
|
-
'<div class="recallPromptHead">' +
|
|
1185
|
-
'<span>Suggested prompt ' + numberLabel + '</span>' +
|
|
1186
|
-
'<button type="button" class="recallCopy" data-recall-copy aria-label="Copy suggested prompt ' + (index + 1) + '">' + setupIcon("copy") + '<span>Copy</span></button>' +
|
|
1187
|
-
'</div>' +
|
|
1188
|
-
'<p class="recallPromptText">' + esc(question) + '</p>' +
|
|
1189
|
-
'<div class="recallPromptActions" aria-label="Use suggested prompt ' + (index + 1) + '">' +
|
|
1190
|
-
'<button type="button" class="recallAgent" data-recall-codex><span class="recallAgentIcon"><img src="/hud-assets/codex.svg" alt="" /></span><span>Ask in Codex</span></button>' +
|
|
1191
|
-
'<button type="button" class="recallAgent is-claude" data-recall-claude><span class="recallAgentIcon"><img src="/hud-assets/claude.svg" alt="" /></span><span>Ask in Claude</span></button>' +
|
|
1192
|
-
'</div>' +
|
|
1193
|
-
'</article>';
|
|
1194
|
-
}).join("");
|
|
1360
|
+
var primary = questions[0];
|
|
1195
1361
|
return '<div class="tryCard">' +
|
|
1196
|
-
'<
|
|
1197
|
-
'<
|
|
1198
|
-
|
|
1199
|
-
|
|
1362
|
+
'<h3 class="tryTitle">Let Echo prove it remembers</h3>' +
|
|
1363
|
+
'<article class="recallPrompt recallPromptPrimary" data-recall-prompt="' + esc(primary) + '">' +
|
|
1364
|
+
'<div class="recallPromptHead"><span>Your question</span><button type="button" class="recallCopy" data-recall-copy aria-label="Copy your first recall question">' + setupIcon("copy") + '<span>Copy</span></button></div>' +
|
|
1365
|
+
'<p class="recallPromptText">' + esc(primary) + '</p>' +
|
|
1366
|
+
'<div class="recallPromptActions" aria-label="Try your first memory recall">' +
|
|
1367
|
+
'<button type="button" class="recallAgent recallAgentPrimary" data-recall-codex><span class="recallAgentIcon"><img src="/hud-assets/codex.svg" alt="" /></span><span>Ask in Codex</span></button>' +
|
|
1368
|
+
'<button type="button" class="recallAgent is-claude" data-recall-claude><span class="recallAgentIcon"><img src="/hud-assets/claude.svg" alt="" /></span><span>Use Claude instead</span></button>' +
|
|
1369
|
+
'</div>' +
|
|
1370
|
+
'</article>' +
|
|
1200
1371
|
'<p class="launchStatus" id="launchStatus"></p>' +
|
|
1201
1372
|
'</div>';
|
|
1202
1373
|
}
|
|
@@ -1211,12 +1382,6 @@ export const SETUP_PAGE_CLIENT_EXTRACTION = String.raw ` /* ---------- dash
|
|
|
1211
1382
|
if (claudeBtn) claudeBtn.addEventListener("click", function () { void openClaudeDesktop(prompt); });
|
|
1212
1383
|
});
|
|
1213
1384
|
}
|
|
1214
|
-
function hudTip() {
|
|
1215
|
-
return '<p class="hudTip"><img src="/hud-assets/echo-face-cutout.png" alt="" /><span><strong>Live HUD:</strong> Echo watches context quality while you work. When it drops, <strong>Start new session with context</strong> opens a fresh Codex or Claude Code session and carries the useful work forward. Open it with <code>echomem-hud app</code>.</span></p>';
|
|
1216
|
-
}
|
|
1217
|
-
function doneCloseNote() {
|
|
1218
|
-
return '<p class="doneCloseNote">You can safely close this page. EchoMem is set up, and your memories are saved.</p>';
|
|
1219
|
-
}
|
|
1220
1385
|
function renderEnding() {
|
|
1221
1386
|
setCityMode(false);
|
|
1222
1387
|
setExtractMode(true);
|
|
@@ -1226,27 +1391,30 @@ export const SETUP_PAGE_CLIENT_EXTRACTION = String.raw ` /* ---------- dash
|
|
|
1226
1391
|
setHead("Ending extraction", "Ending");
|
|
1227
1392
|
app.className = "";
|
|
1228
1393
|
app.innerHTML =
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
'<h2
|
|
1232
|
-
'<p>
|
|
1233
|
-
'<div class="miniBar"><i></i></div>' +
|
|
1234
|
-
'</
|
|
1394
|
+
setupJourney(4) +
|
|
1395
|
+
'<section class="lifecycleState lifecyclePending">' +
|
|
1396
|
+
'<h2>Stopping the remaining queued work…</h2>' +
|
|
1397
|
+
'<p>Saved memories stay intact.</p>' +
|
|
1398
|
+
'<div class="miniBar" aria-label="Ending extraction"><i></i></div>' +
|
|
1399
|
+
'</section>';
|
|
1235
1400
|
}
|
|
1236
1401
|
function renderSkipped() {
|
|
1237
1402
|
setCityMode(false);
|
|
1238
1403
|
setExtractMode(true);
|
|
1239
1404
|
setHead("Extraction ended for now", "Ended");
|
|
1240
1405
|
app.className = "";
|
|
1406
|
+
var savedCount = Math.max(0, Number(lastExtractionProgress && lastExtractionProgress.extracted || 0));
|
|
1241
1407
|
app.innerHTML =
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
'<
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1408
|
+
setupJourney(savedCount > 0 ? 5 : 4) +
|
|
1409
|
+
'<section class="lifecycleState lifecyclePaused">' +
|
|
1410
|
+
'<header class="lifecycleIntro">' +
|
|
1411
|
+
'<h2>' + (savedCount > 0 ? esc(number(savedCount)) + ' memories are ready to use.' : 'Import ended safely.') + '</h2>' +
|
|
1412
|
+
'<p>The remaining queue was canceled. Saved memories were not removed.</p>' +
|
|
1413
|
+
'</header>' +
|
|
1414
|
+
(savedCount > 0 ? tryItCard() : '') +
|
|
1415
|
+
'<p class="lifecycleResume">Continue later with <code>echomem-mcp init</code>.</p>' +
|
|
1416
|
+
'</section>';
|
|
1417
|
+
if (savedCount > 0) wireTryButtons();
|
|
1250
1418
|
}
|
|
1251
1419
|
async function endExtraction() {
|
|
1252
1420
|
if (extractionEnded) return;
|