@echomem/mcp 1.4.18 โ 1.4.20
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 +12 -5
- package/dist/codex-session-files.js +7 -1
- package/dist/hud/server.js +17 -13
- package/dist/hud/web.js +204 -13
- package/dist/index.js +61 -30
- package/dist/keystore.js +9 -19
- package/dist/migrate.js +78 -36
- package/dist/setup-page/client-core.js +31 -1
- package/dist/setup-page/client-extraction.js +581 -157
- package/dist/setup-page/client-lifecycle.js +64 -43
- package/dist/setup-page/client-report-city.js +14 -5
- package/dist/setup-page/styles-extraction.js +1176 -148
- package/dist/setup-page/styles-foundation.js +20 -15
- package/dist/setup-page/styles-website-alignment.js +818 -0
- package/dist/setup-page/styles.js +2 -0
- package/dist/setup-preview.js +64 -1
- package/dist/setup.js +328 -39
- package/dist/v1-contract.js +8 -8
- package/package.json +1 -1
- package/templates/codex-skills/echomem-forget/SKILL.md +16 -0
- package/templates/codex-skills/echomem-forget/agents/openai.yaml +6 -0
- package/templates/codex-skills/echomem-login/SKILL.md +27 -0
- package/templates/codex-skills/echomem-login/agents/openai.yaml +6 -0
- package/templates/codex-skills/echomem-save/SKILL.md +14 -0
- package/templates/codex-skills/echomem-save/agents/openai.yaml +6 -0
- package/templates/codex-skills/echomem-search/SKILL.md +14 -0
- package/templates/codex-skills/echomem-search/agents/openai.yaml +6 -0
package/README.md
CHANGED
|
@@ -25,8 +25,11 @@ For **zero-knowledge accounts**, the key never leaves your machine. The bridge h
|
|
|
25
25
|
- **writes:** the key is handed to your own backend transiently in the `X-Encryption-Key` header so
|
|
26
26
|
memories are encrypted at rest (the server processes plaintext for the request only โ never stores
|
|
27
27
|
the key).
|
|
28
|
-
- **
|
|
29
|
-
|
|
28
|
+
- **trusted-device lifecycle:** after a verified login/unlock, the MCP key remains available until
|
|
29
|
+
the user runs `echomem-mcp lock`, logs out, or removes the local credentials. If the key is absent,
|
|
30
|
+
tools tell the user to run `echomem-mcp unlock` in their own terminal. The passphrase prompt is
|
|
31
|
+
visible while typed characters stay hidden, and the current agent session can retry immediately
|
|
32
|
+
after unlock โ **ciphertext is never handed to the model**.
|
|
30
33
|
|
|
31
34
|
Unencrypted accounts are unaffected. `search_memories` returns retrieved memories by default so
|
|
32
35
|
your MCP client does the final answer generation; pass `includeAnswer: true` only if you need the
|
|
@@ -47,8 +50,11 @@ to the global install, but installing globally avoids the issue entirely). The c
|
|
|
47
50
|
launch-at-login also runs from the installed path. `init` is the flagship one-liner โ it wraps `setup --all --with-hud`: it writes each
|
|
48
51
|
installed agent's MCP config (with **no secret** in it โ credentials live in
|
|
49
52
|
`~/.echomem/credentials.json`, mode 0600), adds the EchoMem memory guidance to their global
|
|
50
|
-
`AGENTS.md` / `CLAUDE.md`,
|
|
51
|
-
|
|
53
|
+
`AGENTS.md` / `CLAUDE.md`, installs first-party `echomem-search`, `echomem-save`,
|
|
54
|
+
`echomem-forget`, and `echomem-login` skills for Codex, opens the browser to approve the device
|
|
55
|
+
(and unlock the vault for encrypted accounts), then launches the context HUD. EchoMem updates only
|
|
56
|
+
its own skill folders; other memory-provider skills are detected and reported but never modified.
|
|
57
|
+
Reload your editors and you're done.
|
|
52
58
|
|
|
53
59
|
Prefer to keep it minimal? `echomem-mcp setup` configures only the auto-detected editor and skips the
|
|
54
60
|
HUD; the granular commands below still work.
|
|
@@ -64,7 +70,8 @@ HUD; the granular commands below still work.
|
|
|
64
70
|
| `npx -y @echomem/mcp@latest update --client codex` | Update one client only |
|
|
65
71
|
| `echomem-mcp setup --with-hud [--client codex]` | Write client config + log in + launch the EchoMem context HUD |
|
|
66
72
|
| `echomem-mcp login` | Approve device in browser (or use `--token` / `--passphrase`) |
|
|
67
|
-
| `echomem-mcp unlock` |
|
|
73
|
+
| `echomem-mcp unlock` | Privately unlock the vault on this trusted device |
|
|
74
|
+
| `echomem-mcp lock` | Remove the local vault key while keeping the device login |
|
|
68
75
|
| `echomem-mcp status` | Show token / key / detected clients, configured bridge versions, and update guidance |
|
|
69
76
|
| `echomem-mcp doctor [--no-network]` | Diagnose configured client bridge versions |
|
|
70
77
|
| `echomem-mcp logout` | Remove stored credentials |
|
|
@@ -35,6 +35,12 @@ function normalizedSessionId(value) {
|
|
|
35
35
|
const trimmed = value.trim();
|
|
36
36
|
return new RegExp(`^${UUID_RE.source}$`, "i").test(trimmed) ? trimmed.toLowerCase() : trimmed;
|
|
37
37
|
}
|
|
38
|
+
function isAutomaticCodexSource(source) {
|
|
39
|
+
if (typeof source !== "string")
|
|
40
|
+
return false;
|
|
41
|
+
const normalized = source.trim().toLowerCase().replace(/-/g, "_");
|
|
42
|
+
return normalized === "auto_review" || normalized === "guardian";
|
|
43
|
+
}
|
|
38
44
|
function metadataFromFile(file) {
|
|
39
45
|
try {
|
|
40
46
|
for (const line of initialJsonLines(file)) {
|
|
@@ -63,7 +69,7 @@ function metadataFromFile(file) {
|
|
|
63
69
|
return {
|
|
64
70
|
id: rawId || null,
|
|
65
71
|
startedAtMs: Number.isFinite(timestamp) ? timestamp : null,
|
|
66
|
-
userInitiated: !isSubagent && !hasParent,
|
|
72
|
+
userInitiated: !isSubagent && !hasParent && !isAutomaticCodexSource(source),
|
|
67
73
|
};
|
|
68
74
|
}
|
|
69
75
|
return { id: null, startedAtMs: null, userInitiated: null };
|
package/dist/hud/server.js
CHANGED
|
@@ -319,6 +319,9 @@ async function handleBillingStatus(res) {
|
|
|
319
319
|
}
|
|
320
320
|
let plan = "unknown";
|
|
321
321
|
let paid = false;
|
|
322
|
+
let historicalConversationQuota = null;
|
|
323
|
+
let memoryProcessingQuota = null;
|
|
324
|
+
let memorySearchQuota = null;
|
|
322
325
|
try {
|
|
323
326
|
const response = await fetch(`${API_BASE}/api/extension/account/bootstrap`, {
|
|
324
327
|
headers: { Authorization: `Bearer ${token}` },
|
|
@@ -327,34 +330,35 @@ async function handleBillingStatus(res) {
|
|
|
327
330
|
if (response.ok) {
|
|
328
331
|
plan = typeof data.plan === "string" ? data.plan.toLowerCase() : "free";
|
|
329
332
|
paid = ["pro", "power", "team", "enterprise"].includes(plan);
|
|
333
|
+
historicalConversationQuota = data.historicalConversationQuota ?? null;
|
|
334
|
+
memoryProcessingQuota = data.memoryProcessingQuota ?? null;
|
|
335
|
+
memorySearchQuota = data.memorySearchQuota ?? null;
|
|
330
336
|
}
|
|
331
337
|
}
|
|
332
338
|
catch {
|
|
333
339
|
/* Local alert still gives the HUD something useful. */
|
|
334
340
|
}
|
|
335
|
-
|
|
341
|
+
const quotaPayload = {
|
|
342
|
+
historicalConversationQuota,
|
|
343
|
+
memoryProcessingQuota,
|
|
344
|
+
memorySearchQuota,
|
|
345
|
+
};
|
|
346
|
+
// Free now includes recall. Ignore old plan_required alerts that may still
|
|
347
|
+
// be present on disk from a previous bridge version.
|
|
348
|
+
if (alert && (alert.kind === "quota_exceeded" || alert.kind === "billing_attention")) {
|
|
336
349
|
return respond({
|
|
337
350
|
ok: true,
|
|
338
351
|
state: alert.kind,
|
|
339
352
|
plan: alert.plan || plan,
|
|
340
|
-
message: alert.kind === "quota_exceeded" ? "
|
|
353
|
+
message: alert.kind === "quota_exceeded" ? "Plan limit reached." : "Billing needs attention.",
|
|
341
354
|
detail: alert.message,
|
|
342
355
|
code: alert.code,
|
|
343
356
|
pricingUrl: appendSource(alert.pricingUrl || pricingUrl, "hud"),
|
|
344
357
|
updatedAt: alert.updatedAt,
|
|
358
|
+
...quotaPayload,
|
|
345
359
|
});
|
|
346
360
|
}
|
|
347
|
-
|
|
348
|
-
return respond({
|
|
349
|
-
ok: true,
|
|
350
|
-
state: "plan_required",
|
|
351
|
-
plan,
|
|
352
|
-
message: "Recall needs a plan.",
|
|
353
|
-
detail: "Search and recall require Echo Pro or Power. Saving conversations keeps working.",
|
|
354
|
-
pricingUrl,
|
|
355
|
-
});
|
|
356
|
-
}
|
|
357
|
-
respond({ ok: true, state: "ok", plan, paid, pricingUrl });
|
|
361
|
+
respond({ ok: true, state: "ok", plan, paid, pricingUrl, ...quotaPayload });
|
|
358
362
|
}
|
|
359
363
|
function appendSource(url, source) {
|
|
360
364
|
try {
|
package/dist/hud/web.js
CHANGED
|
@@ -678,6 +678,85 @@ export const HUD_HTML = String.raw `<!doctype html>
|
|
|
678
678
|
box-shadow: 0 12px 20px -16px rgba(122, 36, 30, 0.95);
|
|
679
679
|
}
|
|
680
680
|
.billing-link:hover { filter: brightness(0.96); }
|
|
681
|
+
.quota-card {
|
|
682
|
+
display: none;
|
|
683
|
+
margin: 0 0 10px;
|
|
684
|
+
padding: 12px 13px 13px;
|
|
685
|
+
border: 1px solid rgba(26, 58, 143, 0.18);
|
|
686
|
+
border-radius: 12px;
|
|
687
|
+
background: rgba(255, 255, 255, 0.72);
|
|
688
|
+
box-shadow: 0 1px 5px rgba(26, 58, 143, 0.05);
|
|
689
|
+
}
|
|
690
|
+
.quota-head {
|
|
691
|
+
display: flex;
|
|
692
|
+
align-items: center;
|
|
693
|
+
gap: 7px;
|
|
694
|
+
margin-bottom: 9px;
|
|
695
|
+
}
|
|
696
|
+
.quota-title {
|
|
697
|
+
color: var(--ink);
|
|
698
|
+
font-size: 10.5px;
|
|
699
|
+
font-weight: 850;
|
|
700
|
+
letter-spacing: 0.015em;
|
|
701
|
+
}
|
|
702
|
+
.quota-plan {
|
|
703
|
+
border-radius: 999px;
|
|
704
|
+
background: rgba(26, 58, 143, 0.07);
|
|
705
|
+
padding: 2px 7px;
|
|
706
|
+
color: var(--blue);
|
|
707
|
+
font-size: 9px;
|
|
708
|
+
font-weight: 850;
|
|
709
|
+
text-transform: uppercase;
|
|
710
|
+
}
|
|
711
|
+
.quota-manage {
|
|
712
|
+
margin-left: auto;
|
|
713
|
+
border: 0;
|
|
714
|
+
padding: 0;
|
|
715
|
+
background: transparent;
|
|
716
|
+
color: var(--faint);
|
|
717
|
+
font-family: inherit;
|
|
718
|
+
font-size: 9.5px;
|
|
719
|
+
font-weight: 750;
|
|
720
|
+
cursor: pointer;
|
|
721
|
+
}
|
|
722
|
+
.quota-manage:hover { color: var(--blue); }
|
|
723
|
+
.quota-list { display: grid; gap: 9px; }
|
|
724
|
+
.quota-row { --quota-color: var(--blue); }
|
|
725
|
+
.quota-row.is-warn { --quota-color: #c98712; }
|
|
726
|
+
.quota-row.is-limit { --quota-color: #c7372f; }
|
|
727
|
+
.quota-line, .quota-subline {
|
|
728
|
+
display: flex;
|
|
729
|
+
align-items: baseline;
|
|
730
|
+
justify-content: space-between;
|
|
731
|
+
gap: 10px;
|
|
732
|
+
}
|
|
733
|
+
.quota-name { color: var(--muted); font-size: 10px; font-weight: 720; }
|
|
734
|
+
.quota-value {
|
|
735
|
+
color: var(--ink);
|
|
736
|
+
font-family: "JetBrains Mono", ui-monospace, monospace;
|
|
737
|
+
font-size: 9.5px;
|
|
738
|
+
font-weight: 760;
|
|
739
|
+
white-space: nowrap;
|
|
740
|
+
}
|
|
741
|
+
.quota-track {
|
|
742
|
+
height: 4px;
|
|
743
|
+
margin-top: 4px;
|
|
744
|
+
overflow: hidden;
|
|
745
|
+
border-radius: 999px;
|
|
746
|
+
background: rgba(26, 58, 143, 0.11);
|
|
747
|
+
}
|
|
748
|
+
.quota-fill {
|
|
749
|
+
width: 0;
|
|
750
|
+
height: 100%;
|
|
751
|
+
border-radius: inherit;
|
|
752
|
+
background: var(--quota-color);
|
|
753
|
+
transition: width .28s ease, background .2s ease;
|
|
754
|
+
}
|
|
755
|
+
.quota-subline { margin-top: 3px; }
|
|
756
|
+
.quota-reset, .quota-percent { color: var(--faint); font-size: 8.5px; font-weight: 650; }
|
|
757
|
+
.quota-row.is-limit .quota-value,
|
|
758
|
+
.quota-row.is-limit .quota-percent { color: #9d2b25; font-weight: 850; }
|
|
759
|
+
.quota-row.is-unavailable .quota-fill { background: rgba(26, 58, 143, 0.14); }
|
|
681
760
|
button.renew-btn {
|
|
682
761
|
display: none;
|
|
683
762
|
width: 100%;
|
|
@@ -838,6 +917,30 @@ export const HUD_HTML = String.raw `<!doctype html>
|
|
|
838
917
|
</div>
|
|
839
918
|
<div class="details">
|
|
840
919
|
<div class="timeline-scrollbar hidden" id="timelineScrollbar" aria-hidden="true"><span class="timeline-thumb" id="timelineThumb"></span></div>
|
|
920
|
+
<section class="quota-card" id="quotacard" aria-label="Plan usage">
|
|
921
|
+
<div class="quota-head">
|
|
922
|
+
<span class="quota-title">Plan usage</span>
|
|
923
|
+
<span class="quota-plan" id="quotaplan">Free</span>
|
|
924
|
+
<button class="quota-manage" id="quotamanage" type="button">Manage</button>
|
|
925
|
+
</div>
|
|
926
|
+
<div class="quota-list">
|
|
927
|
+
<div class="quota-row" data-quota="processing">
|
|
928
|
+
<div class="quota-line"><span class="quota-name">Processing this week</span><span class="quota-value"></span></div>
|
|
929
|
+
<div class="quota-track"><div class="quota-fill"></div></div>
|
|
930
|
+
<div class="quota-subline"><span class="quota-reset"></span><span class="quota-percent"></span></div>
|
|
931
|
+
</div>
|
|
932
|
+
<div class="quota-row" data-quota="searches">
|
|
933
|
+
<div class="quota-line"><span class="quota-name">Memory searches</span><span class="quota-value"></span></div>
|
|
934
|
+
<div class="quota-track"><div class="quota-fill"></div></div>
|
|
935
|
+
<div class="quota-subline"><span class="quota-reset"></span><span class="quota-percent"></span></div>
|
|
936
|
+
</div>
|
|
937
|
+
<div class="quota-row" data-quota="historical">
|
|
938
|
+
<div class="quota-line"><span class="quota-name">Bulk history imports</span><span class="quota-value"></span></div>
|
|
939
|
+
<div class="quota-track"><div class="quota-fill"></div></div>
|
|
940
|
+
<div class="quota-subline"><span class="quota-reset">Lifetime limit</span><span class="quota-percent"></span></div>
|
|
941
|
+
</div>
|
|
942
|
+
</div>
|
|
943
|
+
</section>
|
|
841
944
|
<div class="now-card" id="nowcard">
|
|
842
945
|
<div class="now-sess">
|
|
843
946
|
<img class="now-icon" id="nowicon" src="/assets/hud/codex.svg" alt="" />
|
|
@@ -903,6 +1006,9 @@ export const HUD_HTML = String.raw `<!doctype html>
|
|
|
903
1006
|
const billingTitle = document.getElementById("billingtitle");
|
|
904
1007
|
const billingCopy = document.getElementById("billingcopy");
|
|
905
1008
|
const billingLink = document.getElementById("billinglink");
|
|
1009
|
+
const quotaCard = document.getElementById("quotacard");
|
|
1010
|
+
const quotaPlan = document.getElementById("quotaplan");
|
|
1011
|
+
const quotaManage = document.getElementById("quotamanage");
|
|
906
1012
|
const renewBtn = document.getElementById("renewbtn");
|
|
907
1013
|
const renewResult = document.getElementById("renewresult");
|
|
908
1014
|
const renewMsg = document.getElementById("renewmsg");
|
|
@@ -1134,7 +1240,7 @@ export const HUD_HTML = String.raw `<!doctype html>
|
|
|
1134
1240
|
if (!id) return;
|
|
1135
1241
|
fetch("/jump?id=" + encodeURIComponent(id)).catch(() => {});
|
|
1136
1242
|
}
|
|
1137
|
-
|
|
1243
|
+
function openPricing(e) {
|
|
1138
1244
|
e.stopPropagation();
|
|
1139
1245
|
const url = billingStatus && billingStatus.pricingUrl;
|
|
1140
1246
|
if (!url) return;
|
|
@@ -1146,7 +1252,9 @@ export const HUD_HTML = String.raw `<!doctype html>
|
|
|
1146
1252
|
.catch(() => {
|
|
1147
1253
|
try { window.open(url, "_blank", "noopener"); } catch {}
|
|
1148
1254
|
});
|
|
1149
|
-
}
|
|
1255
|
+
}
|
|
1256
|
+
if (billingLink) billingLink.addEventListener("click", openPricing);
|
|
1257
|
+
if (quotaManage) quotaManage.addEventListener("click", openPricing);
|
|
1150
1258
|
function toggleSessionDetails(id) {
|
|
1151
1259
|
if (!id) return;
|
|
1152
1260
|
const nextId = lastState && lastState.expandedId === id ? "auto" : id;
|
|
@@ -1614,6 +1722,18 @@ export const HUD_HTML = String.raw `<!doctype html>
|
|
|
1614
1722
|
const previewExpanded = previewParams.get("expanded") === "1" || previewParams.get("pinned") === "1";
|
|
1615
1723
|
if (previewParams.get("capture") === "1") hud.classList.add("capture-preview");
|
|
1616
1724
|
if (previewColor === "green" || previewColor === "amber" || previewColor === "red") {
|
|
1725
|
+
const previewProcessingUsed = previewColor === "red" ? 200000 : previewColor === "amber" ? 163000 : 84000;
|
|
1726
|
+
const previewSearchUsed = previewColor === "red" ? 100 : previewColor === "amber" ? 82 : 32;
|
|
1727
|
+
billingStatus = {
|
|
1728
|
+
ok: true,
|
|
1729
|
+
state: "ok",
|
|
1730
|
+
plan: "pro",
|
|
1731
|
+
paid: true,
|
|
1732
|
+
pricingUrl: "https://yeahecho.com/pricing?source=hud_preview",
|
|
1733
|
+
memoryProcessingQuota: { used: previewProcessingUsed, limit: 200000, remaining: 200000 - previewProcessingUsed, resetAt: new Date(Date.now() + 31 * 60 * 60 * 1000).toISOString() },
|
|
1734
|
+
memorySearchQuota: { used: previewSearchUsed, limit: 100, remaining: 100 - previewSearchUsed, resetAt: new Date(Date.now() + 31 * 60 * 60 * 1000).toISOString() },
|
|
1735
|
+
historicalConversationQuota: { used: 191, limit: 250, remaining: 59 },
|
|
1736
|
+
};
|
|
1617
1737
|
const sample = {
|
|
1618
1738
|
client: "codex",
|
|
1619
1739
|
label: "EchoMem-Chrome",
|
|
@@ -1824,25 +1944,96 @@ export const HUD_HTML = String.raw `<!doctype html>
|
|
|
1824
1944
|
|
|
1825
1945
|
function renderBillingStatus() {
|
|
1826
1946
|
if (!billingBanner) return;
|
|
1947
|
+
renderQuotaUsage();
|
|
1827
1948
|
const state = billingStatus && billingStatus.state;
|
|
1828
|
-
const blocked = state === "
|
|
1949
|
+
const blocked = state === "quota_exceeded" || state === "billing_attention";
|
|
1829
1950
|
billingBanner.style.display = blocked ? "block" : "none";
|
|
1830
1951
|
billingBanner.classList.toggle("limit", state === "quota_exceeded");
|
|
1831
1952
|
if (!blocked) return;
|
|
1832
|
-
const title = state === "quota_exceeded"
|
|
1833
|
-
|
|
1834
|
-
|
|
1835
|
-
|
|
1836
|
-
: billingStatus.message || "Billing needs attention";
|
|
1837
|
-
const detail = state === "quota_exceeded"
|
|
1838
|
-
? "You used all Pro searches this month. Upgrade or wait for reset."
|
|
1839
|
-
: state === "plan_required"
|
|
1840
|
-
? "Search memory requires Pro or Power. Saving still works."
|
|
1841
|
-
: billingStatus.detail || "Open pricing to restore memory search.";
|
|
1953
|
+
const title = billingStatus.message || (state === "quota_exceeded" ? "Plan limit reached" : "Billing needs attention");
|
|
1954
|
+
const detail = billingStatus.detail || (state === "quota_exceeded"
|
|
1955
|
+
? "A weekly limit has been reached. Wait for reset or change your plan."
|
|
1956
|
+
: "Open pricing to review your plan.");
|
|
1842
1957
|
if (billingTitle) billingTitle.textContent = title;
|
|
1843
1958
|
if (billingCopy) billingCopy.textContent = detail;
|
|
1844
1959
|
}
|
|
1845
1960
|
|
|
1961
|
+
function renderQuotaUsage() {
|
|
1962
|
+
if (!quotaCard) return;
|
|
1963
|
+
const status = billingStatus || {};
|
|
1964
|
+
const quotaRows = [
|
|
1965
|
+
["processing", status.memoryProcessingQuota, false],
|
|
1966
|
+
["searches", status.memorySearchQuota, false],
|
|
1967
|
+
["historical", status.historicalConversationQuota, true],
|
|
1968
|
+
];
|
|
1969
|
+
const hasQuota = quotaRows.some((entry) => entry[1] && typeof entry[1] === "object");
|
|
1970
|
+
const visible = hasQuota && status.state !== "not_connected" && status.plan !== "unknown";
|
|
1971
|
+
quotaCard.style.display = visible ? "block" : "none";
|
|
1972
|
+
if (!visible) return;
|
|
1973
|
+
if (quotaPlan) quotaPlan.textContent = planLabel(status.plan || "free");
|
|
1974
|
+
for (const entry of quotaRows) renderQuotaRow(entry[0], entry[1], entry[2]);
|
|
1975
|
+
}
|
|
1976
|
+
|
|
1977
|
+
function renderQuotaRow(key, snapshot, lifetime) {
|
|
1978
|
+
if (!quotaCard) return;
|
|
1979
|
+
const row = quotaCard.querySelector('[data-quota="' + key + '"]');
|
|
1980
|
+
if (!row) return;
|
|
1981
|
+
const valueEl = row.querySelector(".quota-value");
|
|
1982
|
+
const fillEl = row.querySelector(".quota-fill");
|
|
1983
|
+
const resetEl = row.querySelector(".quota-reset");
|
|
1984
|
+
const percentEl = row.querySelector(".quota-percent");
|
|
1985
|
+
const used = quotaNumber(snapshot && snapshot.used);
|
|
1986
|
+
const limit = quotaNumber(snapshot && snapshot.limit);
|
|
1987
|
+
const available = used !== null && limit !== null;
|
|
1988
|
+
const unlimited = available && limit < 0;
|
|
1989
|
+
const percent = available && limit > 0 ? clamp(Math.round(used / limit * 100), 0, 100) : 0;
|
|
1990
|
+
|
|
1991
|
+
row.classList.toggle("is-unavailable", !available);
|
|
1992
|
+
row.classList.toggle("is-warn", available && !unlimited && percent >= 80 && percent < 100);
|
|
1993
|
+
row.classList.toggle("is-limit", available && !unlimited && percent >= 100);
|
|
1994
|
+
if (valueEl) {
|
|
1995
|
+
valueEl.textContent = unlimited
|
|
1996
|
+
? formatQuotaNumber(used) + " / Unlimited"
|
|
1997
|
+
: (used === null ? "โ" : formatQuotaNumber(used)) + " / " + (limit === null ? "โ" : formatQuotaNumber(limit));
|
|
1998
|
+
}
|
|
1999
|
+
if (fillEl) fillEl.style.width = unlimited ? "0%" : percent + "%";
|
|
2000
|
+
if (percentEl) percentEl.textContent = !available
|
|
2001
|
+
? "Usage unavailable"
|
|
2002
|
+
: unlimited
|
|
2003
|
+
? "Unlimited"
|
|
2004
|
+
: percent >= 100
|
|
2005
|
+
? "Limit reached"
|
|
2006
|
+
: percent + "%";
|
|
2007
|
+
if (resetEl) resetEl.textContent = lifetime ? "Lifetime limit" : quotaResetLabel(snapshot && snapshot.resetAt);
|
|
2008
|
+
row.setAttribute("aria-label", key + " usage " + (percentEl ? percentEl.textContent : ""));
|
|
2009
|
+
}
|
|
2010
|
+
|
|
2011
|
+
function quotaNumber(value) {
|
|
2012
|
+
return typeof value === "number" && isFinite(value) ? value : null;
|
|
2013
|
+
}
|
|
2014
|
+
|
|
2015
|
+
function formatQuotaNumber(value) {
|
|
2016
|
+
const n = Math.max(0, Number(value) || 0);
|
|
2017
|
+
if (n >= 1000000) return (n / 1000000).toFixed(n < 10000000 ? 1 : 0).replace(/\.0$/, "") + "M";
|
|
2018
|
+
if (n >= 1000) return (n / 1000).toFixed(n < 100000 ? 1 : 0).replace(/\.0$/, "") + "K";
|
|
2019
|
+
return Math.round(n).toLocaleString("en-US");
|
|
2020
|
+
}
|
|
2021
|
+
|
|
2022
|
+
function quotaResetLabel(value) {
|
|
2023
|
+
if (!value) return "Weekly reset";
|
|
2024
|
+
const resetAt = new Date(value);
|
|
2025
|
+
if (!isFinite(resetAt.getTime())) return "Weekly reset";
|
|
2026
|
+
if (resetAt.getTime() <= Date.now()) return "Resetting now";
|
|
2027
|
+
return "Resets " + resetAt.toLocaleString(undefined, {
|
|
2028
|
+
weekday: "short",
|
|
2029
|
+
month: "short",
|
|
2030
|
+
day: "numeric",
|
|
2031
|
+
hour: "numeric",
|
|
2032
|
+
minute: "2-digit",
|
|
2033
|
+
timeZoneName: "short",
|
|
2034
|
+
});
|
|
2035
|
+
}
|
|
2036
|
+
|
|
1846
2037
|
function renderWhy(score, oldPct, repeatCount, sat, signal, color) {
|
|
1847
2038
|
const rows = [];
|
|
1848
2039
|
for (const row of junkBreakdown(score)) rows.push(row);
|
package/dist/index.js
CHANGED
|
@@ -22,11 +22,6 @@ class NoTokenError extends Error {
|
|
|
22
22
|
}
|
|
23
23
|
/** Thrown when an encrypted account has no usable key โ the model gets an unlock nudge, never ciphertext. */
|
|
24
24
|
class LockedError extends Error {
|
|
25
|
-
expired;
|
|
26
|
-
constructor(expired) {
|
|
27
|
-
super("EchoMem vault locked");
|
|
28
|
-
this.expired = expired;
|
|
29
|
-
}
|
|
30
25
|
}
|
|
31
26
|
function stringifyErrorValue(value) {
|
|
32
27
|
if (value == null) {
|
|
@@ -545,10 +540,16 @@ class EchoMemApiClient {
|
|
|
545
540
|
async fetchMemoryMap() {
|
|
546
541
|
try {
|
|
547
542
|
const enc = await this.encState(); // unencrypted โ {enabled:false}; encrypted+locked โ throws
|
|
548
|
-
|
|
549
|
-
|
|
543
|
+
// Tool-list decoration is not a user-initiated search and must not spend
|
|
544
|
+
// one of the account's weekly searches. Build the best-effort map from
|
|
545
|
+
// the ordinary paginated memory listing instead.
|
|
546
|
+
const response = await this.axios.get("/api/extension/memories?limit=200", { timeout: 6000 });
|
|
550
547
|
const data = enc.enabled ? await this.decryptResult(response.data, enc.key) : response.data;
|
|
551
|
-
const memories = Array.isArray(data?.
|
|
548
|
+
const memories = Array.isArray(data?.data)
|
|
549
|
+
? data.data
|
|
550
|
+
: Array.isArray(data?.memories)
|
|
551
|
+
? data.memories
|
|
552
|
+
: [];
|
|
552
553
|
const keys = [...new Set(memories.map((m) => String(m?.keys || "").trim()).filter(Boolean))];
|
|
553
554
|
if (!keys.length)
|
|
554
555
|
return undefined;
|
|
@@ -585,7 +586,7 @@ class EchoMemApiClient {
|
|
|
585
586
|
// No usable key: consult the server to tell "unencrypted" apart from "encrypted but locked/expired".
|
|
586
587
|
const cfg = await this.getEncryptionConfig();
|
|
587
588
|
if (cfg.enabled)
|
|
588
|
-
throw new LockedError(
|
|
589
|
+
throw new LockedError("EchoMem vault locked");
|
|
589
590
|
return { enabled: false };
|
|
590
591
|
}
|
|
591
592
|
pruneDeleteConfirmations(now = Date.now()) {
|
|
@@ -789,6 +790,7 @@ class EchoMemApiClient {
|
|
|
789
790
|
startDate,
|
|
790
791
|
endDate,
|
|
791
792
|
limit,
|
|
793
|
+
requestId: randomUUID(),
|
|
792
794
|
});
|
|
793
795
|
return enc.enabled ? await this.decryptResult(response.data, enc.key) : response.data;
|
|
794
796
|
}
|
|
@@ -833,21 +835,41 @@ class EchoMemApiClient {
|
|
|
833
835
|
// For an encrypted account, hand the server the key transiently in the X-Encryption-Key header
|
|
834
836
|
// so it encrypts at rest (mirrors the extension's write path, spec ยง3.1a). Locked โ LockedError.
|
|
835
837
|
const enc = await this.encState();
|
|
836
|
-
const config =
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
838
|
+
const config = {
|
|
839
|
+
headers: {
|
|
840
|
+
"X-EchoMem-Request-Id": randomUUID(),
|
|
841
|
+
...(enc.enabled && enc.key ? { "X-Encryption-Key": enc.key } : {}),
|
|
842
|
+
},
|
|
843
|
+
};
|
|
844
|
+
try {
|
|
845
|
+
const response = await this.axios.post("/api/extension/memories/ingest", {
|
|
846
|
+
rawData,
|
|
847
|
+
sourceUrl: parsed.url,
|
|
848
|
+
source: parsed.source || "mcp_server",
|
|
849
|
+
title: parsed.title,
|
|
850
|
+
// Stable per-session id so multiple saves in this coding session group under one context.
|
|
851
|
+
conversationKey: this.sessionId,
|
|
852
|
+
passthrough: parsed.passthrough || false,
|
|
853
|
+
triggerMessage: parsed.triggerMessage ||
|
|
854
|
+
lastUserMessageFromMessages(parsed.messages) ||
|
|
855
|
+
lastUserMessageFromConversationText(parsed.conversation),
|
|
856
|
+
triggerMessageRole: parsed.triggerMessageRole || "user",
|
|
857
|
+
}, config);
|
|
858
|
+
return response.data;
|
|
859
|
+
}
|
|
860
|
+
catch (error) {
|
|
861
|
+
if (axios.isAxiosError(error) && error.response?.status === 429) {
|
|
862
|
+
const data = error.response.data;
|
|
863
|
+
if (data?.error === "MEMORY_PROCESSING_QUOTA_EXCEEDED") {
|
|
864
|
+
const message = typeof data.message === "string"
|
|
865
|
+
? data.message
|
|
866
|
+
: "Weekly memory-processing limit reached. This conversation was not saved.";
|
|
867
|
+
const upgrade = typeof data.upgradeUrl === "string" ? ` Upgrade: ${data.upgradeUrl}` : "";
|
|
868
|
+
throw new McpError(ErrorCode.InvalidRequest, `${message}${upgrade}`);
|
|
869
|
+
}
|
|
870
|
+
}
|
|
871
|
+
throw error;
|
|
872
|
+
}
|
|
851
873
|
}
|
|
852
874
|
async deleteMemory(args) {
|
|
853
875
|
const parsed = deleteMemorySchema.parse(args ?? {});
|
|
@@ -900,6 +922,7 @@ class EchoMemApiClient {
|
|
|
900
922
|
startDate: parsed.startDate,
|
|
901
923
|
endDate: parsed.endDate,
|
|
902
924
|
limit: parsed.limit,
|
|
925
|
+
requestId: randomUUID(),
|
|
903
926
|
});
|
|
904
927
|
return enc.enabled ? await this.decryptResult(response.data, enc.key) : response.data;
|
|
905
928
|
}
|
|
@@ -918,6 +941,7 @@ class EchoMemApiClient {
|
|
|
918
941
|
const response = await this.axios.post("/api/extension/memories/keywords", {
|
|
919
942
|
keywords: parsed.keywords,
|
|
920
943
|
limit: parsed.limit,
|
|
944
|
+
requestId: randomUUID(),
|
|
921
945
|
});
|
|
922
946
|
return enc.enabled ? await this.decryptResult(response.data, enc.key) : response.data;
|
|
923
947
|
}
|
|
@@ -1192,9 +1216,13 @@ class EchoMemMCPServer {
|
|
|
1192
1216
|
content: [
|
|
1193
1217
|
{
|
|
1194
1218
|
type: "text",
|
|
1195
|
-
text:
|
|
1196
|
-
|
|
1197
|
-
|
|
1219
|
+
text: [
|
|
1220
|
+
"๐ EchoMem vault is locked.",
|
|
1221
|
+
"This encrypted account has no usable local decryption key. Once unlocked, this trusted device stays unlocked until you explicitly lock it or log out.",
|
|
1222
|
+
"Action required from the user: open Terminal and run `echomem-mcp unlock` yourself. Do not have the agent run this interactive command and do not send your passphrase in chat.",
|
|
1223
|
+
"At `Vault passphrase (typing is hidden):`, type the passphrase and press Return. No characters will appear while you type; that is expected.",
|
|
1224
|
+
"After the success message, retry this EchoMem action in the current session โ no editor restart is needed.",
|
|
1225
|
+
].join("\n"),
|
|
1198
1226
|
},
|
|
1199
1227
|
],
|
|
1200
1228
|
};
|
|
@@ -1362,7 +1390,7 @@ Details: ${m.details || "N/A"}`)
|
|
|
1362
1390
|
rec.conversation_chars = text.length;
|
|
1363
1391
|
rec.save_source = typeof a?.source === "string" ? a.source : sourceFallback;
|
|
1364
1392
|
}
|
|
1365
|
-
const { success, memoriesExtracted, extractedMemories, contextId, capsuleId, passthrough: isPassthrough, error } = await this.client.saveConversation(enrichedArgs);
|
|
1393
|
+
const { success, memoriesExtracted, memoriesDiscarded, extractedMemories, contextId, capsuleId, passthrough: isPassthrough, error } = await this.client.saveConversation(enrichedArgs);
|
|
1366
1394
|
if (!success)
|
|
1367
1395
|
throw new Error(`EchoMem API Error: ${error}`);
|
|
1368
1396
|
if (rec)
|
|
@@ -1374,7 +1402,7 @@ Details: ${m.details || "N/A"}`)
|
|
|
1374
1402
|
typeof capsuleId === "string" && capsuleId ? `Capsule ID: ${capsuleId}` : "",
|
|
1375
1403
|
typeof contextId === "string" && contextId ? `Context: ${contextId}` : "",
|
|
1376
1404
|
"",
|
|
1377
|
-
`To reload this capsule in a fresh session
|
|
1405
|
+
`To reload this capsule in a fresh session: get_memories_by_context({ contextId: "${contextId}" })`,
|
|
1378
1406
|
].filter(Boolean).join("\n");
|
|
1379
1407
|
return { content: [{ type: "text", text }] };
|
|
1380
1408
|
}
|
|
@@ -1396,9 +1424,12 @@ Details: ${m.details || "N/A"}`)
|
|
|
1396
1424
|
.join("\n\n");
|
|
1397
1425
|
const text = [
|
|
1398
1426
|
`Successfully ingested conversation. Extracted ${memoriesExtracted} memory distinct events.`,
|
|
1427
|
+
typeof memoriesDiscarded === "number" && memoriesDiscarded > 0
|
|
1428
|
+
? `${memoriesDiscarded} additional memories were not stored because your active-memory limit was reached.`
|
|
1429
|
+
: "",
|
|
1399
1430
|
list,
|
|
1400
1431
|
saved.length
|
|
1401
|
-
? `Verify these captured the key facts.
|
|
1432
|
+
? `Verify these captured the key facts. Re-fetch this exact batch later by searching the ids above${typeof contextId === "string" && contextId ? ` (context ${contextId})` : ""}.`
|
|
1402
1433
|
: "",
|
|
1403
1434
|
].filter(Boolean).join("\n\n");
|
|
1404
1435
|
return { content: [{ type: "text", text }] };
|
package/dist/keystore.js
CHANGED
|
@@ -10,13 +10,13 @@
|
|
|
10
10
|
* Storage posture: a 0600 file in the user's home โ the same on-disk trust level as the extension's
|
|
11
11
|
* `chrome.storage.local`. The OS keychain (spec ยง8's ideal) is a drop-in for `read`/`write` later;
|
|
12
12
|
* it is deliberately deferred because `keytar` is a native build that complicates `npx` distribution.
|
|
13
|
-
* The
|
|
14
|
-
*
|
|
13
|
+
* The MCP bridge uses an explicit trusted-device lifecycle: a verified encryption key remains
|
|
14
|
+
* available until the user runs `lock`/`logout` or removes the local credentials. Interactive app
|
|
15
|
+
* session TTLs are intentionally separate from this device-bound developer workflow.
|
|
15
16
|
*/
|
|
16
17
|
import fs from "node:fs";
|
|
17
18
|
import os from "node:os";
|
|
18
19
|
import path from "node:path";
|
|
19
|
-
export const DEFAULT_KEY_TTL_MS = 7 * 24 * 60 * 60 * 1000;
|
|
20
20
|
/** The bridge's local config dir (`~/.echomem` or `$ECHO_CONFIG_DIR`) โ home for credentials + telemetry. */
|
|
21
21
|
export function echoConfigDir() {
|
|
22
22
|
return process.env.ECHO_CONFIG_DIR || path.join(os.homedir(), ".echomem");
|
|
@@ -55,29 +55,19 @@ export class KeyStore {
|
|
|
55
55
|
getToken() {
|
|
56
56
|
return process.env.ECHO_API_TOKEN || readFileCreds().token;
|
|
57
57
|
}
|
|
58
|
-
/** Encryption key (base64), or undefined if absent
|
|
58
|
+
/** Encryption key (base64), or undefined if absent. Env wins. */
|
|
59
59
|
getKey() {
|
|
60
60
|
if (process.env.ECHO_ENCRYPTION_KEY)
|
|
61
61
|
return process.env.ECHO_ENCRYPTION_KEY;
|
|
62
|
-
|
|
63
|
-
if (!creds.key)
|
|
64
|
-
return undefined;
|
|
65
|
-
if (creds.keyExpiresAt && Date.now() > creds.keyExpiresAt)
|
|
66
|
-
return undefined;
|
|
67
|
-
return creds.key;
|
|
68
|
-
}
|
|
69
|
-
/** True when a key is present but past its TTL โ used to tell "locked (expired)" from "never set". */
|
|
70
|
-
isKeyExpired() {
|
|
71
|
-
if (process.env.ECHO_ENCRYPTION_KEY)
|
|
72
|
-
return false;
|
|
73
|
-
const creds = readFileCreds();
|
|
74
|
-
return !!(creds.key && creds.keyExpiresAt && Date.now() > creds.keyExpiresAt);
|
|
62
|
+
return readFileCreds().key;
|
|
75
63
|
}
|
|
76
64
|
saveToken(token) {
|
|
77
65
|
writeFileCreds({ ...readFileCreds(), token });
|
|
78
66
|
}
|
|
79
|
-
saveKey(keyBase64
|
|
80
|
-
|
|
67
|
+
saveKey(keyBase64) {
|
|
68
|
+
const creds = readFileCreds();
|
|
69
|
+
delete creds.keyExpiresAt;
|
|
70
|
+
writeFileCreds({ ...creds, key: keyBase64 });
|
|
81
71
|
}
|
|
82
72
|
clearKey() {
|
|
83
73
|
const creds = readFileCreds();
|