@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.
@@ -8,22 +8,21 @@ export const SETUP_PAGE_CLIENT_LIFECYCLE = String.raw ` /* ---------- scan-
8
8
  setHead("Local file access", "Consent");
9
9
  app.className = "consentStage mvpConsentStage";
10
10
  app.innerHTML =
11
+ setupJourney(1) +
11
12
  '<section class="mvpConsent" aria-labelledby="mvpConsentTitle">' +
12
- '<img class="mvpConsentEcho" src="/hud-assets/echo-face-cutout.png" alt="" />' +
13
- '<p class="mvpKicker">Before we begin</p>' +
14
- '<h2 id="mvpConsentTitle">Echo has something for you.</h2>' +
15
- '<p class="mvpConsentIntro">Let Echo read your local Codex and Claude Code history to build a private report. Youll see what’s filling your context window, what keeps getting repeated, and where memory could help.</p>' +
16
- '<p class="mvpPrivacy">The scan runs on this Mac. Nothing leaves it unless you later choose memories to extract.</p>' +
17
- '<button type="button" class="primary mvpPrimary" id="allowLocalScan">Allow scan &amp; build my report <span aria-hidden="true">→</span></button>' +
18
- '<button type="button" class="mvpQuiet" id="declineLocalScan">Not now</button>' +
13
+ '<div class="mvpConsentHero">' +
14
+ '<img class="mvpConsentEcho" src="/hud-assets/echo-face-cutout.png" alt="" />' +
15
+ '<div class="mvpConsentPitch">' +
16
+ '<h2 id="mvpConsentTitle">Start with the context youve already built.</h2>' +
17
+ '<p class="mvpConsentIntro">See what you keep re-explaining—and what’s worth carrying into future sessions.</p>' +
18
+ '</div>' +
19
+ '</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>' +
19
22
  '<p class="consentFine" id="consentStatus" role="status" aria-live="polite"></p>' +
20
23
  '</section>';
21
24
  var allow = document.getElementById("allowLocalScan");
22
25
  if (allow) allow.onclick = allowLocalScan;
23
- var decline = document.getElementById("declineLocalScan");
24
- if (decline) decline.onclick = function () {
25
- setConsentStatus("Echo can't continue setup without permission to scan your local history.", "required");
26
- };
27
26
  }
28
27
  function setConsentStatus(message, tone) {
29
28
  var node = document.getElementById("consentStatus");
@@ -716,8 +716,9 @@ export const SETUP_PAGE_CLIENT_REPORT_AUDIT = String.raw ` /* ---------- fo
716
716
  '</section>' +
717
717
  '<div class="audit-list">' + groups.map(function (group, index) { return auditRowHtml(group, index, totalWaste, slices[index]); }).join('') + '</div>' +
718
718
  '<footer class="audit-end">' +
719
- '<h3>Stop burning tokens on these mistakes.</h3>' +
720
- '<button class="rdo-cta" data-connect-echo>' + (connected ? 'Continue' : 'Connect Echo') +
719
+ '<h3>Turn this lost context into memory.</h3>' +
720
+ '<p class="audit-memory-bridge">Your report found where context is being repeated. Next, Echo can turn selected coding sessions into memories your agents can recall.</p>' +
721
+ '<button class="rdo-cta" data-connect-echo>Build my first memories' +
721
722
  '<img class="cta-face" src="/hud-assets/echo-face-cutout.png" alt="Echo" />' +
722
723
  '<svg class="cta-arrow" viewBox="0 0 64 26" aria-hidden="true">' +
723
724
  '<defs><filter id="ctaPencil" x="-20%" y="-40%" width="140%" height="180%"><feTurbulence type="fractalNoise" baseFrequency="0.05 0.12" numOctaves="3" seed="11"/><feDisplacementMap in="SourceGraphic" scale="2.6"/></filter></defs>' +
@@ -119,10 +119,11 @@ export const SETUP_PAGE_CLIENT_REPORT_CITY = String.raw ` /* ---------- ful
119
119
  'Local Only' +
120
120
  '</span>' +
121
121
  '</nav>' +
122
- '<button class="navConnect" data-connect-echo><span class="navDot"></span>Start Now' +
122
+ '<button class="navConnect" data-connect-echo><span class="navDot"></span>Build my first memories' +
123
123
  '<svg viewBox="0 0 12 12" fill="none" aria-hidden="true"><path d="M3.5 8.5 8.5 3.5M4.5 3.5h4v4" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/></svg>' +
124
124
  '</button>' +
125
125
  '</header>' +
126
+ setupJourney(2) +
126
127
  '<section class="cityHero"><div class="cityFigureRow">' +
127
128
  '<figure class="cityFig">' +
128
129
  '<div class="cityShell" id="city-shell">' +
@@ -130,7 +131,7 @@ export const SETUP_PAGE_CLIENT_REPORT_CITY = String.raw ` /* ---------- ful
130
131
  '</div>' +
131
132
  '<button class="cityFullBtn" id="city-full-btn" type="button">' +
132
133
  '<svg viewBox="0 0 12 12" fill="none" aria-hidden="true"><path d="M7.5 1.5h3v3M4.5 10.5h-3v-3M10.5 1.5 7 5M1.5 10.5 5 7" stroke="currentColor" stroke-width="1.4" stroke-linecap="round" stroke-linejoin="round"/></svg>' +
133
- 'Enter' +
134
+ 'Explore the memory city' +
134
135
  '</button>' +
135
136
  '</figure>' +
136
137
  '<div class="cityNote" id="city-fieldnote"></div>' +
@@ -168,59 +169,188 @@ export const SETUP_PAGE_CLIENT_REPORT_CITY = String.raw ` /* ---------- ful
168
169
  setScanMode(false);
169
170
  setCityMode(false);
170
171
  }
172
+ function formatReportEta(seconds) {
173
+ seconds = Math.max(1, Math.round(seconds));
174
+ if (seconds >= 3600) {
175
+ var hours = Math.floor(seconds / 3600);
176
+ var minutesAfterHours = Math.ceil((seconds % 3600) / 60);
177
+ return "About " + hours + "h" + (minutesAfterHours ? " " + minutesAfterHours + "m" : "") + " remaining";
178
+ }
179
+ if (seconds >= 60) {
180
+ var minutes = Math.floor(seconds / 60);
181
+ var secondsAfterMinutes = seconds % 60;
182
+ return "About " + minutes + "m " + secondsAfterMinutes + "s remaining";
183
+ }
184
+ return "About " + seconds + "s remaining";
185
+ }
186
+ function estimateReportRemaining(progress, overall, stage) {
187
+ void overall;
188
+ var now = Date.now();
189
+ var stageElapsed = Math.max(0, Number(progress.stageElapsedMs || 0) / 1000);
190
+ var stageDone = Math.max(0, Number(progress.stageDone || 0));
191
+ var stageTotal = Math.max(0, Number(progress.stageTotal || 0));
192
+ function tickExistingEstimate() {
193
+ if (reportEtaDisplaySeconds <= 0) {
194
+ return { seconds: 0, overdue: false, calibrating: true };
195
+ }
196
+ var sinceLast = reportEtaUpdatedAt ? Math.max(0, (now - reportEtaUpdatedAt) / 1000) : 0;
197
+ reportEtaDisplaySeconds = Math.max(0, reportEtaDisplaySeconds - sinceLast);
198
+ reportEtaUpdatedAt = now;
199
+ return {
200
+ seconds: reportEtaDisplaySeconds,
201
+ overdue: now >= reportEtaDeadline || reportEtaDisplaySeconds <= 0,
202
+ calibrating: false,
203
+ };
204
+ }
205
+
206
+ // The visual progress bands describe work completed, not wall-clock cost. In particular,
207
+ // reaching the 75% classification band does NOT mean only 25% of the time remains.
208
+ // Calibrate only from movement inside the expensive classification stage.
209
+ if (stage !== "classifying-repeated-context") {
210
+ return tickExistingEstimate();
211
+ }
212
+ if (reportEtaStage !== stage) {
213
+ reportEtaStage = stage;
214
+ reportEtaStageDone = stageDone;
215
+ reportEtaStageElapsed = stageElapsed;
216
+ reportEtaObservedRate = 0;
217
+ return { seconds: 0, overdue: false, calibrating: true };
218
+ }
219
+
220
+ // The first canonical completions can be cache hits and arrive almost instantly. Treat the
221
+ // first observation as a baseline, then measure only later movement over real elapsed time.
222
+ if (stageDone > reportEtaStageDone && stageElapsed > reportEtaStageElapsed) {
223
+ var sampleElapsed = stageElapsed - reportEtaStageElapsed;
224
+ var sampleDone = stageDone - reportEtaStageDone;
225
+ if (sampleElapsed >= 2 && sampleDone > 0) {
226
+ var sampleRate = sampleDone / sampleElapsed;
227
+ reportEtaObservedRate = reportEtaObservedRate > 0
228
+ ? reportEtaObservedRate * 0.65 + sampleRate * 0.35
229
+ : sampleRate;
230
+ reportEtaStageDone = stageDone;
231
+ reportEtaStageElapsed = stageElapsed;
232
+ }
233
+ }
234
+ if (
235
+ reportEtaObservedRate <= 0 ||
236
+ !isFinite(reportEtaObservedRate)
237
+ ) {
238
+ return { seconds: 0, overdue: false, calibrating: true };
239
+ }
240
+ if (stageTotal <= stageDone) return tickExistingEstimate();
241
+
242
+ // Add a safety margin for the tail of slower sessions plus final report reconciliation.
243
+ // This intentionally prefers a slightly early completion over a fake three-second promise.
244
+ var observedRemaining = ((stageTotal - stageDone) / reportEtaObservedRate) * 1.35 + 5;
245
+ observedRemaining = Math.min(15 * 60, Math.max(8, observedRemaining));
246
+
247
+ if (!reportEtaDeadline) {
248
+ reportEtaDeadline = now + observedRemaining * 1000;
249
+ reportEtaDisplaySeconds = observedRemaining;
250
+ } else {
251
+ var sinceLast = reportEtaUpdatedAt ? Math.max(0, (now - reportEtaUpdatedAt) / 1000) : 0;
252
+ var clockRemaining = Math.max(0, reportEtaDisplaySeconds - sinceLast);
253
+ // A faster observed rate may shorten the estimate. A slower sample never pushes the
254
+ // countdown back upward; passing the first deadline changes the copy to "Finishing up".
255
+ reportEtaDisplaySeconds = Math.min(clockRemaining, observedRemaining);
256
+ }
257
+ reportEtaUpdatedAt = now;
258
+ return {
259
+ seconds: Math.max(0, reportEtaDisplaySeconds),
260
+ overdue: now >= reportEtaDeadline || reportEtaDisplaySeconds <= 0,
261
+ calibrating: false,
262
+ };
263
+ }
171
264
  function renderLoading() {
172
265
  var progress = reportEnvelope && reportEnvelope.kind === "scanning" ? (reportEnvelope.progress || {}) : {};
173
- var scanned = Math.max(0, Number(progress.scanned || 0));
174
- var total = Math.max(0, Number(progress.total || 0));
175
- var pct = total > 0 ? Math.min(100, Math.round((scanned / total) * 100)) : 0;
176
- if (!reportEstimateDeadline) reportEstimateDeadline = Date.now() + 30000;
177
- function estimateCaption() {
178
- var seconds = Math.ceil((reportEstimateDeadline - Date.now()) / 1000);
179
- return seconds > 0
180
- ? "About " + seconds + " seconds remaining"
181
- : "Still working — your report will be ready soon.";
266
+ var scanId = String(reportEnvelope && reportEnvelope.scanId || "");
267
+ if (scanId !== reportEtaScanId) {
268
+ reportEtaScanId = scanId;
269
+ reportEtaDeadline = 0;
270
+ reportEtaDisplaySeconds = 0;
271
+ reportEtaUpdatedAt = 0;
272
+ reportEtaStage = "";
273
+ reportEtaStageDone = 0;
274
+ reportEtaStageElapsed = 0;
275
+ reportEtaObservedRate = 0;
182
276
  }
183
- var caption = estimateCaption();
184
- function updateEstimateCaption() {
185
- var node = document.getElementById("scan-load-caption");
186
- if (!node) return;
187
- node.textContent = estimateCaption();
188
- if (Date.now() >= reportEstimateDeadline && reportEstimateTimer) {
189
- clearInterval(reportEstimateTimer);
190
- reportEstimateTimer = null;
191
- }
277
+ var scannedValue = Number(progress.scanned);
278
+ var totalValue = Number(progress.total);
279
+ var scanned = isFinite(scannedValue) ? Math.max(0, scannedValue) : 0;
280
+ var total = isFinite(totalValue) ? Math.max(0, totalValue) : 0;
281
+ var visibleScanned = total > 0 ? Math.min(scanned, total) : scanned;
282
+ // The bar tracks the whole scan (reading summary classifying → finalizing), which the
283
+ // file counters alone cannot express: they hit their maximum while analysis is still running.
284
+ var overallValue = Number(progress.overall);
285
+ var overall = isFinite(overallValue) ? Math.min(1, Math.max(0, overallValue)) : 0;
286
+ var pct = Math.round(overall * 100);
287
+ var stage = String(progress.stage || "starting");
288
+ var stageDone = Math.max(0, Number(progress.stageDone || 0));
289
+ var stageTotal = Math.max(0, Number(progress.stageTotal || 0));
290
+ var stageElapsed = Math.max(0, Number(progress.stageElapsedMs || 0));
291
+ var beat = Math.floor(stageElapsed / 4500);
292
+ var progressText = "";
293
+ if (total > 0 && stage === "reading-transcripts") {
294
+ progressText = "Reading " + number(visibleScanned) + " of " + number(total) + " sessions…";
295
+ } else if (stage === "building-summary") {
296
+ var summaryBeats = [
297
+ "Organizing what Echo found…",
298
+ "Building the report summary…"
299
+ ];
300
+ progressText = summaryBeats[beat % summaryBeats.length];
301
+ } else if (stage === "classifying-repeated-context") {
302
+ var analysisCount = stageTotal > 0
303
+ ? " · " + number(Math.min(stageDone, stageTotal)) + " of " + number(stageTotal)
304
+ : "";
305
+ var analysisBeats = [
306
+ "Comparing context across sessions" + analysisCount + "…",
307
+ "Finding repeated context" + analysisCount + "…",
308
+ "Separating useful context from noise" + analysisCount + "…"
309
+ ];
310
+ progressText = analysisBeats[beat % analysisBeats.length];
311
+ } else if (stage === "finalizing-report") {
312
+ progressText = beat % 2 === 0 ? "Checking the report totals…" : "Preparing your report…";
192
313
  }
314
+ var etaState = estimateReportRemaining(progress, overall, stage);
315
+ var etaText = etaState.overdue
316
+ ? "Taking a little longer than estimated…"
317
+ : (etaState.seconds > 0 ? formatReportEta(etaState.seconds) : "Estimating time remaining…");
193
318
  var fill = document.getElementById("scan-load-fill");
319
+ var caption = document.getElementById("scan-load-caption");
320
+ var eta = document.getElementById("scan-load-eta");
194
321
  if (app.className === "scanLoading" && fill) {
195
322
  fill.style.width = pct + "%";
196
323
  fill.parentElement.setAttribute("aria-valuenow", String(pct));
197
- var existingCaption = document.getElementById("scan-load-caption");
198
- if (existingCaption) existingCaption.textContent = caption;
324
+ fill.parentElement.setAttribute("aria-valuetext", (progressText || "Preparing session count") + " " + etaText);
325
+ if (caption) {
326
+ caption.textContent = progressText;
327
+ caption.hidden = !progressText;
328
+ }
329
+ if (eta) eta.textContent = etaText;
199
330
  return;
200
331
  }
201
332
  setScanMode(true);
202
- setHead("Reading your local coding history", "Local scan");
333
+ setHead("Building your report", "Local scan");
203
334
  app.className = "scanLoading";
204
335
  app.innerHTML =
205
- '<section class="mvpReading" aria-live="polite">' +
206
- '<span class="scanMascot mvpReadingAnimation" aria-hidden="true">' +
207
- '<span class="scanMascotFrames">' +
208
- '<img class="scanMascotFrame" src="/hud-assets/scan-loop/frame-01-ready.png" alt="" />' +
209
- '<img class="scanMascotFrame" src="/hud-assets/scan-loop/frame-02-open.png" alt="" />' +
210
- '<img class="scanMascotFrame" src="/hud-assets/scan-loop/frame-03-pull.png" alt="" />' +
211
- '<img class="scanMascotFrame" src="/hud-assets/scan-loop/frame-04-read.png" alt="" />' +
212
- '<img class="scanMascotFrame" src="/hud-assets/scan-loop/frame-04-read.png" alt="" />' +
213
- '<img class="scanMascotFrame" src="/hud-assets/scan-loop/frame-05-file.png" alt="" />' +
214
- '<img class="scanMascotFrame" src="/hud-assets/scan-loop/frame-06-close.png" alt="" />' +
215
- '</span>' +
336
+ setupJourney(1) +
337
+ '<section class="mvpReading" aria-labelledby="scanLoadingTitle">' +
338
+ '<span class="mvpReadingMascot scanMascotFrames" aria-hidden="true">' +
339
+ '<img class="scanMascotFrame" src="/hud-assets/scan-loop/frame-01-ready.png" alt="" />' +
340
+ '<img class="scanMascotFrame" src="/hud-assets/scan-loop/frame-02-open.png" alt="" />' +
341
+ '<img class="scanMascotFrame" src="/hud-assets/scan-loop/frame-03-pull.png" alt="" />' +
342
+ '<img class="scanMascotFrame" src="/hud-assets/scan-loop/frame-04-read.png" alt="" />' +
343
+ '<img class="scanMascotFrame" src="/hud-assets/scan-loop/frame-04-read.png" alt="" />' +
344
+ '<img class="scanMascotFrame" src="/hud-assets/scan-loop/frame-05-file.png" alt="" />' +
345
+ '<img class="scanMascotFrame" src="/hud-assets/scan-loop/frame-06-close.png" alt="" />' +
216
346
  '</span>' +
217
- '<h2>Reading your coding history…</h2>' +
218
- '<p id="scan-load-caption" class="scanCaption">' + esc(caption) + '</p>' +
219
- '<div class="scanProgress mvpReadingProgress" role="progressbar" aria-label="Local scan progress" aria-valuemin="0" aria-valuemax="100" aria-valuenow="' + pct + '">' +
347
+ '<h2 id="scanLoadingTitle">Building your report…</h2>' +
348
+ '<p class="mvpReadingCount" id="scan-load-caption" aria-live="polite"' + (progressText ? "" : " hidden") + '>' + esc(progressText) + '</p>' +
349
+ '<p class="mvpReadingEta" id="scan-load-eta">' + esc(etaText) + '</p>' +
350
+ '<div class="scanProgress mvpReadingProgress" role="progressbar" aria-labelledby="scanLoadingTitle" aria-valuemin="0" aria-valuemax="100" aria-valuenow="' + pct + '" aria-valuetext="' + esc((progressText || "Preparing session count") + " " + etaText) + '">' +
220
351
  '<i id="scan-load-fill" class="scanProgressFill" style="width:' + pct + '%"></i>' +
221
352
  '</div>' +
222
353
  '</section>';
223
- if (!reportEstimateTimer) reportEstimateTimer = setInterval(updateEstimateCaption, 1000);
224
354
  }
225
355
 
226
356
  `;
@@ -1462,6 +1462,43 @@ export const SETUP_PAGE_STYLES_EXTRACTION = String.raw ` /* ---- dashboard (p
1462
1462
  .savedHeading strong {
1463
1463
  font-variant-numeric: tabular-nums;
1464
1464
  }
1465
+ .firstRecallStage {
1466
+ display: flex;
1467
+ flex-direction: column;
1468
+ gap: clamp(22px, 3vw, 34px);
1469
+ }
1470
+ .firstRecallStage .resultTop h2 {
1471
+ max-width: none;
1472
+ margin: 0;
1473
+ color: var(--echo-ink-text);
1474
+ font-family: var(--echo-font-brand);
1475
+ font-size: clamp(42px, 6vw, 72px);
1476
+ font-weight: 700;
1477
+ line-height: 0.96;
1478
+ letter-spacing: -0.045em;
1479
+ }
1480
+ .firstRecallStage .savedHeading {
1481
+ display: block;
1482
+ }
1483
+ .firstRecallStage .savedHeading .savedMain {
1484
+ color: var(--echo-ink-text);
1485
+ }
1486
+ .firstRecallStage .savedHeading strong {
1487
+ color: var(--echo-ink-primary);
1488
+ font-weight: 900;
1489
+ }
1490
+ .firstRecallStage .tryCard {
1491
+ margin-top: 0;
1492
+ }
1493
+ .firstRecallStage .tryCard > .tryTitle {
1494
+ margin-bottom: 0;
1495
+ color: var(--echo-ink-primary);
1496
+ font-size: clamp(26px, 3vw, 38px);
1497
+ line-height: 1.04;
1498
+ }
1499
+ .firstRecallStage .recallPromptPrimary {
1500
+ margin-top: 18px;
1501
+ }
1465
1502
  .viewEchoLink {
1466
1503
  color: var(--echo-ink-mute);
1467
1504
  font-style: normal;
@@ -165,7 +165,9 @@ export const SETUP_PAGE_STYLES_FOUNDATION = String.raw ` :root {
165
165
  min-width: 10px;
166
166
  border-radius: 0;
167
167
  background: var(--echo-ink-primary);
168
- transition: width 320ms ease;
168
+ /* Matched to the 700ms report poll so the fill glides between updates instead of
169
+ stepping and then sitting still for the rest of the interval. */
170
+ transition: width 700ms linear;
169
171
  }
170
172
  .scanCaption {
171
173
  margin: 20px 0 0;