@echomem/mcp 1.4.44 → 1.4.46

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 (50) hide show
  1. package/README.md +28 -25
  2. package/dist/city/README.md +9 -0
  3. package/dist/city/echo-ai-city-only.html +2232 -0
  4. package/dist/city/echo-extraction-plate.html +330 -0
  5. package/dist/city/echo-face-cutout.png +0 -0
  6. package/dist/city/personality_stickers/bossy.png +0 -0
  7. package/dist/city/personality_stickers/ghosty.png +0 -0
  8. package/dist/city/personality_stickers/loopy.png +0 -0
  9. package/dist/city/personality_stickers/lusty.png +0 -0
  10. package/dist/city/personality_stickers/maxxy.png +0 -0
  11. package/dist/city/personality_stickers/tabby.png +0 -0
  12. package/dist/city/vendor/OrbitControls.js +1417 -0
  13. package/dist/city/vendor/RoundedBoxGeometry.js +155 -0
  14. package/dist/city/vendor/echo_general-file-21.riv +0 -0
  15. package/dist/city/vendor/rive.js +8139 -0
  16. package/dist/city/vendor/rive.wasm +0 -0
  17. package/dist/city/vendor/three.module.min.js +6 -0
  18. package/dist/context-analysis/claude-native-canonical.js +2 -2
  19. package/dist/context-analysis/vendored-canonical.js +2 -2
  20. package/dist/context-analysis/workspace-report.js +3 -3
  21. package/dist/forensics.js +1531 -0
  22. package/dist/hud/hooks.js +31 -43
  23. package/dist/index.js +79 -15
  24. package/dist/local-data-paths.js +38 -0
  25. package/dist/migrate.js +140 -70
  26. package/dist/report.js +721 -0
  27. package/dist/save-checkpoint-hook.js +1 -1
  28. package/dist/setup-page/client-core.js +372 -18
  29. package/dist/setup-page/client-extraction.js +204 -37
  30. package/dist/setup-page/client-lifecycle.js +101 -31
  31. package/dist/setup-page/client-report-audit.js +819 -0
  32. package/dist/setup-page/client-report-city.js +356 -0
  33. package/dist/setup-page/client-report.js +6 -0
  34. package/dist/setup-page/client.js +2 -0
  35. package/dist/setup-page/styles-city-report.js +880 -0
  36. package/dist/setup-page/styles-context-audit.js +470 -0
  37. package/dist/setup-page/styles-extraction.js +31 -1
  38. package/dist/setup-page/styles-foundation.js +89 -0
  39. package/dist/setup-page/styles-mvp.js +155 -10
  40. package/dist/setup-page/styles-website-alignment.js +204 -0
  41. package/dist/setup-page/styles.js +4 -0
  42. package/dist/setup-page.js +4 -4
  43. package/dist/setup-preview.js +212 -4
  44. package/dist/setup.js +793 -300
  45. package/dist/source-session.js +3 -9
  46. package/dist/v1-contract.js +8 -0
  47. package/package.json +7 -9
  48. package/dist/config-files.js +0 -63
  49. package/dist/local-jsonl.js +0 -87
  50. package/dist/onboarding-stats.js +0 -16
@@ -1,7 +1,97 @@
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
+ }
3
92
  function renderDiscoveryCheck(slow) {
4
93
  void slow;
94
+ setCityMode(false);
5
95
  setExtractMode(false);
6
96
  setReadyMode(true);
7
97
  setHead("Loading", "Working");
@@ -9,6 +99,7 @@ export const SETUP_PAGE_CLIENT_EXTRACTION = String.raw ` /* ---------- dash
9
99
  app.innerHTML = '<div class="discoverySpinner" role="status" aria-label="Loading"><span aria-hidden="true"></span></div>';
10
100
  }
11
101
  function renderDiscoveryCheckIssue(error) {
102
+ setCityMode(false);
12
103
  setExtractMode(false);
13
104
  setReadyMode(true);
14
105
  setHead("Session refresh paused", "Needs attention");
@@ -16,7 +107,7 @@ export const SETUP_PAGE_CLIENT_EXTRACTION = String.raw ` /* ---------- dash
16
107
  app.innerHTML =
17
108
  '<div style="padding:24px"><h2>Echo could not refresh session status</h2>' +
18
109
  '<p class="helper">Keep the terminal open and try the local check again.</p>' +
19
- (error ? '<details class="technicalDetails"><summary>Technical details</summary><code>' + esc(error) + '</code></details>' : '') +
110
+ (error ? '<details class="reportTechnical"><summary>Technical details</summary><code>' + esc(error) + '</code></details>' : '') +
20
111
  '<div class="actions"><button id="retryDiscoveryCheck" class="primary">Try again</button></div></div>';
21
112
  var retry = document.getElementById("retryDiscoveryCheck");
22
113
  if (retry) retry.onclick = function () {
@@ -25,11 +116,13 @@ export const SETUP_PAGE_CLIENT_EXTRACTION = String.raw ` /* ---------- dash
25
116
  };
26
117
  }
27
118
  function renderLogoutPending() {
119
+ setCityMode(false);
28
120
  setHead("Signing out locally", "Working");
29
121
  app.className = "loading-panel";
30
122
  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>';
31
123
  }
32
124
  function renderLogoutIssue(error) {
125
+ setCityMode(false);
33
126
  setHead("Sign out did not finish", "Needs attention");
34
127
  app.className = "loading-panel";
35
128
  app.innerHTML =
@@ -43,6 +136,7 @@ export const SETUP_PAGE_CLIENT_EXTRACTION = String.raw ` /* ---------- dash
43
136
  if (statsPollStarted) return;
44
137
  var statsEpoch = accountStateEpoch;
45
138
  statsPollStarted = true;
139
+ setCityMode(false);
46
140
  setExtractMode(false);
47
141
  setReadyMode(true);
48
142
  renderDiscoveryCheck(false);
@@ -75,14 +169,23 @@ export const SETUP_PAGE_CLIENT_EXTRACTION = String.raw ` /* ---------- dash
75
169
  if (res.status !== 202) throw new Error(await res.text() || ("HTTP " + res.status));
76
170
  } catch (e) {
77
171
  lastError = e && e.message ? e.message : String(e);
78
- if (!stats && Date.now() - started > 25000) renderDiscoveryCheckIssue(lastError);
172
+ if (!stats && Date.now() - started > 25000) renderBridgeIssue(lastError);
79
173
  if (Date.now() - started > 20000) { statsSlow = true; if (stats) renderDashboard(); }
80
174
  }
81
175
  await sleep(statsSlow ? 1500 : 500);
82
176
  }
83
177
  } finally { statsPollStarted = false; }
84
178
  }
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
+ }
85
187
  function renderPlanGate() {
188
+ setCityMode(false);
86
189
  setExtractMode(false);
87
190
  setReadyMode(true);
88
191
  setHead("Choose your Echo", "Plan");
@@ -95,7 +198,7 @@ export const SETUP_PAGE_CLIENT_EXTRACTION = String.raw ` /* ---------- dash
95
198
  readyStage = "plan";
96
199
  app.className = "planGateStage";
97
200
  app.innerHTML =
98
- setupJourney(2) +
201
+ setupJourney(3) +
99
202
  '<section class="planGateDecision">' +
100
203
  '<div class="setupPlanSlot" id="setupPlan"></div>' +
101
204
  '<aside class="planGateAccount" aria-label="Connected account"><div class="setupAccountSlot" id="setupAccount"></div></aside>' +
@@ -104,6 +207,7 @@ export const SETUP_PAGE_CLIENT_EXTRACTION = String.raw ` /* ---------- dash
104
207
  renderAccountChoice();
105
208
  }
106
209
  function renderDashboard(error) {
210
+ setCityMode(false);
107
211
  setExtractMode(false);
108
212
  setReadyMode(true);
109
213
  var migratable = stats && stats.migratable ? stats.migratable : {};
@@ -127,6 +231,7 @@ export const SETUP_PAGE_CLIENT_EXTRACTION = String.raw ` /* ---------- dash
127
231
  var pendN = hasPendingCount ? pending : 0;
128
232
  var pendingCodex = typeof migratable.pendingCodex === "number" ? migratable.pendingCodex : null;
129
233
  var pendingClaudeCode = typeof migratable.pendingClaudeCode === "number" ? migratable.pendingClaudeCode : null;
234
+ var pendingCowork = typeof migratable.pendingCowork === "number" ? migratable.pendingCowork : null;
130
235
  if (connected && !billingStatusLoading && !setupPlanConfirmed()) {
131
236
  renderPlanGate();
132
237
  return;
@@ -149,17 +254,19 @@ export const SETUP_PAGE_CLIENT_EXTRACTION = String.raw ` /* ---------- dash
149
254
  ? "<strong>" + esc(number(pendN)) + " sessions</strong> found."
150
255
  : "<strong>" + esc(number(pendN)) + " sessions</strong> are ready to review."));
151
256
  var sub = degradedWithoutCounts
152
- ? "You can finish setup now. Your conversations stay on this computer, and you can retry the history import later."
257
+ ? "You can finish setup now. Your conversations stay on this Mac, and you can retry the history import later."
153
258
  : !pendingTrusted
154
259
  ? "Echo is matching your local history against what is already in memory."
155
260
  : (knownDone
156
261
  ? (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.")
157
262
  : (planLimited
158
263
  ? (importableNow > 0
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.")
264
+ ? currentPlanLabel + " can import " + number(importableNow) + " of them. Choose which ones to bring into memory; the other " + number(deferredByPlan) + " stay on this Mac."
265
+ : currentPlanLabel + " has no coding-session imports remaining. These sessions stay on this Mac.")
161
266
  : ""));
162
- // Mount the dashboard shell once; later polls only patch its text and counts.
267
+ // Mount the dashboard shell ONCE so the WebGL plate iframe is never re-created across /stats polls.
268
+ // (Rebuilding app.innerHTML on every partial-scan poll reloaded the iframe → the city flickered.)
269
+ // Later polls only patch the text/counts in place; the iframe stays mounted.
163
270
  var live = dashMounted && document.getElementById("exHeadline");
164
271
  if (!live) {
165
272
  extractMounted = false;
@@ -171,7 +278,7 @@ export const SETUP_PAGE_CLIENT_EXTRACTION = String.raw ` /* ---------- dash
171
278
  // one-line strip, not stat cards; the ETA is a commitment-moment note under the button,
172
279
  // not a display-size headline.
173
280
  app.innerHTML =
174
- setupJourney(3) +
281
+ setupJourney(4) +
175
282
  '<aside class="readyUtilityNav" aria-label="Account and plan settings">' +
176
283
  '<div class="planGateAccount readyAccountNav" aria-label="Connected account"><div class="setupAccountSlot" id="setupAccount"></div></div>' +
177
284
  '<div class="readyPlanNav" aria-label="Current plan"><span class="readyPlanNavLabel">Plan</span><div class="setupPlanSlot" id="setupPlan"></div></div>' +
@@ -206,6 +313,7 @@ export const SETUP_PAGE_CLIENT_EXTRACTION = String.raw ` /* ---------- dash
206
313
  document.getElementById("exSub").textContent = sub;
207
314
  var codexN = pendingTrusted && pendingCodex !== null ? pendingCodex : (sessions.codex || 0);
208
315
  var claudeN = pendingTrusted && pendingClaudeCode !== null ? pendingClaudeCode : (sessions.claudeCode || 0);
316
+ var coworkN = pendingTrusted && pendingCowork !== null ? pendingCowork : (sessions.cowork || 0);
209
317
  // Proof strip: one quiet line of evidence that the scan is real. Hidden when there is nothing to show.
210
318
  var srcIcon = function (id, fallback) {
211
319
  var asset = id === "claude-desktop" ? "claude" : "codex";
@@ -219,6 +327,8 @@ export const SETUP_PAGE_CLIENT_EXTRACTION = String.raw ` /* ---------- dash
219
327
  ? '<span class="pf">' + srcIcon("codex", "CX") + '<strong>' + esc(number(codexN)) + '</strong> Codex</span>' +
220
328
  '<span class="pfDot">&middot;</span>' +
221
329
  '<span class="pf">' + srcIcon("claude-desktop", "CL") + '<strong>' + esc(number(claudeN)) + '</strong> Claude Code</span>' +
330
+ '<span class="pfDot">&middot;</span>' +
331
+ '<span class="pf">' + srcIcon("claude-desktop", "CW") + '<strong>' + esc(number(coworkN)) + '</strong> Cowork</span>' +
222
332
  '<span class="pfNote">found on this computer</span>'
223
333
  : "");
224
334
  // Reassurances live at the moment of commitment — right under the button.
@@ -286,12 +396,14 @@ export const SETUP_PAGE_CLIENT_EXTRACTION = String.raw ` /* ---------- dash
286
396
  stopBillingPoll();
287
397
  decisionMade = true;
288
398
  extractionEnded = false;
399
+ setCityMode(false);
400
+ setScanMode(false);
289
401
  setReadyMode(false);
290
402
  setExtractMode(true);
291
403
  setHead("Extracting", "Starting");
292
404
  app.className = "extractionStartStage";
293
405
  app.innerHTML =
294
- setupJourney(3) +
406
+ setupJourney(4) +
295
407
  '<div class="discoverySpinner" role="status" aria-label="Starting import"><span aria-hidden="true"></span></div>';
296
408
  var started;
297
409
  try {
@@ -383,6 +495,7 @@ export const SETUP_PAGE_CLIENT_EXTRACTION = String.raw ` /* ---------- dash
383
495
  function renderProgress(progress) {
384
496
  if (extractionEnded) return;
385
497
  lastExtractionProgress = progress;
498
+ setCityMode(false);
386
499
  setExtractMode(true);
387
500
  var status = progress.status;
388
501
  var done = status === "completed";
@@ -394,15 +507,18 @@ export const SETUP_PAGE_CLIENT_EXTRACTION = String.raw ` /* ---------- dash
394
507
  var pct = total > 0 ? Math.round((completed / total) * 100) : (done ? 100 : 0);
395
508
  var preparing = (status === "starting" || status === "preparing");
396
509
  app.className = "";
397
- // Mount the shell once; each poll only rebuilds the text column.
510
+ // Mount the shell ONCE so the WebGL plate iframe survives every poll (it self-polls /progress);
511
+ // each poll only rebuilds the text column.
398
512
  if (!extractMounted) {
399
513
  app.innerHTML =
400
- '<div id="setupJourneySlot">' + setupJourney(3) + '</div>' +
401
- '<section class="exHero"><div class="exCopy" id="exText"></div></section>';
514
+ '<div id="setupJourneySlot">' + setupJourney(4) + '</div>' +
515
+ '<section class="exHero"><div class="exCopy" id="exText"></div>' +
516
+ plateBlock() +
517
+ '</section>';
402
518
  extractMounted = true;
403
519
  }
404
520
  var journeySlot = document.getElementById("setupJourneySlot");
405
- if (journeySlot) journeySlot.innerHTML = setupJourney(done ? 4 : 3);
521
+ if (journeySlot) journeySlot.innerHTML = setupJourney(done ? 5 : 4);
406
522
  var shell = app.querySelector(".exHero");
407
523
  if (shell) {
408
524
  shell.classList.toggle("exHeroResult", done || stopped);
@@ -664,7 +780,9 @@ export const SETUP_PAGE_CLIENT_EXTRACTION = String.raw ` /* ---------- dash
664
780
  }
665
781
  }
666
782
  function sessionSourceLabel(source) {
667
- return source === "claude-code" ? "Claude Code" : "Codex";
783
+ if (source === "claude-code") return "Claude Code";
784
+ if (source === "claude-desktop") return "Cowork";
785
+ return "Codex";
668
786
  }
669
787
  function sessionDateLabel(value) {
670
788
  if (!value) return "Date unavailable";
@@ -819,11 +937,12 @@ export const SETUP_PAGE_CLIENT_EXTRACTION = String.raw ` /* ---------- dash
819
937
  var key = String(session.key || "");
820
938
  var encodedKey = encodeURIComponent(key);
821
939
  var checked = !!selectedSessionKeys[key];
822
- var sourceIcon = session.source === "claude-code" ? "claude" : "codex";
823
- var sourceFallback = session.source === "claude-code" ? "CL" : "CX";
940
+ var isClaude = session.source === "claude-code" || session.source === "claude-desktop";
941
+ var sourceIcon = isClaude ? "claude" : "codex";
942
+ var sourceFallback = session.source === "claude-desktop" ? "CW" : (session.source === "claude-code" ? "CL" : "CX");
824
943
  return '<label class="sessionPickerRow' + (checked ? " is-selected" : "") + '">' +
825
944
  '<input type="checkbox" data-session-key="' + esc(encodedKey) + '"' + (checked ? " checked" : "") + ' />' +
826
- '<span class="sessionPickerSource ' + (session.source === "claude-code" ? "is-claude" : "is-codex") + '"><img src="/hud-assets/' + sourceIcon + '.svg" alt="" onerror="this.style.display=&quot;none&quot;;this.nextElementSibling.style.display=&quot;grid&quot;;" /><span class="sessionPickerSourceFallback">' + sourceFallback + '</span></span>' +
945
+ '<span class="sessionPickerSource ' + (isClaude ? "is-claude" : "is-codex") + '"><img src="/hud-assets/' + sourceIcon + '.svg" alt="" onerror="this.style.display=&quot;none&quot;;this.nextElementSibling.style.display=&quot;grid&quot;;" /><span class="sessionPickerSourceFallback">' + sourceFallback + '</span></span>' +
827
946
  '<span class="sessionPickerMain"><strong>' + esc(session.title || "Untitled coding session") + '</strong><small>' + esc(session.project || "No project detected") + ' · ' + esc(sessionDateLabel(session.date)) + '</small></span>' +
828
947
  '<span class="sessionPickerMeta">' + esc(compact(Number(session.approxInputTokens) || 0)) + ' tok</span>' +
829
948
  '</label>';
@@ -835,7 +954,7 @@ export const SETUP_PAGE_CLIENT_EXTRACTION = String.raw ` /* ---------- dash
835
954
  '<div class="sessionPickerShell">' +
836
955
  '<header class="sessionPickerHead"><div><h2 id="sessionPickerTitle">Choose conversations</h2><p class="sessionPickerSummary"><span class="sessionPickerQuota">Up to <strong>' + esc(number(limit)) + '</strong></span>' + inlineUpgrade + '<span class="sessionPickerOrder">&middot; newest selected first</span></p></div><button type="button" class="sessionPickerClose" id="closeSessionPicker" aria-label="Close">×</button></header>' +
837
956
  '<div class="sessionPickerToolbar"><label class="sessionSearch"><span>Search</span><input type="search" id="sessionPickerSearch" placeholder="Title or project" value="' + esc(sessionPickerQuery) + '" /></label>' +
838
- '<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>' +
957
+ '<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><option value="claude-desktop"' + (sessionPickerSource === "claude-desktop" ? " selected" : "") + '>Cowork</option></select></label>' +
839
958
  '</div>' +
840
959
  '<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>' +
841
960
  '<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>' +
@@ -923,12 +1042,13 @@ export const SETUP_PAGE_CLIENT_EXTRACTION = String.raw ` /* ---------- dash
923
1042
  ? account.email
924
1043
  : "the connected account";
925
1044
  accountSwitchPending = true;
1045
+ setCityMode(false);
926
1046
  setExtractMode(false);
927
1047
  setReadyMode(true);
928
1048
  setHead("Switch EchoMem account", "Confirm");
929
1049
  app.className = "accountSwitchStage";
930
1050
  app.innerHTML =
931
- setupJourney(3) +
1051
+ setupJourney(4) +
932
1052
  '<section class="accountSwitchConfirm" aria-labelledby="accountSwitchTitle">' +
933
1053
  '<button type="button" class="flowBack" id="cancelAccountSwitch">← Back</button>' +
934
1054
  '<h2 id="accountSwitchTitle">Switch account?</h2>' +
@@ -977,6 +1097,41 @@ export const SETUP_PAGE_CLIENT_EXTRACTION = String.raw ` /* ---------- dash
977
1097
  function paidRecallPlan(plan) {
978
1098
  return ["pro", "power", "team", "enterprise"].indexOf(String(plan || "").toLowerCase()) >= 0;
979
1099
  }
1100
+ var planQuotaCatalog = null;
1101
+ var planQuotaCatalogLastFetchedAt = 0;
1102
+ function quotaLimitsForPlan(plan) {
1103
+ var normalized = String(plan || "free").toLowerCase();
1104
+ if (normalized === "team") normalized = "pro";
1105
+ if (normalized === "enterprise") normalized = "power";
1106
+ var limits = planQuotaCatalog && planQuotaCatalog[normalized];
1107
+ if (!limits || typeof limits !== "object") return null;
1108
+ if (
1109
+ typeof limits.historicalConversationLimit !== "number" ||
1110
+ typeof limits.memoryProcessingInputTokensWeeklyLimit !== "number" ||
1111
+ typeof limits.memorySearchWeeklyLimit !== "number"
1112
+ ) return null;
1113
+ return limits;
1114
+ }
1115
+ function quotaNumber(value) {
1116
+ return number(Math.max(0, Math.floor(value)));
1117
+ }
1118
+ function quotaTokens(value) {
1119
+ var normalized = Math.max(0, Math.floor(value));
1120
+ if (normalized > 0 && normalized % 1000000 === 0) return String(normalized / 1000000) + "M";
1121
+ return normalized > 0 && normalized % 1000 === 0 ? String(normalized / 1000) + "K" : quotaNumber(normalized);
1122
+ }
1123
+ async function refreshPlanQuotaCatalog() {
1124
+ if (Date.now() - planQuotaCatalogLastFetchedAt < 60000) return;
1125
+ try {
1126
+ var response = await fetch("https://echo-mem-chrome.vercel.app/api/public/plan-quotas", { cache: "no-store" });
1127
+ var payload = response.ok ? await response.json() : null;
1128
+ if (!payload || !payload.plans || typeof payload.plans !== "object") return;
1129
+ planQuotaCatalog = payload.plans;
1130
+ planQuotaCatalogLastFetchedAt = Date.now();
1131
+ } catch (_) {
1132
+ // Generic labels remain visible until the managed catalog is available.
1133
+ }
1134
+ }
980
1135
  function setupPlanPrice(plan) {
981
1136
  return plan === "power" ? "$100" : "$20";
982
1137
  }
@@ -1038,7 +1193,7 @@ export const SETUP_PAGE_CLIENT_EXTRACTION = String.raw ` /* ---------- dash
1038
1193
  var statusLabel = canceled ? "Canceled" : (state === "trialing" ? "Trial active" : "Active");
1039
1194
  var lifecycle = paidPlanLifecycleText(plan);
1040
1195
  var quota = billingStatus && billingStatus.historicalConversationQuota;
1041
- var limit = quota && typeof quota.limit === "number" ? quota.limit : (plan === "power" ? 2000 : 500);
1196
+ var limit = quota && typeof quota.limit === "number" ? quota.limit : null;
1042
1197
  var moneyFact = canceled
1043
1198
  ? "No future charge"
1044
1199
  : (state === "trialing" ? setupPlanPrice(plan) + "/month after trial" : setupPlanPrice(plan) + "/month");
@@ -1047,7 +1202,7 @@ export const SETUP_PAGE_CLIENT_EXTRACTION = String.raw ` /* ---------- dash
1047
1202
  '<section class="billingCommitment' + (canceled ? ' is-canceled' : '') + '" aria-label="Current plan commitment">' +
1048
1203
  '<div class="billingCommitmentIdentity"><span>' + esc(statusLabel) + '</span><strong>' + esc(planLabel) + '</strong></div>' +
1049
1204
  '<div class="billingCommitmentFacts">' +
1050
- '<span><b>' + esc(number(limit)) + '</b> coding-session imports</span>' +
1205
+ '<span><b>' + esc(limit === null ? "Plan import allowance" : quotaNumber(limit)) + '</b>' + (limit === null ? "" : " coding-session imports") + '</span>' +
1051
1206
  '<span><b>' + esc(moneyFact) + '</b></span>' +
1052
1207
  '</div>' +
1053
1208
  '<p>' + esc(lifecycle || "Your paid plan is active.") + '</p>' +
@@ -1058,6 +1213,19 @@ export const SETUP_PAGE_CLIENT_EXTRACTION = String.raw ` /* ---------- dash
1058
1213
  }
1059
1214
  function setupPlanDefinition(plan) {
1060
1215
  var trialAvailable = !billingStatus || billingStatus.trialAvailable !== false;
1216
+ var limits = quotaLimitsForPlan(plan);
1217
+ var fallbackFeatures = plan === "power"
1218
+ ? ["2,000 past chats", "agent-heavy memory", "2,000 memory recalls / week"]
1219
+ : plan === "pro"
1220
+ ? ["500 past chats", "daily memory updates", "500 memory recalls / week"]
1221
+ : ["100 coding sessions", "a few new sessions / week", "100 memory recalls / week"];
1222
+ var features = limits
1223
+ ? [
1224
+ quotaNumber(limits.historicalConversationLimit) + " past chats",
1225
+ quotaTokens(limits.memoryProcessingInputTokensWeeklyLimit) + " new-chat tokens / week",
1226
+ quotaNumber(limits.memorySearchWeeklyLimit) + " memory recalls / week"
1227
+ ]
1228
+ : fallbackFeatures;
1061
1229
  if (plan === "power") return {
1062
1230
  id: "power",
1063
1231
  name: "Power",
@@ -1067,11 +1235,7 @@ export const SETUP_PAGE_CLIENT_EXTRACTION = String.raw ` /* ---------- dash
1067
1235
  cadence: trialAvailable ? "per month · 14-day trial" : "per month · billed immediately",
1068
1236
  sticker: "/hud-assets/echo-pricing-power-sticker.png",
1069
1237
  stickerAlt: "Power Echo arrives with a gold key and a crew of notebook helpers.",
1070
- features: [
1071
- "2,000 past chats",
1072
- "agent-heavy memory",
1073
- "2,000 memory recalls / week"
1074
- ]
1238
+ features: features
1075
1239
  };
1076
1240
  if (plan === "pro") return {
1077
1241
  id: "pro",
@@ -1083,11 +1247,7 @@ export const SETUP_PAGE_CLIENT_EXTRACTION = String.raw ` /* ---------- dash
1083
1247
  sticker: "/hud-assets/echo-pricing-pro-sticker.png",
1084
1248
  stickerAlt: "Pro Echo organizes tabbed notebooks and a daily refresh control.",
1085
1249
  popular: true,
1086
- features: [
1087
- "500 past chats",
1088
- "daily memory updates",
1089
- "500 memory recalls / week"
1090
- ]
1250
+ features: features
1091
1251
  };
1092
1252
  return {
1093
1253
  id: "free",
@@ -1098,7 +1258,7 @@ export const SETUP_PAGE_CLIENT_EXTRACTION = String.raw ` /* ---------- dash
1098
1258
  cadence: "free forever · no card needed",
1099
1259
  sticker: "/hud-assets/echo-pricing-free-sticker.png",
1100
1260
  stickerAlt: "Original Echo hugs one simple memory card.",
1101
- features: ["100 coding sessions", "a few new sessions / week", "100 memory recalls / week"]
1261
+ features: features
1102
1262
  };
1103
1263
  }
1104
1264
  function renderPlanHabitat() {
@@ -1220,7 +1380,11 @@ export const SETUP_PAGE_CLIENT_EXTRACTION = String.raw ` /* ---------- dash
1220
1380
  if (setupPlanChoice === "free") {
1221
1381
  if (readySettings) readySettings.classList.remove("is-plan-open", "is-checkout-pending");
1222
1382
  if (planGateDecision) planGateDecision.classList.remove("is-checkout-pending");
1223
- slot.innerHTML = '<div class="setupPlanConfirmed"><span class="setupPlanConfirmedIcon" aria-hidden="true">' + setupIcon("check") + '</span><span class="setupPlanConfirmedCopy"><strong>Original Echo</strong><span>100 coding sessions and 100 memory recalls each week.</span></span><button type="button" class="textButton" id="changeSetupPlan">Change</button></div>';
1383
+ var freeLimits = quotaLimitsForPlan("free");
1384
+ var freePlanCopy = freeLimits
1385
+ ? quotaNumber(freeLimits.historicalConversationLimit) + " coding sessions and " + quotaNumber(freeLimits.memorySearchWeeklyLimit) + " memory recalls each week."
1386
+ : "Your managed import and weekly memory allowances are ready.";
1387
+ slot.innerHTML = '<div class="setupPlanConfirmed"><span class="setupPlanConfirmedIcon" aria-hidden="true">' + setupIcon("check") + '</span><span class="setupPlanConfirmedCopy"><strong>Original Echo</strong><span>' + esc(freePlanCopy) + '</span></span><button type="button" class="textButton" id="changeSetupPlan">Change</button></div>';
1224
1388
  renderBillingCommitment("");
1225
1389
  var changeBtn = document.getElementById("changeSetupPlan");
1226
1390
  if (changeBtn) changeBtn.onclick = function () {
@@ -1321,6 +1485,7 @@ export const SETUP_PAGE_CLIENT_EXTRACTION = String.raw ` /* ---------- dash
1321
1485
  var nextBillingStatus = await getJson("/billing-status");
1322
1486
  if (billingEpoch !== accountStateEpoch || !connected) return;
1323
1487
  billingStatus = nextBillingStatus;
1488
+ await refreshPlanQuotaCatalog();
1324
1489
  billingLastCheckedAt = Date.now();
1325
1490
  billingCheckMessage = "";
1326
1491
  if (readyStage === "sessions" && setupPlanConfirmed()) {
@@ -1423,7 +1588,7 @@ export const SETUP_PAGE_CLIENT_EXTRACTION = String.raw ` /* ---------- dash
1423
1588
  '<p class="recallPromptText">' + esc(primary) + '</p>' +
1424
1589
  '<div class="recallPromptActions" aria-label="Try your first memory recall">' +
1425
1590
  '<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>' +
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>' : '') +
1591
+ '<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>' +
1427
1592
  '</div>' +
1428
1593
  '</article>' +
1429
1594
  '<p class="launchStatus" id="launchStatus"></p>' +
@@ -1441,6 +1606,7 @@ export const SETUP_PAGE_CLIENT_EXTRACTION = String.raw ` /* ---------- dash
1441
1606
  });
1442
1607
  }
1443
1608
  function renderEnding() {
1609
+ setCityMode(false);
1444
1610
  setExtractMode(true);
1445
1611
  // This view replaces the live extraction shell. Mark it unmounted so a failed
1446
1612
  // pause can rebuild the progress page instead of trying to patch missing DOM.
@@ -1448,7 +1614,7 @@ export const SETUP_PAGE_CLIENT_EXTRACTION = String.raw ` /* ---------- dash
1448
1614
  setHead("Ending extraction", "Ending");
1449
1615
  app.className = "";
1450
1616
  app.innerHTML =
1451
- setupJourney(3) +
1617
+ setupJourney(4) +
1452
1618
  '<section class="lifecycleState lifecyclePending">' +
1453
1619
  '<h2>Stopping the remaining queued work…</h2>' +
1454
1620
  '<p>Saved memories stay intact.</p>' +
@@ -1456,12 +1622,13 @@ export const SETUP_PAGE_CLIENT_EXTRACTION = String.raw ` /* ---------- dash
1456
1622
  '</section>';
1457
1623
  }
1458
1624
  function renderSkipped() {
1625
+ setCityMode(false);
1459
1626
  setExtractMode(true);
1460
1627
  setHead("Extraction ended for now", "Ended");
1461
1628
  app.className = "";
1462
1629
  var savedCount = Math.max(0, Number(lastExtractionProgress && lastExtractionProgress.extracted || 0));
1463
1630
  app.innerHTML =
1464
- setupJourney(savedCount > 0 ? 4 : 3) +
1631
+ setupJourney(savedCount > 0 ? 5 : 4) +
1465
1632
  '<section class="lifecycleState lifecyclePaused">' +
1466
1633
  '<header class="lifecycleIntro">' +
1467
1634
  '<h2>' + (savedCount > 0 ? esc(number(savedCount)) + ' memories are ready to use.' : 'Import ended safely.') + '</h2>' +
@@ -1525,7 +1692,7 @@ export const SETUP_PAGE_CLIENT_EXTRACTION = String.raw ` /* ---------- dash
1525
1692
  statsSlow = false;
1526
1693
  connected = false;
1527
1694
  decisionMade = false;
1528
- renderLocalLogin("Signed out locally. Connect EchoMem again to extract.");
1695
+ renderForensicReport("Signed out locally. Connect EchoMem again to extract.");
1529
1696
  } catch (e) {
1530
1697
  decisionMade = false;
1531
1698
  renderLogoutIssue(e && e.message ? e.message : String(e));