@echomem/mcp 1.4.14 → 1.4.15
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/dist/hud/diagnostics.js +139 -0
- package/dist/hud/server.js +19 -0
- package/dist/hud/web.js +114 -0
- package/dist/index.js +95 -32
- package/dist/setup-page/client-core.js +2 -0
- package/dist/setup-page/client-extraction.js +80 -4
- package/dist/setup-page/client-lifecycle.js +71 -5
- package/dist/setup-page/document.js +1 -1
- package/dist/setup-page/styles-extraction.js +142 -0
- package/dist/setup.js +108 -40
- package/dist/v1-contract.js +8 -7
- package/package.json +1 -1
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { randomUUID } from "node:crypto";
|
|
4
|
+
import { echoConfigDir, KeyStore } from "../keystore.js";
|
|
5
|
+
const API_BASE = (process.env.ECHO_API_BASE_URL || "https://echo-mem-chrome.vercel.app").replace(/\/$/, "");
|
|
6
|
+
const EVENTS_TAIL_LINES = 400;
|
|
7
|
+
function record(value) {
|
|
8
|
+
return typeof value === "object" && value !== null && !Array.isArray(value)
|
|
9
|
+
? value
|
|
10
|
+
: null;
|
|
11
|
+
}
|
|
12
|
+
function number(value) {
|
|
13
|
+
return typeof value === "number" && Number.isFinite(value) ? value : undefined;
|
|
14
|
+
}
|
|
15
|
+
function text(value) {
|
|
16
|
+
return typeof value === "string" && value.trim() ? value.trim() : undefined;
|
|
17
|
+
}
|
|
18
|
+
function briefError(error) {
|
|
19
|
+
const message = error instanceof Error ? error.message : String(error || "Connection failed");
|
|
20
|
+
return message
|
|
21
|
+
.replace(/Bearer\s+[A-Za-z0-9._-]+/gi, "Bearer [redacted]")
|
|
22
|
+
.replace(/ec_[A-Za-z0-9._-]+/g, "ec_[redacted]")
|
|
23
|
+
.replace(/\s+/g, " ")
|
|
24
|
+
.slice(0, 180);
|
|
25
|
+
}
|
|
26
|
+
function normalizePlan(value) {
|
|
27
|
+
const plan = text(value)?.toLowerCase();
|
|
28
|
+
return plan && ["free", "pro", "power", "team", "enterprise"].includes(plan) ? plan : undefined;
|
|
29
|
+
}
|
|
30
|
+
/** Parse the account bootstrap contract without importing app-only code into the npm package. */
|
|
31
|
+
export function accountDiagnosticsFromBootstrap(payload) {
|
|
32
|
+
const root = record(payload) ?? {};
|
|
33
|
+
const entitlements = record(root.entitlements) ?? {};
|
|
34
|
+
const usage = record(root.usage) ?? {};
|
|
35
|
+
const activation = record(root.activation) ?? {};
|
|
36
|
+
const limit = number(entitlements.deepSearchPerMonth);
|
|
37
|
+
const searchesThisMonth = number(activation.memorySearchMonthToDateCount);
|
|
38
|
+
const searchesRemaining = limit === undefined || searchesThisMonth === undefined
|
|
39
|
+
? undefined
|
|
40
|
+
: limit < 0
|
|
41
|
+
? null
|
|
42
|
+
: Math.max(0, limit - searchesThisMonth);
|
|
43
|
+
return {
|
|
44
|
+
plan: normalizePlan(root.plan),
|
|
45
|
+
deepSearchPerMonth: limit,
|
|
46
|
+
searchesThisMonth,
|
|
47
|
+
searchesRemaining,
|
|
48
|
+
memoryCount: number(usage.memories),
|
|
49
|
+
sourceCount: number(usage.sources),
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
/** Read only the latest memory-search trace; never return a query, token, or memory content. */
|
|
53
|
+
export function latestSearchTraceFromEvents(lines) {
|
|
54
|
+
for (let index = lines.length - 1; index >= 0; index -= 1) {
|
|
55
|
+
try {
|
|
56
|
+
const event = record(JSON.parse(lines[index] || ""));
|
|
57
|
+
if (!event || event.type !== "tool_call" || event.tool !== "search_memories")
|
|
58
|
+
continue;
|
|
59
|
+
const at = text(event.ts);
|
|
60
|
+
if (!at)
|
|
61
|
+
continue;
|
|
62
|
+
return {
|
|
63
|
+
at,
|
|
64
|
+
ok: typeof event.ok === "boolean" ? event.ok : undefined,
|
|
65
|
+
requestId: text(event.request_id),
|
|
66
|
+
endpoint: text(event.endpoint),
|
|
67
|
+
httpStatus: number(event.http_status),
|
|
68
|
+
errorCode: text(event.error_code),
|
|
69
|
+
errorKind: text(event.error_kind),
|
|
70
|
+
latencyMs: number(event.latency_ms),
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
catch {
|
|
74
|
+
// A concurrently appended partial line or old malformed record is safely ignored.
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
return undefined;
|
|
78
|
+
}
|
|
79
|
+
function loadLatestSearchTrace() {
|
|
80
|
+
try {
|
|
81
|
+
const file = path.join(echoConfigDir(), "events.jsonl");
|
|
82
|
+
return latestSearchTraceFromEvents(fs.readFileSync(file, "utf8").split("\n").slice(-EVENTS_TAIL_LINES));
|
|
83
|
+
}
|
|
84
|
+
catch {
|
|
85
|
+
return undefined;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* Read-only, local HUD diagnostics. Credentials are used only in the Authorization header to
|
|
90
|
+
* EchoMem's authenticated account endpoint; this never sends transcripts, queries, or memories.
|
|
91
|
+
*/
|
|
92
|
+
export async function loadHudDiagnostics() {
|
|
93
|
+
const updatedAt = new Date().toISOString();
|
|
94
|
+
const lastSearch = loadLatestSearchTrace();
|
|
95
|
+
const token = new KeyStore().getToken();
|
|
96
|
+
if (!token) {
|
|
97
|
+
return { updatedAt, account: { state: "not_logged_in" }, accountRequest: {}, lastSearch };
|
|
98
|
+
}
|
|
99
|
+
const requestId = `hud-account-${randomUUID()}`;
|
|
100
|
+
const startedAt = Date.now();
|
|
101
|
+
try {
|
|
102
|
+
const response = await fetch(`${API_BASE}/api/extension/account/bootstrap`, {
|
|
103
|
+
headers: {
|
|
104
|
+
Authorization: `Bearer ${token}`,
|
|
105
|
+
"X-EchoMem-Request-Id": requestId,
|
|
106
|
+
},
|
|
107
|
+
});
|
|
108
|
+
const payload = await response.json().catch(() => ({}));
|
|
109
|
+
const telemetry = record(payload)?.telemetry;
|
|
110
|
+
const echoedRequestId = text(record(telemetry)?.requestId) ?? requestId;
|
|
111
|
+
const accountRequest = {
|
|
112
|
+
requestId: echoedRequestId,
|
|
113
|
+
httpStatus: response.status,
|
|
114
|
+
elapsedMs: Date.now() - startedAt,
|
|
115
|
+
};
|
|
116
|
+
if (!response.ok) {
|
|
117
|
+
return {
|
|
118
|
+
updatedAt,
|
|
119
|
+
account: { state: "error", error: `Account check returned HTTP ${response.status}` },
|
|
120
|
+
accountRequest,
|
|
121
|
+
lastSearch,
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
return {
|
|
125
|
+
updatedAt,
|
|
126
|
+
account: { state: "connected", ...accountDiagnosticsFromBootstrap(payload) },
|
|
127
|
+
accountRequest,
|
|
128
|
+
lastSearch,
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
catch (error) {
|
|
132
|
+
return {
|
|
133
|
+
updatedAt,
|
|
134
|
+
account: { state: "error", error: briefError(error) },
|
|
135
|
+
accountRequest: { requestId, elapsedMs: Date.now() - startedAt },
|
|
136
|
+
lastSearch,
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
}
|
package/dist/hud/server.js
CHANGED
|
@@ -6,6 +6,7 @@ import { fileURLToPath } from "node:url";
|
|
|
6
6
|
import { HudMonitor } from "./monitor.js";
|
|
7
7
|
import { HUD_HTML } from "./web.js";
|
|
8
8
|
import { buildCapsuleText } from "./capsule.js";
|
|
9
|
+
import { loadHudDiagnostics } from "./diagnostics.js";
|
|
9
10
|
import { homePath, newestFile, walkFiles } from "./fs.js";
|
|
10
11
|
import { KeyStore } from "../keystore.js";
|
|
11
12
|
import { assembleCodex, assembleClaude } from "../migrate.js";
|
|
@@ -42,6 +43,24 @@ export async function createHudServer(opts = {}) {
|
|
|
42
43
|
res.end(JSON.stringify(latest, null, 2));
|
|
43
44
|
return;
|
|
44
45
|
}
|
|
46
|
+
if (url.pathname === "/diagnostics") {
|
|
47
|
+
loadHudDiagnostics().then((diagnostics) => {
|
|
48
|
+
if (res.writableEnded)
|
|
49
|
+
return;
|
|
50
|
+
res.writeHead(200, { "content-type": "application/json", "cache-control": "no-store" });
|
|
51
|
+
res.end(JSON.stringify(diagnostics));
|
|
52
|
+
}).catch(() => {
|
|
53
|
+
if (res.writableEnded)
|
|
54
|
+
return;
|
|
55
|
+
res.writeHead(200, { "content-type": "application/json", "cache-control": "no-store" });
|
|
56
|
+
res.end(JSON.stringify({
|
|
57
|
+
updatedAt: new Date().toISOString(),
|
|
58
|
+
account: { state: "error", error: "Diagnostics unavailable" },
|
|
59
|
+
accountRequest: {},
|
|
60
|
+
}));
|
|
61
|
+
});
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
45
64
|
if (url.pathname === "/select") {
|
|
46
65
|
const id = url.searchParams.get("id");
|
|
47
66
|
monitor.pin(id && id !== "auto" ? id : null);
|
package/dist/hud/web.js
CHANGED
|
@@ -217,6 +217,32 @@ export const HUD_HTML = String.raw `<!doctype html>
|
|
|
217
217
|
cursor: grab;
|
|
218
218
|
}
|
|
219
219
|
#hud.open .details { display: block; }
|
|
220
|
+
.diagnostics {
|
|
221
|
+
margin: 10px 0 0;
|
|
222
|
+
padding: 10px 11px;
|
|
223
|
+
border: 1px solid rgba(26, 58, 143, 0.18);
|
|
224
|
+
border-radius: 10px;
|
|
225
|
+
background: rgba(255, 255, 255, 0.72);
|
|
226
|
+
}
|
|
227
|
+
.diagnostics-head { display: flex; align-items: center; justify-content: space-between; gap: 8px; }
|
|
228
|
+
.diagnostics-title { font-size: 10px; font-weight: 820; letter-spacing: .02em; color: var(--blue); }
|
|
229
|
+
.diagnostics-refresh {
|
|
230
|
+
border: 0;
|
|
231
|
+
padding: 0;
|
|
232
|
+
background: transparent;
|
|
233
|
+
color: var(--faint);
|
|
234
|
+
font-family: inherit;
|
|
235
|
+
font-size: 10px;
|
|
236
|
+
font-weight: 730;
|
|
237
|
+
cursor: pointer;
|
|
238
|
+
}
|
|
239
|
+
.diagnostics-refresh:hover { color: var(--blue); }
|
|
240
|
+
.diagnostics-body { display: grid; gap: 5px; margin-top: 7px; }
|
|
241
|
+
.diagnostics-line { display: flex; align-items: baseline; justify-content: space-between; gap: 10px; font-size: 10px; line-height: 1.25; }
|
|
242
|
+
.diagnostics-k { flex: 0 0 auto; color: var(--faint); font-weight: 650; }
|
|
243
|
+
.diagnostics-v { min-width: 0; overflow: hidden; text-align: right; text-overflow: ellipsis; white-space: nowrap; color: var(--muted); font-family: "JetBrains Mono", ui-monospace, monospace; font-size: 9.5px; font-weight: 650; }
|
|
244
|
+
.diagnostics-v.bad { color: var(--brick); }
|
|
245
|
+
.diagnostics-v.wrap { white-space: normal; overflow-wrap: anywhere; }
|
|
220
246
|
/* The current session is the one object every number and action belongs to — one card. */
|
|
221
247
|
.now-card {
|
|
222
248
|
border: 1px solid rgba(26, 58, 143, 0.24);
|
|
@@ -690,6 +716,10 @@ export const HUD_HTML = String.raw `<!doctype html>
|
|
|
690
716
|
</div>
|
|
691
717
|
</div>
|
|
692
718
|
</div>
|
|
719
|
+
<section class="diagnostics" aria-label="Memory access diagnostics">
|
|
720
|
+
<div class="diagnostics-head"><span class="diagnostics-title">Memory access</span><button class="diagnostics-refresh" id="diagnosticsrefresh" type="button">Refresh</button></div>
|
|
721
|
+
<div class="diagnostics-body" id="diagnosticsbody"><span class="diagnostics-v">Open this panel to check account status.</span></div>
|
|
722
|
+
</section>
|
|
693
723
|
<div class="tabs" id="tabs"></div>
|
|
694
724
|
<div class="meta-row"><span class="meta-line" id="metaline"></span><button class="switchbtn agent-switch" id="switchbtn" title="Switch agent" style="display:none" type="button"></button></div>
|
|
695
725
|
<div class="path" id="path"></div>
|
|
@@ -730,6 +760,8 @@ export const HUD_HTML = String.raw `<!doctype html>
|
|
|
730
760
|
const switchBtn = document.getElementById("switchbtn");
|
|
731
761
|
const revealEl = document.getElementById("reveal");
|
|
732
762
|
const missing = document.getElementById("missing");
|
|
763
|
+
const diagnosticsBody = document.getElementById("diagnosticsbody");
|
|
764
|
+
const diagnosticsRefresh = document.getElementById("diagnosticsrefresh");
|
|
733
765
|
let prevTurnState = null;
|
|
734
766
|
let lastCapsule = "";
|
|
735
767
|
let renewResultSessionKey = "";
|
|
@@ -741,6 +773,8 @@ export const HUD_HTML = String.raw `<!doctype html>
|
|
|
741
773
|
let renewStartMs = 0;
|
|
742
774
|
let renewEta = null;
|
|
743
775
|
let renewEstimateSeq = 0;
|
|
776
|
+
let diagnosticsLoaded = false;
|
|
777
|
+
let diagnosticsInFlight = false;
|
|
744
778
|
const CHECKPOINT_CACHE_KEY = "echomemHudCheckpointCacheV2";
|
|
745
779
|
const CHECKPOINT_CACHE_LIMIT = 24;
|
|
746
780
|
const CHECKPOINT_CACHE_TTL_MS = 7 * 24 * 60 * 60 * 1000;
|
|
@@ -753,6 +787,85 @@ export const HUD_HTML = String.raw `<!doctype html>
|
|
|
753
787
|
// viewing", so the user can always point the HUD at the right session. Flip false to show the viewer.
|
|
754
788
|
const DEMO_CLEAN = true;
|
|
755
789
|
|
|
790
|
+
function diagnosticsLine(label, value, bad, wrap) {
|
|
791
|
+
return '<span class="diagnostics-line"><span class="diagnostics-k">' + esc(label) + '</span><span class="diagnostics-v' + (bad ? ' bad' : '') + (wrap ? ' wrap' : '') + '">' + esc(value) + '</span></span>';
|
|
792
|
+
}
|
|
793
|
+
|
|
794
|
+
function requestLabel(requestId) {
|
|
795
|
+
return requestId ? requestId : "not available";
|
|
796
|
+
}
|
|
797
|
+
|
|
798
|
+
function planLabel(plan) {
|
|
799
|
+
if (!plan) return "connected";
|
|
800
|
+
return plan.slice(0, 1).toUpperCase() + plan.slice(1);
|
|
801
|
+
}
|
|
802
|
+
|
|
803
|
+
function renderDiagnostics(data) {
|
|
804
|
+
if (!diagnosticsBody) return;
|
|
805
|
+
const account = (data && data.account) || {};
|
|
806
|
+
const accountRequest = (data && data.accountRequest) || {};
|
|
807
|
+
const lines = [];
|
|
808
|
+
if (account.state === "connected") {
|
|
809
|
+
lines.push(diagnosticsLine("account", planLabel(account.plan) + " · authenticated"));
|
|
810
|
+
if (typeof account.searchesThisMonth === "number" && typeof account.deepSearchPerMonth === "number") {
|
|
811
|
+
const limit = account.deepSearchPerMonth < 0 ? "Unlimited" : String(account.deepSearchPerMonth);
|
|
812
|
+
const remaining = account.searchesRemaining === null ? "" : (typeof account.searchesRemaining === "number" ? " · " + account.searchesRemaining + " left" : "");
|
|
813
|
+
lines.push(diagnosticsLine("searches", account.searchesThisMonth + " / " + limit + " UTC month" + remaining));
|
|
814
|
+
} else {
|
|
815
|
+
lines.push(diagnosticsLine("searches", "month-to-date not reported"));
|
|
816
|
+
}
|
|
817
|
+
} else if (account.state === "not_logged_in") {
|
|
818
|
+
lines.push(diagnosticsLine("account", "not logged in", true));
|
|
819
|
+
} else {
|
|
820
|
+
lines.push(diagnosticsLine("account", account.error || "unavailable", true));
|
|
821
|
+
}
|
|
822
|
+
const accountStatus = typeof accountRequest.httpStatus === "number"
|
|
823
|
+
? "HTTP " + accountRequest.httpStatus + (typeof accountRequest.elapsedMs === "number" ? " · " + accountRequest.elapsedMs + "ms" : "")
|
|
824
|
+
: "not sent";
|
|
825
|
+
lines.push(diagnosticsLine("account check", accountStatus, account.state === "error"));
|
|
826
|
+
if (accountRequest.requestId) lines.push(diagnosticsLine("account request", requestLabel(accountRequest.requestId), false, true));
|
|
827
|
+
|
|
828
|
+
const last = data && data.lastSearch;
|
|
829
|
+
if (!last) {
|
|
830
|
+
lines.push(diagnosticsLine("last search", "no local trace yet"));
|
|
831
|
+
} else {
|
|
832
|
+
const outcome = last.ok === false ? "failed" : last.ok === true ? "succeeded" : "recorded";
|
|
833
|
+
const status = typeof last.httpStatus === "number" ? "HTTP " + last.httpStatus : outcome;
|
|
834
|
+
const errorLabel = last.errorCode || (last.errorKind && last.errorKind !== "none" ? last.errorKind : "");
|
|
835
|
+
const detail = [status, errorLabel].filter(Boolean).join(" · ");
|
|
836
|
+
lines.push(diagnosticsLine("last search", detail || outcome, last.ok === false));
|
|
837
|
+
if (last.requestId) lines.push(diagnosticsLine("search request", requestLabel(last.requestId), last.ok === false, true));
|
|
838
|
+
}
|
|
839
|
+
diagnosticsBody.innerHTML = lines.join("");
|
|
840
|
+
diagnosticsLoaded = true;
|
|
841
|
+
syncHeight();
|
|
842
|
+
}
|
|
843
|
+
|
|
844
|
+
async function refreshDiagnostics() {
|
|
845
|
+
if (!diagnosticsBody || diagnosticsInFlight) return;
|
|
846
|
+
diagnosticsInFlight = true;
|
|
847
|
+
diagnosticsBody.innerHTML = diagnosticsLine("memory access", "checking…");
|
|
848
|
+
if (diagnosticsRefresh) diagnosticsRefresh.disabled = true;
|
|
849
|
+
try {
|
|
850
|
+
const response = await fetch("/diagnostics");
|
|
851
|
+
renderDiagnostics(await response.json());
|
|
852
|
+
} catch (_) {
|
|
853
|
+
renderDiagnostics({ account: { state: "error", error: "local diagnostic fetch failed" }, accountRequest: {} });
|
|
854
|
+
} finally {
|
|
855
|
+
diagnosticsInFlight = false;
|
|
856
|
+
if (diagnosticsRefresh) diagnosticsRefresh.disabled = false;
|
|
857
|
+
}
|
|
858
|
+
}
|
|
859
|
+
|
|
860
|
+
function maybeLoadDiagnostics() {
|
|
861
|
+
if (!diagnosticsLoaded && !diagnosticsInFlight) refreshDiagnostics();
|
|
862
|
+
}
|
|
863
|
+
|
|
864
|
+
if (diagnosticsRefresh) diagnosticsRefresh.addEventListener("click", (event) => {
|
|
865
|
+
event.stopPropagation();
|
|
866
|
+
refreshDiagnostics();
|
|
867
|
+
});
|
|
868
|
+
|
|
756
869
|
// Mini mode: resting state is a small pill; opening the panel shows the full layout, closing
|
|
757
870
|
// returns to the pill. Preference persists (localStorage here, window size in main's bounds file).
|
|
758
871
|
const miniBtn = document.getElementById("minibtn");
|
|
@@ -889,6 +1002,7 @@ export const HUD_HTML = String.raw `<!doctype html>
|
|
|
889
1002
|
}
|
|
890
1003
|
if (event.target instanceof HTMLElement && (event.target.closest(".switchbtn") || event.target.closest(".cornerbtn"))) return;
|
|
891
1004
|
hud.classList.toggle("open");
|
|
1005
|
+
if (hud.classList.contains("open")) maybeLoadDiagnostics();
|
|
892
1006
|
syncHeight();
|
|
893
1007
|
});
|
|
894
1008
|
|
package/dist/index.js
CHANGED
|
@@ -105,6 +105,11 @@ function readNumber(record, key) {
|
|
|
105
105
|
const value = record[key];
|
|
106
106
|
return typeof value === "number" && Number.isFinite(value) ? value : undefined;
|
|
107
107
|
}
|
|
108
|
+
function errorCodeFrom(value) {
|
|
109
|
+
if (!isRecord(value))
|
|
110
|
+
return undefined;
|
|
111
|
+
return readString(value, "code") || readString(value, "errorCode");
|
|
112
|
+
}
|
|
108
113
|
function compactOneLine(value, maxLength) {
|
|
109
114
|
if (!value)
|
|
110
115
|
return undefined;
|
|
@@ -569,30 +574,59 @@ class EchoMemApiClient {
|
|
|
569
574
|
throw error;
|
|
570
575
|
}
|
|
571
576
|
}
|
|
572
|
-
async searchMemoriesRaw(query, opts, enc, retrievalOnly = false) {
|
|
573
|
-
const
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
577
|
+
async searchMemoriesRaw(query, opts, enc, retrievalOnly = false, trace) {
|
|
578
|
+
const endpoint = "/api/extension/memories/search";
|
|
579
|
+
let response;
|
|
580
|
+
try {
|
|
581
|
+
response = await this.axios.post(endpoint, {
|
|
582
|
+
query,
|
|
583
|
+
k: opts.limit,
|
|
584
|
+
similarityThreshold: opts.threshold,
|
|
585
|
+
timeFrameDays: opts.timeFrameDays,
|
|
586
|
+
requestId: opts.requestId,
|
|
587
|
+
sessionId: this.sessionId,
|
|
588
|
+
});
|
|
589
|
+
trace?.({ requestId: opts.requestId, endpoint, httpStatus: response.status });
|
|
590
|
+
}
|
|
591
|
+
catch (error) {
|
|
592
|
+
trace?.({
|
|
593
|
+
requestId: opts.requestId,
|
|
594
|
+
endpoint,
|
|
595
|
+
httpStatus: axios.isAxiosError(error) ? error.response?.status : undefined,
|
|
596
|
+
errorCode: axios.isAxiosError(error) ? errorCodeFrom(error.response?.data) : undefined,
|
|
597
|
+
});
|
|
598
|
+
throw error;
|
|
599
|
+
}
|
|
581
600
|
const data = enc.enabled ? await this.decryptResult(response.data, enc.key) : response.data;
|
|
582
601
|
return retrievalOnly && isRecord(data)
|
|
583
602
|
? { ...data, retrievalOnly: true, tuned: false }
|
|
584
603
|
: data;
|
|
585
604
|
}
|
|
586
|
-
async searchMemoriesRetrieved(query, opts, enc) {
|
|
587
|
-
const
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
605
|
+
async searchMemoriesRetrieved(query, opts, enc, trace) {
|
|
606
|
+
const endpoint = "/api/extension/memories/deep-search/candidates";
|
|
607
|
+
let response;
|
|
608
|
+
try {
|
|
609
|
+
response = await this.axios.post(endpoint, {
|
|
610
|
+
query,
|
|
611
|
+
requestId: opts.requestId,
|
|
612
|
+
logicalRequestId: opts.requestId,
|
|
613
|
+
sessionId: this.sessionId,
|
|
614
|
+
userTimeZone: Intl.DateTimeFormat().resolvedOptions().timeZone || "UTC",
|
|
615
|
+
});
|
|
616
|
+
trace?.({ requestId: opts.requestId, endpoint, httpStatus: response.status });
|
|
617
|
+
}
|
|
618
|
+
catch (error) {
|
|
619
|
+
trace?.({
|
|
620
|
+
requestId: opts.requestId,
|
|
621
|
+
endpoint,
|
|
622
|
+
httpStatus: axios.isAxiosError(error) ? error.response?.status : undefined,
|
|
623
|
+
errorCode: axios.isAxiosError(error) ? errorCodeFrom(error.response?.data) : undefined,
|
|
624
|
+
});
|
|
625
|
+
throw error;
|
|
626
|
+
}
|
|
594
627
|
const data = response.data ?? {};
|
|
595
628
|
if (data.success === false) {
|
|
629
|
+
trace?.({ requestId: opts.requestId, endpoint, httpStatus: response.status, errorCode: errorCodeFrom(data) });
|
|
596
630
|
throw new Error(`deep-search candidates proxy error: ${data.error || "unknown"}`);
|
|
597
631
|
}
|
|
598
632
|
const rawCandidates = Array.isArray(data.personalCandidates) ? data.personalCandidates : [];
|
|
@@ -648,16 +682,31 @@ class EchoMemApiClient {
|
|
|
648
682
|
// never hits dg-web directly, so no new auth is needed and no other caller is
|
|
649
683
|
// disrupted. NOTE: encrypted users still get server-side plaintext only; the
|
|
650
684
|
// phase-1 + bridge-local-decrypt + phase-2 flow (spec §11) is the remaining work.
|
|
651
|
-
async searchMemoriesTuned(query, requestId) {
|
|
652
|
-
const
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
685
|
+
async searchMemoriesTuned(query, requestId, trace) {
|
|
686
|
+
const endpoint = "/api/extension/memories/deep-search";
|
|
687
|
+
let response;
|
|
688
|
+
try {
|
|
689
|
+
response = await this.axios.post(endpoint, {
|
|
690
|
+
query,
|
|
691
|
+
requestId,
|
|
692
|
+
logicalRequestId: requestId,
|
|
693
|
+
sessionId: this.sessionId,
|
|
694
|
+
userTimeZone: Intl.DateTimeFormat().resolvedOptions().timeZone || "UTC",
|
|
695
|
+
});
|
|
696
|
+
trace?.({ requestId, endpoint, httpStatus: response.status });
|
|
697
|
+
}
|
|
698
|
+
catch (error) {
|
|
699
|
+
trace?.({
|
|
700
|
+
requestId,
|
|
701
|
+
endpoint,
|
|
702
|
+
httpStatus: axios.isAxiosError(error) ? error.response?.status : undefined,
|
|
703
|
+
errorCode: axios.isAxiosError(error) ? errorCodeFrom(error.response?.data) : undefined,
|
|
704
|
+
});
|
|
705
|
+
throw error;
|
|
706
|
+
}
|
|
659
707
|
const data = response.data ?? {};
|
|
660
708
|
if (data.success === false) {
|
|
709
|
+
trace?.({ requestId, endpoint, httpStatus: response.status, errorCode: errorCodeFrom(data) });
|
|
661
710
|
throw new Error(`deep-search proxy error: ${data.error || "unknown"}`);
|
|
662
711
|
}
|
|
663
712
|
const primary = (data.retrievedMemorySnapshot?.primary ?? []);
|
|
@@ -669,7 +718,7 @@ class EchoMemApiClient {
|
|
|
669
718
|
}));
|
|
670
719
|
return { success: true, tuned: true, answer: String(data.answer || data.response || "").trim(), memories };
|
|
671
720
|
}
|
|
672
|
-
async searchMemories(args) {
|
|
721
|
+
async searchMemories(args, trace) {
|
|
673
722
|
const parsed = searchMemoriesSchema.parse(args ?? {});
|
|
674
723
|
const query = parsed.query?.trim();
|
|
675
724
|
const limit = parsed.limit ?? parsed.k ?? 10;
|
|
@@ -695,17 +744,17 @@ class EchoMemApiClient {
|
|
|
695
744
|
requestId: randomUUID(),
|
|
696
745
|
};
|
|
697
746
|
if (!parsed.includeAnswer) {
|
|
698
|
-
return this.searchMemoriesRetrieved(query, searchOpts, enc);
|
|
747
|
+
return this.searchMemoriesRetrieved(query, searchOpts, enc, trace);
|
|
699
748
|
}
|
|
700
749
|
// Encrypted account: the server can't synthesize over plaintext it doesn't hold, so use the raw
|
|
701
750
|
// retriever (returns ciphertext) and decrypt locally — zero-knowledge preserved end-to-end.
|
|
702
751
|
if (enc.enabled) {
|
|
703
|
-
return await this.searchMemoriesRaw(query, searchOpts, enc);
|
|
752
|
+
return await this.searchMemoriesRaw(query, searchOpts, enc, false, trace);
|
|
704
753
|
}
|
|
705
754
|
// A synthesized answer is metered through the legacy route. Do not fall
|
|
706
755
|
// back to raw retrieval after a ledger outage: that would return a result
|
|
707
756
|
// without a durable usage fact.
|
|
708
|
-
return this.searchMemoriesTuned(query, searchOpts.requestId);
|
|
757
|
+
return this.searchMemoriesTuned(query, searchOpts.requestId, trace);
|
|
709
758
|
}
|
|
710
759
|
/** Decrypt the model-visible fields on a `{ memories: [...] }` response locally. */
|
|
711
760
|
async decryptResult(data, key) {
|
|
@@ -1129,6 +1178,13 @@ class EchoMemMCPServer {
|
|
|
1129
1178
|
map_injected: rec.map_injected,
|
|
1130
1179
|
encrypted_user: rec.encrypted,
|
|
1131
1180
|
extracted_memory_count: rec.memories_extracted,
|
|
1181
|
+
// Preserve only the safe correlation fields needed to join a
|
|
1182
|
+
// bridge-local failure to the central operational trace. Query
|
|
1183
|
+
// text and upstream bodies never leave the local event stream.
|
|
1184
|
+
request_id: rec.request_id,
|
|
1185
|
+
endpoint: rec.endpoint,
|
|
1186
|
+
http_status: rec.http_status,
|
|
1187
|
+
error_code: rec.error_code ?? (rec.error_kind === "none" ? undefined : rec.error_kind),
|
|
1132
1188
|
};
|
|
1133
1189
|
void this.client.trackMcpAnalyticsEvent(rec.ok ? "[MCP] EchoMem Tool Succeeded" : "[MCP] EchoMem Tool Failed", finalAnalytics, `${analyticsCallId}:generic-completed`);
|
|
1134
1190
|
void this.client.trackMcpAnalyticsEvent(toolEventName(canonicalName, rec.ok ? "Succeeded" : "Failed"), finalAnalytics, `${analyticsCallId}:completed`);
|
|
@@ -1142,7 +1198,14 @@ class EchoMemMCPServer {
|
|
|
1142
1198
|
rec.query_len = query.length;
|
|
1143
1199
|
rec.query_hash = hashText(query);
|
|
1144
1200
|
}
|
|
1145
|
-
const result = await this.client.searchMemories(args)
|
|
1201
|
+
const result = await this.client.searchMemories(args, (trace) => {
|
|
1202
|
+
if (!rec)
|
|
1203
|
+
return;
|
|
1204
|
+
rec.request_id = trace.requestId;
|
|
1205
|
+
rec.endpoint = trace.endpoint;
|
|
1206
|
+
rec.http_status = trace.httpStatus;
|
|
1207
|
+
rec.error_code = trace.errorCode;
|
|
1208
|
+
});
|
|
1146
1209
|
if (rec) {
|
|
1147
1210
|
// "Which memories were used": topic keys + delivered context size, for the comparison dataset.
|
|
1148
1211
|
const mems = Array.isArray(result?.memories) ? result.memories : [];
|
|
@@ -1249,7 +1312,7 @@ Details: ${m.details || "N/A"}`)
|
|
|
1249
1312
|
typeof capsuleId === "string" && capsuleId ? `Capsule ID: ${capsuleId}` : "",
|
|
1250
1313
|
typeof contextId === "string" && contextId ? `Context: ${contextId}` : "",
|
|
1251
1314
|
"",
|
|
1252
|
-
`To reload this capsule in a fresh session: get_memories_by_context({ contextId: "${contextId}" })`,
|
|
1315
|
+
`To reload this capsule in a fresh session with Pro/Power recall: get_memories_by_context({ contextId: "${contextId}" })`,
|
|
1253
1316
|
].filter(Boolean).join("\n");
|
|
1254
1317
|
return { content: [{ type: "text", text }] };
|
|
1255
1318
|
}
|
|
@@ -1273,7 +1336,7 @@ Details: ${m.details || "N/A"}`)
|
|
|
1273
1336
|
`Successfully ingested conversation. Extracted ${memoriesExtracted} memory distinct events.`,
|
|
1274
1337
|
list,
|
|
1275
1338
|
saved.length
|
|
1276
|
-
? `Verify these captured the key facts.
|
|
1339
|
+
? `Verify these captured the key facts. With Pro/Power recall, re-fetch this exact batch later by searching the ids above${typeof contextId === "string" && contextId ? ` (context ${contextId})` : ""}.`
|
|
1277
1340
|
: "",
|
|
1278
1341
|
].filter(Boolean).join("\n\n");
|
|
1279
1342
|
return { content: [{ type: "text", text }] };
|
|
@@ -21,6 +21,8 @@ export const SETUP_PAGE_CLIENT_CORE = String.raw ` var params = new URLSear
|
|
|
21
21
|
var reportMounted = false; // report shell (city iframe) mounted once; loading is an overlay on it, not a separate page
|
|
22
22
|
var authUrl = "";
|
|
23
23
|
var workspacePath = "";
|
|
24
|
+
var billingStatus = null;
|
|
25
|
+
var recallGateSkipped = false;
|
|
24
26
|
var authWindow = null;
|
|
25
27
|
var connectionPollStarted = false;
|
|
26
28
|
var statsPollStarted = false;
|
|
@@ -342,7 +342,7 @@ export const SETUP_PAGE_CLIENT_EXTRACTION = String.raw ` /* ---------- dash
|
|
|
342
342
|
'<div class="resultNumber"><strong>' + esc(number(extracted)) + '</strong><span>' + esc(memoryNoun) + '</span></div>') +
|
|
343
343
|
'</div>' +
|
|
344
344
|
'</div>' +
|
|
345
|
-
(done ?
|
|
345
|
+
(done ? recallGateBlock() : "") +
|
|
346
346
|
(done ? "" :
|
|
347
347
|
'<div class="memoryReceipt">' +
|
|
348
348
|
'<p class="receiptK">What just happened</p>' +
|
|
@@ -353,7 +353,7 @@ export const SETUP_PAGE_CLIENT_EXTRACTION = String.raw ` /* ---------- dash
|
|
|
353
353
|
'</div>') +
|
|
354
354
|
'</div>' +
|
|
355
355
|
(done ? hudTip() + doneCloseNote() : "");
|
|
356
|
-
|
|
356
|
+
wireRecallGate();
|
|
357
357
|
return;
|
|
358
358
|
}
|
|
359
359
|
setHead(preparing ? "Preparing" : "Extracting", preparing ? "Preparing" : "Extracting");
|
|
@@ -447,6 +447,82 @@ export const SETUP_PAGE_CLIENT_EXTRACTION = String.raw ` /* ---------- dash
|
|
|
447
447
|
setLaunchStatus(msg);
|
|
448
448
|
}
|
|
449
449
|
}
|
|
450
|
+
function paidRecallPlan(plan) {
|
|
451
|
+
return ["pro", "power", "team", "enterprise"].indexOf(String(plan || "").toLowerCase()) >= 0;
|
|
452
|
+
}
|
|
453
|
+
function recallPricingUrl() {
|
|
454
|
+
return (billingStatus && billingStatus.pricingUrl)
|
|
455
|
+
|| "https://echoknows.com/pricing?source=mcp_onboarding";
|
|
456
|
+
}
|
|
457
|
+
function recallGateUnlocked() {
|
|
458
|
+
return recallGateSkipped || paidRecallPlan(billingStatus && billingStatus.plan);
|
|
459
|
+
}
|
|
460
|
+
function recallGateBlock() {
|
|
461
|
+
var unlocked = recallGateUnlocked();
|
|
462
|
+
return '<div class="recallGateBlock">' +
|
|
463
|
+
'<section class="recallGate' + (unlocked ? " is-hidden" : "") + '" id="recallGate">' +
|
|
464
|
+
'<p class="recallEyebrow">Memory saved. Recall is next.</p>' +
|
|
465
|
+
'<h3>Choose your 14-day trial plan for Codex and Claude recall.</h3>' +
|
|
466
|
+
'<p>Saving conversations and extracting memory stay included. Compare Pro and Power, then choose the plan that should unlock <code>search_memories</code>, project recall, and the prompt below.</p>' +
|
|
467
|
+
'<div class="recallGateActions">' +
|
|
468
|
+
'<button type="button" class="primary" id="recallStartTrial">Compare plans</button>' +
|
|
469
|
+
'<button type="button" class="secondary" id="recallSkip">Skip for now</button>' +
|
|
470
|
+
'</div>' +
|
|
471
|
+
'<p class="recallFine">Card required for trial. Cancel anytime.</p>' +
|
|
472
|
+
'<p class="launchStatus" id="recallGateStatus"></p>' +
|
|
473
|
+
'</section>' +
|
|
474
|
+
'<div id="recallTryWrap" class="recallTryWrap' + (unlocked ? "" : " is-hidden") + '">' +
|
|
475
|
+
'<p id="recallSkipNote" class="recallSkipNote' + (recallGateSkipped ? "" : " is-hidden") + '">Saving will keep working. Search and recall require Pro or Power when this prompt calls <code>search_memories</code>.</p>' +
|
|
476
|
+
tryItCard() +
|
|
477
|
+
'</div>' +
|
|
478
|
+
'</div>';
|
|
479
|
+
}
|
|
480
|
+
function setRecallGateStatus(message) {
|
|
481
|
+
var node = document.getElementById("recallGateStatus");
|
|
482
|
+
if (node) node.textContent = message || "";
|
|
483
|
+
}
|
|
484
|
+
function applyRecallGate() {
|
|
485
|
+
var gate = document.getElementById("recallGate");
|
|
486
|
+
var wrap = document.getElementById("recallTryWrap");
|
|
487
|
+
var skipNote = document.getElementById("recallSkipNote");
|
|
488
|
+
var unlocked = recallGateUnlocked();
|
|
489
|
+
if (gate) gate.classList.toggle("is-hidden", unlocked);
|
|
490
|
+
if (wrap) wrap.classList.toggle("is-hidden", !unlocked);
|
|
491
|
+
if (skipNote) skipNote.classList.toggle("is-hidden", !recallGateSkipped);
|
|
492
|
+
}
|
|
493
|
+
async function refreshBillingStatus() {
|
|
494
|
+
try {
|
|
495
|
+
billingStatus = await getJson("/billing-status");
|
|
496
|
+
applyRecallGate();
|
|
497
|
+
if (!recallGateUnlocked() && billingStatus && billingStatus.plan === "unknown") {
|
|
498
|
+
setRecallGateStatus("Could not check your plan. You can start the trial or skip and keep setup going.");
|
|
499
|
+
}
|
|
500
|
+
} catch (_) {
|
|
501
|
+
setRecallGateStatus("Could not check your plan. You can start the trial or skip and keep setup going.");
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
function openRecallPricing() {
|
|
505
|
+
var url = recallPricingUrl();
|
|
506
|
+
setRecallGateStatus("Opening pricing in a new tab...");
|
|
507
|
+
try {
|
|
508
|
+
var opened = window.open(url, "_blank", "noopener,noreferrer");
|
|
509
|
+
if (!opened) setRecallGateStatus("Your browser blocked the pricing tab. Allow pop-ups for this page, then click Compare plans again.");
|
|
510
|
+
} catch (_) {
|
|
511
|
+
setRecallGateStatus("Could not open a new tab. Allow pop-ups for this page, then click Compare plans again.");
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
function skipRecallGate() {
|
|
515
|
+
recallGateSkipped = true;
|
|
516
|
+
applyRecallGate();
|
|
517
|
+
}
|
|
518
|
+
function wireRecallGate() {
|
|
519
|
+
var trialBtn = document.getElementById("recallStartTrial");
|
|
520
|
+
if (trialBtn) trialBtn.addEventListener("click", openRecallPricing);
|
|
521
|
+
var skipBtn = document.getElementById("recallSkip");
|
|
522
|
+
if (skipBtn) skipBtn.addEventListener("click", skipRecallGate);
|
|
523
|
+
wireTryButtons();
|
|
524
|
+
void refreshBillingStatus();
|
|
525
|
+
}
|
|
450
526
|
function tryItCard() {
|
|
451
527
|
var prompt = recallPrompt();
|
|
452
528
|
return '<div class="tryCard">' +
|
|
@@ -500,10 +576,10 @@ export const SETUP_PAGE_CLIENT_EXTRACTION = String.raw ` /* ---------- dash
|
|
|
500
576
|
'<span class="seal">Skipped for now</span>' +
|
|
501
577
|
'<h2>You can come back anytime.</h2>' +
|
|
502
578
|
'<p>Echo saved what already finished. When you are ready, run <code>echomem-mcp init</code> again — it picks up right where it left off.</p>' +
|
|
503
|
-
|
|
579
|
+
recallGateBlock() +
|
|
504
580
|
hudTip() +
|
|
505
581
|
'</div>';
|
|
506
|
-
|
|
582
|
+
wireRecallGate();
|
|
507
583
|
}
|
|
508
584
|
async function endExtraction() {
|
|
509
585
|
if (extractionEnded) return;
|
|
@@ -1,5 +1,71 @@
|
|
|
1
1
|
// Generated by splitting the previous inline setup page. Keep this source readable: it is emitted into one browser IIFE.
|
|
2
2
|
export const SETUP_PAGE_CLIENT_LIFECYCLE = String.raw ` /* ---------- scan-first poll for the forensic report ---------- */
|
|
3
|
+
function renderLocalScanConsent() {
|
|
4
|
+
setScanMode(false);
|
|
5
|
+
setCityMode(false);
|
|
6
|
+
setExtractMode(false);
|
|
7
|
+
setReadyMode(false);
|
|
8
|
+
setHead("Local file access", "Consent");
|
|
9
|
+
app.className = "consentStage";
|
|
10
|
+
app.innerHTML =
|
|
11
|
+
'<section class="localConsent">' +
|
|
12
|
+
'<p class="consentEyebrow">Local visualization</p>' +
|
|
13
|
+
'<h2>Let Echo scan locally to build your Agent Doctor view?</h2>' +
|
|
14
|
+
'<p>Echo reads local Codex and Claude Code history on this machine only, then turns it into a private usage visualization. Nothing from this scan leaves your device.</p>' +
|
|
15
|
+
'<div class="consentActions">' +
|
|
16
|
+
'<button type="button" class="primary" id="allowLocalScan">Continue with local scan</button>' +
|
|
17
|
+
'<button type="button" class="textButton" id="declineLocalScan">Skip the insight for now</button>' +
|
|
18
|
+
'</div>' +
|
|
19
|
+
'<p class="consentFine" id="consentStatus"></p>' +
|
|
20
|
+
'</section>';
|
|
21
|
+
var allow = document.getElementById("allowLocalScan");
|
|
22
|
+
if (allow) allow.onclick = allowLocalScan;
|
|
23
|
+
var decline = document.getElementById("declineLocalScan");
|
|
24
|
+
if (decline) decline.onclick = declineLocalScan;
|
|
25
|
+
}
|
|
26
|
+
function setConsentStatus(message) {
|
|
27
|
+
var node = document.getElementById("consentStatus");
|
|
28
|
+
if (node) node.textContent = message || "";
|
|
29
|
+
}
|
|
30
|
+
async function allowLocalScan() {
|
|
31
|
+
setConsentStatus("Starting local scan...");
|
|
32
|
+
try {
|
|
33
|
+
await postJson("/report-consent", { allowed: true });
|
|
34
|
+
await showReport();
|
|
35
|
+
} catch (error) {
|
|
36
|
+
renderReportIssue("CONSENT_SAVE_FAILED", error && error.message ? error.message : "Could not start the local scan.");
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
async function declineLocalScan() {
|
|
40
|
+
setConsentStatus("Skipping Agent Doctor...");
|
|
41
|
+
try {
|
|
42
|
+
await postJson("/report-consent", { allowed: false });
|
|
43
|
+
} catch (_) {}
|
|
44
|
+
renderLocalScanSkipped();
|
|
45
|
+
}
|
|
46
|
+
function renderLocalScanSkipped() {
|
|
47
|
+
setScanMode(false);
|
|
48
|
+
setCityMode(false);
|
|
49
|
+
setExtractMode(false);
|
|
50
|
+
setReadyMode(true);
|
|
51
|
+
setHead("Agent Doctor skipped", connected ? "Signed in" : "Optional");
|
|
52
|
+
app.className = "consentStage";
|
|
53
|
+
app.innerHTML =
|
|
54
|
+
'<section class="localConsent">' +
|
|
55
|
+
'<p class="consentEyebrow">Visualization skipped</p>' +
|
|
56
|
+
'<h2>No problem. Echo can still set up memory.</h2>' +
|
|
57
|
+
'<p>We will skip Agent Doctor and continue with EchoMem setup.</p>' +
|
|
58
|
+
'<div class="consentActions">' +
|
|
59
|
+
'<button type="button" class="primary" data-connect-echo>' + (connected ? "Continue setup" : "Connect EchoMem") + '</button>' +
|
|
60
|
+
'<button type="button" class="textButton" id="changeConsent">Go back</button>' +
|
|
61
|
+
'</div>' +
|
|
62
|
+
'<p class="consentFine" data-signin-help></p>' +
|
|
63
|
+
'<a class="button secondary hidden" data-signin-fallback target="_blank" rel="noopener noreferrer">Open sign-in</a>' +
|
|
64
|
+
'</section>';
|
|
65
|
+
bindConnect();
|
|
66
|
+
var change = document.getElementById("changeConsent");
|
|
67
|
+
if (change) change.onclick = renderLocalScanConsent;
|
|
68
|
+
}
|
|
3
69
|
async function showReport() {
|
|
4
70
|
if (reportPollStarted) return;
|
|
5
71
|
reportPollStarted = true;
|
|
@@ -103,14 +169,14 @@ export const SETUP_PAGE_CLIENT_LIFECYCLE = String.raw ` /* ---------- scan-
|
|
|
103
169
|
}
|
|
104
170
|
return;
|
|
105
171
|
}
|
|
106
|
-
// Learn the auth URL + connection state
|
|
107
|
-
|
|
172
|
+
// Learn the auth URL + connection state before asking for optional local file access.
|
|
173
|
+
try {
|
|
174
|
+
var cfg = await getJson("/config");
|
|
108
175
|
authUrl = cfg.authUrl || authUrl;
|
|
109
176
|
connected = !!cfg.connected;
|
|
110
177
|
workspacePath = cfg.workspacePath || workspacePath;
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
await showReport();
|
|
178
|
+
} catch (_) {}
|
|
179
|
+
renderLocalScanConsent();
|
|
114
180
|
}
|
|
115
181
|
__ECHO_SETUP_STARTUP__;
|
|
116
182
|
`;
|
|
@@ -11,7 +11,7 @@ const SETUP_PAGE_BODY = String.raw `
|
|
|
11
11
|
</div>
|
|
12
12
|
<div class="pill" id="status">Local</div>
|
|
13
13
|
</header>
|
|
14
|
-
<section id="app" class="state">
|
|
14
|
+
<section id="app" class="state">Preparing setup...</section>
|
|
15
15
|
</main>
|
|
16
16
|
<dialog id="echo-note" class="echo-note-dlg">
|
|
17
17
|
<button class="nclose" aria-label="Close">×</button>
|
|
@@ -59,7 +59,73 @@ export const SETUP_PAGE_STYLES_EXTRACTION = String.raw ` /* ---- dashboard (p
|
|
|
59
59
|
}
|
|
60
60
|
.primary { background: var(--echo-ink-primary); color: #fff; }
|
|
61
61
|
.secondary { background: #fff; color: var(--echo-ink-primary); }
|
|
62
|
+
.textButton {
|
|
63
|
+
min-height: 36px;
|
|
64
|
+
border-color: transparent;
|
|
65
|
+
background: transparent;
|
|
66
|
+
color: var(--echo-ink-faint);
|
|
67
|
+
padding: 0 8px;
|
|
68
|
+
font-size: 13px;
|
|
69
|
+
font-weight: 700;
|
|
70
|
+
}
|
|
71
|
+
.textButton:hover { color: var(--echo-ink-primary); background: transparent; }
|
|
62
72
|
button:disabled { cursor: not-allowed; opacity: 0.55; }
|
|
73
|
+
.consentStage {
|
|
74
|
+
display: grid;
|
|
75
|
+
place-items: center;
|
|
76
|
+
min-height: min(620px, calc(100vh - 180px));
|
|
77
|
+
}
|
|
78
|
+
.localConsent {
|
|
79
|
+
width: min(680px, 100%);
|
|
80
|
+
border: 2px solid var(--echo-ink-primary);
|
|
81
|
+
border-radius: 8px;
|
|
82
|
+
background: var(--echo-paper-white);
|
|
83
|
+
padding: clamp(22px, 4vw, 34px);
|
|
84
|
+
box-shadow: 0 18px 38px -28px rgba(26,58,143,0.45);
|
|
85
|
+
}
|
|
86
|
+
.consentEyebrow {
|
|
87
|
+
margin: 0 0 10px;
|
|
88
|
+
font-family: var(--echo-font-mono);
|
|
89
|
+
font-size: 11px;
|
|
90
|
+
font-weight: 800;
|
|
91
|
+
letter-spacing: 0.08em;
|
|
92
|
+
text-transform: uppercase;
|
|
93
|
+
color: var(--echo-ink-primary);
|
|
94
|
+
}
|
|
95
|
+
.localConsent h2 {
|
|
96
|
+
max-width: 620px;
|
|
97
|
+
margin: 0;
|
|
98
|
+
font-size: clamp(30px, 3.4vw, 42px);
|
|
99
|
+
line-height: 1.04;
|
|
100
|
+
color: var(--echo-ink-primary);
|
|
101
|
+
}
|
|
102
|
+
.localConsent > p:not(.consentEyebrow):not(.consentFine) {
|
|
103
|
+
max-width: 590px;
|
|
104
|
+
margin: 14px 0 0;
|
|
105
|
+
font-size: 16px;
|
|
106
|
+
line-height: 1.55;
|
|
107
|
+
color: var(--echo-ink-mute);
|
|
108
|
+
}
|
|
109
|
+
.consentActions {
|
|
110
|
+
display: flex;
|
|
111
|
+
flex-wrap: wrap;
|
|
112
|
+
gap: 10px;
|
|
113
|
+
align-items: center;
|
|
114
|
+
margin-top: 22px;
|
|
115
|
+
}
|
|
116
|
+
.consentActions button {
|
|
117
|
+
min-height: 48px;
|
|
118
|
+
padding: 0 18px;
|
|
119
|
+
}
|
|
120
|
+
.consentFine {
|
|
121
|
+
min-height: 18px;
|
|
122
|
+
margin: 10px 0 0;
|
|
123
|
+
font-family: var(--echo-font-mono);
|
|
124
|
+
font-size: 11px;
|
|
125
|
+
line-height: 1.45;
|
|
126
|
+
color: var(--echo-ink-faint);
|
|
127
|
+
}
|
|
128
|
+
.consentFine:empty { display: none; }
|
|
63
129
|
.track { height: 12px; overflow: hidden; border-radius: 999px; background: #e8e8e1; margin: 16px 0 10px; }
|
|
64
130
|
.fill { height: 100%; width: 0%; background: var(--echo-ink-primary); transition: width 180ms ease; }
|
|
65
131
|
/* indeterminate "we're working on it" bar for the prep phase (before job counts move) */
|
|
@@ -93,6 +159,7 @@ export const SETUP_PAGE_STYLES_EXTRACTION = String.raw ` /* ---- dashboard (p
|
|
|
93
159
|
.explainCopy h2 { font-size: 38px; }
|
|
94
160
|
.explainCta .promiseActions { width: 100%; }
|
|
95
161
|
.explainCta .primary, .explainCta .secondary { width: 100%; }
|
|
162
|
+
.consentActions .primary { width: 100%; }
|
|
96
163
|
.cityHero { min-height: auto; }
|
|
97
164
|
.cityShell {
|
|
98
165
|
width: 100vw;
|
|
@@ -615,6 +682,81 @@ export const SETUP_PAGE_STYLES_EXTRACTION = String.raw ` /* ---- dashboard (p
|
|
|
615
682
|
.exCountdown strong { color: var(--echo-ink-primary); font-weight: 700; }
|
|
616
683
|
.exOverdue { margin: 0; max-width: 620px; border: 1px solid rgba(196, 148, 42, 0.45); background: rgba(233, 185, 73, 0.12); border-radius: 8px; padding: 12px 14px; font-size: 13px; line-height: 1.5; color: var(--echo-ink-text); }
|
|
617
684
|
.exOverdue strong { display: block; margin-bottom: 4px; color: var(--echo-ink-primary); }
|
|
685
|
+
.is-hidden { display: none !important; }
|
|
686
|
+
.recallGateBlock { width: min(760px, 100%); margin: 2px auto 0; }
|
|
687
|
+
.recallGate {
|
|
688
|
+
width: 100%;
|
|
689
|
+
border: 2px solid var(--echo-ink-primary);
|
|
690
|
+
border-radius: 8px;
|
|
691
|
+
background: var(--echo-paper-white);
|
|
692
|
+
padding: clamp(18px, 3vw, 26px);
|
|
693
|
+
box-shadow: 0 18px 38px -28px rgba(26,58,143,0.45);
|
|
694
|
+
text-align: left;
|
|
695
|
+
}
|
|
696
|
+
.recallEyebrow {
|
|
697
|
+
margin: 0 0 10px;
|
|
698
|
+
font-family: var(--echo-font-mono);
|
|
699
|
+
font-size: 11px;
|
|
700
|
+
font-weight: 800;
|
|
701
|
+
letter-spacing: 0.08em;
|
|
702
|
+
text-transform: uppercase;
|
|
703
|
+
color: var(--echo-ink-primary);
|
|
704
|
+
}
|
|
705
|
+
.recallGate h3 {
|
|
706
|
+
margin: 0;
|
|
707
|
+
max-width: 680px;
|
|
708
|
+
font-family: var(--echo-font-brand);
|
|
709
|
+
font-size: clamp(30px, 3.8vw, 48px);
|
|
710
|
+
line-height: 0.98;
|
|
711
|
+
font-weight: 900;
|
|
712
|
+
color: var(--echo-ink-primary);
|
|
713
|
+
letter-spacing: 0;
|
|
714
|
+
}
|
|
715
|
+
.recallGate p {
|
|
716
|
+
max-width: 620px;
|
|
717
|
+
margin: 14px 0 0;
|
|
718
|
+
font-size: 15px;
|
|
719
|
+
line-height: 1.55;
|
|
720
|
+
color: var(--echo-ink-mute);
|
|
721
|
+
}
|
|
722
|
+
.recallGate code,
|
|
723
|
+
.recallSkipNote code {
|
|
724
|
+
font-family: var(--echo-font-mono);
|
|
725
|
+
font-size: 0.92em;
|
|
726
|
+
background: var(--echo-paper-soft);
|
|
727
|
+
border: 1px solid var(--echo-line-soft);
|
|
728
|
+
border-radius: 5px;
|
|
729
|
+
padding: 1px 5px;
|
|
730
|
+
color: var(--echo-ink-primary);
|
|
731
|
+
}
|
|
732
|
+
.recallGateActions {
|
|
733
|
+
display: flex;
|
|
734
|
+
flex-wrap: wrap;
|
|
735
|
+
gap: 10px;
|
|
736
|
+
align-items: center;
|
|
737
|
+
margin-top: 22px;
|
|
738
|
+
}
|
|
739
|
+
.recallGateActions button {
|
|
740
|
+
min-height: 48px;
|
|
741
|
+
padding: 0 18px;
|
|
742
|
+
}
|
|
743
|
+
.recallGate .recallFine {
|
|
744
|
+
margin-top: 10px;
|
|
745
|
+
font-family: var(--echo-font-mono);
|
|
746
|
+
font-size: 11px;
|
|
747
|
+
color: var(--echo-ink-faint);
|
|
748
|
+
}
|
|
749
|
+
.recallTryWrap { width: 100%; }
|
|
750
|
+
.recallSkipNote {
|
|
751
|
+
margin: 0 0 12px;
|
|
752
|
+
border: 1px solid var(--echo-line-soft);
|
|
753
|
+
border-radius: 8px;
|
|
754
|
+
background: rgba(246,244,238,0.86);
|
|
755
|
+
padding: 10px 12px;
|
|
756
|
+
font-size: 13px;
|
|
757
|
+
line-height: 1.45;
|
|
758
|
+
color: var(--echo-ink-mute);
|
|
759
|
+
}
|
|
618
760
|
.tryCard { margin-top: 2px; max-width: 760px; border: 2px solid var(--echo-ink-primary); border-radius: 8px; background: var(--echo-paper-white); padding: clamp(18px, 3vw, 26px); box-shadow: 0 18px 38px -28px rgba(26,58,143,0.45); }
|
|
619
761
|
.tryCard .tryK { margin: 0 0 10px; font-family: var(--echo-font-mono); font-size: 12px; font-weight: 800; letter-spacing: 0.08em; text-transform: uppercase; color: var(--echo-ink-primary); }
|
|
620
762
|
.tryCard .tryTitle {
|
package/dist/setup.js
CHANGED
|
@@ -38,6 +38,7 @@ import { checkLatestUpdateStatus, compareSemver, readCachedUpdateStatus } from "
|
|
|
38
38
|
// device unless the user explicitly starts migration. Override the hosted auth origin with ECHO_WEB_URL.
|
|
39
39
|
const WEB_URL = (process.env.ECHO_WEB_URL || "https://yeahecho.com").replace(/\/$/, "");
|
|
40
40
|
const API_BASE_URL = (process.env.ECHO_API_BASE_URL || "https://echo-mem-chrome.vercel.app").replace(/\/$/, "");
|
|
41
|
+
const PRICING_URL = (process.env.ECHO_PRICING_URL || "https://echoknows.com/pricing").replace(/\/$/, "");
|
|
41
42
|
function home(...p) {
|
|
42
43
|
return path.join(os.homedir(), ...p);
|
|
43
44
|
}
|
|
@@ -1250,6 +1251,29 @@ export function startCallbackServer(opts = {}) {
|
|
|
1250
1251
|
json(res, 200, payload);
|
|
1251
1252
|
return;
|
|
1252
1253
|
}
|
|
1254
|
+
if (route === "/billing-status" && req.method === "GET") {
|
|
1255
|
+
if (!checkNonce(url.searchParams.get("nonce") || undefined))
|
|
1256
|
+
return void text(res, 403, "bad nonce");
|
|
1257
|
+
const token = new KeyStore().getToken();
|
|
1258
|
+
const pricingUrl = `${PRICING_URL}?source=mcp_onboarding`;
|
|
1259
|
+
if (!token) {
|
|
1260
|
+
json(res, 200, { plan: "free", paid: false, pricingUrl });
|
|
1261
|
+
return;
|
|
1262
|
+
}
|
|
1263
|
+
try {
|
|
1264
|
+
const response = await authedAxios(token).get("/api/extension/account/bootstrap", { timeout: 6000 });
|
|
1265
|
+
const plan = (asString(response.data?.plan) || "free").toLowerCase();
|
|
1266
|
+
json(res, 200, {
|
|
1267
|
+
plan,
|
|
1268
|
+
paid: ["pro", "power", "team", "enterprise"].includes(plan),
|
|
1269
|
+
pricingUrl,
|
|
1270
|
+
});
|
|
1271
|
+
}
|
|
1272
|
+
catch {
|
|
1273
|
+
json(res, 200, { plan: "unknown", paid: false, pricingUrl });
|
|
1274
|
+
}
|
|
1275
|
+
return;
|
|
1276
|
+
}
|
|
1253
1277
|
if (route === "/report" && req.method === "GET") {
|
|
1254
1278
|
// Local forensic "Context Doctor" report — computed locally, served BEFORE auth (scan-first).
|
|
1255
1279
|
res.setHeader("Cache-Control", "no-store");
|
|
@@ -1353,6 +1377,22 @@ export function startCallbackServer(opts = {}) {
|
|
|
1353
1377
|
json(res, 200, progress);
|
|
1354
1378
|
return;
|
|
1355
1379
|
}
|
|
1380
|
+
if (route === "/report-consent" && req.method === "POST") {
|
|
1381
|
+
let body;
|
|
1382
|
+
try {
|
|
1383
|
+
body = await readJsonBody(req);
|
|
1384
|
+
}
|
|
1385
|
+
catch {
|
|
1386
|
+
text(res, 400, "bad json");
|
|
1387
|
+
return;
|
|
1388
|
+
}
|
|
1389
|
+
if (!checkNonce(asString(body.nonce)))
|
|
1390
|
+
return void text(res, 403, "bad nonce");
|
|
1391
|
+
const allowed = body.allowed === true;
|
|
1392
|
+
opts.onReportConsent?.(allowed);
|
|
1393
|
+
json(res, 200, { ok: true, allowed });
|
|
1394
|
+
return;
|
|
1395
|
+
}
|
|
1356
1396
|
if (route === "/logout" && req.method === "POST") {
|
|
1357
1397
|
let body;
|
|
1358
1398
|
try {
|
|
@@ -1780,6 +1820,8 @@ async function cmdLogin(flags) {
|
|
|
1780
1820
|
const nonce = devNonce || randomUUID();
|
|
1781
1821
|
let stats = null;
|
|
1782
1822
|
let forensicReport = null;
|
|
1823
|
+
let forensicConsent = "pending";
|
|
1824
|
+
let forensicScanStarted = false;
|
|
1783
1825
|
const forensicStartedAt = Date.now();
|
|
1784
1826
|
let forensicStageStartedAt = forensicStartedAt;
|
|
1785
1827
|
let forensicStage = "starting";
|
|
@@ -1799,6 +1841,28 @@ async function cmdLogin(flags) {
|
|
|
1799
1841
|
getStats: () => stats,
|
|
1800
1842
|
getReport: () => forensicReport,
|
|
1801
1843
|
getReportProgress: () => forensicProgress,
|
|
1844
|
+
onReportConsent: (allowed) => {
|
|
1845
|
+
if (!allowed) {
|
|
1846
|
+
forensicConsent = "declined";
|
|
1847
|
+
forensicProgress = {
|
|
1848
|
+
status: "failed",
|
|
1849
|
+
scanned: 0,
|
|
1850
|
+
total: 0,
|
|
1851
|
+
stage: "failed",
|
|
1852
|
+
label: "Local scan skipped",
|
|
1853
|
+
elapsedMs: Date.now() - forensicStartedAt,
|
|
1854
|
+
stageElapsedMs: Date.now() - forensicStageStartedAt,
|
|
1855
|
+
updatedAt: Date.now(),
|
|
1856
|
+
error: {
|
|
1857
|
+
code: "REPORT_SCAN_DECLINED",
|
|
1858
|
+
message: "Local file analysis was skipped. EchoMem can still connect, save conversations, and import memories.",
|
|
1859
|
+
},
|
|
1860
|
+
};
|
|
1861
|
+
return;
|
|
1862
|
+
}
|
|
1863
|
+
forensicConsent = "allowed";
|
|
1864
|
+
startForensicScan();
|
|
1865
|
+
},
|
|
1802
1866
|
});
|
|
1803
1867
|
const callbackUrl = `http://127.0.0.1:${srv.port}/callback`;
|
|
1804
1868
|
const localSetupUrl = `http://127.0.0.1:${srv.port}/setup?nonce=${nonce}`;
|
|
@@ -1812,46 +1876,50 @@ async function cmdLogin(flags) {
|
|
|
1812
1876
|
openBrowser(localSetupUrl);
|
|
1813
1877
|
console.log(`If it didn't open, visit:\n ${localSetupUrl}\n`);
|
|
1814
1878
|
console.log("Waiting for browser approval for up to 15 minutes…");
|
|
1815
|
-
|
|
1816
|
-
|
|
1817
|
-
|
|
1818
|
-
|
|
1819
|
-
|
|
1820
|
-
|
|
1821
|
-
|
|
1822
|
-
|
|
1823
|
-
|
|
1824
|
-
|
|
1825
|
-
|
|
1826
|
-
|
|
1827
|
-
|
|
1828
|
-
|
|
1829
|
-
|
|
1830
|
-
|
|
1831
|
-
|
|
1832
|
-
|
|
1833
|
-
|
|
1834
|
-
|
|
1835
|
-
|
|
1836
|
-
|
|
1837
|
-
|
|
1838
|
-
|
|
1839
|
-
|
|
1840
|
-
|
|
1841
|
-
|
|
1842
|
-
|
|
1843
|
-
|
|
1844
|
-
|
|
1845
|
-
|
|
1846
|
-
|
|
1847
|
-
|
|
1848
|
-
|
|
1849
|
-
|
|
1850
|
-
|
|
1851
|
-
|
|
1852
|
-
|
|
1853
|
-
|
|
1854
|
-
|
|
1879
|
+
const startForensicScan = () => {
|
|
1880
|
+
if (forensicScanStarted || forensicConsent !== "allowed")
|
|
1881
|
+
return;
|
|
1882
|
+
forensicScanStarted = true;
|
|
1883
|
+
// Build the local forensic "Context Doctor" report off-thread only after explicit consent.
|
|
1884
|
+
buildForensicReportOffThread((progress) => {
|
|
1885
|
+
const now = Date.now();
|
|
1886
|
+
const nextStage = progress.stage || forensicStage;
|
|
1887
|
+
if (nextStage !== forensicStage) {
|
|
1888
|
+
forensicStage = nextStage;
|
|
1889
|
+
forensicStageStartedAt = now;
|
|
1890
|
+
console.log(`Local scan: ${forensicStageLabel(forensicStage)}…`);
|
|
1891
|
+
}
|
|
1892
|
+
forensicProgress = {
|
|
1893
|
+
status: "running",
|
|
1894
|
+
scanned: progress.done,
|
|
1895
|
+
total: progress.total,
|
|
1896
|
+
stage: forensicStage,
|
|
1897
|
+
label: forensicStageLabel(forensicStage),
|
|
1898
|
+
detail: progress.detail,
|
|
1899
|
+
elapsedMs: now - forensicStartedAt,
|
|
1900
|
+
stageElapsedMs: now - forensicStageStartedAt,
|
|
1901
|
+
updatedAt: now,
|
|
1902
|
+
};
|
|
1903
|
+
})
|
|
1904
|
+
.then((r) => {
|
|
1905
|
+
forensicReport = r;
|
|
1906
|
+
})
|
|
1907
|
+
.catch((e) => {
|
|
1908
|
+
const now = Date.now();
|
|
1909
|
+
forensicProgress = {
|
|
1910
|
+
status: "failed",
|
|
1911
|
+
scanned: forensicProgress.scanned,
|
|
1912
|
+
total: forensicProgress.total,
|
|
1913
|
+
stage: "failed",
|
|
1914
|
+
label: "Local scan failed",
|
|
1915
|
+
elapsedMs: now - forensicStartedAt,
|
|
1916
|
+
stageElapsedMs: now - forensicStageStartedAt,
|
|
1917
|
+
updatedAt: now,
|
|
1918
|
+
error: safeForensicError(e),
|
|
1919
|
+
};
|
|
1920
|
+
console.error(`Could not build the local report: ${e instanceof Error ? e.message : String(e)}`);
|
|
1921
|
+
});
|
|
1922
|
+
};
|
|
1855
1923
|
let token;
|
|
1856
1924
|
let key;
|
|
1857
1925
|
try {
|
package/dist/v1-contract.js
CHANGED
|
@@ -108,6 +108,7 @@ export function listToolSpecs(opts = {}) {
|
|
|
108
108
|
const currentTime = new Date().toISOString();
|
|
109
109
|
const map = opts.map?.trim();
|
|
110
110
|
const updateNotice = opts.updateNotice?.trim();
|
|
111
|
+
const paidRecallNote = "Requires Echo Pro or Power; saving conversations and memory extraction remain included.";
|
|
111
112
|
const updateSection = updateNotice ? `\n\nUPDATE NOTICE: ${updateNotice}` : "";
|
|
112
113
|
const mapSection = map
|
|
113
114
|
? `\n\nThis user's EchoMem currently covers these topics (a relevance guide — recall when the task relates to one of them):\n${map}\n`
|
|
@@ -115,7 +116,7 @@ export function listToolSpecs(opts = {}) {
|
|
|
115
116
|
return [
|
|
116
117
|
{
|
|
117
118
|
name: canonicalToolNames.search,
|
|
118
|
-
description: withMcpVersion(`Recall the user's prior decisions, preferences, and project context from EchoMem — their long-term memory across ALL their AI tools, not just this session. Use it instead of re-deriving or re-asking what the user already settled
|
|
119
|
+
description: withMcpVersion(`Recall the user's prior decisions, preferences, and project context from EchoMem — their long-term memory across ALL their AI tools, not just this session. Use it instead of re-deriving or re-asking what the user already settled. ${paidRecallNote}${mapSection}\nReturns the ranked memories; set includeAnswer=true only if you need the legacy synthesized answer. Current time: ${currentTime}.${updateSection}`),
|
|
119
120
|
inputSchema: {
|
|
120
121
|
type: "object",
|
|
121
122
|
properties: {
|
|
@@ -138,7 +139,7 @@ export function listToolSpecs(opts = {}) {
|
|
|
138
139
|
},
|
|
139
140
|
{
|
|
140
141
|
name: "search_memories_by_description_semantic",
|
|
141
|
-
description:
|
|
142
|
+
description: `Legacy alias for search_memories. ${paidRecallNote}`,
|
|
142
143
|
inputSchema: {
|
|
143
144
|
type: "object",
|
|
144
145
|
properties: {
|
|
@@ -161,7 +162,7 @@ export function listToolSpecs(opts = {}) {
|
|
|
161
162
|
},
|
|
162
163
|
{
|
|
163
164
|
name: canonicalToolNames.save,
|
|
164
|
-
description: "Save this conversation into EchoMem as long-term memory (durable memories are extracted automatically). passthrough=true stores the text verbatim as a session capsule
|
|
165
|
+
description: "Save this conversation into EchoMem as long-term memory (durable memories are extracted automatically). Saving is included. passthrough=true stores the text verbatim as a session capsule; reloading/searching saved memories requires Echo Pro or Power.",
|
|
165
166
|
inputSchema: {
|
|
166
167
|
type: "object",
|
|
167
168
|
properties: {
|
|
@@ -194,7 +195,7 @@ export function listToolSpecs(opts = {}) {
|
|
|
194
195
|
},
|
|
195
196
|
{
|
|
196
197
|
name: canonicalToolNames.timeRange,
|
|
197
|
-
description: `Retrieve memories within a specific date range. Current time: ${currentTime}.`,
|
|
198
|
+
description: `Retrieve memories within a specific date range. ${paidRecallNote} Current time: ${currentTime}.`,
|
|
198
199
|
inputSchema: {
|
|
199
200
|
type: "object",
|
|
200
201
|
properties: {
|
|
@@ -212,7 +213,7 @@ export function listToolSpecs(opts = {}) {
|
|
|
212
213
|
},
|
|
213
214
|
{
|
|
214
215
|
name: canonicalToolNames.keywords,
|
|
215
|
-
description:
|
|
216
|
+
description: `Search memories based on keywords in keys field. ${paidRecallNote}`,
|
|
216
217
|
inputSchema: {
|
|
217
218
|
type: "object",
|
|
218
219
|
properties: {
|
|
@@ -369,7 +370,7 @@ export function listToolSpecs(opts = {}) {
|
|
|
369
370
|
},
|
|
370
371
|
{
|
|
371
372
|
name: canonicalToolNames.getByContext,
|
|
372
|
-
description: withMcpVersion(`Deterministically re-fetch the exact batch of memories saved under one contextId — no semantic search, no ranking, just that session's saved capsule. save_conversation returns a contextId; pass it here to pull back precisely those memories, e.g. to warm up a fresh session with what a prior session saved, or to verify the saved facts are still present. Current time: ${currentTime}.`),
|
|
373
|
+
description: withMcpVersion(`Deterministically re-fetch the exact batch of memories saved under one contextId — no semantic search, no ranking, just that session's saved capsule. ${paidRecallNote} save_conversation returns a contextId; pass it here to pull back precisely those memories, e.g. to warm up a fresh session with what a prior session saved, or to verify the saved facts are still present. Current time: ${currentTime}.`),
|
|
373
374
|
inputSchema: {
|
|
374
375
|
type: "object",
|
|
375
376
|
properties: {
|
|
@@ -386,7 +387,7 @@ export function listToolSpecs(opts = {}) {
|
|
|
386
387
|
},
|
|
387
388
|
{
|
|
388
389
|
name: canonicalToolNames.checkpointByContext,
|
|
389
|
-
description: withMcpVersion(`Rebuild a clean-context checkpoint / decision log from one EchoMem contextId. Use this when the user or EchoMem HUD gives you a contextId for a renewed coding session and you need the session handoff, not a raw memory dump. It deterministically fetches that context and formats it as orientation state: decisions, carryover, constraints, and checkpoints. Current time: ${currentTime}.`),
|
|
390
|
+
description: withMcpVersion(`Rebuild a clean-context checkpoint / decision log from one EchoMem contextId. ${paidRecallNote} Use this when the user or EchoMem HUD gives you a contextId for a renewed coding session and you need the session handoff, not a raw memory dump. It deterministically fetches that context and formats it as orientation state: decisions, carryover, constraints, and checkpoints. Current time: ${currentTime}.`),
|
|
390
391
|
inputSchema: {
|
|
391
392
|
type: "object",
|
|
392
393
|
properties: {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@echomem/mcp",
|
|
3
|
-
"version": "1.4.
|
|
3
|
+
"version": "1.4.15",
|
|
4
4
|
"description": "EchoMem MCP bridge: cloud-first memory tools, local context HUD, and the Agent Doctor workspace forensics report (cost ledger + 3D repo city)",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"type": "module",
|