@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 +14 -8
- package/assets/hud/claude.svg +1 -0
- package/assets/hud/codex.svg +1 -0
- package/assets/hud/session-viewer.html +35 -0
- package/dist/city/echo-ai-city-only.html +31 -13
- package/dist/city/echo-ai-city-only.template.html +31 -13
- package/dist/city/echo-face-cutout.png +0 -0
- package/dist/hud/autostart.js +66 -0
- package/dist/hud/cli.js +31 -0
- package/dist/hud/electron-main.js +182 -19
- package/dist/hud/monitor.js +36 -68
- package/dist/hud/preload.cjs +3 -0
- package/dist/hud/server.js +321 -4
- package/dist/hud/web.js +633 -69
- package/dist/index.js +116 -22
- package/dist/migrate.js +18 -0
- package/dist/setup-page.js +978 -60
- package/dist/setup.js +355 -42
- package/dist/v1-contract.js +20 -2
- package/package.json +3 -2
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:
|
|
39
|
-
|
|
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
|
-
|
|
43
|
-
|
|
44
|
-
config (with **no secret** in it — credentials live in
|
|
45
|
-
|
|
46
|
-
|
|
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
|
|
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
|
-
|
|
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(
|
|
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(
|
|
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(
|
|
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
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
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(
|
|
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(
|
|
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
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
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
|
-
|
|
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, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
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
|
`);
|