@echomem/mcp 1.4.7 → 1.4.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -35,19 +35,25 @@ legacy EchoMem-synthesized recall answer.
35
35
  ## Quick start (recommended)
36
36
 
37
37
  ```bash
38
- # One command: install the CLI, detect your editor, write MCP config, log in via the browser.
39
- npm i -g @echomem/mcp@latest && echomem-mcp setup
38
+ # One command, everything: download the bridge + HUD, configure every coding agent you have
39
+ # installed (Codex, Claude Code, Claude Desktop, Cursor, Windsurf…), log in, and launch the HUD.
40
+ npm i -g @echomem/mcp@latest && echomem-mcp init
40
41
  ```
41
42
 
42
- This keeps `echomem-mcp` available for later commands like `status`, `update`, `unlock`, and
43
- playground setup. `setup` detects the client (Cursor / Windsurf / Claude Desktop), writes its MCP
44
- config (with **no secret** in it — credentials live in `~/.echomem/credentials.json`, mode 0600),
45
- then opens the browser to approve the device and, for encrypted accounts, unlock the vault. Reload
46
- your editor and you're done.
43
+ A global install (not `npx`) is recommended because the context HUD's launch-at-login runs from the
44
+ installed path. `init` is the flagship one-liner it wraps `setup --all --with-hud`: it writes each
45
+ installed agent's MCP config (with **no secret** in it — credentials live in
46
+ `~/.echomem/credentials.json`, mode 0600), adds the EchoMem memory guidance to their global
47
+ `AGENTS.md` / `CLAUDE.md`, opens the browser to approve the device (and unlock the vault for
48
+ encrypted accounts), then launches the context HUD. Reload your editors and you're done.
49
+
50
+ Prefer to keep it minimal? `echomem-mcp setup` configures only the auto-detected editor and skips the
51
+ HUD; the granular commands below still work.
47
52
 
48
53
  | Command | What it does |
49
54
  |---|---|
50
- | `npm i -g @echomem/mcp@latest && echomem-mcp setup` | Install the CLI globally and run setup in one explicit step |
55
+ | `npm i -g @echomem/mcp@latest && echomem-mcp init` | **Everything in one command** all installed agents + HUD + login |
56
+ | `npm i -g @echomem/mcp@latest && echomem-mcp setup` | Install the CLI globally and configure just the detected editor |
51
57
  | `npx -y @echomem/mcp@latest setup` | One-off setup without keeping a global CLI command |
52
58
  | `echomem-mcp setup [--client cursor\|windsurf\|claude-desktop\|claude-code\|codex]` | Write client config + log in |
53
59
  | `echomem-mcp setup --skip-login [--client cursor\|windsurf\|claude-desktop\|claude-code\|codex]` | Write client config without opening the browser or changing credentials |
@@ -0,0 +1 @@
1
+ <svg height="1em" style="flex:none;line-height:1" viewBox="0 0 24 24" width="1em" xmlns="http://www.w3.org/2000/svg"><title>Claude</title><path d="M4.709 15.955l4.72-2.647.08-.23-.08-.128H9.2l-.79-.048-2.698-.073-2.339-.097-2.266-.122-.571-.121L0 11.784l.055-.352.48-.321.686.06 1.52.103 2.278.158 1.652.097 2.449.255h.389l.055-.157-.134-.098-.103-.097-2.358-1.596-2.552-1.688-1.336-.972-.724-.491-.364-.462-.158-1.008.656-.722.881.06.225.061.893.686 1.908 1.476 2.491 1.833.365.304.145-.103.019-.073-.164-.274-1.355-2.446-1.446-2.49-.644-1.032-.17-.619a2.97 2.97 0 01-.104-.729L6.283.134 6.696 0l.996.134.42.364.62 1.414 1.002 2.229 1.555 3.03.456.898.243.832.091.255h.158V9.01l.128-1.706.237-2.095.23-2.695.08-.76.376-.91.747-.492.584.28.48.685-.067.444-.286 1.851-.559 2.903-.364 1.942h.212l.243-.242.985-1.306 1.652-2.064.73-.82.85-.904.547-.431h1.033l.76 1.129-.34 1.166-1.064 1.347-.881 1.142-1.264 1.7-.79 1.36.073.11.188-.02 2.856-.606 1.543-.28 1.841-.315.833.388.091.395-.328.807-1.969.486-2.309.462-3.439.813-.042.03.049.061 1.549.146.662.036h1.622l3.02.225.79.522.474.638-.079.485-1.215.62-1.64-.389-3.829-.91-1.312-.329h-.182v.11l1.093 1.068 2.006 1.81 2.509 2.33.127.578-.322.455-.34-.049-2.205-1.657-.851-.747-1.926-1.62h-.128v.17l.444.649 2.345 3.521.122 1.08-.17.353-.608.213-.668-.122-1.374-1.925-1.415-2.167-1.143-1.943-.14.08-.674 7.254-.316.37-.729.28-.607-.461-.322-.747.322-1.476.389-1.924.315-1.53.286-1.9.17-.632-.012-.042-.14.018-1.434 1.967-2.18 2.945-1.726 1.845-.414.164-.717-.37.067-.662.401-.589 2.388-3.036 1.44-1.882.93-1.086-.006-.158h-.055L4.132 18.56l-1.13.146-.487-.456.061-.746.231-.243 1.908-1.312-.006.006z" fill="#D97757" fill-rule="nonzero"></path></svg>
@@ -0,0 +1 @@
1
+ <svg height="1em" style="flex:none;line-height:1" viewBox="0 0 24 24" width="1em" xmlns="http://www.w3.org/2000/svg"><title>Codex</title><path d="M19.503 0H4.496A4.496 4.496 0 000 4.496v15.007A4.496 4.496 0 004.496 24h15.007A4.496 4.496 0 0024 19.503V4.496A4.496 4.496 0 0019.503 0z" fill="#fff"></path><path d="M9.064 3.344a4.578 4.578 0 012.285-.312c1 .115 1.891.54 2.673 1.275.01.01.024.017.037.021a.09.09 0 00.043 0 4.55 4.55 0 013.046.275l.047.022.116.057a4.581 4.581 0 012.188 2.399c.209.51.313 1.041.315 1.595a4.24 4.24 0 01-.134 1.223.123.123 0 00.03.115c.594.607.988 1.33 1.183 2.17.289 1.425-.007 2.71-.887 3.854l-.136.166a4.548 4.548 0 01-2.201 1.388.123.123 0 00-.081.076c-.191.551-.383 1.023-.74 1.494-.9 1.187-2.222 1.846-3.711 1.838-1.187-.006-2.239-.44-3.157-1.302a.107.107 0 00-.105-.024c-.388.125-.78.143-1.204.138a4.441 4.441 0 01-1.945-.466 4.544 4.544 0 01-1.61-1.335c-.152-.202-.303-.392-.414-.617a5.81 5.81 0 01-.37-.961 4.582 4.582 0 01-.014-2.298.124.124 0 00.006-.056.085.085 0 00-.027-.048 4.467 4.467 0 01-1.034-1.651 3.896 3.896 0 01-.251-1.192 5.189 5.189 0 01.141-1.6c.337-1.112.982-1.985 1.933-2.618.212-.141.413-.251.601-.33.215-.089.43-.164.646-.227a.098.098 0 00.065-.066 4.51 4.51 0 01.829-1.615 4.535 4.535 0 011.837-1.388zm3.482 10.565a.637.637 0 000 1.272h3.636a.637.637 0 100-1.272h-3.636zM8.462 9.23a.637.637 0 00-1.106.631l1.272 2.224-1.266 2.136a.636.636 0 101.095.649l1.454-2.455a.636.636 0 00.005-.64L8.462 9.23z" fill="url(#lobe-icons-codex-gradient)"></path><defs><linearGradient gradientUnits="userSpaceOnUse" id="lobe-icons-codex-gradient" x1="12" x2="12" y1="3" y2="21"><stop stop-color="#B1A7FF"></stop><stop offset=".5" stop-color="#7A9DFF"></stop><stop offset="1" stop-color="#3941FF"></stop></linearGradient></defs></svg>
@@ -1668,6 +1668,40 @@
1668
1668
  document.body.prepend(bar);
1669
1669
  }
1670
1670
 
1671
+ // Session switcher: one tab per recent Codex / Claude Code / Claude Desktop session (from the HUD).
1672
+ async function injectSessionTabs(currentPath, mode) {
1673
+ if (document.getElementById("echoSessionBar")) return;
1674
+ let sessions = [];
1675
+ try {
1676
+ const state = await (await fetch("/state")).json();
1677
+ sessions = (state && state.recentSessions) || [];
1678
+ } catch { return; }
1679
+ if (!sessions.length) return;
1680
+ const clientName = (c) => c === "codex" ? "Codex" : c === "claude-code" ? "Claude Code" : c === "claude-desktop" ? "Claude Desktop" : c;
1681
+ const bar = document.createElement("div");
1682
+ bar.id = "echoSessionBar";
1683
+ bar.style.cssText = "position:sticky;top:0;z-index:61;display:flex;gap:6px;align-items:center;padding:8px 14px;background:#010409;border-bottom:1px solid #30363d;overflow-x:auto;font:12px system-ui,-apple-system,sans-serif";
1684
+ const tag = document.createElement("span");
1685
+ tag.textContent = "session:"; tag.style.cssText = "color:#8b949e;margin-right:2px;flex:0 0 auto";
1686
+ bar.append(tag);
1687
+ sessions.forEach((sess) => {
1688
+ const active = sess.sourcePath === currentPath;
1689
+ const b = document.createElement("button");
1690
+ const title = (sess.title || "").length > 24 ? (sess.title || "").slice(0, 23) + "…" : (sess.title || "");
1691
+ b.textContent = (sess.live ? "● " : "") + clientName(sess.client) + " · " + title;
1692
+ b.title = clientName(sess.client) + " · " + (sess.title || "");
1693
+ b.style.cssText = "flex:0 0 auto;padding:6px 12px;border-radius:999px;cursor:pointer;color:#c9d1d9;white-space:nowrap;border:1px solid " + (active ? "#1f6feb" : "#30363d") + ";background:" + (active ? "#132f5e" : "#161b22");
1694
+ b.onclick = () => {
1695
+ const u = new URL(window.location.href);
1696
+ u.searchParams.set("path", sess.sourcePath);
1697
+ u.searchParams.set("mode", mode);
1698
+ window.location.href = u.toString();
1699
+ };
1700
+ bar.append(b);
1701
+ });
1702
+ document.body.prepend(bar); // above the mode bar → session on top, then event-log/payload
1703
+ }
1704
+
1671
1705
  async function initFromQuery() {
1672
1706
  const params = new URLSearchParams(window.location.search);
1673
1707
  const sessionPath = params.get("path");
@@ -1675,6 +1709,7 @@
1675
1709
  let src = params.get("src");
1676
1710
  if (sessionPath) {
1677
1711
  injectModeToggle(sessionPath, mode);
1712
+ await injectSessionTabs(sessionPath, mode);
1678
1713
  src = (mode === "payload" ? "/session-payload" : "/session-raw") + "?path=" + encodeURIComponent(sessionPath);
1679
1714
  }
1680
1715
  if (!src) return;
@@ -38,7 +38,9 @@
38
38
  .v7-head{right:56px;top:48px;width:410px;text-align:right;}
39
39
  .v7-eyebrow{font-family:var(--mono);font-size:12px;font-weight:800;letter-spacing:.16em;text-transform:uppercase;color:#9aa0aa;}
40
40
  .v7-title{font-size:34px;font-weight:800;letter-spacing:-.02em;line-height:1.05;margin-top:8px;}
41
- .v7-personaName{font-size:64px;font-weight:800;letter-spacing:-.035em;line-height:.9;margin-top:8px;text-transform:lowercase;}
41
+ /* Persona is identity flavor (a badge), not the page headline — one display-size element per
42
+ screen, and that's "Here's what you built." */
43
+ .v7-personaName{font-size:46px;font-weight:800;letter-spacing:-.03em;line-height:.92;margin-top:8px;text-transform:lowercase;}
42
44
  .v7-rank{margin-top:12px;font-family:var(--mono);font-size:12px;font-weight:800;letter-spacing:.16em;text-transform:uppercase;color:#8d929c;}
43
45
  .v7-combo{display:flex;gap:8px;flex-wrap:wrap;margin-top:16px;}
44
46
  .v7-combo .ct{font-family:var(--mono);font-size:11px;letter-spacing:.02em;color:#3a3c43;border:1px solid var(--line);
@@ -59,6 +61,9 @@
59
61
  .v7-stat .v{font-size:23px;font-weight:800;letter-spacing:-.01em;line-height:1;}
60
62
  .v7-stat .k{font-family:var(--mono);font-size:11px;font-weight:700;letter-spacing:.08em;text-transform:uppercase;color:#9296a0;margin-top:4px;}
61
63
  .v7-band{left:90px;right:90px;bottom:250px;text-align:center;}
64
+ /* Echo delivers the verdict: the mascot makes the band a character speaking, not a floating stat. */
65
+ .v7-echo{width:56px;height:56px;object-fit:contain;margin:10px auto 2px;display:block;
66
+ filter:drop-shadow(0 12px 20px rgba(26,58,143,.22));}
62
67
  .v7-band .eyebrow{font-family:var(--mono);font-size:12px;font-weight:800;letter-spacing:.2em;text-transform:uppercase;color:#a4a7af;}
63
68
  .v7-band .bignum{font-size:64px;font-weight:800;letter-spacing:-.03em;line-height:.9;margin-top:10px;color:var(--red);
64
69
  min-height:64px;transition:opacity .52s ease,transform .52s ease;}
@@ -227,7 +232,7 @@
227
232
  body.setup-mode .counterfactual,
228
233
  body.setup-mode .setup-connect{display:none;}
229
234
  body.setup-mode .v7-persona{left:clamp(48px,5vw,86px);top:clamp(42px,5vh,72px);width:min(360px,32vw);}
230
- body.setup-mode .v7-personaName{font-size:clamp(54px,5.3vw,86px);}
235
+ body.setup-mode .v7-personaName{font-size:clamp(38px,3.4vw,54px);}
231
236
  body.setup-mode .v7-head{right:clamp(48px,5vw,86px);top:clamp(42px,5vh,72px);width:min(520px,38vw);}
232
237
  body.setup-mode .v7-head .v7-eyebrow{font-size:clamp(10px,.72vw,12px);}
233
238
  body.setup-mode .v7-title{font-size:clamp(28px,2.1vw,40px);}
@@ -235,7 +240,10 @@
235
240
  body.setup-mode .v7-stat .v{font-size:clamp(20px,1.45vw,28px);}
236
241
  body.setup-mode .v7-stat .k{font-size:clamp(9px,.72vw,11px);}
237
242
  body.setup-mode .v7-band{left:clamp(48px,5vw,86px);right:auto;bottom:clamp(82px,8vh,130px);
238
- width:min(570px,38vw);text-align:left;}
243
+ width:min(640px,42vw);text-align:left;display:grid;grid-template-columns:72px minmax(0,1fr);
244
+ column-gap:16px;align-items:start;}
245
+ body.setup-mode .v7-echo{width:72px;height:72px;margin:2px 0 0;}
246
+ body.setup-mode .v7-bandCopy{min-width:0;}
239
247
  body.setup-mode .v7-band .eyebrow{display:none;}
240
248
  body.setup-mode .v7-band .bignum{display:flex;align-items:baseline;gap:10px;flex-wrap:wrap;
241
249
  margin-top:0;min-height:0;line-height:.96;}
@@ -250,7 +258,7 @@
250
258
  body.setup-mode .v7-ctaWrap .micro{max-width:430px;margin-left:auto;}
251
259
  body.setup-mode .v7-foot{left:clamp(48px,5vw,86px);right:clamp(48px,5vw,86px);}
252
260
  @media (max-height:820px){
253
- body.setup-mode .v7-personaName{font-size:clamp(44px,6vh,62px);}
261
+ body.setup-mode .v7-personaName{font-size:clamp(32px,4.4vh,46px);}
254
262
  body.setup-mode .v7-title{font-size:clamp(30px,5vh,44px);}
255
263
  body.setup-mode .v7-band{bottom:58px;width:min(500px,40vw);}
256
264
  body.setup-mode .v7-ctaWrap{bottom:58px;width:min(390px,32vw);}
@@ -323,8 +331,11 @@
323
331
  </div>
324
332
  <div class="v7-z v7-band">
325
333
  <div class="eyebrow">What we found</div>
334
+ <img class="v7-echo" src="echo-face-cutout.png" alt="" />
335
+ <div class="v7-bandCopy">
326
336
  <div class="bignum" id="v7-metric"><span id="v7-metric-value">85.0%</span><span id="v7-metric-label">repeated reads</span><button class="v7-infoBtn" id="v7-info-btn" type="button" aria-label="How we read your city" aria-expanded="false" aria-controls="v7-info-sheet">i</button></div>
327
337
  <div class="say" id="v7-metric-say">Your agents re-load context they've already seen — unchanged files, the same rules, stale maps — every session. That's context rot. It's <b>how coding agents work today</b>, and it's the haze on your city.</div>
338
+ </div>
328
339
  </div>
329
340
  <div class="v7-z v7-ctaWrap">
330
341
  <div class="pre">Ready when you are</div>
@@ -896,17 +907,19 @@ function addCube(o){
896
907
  }
897
908
  // repo summary as an upright billboard above each bar — white text, ALWAYS faces the camera (never skewed).
898
909
  // name = small tag just above the bar top; tokens + $ fly higher above it, clear of the cube.
899
- function addTopLabel(o){
910
+ function addTopLabel(o, numbers=true, lift=0){
900
911
  const PW=320, PH=280;
901
912
  const cv=document.createElement('canvas'); cv.width=PW; cv.height=PH;
902
913
  const g=cv.getContext('2d'); g.textAlign='center';
903
914
  g.shadowColor='rgba(0,0,0,0.34)'; g.shadowBlur=9; g.shadowOffsetY=1; // subtle contrast over light areas
904
- // tokens — the headline, flies highest
905
- g.fillStyle='#ffffff'; g.font=`800 ${Math.round(PW*0.19)}px "IBM Plex Mono", monospace`;
906
- g.fillText(fmtTok(o.tokens), PW/2, PH*0.18);
907
- // dollars — just under the tokens, also up high
908
- g.fillStyle='rgba(255,255,255,0.92)'; g.font=`700 ${Math.round(PW*0.11)}px "IBM Plex Mono", monospace`;
909
- g.fillText('$'+Math.round(o.cost*(setupMode?1:COST_SCALE)).toLocaleString('en-US'), PW/2, PH*0.40);
915
+ if(numbers){
916
+ // tokens the headline, flies highest
917
+ g.fillStyle='#ffffff'; g.font=`800 ${Math.round(PW*0.19)}px "IBM Plex Mono", monospace`;
918
+ g.fillText(fmtTok(o.tokens), PW/2, PH*0.18);
919
+ // dollars just under the tokens, also up high
920
+ g.fillStyle='rgba(255,255,255,0.92)'; g.font=`700 ${Math.round(PW*0.11)}px "IBM Plex Mono", monospace`;
921
+ g.fillText('$'+Math.round(o.cost*(setupMode?1:COST_SCALE)).toLocaleString('en-US'), PW/2, PH*0.40);
922
+ }
910
923
  // name — small tag sitting just above the bar top
911
924
  g.fillStyle='rgba(255,255,255,0.95)'; g.font=`600 ${Math.round(PW*0.075)}px "IBM Plex Mono", monospace`;
912
925
  const fw=g.measureText(o.name).width, maxw=PW*0.94;
@@ -915,7 +928,7 @@ function addTopLabel(o){
915
928
  const sprite=new THREE.Sprite(new THREE.SpriteMaterial({map:tex, transparent:true, toneMapped:false, depthWrite:false}));
916
929
  const wWorld=Math.min(Math.max(o.w*0.95, 42), 128), hWorld=wWorld*PH/PW; // scales with bar size, clamped legible
917
930
  sprite.scale.set(wWorld, hWorld, 1);
918
- sprite.position.set(o.x, TRAY_TOP + o.h - 3 + 6 + 0.36*hWorld, o.z); // name tags the top; tokens/$ fly higher
931
+ sprite.position.set(o.x, TRAY_TOP + o.h - 3 + 6 + 0.36*hWorld + lift, o.z); // name tags the top; tokens/$ fly higher; lift staggers neighbors
919
932
  scene.add(sprite);
920
933
  }
921
934
  // fine ivory grain — textures EVERY face (incl. the curved side walls), so the tray isn't flat plastic
@@ -962,7 +975,12 @@ tray.position.set(CX, TRAY_TOP/2, CZ); tray.castShadow=true; tray.receiveShadow=
962
975
  plane.rotation.x=-Math.PI/2; plane.position.set(CX, TRAY_TOP+0.6, CZ); scene.add(plane);
963
976
  })();
964
977
 
965
- all.forEach(o=>{ o.mesh=addCube(o); o.mesh.userData=o; if(o.w>=LABEL_MIN_BASE) addTopLabel(o); });
978
+ // De-crowd the skyline: only the top token movers fly their numbers; every other building keeps just
979
+ // its name tag (hover still shows the full repo detail card). Kills the mid-air label collisions.
980
+ const NUMBERED_LABELS=4;
981
+ const numberedOrder=all.slice().sort((a,b)=>(b.tokens||0)-(a.tokens||0)).slice(0,NUMBERED_LABELS);
982
+ const numberedLift=new Map(numberedOrder.map((o,i)=>[o.name, (i%2)*16])); // alternate heights so neighbors' numbers never collide
983
+ all.forEach(o=>{ o.mesh=addCube(o); o.mesh.userData=o; if(o.w>=LABEL_MIN_BASE) addTopLabel(o, numberedLift.has(o.name), numberedLift.get(o.name)||0); });
966
984
 
967
985
  // bottom-right standing "signpost": tray summary — total tokens + how much is input
968
986
  let signpostCanvas=null, signpostTexture=null;
@@ -38,7 +38,9 @@
38
38
  .v7-head{right:56px;top:48px;width:410px;text-align:right;}
39
39
  .v7-eyebrow{font-family:var(--mono);font-size:12px;font-weight:800;letter-spacing:.16em;text-transform:uppercase;color:#9aa0aa;}
40
40
  .v7-title{font-size:34px;font-weight:800;letter-spacing:-.02em;line-height:1.05;margin-top:8px;}
41
- .v7-personaName{font-size:64px;font-weight:800;letter-spacing:-.035em;line-height:.9;margin-top:8px;text-transform:lowercase;}
41
+ /* Persona is identity flavor (a badge), not the page headline — one display-size element per
42
+ screen, and that's "Here's what you built." */
43
+ .v7-personaName{font-size:46px;font-weight:800;letter-spacing:-.03em;line-height:.92;margin-top:8px;text-transform:lowercase;}
42
44
  .v7-rank{margin-top:12px;font-family:var(--mono);font-size:12px;font-weight:800;letter-spacing:.16em;text-transform:uppercase;color:#8d929c;}
43
45
  .v7-combo{display:flex;gap:8px;flex-wrap:wrap;margin-top:16px;}
44
46
  .v7-combo .ct{font-family:var(--mono);font-size:11px;letter-spacing:.02em;color:#3a3c43;border:1px solid var(--line);
@@ -59,6 +61,9 @@
59
61
  .v7-stat .v{font-size:23px;font-weight:800;letter-spacing:-.01em;line-height:1;}
60
62
  .v7-stat .k{font-family:var(--mono);font-size:11px;font-weight:700;letter-spacing:.08em;text-transform:uppercase;color:#9296a0;margin-top:4px;}
61
63
  .v7-band{left:90px;right:90px;bottom:250px;text-align:center;}
64
+ /* Echo delivers the verdict: the mascot makes the band a character speaking, not a floating stat. */
65
+ .v7-echo{width:56px;height:56px;object-fit:contain;margin:10px auto 2px;display:block;
66
+ filter:drop-shadow(0 12px 20px rgba(26,58,143,.22));}
62
67
  .v7-band .eyebrow{font-family:var(--mono);font-size:12px;font-weight:800;letter-spacing:.2em;text-transform:uppercase;color:#a4a7af;}
63
68
  .v7-band .bignum{font-size:64px;font-weight:800;letter-spacing:-.03em;line-height:.9;margin-top:10px;color:var(--red);
64
69
  min-height:64px;transition:opacity .52s ease,transform .52s ease;}
@@ -227,7 +232,7 @@
227
232
  body.setup-mode .counterfactual,
228
233
  body.setup-mode .setup-connect{display:none;}
229
234
  body.setup-mode .v7-persona{left:clamp(48px,5vw,86px);top:clamp(42px,5vh,72px);width:min(360px,32vw);}
230
- body.setup-mode .v7-personaName{font-size:clamp(54px,5.3vw,86px);}
235
+ body.setup-mode .v7-personaName{font-size:clamp(38px,3.4vw,54px);}
231
236
  body.setup-mode .v7-head{right:clamp(48px,5vw,86px);top:clamp(42px,5vh,72px);width:min(520px,38vw);}
232
237
  body.setup-mode .v7-head .v7-eyebrow{font-size:clamp(10px,.72vw,12px);}
233
238
  body.setup-mode .v7-title{font-size:clamp(28px,2.1vw,40px);}
@@ -235,7 +240,10 @@
235
240
  body.setup-mode .v7-stat .v{font-size:clamp(20px,1.45vw,28px);}
236
241
  body.setup-mode .v7-stat .k{font-size:clamp(9px,.72vw,11px);}
237
242
  body.setup-mode .v7-band{left:clamp(48px,5vw,86px);right:auto;bottom:clamp(82px,8vh,130px);
238
- width:min(570px,38vw);text-align:left;}
243
+ width:min(640px,42vw);text-align:left;display:grid;grid-template-columns:72px minmax(0,1fr);
244
+ column-gap:16px;align-items:start;}
245
+ body.setup-mode .v7-echo{width:72px;height:72px;margin:2px 0 0;}
246
+ body.setup-mode .v7-bandCopy{min-width:0;}
239
247
  body.setup-mode .v7-band .eyebrow{display:none;}
240
248
  body.setup-mode .v7-band .bignum{display:flex;align-items:baseline;gap:10px;flex-wrap:wrap;
241
249
  margin-top:0;min-height:0;line-height:.96;}
@@ -250,7 +258,7 @@
250
258
  body.setup-mode .v7-ctaWrap .micro{max-width:430px;margin-left:auto;}
251
259
  body.setup-mode .v7-foot{left:clamp(48px,5vw,86px);right:clamp(48px,5vw,86px);}
252
260
  @media (max-height:820px){
253
- body.setup-mode .v7-personaName{font-size:clamp(44px,6vh,62px);}
261
+ body.setup-mode .v7-personaName{font-size:clamp(32px,4.4vh,46px);}
254
262
  body.setup-mode .v7-title{font-size:clamp(30px,5vh,44px);}
255
263
  body.setup-mode .v7-band{bottom:58px;width:min(500px,40vw);}
256
264
  body.setup-mode .v7-ctaWrap{bottom:58px;width:min(390px,32vw);}
@@ -323,8 +331,11 @@
323
331
  </div>
324
332
  <div class="v7-z v7-band">
325
333
  <div class="eyebrow">What we found</div>
334
+ <img class="v7-echo" src="echo-face-cutout.png" alt="" />
335
+ <div class="v7-bandCopy">
326
336
  <div class="bignum" id="v7-metric"><span id="v7-metric-value">{{dirtyPct}}%</span><span id="v7-metric-label">repeated reads</span><button class="v7-infoBtn" id="v7-info-btn" type="button" aria-label="How we read your city" aria-expanded="false" aria-controls="v7-info-sheet">i</button></div>
327
337
  <div class="say" id="v7-metric-say">Your agents re-load context they've already seen — unchanged files, the same rules, stale maps — every session. That's context rot. It's <b>how coding agents work today</b>, and it's the haze on your city.</div>
338
+ </div>
328
339
  </div>
329
340
  <div class="v7-z v7-ctaWrap">
330
341
  <div class="pre">Ready when you are</div>
@@ -896,17 +907,19 @@ function addCube(o){
896
907
  }
897
908
  // repo summary as an upright billboard above each bar — white text, ALWAYS faces the camera (never skewed).
898
909
  // name = small tag just above the bar top; tokens + $ fly higher above it, clear of the cube.
899
- function addTopLabel(o){
910
+ function addTopLabel(o, numbers=true, lift=0){
900
911
  const PW=320, PH=280;
901
912
  const cv=document.createElement('canvas'); cv.width=PW; cv.height=PH;
902
913
  const g=cv.getContext('2d'); g.textAlign='center';
903
914
  g.shadowColor='rgba(0,0,0,0.34)'; g.shadowBlur=9; g.shadowOffsetY=1; // subtle contrast over light areas
904
- // tokens — the headline, flies highest
905
- g.fillStyle='#ffffff'; g.font=`800 ${Math.round(PW*0.19)}px "IBM Plex Mono", monospace`;
906
- g.fillText(fmtTok(o.tokens), PW/2, PH*0.18);
907
- // dollars — just under the tokens, also up high
908
- g.fillStyle='rgba(255,255,255,0.92)'; g.font=`700 ${Math.round(PW*0.11)}px "IBM Plex Mono", monospace`;
909
- g.fillText('$'+Math.round(o.cost*(setupMode?1:COST_SCALE)).toLocaleString('en-US'), PW/2, PH*0.40);
915
+ if(numbers){
916
+ // tokens the headline, flies highest
917
+ g.fillStyle='#ffffff'; g.font=`800 ${Math.round(PW*0.19)}px "IBM Plex Mono", monospace`;
918
+ g.fillText(fmtTok(o.tokens), PW/2, PH*0.18);
919
+ // dollars just under the tokens, also up high
920
+ g.fillStyle='rgba(255,255,255,0.92)'; g.font=`700 ${Math.round(PW*0.11)}px "IBM Plex Mono", monospace`;
921
+ g.fillText('$'+Math.round(o.cost*(setupMode?1:COST_SCALE)).toLocaleString('en-US'), PW/2, PH*0.40);
922
+ }
910
923
  // name — small tag sitting just above the bar top
911
924
  g.fillStyle='rgba(255,255,255,0.95)'; g.font=`600 ${Math.round(PW*0.075)}px "IBM Plex Mono", monospace`;
912
925
  const fw=g.measureText(o.name).width, maxw=PW*0.94;
@@ -915,7 +928,7 @@ function addTopLabel(o){
915
928
  const sprite=new THREE.Sprite(new THREE.SpriteMaterial({map:tex, transparent:true, toneMapped:false, depthWrite:false}));
916
929
  const wWorld=Math.min(Math.max(o.w*0.95, 42), 128), hWorld=wWorld*PH/PW; // scales with bar size, clamped legible
917
930
  sprite.scale.set(wWorld, hWorld, 1);
918
- sprite.position.set(o.x, TRAY_TOP + o.h - 3 + 6 + 0.36*hWorld, o.z); // name tags the top; tokens/$ fly higher
931
+ sprite.position.set(o.x, TRAY_TOP + o.h - 3 + 6 + 0.36*hWorld + lift, o.z); // name tags the top; tokens/$ fly higher; lift staggers neighbors
919
932
  scene.add(sprite);
920
933
  }
921
934
  // fine ivory grain — textures EVERY face (incl. the curved side walls), so the tray isn't flat plastic
@@ -962,7 +975,12 @@ tray.position.set(CX, TRAY_TOP/2, CZ); tray.castShadow=true; tray.receiveShadow=
962
975
  plane.rotation.x=-Math.PI/2; plane.position.set(CX, TRAY_TOP+0.6, CZ); scene.add(plane);
963
976
  })();
964
977
 
965
- all.forEach(o=>{ o.mesh=addCube(o); o.mesh.userData=o; if(o.w>=LABEL_MIN_BASE) addTopLabel(o); });
978
+ // De-crowd the skyline: only the top token movers fly their numbers; every other building keeps just
979
+ // its name tag (hover still shows the full repo detail card). Kills the mid-air label collisions.
980
+ const NUMBERED_LABELS=4;
981
+ const numberedOrder=all.slice().sort((a,b)=>(b.tokens||0)-(a.tokens||0)).slice(0,NUMBERED_LABELS);
982
+ const numberedLift=new Map(numberedOrder.map((o,i)=>[o.name, (i%2)*16])); // alternate heights so neighbors' numbers never collide
983
+ all.forEach(o=>{ o.mesh=addCube(o); o.mesh.userData=o; if(o.w>=LABEL_MIN_BASE) addTopLabel(o, numberedLift.has(o.name), numberedLift.get(o.name)||0); });
966
984
 
967
985
  // bottom-right standing "signpost": tray summary — total tokens + how much is input
968
986
  let signpostCanvas=null, signpostTexture=null;
Binary file
@@ -0,0 +1,66 @@
1
+ import fs from "node:fs";
2
+ import os from "node:os";
3
+ import path from "node:path";
4
+ import { spawnSync } from "node:child_process";
5
+ // Show-after-restart for the HUD via a macOS LaunchAgent. We ship a CLI, not an .app bundle, so this is
6
+ // the standard way to make the overlay persist across quit/reboot. Shared by the CLI (`echomem-hud
7
+ // autostart`) and the Electron context menu so both write an identical, robust registration.
8
+ const LABEL = "com.echomem.hud";
9
+ export function autostartPlistPath() {
10
+ return path.join(os.homedir(), "Library", "LaunchAgents", `${LABEL}.plist`);
11
+ }
12
+ export function autostartSupported() {
13
+ return process.platform === "darwin";
14
+ }
15
+ export function isAutostartEnabled() {
16
+ return autostartSupported() && fs.existsSync(autostartPlistPath());
17
+ }
18
+ function escapeXml(value) {
19
+ return value.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
20
+ }
21
+ function launchctlUid() {
22
+ return typeof process.getuid === "function" ? process.getuid() : 501;
23
+ }
24
+ /**
25
+ * Register the LaunchAgent. `programArgs` is the full ProgramArguments array — the caller supplies the
26
+ * exact launch command (electron binary + electron-main.js + flags) so it works regardless of whether
27
+ * node is on launchd's PATH. Loads immediately as well as at future logins.
28
+ */
29
+ export function enableAutostart(programArgs) {
30
+ if (!autostartSupported())
31
+ return { ok: false, message: "autostart is currently macOS-only." };
32
+ const plist = autostartPlistPath();
33
+ const argsXml = programArgs.map((a) => ` <string>${escapeXml(a)}</string>`).join("\n");
34
+ const xml = `<?xml version="1.0" encoding="UTF-8"?>
35
+ <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
36
+ <plist version="1.0"><dict>
37
+ <key>Label</key><string>${LABEL}</string>
38
+ <key>ProgramArguments</key><array>
39
+ ${argsXml}
40
+ </array>
41
+ <key>RunAtLoad</key><true/>
42
+ <key>StandardOutPath</key><string>/tmp/echomem-hud.log</string>
43
+ <key>StandardErrorPath</key><string>/tmp/echomem-hud.log</string>
44
+ </dict></plist>
45
+ `;
46
+ fs.mkdirSync(path.dirname(plist), { recursive: true });
47
+ fs.writeFileSync(plist, xml);
48
+ const uid = launchctlUid();
49
+ spawnSync("launchctl", ["bootout", `gui/${uid}/${LABEL}`], { stdio: "ignore" }); // replace stale reg
50
+ const boot = spawnSync("launchctl", ["bootstrap", `gui/${uid}`, plist], { encoding: "utf8" });
51
+ return boot.status === 0
52
+ ? { ok: true, message: `EchoMem HUD will show after restart. Off anytime: echomem-hud autostart off` }
53
+ : { ok: true, message: `Wrote ${plist} — shows after your next restart.` };
54
+ }
55
+ export function disableAutostart() {
56
+ if (!autostartSupported())
57
+ return;
58
+ const uid = launchctlUid();
59
+ spawnSync("launchctl", ["bootout", `gui/${uid}/${LABEL}`], { stdio: "ignore" });
60
+ try {
61
+ fs.rmSync(autostartPlistPath(), { force: true });
62
+ }
63
+ catch {
64
+ /* already gone */
65
+ }
66
+ }
package/dist/hud/cli.js CHANGED
@@ -3,6 +3,7 @@ import { spawn } from "node:child_process";
3
3
  import { createRequire } from "node:module";
4
4
  import { fileURLToPath } from "node:url";
5
5
  import { adapterList } from "./adapters.js";
6
+ import { autostartPlistPath, autostartSupported, disableAutostart, enableAutostart, isAutostartEnabled } from "./autostart.js";
6
7
  import { statSignature } from "./fs.js";
7
8
  import { installHooks } from "./hooks.js";
8
9
  import { HudMonitor } from "./monitor.js";
@@ -23,6 +24,8 @@ try {
23
24
  await cmdApp(flags);
24
25
  else if (command === "install-hooks")
25
26
  await cmdInstallHooks(flags);
27
+ else if (command === "autostart")
28
+ cmdAutostart(argv[1] || "status", flags);
26
29
  else if (command === "status")
27
30
  await cmdStatus(flags);
28
31
  else if (command === "report")
@@ -79,6 +82,33 @@ async function cmdApp(flags) {
79
82
  child.unref();
80
83
  console.log("EchoMem HUD app launched.");
81
84
  }
85
+ /**
86
+ * `echomem-hud autostart on|off|status` — show the HUD after restart via a macOS LaunchAgent, so
87
+ * closing/rebooting doesn't lose it (we ship a CLI, not an .app, so this is the standard mechanism).
88
+ * The registration runs the electron binary directly on electron-main.js — no dependency on node
89
+ * being on launchd's PATH — matching what the in-app "Show after restart" toggle writes.
90
+ */
91
+ function cmdAutostart(action, flags) {
92
+ if (!autostartSupported()) {
93
+ console.log("autostart is currently macOS-only (LaunchAgent).");
94
+ return;
95
+ }
96
+ if (action === "on") {
97
+ const require = createRequire(import.meta.url);
98
+ const electronPath = require("electron");
99
+ const mainPath = fileURLToPath(new URL("./electron-main.js", import.meta.url));
100
+ const client = typeof flags.client === "string" ? flags.client : "auto";
101
+ const result = enableAutostart([electronPath, mainPath, "--client", client]);
102
+ console.log(`✅ ${result.message}`);
103
+ return;
104
+ }
105
+ if (action === "off") {
106
+ disableAutostart();
107
+ console.log("✅ Show after restart disabled — the HUD will not relaunch after restart.");
108
+ return;
109
+ }
110
+ console.log(isAutostartEnabled() ? `Autostart: ON (${autostartPlistPath()})` : "Autostart: OFF. Enable with: echomem-hud autostart on");
111
+ }
82
112
  async function cmdInstallHooks(flags) {
83
113
  const paths = installHooks(parseMode(flags.client));
84
114
  console.log(`Installed EchoMem HUD hook support:\n${paths.map((p) => `- ${p}`).join("\n")}`);
@@ -136,6 +166,7 @@ Usage:
136
166
  echomem-hud serve [--client codex|claude-code|claude-desktop|both|auto] [--port 17377]
137
167
  echomem-hud app [--client codex|claude-code|claude-desktop|both|auto]
138
168
  echomem-hud install-hooks [--client codex|claude-code|both]
169
+ echomem-hud autostart on|off|status (show the HUD after restart — macOS)
139
170
  echomem-hud status
140
171
  echomem-hud report [--client codex|claude-code|claude-desktop|auto] [--limit 40] [--json]
141
172
  `);