@echomem/mcp 1.4.42 → 1.4.44

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.
Files changed (48) hide show
  1. package/README.md +25 -28
  2. package/dist/config-files.js +63 -0
  3. package/dist/context-analysis/claude-native-canonical.js +2 -2
  4. package/dist/context-analysis/vendored-canonical.js +2 -2
  5. package/dist/context-analysis/workspace-report.js +3 -3
  6. package/dist/hud/hooks.js +43 -31
  7. package/dist/index.js +5 -8
  8. package/dist/local-jsonl.js +87 -0
  9. package/dist/migrate.js +6 -4
  10. package/dist/onboarding-stats.js +16 -0
  11. package/dist/save-checkpoint-hook.js +1 -1
  12. package/dist/setup-page/client-core.js +18 -372
  13. package/dist/setup-page/client-extraction.js +19 -134
  14. package/dist/setup-page/client-lifecycle.js +31 -101
  15. package/dist/setup-page/client.js +0 -2
  16. package/dist/setup-page/styles-extraction.js +1 -31
  17. package/dist/setup-page/styles-foundation.js +0 -89
  18. package/dist/setup-page/styles-mvp.js +10 -155
  19. package/dist/setup-page/styles-website-alignment.js +0 -204
  20. package/dist/setup-page/styles.js +0 -4
  21. package/dist/setup-page.js +4 -4
  22. package/dist/setup-preview.js +4 -212
  23. package/dist/setup.js +378 -668
  24. package/dist/v1-contract.js +0 -8
  25. package/package.json +9 -7
  26. package/dist/city/README.md +0 -9
  27. package/dist/city/echo-ai-city-only.html +0 -2232
  28. package/dist/city/echo-extraction-plate.html +0 -330
  29. package/dist/city/echo-face-cutout.png +0 -0
  30. package/dist/city/personality_stickers/bossy.png +0 -0
  31. package/dist/city/personality_stickers/ghosty.png +0 -0
  32. package/dist/city/personality_stickers/loopy.png +0 -0
  33. package/dist/city/personality_stickers/lusty.png +0 -0
  34. package/dist/city/personality_stickers/maxxy.png +0 -0
  35. package/dist/city/personality_stickers/tabby.png +0 -0
  36. package/dist/city/vendor/OrbitControls.js +0 -1417
  37. package/dist/city/vendor/RoundedBoxGeometry.js +0 -155
  38. package/dist/city/vendor/echo_general-file-21.riv +0 -0
  39. package/dist/city/vendor/rive.js +0 -8139
  40. package/dist/city/vendor/rive.wasm +0 -0
  41. package/dist/city/vendor/three.module.min.js +0 -6
  42. package/dist/forensics.js +0 -1531
  43. package/dist/report.js +0 -721
  44. package/dist/setup-page/client-report-audit.js +0 -819
  45. package/dist/setup-page/client-report-city.js +0 -356
  46. package/dist/setup-page/client-report.js +0 -6
  47. package/dist/setup-page/styles-city-report.js +0 -880
  48. package/dist/setup-page/styles-context-audit.js +0 -470
@@ -1,97 +1,7 @@
1
1
  // Generated by splitting the previous inline setup page. Keep this source readable: it is emitted into one browser IIFE.
2
2
  export const SETUP_PAGE_CLIENT_EXTRACTION = String.raw ` /* ---------- dashboard (post-auth extraction) ---------- */
3
- function acceptReportScanId(envelope) {
4
- var scanId = envelope && typeof envelope.scanId === "string" ? envelope.scanId : "";
5
- if (!scanId) throw new Error("Report response is missing its scan identity.");
6
- if (activeScanId && activeScanId !== scanId) throw new Error("The local scan identity changed before the report completed.");
7
- activeScanId = scanId;
8
- }
9
- function validateProductionReportEnvelope(envelope) {
10
- if (!envelope || typeof envelope !== "object" || envelope.schemaVersion !== 1 || envelope.mode !== "production") {
11
- throw new Error("Report response has an unsupported production schema.");
12
- }
13
- acceptReportScanId(envelope);
14
- if (["scanning", "ready", "empty", "failed"].indexOf(envelope.kind) === -1) {
15
- throw new Error("Report response has an unknown state.");
16
- }
17
- if (envelope.kind === "scanning") return envelope;
18
- if (envelope.kind === "failed") {
19
- if (!envelope.error || typeof envelope.error.code !== "string" || typeof envelope.error.message !== "string") {
20
- throw new Error("Report failure response is malformed.");
21
- }
22
- return envelope;
23
- }
24
- var r = envelope.report;
25
- if (!r || typeof r !== "object" || r.schemaVersion !== 1 || r.dataOrigin !== "local-workspace-scan") {
26
- throw new Error("Report did not prove that it came from this local workspace scan.");
27
- }
28
- var scale = r.scale || {};
29
- var sessions = Number(scale.sessionCount);
30
- var gs = gsClean(r.canonicalGoldenStandard);
31
- var summary = (gs && gs.summary) || {};
32
- var canonicalSessions = Number(summary.sessionsAnalyzed || 0);
33
- var canonicalInput = Number(summary.officialInputTokens || 0);
34
- var skipped = Number((gs && gs.diagnostics && gs.diagnostics.skippedSessions) || 0);
35
- if (envelope.kind === "empty") {
36
- if (sessions !== 0 || canonicalSessions !== 0 || canonicalInput !== 0 || Number(scale.totalInputTokens || 0) !== 0) {
37
- throw new Error("An empty report contained non-empty usage data.");
38
- }
39
- return envelope;
40
- }
41
- if (
42
- !gs ||
43
- !Number.isFinite(sessions) || sessions <= 0 ||
44
- !Number.isFinite(canonicalSessions) || canonicalSessions <= 0 || canonicalSessions > sessions ||
45
- !Number.isFinite(skipped) || skipped < 0 || canonicalSessions + skipped !== sessions ||
46
- !Number.isFinite(canonicalInput) || canonicalInput <= 0
47
- ) {
48
- throw new Error("Canonical analysis is incomplete or does not match the provider session cohort.");
49
- }
50
- if (!Array.isArray(r.repos) || r.repos.length === 0) {
51
- throw new Error("The local report has no workspace rows to render.");
52
- }
53
- gsBillingProjection(r, gs);
54
- return envelope;
55
- }
56
- function renderReportIssue(code, message) {
57
- void message;
58
- report = null;
59
- reportEnvelope = null;
60
- resetReportSurface();
61
- var canContinue = String(code || "").indexOf("REPORT_") === 0 || code === "RENDER_FAILED";
62
- setHead("We couldn't finish this scan", "Needs attention");
63
- app.className = "reportMessageStage";
64
- app.innerHTML =
65
- '<section class="reportMessage" data-report-state="failed">' +
66
- '<h2>We couldn’t finish this scan.</h2>' +
67
- '<p>Your coding history is unchanged.</p>' +
68
- (canContinue
69
- ? '<p>This optional report can be retried later.</p><div class="actions"><button type="button" class="primary" data-connect-echo>Continue without report</button></div>'
70
- : '<p>Close this tab and run <code>echomem-mcp init</code> again.</p>') +
71
- '<details class="reportTechnical"><summary>Technical details</summary><code>' + esc(code || "REPORT_FAILED") + '</code></details>' +
72
- '</section>';
73
- if (canContinue) bindConnect();
74
- }
75
- function renderEmptyReport() {
76
- report = null;
77
- reportEnvelope = null;
78
- resetReportSurface();
79
- setHead("No local coding history found", "Local scan complete");
80
- app.className = "reportMessageStage";
81
- app.innerHTML =
82
- '<section class="reportMessage" data-report-state="empty">' +
83
- '<h2>No coding history found.</h2>' +
84
- '<p>You can continue now and import local coding history later.</p>' +
85
- '<div class="actions"><button type="button" class="primary" data-connect-echo>Continue setup</button></div>' +
86
- '</section>';
87
- bindConnect();
88
- }
89
- function renderBridgeIssue() {
90
- renderReportIssue("BRIDGE_UNREACHABLE", "The local bridge stopped answering before your report was ready. Your terminal may have closed or the process may have stopped.");
91
- }
92
3
  function renderDiscoveryCheck(slow) {
93
4
  void slow;
94
- setCityMode(false);
95
5
  setExtractMode(false);
96
6
  setReadyMode(true);
97
7
  setHead("Loading", "Working");
@@ -99,7 +9,6 @@ export const SETUP_PAGE_CLIENT_EXTRACTION = String.raw ` /* ---------- dash
99
9
  app.innerHTML = '<div class="discoverySpinner" role="status" aria-label="Loading"><span aria-hidden="true"></span></div>';
100
10
  }
101
11
  function renderDiscoveryCheckIssue(error) {
102
- setCityMode(false);
103
12
  setExtractMode(false);
104
13
  setReadyMode(true);
105
14
  setHead("Session refresh paused", "Needs attention");
@@ -107,7 +16,7 @@ export const SETUP_PAGE_CLIENT_EXTRACTION = String.raw ` /* ---------- dash
107
16
  app.innerHTML =
108
17
  '<div style="padding:24px"><h2>Echo could not refresh session status</h2>' +
109
18
  '<p class="helper">Keep the terminal open and try the local check again.</p>' +
110
- (error ? '<details class="reportTechnical"><summary>Technical details</summary><code>' + esc(error) + '</code></details>' : '') +
19
+ (error ? '<details class="technicalDetails"><summary>Technical details</summary><code>' + esc(error) + '</code></details>' : '') +
111
20
  '<div class="actions"><button id="retryDiscoveryCheck" class="primary">Try again</button></div></div>';
112
21
  var retry = document.getElementById("retryDiscoveryCheck");
113
22
  if (retry) retry.onclick = function () {
@@ -116,13 +25,11 @@ export const SETUP_PAGE_CLIENT_EXTRACTION = String.raw ` /* ---------- dash
116
25
  };
117
26
  }
118
27
  function renderLogoutPending() {
119
- setCityMode(false);
120
28
  setHead("Signing out locally", "Working");
121
29
  app.className = "loading-panel";
122
30
  app.innerHTML = '<div style="padding:24px"><h2>Signing this device out</h2><p class="helper">If Echo is scanning local files, this can take a few seconds to answer.</p></div>';
123
31
  }
124
32
  function renderLogoutIssue(error) {
125
- setCityMode(false);
126
33
  setHead("Sign out did not finish", "Needs attention");
127
34
  app.className = "loading-panel";
128
35
  app.innerHTML =
@@ -136,7 +43,6 @@ export const SETUP_PAGE_CLIENT_EXTRACTION = String.raw ` /* ---------- dash
136
43
  if (statsPollStarted) return;
137
44
  var statsEpoch = accountStateEpoch;
138
45
  statsPollStarted = true;
139
- setCityMode(false);
140
46
  setExtractMode(false);
141
47
  setReadyMode(true);
142
48
  renderDiscoveryCheck(false);
@@ -169,23 +75,14 @@ export const SETUP_PAGE_CLIENT_EXTRACTION = String.raw ` /* ---------- dash
169
75
  if (res.status !== 202) throw new Error(await res.text() || ("HTTP " + res.status));
170
76
  } catch (e) {
171
77
  lastError = e && e.message ? e.message : String(e);
172
- if (!stats && Date.now() - started > 25000) renderBridgeIssue(lastError);
78
+ if (!stats && Date.now() - started > 25000) renderDiscoveryCheckIssue(lastError);
173
79
  if (Date.now() - started > 20000) { statsSlow = true; if (stats) renderDashboard(); }
174
80
  }
175
81
  await sleep(statsSlow ? 1500 : 500);
176
82
  }
177
83
  } finally { statsPollStarted = false; }
178
84
  }
179
- // The plate slot: ONLY the WebGL clay city (echo-extraction-plate.html). It self-fetches /report
180
- // (your repos) and polls /progress (extraction state), growing as memories are created. The old SVG
181
- // fallback board was removed — it was bleeding through behind the transparent canvas (the double board).
182
- function plateBlock() {
183
- return '<div class="exPlate"><div class="plateStack">' +
184
- '<iframe class="plateGl" title="Extraction plate" src="/city/echo-extraction-plate.html?nonce=' + encodeURIComponent(nonce) + '"></iframe>' +
185
- '</div></div>';
186
- }
187
85
  function renderPlanGate() {
188
- setCityMode(false);
189
86
  setExtractMode(false);
190
87
  setReadyMode(true);
191
88
  setHead("Choose your Echo", "Plan");
@@ -198,7 +95,7 @@ export const SETUP_PAGE_CLIENT_EXTRACTION = String.raw ` /* ---------- dash
198
95
  readyStage = "plan";
199
96
  app.className = "planGateStage";
200
97
  app.innerHTML =
201
- setupJourney(3) +
98
+ setupJourney(2) +
202
99
  '<section class="planGateDecision">' +
203
100
  '<div class="setupPlanSlot" id="setupPlan"></div>' +
204
101
  '<aside class="planGateAccount" aria-label="Connected account"><div class="setupAccountSlot" id="setupAccount"></div></aside>' +
@@ -207,7 +104,6 @@ export const SETUP_PAGE_CLIENT_EXTRACTION = String.raw ` /* ---------- dash
207
104
  renderAccountChoice();
208
105
  }
209
106
  function renderDashboard(error) {
210
- setCityMode(false);
211
107
  setExtractMode(false);
212
108
  setReadyMode(true);
213
109
  var migratable = stats && stats.migratable ? stats.migratable : {};
@@ -253,19 +149,17 @@ export const SETUP_PAGE_CLIENT_EXTRACTION = String.raw ` /* ---------- dash
253
149
  ? "<strong>" + esc(number(pendN)) + " sessions</strong> found."
254
150
  : "<strong>" + esc(number(pendN)) + " sessions</strong> are ready to review."));
255
151
  var sub = degradedWithoutCounts
256
- ? "You can finish setup now. Your conversations stay on this Mac, and you can retry the history import later."
152
+ ? "You can finish setup now. Your conversations stay on this computer, and you can retry the history import later."
257
153
  : !pendingTrusted
258
154
  ? "Echo is matching your local history against what is already in memory."
259
155
  : (knownDone
260
156
  ? (skippedActive ? number(skippedActive) + " active conversation" + (skippedActive === 1 ? " is" : "s are") + " still changing, so Echo will pick them up later." : "Your history is already in EchoMem. Nothing new to extract.")
261
157
  : (planLimited
262
158
  ? (importableNow > 0
263
- ? currentPlanLabel + " can import " + number(importableNow) + " of them. Choose which ones to bring into memory; the other " + number(deferredByPlan) + " stay on this Mac."
264
- : currentPlanLabel + " has no coding-session imports remaining. These sessions stay on this Mac.")
159
+ ? currentPlanLabel + " can import " + number(importableNow) + " of them. Choose which ones to bring into memory; the other " + number(deferredByPlan) + " stay on this computer."
160
+ : currentPlanLabel + " has no coding-session imports remaining. These sessions stay on this computer.")
265
161
  : ""));
266
- // Mount the dashboard shell ONCE so the WebGL plate iframe is never re-created across /stats polls.
267
- // (Rebuilding app.innerHTML on every partial-scan poll reloaded the iframe → the city flickered.)
268
- // Later polls only patch the text/counts in place; the iframe stays mounted.
162
+ // Mount the dashboard shell once; later polls only patch its text and counts.
269
163
  var live = dashMounted && document.getElementById("exHeadline");
270
164
  if (!live) {
271
165
  extractMounted = false;
@@ -277,7 +171,7 @@ export const SETUP_PAGE_CLIENT_EXTRACTION = String.raw ` /* ---------- dash
277
171
  // one-line strip, not stat cards; the ETA is a commitment-moment note under the button,
278
172
  // not a display-size headline.
279
173
  app.innerHTML =
280
- setupJourney(4) +
174
+ setupJourney(3) +
281
175
  '<aside class="readyUtilityNav" aria-label="Account and plan settings">' +
282
176
  '<div class="planGateAccount readyAccountNav" aria-label="Connected account"><div class="setupAccountSlot" id="setupAccount"></div></div>' +
283
177
  '<div class="readyPlanNav" aria-label="Current plan"><span class="readyPlanNavLabel">Plan</span><div class="setupPlanSlot" id="setupPlan"></div></div>' +
@@ -325,7 +219,7 @@ export const SETUP_PAGE_CLIENT_EXTRACTION = String.raw ` /* ---------- dash
325
219
  ? '<span class="pf">' + srcIcon("codex", "CX") + '<strong>' + esc(number(codexN)) + '</strong> Codex</span>' +
326
220
  '<span class="pfDot">&middot;</span>' +
327
221
  '<span class="pf">' + srcIcon("claude-desktop", "CL") + '<strong>' + esc(number(claudeN)) + '</strong> Claude Code</span>' +
328
- '<span class="pfNote">found on this Mac</span>'
222
+ '<span class="pfNote">found on this computer</span>'
329
223
  : "");
330
224
  // Reassurances live at the moment of commitment — right under the button.
331
225
  document.getElementById("exEta").innerHTML = degradedWithoutCounts
@@ -392,14 +286,12 @@ export const SETUP_PAGE_CLIENT_EXTRACTION = String.raw ` /* ---------- dash
392
286
  stopBillingPoll();
393
287
  decisionMade = true;
394
288
  extractionEnded = false;
395
- setCityMode(false);
396
- setScanMode(false);
397
289
  setReadyMode(false);
398
290
  setExtractMode(true);
399
291
  setHead("Extracting", "Starting");
400
292
  app.className = "extractionStartStage";
401
293
  app.innerHTML =
402
- setupJourney(4) +
294
+ setupJourney(3) +
403
295
  '<div class="discoverySpinner" role="status" aria-label="Starting import"><span aria-hidden="true"></span></div>';
404
296
  var started;
405
297
  try {
@@ -491,7 +383,6 @@ export const SETUP_PAGE_CLIENT_EXTRACTION = String.raw ` /* ---------- dash
491
383
  function renderProgress(progress) {
492
384
  if (extractionEnded) return;
493
385
  lastExtractionProgress = progress;
494
- setCityMode(false);
495
386
  setExtractMode(true);
496
387
  var status = progress.status;
497
388
  var done = status === "completed";
@@ -503,18 +394,15 @@ export const SETUP_PAGE_CLIENT_EXTRACTION = String.raw ` /* ---------- dash
503
394
  var pct = total > 0 ? Math.round((completed / total) * 100) : (done ? 100 : 0);
504
395
  var preparing = (status === "starting" || status === "preparing");
505
396
  app.className = "";
506
- // Mount the shell ONCE so the WebGL plate iframe survives every poll (it self-polls /progress);
507
- // each poll only rebuilds the text column.
397
+ // Mount the shell once; each poll only rebuilds the text column.
508
398
  if (!extractMounted) {
509
399
  app.innerHTML =
510
- '<div id="setupJourneySlot">' + setupJourney(4) + '</div>' +
511
- '<section class="exHero"><div class="exCopy" id="exText"></div>' +
512
- plateBlock() +
513
- '</section>';
400
+ '<div id="setupJourneySlot">' + setupJourney(3) + '</div>' +
401
+ '<section class="exHero"><div class="exCopy" id="exText"></div></section>';
514
402
  extractMounted = true;
515
403
  }
516
404
  var journeySlot = document.getElementById("setupJourneySlot");
517
- if (journeySlot) journeySlot.innerHTML = setupJourney(done ? 5 : 4);
405
+ if (journeySlot) journeySlot.innerHTML = setupJourney(done ? 4 : 3);
518
406
  var shell = app.querySelector(".exHero");
519
407
  if (shell) {
520
408
  shell.classList.toggle("exHeroResult", done || stopped);
@@ -1035,13 +923,12 @@ export const SETUP_PAGE_CLIENT_EXTRACTION = String.raw ` /* ---------- dash
1035
923
  ? account.email
1036
924
  : "the connected account";
1037
925
  accountSwitchPending = true;
1038
- setCityMode(false);
1039
926
  setExtractMode(false);
1040
927
  setReadyMode(true);
1041
928
  setHead("Switch EchoMem account", "Confirm");
1042
929
  app.className = "accountSwitchStage";
1043
930
  app.innerHTML =
1044
- setupJourney(4) +
931
+ setupJourney(3) +
1045
932
  '<section class="accountSwitchConfirm" aria-labelledby="accountSwitchTitle">' +
1046
933
  '<button type="button" class="flowBack" id="cancelAccountSwitch">← Back</button>' +
1047
934
  '<h2 id="accountSwitchTitle">Switch account?</h2>' +
@@ -1536,7 +1423,7 @@ export const SETUP_PAGE_CLIENT_EXTRACTION = String.raw ` /* ---------- dash
1536
1423
  '<p class="recallPromptText">' + esc(primary) + '</p>' +
1537
1424
  '<div class="recallPromptActions" aria-label="Try your first memory recall">' +
1538
1425
  '<button type="button" class="recallAgent recallAgentPrimary" data-recall-codex><span class="recallAgentIcon"><img src="/hud-assets/codex.svg" alt="" /></span><span>Ask in Codex</span></button>' +
1539
- '<button type="button" class="recallAgent is-claude" data-recall-claude><span class="recallAgentIcon"><img src="/hud-assets/claude.svg" alt="" /></span><span>Use Claude instead</span></button>' +
1426
+ (canOpenClaudeDesktop ? '<button type="button" class="recallAgent is-claude" data-recall-claude><span class="recallAgentIcon"><img src="/hud-assets/claude.svg" alt="" /></span><span>Use Claude instead</span></button>' : '') +
1540
1427
  '</div>' +
1541
1428
  '</article>' +
1542
1429
  '<p class="launchStatus" id="launchStatus"></p>' +
@@ -1554,7 +1441,6 @@ export const SETUP_PAGE_CLIENT_EXTRACTION = String.raw ` /* ---------- dash
1554
1441
  });
1555
1442
  }
1556
1443
  function renderEnding() {
1557
- setCityMode(false);
1558
1444
  setExtractMode(true);
1559
1445
  // This view replaces the live extraction shell. Mark it unmounted so a failed
1560
1446
  // pause can rebuild the progress page instead of trying to patch missing DOM.
@@ -1562,7 +1448,7 @@ export const SETUP_PAGE_CLIENT_EXTRACTION = String.raw ` /* ---------- dash
1562
1448
  setHead("Ending extraction", "Ending");
1563
1449
  app.className = "";
1564
1450
  app.innerHTML =
1565
- setupJourney(4) +
1451
+ setupJourney(3) +
1566
1452
  '<section class="lifecycleState lifecyclePending">' +
1567
1453
  '<h2>Stopping the remaining queued work…</h2>' +
1568
1454
  '<p>Saved memories stay intact.</p>' +
@@ -1570,13 +1456,12 @@ export const SETUP_PAGE_CLIENT_EXTRACTION = String.raw ` /* ---------- dash
1570
1456
  '</section>';
1571
1457
  }
1572
1458
  function renderSkipped() {
1573
- setCityMode(false);
1574
1459
  setExtractMode(true);
1575
1460
  setHead("Extraction ended for now", "Ended");
1576
1461
  app.className = "";
1577
1462
  var savedCount = Math.max(0, Number(lastExtractionProgress && lastExtractionProgress.extracted || 0));
1578
1463
  app.innerHTML =
1579
- setupJourney(savedCount > 0 ? 5 : 4) +
1464
+ setupJourney(savedCount > 0 ? 4 : 3) +
1580
1465
  '<section class="lifecycleState lifecyclePaused">' +
1581
1466
  '<header class="lifecycleIntro">' +
1582
1467
  '<h2>' + (savedCount > 0 ? esc(number(savedCount)) + ' memories are ready to use.' : 'Import ended safely.') + '</h2>' +
@@ -1640,7 +1525,7 @@ export const SETUP_PAGE_CLIENT_EXTRACTION = String.raw ` /* ---------- dash
1640
1525
  statsSlow = false;
1641
1526
  connected = false;
1642
1527
  decisionMade = false;
1643
- renderForensicReport("Signed out locally. Connect EchoMem again to extract.");
1528
+ renderLocalLogin("Signed out locally. Connect EchoMem again to extract.");
1644
1529
  } catch (e) {
1645
1530
  decisionMade = false;
1646
1531
  renderLogoutIssue(e && e.message ? e.message : String(e));
@@ -1,8 +1,6 @@
1
1
  // Generated by splitting the previous inline setup page. Keep this source readable: it is emitted into one browser IIFE.
2
- export const SETUP_PAGE_CLIENT_LIFECYCLE = String.raw ` /* ---------- scan-first poll for the forensic report ---------- */
3
- function renderLocalScanConsent() {
4
- setScanMode(false);
5
- setCityMode(false);
2
+ export const SETUP_PAGE_CLIENT_LIFECYCLE = String.raw ` /* ---------- local-history permission and setup routing ---------- */
3
+ function renderLocalHistoryConsent() {
6
4
  setExtractMode(false);
7
5
  setReadyMode(true);
8
6
  setHead("Local file access", "Consent");
@@ -14,15 +12,15 @@ export const SETUP_PAGE_CLIENT_LIFECYCLE = String.raw ` /* ---------- scan-
14
12
  '<img class="mvpConsentEcho" src="/hud-assets/echo-face-cutout.png" alt="" />' +
15
13
  '<div class="mvpConsentPitch">' +
16
14
  '<h2 id="mvpConsentTitle">Start with the context you’ve already built.</h2>' +
17
- '<p class="mvpConsentIntro">See what you keep re-explaining—and what’s worth carrying into future sessions.</p>' +
15
+ '<p class="mvpConsentIntro">Choose which local coding conversations to bring into EchoMem.</p>' +
18
16
  '</div>' +
19
17
  '</div>' +
20
- '<p class="mvpConsentTrustLine">Your history stays on your Mac until you choose what to import. ' + dataHandlingLink("Data handling") + '</p>' +
21
- '<button type="button" class="primary mvpPrimary" id="allowLocalScan">Build my private report <span aria-hidden="true">→</span></button>' +
18
+ '<p class="mvpConsentTrustLine">Your history stays on this computer until you choose what to import. ' + dataHandlingLink("Data handling") + '</p>' +
19
+ '<button type="button" class="primary mvpPrimary" id="allowLocalHistory">Continue <span aria-hidden="true">→</span></button>' +
22
20
  '<p class="consentFine" id="consentStatus" role="status" aria-live="polite"></p>' +
23
21
  '</section>';
24
- var allow = document.getElementById("allowLocalScan");
25
- if (allow) allow.onclick = allowLocalScan;
22
+ var allow = document.getElementById("allowLocalHistory");
23
+ if (allow) allow.onclick = allowLocalHistory;
26
24
  }
27
25
  function setConsentStatus(message, tone) {
28
26
  var node = document.getElementById("consentStatus");
@@ -31,18 +29,26 @@ export const SETUP_PAGE_CLIENT_LIFECYCLE = String.raw ` /* ---------- scan-
31
29
  node.classList.toggle("is-required", tone === "required");
32
30
  }
33
31
  }
34
- async function allowLocalScan() {
35
- var allow = document.getElementById("allowLocalScan");
32
+ function continueAfterLocalHistoryConsent() {
33
+ if (connected) {
34
+ try { void refreshBillingStatus(); } catch (_) {}
35
+ void waitForStats();
36
+ return;
37
+ }
38
+ rememberSetupPlanChoice("");
39
+ renderLocalLogin("");
40
+ }
41
+ async function allowLocalHistory() {
42
+ var allow = document.getElementById("allowLocalHistory");
36
43
  if (allow) allow.disabled = true;
37
- setConsentStatus("Starting local scan...");
44
+ setConsentStatus("Saving your choice...");
38
45
  try {
39
- await postJson("/report-consent", { allowed: true }, 5000);
46
+ await postJson("/local-history-consent", { allowed: true }, 5000);
40
47
  localHistoryConsentGranted = true;
41
- await showReport();
48
+ continueAfterLocalHistoryConsent();
42
49
  } catch (error) {
43
50
  // The localhost bridge may have accepted consent even if the browser loses the POST
44
- // response while it swaps connections. Recover from the server's authoritative state
45
- // instead of leaving the user forever on "Starting local scan...".
51
+ // response while it swaps connections. Recover from the server's authoritative state.
46
52
  try {
47
53
  var configResponse = await fetchWithTimeout(
48
54
  "/config?nonce=" + encodeURIComponent(nonce),
@@ -52,80 +58,14 @@ export const SETUP_PAGE_CLIENT_LIFECYCLE = String.raw ` /* ---------- scan-
52
58
  var config = configResponse.ok ? await configResponse.json() : null;
53
59
  if (config && config.consentGranted === true) {
54
60
  localHistoryConsentGranted = true;
55
- await showReport();
61
+ continueAfterLocalHistoryConsent();
56
62
  return;
57
63
  }
58
64
  } catch (_) {}
59
- renderReportIssue("CONSENT_SAVE_FAILED", error && error.message ? error.message : "Could not start the local scan.");
65
+ if (allow) allow.disabled = false;
66
+ setConsentStatus(error && error.message ? error.message : "Could not save local-history access.", "required");
60
67
  }
61
68
  }
62
- async function showReport() {
63
- if (reportPollStarted) return;
64
- reportPollStarted = true;
65
- if (!report) renderLoading();
66
- try {
67
- var fails = 0;
68
- for (;;) {
69
- if (decisionMade) return;
70
- var res;
71
- try {
72
- res = await fetchWithTimeout("/report?nonce=" + encodeURIComponent(nonce), { credentials: "omit", cache: "no-store" }, 12000);
73
- } catch (_) {
74
- // Only a RUN of consecutive failures is a real bridge problem. A long scan that keeps
75
- // answering valid 202 envelopes must never trip this.
76
- fails++;
77
- if (fails >= 8) { renderBridgeIssue(); return; }
78
- await sleep(700);
79
- continue;
80
- }
81
- if (res.status === 403) {
82
- renderReportIssue("SETUP_SESSION_INVALID", "This setup link is missing or has an invalid local session nonce.");
83
- return;
84
- }
85
- var envelope;
86
- try {
87
- envelope = await res.json();
88
- } catch (_) {
89
- renderReportIssue("REPORT_RESPONSE_INVALID", "The local bridge returned a report response that could not be decoded.");
90
- return;
91
- }
92
- try {
93
- validateProductionReportEnvelope(envelope);
94
- } catch (_) {
95
- renderReportIssue("REPORT_SCHEMA_INVALID", "The local report failed its provenance or ledger consistency checks.");
96
- return;
97
- }
98
- if (res.status === 202 && envelope.kind === "scanning") {
99
- fails = 0;
100
- reportEnvelope = envelope;
101
- scheduleReportToCity();
102
- renderLoading();
103
- await sleep(700);
104
- continue;
105
- }
106
- if (res.status === 200 && envelope.kind === "empty") {
107
- renderEmptyReport();
108
- return;
109
- }
110
- if (res.status === 200 && envelope.kind === "ready") {
111
- reportEnvelope = envelope;
112
- report = envelope.report;
113
- try {
114
- renderForensicReport();
115
- } catch (_) {
116
- renderReportIssue("RENDER_FAILED", "Your validated local report could not be rendered safely.");
117
- }
118
- return;
119
- }
120
- if (envelope.kind === "failed") {
121
- renderReportIssue(envelope.error.code, envelope.error.message);
122
- return;
123
- }
124
- renderReportIssue("REPORT_STATE_MISMATCH", "The local bridge returned a report state that did not match its HTTP status.");
125
- return;
126
- }
127
- } finally { reportPollStarted = false; }
128
- }
129
69
 
130
70
  window.addEventListener("message", function (event) {
131
71
  if (event.origin !== location.origin) return;
@@ -140,27 +80,22 @@ export const SETUP_PAGE_CLIENT_LIFECYCLE = String.raw ` /* ---------- scan-
140
80
  rememberSetupPlanChoice("");
141
81
  try { void refreshBillingStatus(); } catch (_) {}
142
82
  void waitForStats();
143
- } else if (data.type === "echomem:city-ready" && data.nonce === nonce) {
144
- scheduleReportToCity();
145
- } else if (data.type === "echomem:connect-click") {
146
- onConnectClick();
147
83
  }
148
84
  });
149
85
  async function init() {
150
86
  if (!nonce) throw new Error("Missing setup nonce. Re-run echomem-mcp init.");
151
- // Older builds cached a report under the nonce. It is never an authority now; the current
152
- // server envelope and scan identity are the only inputs allowed to unlock personalized UI.
153
- try { sessionStorage.removeItem("echomem:setup-report:" + nonce); } catch (_) {}
154
87
  var configLoaded = false;
155
88
  var consentRequired = true;
156
89
  var consentGranted = false;
157
- // Learn the connection state and server-side consent gate before routing.
158
90
  try {
159
91
  var cfg = await getJson("/config");
160
92
  configLoaded = true;
161
93
  connected = !!cfg.connected;
162
94
  setupFlow = cfg.flow === "login" ? "login" : "onboarding";
163
95
  workspacePath = cfg.workspacePath || workspacePath;
96
+ setupPlatform = cfg.platform || setupPlatform;
97
+ canOpenClaudeDesktop = !!(cfg.capabilities && cfg.capabilities.openClaudeDesktop);
98
+ canOpenAgentSessions = !!(cfg.capabilities && cfg.capabilities.openAgentSessions);
164
99
  consentRequired = cfg.consentRequired !== false;
165
100
  consentGranted = cfg.consentGranted === true;
166
101
  localHistoryConsentGranted = consentGranted;
@@ -173,32 +108,27 @@ export const SETUP_PAGE_CLIENT_LIFECYCLE = String.raw ` /* ---------- scan-
173
108
  if (!connected) rememberSetupPlanChoice("");
174
109
  if (params.get("connected") === "1") {
175
110
  if (!configLoaded || (consentRequired && !consentGranted)) {
176
- renderLocalScanConsent();
111
+ renderLocalHistoryConsent();
177
112
  setConsentStatus("Local history access is required before you can connect EchoMem or continue setup.", "required");
178
113
  return;
179
114
  }
180
115
  connected = true;
181
116
  if (notifyOpenerConnected()) {
182
- // Popup flow: hand the main tab back control, then this popup closes.
183
117
  setHead("EchoMem connected", "Done");
184
118
  app.className = "notice";
185
119
  app.innerHTML = '<h2 style="color:#203820">This device is connected</h2><p style="margin-top:10px">Returning to the local setup page...</p>';
186
- // Fallback: if window.close() was blocked (auth opened as a tab, or the browser refused),
187
- // this window would otherwise hang on "Returning..." forever. After a beat, take over here
188
- // so the user always lands on the dashboard instead of a dead page.
189
120
  setTimeout(function () { if (!window.closed) void waitForStats(); }, 1500);
190
121
  } else {
191
- // Same-tab fallback (popup was blocked): go straight to the extraction dashboard.
192
122
  try { void refreshBillingStatus(); } catch (_) {}
193
123
  void waitForStats();
194
124
  }
195
125
  return;
196
126
  }
197
127
  if (consentGranted) {
198
- await showReport();
128
+ continueAfterLocalHistoryConsent();
199
129
  return;
200
130
  }
201
- renderLocalScanConsent();
131
+ renderLocalHistoryConsent();
202
132
  }
203
133
  __ECHO_SETUP_STARTUP__;
204
134
  `;
@@ -1,11 +1,9 @@
1
1
  import { SETUP_PAGE_CLIENT_CORE } from "./client-core.js";
2
- import { SETUP_PAGE_CLIENT_REPORT } from "./client-report.js";
3
2
  import { SETUP_PAGE_CLIENT_EXTRACTION } from "./client-extraction.js";
4
3
  import { SETUP_PAGE_CLIENT_LIFECYCLE } from "./client-lifecycle.js";
5
4
  export function renderSetupPageClient(previewState, startupScript) {
6
5
  const body = [
7
6
  SETUP_PAGE_CLIENT_CORE,
8
- SETUP_PAGE_CLIENT_REPORT,
9
7
  SETUP_PAGE_CLIENT_EXTRACTION,
10
8
  SETUP_PAGE_CLIENT_LIFECYCLE,
11
9
  ].join("\n")
@@ -598,13 +598,8 @@ export const SETUP_PAGE_STYLES_EXTRACTION = String.raw ` /* ---- dashboard (p
598
598
  .consentActions .primary { width: 100%; }
599
599
  .localAuthActions .primary,
600
600
  .localAuthForm > .primary { width: 100%; }
601
- .cityHero { min-height: auto; }
602
- .cityShell {
603
- width: 100vw;
604
- height: 100vh;
605
- }
606
601
  }
607
- /* extraction dashboard — plate layout */
602
+ /* extraction dashboard */
608
603
  .exHero { display: grid; grid-template-columns: minmax(0, 1fr) minmax(300px, 0.95fr); gap: clamp(20px, 3vw, 40px); align-items: center; margin-top: 8px; }
609
604
  /* Centered composition: mascot on top, then everything stacked and centered on one axis. */
610
605
  .readyDecision {
@@ -674,7 +669,6 @@ export const SETUP_PAGE_STYLES_EXTRACTION = String.raw ` /* ---- dashboard (p
674
669
  justify-items: stretch;
675
670
  margin: 0 auto;
676
671
  }
677
- .exHeroResult .exPlate { display: none; }
678
672
  .exHeroResult .exCopy { width: 100%; margin: 0 auto; }
679
673
  .exCopy h2 { max-width: 620px; margin: 8px 0 0; font-size: clamp(34px, 4.8vw, 58px); line-height: 1.01; }
680
674
  .exCopy .exSub { margin: 14px 0 0; max-width: 590px; font-size: clamp(15px, 1.3vw, 17px); line-height: 1.6; color: var(--echo-ink-mute); }
@@ -896,19 +890,6 @@ export const SETUP_PAGE_STYLES_EXTRACTION = String.raw ` /* ---- dashboard (p
896
890
  font-size: 12px;
897
891
  line-height: 1.35;
898
892
  }
899
- .exPlate { width: 100%; }
900
- /* WebGL clay city (echo-extraction-plate.html) — the only plate; no SVG layer */
901
- .plateStack { position: relative; width: 100%; aspect-ratio: 640 / 470; }
902
- .plateStack .plateGl { position: absolute; inset: 0; width: 100%; height: 100%; border: 0; background: transparent; }
903
- .exHeroActive {
904
- grid-template-columns: minmax(0, 640px);
905
- justify-content: center;
906
- justify-items: stretch;
907
- width: 100%;
908
- max-width: min(920px, calc(100vw - clamp(36px, 8vw, 120px)));
909
- margin: 0 auto;
910
- }
911
- .exHeroActive .exPlate { display: none; }
912
893
  .exHeroActive .exCopy { width: 100%; }
913
894
  .workbench { display: grid; gap: 14px; max-width: 640px; }
914
895
  .workbench h2 {
@@ -2538,15 +2519,4 @@ export const SETUP_PAGE_STYLES_EXTRACTION = String.raw ` /* ---- dashboard (p
2538
2519
  .sessionPickerFoot button { width: 100%; }
2539
2520
  }
2540
2521
 
2541
- /* ===== workspace readout dashboard (Part 2 — replaces explainPage) ===== */
2542
- @keyframes rdoRise { from { opacity: 0; translate: 0 14px; } to { opacity: 1; translate: 0 0; } }
2543
- @keyframes rdoGrow { from { transform: scaleY(0); } to { transform: scaleY(1); } }
2544
- @keyframes rdoDraw { from { transform: scaleX(0); } to { transform: scaleX(1); } }
2545
- @keyframes rdoWipe { from { clip-path: inset(0 100% 0 0); } to { clip-path: inset(0 0 0 0); } }
2546
- /* Pull the report up toward the city figure — the canvas bottom fades out now,
2547
- so the document can tuck into that empty band without touching the board. */
2548
- body.cityMode .detailsWrap { background: var(--echo-paper-canvas); margin-top: clamp(-120px, -7vw, -56px); }
2549
-
2550
- /* Context audit — a printed report sheet on the desk: serif document type, tight rhythm. */
2551
- .context-audit {
2552
2522
  `;