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