@echomem/mcp 1.4.26 → 1.4.28

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.
@@ -28,12 +28,14 @@ export const MCP_SERVER_INSTRUCTIONS = [
28
28
  `If the user or local config expects a newer EchoMem MCP version than ${MCP_PACKAGE_VERSION}, update once with \`${MCP_UPDATE_ALL_COMMAND}\` and start a new MCP session.`,
29
29
  "Use echomem_update_status to check whether npm has a newer bridge; it is cached and non-blocking during normal tool listing.",
30
30
  "Do not auto-update on every MCP startup; this bridge is intentionally stable between explicit updates.",
31
+ "Before the final response for work that produced a durable decision, confirmed preference, implementation, resolved bug, commit, passing verification, release, or project milestone, call save_conversation. This is a private checkpoint and does not require a separate user request. Skip secrets, incidental chatter, and trivial read-only work. If an encrypted vault is locked, tell the user to run echomem-mcp unlock rather than silently skipping the checkpoint.",
32
+ "For a user with a company group, call get_group_session_sharing near session start or after the first qualifying private save. If this exact session has no decision, ask once whether memories saved from the session should be shared with the current group, then store only the explicit Yes/No answer with set_group_session_sharing. A Yes decision syncs eligible memories after each private save; a No decision keeps later saves private. Flagged memories stay private.",
31
33
  "For company-group sharing, use get_group_context for orientation; create_memory_group/create_group_invite/join_memory_group for membership; and prepare_group_publication as a no-publication preview.",
32
34
  "After joining or when profile fields are missing, use candidate memory evidence to propose a title and responsibility summary. Ask the user to confirm that proposal together with the publication preview, then call update_group_profile and complete_group_publication.",
33
- "Use owner-only /memories/timeline?memoryId=... links for the user's private search, flag, and publication-preview evidence. Use /company/memories?memoryId=... only for already-published group evidence.",
34
- "Each group-search result is one memory: preserve its Memory ID and direct echoknows.com link when citing it.",
35
+ "Use one canonical https://echoknows.com/memory/<memory-id> link for private, group, and friend evidence. Label it with the memory key; the site resolves the authorized representation.",
36
+ "Each search result is one memory: preserve its Memory ID and canonical echoknows.com link when citing it.",
35
37
  "During a publication preview, if an unflagged candidate appears sensitive, proactively ask whether the user wants to mark its exact ID for publication attention first. Explain that marking does not publish or change encryption; it means the agent will call it out and ask for detailed confirmation whenever a later publication includes it. Never auto-flag inferred sensitivity. For sensitive-topic flags, search and preview exact owned memories before confirmed flag_memories_for_publication_attention. Separate already-flagged candidates, state that nothing has been published yet, and offer to exclude them, review them separately, or first search for and mark similar sensitive owned memories.",
36
- "Never save an inferred group profile or complete a group publication without explicit confirmation, and never store or log an echo_grp_ invite code.",
38
+ "Never save an inferred group profile. Manual prepared publication requires explicit preview confirmation; flagged memories still require separate exact-memory confirmation. Never store or log an echo_grp_ invite code.",
37
39
  ].join(" ");
38
40
  export function withMcpVersion(description) {
39
41
  return `${description}\n\nEchoMem MCP bridge: ${MCP_PACKAGE_LABEL}. If this version is stale, update once with \`${MCP_UPDATE_ALL_COMMAND}\` (or add \`--client cursor|windsurf|claude-desktop|claude-code|codex\` for a single client), then start a new MCP session. Do not run updates repeatedly or on every startup.`;
@@ -0,0 +1,111 @@
1
+ import fs from "node:fs";
2
+ const SAVE_TOOL_RE = /^(?:mcp__[\w.-]+__)?save_conversation$/i;
3
+ const MUTATION_TOOL_RE = /(?:^|[._])(?:apply_patch|Edit|Write)$/i;
4
+ const DURABLE_COMMAND_RE = /\b(?:git\s+commit|npm\s+(?:run\s+)?(?:test|build|lint)|pnpm\s+(?:test|build|lint)|yarn\s+(?:test|build|lint)|pytest|cargo\s+test|go\s+test|deploy|publish)\b/i;
5
+ const DURABLE_LANGUAGE_RE = /\b(?:implemented|fixed|resolved|shipped|deployed|published|committed|milestone|decision|architecture|root cause|tests? pass(?:ed|ing)?|build pass(?:ed|ing)?)\b|(?:实现|修复|解决|发布|提交|里程碑|决定|架构|根因|测试通过|构建通过)/i;
6
+ const CHECKPOINT_REASON = [
7
+ "Run EchoMem's private completion checkpoint before finishing this turn.",
8
+ "Review the work that just completed. If it produced a durable decision, confirmed preference, implementation, resolved bug, commit, passing verification, release, or project milestone, call `save_conversation` now with a concise structured summary of the outcome, reasoning, relevant files, and constraints.",
9
+ "Do not save secrets or incidental chatter. If the work was not durable, do not call the tool and finish normally.",
10
+ "Private persistence happens first. For a user with a company group, call `get_group_session_sharing` near session start or after this first qualifying private save.",
11
+ "If this exact session has no decision, ask once whether memories saved from the session should be shared with the current group, then call `set_group_session_sharing` only with the explicit Yes/No answer. A Yes decision automatically syncs eligible memories after later private saves; a No decision keeps later saves private.",
12
+ "Flagged memories are withheld from automatic session sync and remain private.",
13
+ "If EchoMem reports that the encrypted vault is locked, tell the user to run `echomem-mcp unlock`; never silently skip a qualifying checkpoint.",
14
+ ].join(" ");
15
+ function currentTurnSlice(transcript) {
16
+ const lines = transcript.split(/\r?\n/);
17
+ let start = 0;
18
+ for (let index = lines.length - 1; index >= 0; index -= 1) {
19
+ const line = lines[index];
20
+ if (/"type"\s*:\s*"user_message"/.test(line)
21
+ || /"type"\s*:\s*"user"/.test(line)
22
+ || /"role"\s*:\s*"user"/.test(line)) {
23
+ start = index;
24
+ break;
25
+ }
26
+ }
27
+ return lines.slice(start).join("\n");
28
+ }
29
+ function visibleTextSize(value) {
30
+ return value
31
+ .replace(/"[^"]+"\s*:/g, "")
32
+ .replace(/[{}\[\]",\\]/g, "")
33
+ .trim()
34
+ .length;
35
+ }
36
+ function toolNamesFromTranscript(transcript) {
37
+ const names = [];
38
+ const visit = (value) => {
39
+ if (Array.isArray(value)) {
40
+ value.forEach(visit);
41
+ return;
42
+ }
43
+ if (typeof value !== "object" || value === null)
44
+ return;
45
+ const record = value;
46
+ const type = typeof record.type === "string" ? record.type : "";
47
+ if ((type === "function_call" || type === "tool_use" || type === "mcp_tool_call")
48
+ && typeof record.name === "string") {
49
+ names.push(record.name);
50
+ }
51
+ Object.values(record).forEach(visit);
52
+ };
53
+ for (const line of transcript.split(/\r?\n/)) {
54
+ try {
55
+ visit(JSON.parse(line));
56
+ }
57
+ catch {
58
+ // Hook transcripts are JSONL. Ignore a partial final line while Codex is still flushing it.
59
+ }
60
+ }
61
+ return names;
62
+ }
63
+ export function evaluateSaveCheckpoint(payload, transcript) {
64
+ if (payload.stop_hook_active === true) {
65
+ return { decision: "allow", reason: "already_checked" };
66
+ }
67
+ if (!transcript) {
68
+ return { decision: "allow", reason: "missing_transcript" };
69
+ }
70
+ const turn = currentTurnSlice(transcript);
71
+ const toolNames = toolNamesFromTranscript(turn);
72
+ if (toolNames.some((name) => SAVE_TOOL_RE.test(name))) {
73
+ return { decision: "allow", reason: "already_saved" };
74
+ }
75
+ const assistant = typeof payload.last_assistant_message === "string" ? payload.last_assistant_message : "";
76
+ const durable = toolNames.some((name) => MUTATION_TOOL_RE.test(name))
77
+ || DURABLE_COMMAND_RE.test(turn)
78
+ || DURABLE_LANGUAGE_RE.test(assistant)
79
+ || DURABLE_LANGUAGE_RE.test(turn)
80
+ || (visibleTextSize(turn) >= 1_200 && assistant.trim().length >= 240);
81
+ if (!durable) {
82
+ return { decision: "allow", reason: "not_durable" };
83
+ }
84
+ return { decision: "block", reason: CHECKPOINT_REASON };
85
+ }
86
+ export function runSaveCheckpointHook(input) {
87
+ let payload = {};
88
+ try {
89
+ const parsed = JSON.parse(input);
90
+ if (typeof parsed === "object" && parsed !== null)
91
+ payload = parsed;
92
+ }
93
+ catch {
94
+ return JSON.stringify({ continue: true });
95
+ }
96
+ const transcriptPath = typeof payload.transcript_path === "string" ? payload.transcript_path : "";
97
+ let transcript = null;
98
+ if (transcriptPath) {
99
+ try {
100
+ transcript = fs.readFileSync(transcriptPath, "utf8");
101
+ }
102
+ catch {
103
+ transcript = null;
104
+ }
105
+ }
106
+ const result = evaluateSaveCheckpoint(payload, transcript);
107
+ if (result.decision === "block") {
108
+ return JSON.stringify({ decision: "block", reason: result.reason });
109
+ }
110
+ return JSON.stringify({ continue: true });
111
+ }
@@ -32,6 +32,13 @@ export const SETUP_PAGE_CLIENT_CORE = String.raw ` var params = new URLSear
32
32
  var setupPlanChoice = "";
33
33
  var setupPlanPreview = "free";
34
34
  var billingActivationPendingPlan = "";
35
+ var pendingCheckoutSessionId = "";
36
+ var billingSyncLastAttemptAt = 0;
37
+ var billingSyncAttempts = 0;
38
+ var billingLastCheckedAt = 0;
39
+ var billingCheckMessage = "";
40
+ var firstRecallPollTimer = null;
41
+ var firstRecallPollStartedAt = 0;
35
42
  var selectedSessionKeys = Object.create(null);
36
43
  var sessionSelectionTouched = false;
37
44
  var sessionPickerQuery = "";
@@ -40,10 +47,19 @@ export const SETUP_PAGE_CLIENT_CORE = String.raw ` var params = new URLSear
40
47
  var localAuthEmail = "";
41
48
  var localAuthTermsAccepted = false;
42
49
  var localAuthAgeConfirmed = false;
50
+ var accountSwitchPending = false;
51
+ // Invalidates async work started for a previously connected account.
52
+ var accountStateEpoch = 0;
43
53
  var statsPollStarted = false;
44
54
  var reportPollStarted = false;
45
- var reportEstimateDeadline = 0;
46
- var reportEstimateTimer = null;
55
+ var reportEtaDeadline = 0;
56
+ var reportEtaDisplaySeconds = 0;
57
+ var reportEtaUpdatedAt = 0;
58
+ var reportEtaStage = "";
59
+ var reportEtaStageDone = 0;
60
+ var reportEtaStageElapsed = 0;
61
+ var reportEtaObservedRate = 0;
62
+ var reportEtaScanId = "";
47
63
  var readyStage = "";
48
64
  var NIGHT_HOURS = { 22: 1, 23: 1, 0: 1, 1: 1, 2: 1, 3: 1 };
49
65
 
@@ -52,6 +68,10 @@ export const SETUP_PAGE_CLIENT_CORE = String.raw ` var params = new URLSear
52
68
  if (storedSetupPlanChoice === "free" || storedSetupPlanChoice === "pro" || storedSetupPlanChoice === "power") {
53
69
  setupPlanChoice = storedSetupPlanChoice;
54
70
  }
71
+ var storedCheckoutSessionId = sessionStorage.getItem("echomem:checkout-session:" + nonce) || "";
72
+ if (/^cs_(?:test_|live_)?[A-Za-z0-9_]+$/.test(storedCheckoutSessionId)) {
73
+ pendingCheckoutSessionId = storedCheckoutSessionId;
74
+ }
55
75
  } catch (_) {}
56
76
 
57
77
  function esc(value) {
@@ -59,6 +79,11 @@ export const SETUP_PAGE_CLIENT_CORE = String.raw ` var params = new URLSear
59
79
  return { "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&#39;" }[ch];
60
80
  });
61
81
  }
82
+ function dataHandlingLink(label) {
83
+ return '<a class="dataHandlingLink" href="https://echoknows.com/privacy-policy" target="_blank" rel="noopener noreferrer">' +
84
+ esc(label || "How Echo handles your coding history") +
85
+ '</a>';
86
+ }
62
87
  function rememberSetupPlanChoice(choice) {
63
88
  setupPlanChoice = choice === "free" || choice === "pro" || choice === "power" ? choice : "";
64
89
  try {
@@ -66,6 +91,18 @@ export const SETUP_PAGE_CLIENT_CORE = String.raw ` var params = new URLSear
66
91
  else sessionStorage.removeItem("echomem:setup-plan:" + nonce);
67
92
  } catch (_) {}
68
93
  }
94
+ function rememberCheckoutSessionId(sessionId) {
95
+ pendingCheckoutSessionId = /^cs_(?:test_|live_)?[A-Za-z0-9_]+$/.test(String(sessionId || ""))
96
+ ? String(sessionId)
97
+ : "";
98
+ try {
99
+ if (pendingCheckoutSessionId) {
100
+ sessionStorage.setItem("echomem:checkout-session:" + nonce, pendingCheckoutSessionId);
101
+ } else {
102
+ sessionStorage.removeItem("echomem:checkout-session:" + nonce);
103
+ }
104
+ } catch (_) {}
105
+ }
69
106
  function setupIcon(name) {
70
107
  var paths = {
71
108
  "shield-check": '<path d="M12 3 5 6v5c0 4.6 2.9 8.1 7 10 4.1-1.9 7-5.4 7-10V6l-7-3Z"/><path d="m8.8 12.1 2 2 4.4-4.5"/>',
@@ -80,6 +117,18 @@ export const SETUP_PAGE_CLIENT_CORE = String.raw ` var params = new URLSear
80
117
  };
81
118
  return '<svg class="setupGlyph" viewBox="0 0 24 24" fill="none" aria-hidden="true" focusable="false"><g stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round">' + (paths[name] || "") + '</g></svg>';
82
119
  }
120
+ function setupJourney(currentStep) {
121
+ var labels = ["Scan", "Report", "Connect", "Import", "Try Echo"];
122
+ var items = labels.map(function (label, index) {
123
+ var step = index + 1;
124
+ var state = step < currentStep ? " is-complete" : (step === currentStep ? " is-current" : "");
125
+ return '<li class="setupJourneyStep' + state + '"' + (step === currentStep ? ' aria-current="step"' : "") + '>' +
126
+ '<span>' + (step < currentStep ? setupIcon("check") : esc(step)) + '</span>' +
127
+ '<b>' + esc(label) + '</b>' +
128
+ '</li>';
129
+ }).join("");
130
+ return '<nav class="setupJourney" aria-label="EchoMem setup progress"><ol>' + items + '</ol></nav>';
131
+ }
83
132
  function setHead(nextTitle, nextStatus) {
84
133
  title.textContent = nextTitle;
85
134
  status.textContent = nextStatus;
@@ -179,7 +228,7 @@ export const SETUP_PAGE_CLIENT_CORE = String.raw ` var params = new URLSear
179
228
  function localAuthLegalHtml() {
180
229
  return '<label class="localAuthConsent">' +
181
230
  '<input type="checkbox" id="localAuthConsent" ' + (localAuthAgeConfirmed && localAuthTermsAccepted ? "checked" : "") + ' />' +
182
- '<span>I confirm that I am at least 18 years old and that I have read and agree to Echo\'s <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>. See our <a href="https://echoknows.com/subprocessor-list" target="_blank" rel="noopener noreferrer">Subprocessor List</a>.</span>' +
231
+ '<span>I’m at least 18 and agree to Echo\'s <a href="https://echoknows.com/terms-of-use" target="_blank" rel="noopener noreferrer">Terms</a>, <a href="https://echoknows.com/memory-terms" target="_blank" rel="noopener noreferrer">Memory Terms</a>, and <a href="https://echoknows.com/privacy-policy" target="_blank" rel="noopener noreferrer">Privacy Policy</a>. <a href="https://echoknows.com/subprocessor-list" target="_blank" rel="noopener noreferrer">Subprocessors</a></span>' +
183
232
  '</label>';
184
233
  }
185
234
  function captureLocalAuthConsents() {
@@ -209,24 +258,28 @@ export const SETUP_PAGE_CLIENT_CORE = String.raw ` var params = new URLSear
209
258
  button.disabled = !value || !localAuthTermsAccepted || !localAuthAgeConfirmed;
210
259
  }
211
260
  function renderLocalLogin(message) {
261
+ var onboardingConnect = setupFlow !== "login";
212
262
  setCityMode(false);
213
263
  setScanMode(false);
214
264
  setExtractMode(false);
215
265
  setReadyMode(true);
216
266
  setHead("Connect EchoMem", "Local login");
217
- app.className = "localAuthStage localAuthWelcomeStage";
267
+ app.className = "localAuthStage localAuthWelcomeStage localAuthEmailStage";
218
268
  app.innerHTML =
269
+ setupJourney(3) +
219
270
  '<section class="localAuthWelcome" aria-labelledby="localAuthWelcomeTitle">' +
220
271
  '<div class="localAuthOrbScene" aria-hidden="true"><span class="localAuthOrb"></span><span class="localAuthOrbShadow"></span></div>' +
221
272
  '<div class="localAuthWelcomeCopy">' +
222
- '<h2 id="localAuthWelcomeTitle">Welcome to Echo</h2>' +
223
- '<p>Your memories, sovereign and shared.</p>' +
273
+ '<h2 id="localAuthWelcomeTitle">' + (onboardingConnect ? 'Take your context with you.' : 'Welcome to Echo') + '</h2>' +
274
+ (onboardingConnect
275
+ ? '<p class="localAuthConnectPurpose">Connect this Mac to Echo to use the memories you choose across your AI tools.</p>'
276
+ : '') +
224
277
  '</div>' +
225
278
  '<form class="localAuthWelcomeForm" id="localAuthEmailForm">' +
226
279
  '<div class="localAuthEmailCapsule">' +
227
280
  '<label class="srOnly" for="localAuthEmail">Email</label>' +
228
281
  '<input id="localAuthEmail" type="email" autocomplete="email" placeholder="Enter your email" value="' + esc(localAuthEmail) + '" />' +
229
- '<button id="localAuthSend" type="submit" aria-label="Send one-time code"><span aria-hidden="true">→</span></button>' +
282
+ '<button id="localAuthSend" type="submit">Email me a code</button>' +
230
283
  '</div>' +
231
284
  localAuthLegalHtml() +
232
285
  '<p class="localAuthStatus' + (message ? " is-error" : "") + '" id="localAuthStatus" role="status" aria-live="polite">' + esc(message || "") + '</p>' +
@@ -254,9 +307,8 @@ export const SETUP_PAGE_CLIENT_CORE = String.raw ` var params = new URLSear
254
307
  '<section class="localAuthCard localAuthComplete">' +
255
308
  '<div class="localAuthLead">' +
256
309
  '<img src="/hud-assets/echo-face-cutout.png" alt="" />' +
257
- '<p class="consentEyebrow">This device is connected</p>' +
258
310
  '<h2 class="siteHeadline">You\'re signed in.</h2>' +
259
- '<p>Your local device token and encryption key are ready. Return to Terminal, or run <code>echomem-mcp init</code> when you want to start local-history onboarding.</p>' +
311
+ '<p>Return to Terminal, or run <code>echomem-mcp init</code> to start onboarding.</p>' +
260
312
  '</div>' +
261
313
  '</section>';
262
314
  }
@@ -280,7 +332,7 @@ export const SETUP_PAGE_CLIENT_CORE = String.raw ` var params = new URLSear
280
332
  return;
281
333
  }
282
334
  var button = document.getElementById("localAuthSend");
283
- if (button) { button.disabled = true; button.innerHTML = '<span class="localAuthSpinner" aria-hidden="true"></span>'; }
335
+ if (button) { button.disabled = true; button.innerHTML = '<span class="localAuthSpinner" aria-hidden="true"></span><span>Sending…</span>'; }
284
336
  setLocalAuthStatus("Sending verification code...");
285
337
  try {
286
338
  var data = await postJson("/local-auth/send-otp", {
@@ -291,7 +343,7 @@ export const SETUP_PAGE_CLIENT_CORE = String.raw ` var params = new URLSear
291
343
  localAuthEmail = data.email || localAuthEmail;
292
344
  renderLocalOtp("Code sent. Check your inbox.");
293
345
  } catch (error) {
294
- if (button) { button.disabled = false; button.innerHTML = '<span aria-hidden="true">→</span>'; }
346
+ if (button) { button.disabled = false; button.textContent = "Email me a code"; }
295
347
  setLocalAuthStatus(error && error.message ? error.message : "Could not send the code.", "error");
296
348
  }
297
349
  }
@@ -303,6 +355,7 @@ export const SETUP_PAGE_CLIENT_CORE = String.raw ` var params = new URLSear
303
355
  setHead("Check your email", "Local login");
304
356
  app.className = "localAuthStage localAuthWelcomeStage localAuthOtpStage";
305
357
  app.innerHTML =
358
+ setupJourney(3) +
306
359
  '<section class="localAuthWelcome localAuthOtpWelcome" aria-labelledby="localAuthOtpTitle">' +
307
360
  '<div class="localAuthOrbScene" aria-hidden="true"><span class="localAuthOrb"></span><span class="localAuthOrbShadow"></span></div>' +
308
361
  '<div class="localAuthWelcomeCopy">' +
@@ -398,17 +451,22 @@ export const SETUP_PAGE_CLIENT_CORE = String.raw ` var params = new URLSear
398
451
  setHead(setupMode ? "Create encrypted vault" : "Unlock encrypted vault", "Local passphrase");
399
452
  app.className = "localAuthStage localAuthWelcomeStage localAuthPassphraseStage";
400
453
  app.innerHTML =
454
+ setupJourney(3) +
401
455
  '<section class="localAuthWelcome localAuthPassphraseWelcome">' +
402
456
  '<div class="localAuthOrbScene" aria-hidden="true"><span class="localAuthOrb"></span><span class="localAuthOrbShadow"></span></div>' +
403
457
  '<div class="localAuthWelcomeCopy">' +
404
- '<h2>' + (setupMode ? 'Create your local vault passphrase.' : 'Enter your vault passphrase.') + '</h2>' +
405
- '<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>' +
458
+ '<h2>' + (setupMode ? 'Protect your memories with a passphrase.' : 'Enter your vault passphrase.') + '</h2>' +
459
+ '<p>' + (setupMode ? 'It encrypts memories before upload and unlocks them on your trusted devices. Echo can’t recover it, so save it in your password manager.' : 'Verified only on this device. It is never sent to EchoMem.') + '</p>' +
406
460
  '</div>' +
407
461
  '<form class="localAuthWelcomeForm localAuthPassphraseForm" id="localAuthPassphraseForm">' +
408
462
  '<label class="localAuthField"><span>Passphrase</span><input id="localAuthPassphrase" type="password" autocomplete="' + (setupMode ? "new-password" : "current-password") + '" placeholder="Encryption passphrase" /></label>' +
409
463
  (setupMode ? '<label class="localAuthField"><span>Confirm passphrase</span><input id="localAuthPassphraseConfirm" type="password" autocomplete="new-password" placeholder="Confirm passphrase" /></label>' : '') +
410
464
  '<button class="primary localAuthPassphraseSubmit" id="localAuthUnlock" type="submit">' + (setupMode ? "Create vault" : "Unlock") + '</button>' +
411
- '<button class="secondary localAuthBackWide" id="localAuthRestart" type="button">Use a different email</button>' +
465
+ (setupMode
466
+ ? '<p class="localAuthVaultDeferNote">You can turn on encryption later.</p>' +
467
+ '<button class="localAuthTextAction" id="localAuthSkipVault" type="button">Skip encryption for now</button>'
468
+ : '') +
469
+ '<button class="localAuthTextAction localAuthRestartAction" id="localAuthRestart" type="button">Use a different email</button>' +
412
470
  '<p class="localAuthStatus' + (message ? " is-error" : "") + '" id="localAuthStatus" role="status" aria-live="polite">' + esc(message || "") + '</p>' +
413
471
  '</form>' +
414
472
  '</section>';
@@ -416,9 +474,23 @@ export const SETUP_PAGE_CLIENT_CORE = String.raw ` var params = new URLSear
416
474
  if (form) form.onsubmit = function (event) { event.preventDefault(); void submitLocalPassphrase(setupMode); };
417
475
  var restart = document.getElementById("localAuthRestart");
418
476
  if (restart) restart.onclick = function () { renderLocalLogin(""); };
477
+ var skipVault = document.getElementById("localAuthSkipVault");
478
+ if (skipVault) skipVault.onclick = function () { void skipLocalVaultSetup(); };
419
479
  var pass = document.getElementById("localAuthPassphrase");
420
480
  if (pass) pass.focus();
421
481
  }
482
+ async function skipLocalVaultSetup() {
483
+ var button = document.getElementById("localAuthSkipVault");
484
+ if (button) { button.disabled = true; button.textContent = "Continuing…"; }
485
+ setLocalAuthStatus("Continuing without encryption…");
486
+ try {
487
+ await postJson("/local-auth/skip-encryption", {});
488
+ await finishLocalLogin();
489
+ } catch (error) {
490
+ if (button) { button.disabled = false; button.textContent = "Skip encryption for now"; }
491
+ setLocalAuthStatus(error && error.message ? error.message : "Could not continue without encryption.", "error");
492
+ }
493
+ }
422
494
  async function submitLocalPassphrase(setupMode) {
423
495
  var pass = document.getElementById("localAuthPassphrase");
424
496
  var confirm = document.getElementById("localAuthPassphraseConfirm");
@@ -441,7 +513,7 @@ export const SETUP_PAGE_CLIENT_CORE = String.raw ` var params = new URLSear
441
513
  function onConnectClick() {
442
514
  if (!localHistoryConsentGranted) {
443
515
  renderLocalScanConsent();
444
- setConsentStatus("Local history access is required before you can connect EchoMem or continue setup.", "required");
516
+ setConsentStatus("Allow the local scan to continue.", "required");
445
517
  return;
446
518
  }
447
519
  if (connected) { void waitForStats(); return; }