@echomem/mcp 1.4.18 → 1.4.20
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 +12 -5
- package/dist/codex-session-files.js +7 -1
- package/dist/hud/server.js +17 -13
- package/dist/hud/web.js +204 -13
- package/dist/index.js +61 -30
- package/dist/keystore.js +9 -19
- package/dist/migrate.js +78 -36
- package/dist/setup-page/client-core.js +31 -1
- package/dist/setup-page/client-extraction.js +581 -157
- package/dist/setup-page/client-lifecycle.js +64 -43
- package/dist/setup-page/client-report-city.js +14 -5
- package/dist/setup-page/styles-extraction.js +1176 -148
- package/dist/setup-page/styles-foundation.js +20 -15
- package/dist/setup-page/styles-website-alignment.js +818 -0
- package/dist/setup-page/styles.js +2 -0
- package/dist/setup-preview.js +64 -1
- package/dist/setup.js +328 -39
- package/dist/v1-contract.js +8 -8
- package/package.json +1 -1
- package/templates/codex-skills/echomem-forget/SKILL.md +16 -0
- package/templates/codex-skills/echomem-forget/agents/openai.yaml +6 -0
- package/templates/codex-skills/echomem-login/SKILL.md +27 -0
- package/templates/codex-skills/echomem-login/agents/openai.yaml +6 -0
- package/templates/codex-skills/echomem-save/SKILL.md +14 -0
- package/templates/codex-skills/echomem-save/agents/openai.yaml +6 -0
- package/templates/codex-skills/echomem-search/SKILL.md +14 -0
- package/templates/codex-skills/echomem-search/agents/openai.yaml +6 -0
|
@@ -102,6 +102,7 @@ export const SETUP_PAGE_CLIENT_EXTRACTION = String.raw ` /* ---------- dash
|
|
|
102
102
|
try {
|
|
103
103
|
var cfg = await getJson("/config");
|
|
104
104
|
authUrl = cfg.authUrl || authUrl;
|
|
105
|
+
switchAccountUrl = cfg.switchAccountUrl || switchAccountUrl || authUrl;
|
|
105
106
|
if (cfg.connected) { connected = true; closeAuthWindow(); await waitForStats(); return; }
|
|
106
107
|
} catch (_) {}
|
|
107
108
|
await sleep(900);
|
|
@@ -172,9 +173,11 @@ export const SETUP_PAGE_CLIENT_EXTRACTION = String.raw ` /* ---------- dash
|
|
|
172
173
|
var pendingCodex = typeof migratable.pendingCodex === "number" ? migratable.pendingCodex : null;
|
|
173
174
|
var pendingClaudeCode = typeof migratable.pendingClaudeCode === "number" ? migratable.pendingClaudeCode : null;
|
|
174
175
|
setHead("Turn coding history into memory", pendingTrusted ? (knownDone ? "Done" : "Ready") : (localScanReady ? "Ready" : "Scanning"));
|
|
175
|
-
var
|
|
176
|
-
? "Counting your new conversations
|
|
177
|
-
: (knownDone
|
|
176
|
+
var headlineHtml = !pendingTrusted
|
|
177
|
+
? "Counting your <strong>new conversations…</strong>"
|
|
178
|
+
: (knownDone
|
|
179
|
+
? "You are <strong>all caught up.</strong>"
|
|
180
|
+
: "Echo found <strong>" + esc(number(pendN)) + " new coding session" + (pendN === 1 ? "" : "s") + ".</strong>");
|
|
178
181
|
var sub = !pendingTrusted
|
|
179
182
|
? "Echo is matching your local history against what is already in memory."
|
|
180
183
|
: (knownDone
|
|
@@ -196,38 +199,45 @@ export const SETUP_PAGE_CLIENT_EXTRACTION = String.raw ` /* ---------- dash
|
|
|
196
199
|
app.innerHTML =
|
|
197
200
|
'<div id="exBanner"></div>' +
|
|
198
201
|
'<section class="readyDecision">' +
|
|
202
|
+
'<p class="utilityEyebrow readyEyebrow">History import / review</p>' +
|
|
199
203
|
'<div class="readyHero">' +
|
|
200
204
|
'<img class="readyMascot" src="/hud-assets/echo-face-cutout.png" alt="" />' +
|
|
201
205
|
'<div class="exCopy">' +
|
|
202
|
-
'<h2 id="exHeadline"></h2>' +
|
|
206
|
+
'<h2 class="siteHeadline" id="exHeadline"></h2>' +
|
|
203
207
|
'<p class="exSub" id="exSub"></p>' +
|
|
204
208
|
'</div>' +
|
|
205
209
|
'</div>' +
|
|
206
210
|
'<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>' +
|
|
207
214
|
'<div class="readyGo">' +
|
|
208
215
|
'<div class="actions">' +
|
|
209
216
|
'<button id="migrate" class="primary"></button>' +
|
|
217
|
+
'<button type="button" id="skipHistoryImport" class="secondary skipHistoryButton">Skip history import</button>' +
|
|
210
218
|
'</div>' +
|
|
211
219
|
'<p class="ctaMeta" id="exEta"></p>' +
|
|
212
220
|
'</div>' +
|
|
213
221
|
'<div class="readyFoot">' +
|
|
214
222
|
'<details class="readyExplain" id="readyExplain"></details>' +
|
|
215
223
|
'</div>' +
|
|
216
|
-
'</section>'
|
|
224
|
+
'</section>' +
|
|
225
|
+
'<dialog class="sessionPicker" id="sessionPicker" aria-labelledby="sessionPickerTitle"></dialog>';
|
|
217
226
|
document.getElementById("migrate").onclick = startMigrate;
|
|
218
227
|
dashMounted = true;
|
|
219
228
|
}
|
|
220
229
|
document.getElementById("exBanner").innerHTML =
|
|
221
230
|
(statsSlow && !stats ? '<div class="warning">Local counts are taking longer than expected. You can still ask Echo to extract anything unprocessed.</div>' : '') +
|
|
222
231
|
(error ? '<div class="error">' + esc(error) + '</div>' : '');
|
|
223
|
-
document.getElementById("exHeadline").
|
|
232
|
+
document.getElementById("exHeadline").innerHTML = headlineHtml;
|
|
224
233
|
document.getElementById("exSub").textContent = sub;
|
|
225
234
|
var codexN = pendingTrusted && pendingCodex !== null ? pendingCodex : (sessions.codex || 0);
|
|
226
235
|
var claudeN = pendingTrusted && pendingClaudeCode !== null ? pendingClaudeCode : (sessions.claudeCode || 0);
|
|
227
236
|
var alreadyN = Math.max(0, (sessions.total || 0) - pendN);
|
|
228
237
|
// Proof strip: one quiet line of evidence that the scan is real. Hidden when there is nothing to show.
|
|
229
238
|
var srcIcon = function (id, fallback) {
|
|
230
|
-
|
|
239
|
+
var asset = id === "claude-desktop" ? "claude" : "codex";
|
|
240
|
+
return '<span class="pfIcon"><img src="/hud-assets/' + asset + '.svg" alt="" onerror="this.style.display="none";this.nextElementSibling.style.display="grid";" /><span class="pfFallback">' + fallback + '</span></span>';
|
|
231
241
|
};
|
|
232
242
|
document.getElementById("exSources").innerHTML = !pendingTrusted
|
|
233
243
|
? '<span class="pfNote">Scanning local history…</span>'
|
|
@@ -251,20 +261,40 @@ export const SETUP_PAGE_CLIENT_EXTRACTION = String.raw ` /* ---------- dash
|
|
|
251
261
|
'<div class="readyStep"><strong>Skip repeats</strong><span>Anything already in memory is ignored.</span></div>' +
|
|
252
262
|
'<div class="readyStep"><strong>Save memory</strong><span>Decisions, fixes, context, and next steps become searchable.</span></div>' +
|
|
253
263
|
'</div>';
|
|
264
|
+
renderSetupPlan(canExtract);
|
|
265
|
+
renderAccountChoice();
|
|
266
|
+
renderSessionSelection(canExtract);
|
|
267
|
+
if (canExtract && !billingStatus && !billingStatusLoading) void refreshBillingStatus();
|
|
254
268
|
// While still counting, hide the primary button entirely; the headline + source split
|
|
255
269
|
// already say it is working, and the ready state should present one clear action.
|
|
256
270
|
var migrateBtn = document.getElementById("migrate");
|
|
271
|
+
var skipHistoryBtn = document.getElementById("skipHistoryImport");
|
|
257
272
|
if (pendingTrusted) {
|
|
258
273
|
migrateBtn.style.display = "";
|
|
259
|
-
migrateBtn.disabled =
|
|
260
|
-
migrateBtn.
|
|
274
|
+
migrateBtn.disabled = canExtract && (!setupPlanConfirmed() || selectedSessionKeyList().length === 0);
|
|
275
|
+
migrateBtn.innerHTML = '<span>' + esc(canExtract ? extractLabel : "Finish setup") + '</span>' + (canExtract ? setupIcon("arrow-right") : "");
|
|
261
276
|
migrateBtn.onclick = canExtract ? startMigrate : skip;
|
|
277
|
+
if (skipHistoryBtn) {
|
|
278
|
+
skipHistoryBtn.style.display = canExtract ? "" : "none";
|
|
279
|
+
skipHistoryBtn.onclick = skip;
|
|
280
|
+
}
|
|
262
281
|
} else {
|
|
263
282
|
migrateBtn.style.display = "none";
|
|
283
|
+
if (skipHistoryBtn) skipHistoryBtn.style.display = "none";
|
|
264
284
|
}
|
|
265
285
|
}
|
|
266
286
|
async function startMigrate() {
|
|
267
287
|
if (decisionMade) return;
|
|
288
|
+
if (!setupPlanConfirmed()) {
|
|
289
|
+
setSetupPlanStatus("Choose Free, Pro, or Power before extraction starts.");
|
|
290
|
+
return;
|
|
291
|
+
}
|
|
292
|
+
var selectedKeys = selectedSessionKeyList();
|
|
293
|
+
if (!selectedKeys.length) {
|
|
294
|
+
renderSessionSelection(true, "Choose at least one conversation to extract.");
|
|
295
|
+
return;
|
|
296
|
+
}
|
|
297
|
+
stopBillingPoll();
|
|
268
298
|
decisionMade = true;
|
|
269
299
|
extractionEnded = false;
|
|
270
300
|
setCityMode(false);
|
|
@@ -272,7 +302,7 @@ export const SETUP_PAGE_CLIENT_EXTRACTION = String.raw ` /* ---------- dash
|
|
|
272
302
|
app.className = "";
|
|
273
303
|
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>';
|
|
274
304
|
try {
|
|
275
|
-
var started = await postJson("/migrate", {});
|
|
305
|
+
var started = await postJson("/migrate", { conversationKeys: selectedKeys });
|
|
276
306
|
extractMounted = false;
|
|
277
307
|
renderProgress(Object.assign({ status: "starting", completed: 0, failed: 0, extracted: 0 }, started));
|
|
278
308
|
await pollProgress();
|
|
@@ -282,10 +312,11 @@ export const SETUP_PAGE_CLIENT_EXTRACTION = String.raw ` /* ---------- dash
|
|
|
282
312
|
}
|
|
283
313
|
}
|
|
284
314
|
async function pollProgress() {
|
|
315
|
+
var runId = ++progressPollRunId;
|
|
285
316
|
for (;;) {
|
|
286
|
-
if (extractionEnded) return;
|
|
317
|
+
if (extractionEnded || runId !== progressPollRunId) return;
|
|
287
318
|
var progress = await getJson("/progress");
|
|
288
|
-
if (extractionEnded) return;
|
|
319
|
+
if (extractionEnded || runId !== progressPollRunId) return;
|
|
289
320
|
renderProgress(progress);
|
|
290
321
|
if (progress.status === "completed" || progress.status === "failed") return;
|
|
291
322
|
await sleep(1200);
|
|
@@ -293,6 +324,7 @@ export const SETUP_PAGE_CLIENT_EXTRACTION = String.raw ` /* ---------- dash
|
|
|
293
324
|
}
|
|
294
325
|
function renderProgress(progress) {
|
|
295
326
|
if (extractionEnded) return;
|
|
327
|
+
lastExtractionProgress = progress;
|
|
296
328
|
setCityMode(false);
|
|
297
329
|
setExtractMode(true);
|
|
298
330
|
var status = progress.status;
|
|
@@ -327,22 +359,25 @@ export const SETUP_PAGE_CLIENT_EXTRACTION = String.raw ` /* ---------- dash
|
|
|
327
359
|
var savedMemoryLabel = extracted === 1 ? "memory saved" : "memories saved";
|
|
328
360
|
var conversationNoun = completed === 1 ? "conversation" : "conversations";
|
|
329
361
|
var timelineUrl = "https://yeahecho.com/memories/timeline";
|
|
330
|
-
var stopDetail = String(progress.error) === "
|
|
362
|
+
var stopDetail = String(progress.error) === "vault-locked"
|
|
331
363
|
? "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."
|
|
332
364
|
: esc(progress.error || "Re-run extraction to finish the rest. Already-done conversations are skipped.");
|
|
365
|
+
var completionNote = done && progress.latest && progress.latest !== "Import complete."
|
|
366
|
+
? '<p class="exSub">' + esc(progress.latest) + '</p>'
|
|
367
|
+
: "";
|
|
333
368
|
t.innerHTML =
|
|
334
369
|
'<div class="resultStage ' + (done ? "ok" : "warn") + '">' +
|
|
335
|
-
(done ? "" : '<p class="
|
|
370
|
+
(done ? '<p class="utilityEyebrow">Memory extraction / complete</p>' : '<p class="utilityEyebrow">Memory extraction / stopped</p>') +
|
|
336
371
|
'<div class="resultTop">' +
|
|
337
372
|
'<div>' +
|
|
338
373
|
(done
|
|
339
|
-
? '<h2 class="savedHeading"><span class="savedMain"><strong>' + esc(number(extracted)) + '</strong> ' + esc(savedMemoryLabel) + '</span><a class="viewEchoLink" href="' + timelineUrl + '" target="_blank" rel="noopener noreferrer">(view in yeahecho)</a></h2>'
|
|
340
|
-
: '<h2>Some conversations need another pass.</h2>' +
|
|
374
|
+
? '<h2 class="savedHeading siteHeadline"><span class="savedMain"><strong>' + esc(number(extracted)) + '</strong> ' + esc(savedMemoryLabel) + '</span><a class="viewEchoLink" href="' + timelineUrl + '" target="_blank" rel="noopener noreferrer">(view in yeahecho)</a></h2>' + completionNote
|
|
375
|
+
: '<h2 class="siteHeadline">Some conversations need <strong>another pass.</strong></h2>' +
|
|
341
376
|
'<p class="exSub">' + stopDetail + '</p>' +
|
|
342
377
|
'<div class="resultNumber"><strong>' + esc(number(extracted)) + '</strong><span>' + esc(memoryNoun) + '</span></div>') +
|
|
343
378
|
'</div>' +
|
|
344
379
|
'</div>' +
|
|
345
|
-
(done ?
|
|
380
|
+
(done ? tryItCard() : "") +
|
|
346
381
|
(done ? "" :
|
|
347
382
|
'<div class="memoryReceipt">' +
|
|
348
383
|
'<p class="receiptK">What just happened</p>' +
|
|
@@ -353,7 +388,7 @@ export const SETUP_PAGE_CLIENT_EXTRACTION = String.raw ` /* ---------- dash
|
|
|
353
388
|
'</div>') +
|
|
354
389
|
'</div>' +
|
|
355
390
|
(done ? hudTip() + doneCloseNote() : "");
|
|
356
|
-
|
|
391
|
+
wireTryButtons();
|
|
357
392
|
return;
|
|
358
393
|
}
|
|
359
394
|
setHead(preparing ? "Preparing" : "Extracting", preparing ? "Preparing" : "Extracting");
|
|
@@ -379,9 +414,13 @@ export const SETUP_PAGE_CLIENT_EXTRACTION = String.raw ` /* ---------- dash
|
|
|
379
414
|
countdownHtml = '<p class="extractFacts"><strong>' + esc(fmtCountdown(remainSec)) + '</strong> left · saved as it goes · safe to end and resume</p>';
|
|
380
415
|
}
|
|
381
416
|
}
|
|
382
|
-
|
|
417
|
+
var recoveryNotice = extractionRecoveryMessage
|
|
418
|
+
? '<div class="error">' + esc(extractionRecoveryMessage) + '</div>'
|
|
419
|
+
: "";
|
|
420
|
+
t.innerHTML = recoveryNotice +
|
|
383
421
|
'<div class="workbench">' +
|
|
384
|
-
'<
|
|
422
|
+
'<p class="utilityEyebrow">Memory extraction / live</p>' +
|
|
423
|
+
'<h2 class="siteHeadline">' + (preparing ? "Setting up the <strong>import.</strong>" : "Building your <strong>memory.</strong>") + '</h2>' +
|
|
385
424
|
'<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>' +
|
|
386
425
|
'<div class="track' + (preparing ? " indef" : "") + '"><div class="fill"' + (preparing ? "" : ' style="width:' + pct + '%"') + '></div></div>' +
|
|
387
426
|
'<p class="exSub">' + esc(number(extracted)) + ' ' + (extracted === 1 ? "memory" : "memories") + ' captured' + (checkingEdgeCases ? ' · finishing checks' : "") + '.</p>' +
|
|
@@ -399,15 +438,75 @@ export const SETUP_PAGE_CLIENT_EXTRACTION = String.raw ` /* ---------- dash
|
|
|
399
438
|
if (m >= 60) { var h = Math.floor(m / 60); return h + "h " + (m % 60) + "m"; }
|
|
400
439
|
return m > 0 ? m + "m " + s + "s" : s + "s";
|
|
401
440
|
}
|
|
402
|
-
function
|
|
403
|
-
|
|
441
|
+
function cleanRecallTopic(value) {
|
|
442
|
+
var topic = String(value || "")
|
|
443
|
+
.replace(/\s+/g, " ")
|
|
444
|
+
.replace(/\s+[·|]\s+\d+$/, "")
|
|
445
|
+
.trim();
|
|
446
|
+
if (!topic || topic.length < 3) return "";
|
|
447
|
+
if (topic.length > 56) topic = topic.slice(0, 53).trim() + "...";
|
|
448
|
+
var generic = {
|
|
449
|
+
"no project detected": true,
|
|
450
|
+
"untitled coding session": true,
|
|
451
|
+
"project": true,
|
|
452
|
+
"projects": true,
|
|
453
|
+
"coding session": true,
|
|
454
|
+
"coding sessions": true
|
|
455
|
+
};
|
|
456
|
+
return generic[topic.toLowerCase()] ? "" : topic;
|
|
457
|
+
}
|
|
458
|
+
function recallTopics() {
|
|
459
|
+
var selected = Object.create(null);
|
|
460
|
+
selectedSessionKeyList().forEach(function (key) { selected[key] = true; });
|
|
461
|
+
var sessions = candidateSessions().filter(function (session) {
|
|
462
|
+
return !!selected[String(session.key || "")];
|
|
463
|
+
});
|
|
464
|
+
if (!sessions.length) sessions = candidateSessions();
|
|
465
|
+
|
|
466
|
+
var scored = Object.create(null);
|
|
467
|
+
function addTopic(value, base, recency) {
|
|
468
|
+
var topic = cleanRecallTopic(value);
|
|
469
|
+
if (!topic) return;
|
|
470
|
+
var key = topic.toLowerCase();
|
|
471
|
+
if (!scored[key]) scored[key] = { topic: topic, score: 0 };
|
|
472
|
+
scored[key].score += base + recency;
|
|
473
|
+
}
|
|
474
|
+
sessions.forEach(function (session, index) {
|
|
475
|
+
var recency = Math.max(1, sessions.length - index);
|
|
476
|
+
addTopic(session.project, 100, recency);
|
|
477
|
+
addTopic(session.title, 20, recency);
|
|
478
|
+
});
|
|
479
|
+
return Object.keys(scored)
|
|
480
|
+
.map(function (key) { return scored[key]; })
|
|
481
|
+
.sort(function (a, b) { return b.score - a.score || a.topic.localeCompare(b.topic); })
|
|
482
|
+
.slice(0, 3)
|
|
483
|
+
.map(function (entry) { return entry.topic; });
|
|
484
|
+
}
|
|
485
|
+
function recallQuestions() {
|
|
486
|
+
var topics = recallTopics();
|
|
487
|
+
var templates = [
|
|
488
|
+
"What do I know about {x}?",
|
|
489
|
+
"Catch me up on {x}.",
|
|
490
|
+
"Where did I leave off with {x}?"
|
|
491
|
+
];
|
|
492
|
+
var questions = topics.map(function (topic, index) {
|
|
493
|
+
return templates[index].replace("{x}", topic);
|
|
494
|
+
});
|
|
495
|
+
var fallbacks = [
|
|
496
|
+
"What have I been working on recently?",
|
|
497
|
+
"What decisions and next steps should I pick up?",
|
|
498
|
+
"What from my recent work needs attention next?"
|
|
499
|
+
];
|
|
500
|
+
fallbacks.forEach(function (question) {
|
|
501
|
+
if (questions.length < 3) questions.push(question);
|
|
502
|
+
});
|
|
503
|
+
return questions.slice(0, 3);
|
|
404
504
|
}
|
|
405
505
|
function setLaunchStatus(message) {
|
|
406
506
|
var node = document.getElementById("launchStatus");
|
|
407
507
|
if (node) node.textContent = message || "";
|
|
408
508
|
}
|
|
409
|
-
async function copyRecallPrompt() {
|
|
410
|
-
var prompt = recallPrompt();
|
|
509
|
+
async function copyRecallPrompt(prompt) {
|
|
411
510
|
try {
|
|
412
511
|
await navigator.clipboard.writeText(prompt);
|
|
413
512
|
return true;
|
|
@@ -415,162 +514,477 @@ export const SETUP_PAGE_CLIENT_EXTRACTION = String.raw ` /* ---------- dash
|
|
|
415
514
|
return false;
|
|
416
515
|
}
|
|
417
516
|
}
|
|
418
|
-
async function copyPromptOnly() {
|
|
419
|
-
var copied = await copyRecallPrompt();
|
|
420
|
-
var copyBtn = document.getElementById("tryCopy");
|
|
517
|
+
async function copyPromptOnly(prompt, copyBtn) {
|
|
518
|
+
var copied = await copyRecallPrompt(prompt);
|
|
421
519
|
if (copyBtn && copied) {
|
|
520
|
+
var original = copyBtn.innerHTML;
|
|
422
521
|
copyBtn.textContent = "Copied";
|
|
423
|
-
window.setTimeout(function () { copyBtn.
|
|
522
|
+
window.setTimeout(function () { copyBtn.innerHTML = original; }, 1600);
|
|
424
523
|
}
|
|
425
|
-
setLaunchStatus(copied ? "
|
|
524
|
+
setLaunchStatus(copied ? "Question copied." : "Could not copy the question automatically.");
|
|
426
525
|
}
|
|
427
526
|
function codexDeepLink(prompt) {
|
|
428
527
|
var q = "prompt=" + encodeURIComponent(prompt);
|
|
429
528
|
if (workspacePath) q += "&path=" + encodeURIComponent(workspacePath);
|
|
430
529
|
return "codex://new?" + q;
|
|
431
530
|
}
|
|
432
|
-
async function tryInCodex() {
|
|
433
|
-
var
|
|
434
|
-
|
|
435
|
-
setLaunchStatus(copied ? "Copied prompt. Opening Codex..." : "Opening Codex. If the prompt is empty, copy it manually.");
|
|
531
|
+
async function tryInCodex(prompt) {
|
|
532
|
+
var copied = await copyRecallPrompt(prompt);
|
|
533
|
+
setLaunchStatus(copied ? "Copied question. Opening Codex..." : "Opening Codex with your question...");
|
|
436
534
|
window.location.href = codexDeepLink(prompt);
|
|
437
535
|
}
|
|
438
|
-
async function openClaudeDesktop() {
|
|
439
|
-
var copied = await copyRecallPrompt();
|
|
440
|
-
setLaunchStatus(copied ? "Copied
|
|
536
|
+
async function openClaudeDesktop(prompt) {
|
|
537
|
+
var copied = await copyRecallPrompt(prompt);
|
|
538
|
+
setLaunchStatus(copied ? "Copied question. Opening Claude Desktop..." : "Opening Claude Desktop...");
|
|
441
539
|
try {
|
|
442
540
|
var res = await postJson("/launch-agent", { agent: "claude-desktop" }, 8000);
|
|
443
|
-
setLaunchStatus(res && res.message ? res.message : "
|
|
541
|
+
setLaunchStatus(res && res.message ? res.message : "Question copied. Paste it into Claude Desktop.");
|
|
444
542
|
} catch (error) {
|
|
445
543
|
var msg = error && error.message ? error.message : "Could not auto-open Claude.";
|
|
446
|
-
if (msg.indexOf("prompt") === -1 && msg.indexOf("Prompt") === -1) msg += " The
|
|
544
|
+
if (msg.indexOf("prompt") === -1 && msg.indexOf("Prompt") === -1) msg += " The question is copied - open Claude Desktop and paste it.";
|
|
447
545
|
setLaunchStatus(msg);
|
|
448
546
|
}
|
|
449
547
|
}
|
|
548
|
+
function candidateSessions() {
|
|
549
|
+
return stats && Array.isArray(stats.candidateSessions) ? stats.candidateSessions : [];
|
|
550
|
+
}
|
|
551
|
+
function historicalSelectionLimit() {
|
|
552
|
+
if (!setupPlanConfirmed()) return 0;
|
|
553
|
+
var quota = billingStatus && billingStatus.historicalConversationQuota;
|
|
554
|
+
if (quota && typeof quota.remaining === "number" && isFinite(quota.remaining)) {
|
|
555
|
+
return Math.max(0, Math.floor(quota.remaining));
|
|
556
|
+
}
|
|
557
|
+
var plan = String(billingStatus && billingStatus.plan || "").toLowerCase();
|
|
558
|
+
if (plan === "power" || plan === "enterprise") return 500;
|
|
559
|
+
if (paidRecallPlan(plan)) return 250;
|
|
560
|
+
return 100;
|
|
561
|
+
}
|
|
562
|
+
function selectedSessionKeyList() {
|
|
563
|
+
var valid = Object.create(null);
|
|
564
|
+
candidateSessions().forEach(function (session) { valid[String(session.key || "")] = true; });
|
|
565
|
+
return Object.keys(selectedSessionKeys).filter(function (key) { return selectedSessionKeys[key] && valid[key]; });
|
|
566
|
+
}
|
|
567
|
+
function ensureSessionSelection() {
|
|
568
|
+
var candidates = candidateSessions();
|
|
569
|
+
var valid = Object.create(null);
|
|
570
|
+
candidates.forEach(function (session) { valid[String(session.key || "")] = true; });
|
|
571
|
+
Object.keys(selectedSessionKeys).forEach(function (key) {
|
|
572
|
+
if (!valid[key]) delete selectedSessionKeys[key];
|
|
573
|
+
});
|
|
574
|
+
if (!setupPlanConfirmed()) return;
|
|
575
|
+
var limit = Math.min(candidates.length, historicalSelectionLimit());
|
|
576
|
+
if (!sessionSelectionTouched) {
|
|
577
|
+
selectedSessionKeys = Object.create(null);
|
|
578
|
+
candidates.slice(0, limit).forEach(function (session) { selectedSessionKeys[String(session.key)] = true; });
|
|
579
|
+
return;
|
|
580
|
+
}
|
|
581
|
+
var selected = selectedSessionKeyList();
|
|
582
|
+
if (selected.length > limit) {
|
|
583
|
+
var keep = Object.create(null);
|
|
584
|
+
var selectedSet = Object.create(null);
|
|
585
|
+
selected.forEach(function (key) { selectedSet[key] = true; });
|
|
586
|
+
candidates.forEach(function (session) {
|
|
587
|
+
var key = String(session.key || "");
|
|
588
|
+
if (selectedSet[key] && Object.keys(keep).length < limit) keep[key] = true;
|
|
589
|
+
});
|
|
590
|
+
selectedSessionKeys = keep;
|
|
591
|
+
}
|
|
592
|
+
}
|
|
593
|
+
function sessionSourceLabel(source) {
|
|
594
|
+
return source === "claude-code" ? "Claude Code" : "Codex";
|
|
595
|
+
}
|
|
596
|
+
function sessionDateLabel(value) {
|
|
597
|
+
if (!value) return "Date unavailable";
|
|
598
|
+
var date = new Date(value);
|
|
599
|
+
if (!isFinite(date.getTime())) return "Date unavailable";
|
|
600
|
+
return date.toLocaleDateString(undefined, { month: "short", day: "numeric", year: "numeric" });
|
|
601
|
+
}
|
|
602
|
+
function renderSessionSelection(canExtract, message) {
|
|
603
|
+
var slot = document.getElementById("sessionSelection");
|
|
604
|
+
if (!slot) return;
|
|
605
|
+
if (!canExtract || !setupPlanConfirmed()) {
|
|
606
|
+
slot.innerHTML = "";
|
|
607
|
+
slot.classList.add("is-hidden");
|
|
608
|
+
return;
|
|
609
|
+
}
|
|
610
|
+
slot.classList.remove("is-hidden");
|
|
611
|
+
var candidates = candidateSessions();
|
|
612
|
+
if (!candidates.length) {
|
|
613
|
+
slot.innerHTML = '<div class="sessionSelectionCard is-loading"><span class="sessionSelectionIcon" aria-hidden="true">' + setupIcon("list-check") + '</span><div><strong>Preparing your conversation list…</strong><p>Echo is reading titles and project names locally so you can review the selection.</p></div></div>';
|
|
614
|
+
return;
|
|
615
|
+
}
|
|
616
|
+
ensureSessionSelection();
|
|
617
|
+
var selected = selectedSessionKeyList();
|
|
618
|
+
var limit = Math.min(candidates.length, historicalSelectionLimit());
|
|
619
|
+
var planLimited = candidates.length > limit;
|
|
620
|
+
var summary = planLimited
|
|
621
|
+
? "Your plan can import " + number(limit) + " of " + number(candidates.length) + " available conversations."
|
|
622
|
+
: "All " + number(candidates.length) + " available conversations are included by default.";
|
|
623
|
+
slot.innerHTML =
|
|
624
|
+
'<section class="sessionSelectionCard" aria-labelledby="sessionSelectionTitle">' +
|
|
625
|
+
'<span class="sessionSelectionIcon" aria-hidden="true">' + setupIcon("list-check") + '</span>' +
|
|
626
|
+
'<div class="sessionSelectionCopy"><span class="sessionSelectionEyebrow">Conversations to import</span><strong id="sessionSelectionTitle">' + esc(number(selected.length)) + ' selected</strong><p>' + esc(message || summary) + '</p></div>' +
|
|
627
|
+
'<button type="button" class="secondary sessionEditButton" id="editSessions">Review & edit</button>' +
|
|
628
|
+
'</section>';
|
|
629
|
+
var edit = document.getElementById("editSessions");
|
|
630
|
+
if (edit) edit.onclick = openSessionPicker;
|
|
631
|
+
var migrateBtn = document.getElementById("migrate");
|
|
632
|
+
if (migrateBtn) migrateBtn.disabled = selected.length === 0 || !setupPlanConfirmed();
|
|
633
|
+
}
|
|
634
|
+
function updateSessionPickerSelectionUi(dialog, input, limit) {
|
|
635
|
+
var selectedCount = selectedSessionKeyList().length;
|
|
636
|
+
hideSessionPickerLimitNotice();
|
|
637
|
+
var row = input && input.closest ? input.closest(".sessionPickerRow") : null;
|
|
638
|
+
if (row) row.classList.toggle("is-selected", !!input.checked);
|
|
639
|
+
var count = dialog.querySelector(".sessionPickerActions strong");
|
|
640
|
+
if (count) count.textContent = number(selectedCount) + " / " + number(limit) + " selected";
|
|
641
|
+
var status = dialog.querySelector("#sessionPickerStatus");
|
|
642
|
+
if (status) status.textContent = "";
|
|
643
|
+
var confirm = dialog.querySelector("#confirmSessions");
|
|
644
|
+
if (confirm) {
|
|
645
|
+
confirm.disabled = selectedCount === 0;
|
|
646
|
+
confirm.textContent = "Use " + number(selectedCount) + " selected";
|
|
647
|
+
}
|
|
648
|
+
}
|
|
649
|
+
function sessionPickerLimitNotice(canDismiss) {
|
|
650
|
+
var plan = String(billingStatus && billingStatus.plan || "").toLowerCase();
|
|
651
|
+
var linkLabel = plan === "power" || plan === "team" || plan === "enterprise"
|
|
652
|
+
? "View plan options"
|
|
653
|
+
: "Upgrade to import more";
|
|
654
|
+
return '<div class="sessionPickerLimitCard">' +
|
|
655
|
+
'<span class="sessionPickerLimitEyebrow">Plan limit</span>' +
|
|
656
|
+
'<h3>Plan limit reached.</h3>' +
|
|
657
|
+
'<p>Upgrade your plan to select and import more conversations.</p>' +
|
|
658
|
+
'<div class="sessionPickerLimitActions"><a id="upgradeSessionPlan" class="button primary" href="' + esc(setupPricingUrl("pro", false)) + '" target="_blank" rel="noopener noreferrer">' + linkLabel + '</a>' +
|
|
659
|
+
(canDismiss ? '<button type="button" class="textButton" id="dismissSessionLimit">Keep editing</button>' : "") + '</div>' +
|
|
660
|
+
'</div>';
|
|
661
|
+
}
|
|
662
|
+
function wireSessionPickerUpgrade() {
|
|
663
|
+
var upgrade = document.getElementById("upgradeSessionPlan");
|
|
664
|
+
if (upgrade) upgrade.onclick = function () { startBillingPoll(); };
|
|
665
|
+
var dismiss = document.getElementById("dismissSessionLimit");
|
|
666
|
+
if (dismiss) dismiss.onclick = hideSessionPickerLimitNotice;
|
|
667
|
+
}
|
|
668
|
+
function showSessionPickerLimitNotice(canDismiss) {
|
|
669
|
+
var wrap = document.getElementById("sessionPickerListWrap");
|
|
670
|
+
var list = wrap && wrap.querySelector ? wrap.querySelector(".sessionPickerList") : null;
|
|
671
|
+
var overlay = document.getElementById("sessionPickerLimitOverlay");
|
|
672
|
+
var status = document.getElementById("sessionPickerStatus");
|
|
673
|
+
if (status) status.textContent = "Plan limit reached. Upgrade to import more.";
|
|
674
|
+
if (!wrap || !overlay) return;
|
|
675
|
+
overlay.innerHTML = sessionPickerLimitNotice(canDismiss);
|
|
676
|
+
overlay.hidden = false;
|
|
677
|
+
if (list) list.inert = true;
|
|
678
|
+
wrap.classList.add("is-limit-blocked");
|
|
679
|
+
wireSessionPickerUpgrade();
|
|
680
|
+
}
|
|
681
|
+
function hideSessionPickerLimitNotice() {
|
|
682
|
+
var wrap = document.getElementById("sessionPickerListWrap");
|
|
683
|
+
var list = wrap && wrap.querySelector ? wrap.querySelector(".sessionPickerList") : null;
|
|
684
|
+
var overlay = document.getElementById("sessionPickerLimitOverlay");
|
|
685
|
+
var status = document.getElementById("sessionPickerStatus");
|
|
686
|
+
if (status) status.textContent = "";
|
|
687
|
+
if (overlay) { overlay.hidden = true; overlay.innerHTML = ""; }
|
|
688
|
+
if (list) list.inert = false;
|
|
689
|
+
if (wrap) wrap.classList.remove("is-limit-blocked");
|
|
690
|
+
}
|
|
691
|
+
function renderSessionPicker() {
|
|
692
|
+
var dialog = document.getElementById("sessionPicker");
|
|
693
|
+
if (!dialog) return;
|
|
694
|
+
ensureSessionSelection();
|
|
695
|
+
var all = candidateSessions();
|
|
696
|
+
var selected = selectedSessionKeyList();
|
|
697
|
+
var limit = Math.min(all.length, historicalSelectionLimit());
|
|
698
|
+
var query = sessionPickerQuery.trim().toLowerCase();
|
|
699
|
+
var filtered = all.filter(function (session) {
|
|
700
|
+
if (sessionPickerSource !== "all" && session.source !== sessionPickerSource) return false;
|
|
701
|
+
if (!query) return true;
|
|
702
|
+
return [session.title, session.project, session.source, session.date].join(" ").toLowerCase().indexOf(query) >= 0;
|
|
703
|
+
});
|
|
704
|
+
var visible = filtered.slice(0, 250);
|
|
705
|
+
var rows = visible.map(function (session) {
|
|
706
|
+
var key = String(session.key || "");
|
|
707
|
+
var encodedKey = encodeURIComponent(key);
|
|
708
|
+
var checked = !!selectedSessionKeys[key];
|
|
709
|
+
var sourceIcon = session.source === "claude-code" ? "claude" : "codex";
|
|
710
|
+
var sourceFallback = session.source === "claude-code" ? "CL" : "CX";
|
|
711
|
+
return '<label class="sessionPickerRow' + (checked ? " is-selected" : "") + '">' +
|
|
712
|
+
'<input type="checkbox" data-session-key="' + esc(encodedKey) + '"' + (checked ? " checked" : "") + ' />' +
|
|
713
|
+
'<span class="sessionPickerSource ' + (session.source === "claude-code" ? "is-claude" : "is-codex") + '"><img src="/hud-assets/' + sourceIcon + '.svg" alt="" onerror="this.style.display="none";this.nextElementSibling.style.display="grid";" /><span class="sessionPickerSourceFallback">' + sourceFallback + '</span></span>' +
|
|
714
|
+
'<span class="sessionPickerMain"><strong>' + esc(session.title || "Untitled coding session") + '</strong><small>' + esc(session.project || "No project detected") + ' · ' + esc(sessionDateLabel(session.date)) + '</small></span>' +
|
|
715
|
+
'<span class="sessionPickerMeta">' + esc(compact(Number(session.approxInputTokens) || 0)) + ' tok</span>' +
|
|
716
|
+
'</label>';
|
|
717
|
+
}).join("");
|
|
718
|
+
var overflow = filtered.length > visible.length
|
|
719
|
+
? '<p class="sessionPickerOverflow">Showing the first 250 matches. Narrow the search to find older conversations.</p>'
|
|
720
|
+
: "";
|
|
721
|
+
dialog.innerHTML =
|
|
722
|
+
'<div class="sessionPickerShell">' +
|
|
723
|
+
'<header class="sessionPickerHead"><div><span>Bulk history import</span><h2 id="sessionPickerTitle">Choose conversations</h2><p>Select up to ' + esc(number(limit)) + '. Newest conversations are selected first.</p></div><button type="button" class="sessionPickerClose" id="closeSessionPicker" aria-label="Close">×</button></header>' +
|
|
724
|
+
'<div class="sessionPickerToolbar"><label class="sessionSearch"><span>Search</span><input type="search" id="sessionPickerSearch" placeholder="Title or project" value="' + esc(sessionPickerQuery) + '" /></label>' +
|
|
725
|
+
'<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>' +
|
|
726
|
+
'</div>' +
|
|
727
|
+
'<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
|
+
'<div class="sessionPickerListWrap' + (limit === 0 ? " is-limit-blocked" : "") + '" id="sessionPickerListWrap"><div class="sessionPickerList"' + (limit === 0 ? " inert" : "") + '>' + (rows || '<div class="sessionPickerEmpty">No conversations match these filters.</div>') + overflow + '</div><div class="sessionPickerLimitOverlay" id="sessionPickerLimitOverlay" role="alert"' + (limit === 0 ? "" : " hidden") + '>' + (limit === 0 ? sessionPickerLimitNotice(false) : "") + '</div></div>' +
|
|
729
|
+
'<footer class="sessionPickerFoot"><p>Only selected conversations will be sent for extraction.</p><div class="sessionPickerFootActions"><button type="button" class="secondary" id="skipSessionImport">Skip history import</button><button type="button" class="primary" id="confirmSessions"' + (selected.length ? "" : " disabled") + '>Use ' + esc(number(selected.length)) + ' selected</button></div></footer>' +
|
|
730
|
+
'</div>';
|
|
731
|
+
var close = document.getElementById("closeSessionPicker");
|
|
732
|
+
var confirm = document.getElementById("confirmSessions");
|
|
733
|
+
var skipImport = document.getElementById("skipSessionImport");
|
|
734
|
+
if (close) close.onclick = function () { dialog.close(); renderSessionSelection(true); };
|
|
735
|
+
if (confirm) confirm.onclick = function () { dialog.close(); renderSessionSelection(true); };
|
|
736
|
+
if (skipImport) skipImport.onclick = skip;
|
|
737
|
+
wireSessionPickerUpgrade();
|
|
738
|
+
var search = document.getElementById("sessionPickerSearch");
|
|
739
|
+
if (search) search.oninput = function () {
|
|
740
|
+
sessionPickerQuery = search.value || "";
|
|
741
|
+
renderSessionPicker();
|
|
742
|
+
var next = document.getElementById("sessionPickerSearch");
|
|
743
|
+
if (next) { next.focus(); next.setSelectionRange(next.value.length, next.value.length); }
|
|
744
|
+
};
|
|
745
|
+
var source = document.getElementById("sessionPickerSource");
|
|
746
|
+
if (source) source.onchange = function () { sessionPickerSource = source.value || "all"; renderSessionPicker(); };
|
|
747
|
+
Array.prototype.forEach.call(dialog.querySelectorAll("[data-session-key]"), function (input) {
|
|
748
|
+
input.onchange = function () {
|
|
749
|
+
var key = decodeURIComponent(input.getAttribute("data-session-key") || "");
|
|
750
|
+
var count = selectedSessionKeyList().length;
|
|
751
|
+
if (input.checked && !selectedSessionKeys[key] && count >= limit) {
|
|
752
|
+
input.checked = false;
|
|
753
|
+
showSessionPickerLimitNotice(limit > 0);
|
|
754
|
+
return;
|
|
755
|
+
}
|
|
756
|
+
sessionSelectionTouched = true;
|
|
757
|
+
if (input.checked) selectedSessionKeys[key] = true;
|
|
758
|
+
else delete selectedSessionKeys[key];
|
|
759
|
+
// Keep the existing list DOM so selecting a row does not reset its scroll position.
|
|
760
|
+
updateSessionPickerSelectionUi(dialog, input, limit);
|
|
761
|
+
};
|
|
762
|
+
});
|
|
763
|
+
var newest = document.getElementById("selectNewestSessions");
|
|
764
|
+
if (newest) newest.onclick = function () {
|
|
765
|
+
sessionSelectionTouched = true;
|
|
766
|
+
selectedSessionKeys = Object.create(null);
|
|
767
|
+
all.slice(0, limit).forEach(function (session) { selectedSessionKeys[String(session.key)] = true; });
|
|
768
|
+
renderSessionPicker();
|
|
769
|
+
};
|
|
770
|
+
var clear = document.getElementById("clearSessions");
|
|
771
|
+
if (clear) clear.onclick = function () { sessionSelectionTouched = true; selectedSessionKeys = Object.create(null); renderSessionPicker(); };
|
|
772
|
+
}
|
|
773
|
+
function openSessionPicker() {
|
|
774
|
+
var dialog = document.getElementById("sessionPicker");
|
|
775
|
+
if (!dialog) return;
|
|
776
|
+
renderSessionPicker();
|
|
777
|
+
if (typeof dialog.showModal === "function") dialog.showModal();
|
|
778
|
+
else dialog.setAttribute("open", "");
|
|
779
|
+
}
|
|
780
|
+
function accountInitials(account) {
|
|
781
|
+
var label = String(account && (account.displayName || account.email) || "EchoMem").trim();
|
|
782
|
+
return label.split(/\s+/).slice(0, 2).map(function (part) { return part.charAt(0).toUpperCase(); }).join("") || "E";
|
|
783
|
+
}
|
|
784
|
+
function renderAccountChoice() {
|
|
785
|
+
var slot = document.getElementById("setupAccount");
|
|
786
|
+
if (!slot) return;
|
|
787
|
+
var account = billingStatus && billingStatus.account;
|
|
788
|
+
var name = account && account.displayName ? account.displayName : "Connected EchoMem account";
|
|
789
|
+
var email = account && account.email ? account.email : "This import will be saved to the account connected on this device.";
|
|
790
|
+
var avatarUrl = account && account.avatarUrl ? String(account.avatarUrl) : "";
|
|
791
|
+
var avatar = '<span class="setupAccountAvatar" aria-hidden="true">' +
|
|
792
|
+
'<span>' + esc(accountInitials(account)) + '</span>' +
|
|
793
|
+
(avatarUrl ? '<img src="' + esc(avatarUrl) + '" alt="" onerror="this.remove()" />' : '') +
|
|
794
|
+
'</span>';
|
|
795
|
+
slot.innerHTML =
|
|
796
|
+
'<section class="setupAccountCard" aria-label="Connected EchoMem account">' +
|
|
797
|
+
avatar +
|
|
798
|
+
'<span class="setupAccountCopy"><small>Saving to</small><strong>' + esc(name) + '</strong><span>' + esc(email) + '</span></span>' +
|
|
799
|
+
'<button type="button" class="textButton setupAccountChange" id="changeSetupAccount">Change account</button>' +
|
|
800
|
+
'</section>';
|
|
801
|
+
var change = document.getElementById("changeSetupAccount");
|
|
802
|
+
if (change) change.onclick = switchSetupAccount;
|
|
803
|
+
}
|
|
804
|
+
async function switchSetupAccount() {
|
|
805
|
+
var button = document.getElementById("changeSetupAccount");
|
|
806
|
+
if (button) { button.disabled = true; button.textContent = "Opening account chooser…"; }
|
|
807
|
+
try {
|
|
808
|
+
await postJson("/logout", {}, 12000);
|
|
809
|
+
stats = null;
|
|
810
|
+
billingStatus = null;
|
|
811
|
+
connected = false;
|
|
812
|
+
selectedSessionKeys = Object.create(null);
|
|
813
|
+
sessionSelectionTouched = false;
|
|
814
|
+
if (!openAuthWindow(switchAccountUrl || authUrl)) {
|
|
815
|
+
if (button) { button.disabled = false; button.textContent = "Change account"; }
|
|
816
|
+
showPopupFallback("Your browser blocked the account chooser. Open it below to continue.", switchAccountUrl || authUrl);
|
|
817
|
+
return;
|
|
818
|
+
}
|
|
819
|
+
startConnectionPoll();
|
|
820
|
+
} catch (error) {
|
|
821
|
+
if (button) { button.disabled = false; button.textContent = "Change account"; }
|
|
822
|
+
setSetupPlanStatus(error && error.message ? error.message : "Could not open the account chooser.");
|
|
823
|
+
}
|
|
824
|
+
}
|
|
450
825
|
function paidRecallPlan(plan) {
|
|
451
826
|
return ["pro", "power", "team", "enterprise"].indexOf(String(plan || "").toLowerCase()) >= 0;
|
|
452
827
|
}
|
|
453
|
-
function
|
|
828
|
+
function setupPricingUrl(plan, startTrial) {
|
|
829
|
+
var paidPlan = plan === "power" ? "power" : "pro";
|
|
454
830
|
return (billingStatus && billingStatus.pricingUrl)
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
function recallTrialAvailable() {
|
|
461
|
-
return !billingStatus || billingStatus.trialAvailable !== false;
|
|
462
|
-
}
|
|
463
|
-
function recallGateBlock() {
|
|
464
|
-
var unlocked = recallGateUnlocked();
|
|
465
|
-
var trialAvailable = recallTrialAvailable();
|
|
466
|
-
return '<div class="recallGateBlock">' +
|
|
467
|
-
'<section class="recallGate' + (unlocked ? " is-hidden" : "") + '" id="recallGate">' +
|
|
468
|
-
'<p class="recallEyebrow"><i></i>Premium recall</p>' +
|
|
469
|
-
'<h3 id="recallGateTitle">' + (trialAvailable ? "Unlock memory search for your agents." : "Ready to turn recall back on?") + '</h3>' +
|
|
470
|
-
'<p id="recallGateBody">' + (trialAvailable ? "Start a 14-day trial and let Codex, Claude, and MCP clients search what Echo saved. Saving conversations keeps working either way." : "Choose Pro or Power to let Codex, Claude, and MCP clients search what Echo saved. Saving conversations keeps working either way.") + '</p>' +
|
|
471
|
-
'<div class="recallTrial' + (trialAvailable ? "" : " is-hidden") + '" id="recallTrialBadge" aria-label="14 days free"><strong>14</strong><span>days free<br>then choose what fits</span></div>' +
|
|
472
|
-
'<p class="recallUsedNote' + (trialAvailable ? " is-hidden" : "") + '" id="recallUsedNote">Your trial was already used. You can still unlock recall by choosing Pro or Power.</p>' +
|
|
473
|
-
'<div class="recallGateActions">' +
|
|
474
|
-
'<button type="button" class="primary" id="recallStartTrial">' + (trialAvailable ? "Compare Pro and Power" : "See pricing") + '</button>' +
|
|
475
|
-
'<button type="button" class="textButton" id="recallSkip">Skip recall for now</button>' +
|
|
476
|
-
'</div>' +
|
|
477
|
-
'<p class="recallFine" id="recallFine">' + (trialAvailable ? "Card required for trial. Cancel anytime." : "Cancel anytime.") + '</p>' +
|
|
478
|
-
'<p class="launchStatus" id="recallGateStatus"></p>' +
|
|
479
|
-
'</section>' +
|
|
480
|
-
'<div id="recallTryWrap" class="recallTryWrap' + (unlocked ? "" : " is-hidden") + '">' +
|
|
481
|
-
'<p id="recallSkipNote" class="recallSkipNote' + (recallGateSkipped ? "" : " is-hidden") + '">Saving will keep working. Search and recall require Pro or Power when this prompt calls <code>search_memories</code>.</p>' +
|
|
482
|
-
tryItCard() +
|
|
483
|
-
'</div>' +
|
|
484
|
-
'</div>';
|
|
831
|
+
? billingStatus.pricingUrl + (startTrial ? "&start=" + paidPlan + "&trial=" + (billingStatus.trialAvailable === false ? "0" : "1") : "")
|
|
832
|
+
: "https://echoknows.com/pricing?source=mcp_onboarding" + (startTrial ? "&start=" + paidPlan + "&trial=1" : "");
|
|
833
|
+
}
|
|
834
|
+
function setupPlanConfirmed() {
|
|
835
|
+
return setupPlanChoice === "free" || paidRecallPlan(billingStatus && billingStatus.plan);
|
|
485
836
|
}
|
|
486
|
-
function
|
|
487
|
-
var node = document.getElementById("
|
|
837
|
+
function setSetupPlanStatus(message) {
|
|
838
|
+
var node = document.getElementById("setupPlanStatus");
|
|
488
839
|
if (node) node.textContent = message || "";
|
|
489
840
|
}
|
|
490
|
-
function
|
|
491
|
-
var
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
var
|
|
500
|
-
|
|
501
|
-
var
|
|
502
|
-
if (
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
841
|
+
function renderSetupPlan(canExtract) {
|
|
842
|
+
var slot = document.getElementById("setupPlan");
|
|
843
|
+
if (!slot) return;
|
|
844
|
+
if (!canExtract) {
|
|
845
|
+
slot.innerHTML = "";
|
|
846
|
+
slot.classList.add("is-hidden");
|
|
847
|
+
return;
|
|
848
|
+
}
|
|
849
|
+
slot.classList.remove("is-hidden");
|
|
850
|
+
var plan = String(billingStatus && billingStatus.plan || "").toLowerCase();
|
|
851
|
+
var paid = paidRecallPlan(plan);
|
|
852
|
+
var migrateBtn = document.getElementById("migrate");
|
|
853
|
+
if (paid) {
|
|
854
|
+
setupPlanChoice = "paid";
|
|
855
|
+
var planLabel = plan.charAt(0).toUpperCase() + plan.slice(1);
|
|
856
|
+
var paidHistory = billingStatus && billingStatus.historicalConversationQuota;
|
|
857
|
+
var paidUsage = paidHistory && typeof paidHistory.used === "number" && typeof paidHistory.limit === "number"
|
|
858
|
+
? paidHistory.used + " / " + paidHistory.limit + " bulk history imports used."
|
|
859
|
+
: "Your bulk history import and weekly new-chat allowance are ready.";
|
|
860
|
+
slot.innerHTML = '<div class="setupPlanConfirmed"><span class="setupPlanConfirmedIcon" aria-hidden="true">' + setupIcon("check") + '</span><span class="setupPlanConfirmedCopy"><strong>' + esc(planLabel) + ' is active.</strong><span>' + esc(paidUsage) + '</span></span></div>';
|
|
861
|
+
renderSessionSelection(true);
|
|
862
|
+
stopBillingPoll();
|
|
863
|
+
return;
|
|
864
|
+
}
|
|
865
|
+
if (setupPlanChoice === "free") {
|
|
866
|
+
slot.innerHTML = '<div class="setupPlanConfirmed"><span class="setupPlanConfirmedIcon" aria-hidden="true">' + setupIcon("check") + '</span><span class="setupPlanConfirmedCopy"><strong>Free selected.</strong><span>100 bulk history imports, 25K new-chat tokens each week, and 10 memory searches each week.</span></span><button type="button" class="textButton" id="changeSetupPlan">Change</button></div>';
|
|
867
|
+
var changeBtn = document.getElementById("changeSetupPlan");
|
|
868
|
+
if (changeBtn) changeBtn.onclick = function () { setupPlanChoice = ""; renderSetupPlan(true); renderSessionSelection(true); };
|
|
869
|
+
renderSessionSelection(true);
|
|
870
|
+
return;
|
|
871
|
+
}
|
|
872
|
+
var trialAvailable = !billingStatus || billingStatus.trialAvailable !== false;
|
|
873
|
+
slot.innerHTML =
|
|
874
|
+
'<section class="setupPlanChoice" aria-labelledby="setupPlanHeading">' +
|
|
875
|
+
'<div class="setupPlanIntro"><h3 id="setupPlanHeading">Choose a plan before extraction starts.</h3><p>Plans set how much history you can import and how much new conversation text Echo processes each week. There is no memory-count limit.</p></div>' +
|
|
876
|
+
'<div class="setupPlanGrid">' +
|
|
877
|
+
'<article class="setupPlanCard">' +
|
|
878
|
+
'<div class="setupPlanHead"><strong>Free</strong><span>$0 forever</span></div>' +
|
|
879
|
+
'<p>Start with the essentials.</p>' +
|
|
880
|
+
'<ul><li>' + setupIcon("check") + '<span>100 bulk history imports</span></li><li>' + setupIcon("check") + '<span>25K new-chat tokens each week</span></li><li>' + setupIcon("check") + '<span>10 memory searches each week</span></li><li>' + setupIcon("check") + '<span>No memory-count limit</span></li></ul>' +
|
|
881
|
+
'<button type="button" class="secondary" id="chooseFreePlan">Continue with Free</button>' +
|
|
882
|
+
'</article>' +
|
|
883
|
+
'<article class="setupPlanCard is-pro">' +
|
|
884
|
+
'<div class="setupPlanHead"><strong>Pro</strong><span>$40 / month</span></div>' +
|
|
885
|
+
'<p>More recall for serious work.</p>' +
|
|
886
|
+
'<ul><li>' + setupIcon("check") + '<span>250 bulk history imports</span></li><li>' + setupIcon("check") + '<span>200K new-chat tokens each week</span></li><li>' + setupIcon("check") + '<span>100 memory searches each week</span></li><li>' + setupIcon("check") + '<span>No memory-count limit</span></li></ul>' +
|
|
887
|
+
'<button type="button" class="primary" id="chooseProPlan">' + (trialAvailable ? "Start 7-day trial" : "Choose Pro") + '</button>' +
|
|
888
|
+
'<small>' + (trialAvailable ? "Card required. Cancel anytime." : "Your trial was already used.") + '</small>' +
|
|
889
|
+
'</article>' +
|
|
890
|
+
'<article class="setupPlanCard">' +
|
|
891
|
+
'<div class="setupPlanHead"><strong>Power</strong><span>$100 / month</span></div>' +
|
|
892
|
+
'<p>Higher limits for people who use Echo every day.</p>' +
|
|
893
|
+
// Future Power feature: API and automation access.
|
|
894
|
+
'<ul><li>' + setupIcon("check") + '<span>500 bulk history imports</span></li><li>' + setupIcon("check") + '<span>750K new-chat tokens each week</span></li><li>' + setupIcon("check") + '<span>250 memory searches each week</span></li><li>' + setupIcon("check") + '<span>No memory-count limit</span></li></ul>' +
|
|
895
|
+
'<button type="button" class="secondary" id="choosePowerPlan">' + (trialAvailable ? "Start 7-day trial" : "Choose Power") + '</button>' +
|
|
896
|
+
'<small>' + (trialAvailable ? "Card required. Cancel anytime." : "Your trial was already used.") + '</small>' +
|
|
897
|
+
'</article>' +
|
|
898
|
+
'</div>' +
|
|
899
|
+
'<div class="setupPlanFoot"><a href="' + esc(setupPricingUrl("pro", false)) + '" target="_blank" rel="noopener noreferrer">See all plan details</a><span id="setupPlanStatus" role="status"></span></div>' +
|
|
900
|
+
'</section>';
|
|
901
|
+
var freeBtn = document.getElementById("chooseFreePlan");
|
|
902
|
+
if (freeBtn) freeBtn.onclick = function () { setupPlanChoice = "free"; renderSetupPlan(true); renderSessionSelection(true); };
|
|
903
|
+
var proBtn = document.getElementById("chooseProPlan");
|
|
904
|
+
if (proBtn) proBtn.onclick = function () { openPaidPricing("pro"); };
|
|
905
|
+
var powerBtn = document.getElementById("choosePowerPlan");
|
|
906
|
+
if (powerBtn) powerBtn.onclick = function () { openPaidPricing("power"); };
|
|
907
|
+
if (migrateBtn) migrateBtn.disabled = true;
|
|
908
|
+
renderSessionSelection(true);
|
|
513
909
|
}
|
|
514
910
|
async function refreshBillingStatus() {
|
|
911
|
+
if (billingStatusLoading) return;
|
|
912
|
+
billingStatusLoading = true;
|
|
515
913
|
try {
|
|
516
914
|
billingStatus = await getJson("/billing-status");
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
915
|
+
renderSetupPlan(true);
|
|
916
|
+
renderAccountChoice();
|
|
917
|
+
renderSessionSelection(true);
|
|
918
|
+
if (billingStatus && billingStatus.plan === "unknown") setSetupPlanStatus("Plan check unavailable. Free still works.");
|
|
521
919
|
} catch (_) {
|
|
522
|
-
|
|
920
|
+
setSetupPlanStatus("Plan check unavailable. Free still works.");
|
|
921
|
+
} finally {
|
|
922
|
+
billingStatusLoading = false;
|
|
523
923
|
}
|
|
524
924
|
}
|
|
525
|
-
function
|
|
526
|
-
|
|
527
|
-
|
|
925
|
+
function refreshBillingOnFocus() { void refreshBillingStatus(); }
|
|
926
|
+
function startBillingPoll() {
|
|
927
|
+
if (billingPollTimer) return;
|
|
928
|
+
window.addEventListener("focus", refreshBillingOnFocus);
|
|
929
|
+
billingPollTimer = window.setInterval(function () { void refreshBillingStatus(); }, 2500);
|
|
930
|
+
}
|
|
931
|
+
function stopBillingPoll() {
|
|
932
|
+
if (billingPollTimer) window.clearInterval(billingPollTimer);
|
|
933
|
+
billingPollTimer = null;
|
|
934
|
+
window.removeEventListener("focus", refreshBillingOnFocus);
|
|
935
|
+
}
|
|
936
|
+
function openPaidPricing(plan) {
|
|
937
|
+
var url = setupPricingUrl(plan, true);
|
|
938
|
+
setSetupPlanStatus("Opening secure checkout…");
|
|
528
939
|
try {
|
|
529
|
-
var opened = window.open(url, "_blank"
|
|
530
|
-
if (!opened)
|
|
940
|
+
var opened = window.open(url, "_blank");
|
|
941
|
+
if (!opened) {
|
|
942
|
+
setSetupPlanStatus("Your browser blocked checkout. Allow pop-ups, then try again.");
|
|
943
|
+
return;
|
|
944
|
+
}
|
|
945
|
+
try { opened.opener = null; } catch (_) {}
|
|
946
|
+
startBillingPoll();
|
|
531
947
|
} catch (_) {
|
|
532
|
-
|
|
948
|
+
setSetupPlanStatus("Could not open checkout. Allow pop-ups, then try again.");
|
|
533
949
|
}
|
|
534
950
|
}
|
|
535
|
-
function skipRecallGate() {
|
|
536
|
-
recallGateSkipped = true;
|
|
537
|
-
applyRecallGate();
|
|
538
|
-
}
|
|
539
|
-
function wireRecallGate() {
|
|
540
|
-
var trialBtn = document.getElementById("recallStartTrial");
|
|
541
|
-
if (trialBtn) trialBtn.addEventListener("click", openRecallPricing);
|
|
542
|
-
var skipBtn = document.getElementById("recallSkip");
|
|
543
|
-
if (skipBtn) skipBtn.addEventListener("click", skipRecallGate);
|
|
544
|
-
wireTryButtons();
|
|
545
|
-
void refreshBillingStatus();
|
|
546
|
-
}
|
|
547
951
|
function tryItCard() {
|
|
548
|
-
var
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
'<
|
|
552
|
-
'<div class="
|
|
553
|
-
'<
|
|
952
|
+
var questions = recallQuestions();
|
|
953
|
+
var prompts = questions.map(function (question, index) {
|
|
954
|
+
var numberLabel = String(index + 1).padStart(2, "0");
|
|
955
|
+
return '<article class="recallPrompt" data-recall-prompt="' + esc(question) + '">' +
|
|
956
|
+
'<div class="recallPromptHead">' +
|
|
957
|
+
'<span>Suggested prompt ' + numberLabel + '</span>' +
|
|
958
|
+
'<button type="button" class="recallCopy" data-recall-copy aria-label="Copy suggested prompt ' + (index + 1) + '">' + setupIcon("copy") + '<span>Copy</span></button>' +
|
|
554
959
|
'</div>' +
|
|
555
|
-
'<
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
'
|
|
560
|
-
'</
|
|
960
|
+
'<p class="recallPromptText">' + esc(question) + '</p>' +
|
|
961
|
+
'<div class="recallPromptActions" aria-label="Use suggested prompt ' + (index + 1) + '">' +
|
|
962
|
+
'<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>' +
|
|
963
|
+
'<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>' +
|
|
964
|
+
'</div>' +
|
|
965
|
+
'</article>';
|
|
966
|
+
}).join("");
|
|
967
|
+
return '<div class="tryCard">' +
|
|
968
|
+
'<p class="tryK">First recall</p>' +
|
|
969
|
+
'<h3 class="tryTitle">Try your new memory</h3>' +
|
|
970
|
+
'<p class="tryLead">These prompts are ready to use. Copy one, or ask it directly in your coding agent.</p>' +
|
|
971
|
+
'<div class="recallPromptList">' + prompts + '</div>' +
|
|
561
972
|
'<p class="launchStatus" id="launchStatus"></p>' +
|
|
562
973
|
'</div>';
|
|
563
974
|
}
|
|
564
975
|
function wireTryButtons() {
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
976
|
+
Array.prototype.forEach.call(document.querySelectorAll("[data-recall-prompt]"), function (row) {
|
|
977
|
+
var prompt = row.getAttribute("data-recall-prompt") || "";
|
|
978
|
+
var copyBtn = row.querySelector("[data-recall-copy]");
|
|
979
|
+
var codexBtn = row.querySelector("[data-recall-codex]");
|
|
980
|
+
var claudeBtn = row.querySelector("[data-recall-claude]");
|
|
981
|
+
if (copyBtn) copyBtn.addEventListener("click", function () { void copyPromptOnly(prompt, copyBtn); });
|
|
982
|
+
if (codexBtn) codexBtn.addEventListener("click", function () { void tryInCodex(prompt); });
|
|
983
|
+
if (claudeBtn) claudeBtn.addEventListener("click", function () { void openClaudeDesktop(prompt); });
|
|
984
|
+
});
|
|
571
985
|
}
|
|
572
986
|
function hudTip() {
|
|
573
|
-
return '<p class="hudTip"><img src="/hud-assets/echo-face-cutout.png" alt="" /><span><strong>Live HUD:</strong> Echo watches
|
|
987
|
+
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>';
|
|
574
988
|
}
|
|
575
989
|
function doneCloseNote() {
|
|
576
990
|
return '<p class="doneCloseNote">You can safely close this page. EchoMem is set up, and your memories are saved.</p>';
|
|
@@ -578,34 +992,39 @@ export const SETUP_PAGE_CLIENT_EXTRACTION = String.raw ` /* ---------- dash
|
|
|
578
992
|
function renderEnding() {
|
|
579
993
|
setCityMode(false);
|
|
580
994
|
setExtractMode(true);
|
|
995
|
+
// This view replaces the live extraction shell. Mark it unmounted so a failed
|
|
996
|
+
// pause can rebuild the progress page instead of trying to patch missing DOM.
|
|
997
|
+
extractMounted = false;
|
|
581
998
|
setHead("Ending extraction", "Ending");
|
|
582
999
|
app.className = "";
|
|
583
1000
|
app.innerHTML =
|
|
584
|
-
'<div class="miniLoad"
|
|
585
|
-
'<
|
|
586
|
-
'<
|
|
1001
|
+
'<div class="miniLoad">' +
|
|
1002
|
+
'<p class="utilityEyebrow">Memory extraction / closing</p>' +
|
|
1003
|
+
'<h2 class="siteHeadline">Stopping the <strong>remaining work</strong></h2>' +
|
|
1004
|
+
'<p>Echo is canceling every queued conversation and releasing its unused reservations. Anything already saved stays saved.</p>' +
|
|
587
1005
|
'<div class="miniBar"><i></i></div>' +
|
|
588
1006
|
'</div>';
|
|
589
1007
|
}
|
|
590
1008
|
function renderSkipped() {
|
|
591
1009
|
setCityMode(false);
|
|
592
1010
|
setExtractMode(true);
|
|
593
|
-
setHead("
|
|
1011
|
+
setHead("Extraction ended for now", "Ended");
|
|
594
1012
|
app.className = "";
|
|
595
1013
|
app.innerHTML =
|
|
596
1014
|
'<div class="skipStage">' +
|
|
597
|
-
'<
|
|
598
|
-
'<h2>You can come back anytime.</h2>' +
|
|
599
|
-
'<p>
|
|
600
|
-
|
|
1015
|
+
'<div class="pauseStatus" role="status"><span aria-hidden="true">' + setupIcon("pause") + '</span><small>Extraction ended for now</small></div>' +
|
|
1016
|
+
'<h2 class="siteHeadline">You can come back <strong>anytime.</strong></h2>' +
|
|
1017
|
+
'<p>All remaining queued work was canceled and its unused reservations were released. When you are ready, run <code>echomem-mcp init</code> again — it rebuilds the remaining list from your local sessions.</p>' +
|
|
1018
|
+
tryItCard() +
|
|
601
1019
|
hudTip() +
|
|
602
1020
|
'</div>';
|
|
603
|
-
|
|
1021
|
+
wireTryButtons();
|
|
604
1022
|
}
|
|
605
1023
|
async function endExtraction() {
|
|
606
1024
|
if (extractionEnded) return;
|
|
607
|
-
var ok = window.confirm("End extraction for now?
|
|
1025
|
+
var ok = window.confirm("End extraction for now? Echo will cancel all remaining queued work and release its unused reservations. Everything already captured stays saved.");
|
|
608
1026
|
if (!ok) return;
|
|
1027
|
+
extractionRecoveryMessage = "";
|
|
609
1028
|
extractionEnded = true;
|
|
610
1029
|
renderEnding();
|
|
611
1030
|
try {
|
|
@@ -614,18 +1033,23 @@ export const SETUP_PAGE_CLIENT_EXTRACTION = String.raw ` /* ---------- dash
|
|
|
614
1033
|
renderSkipped();
|
|
615
1034
|
} catch (error) {
|
|
616
1035
|
// A paused screen is a promise: do not show it until the bridge confirms that no more
|
|
617
|
-
// conversations will be scheduled.
|
|
1036
|
+
// conversations will be scheduled. Restore the last live extraction state immediately;
|
|
1037
|
+
// do not depend on another bridge request before leaving the ending screen.
|
|
618
1038
|
extractionEnded = false;
|
|
619
|
-
var message = error && error.message ? error.message : "Could not
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
1039
|
+
var message = error && error.message ? error.message : "Could not end extraction. It may still be running.";
|
|
1040
|
+
extractionRecoveryMessage = message;
|
|
1041
|
+
extractMounted = false;
|
|
1042
|
+
renderProgress(lastExtractionProgress || {
|
|
1043
|
+
status: "running",
|
|
1044
|
+
total: 0,
|
|
1045
|
+
completed: 0,
|
|
1046
|
+
failed: 0,
|
|
1047
|
+
extracted: 0,
|
|
1048
|
+
latest: "Extraction is still running."
|
|
1049
|
+
});
|
|
1050
|
+
// Rejoin live updates. If the bridge is temporarily unavailable, the restored page and
|
|
1051
|
+
// End for now button remain usable instead of being replaced by another blocking state.
|
|
1052
|
+
void pollProgress().catch(function () {});
|
|
629
1053
|
}
|
|
630
1054
|
}
|
|
631
1055
|
async function skip() {
|