@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.
- package/dist/codex-session-files.js +20 -0
- package/dist/context-analysis/vendored-canonical.js +54 -11
- package/dist/forensics.js +24 -4
- package/dist/hud/cli.js +18 -1
- package/dist/hud/hooks.js +59 -0
- package/dist/index.js +111 -16
- package/dist/migrate.js +24 -11
- package/dist/package-metadata.js +5 -3
- package/dist/save-checkpoint-hook.js +111 -0
- package/dist/setup-page/client-core.js +87 -15
- package/dist/setup-page/client-extraction.js +508 -207
- package/dist/setup-page/client-lifecycle.js +10 -11
- package/dist/setup-page/client-report-audit.js +3 -2
- package/dist/setup-page/client-report-city.js +168 -38
- package/dist/setup-page/styles-extraction.js +37 -0
- package/dist/setup-page/styles-foundation.js +3 -1
- package/dist/setup-page/styles-mvp.js +751 -151
- package/dist/setup-page/styles-website-alignment.js +8 -3
- package/dist/setup-preview.js +46 -6
- package/dist/setup.js +402 -44
- package/dist/v1-contract.js +39 -6
- package/package.json +3 -2
- package/templates/codex-skills/echomem-save/SKILL.md +10 -0
- package/templates/echomem-recall.md +10 -2
package/dist/package-metadata.js
CHANGED
|
@@ -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
|
|
34
|
-
"Each
|
|
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
|
|
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
|
|
46
|
-
var
|
|
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 { "&": "&", "<": "<", ">": ">", '"': """, "'": "'" }[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
|
|
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
|
-
|
|
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"
|
|
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>
|
|
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.
|
|
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 ? '
|
|
405
|
-
'<p>' + (setupMode ? '
|
|
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
|
-
|
|
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("
|
|
516
|
+
setConsentStatus("Allow the local scan to continue.", "required");
|
|
445
517
|
return;
|
|
446
518
|
}
|
|
447
519
|
if (connected) { void waitForStats(); return; }
|