@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
@@ -10,7 +10,7 @@ const CHECKPOINT_REASON = [
10
10
  "Private persistence happens first. For a user with a company group, call `request_group_session_sharing` near conversation start or after a qualifying private save.",
11
11
  "On the first request_group_session_sharing call in a conversation, omit groupSharingScopeId so EchoMem mints a fresh opaque scope. Reuse that exact scope only in this conversation for later get/request/set/save calls, and never persist it as memory. If multiple groups are returned, reuse the same scope and call once per groupId. Relay the tool's exact text question and call set_group_session_sharing only after an explicit Yes/No. Silence leaves that group's state unset. A Share decision syncs eligible memories from later saves carrying the same scope to that group; saves automatically sync to every approved group. Keep private keeps them private for that group.",
12
12
  "Flagged memories are withheld from automatic conversation sync and remain private.",
13
- "If EchoMem reports that the encrypted vault is locked, relay its local unlock instruction; for a standalone installation, use `echomem-mcp unlock`. Never silently skip a qualifying checkpoint.",
13
+ "If EchoMem reports that the encrypted vault is locked, tell the user to open Echo Desktop and unlock the vault there; on a headless system, use `echomem-mcp unlock`. Never silently skip a qualifying checkpoint.",
14
14
  ].join(" ");
15
15
  function currentTurnSlice(transcript) {
16
16
  const lines = transcript.split(/\r?\n/);
@@ -6,28 +6,27 @@ export const SETUP_PAGE_CLIENT_CORE = String.raw ` var params = new URLSear
6
6
  var app = document.getElementById("app");
7
7
  var title = document.getElementById("title");
8
8
  var status = document.getElementById("status");
9
- // The local bridge is authoritative. Login never gets access to history/import routes;
9
+ var report = null;
10
+ var reportEnvelope = null;
11
+ // The local bridge is authoritative. Login never gets access to scan/import routes;
10
12
  // onboarding may request the separate local-history permission.
11
13
  var setupFlow = "onboarding";
14
+ var activeScanId = "";
12
15
  var stats = null;
13
16
  var statsSlow = false;
14
17
  var decisionMade = false;
15
18
  var connected = false;
16
19
  var localHistoryConsentGranted = false;
17
- var extractMounted = false; // extraction shell mounted once; persists across /progress polls
20
+ var extractMounted = false; // WebGL extraction-plate iframe mounted once; persists across /progress polls
18
21
  var lastExtractionProgress = null; // restores the live extraction view if ending fails
19
22
  var extractionRecoveryMessage = ""; // persists pause failures across resumed progress updates
20
23
  var exStartAt = 0; // extraction start (ms) — drives the countdown
21
24
  var exDeadline = 0; // first honest ETA deadline; passing it while running → "taking longer" banner
22
25
  var extractionEnded = false; // user chose to leave the extraction flow before natural completion
23
26
  var progressPollRunId = 0; // invalidates older progress loops when extraction resumes
24
- var dashMounted = false; // dashboard shell mounted once; persists across /stats polls
27
+ var dashMounted = false; // dashboard shell (incl. plate iframe) mounted once; persists across /stats polls
28
+ var reportMounted = false; // report shell (city iframe) mounted once; loading is an overlay on it, not a separate page
25
29
  var workspacePath = "";
26
- var setupPlatform = "unknown";
27
- // Preview states render before /config; production init replaces these with bridge-reported
28
- // capabilities before rendering a user-facing state.
29
- var canOpenClaudeDesktop = true;
30
- var canOpenAgentSessions = true;
31
30
  var billingStatus = null;
32
31
  var billingStatusLoading = false;
33
32
  var setupPlanChoice = "";
@@ -52,7 +51,17 @@ export const SETUP_PAGE_CLIENT_CORE = String.raw ` var params = new URLSear
52
51
  // Invalidates async work started for a previously connected account.
53
52
  var accountStateEpoch = 0;
54
53
  var statsPollStarted = false;
54
+ var reportPollStarted = false;
55
+ var reportEtaDeadline = 0;
56
+ var reportEtaDisplaySeconds = 0;
57
+ var reportEtaUpdatedAt = 0;
58
+ var reportEtaStage = "";
59
+ var reportEtaStageDone = 0;
60
+ var reportEtaStageElapsed = 0;
61
+ var reportEtaObservedRate = 0;
62
+ var reportEtaScanId = "";
55
63
  var readyStage = "";
64
+ var NIGHT_HOURS = { 22: 1, 23: 1, 0: 1, 1: 1, 2: 1, 3: 1 };
56
65
 
57
66
  try {
58
67
  var storedSetupPlanChoice = sessionStorage.getItem("echomem:setup-plan:" + nonce) || "";
@@ -98,6 +107,7 @@ export const SETUP_PAGE_CLIENT_CORE = String.raw ` var params = new URLSear
98
107
  var paths = {
99
108
  "shield-check": '<path d="M12 3 5 6v5c0 4.6 2.9 8.1 7 10 4.1-1.9 7-5.4 7-10V6l-7-3Z"/><path d="m8.8 12.1 2 2 4.4-4.5"/>',
100
109
  "folder-read": '<path d="M3 7.5h6l2-2h4.5A2.5 2.5 0 0 1 18 8v1"/><path d="M3 7.5V18a2 2 0 0 0 2 2h7"/><circle cx="17" cy="15" r="3.5"/><path d="m19.5 17.5 2 2"/>',
110
+ "report": '<path d="M5 20V10"/><path d="M12 20V4"/><path d="M19 20v-7"/><path d="M3 20h18"/>',
101
111
  "cloud-off": '<path d="m3 3 18 18"/><path d="M6.7 6.7A5.5 5.5 0 0 0 6.5 17H17"/><path d="M10.7 5.2A7 7 0 0 1 19 12.1 4 4 0 0 1 19.5 19"/>',
102
112
  "list-check": '<rect x="4" y="4" width="16" height="16" rx="3"/><path d="m7.5 9 1.4 1.4L11.5 8"/><path d="M13.5 9h3"/><path d="m7.5 14 1.4 1.4 2.6-2.4"/><path d="M13.5 14h3"/>',
103
113
  "pause": '<path d="M9 7v10"/><path d="M15 7v10"/>',
@@ -108,7 +118,7 @@ export const SETUP_PAGE_CLIENT_CORE = String.raw ` var params = new URLSear
108
118
  return '<svg class="setupGlyph" viewBox="0 0 24 24" fill="none" aria-hidden="true" focusable="false"><g stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round">' + (paths[name] || "") + '</g></svg>';
109
119
  }
110
120
  function setupJourney(currentStep) {
111
- var labels = ["Access", "Connect", "Import", "Try Echo"];
121
+ var labels = ["Scan", "Report", "Connect", "Import", "Try Echo"];
112
122
  var items = labels.map(function (label, index) {
113
123
  var step = index + 1;
114
124
  var state = step < currentStep ? " is-complete" : (step === currentStep ? " is-current" : "");
@@ -123,13 +133,21 @@ export const SETUP_PAGE_CLIENT_CORE = String.raw ` var params = new URLSear
123
133
  title.textContent = nextTitle;
124
134
  status.textContent = nextStatus;
125
135
  }
136
+ function setCityMode(enabled) {
137
+ document.body.classList.toggle("cityMode", !!enabled);
138
+ if (enabled) document.body.classList.remove("scanMode", "extractMode", "readyMode");
139
+ }
140
+ function setScanMode(enabled) {
141
+ document.body.classList.toggle("scanMode", !!enabled);
142
+ if (enabled) document.body.classList.remove("cityMode", "extractMode", "readyMode");
143
+ }
126
144
  function setExtractMode(enabled) {
127
145
  document.body.classList.toggle("extractMode", !!enabled);
128
- if (enabled) document.body.classList.remove("readyMode");
146
+ if (enabled) document.body.classList.remove("scanMode", "readyMode");
129
147
  }
130
148
  function setReadyMode(enabled) {
131
149
  document.body.classList.toggle("readyMode", !!enabled);
132
- if (enabled) document.body.classList.remove("extractMode");
150
+ if (enabled) document.body.classList.remove("scanMode", "cityMode", "extractMode");
133
151
  }
134
152
  function number(value) {
135
153
  return typeof value === "number" && isFinite(value) ? Math.round(value).toLocaleString() : "-";
@@ -141,6 +159,14 @@ export const SETUP_PAGE_CLIENT_CORE = String.raw ` var params = new URLSear
141
159
  if (abs >= 1e6) return (value / 1e6).toFixed(1) + "M";
142
160
  if (abs >= 1e3) return (value / 1e3).toFixed(1) + "K";
143
161
  return String(Math.round(value));
162
+ }
163
+ function big(value) {
164
+ if (typeof value !== "number" || !isFinite(value)) return "-";
165
+ var n = Math.abs(value);
166
+ if (n >= 1e9) return (value / 1e9).toFixed(2) + "B";
167
+ if (n >= 1e6) return (value / 1e6).toFixed(1) + "M";
168
+ if (n >= 1e3) return Math.round(value / 1e3) + "K";
169
+ return String(Math.round(value));
144
170
  }
145
171
  function sleep(ms) { return new Promise(function (resolve) { setTimeout(resolve, ms); }); }
146
172
  async function fetchWithTimeout(url, options, timeoutMs) {
@@ -174,6 +200,12 @@ export const SETUP_PAGE_CLIENT_CORE = String.raw ` var params = new URLSear
174
200
  try { data = raw ? JSON.parse(raw) : {}; } catch (_) {}
175
201
  if (!res.ok) throw new Error(data.message || data.error || raw || ("HTTP " + res.status));
176
202
  return data;
203
+ }
204
+ function metric(label, value, detail) {
205
+ return '<section class="metric"><p class="label">' + esc(label) + '</p><strong class="value">' + esc(value) + '</strong><p class="detail">' + esc(detail) + '</p></section>';
206
+ }
207
+ function row(label, value) {
208
+ return '<div class="row"><dt>' + esc(label) + '</dt><dd>' + esc(value) + '</dd></div>';
177
209
  }
178
210
  function routeError(code) {
179
211
  if (code === "NO_PENDING_SESSIONS") return "There are no unprocessed local conversations to extract right now.";
@@ -227,18 +259,20 @@ export const SETUP_PAGE_CLIENT_CORE = String.raw ` var params = new URLSear
227
259
  }
228
260
  function renderLocalLogin(message) {
229
261
  var onboardingConnect = setupFlow !== "login";
262
+ setCityMode(false);
263
+ setScanMode(false);
230
264
  setExtractMode(false);
231
265
  setReadyMode(true);
232
266
  setHead("Connect EchoMem", "Local login");
233
267
  app.className = "localAuthStage localAuthWelcomeStage localAuthEmailStage";
234
268
  app.innerHTML =
235
- setupJourney(2) +
269
+ setupJourney(3) +
236
270
  '<section class="localAuthWelcome" aria-labelledby="localAuthWelcomeTitle">' +
237
271
  '<div class="localAuthOrbScene" aria-hidden="true"><span class="localAuthOrb"></span><span class="localAuthOrbShadow"></span></div>' +
238
272
  '<div class="localAuthWelcomeCopy">' +
239
273
  '<h2 id="localAuthWelcomeTitle">' + (onboardingConnect ? 'Take your context with you.' : 'Welcome to Echo') + '</h2>' +
240
274
  (onboardingConnect
241
- ? '<p class="localAuthConnectPurpose">Connect this computer to Echo to use the memories you choose across your AI tools.</p>'
275
+ ? '<p class="localAuthConnectPurpose">Connect this Mac to Echo to use the memories you choose across your AI tools.</p>'
242
276
  : '') +
243
277
  '</div>' +
244
278
  '<form class="localAuthWelcomeForm" id="localAuthEmailForm">' +
@@ -263,6 +297,8 @@ export const SETUP_PAGE_CLIENT_CORE = String.raw ` var params = new URLSear
263
297
  updateLocalAuthSendButton();
264
298
  }
265
299
  function renderLocalLoginComplete() {
300
+ setCityMode(false);
301
+ setScanMode(false);
266
302
  setExtractMode(false);
267
303
  setReadyMode(true);
268
304
  setHead("EchoMem connected", "Done");
@@ -271,7 +307,7 @@ export const SETUP_PAGE_CLIENT_CORE = String.raw ` var params = new URLSear
271
307
  '<section class="localAuthComplete" aria-labelledby="localAuthCompleteTitle">' +
272
308
  '<img src="/hud-assets/echo-face-cutout.png" alt="" />' +
273
309
  '<h2 id="localAuthCompleteTitle">You\'re signed in.</h2>' +
274
- '<p>Return to your terminal.</p>' +
310
+ '<p>Return to Terminal.</p>' +
275
311
  '<p class="localAuthCompleteNext">Run <code>echomem-mcp init</code> when you\'re ready to start onboarding.</p>' +
276
312
  '</section>';
277
313
  }
@@ -311,12 +347,14 @@ export const SETUP_PAGE_CLIENT_CORE = String.raw ` var params = new URLSear
311
347
  }
312
348
  }
313
349
  function renderLocalOtp(message) {
350
+ setCityMode(false);
351
+ setScanMode(false);
314
352
  setExtractMode(false);
315
353
  setReadyMode(true);
316
354
  setHead("Check your email", "Local login");
317
355
  app.className = "localAuthStage localAuthWelcomeStage localAuthOtpStage";
318
356
  app.innerHTML =
319
- setupJourney(2) +
357
+ setupJourney(3) +
320
358
  '<section class="localAuthWelcome localAuthOtpWelcome" aria-labelledby="localAuthOtpTitle">' +
321
359
  '<div class="localAuthOrbScene" aria-hidden="true"><span class="localAuthOrb"></span><span class="localAuthOrbShadow"></span></div>' +
322
360
  '<div class="localAuthWelcomeCopy">' +
@@ -405,12 +443,14 @@ export const SETUP_PAGE_CLIENT_CORE = String.raw ` var params = new URLSear
405
443
  function renderLocalPassphrase(mode, email, message) {
406
444
  localAuthEmail = email || localAuthEmail;
407
445
  var setupMode = mode === "setup";
446
+ setCityMode(false);
447
+ setScanMode(false);
408
448
  setExtractMode(false);
409
449
  setReadyMode(true);
410
450
  setHead(setupMode ? "Create encrypted vault" : "Unlock encrypted vault", "Local passphrase");
411
451
  app.className = "localAuthStage localAuthWelcomeStage localAuthPassphraseStage";
412
452
  app.innerHTML =
413
- setupJourney(2) +
453
+ setupJourney(3) +
414
454
  '<section class="localAuthWelcome localAuthPassphraseWelcome">' +
415
455
  '<div class="localAuthOrbScene" aria-hidden="true"><span class="localAuthOrb"></span><span class="localAuthOrbShadow"></span></div>' +
416
456
  '<div class="localAuthWelcomeCopy">' +
@@ -471,8 +511,8 @@ export const SETUP_PAGE_CLIENT_CORE = String.raw ` var params = new URLSear
471
511
  }
472
512
  function onConnectClick() {
473
513
  if (!localHistoryConsentGranted) {
474
- renderLocalHistoryConsent();
475
- setConsentStatus("Allow local history access to continue.", "required");
514
+ renderLocalScanConsent();
515
+ setConsentStatus("Allow the local scan to continue.", "required");
476
516
  return;
477
517
  }
478
518
  if (connected) { void waitForStats(); return; }
@@ -484,4 +524,318 @@ export const SETUP_PAGE_CLIENT_CORE = String.raw ` var params = new URLSear
484
524
  signin.onclick = onConnectClick;
485
525
  });
486
526
  }
527
+ // Retained from the archived pencil-pie prototype: 3D pencil-sketch pies drawn on a
528
+ // transparent canvas so the graphite sits directly on the page background.
529
+ function pencilMulberry(seed) {
530
+ return function () {
531
+ var t = seed += 0x6D2B79F5;
532
+ t = Math.imul(t ^ t >>> 15, t | 1);
533
+ t ^= t + Math.imul(t ^ t >>> 7, t | 61);
534
+ return ((t ^ t >>> 14) >>> 0) / 4294967296;
535
+ };
536
+ }
537
+ function drawPencilPie(cv, slices, R) {
538
+ var TILT = 0.48, u = R / 270;
539
+ var TH = 58 * u, SHLEN = 154 * u, SHANG = 14 * Math.PI / 180;
540
+ var density = Math.min(1.1, Math.max(0.2, u * u * 2.6));
541
+ cv.width = Math.round(R * 2.62);
542
+ cv.height = Math.round(R * 1.9);
543
+ var cx = R * 1.14, cy = R * 0.64;
544
+ var g = cv.getContext("2d");
545
+ var random = pencilMulberry(2117 + Math.round(R * 7) + slices.length * 131);
546
+ function ePoint(radius, angle, yOff) { return { x: cx + Math.cos(angle) * radius, y: cy + Math.sin(angle) * radius * TILT + (yOff || 0) }; }
547
+ function ePath(radius) { g.beginPath(); g.ellipse(cx, cy, radius, radius * TILT, 0, 0, Math.PI * 2); }
548
+ function wPath(start, sweep) {
549
+ var steps = Math.max(18, Math.ceil(Math.abs(sweep) / (Math.PI / 90)));
550
+ g.beginPath();
551
+ g.moveTo(cx, cy);
552
+ for (var i = 0; i <= steps; i++) { var p = ePoint(R, start + sweep * (i / steps), 0); g.lineTo(p.x, p.y); }
553
+ g.closePath();
554
+ }
555
+ function pStroke(pathFn, color, width, repeats) {
556
+ var rnd = pencilMulberry(808 + Math.round(width * 100) + repeats * 47);
557
+ for (var i = 0; i < repeats; i++) {
558
+ g.save();
559
+ g.translate((rnd() - 0.5) * 1.25, (rnd() - 0.5) * 0.8);
560
+ pathFn();
561
+ g.strokeStyle = color;
562
+ g.globalAlpha = 0.58 / Math.max(1, repeats * 0.64);
563
+ g.lineWidth = width + rnd() * 0.45;
564
+ g.stroke();
565
+ g.restore();
566
+ }
567
+ }
568
+ function hatchClipped(clipFn, angle, spacing, color, alpha) {
569
+ g.save();
570
+ clipFn();
571
+ g.clip();
572
+ g.translate(cx, cy);
573
+ g.rotate(angle);
574
+ g.translate(-cx, -cy);
575
+ g.strokeStyle = color;
576
+ g.globalAlpha = alpha;
577
+ g.lineWidth = 0.58;
578
+ g.lineCap = "round";
579
+ g.beginPath();
580
+ for (var x = cx - R * 1.7; x < cx + R * 1.7; x += spacing) {
581
+ var off = (random() - 0.5) * 2;
582
+ g.moveTo(x + off, cy - R);
583
+ g.lineTo(x + off + (random() - 0.5) * 2, cy + R);
584
+ }
585
+ g.stroke();
586
+ g.restore();
587
+ }
588
+ // Graphite shadow rubbed into the page beneath the disc.
589
+ var dx = Math.cos(SHANG) * SHLEN, dy = Math.sin(SHANG) * SHLEN;
590
+ var shX = cx + dx * 0.46, shY = cy + TH + 12 * u + dy * 0.42;
591
+ g.save();
592
+ g.filter = "blur(" + Math.max(2, 6 * u * 1.6).toFixed(1) + "px)";
593
+ g.fillStyle = "rgba(31,33,28,.05)";
594
+ g.beginPath();
595
+ g.ellipse(shX, shY, R * 1.08, R * TILT * 0.82, SHANG * 0.12, 0, Math.PI * 2);
596
+ g.fill();
597
+ g.restore();
598
+ var L = Math.max(0.8, u * 1.9);
599
+ var dust = Math.round(1380 * density), pass, i;
600
+ var dustTones = ["rgba(35,37,32,.028)", "rgba(35,37,32,.055)", "rgba(35,37,32,.09)"];
601
+ for (pass = 0; pass < 3; pass++) {
602
+ g.beginPath();
603
+ for (i = pass; i < dust; i += 3) {
604
+ var ring = Math.pow(random(), 0.58 + pass * 0.16), ang = random() * Math.PI * 2, trail = Math.pow(random(), 2.1);
605
+ g.rect(shX + Math.cos(ang) * R * 1.06 * ring + dx * trail * 0.48, shY + Math.sin(ang) * R * TILT * 0.76 * ring + dy * trail * 0.32, 0.35 + random() * (1.2 + pass * 0.28), (0.35 + random()) * (0.55 + random() * 0.8));
606
+ }
607
+ g.fillStyle = dustTones[pass];
608
+ g.fill();
609
+ }
610
+ var strokes = Math.round(1040 * density);
611
+ g.save();
612
+ g.lineCap = "round";
613
+ g.lineWidth = 0.72;
614
+ for (pass = 0; pass < 2; pass++) {
615
+ g.beginPath();
616
+ var cross = pass === 0 ? -0.38 : 0.46;
617
+ for (i = pass; i < strokes; i += 2) {
618
+ var ring2 = Math.pow(random(), 0.62), ang2 = random() * Math.PI * 2, trail2 = Math.pow(random(), 2.05);
619
+ var x0 = shX + Math.cos(ang2) * R * 1.04 * ring2 + dx * trail2 * 0.5;
620
+ var y0 = shY + Math.sin(ang2) * R * TILT * 0.75 * ring2 + dy * trail2 * 0.34;
621
+ var len = (3 + random() * (9 + (1 - ring2) * 14)) * L;
622
+ var sa = SHANG + cross + (random() - 0.5) * 0.15;
623
+ g.moveTo(x0, y0);
624
+ g.lineTo(x0 + Math.cos(sa) * len, y0 + Math.sin(sa) * len * 0.62);
625
+ }
626
+ g.strokeStyle = pass === 0 ? "rgba(32,34,29,.14)" : "rgba(32,34,29,.11)";
627
+ g.stroke();
628
+ }
629
+ g.restore();
630
+ // Contact crevice hugging the lower ellipse.
631
+ g.save();
632
+ g.lineCap = "round";
633
+ g.lineWidth = 0.9;
634
+ var contact = Math.round(300 * density);
635
+ for (pass = 0; pass < 3; pass++) {
636
+ g.beginPath();
637
+ for (i = pass; i < contact; i += 3) {
638
+ var cAng = random() * Math.PI;
639
+ var dark = 0.5 + Math.cos(cAng - SHANG) * 0.5;
640
+ var depth = Math.pow(random(), 2.6) * (5 + dark * 8) * L + pass * 1.2;
641
+ var cp = ePoint(R * (0.97 + random() * 0.045), cAng, TH + 1 + depth);
642
+ var tx = -Math.sin(cAng), ty = Math.cos(cAng) * TILT;
643
+ var norm = Math.hypot(tx, ty) || 1;
644
+ var cLen = (3 + random() * (8 + dark * 11)) * L;
645
+ g.moveTo(cp.x - tx / norm * cLen * 0.5, cp.y - ty / norm * cLen * 0.5);
646
+ g.lineTo(cp.x + tx / norm * cLen * 0.5, cp.y + ty / norm * cLen * 0.5);
647
+ }
648
+ g.strokeStyle = ["rgba(27,29,25,.36)", "rgba(27,29,25,.25)", "rgba(27,29,25,.16)"][pass];
649
+ g.stroke();
650
+ }
651
+ g.restore();
652
+ // Cylinder wall.
653
+ function sidePath() {
654
+ var steps = 120, k;
655
+ g.beginPath();
656
+ for (k = 0; k <= steps; k++) { var sp = ePoint(R, Math.PI * k / steps, 0); if (k === 0) g.moveTo(sp.x, sp.y); else g.lineTo(sp.x, sp.y); }
657
+ for (k = steps; k >= 0; k--) { var sp2 = ePoint(R, Math.PI * k / steps, TH); g.lineTo(sp2.x, sp2.y); }
658
+ g.closePath();
659
+ }
660
+ sidePath();
661
+ var sideTone = g.createLinearGradient(cx - Math.cos(SHANG) * R, cy - Math.sin(SHANG) * R * TILT, cx + Math.cos(SHANG) * R, cy + Math.sin(SHANG) * R * TILT);
662
+ sideTone.addColorStop(0, "#e6e1d6");
663
+ sideTone.addColorStop(0.47, "#d8d3c7");
664
+ sideTone.addColorStop(1, "#aaa79f");
665
+ g.fillStyle = sideTone;
666
+ g.fill();
667
+ g.strokeStyle = "rgba(39,41,35,.7)";
668
+ g.lineWidth = 1.5;
669
+ g.stroke();
670
+ var hatchCount = Math.round(138 * Math.max(density, 0.45));
671
+ g.save();
672
+ g.lineCap = "round";
673
+ g.lineWidth = 0.66;
674
+ for (pass = 0; pass < 3; pass++) {
675
+ g.beginPath();
676
+ for (i = 1; i < hatchCount; i++) {
677
+ var hAng = Math.PI * i / hatchCount;
678
+ var hDark = 0.5 + Math.cos(hAng - SHANG) * 0.5;
679
+ if (Math.min(2, Math.floor(hDark * 3)) !== pass) continue;
680
+ var top = ePoint(R, hAng, 0);
681
+ var jit = (random() - 0.5) * 4 * L;
682
+ g.moveTo(top.x + jit * 0.25, top.y + 2 + random() * 3);
683
+ g.lineTo(top.x + jit, top.y + TH - 2 - random() * 5);
684
+ }
685
+ g.strokeStyle = ["rgba(37,39,34,.13)", "rgba(37,39,34,.21)", "rgba(37,39,34,.31)"][pass];
686
+ g.stroke();
687
+ }
688
+ g.restore();
689
+ pStroke(function () { g.beginPath(); g.ellipse(cx, cy, R, R * TILT, 0, 0, Math.PI, false); }, "#272923", 1.05, 3);
690
+ pStroke(function () { g.beginPath(); g.ellipse(cx, cy + TH, R, R * TILT, 0, 0, Math.PI, false); }, "#22241f", 1.45, 4);
691
+ // Disc top.
692
+ ePath(R);
693
+ g.fillStyle = "#f1ede2";
694
+ g.fill();
695
+ hatchClipped(function () { ePath(R); }, -Math.PI / 4, Math.max(3, 8 * u * 2) / 1, "#4c4d47", 0.15);
696
+ pStroke(function () { ePath(R); }, "#22241f", 1.35, 3);
697
+ // Colored wedges: same clock as the CSS conic version (0% = 12 o'clock).
698
+ slices.forEach(function (slice) {
699
+ var pctSpan = Math.max(0, Number(slice.end || 0) - Number(slice.start || 0));
700
+ if (pctSpan < 0.5) return;
701
+ var start = (-90 + Number(slice.start || 0) * 3.6) * Math.PI / 180;
702
+ var sweep = pctSpan * 3.6 * Math.PI / 180;
703
+ wPath(start, sweep);
704
+ g.fillStyle = slice.color || "#778db8";
705
+ g.fill();
706
+ hatchClipped(function () { wPath(start, sweep); }, Math.PI / 5, Math.max(2.4, 5.5 * u * 2), "#26354d", 0.24);
707
+ pStroke(function () { wPath(start, sweep); }, "#20231f", 1.25, 3);
708
+ g.save();
709
+ wPath(start, sweep);
710
+ g.strokeStyle = "rgba(30,32,28,.92)";
711
+ g.lineWidth = 1.45;
712
+ g.stroke();
713
+ g.restore();
714
+ });
715
+ pStroke(function () { ePath(R); }, "#22241f", 1.15, 2);
716
+ }
717
+ function renderPencilPies() {
718
+ Array.prototype.forEach.call(document.querySelectorAll("canvas[data-pencil-pie]"), function (cv) {
719
+ if (cv.getAttribute("data-pie-drawn")) return;
720
+ cv.setAttribute("data-pie-drawn", "1");
721
+ var slices = [];
722
+ try { slices = JSON.parse(cv.getAttribute("data-slices") || "[]"); } catch (_) { slices = []; }
723
+ drawPencilPie(cv, slices, Number(cv.getAttribute("data-radius") || 100));
724
+ });
725
+ }
726
+ function bindSessionLinks() {
727
+ Array.prototype.forEach.call(document.querySelectorAll("[data-open-session]"), function (button) {
728
+ button.onclick = async function () {
729
+ var scope = button.closest(".audit-example-body");
730
+ var status = scope && scope.querySelector(".audit-session-status");
731
+ if (status) status.textContent = "Opening original session…";
732
+ try {
733
+ var result = await postJson("/open-session", {
734
+ source: button.getAttribute("data-session-source"),
735
+ sessionId: button.getAttribute("data-session-id")
736
+ }, 8000);
737
+ if (status) status.textContent = result && result.message ? result.message : "Original session opened.";
738
+ } catch (error) {
739
+ if (status) status.textContent = error && error.message ? error.message : "Could not open the original session.";
740
+ }
741
+ };
742
+ });
743
+ }
744
+ function bindDetailsArrow() {
745
+ var arrow = document.getElementById("details-arrow");
746
+ var target = document.getElementById("setup-details");
747
+ if (arrow && target) {
748
+ arrow.onclick = function () { target.scrollIntoView({ behavior: "smooth", block: "start" }); };
749
+ }
750
+ }
751
+ function bindCityFieldnote() {
752
+ window.addEventListener("message", function (event) {
753
+ if (event.origin !== location.origin) return;
754
+ var data = event.data || {};
755
+ if (data.type !== "echomem:city-fieldnote") return;
756
+ var note = document.getElementById("city-fieldnote");
757
+ if (!note) return;
758
+ var sig = JSON.stringify([data.name, data.rank, data.lede]);
759
+ if (note._fnSig === sig) return;
760
+ note._fnSig = sig;
761
+ var save = fnSavings();
762
+ var saveLine = save
763
+ ? '<p class="fn-save">I can help you get the same things done with ' +
764
+ '<b class="fn-num" data-kind="days" data-target="' + save.days + '">0 days</b> and ' +
765
+ '<b class="fn-num" data-kind="tokens" data-target="' + save.tokens + '">0 tokens</b> <b class="fn-less">less</b>.</p>'
766
+ : '';
767
+ note.innerHTML =
768
+ '<div class="fn-speak">' +
769
+ (data.avatar ? '<img src="' + esc(data.avatar) + '" alt="" />' : '') +
770
+ '<div class="fn-bubble"><h2>hey, <b>' + esc(data.name || "") + '</b><span class="fn-dot">.</span></h2>' + saveLine + '</div>' +
771
+ '</div>' +
772
+ (data.rank ? '<p class="fn-rank">' + data.rank + (data.guide ? '<button class="fn-info" type="button" aria-label="What do these types mean?">i</button>' : '') + '</p>' : '') +
773
+ (data.lede ? '<p class="fn-lede">' + data.lede + '</p>' : '');
774
+ var infoBtn = note.querySelector(".fn-info");
775
+ if (infoBtn) infoBtn.onclick = function () { openPersonaGuide(data.guide); };
776
+ fnAnimateNums(note);
777
+ });
778
+ }
779
+ // The taxonomy quote: every persona kind Echo can assign, with the user's own
780
+ // rank/rhythm/personality highlighted. Data rides in from the city iframe.
781
+ function openPersonaGuide(guide) {
782
+ var dlg = document.getElementById("echo-note");
783
+ if (!dlg || !guide) return;
784
+ dlg.classList.remove("example");
785
+ var cur = guide.current || {};
786
+ function rows(list, curLabel) {
787
+ return (list || []).map(function (t) {
788
+ var you = t.label === curLabel;
789
+ return '<div class="fn-guide-row' + (you ? ' is-you' : '') + '"><b>' + esc(t.label) + '</b>' + (you ? '<i>you</i>' : '') + '<span> &mdash; ' + esc(t.blurb) + '</span></div>';
790
+ }).join("");
791
+ }
792
+ document.getElementById("echo-note-k").textContent = "Persona guide";
793
+ document.getElementById("echo-note-t").textContent = "How Echo reads you";
794
+ document.getElementById("echo-note-b").innerHTML =
795
+ '<p class="fn-guide-sec">Personality &middot; ' + (guide.traits || []).length + ' kinds</p>' + rows(guide.traits, cur.primary) +
796
+ '<p class="fn-guide-sec">Rank &middot; ' + (guide.ranks || []).length + ' levels</p>' + rows(guide.ranks, cur.rank) +
797
+ '<p class="fn-guide-sec">Rhythm &middot; ' + (guide.rhythms || []).length + ' kinds</p>' + rows(guide.rhythms, cur.rhythm);
798
+ var cl = dlg.querySelector(".nclose");
799
+ if (cl) cl.onclick = function () { dlg.close(); };
800
+ dlg.onclick = function (e) { if (e.target === dlg) dlg.close(); };
801
+ if (dlg.showModal) dlg.showModal(); else dlg.setAttribute("open", "");
802
+ }
803
+ // Savings projection. BOTH figures apply the canonical terminal-window noise share to the
804
+ // FULL ledger, so they share one basis: days = waste share of total attention hours; tokens =
805
+ // projected waste share of total provider-billed input. The absolute token figure is a
806
+ // calibrated projection because intermediate requests are billed but not independently scored.
807
+ function fnSavings() {
808
+ var r = report || {};
809
+ var gs = gsClean(r.canonicalGoldenStandard);
810
+ if (!gs) return null;
811
+ var pct = gsDisplayWastePct(gs);
812
+ var projection = gsBillingProjection(r, gs);
813
+ var tokens = Number(projection.projectedWasteTokens || 0);
814
+ var wh = r.userWorkingHours || {};
815
+ var hours = Number(wh.userAttentionHours || 0) * pct / 100;
816
+ var perDay = Number(wh.averageDailyAttentionHours || 0);
817
+ var days = Math.round(perDay > 0 ? hours / perDay : hours / 8);
818
+ if (!tokens && !days) return null;
819
+ return { tokens: tokens, days: days };
820
+ }
821
+ function fnAnimateNums(root) {
822
+ var els = root.querySelectorAll(".fn-num");
823
+ if (!els.length) return;
824
+ var DURATION = 1500, t0 = null;
825
+ function label(kind, value) {
826
+ if (kind === "days") return number(value) + " days";
827
+ return big(value) + " tokens";
828
+ }
829
+ function step(ts) {
830
+ if (t0 === null) t0 = ts;
831
+ var p = Math.min(1, (ts - t0) / DURATION);
832
+ var eased = 1 - Math.pow(1 - p, 3);
833
+ Array.prototype.forEach.call(els, function (el) {
834
+ var target = Number(el.getAttribute("data-target") || 0);
835
+ el.textContent = label(el.getAttribute("data-kind"), Math.round(target * eased));
836
+ });
837
+ if (p < 1) requestAnimationFrame(step);
838
+ }
839
+ requestAnimationFrame(step);
840
+ }
487
841
  `;