@echomem/mcp 1.4.2 → 1.4.4
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 -7
- package/dist/hud/capsule.js +68 -25
- package/dist/hud/cli.js +0 -0
- package/dist/hud/metric.js +16 -3
- package/dist/hud/monitor.js +152 -14
- package/dist/hud/web.js +595 -429
- package/dist/index.js +14 -1
- package/dist/package-metadata.js +4 -2
- package/dist/setup.js +306 -9
- package/dist/update-check.js +154 -0
- package/dist/v1-contract.js +18 -1
- package/package.json +2 -2
package/dist/index.js
CHANGED
|
@@ -13,6 +13,7 @@ import { createHash, randomUUID } from "node:crypto";
|
|
|
13
13
|
import { fetchEncryptionConfig, decryptMemoryFields } from "./encryption.js";
|
|
14
14
|
import { runCli } from "./setup.js";
|
|
15
15
|
import { MCP_PACKAGE_VERSION, MCP_SERVER_INSTRUCTIONS } from "./package-metadata.js";
|
|
16
|
+
import { checkLatestUpdateStatus, formatUpdateNotice, formatUpdateStatusText, startBackgroundUpdateCheck, } from "./update-check.js";
|
|
16
17
|
const ECHO_API_BASE_URL = process.env.ECHO_API_BASE_URL || "https://echo-mem-chrome.vercel.app";
|
|
17
18
|
const MEMORY_FEED_API_URL = process.env.MEMORY_FEED_API_URL || "https://memory-feed.vercel.app";
|
|
18
19
|
/** Thrown when no API token is present yet — the model gets a "run login" nudge, not a hard error. */
|
|
@@ -800,6 +801,7 @@ class EchoMemMCPServer {
|
|
|
800
801
|
mcpClientVersion;
|
|
801
802
|
/** Whether the most recent ListTools response carried the memory map (per-session recall signal). */
|
|
802
803
|
mapInjected = false;
|
|
804
|
+
updateStatus;
|
|
803
805
|
constructor(store) {
|
|
804
806
|
this.server = new Server({
|
|
805
807
|
name: "echomem-mcp",
|
|
@@ -812,6 +814,9 @@ class EchoMemMCPServer {
|
|
|
812
814
|
});
|
|
813
815
|
this.client = new EchoMemApiClient(store);
|
|
814
816
|
this.events = new EventLogger({ session_id: this.client.getSessionId(), app_version: SERVER_VERSION });
|
|
817
|
+
startBackgroundUpdateCheck((status) => {
|
|
818
|
+
this.updateStatus = status;
|
|
819
|
+
});
|
|
815
820
|
this.setupToolHandlers();
|
|
816
821
|
this.server.onerror = (error) => console.error("[MCP Error]", error);
|
|
817
822
|
process.on("SIGINT", async () => {
|
|
@@ -851,7 +856,8 @@ class EchoMemMCPServer {
|
|
|
851
856
|
? await Promise.race([this.mapCache, new Promise((r) => setTimeout(() => r(undefined), 2500))])
|
|
852
857
|
: undefined;
|
|
853
858
|
this.mapInjected = !!map;
|
|
854
|
-
|
|
859
|
+
const updateNotice = formatUpdateNotice(this.updateStatus);
|
|
860
|
+
return { tools: listToolSpecs({ map, updateNotice }) };
|
|
855
861
|
});
|
|
856
862
|
this.server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
857
863
|
const canonicalName = resolveCanonicalToolName(request.params.name);
|
|
@@ -883,6 +889,12 @@ class EchoMemMCPServer {
|
|
|
883
889
|
if (canonicalName === canonicalToolNames.report) {
|
|
884
890
|
return { content: [{ type: "text", text: await buildReportText(false) }] };
|
|
885
891
|
}
|
|
892
|
+
if (canonicalName === canonicalToolNames.updateStatus) {
|
|
893
|
+
const force = isRecord(request.params.arguments) && request.params.arguments.force === true;
|
|
894
|
+
const status = await checkLatestUpdateStatus({ force });
|
|
895
|
+
this.updateStatus = status;
|
|
896
|
+
return { content: [{ type: "text", text: formatUpdateStatusText(status) }] };
|
|
897
|
+
}
|
|
886
898
|
if (canonicalName === canonicalToolNames.contextHealth) {
|
|
887
899
|
const client = isRecord(request.params.arguments) && typeof request.params.arguments.client === "string"
|
|
888
900
|
? request.params.arguments.client
|
|
@@ -994,6 +1006,7 @@ class EchoMemMCPServer {
|
|
|
994
1006
|
rec.latency_ms = Date.now() - t0;
|
|
995
1007
|
this.events.record(rec);
|
|
996
1008
|
if (canonicalName !== canonicalToolNames.report &&
|
|
1009
|
+
canonicalName !== canonicalToolNames.updateStatus &&
|
|
997
1010
|
canonicalName !== canonicalToolNames.contextHealth &&
|
|
998
1011
|
canonicalName !== canonicalToolNames.recompose &&
|
|
999
1012
|
canonicalName !== canonicalToolNames.save &&
|
package/dist/package-metadata.js
CHANGED
|
@@ -22,11 +22,13 @@ export const MCP_PACKAGE_VERSION = stringOrFallback(packageJson.version, FALLBAC
|
|
|
22
22
|
export const MCP_PACKAGE_DESCRIPTION = stringOrFallback(packageJson.description, FALLBACK_PACKAGE.description);
|
|
23
23
|
export const MCP_PACKAGE_LABEL = `${MCP_PACKAGE_NAME}@${MCP_PACKAGE_VERSION}`;
|
|
24
24
|
export const MCP_UPDATE_COMMAND = `npx -y ${MCP_PACKAGE_NAME}@latest update`;
|
|
25
|
+
export const MCP_UPDATE_ALL_COMMAND = `${MCP_UPDATE_COMMAND} --all`;
|
|
25
26
|
export const MCP_SERVER_INSTRUCTIONS = [
|
|
26
27
|
`${MCP_PACKAGE_DESCRIPTION} (${MCP_PACKAGE_LABEL}).`,
|
|
27
|
-
`If the user or local config expects a newer EchoMem MCP version than ${MCP_PACKAGE_VERSION}, update once with \`${
|
|
28
|
+
`If the user or local config expects a newer EchoMem MCP version than ${MCP_PACKAGE_VERSION}, update once with \`${MCP_UPDATE_ALL_COMMAND}\` and start a new MCP session.`,
|
|
29
|
+
"Use echomem_update_status to check whether npm has a newer bridge; it is cached and non-blocking during normal tool listing.",
|
|
28
30
|
"Do not auto-update on every MCP startup; this bridge is intentionally stable between explicit updates.",
|
|
29
31
|
].join(" ");
|
|
30
32
|
export function withMcpVersion(description) {
|
|
31
|
-
return `${description}\n\nEchoMem MCP bridge: ${MCP_PACKAGE_LABEL}. If this version is stale, update once with \`${
|
|
33
|
+
return `${description}\n\nEchoMem MCP bridge: ${MCP_PACKAGE_LABEL}. If this version is stale, update once with \`${MCP_UPDATE_ALL_COMMAND}\` (or add \`--client cursor|windsurf|claude-desktop|claude-code|codex\` for a single client), then start a new MCP session. Do not run updates repeatedly or on every startup.`;
|
|
32
34
|
}
|
package/dist/setup.js
CHANGED
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
*/
|
|
15
15
|
import http from "node:http";
|
|
16
16
|
import { randomUUID } from "node:crypto";
|
|
17
|
-
import { spawn } from "node:child_process";
|
|
17
|
+
import { execFileSync, spawn } from "node:child_process";
|
|
18
18
|
import { Worker } from "node:worker_threads";
|
|
19
19
|
import fs from "node:fs";
|
|
20
20
|
import os from "node:os";
|
|
@@ -30,7 +30,8 @@ import { syncCodexUsage } from "./codex-sync.js";
|
|
|
30
30
|
import { renderSetupPage } from "./setup-page.js";
|
|
31
31
|
import { repoLabel } from "./forensics.js";
|
|
32
32
|
import { installHooks } from "./hud/hooks.js";
|
|
33
|
-
import { MCP_PACKAGE_LABEL, MCP_PACKAGE_VERSION, MCP_UPDATE_COMMAND } from "./package-metadata.js";
|
|
33
|
+
import { MCP_PACKAGE_LABEL, MCP_PACKAGE_NAME, MCP_PACKAGE_VERSION, MCP_UPDATE_ALL_COMMAND, MCP_UPDATE_COMMAND } from "./package-metadata.js";
|
|
34
|
+
import { checkLatestUpdateStatus, compareSemver, readCachedUpdateStatus } from "./update-check.js";
|
|
34
35
|
// The hosted connect-device page is now only the account-auth/token courier. The dashboard itself is
|
|
35
36
|
// served by this localhost bridge, where local logs and processed/unprocessed counts never leave the
|
|
36
37
|
// device unless the user explicitly starts migration. Override the hosted auth origin with ECHO_WEB_URL.
|
|
@@ -142,6 +143,259 @@ export function writeJsonClientConfig(configPath, entry) {
|
|
|
142
143
|
fs.mkdirSync(path.dirname(configPath), { recursive: true });
|
|
143
144
|
fs.writeFileSync(configPath, JSON.stringify(config, null, 2));
|
|
144
145
|
}
|
|
146
|
+
export function writeClaudeCodeConfig(entry) {
|
|
147
|
+
try {
|
|
148
|
+
execFileSync("claude", ["mcp", "add-json", "echomem", JSON.stringify(entry)], {
|
|
149
|
+
encoding: "utf8",
|
|
150
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
151
|
+
timeout: 10000,
|
|
152
|
+
});
|
|
153
|
+
return "wrote";
|
|
154
|
+
}
|
|
155
|
+
catch {
|
|
156
|
+
return "unavailable";
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
function readJsonClientEntry(configPath) {
|
|
160
|
+
try {
|
|
161
|
+
const config = JSON.parse(fs.readFileSync(configPath, "utf8"));
|
|
162
|
+
const servers = config.mcpServers;
|
|
163
|
+
if (!servers || typeof servers !== "object" || Array.isArray(servers))
|
|
164
|
+
return null;
|
|
165
|
+
const entry = servers.echomem;
|
|
166
|
+
return entry && typeof entry === "object" && !Array.isArray(entry) ? entry : null;
|
|
167
|
+
}
|
|
168
|
+
catch {
|
|
169
|
+
return null;
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
function readCodexEntry(configPath) {
|
|
173
|
+
let content = "";
|
|
174
|
+
try {
|
|
175
|
+
content = fs.readFileSync(configPath, "utf8");
|
|
176
|
+
}
|
|
177
|
+
catch {
|
|
178
|
+
return null;
|
|
179
|
+
}
|
|
180
|
+
const lines = content.split("\n");
|
|
181
|
+
const start = lines.findIndex((line) => /^\s*\[mcp_servers\.echomem\]\s*$/.test(line));
|
|
182
|
+
if (start < 0)
|
|
183
|
+
return null;
|
|
184
|
+
let end = start + 1;
|
|
185
|
+
while (end < lines.length && !/^\s*\[/.test(lines[end]))
|
|
186
|
+
end++;
|
|
187
|
+
const block = lines.slice(start, end);
|
|
188
|
+
const command = parseTomlString(block.find((line) => /^\s*command\s*=/.test(line)));
|
|
189
|
+
const args = parseTomlStringArray(block.find((line) => /^\s*args\s*=/.test(line)));
|
|
190
|
+
return command ? { command, args } : null;
|
|
191
|
+
}
|
|
192
|
+
function parseTomlString(line) {
|
|
193
|
+
if (!line)
|
|
194
|
+
return undefined;
|
|
195
|
+
const match = line.match(/=\s*(".*")\s*$/);
|
|
196
|
+
if (!match)
|
|
197
|
+
return undefined;
|
|
198
|
+
try {
|
|
199
|
+
const value = JSON.parse(match[1]);
|
|
200
|
+
return typeof value === "string" ? value : undefined;
|
|
201
|
+
}
|
|
202
|
+
catch {
|
|
203
|
+
return undefined;
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
function parseTomlStringArray(line) {
|
|
207
|
+
if (!line)
|
|
208
|
+
return [];
|
|
209
|
+
const match = line.match(/=\s*(\[.*\])\s*$/);
|
|
210
|
+
if (!match)
|
|
211
|
+
return [];
|
|
212
|
+
try {
|
|
213
|
+
const value = JSON.parse(match[1]);
|
|
214
|
+
return Array.isArray(value) ? value.filter((item) => typeof item === "string") : [];
|
|
215
|
+
}
|
|
216
|
+
catch {
|
|
217
|
+
return [];
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
function entryArgs(entry) {
|
|
221
|
+
return Array.isArray(entry.args) ? entry.args.filter((arg) => typeof arg === "string") : [];
|
|
222
|
+
}
|
|
223
|
+
function entryCommand(entry) {
|
|
224
|
+
return typeof entry.command === "string" && entry.command.trim() ? entry.command.trim() : undefined;
|
|
225
|
+
}
|
|
226
|
+
function entryTarget(entry) {
|
|
227
|
+
const command = entryCommand(entry) ?? "(missing command)";
|
|
228
|
+
const args = entryArgs(entry);
|
|
229
|
+
return [command, ...args].join(" ");
|
|
230
|
+
}
|
|
231
|
+
function packageVersionFromPath(entryPath) {
|
|
232
|
+
let current = entryPath;
|
|
233
|
+
try {
|
|
234
|
+
current = fs.realpathSync(entryPath);
|
|
235
|
+
}
|
|
236
|
+
catch {
|
|
237
|
+
/* keep the original path; it may still be inside an existing package dir */
|
|
238
|
+
}
|
|
239
|
+
try {
|
|
240
|
+
if (fs.existsSync(current) && fs.statSync(current).isFile())
|
|
241
|
+
current = path.dirname(current);
|
|
242
|
+
}
|
|
243
|
+
catch {
|
|
244
|
+
current = path.dirname(current);
|
|
245
|
+
}
|
|
246
|
+
for (let i = 0; i < 12; i += 1) {
|
|
247
|
+
const packagePath = path.join(current, "package.json");
|
|
248
|
+
try {
|
|
249
|
+
const pkg = JSON.parse(fs.readFileSync(packagePath, "utf8"));
|
|
250
|
+
if (pkg.name === MCP_PACKAGE_NAME && typeof pkg.version === "string") {
|
|
251
|
+
return { version: pkg.version, packagePath };
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
catch {
|
|
255
|
+
/* keep walking */
|
|
256
|
+
}
|
|
257
|
+
const parent = path.dirname(current);
|
|
258
|
+
if (parent === current)
|
|
259
|
+
break;
|
|
260
|
+
current = parent;
|
|
261
|
+
}
|
|
262
|
+
return null;
|
|
263
|
+
}
|
|
264
|
+
export function resolveServerEntryVersion(entry) {
|
|
265
|
+
const command = entryCommand(entry);
|
|
266
|
+
const args = entryArgs(entry);
|
|
267
|
+
const npxPackageArg = args.find((arg) => arg === MCP_PACKAGE_NAME || arg.startsWith(`${MCP_PACKAGE_NAME}@`));
|
|
268
|
+
if (command && path.basename(command).replace(/\.(cmd|exe)$/i, "") === "npx" && npxPackageArg) {
|
|
269
|
+
const suffix = npxPackageArg.slice(MCP_PACKAGE_NAME.length);
|
|
270
|
+
if (suffix === "@latest")
|
|
271
|
+
return { runtime: "latest" };
|
|
272
|
+
if (suffix.startsWith("@") && /^\d+\.\d+\.\d+/.test(suffix.slice(1)))
|
|
273
|
+
return { version: suffix.slice(1) };
|
|
274
|
+
return { runtime: "dynamic" };
|
|
275
|
+
}
|
|
276
|
+
const candidates = [...args, command].filter((value) => typeof value === "string");
|
|
277
|
+
for (const candidate of candidates) {
|
|
278
|
+
// Direct local/dev entries often look like ".../packages/mcp-server/dist/index.js";
|
|
279
|
+
// they do not contain the published package name, but walking upward still finds package.json.
|
|
280
|
+
const resolved = packageVersionFromPath(candidate);
|
|
281
|
+
if (resolved)
|
|
282
|
+
return resolved;
|
|
283
|
+
}
|
|
284
|
+
return {};
|
|
285
|
+
}
|
|
286
|
+
function versionState(version, desiredVersion) {
|
|
287
|
+
if (!version || !desiredVersion)
|
|
288
|
+
return "unknown";
|
|
289
|
+
const cmp = compareSemver(version, desiredVersion);
|
|
290
|
+
if (cmp < 0)
|
|
291
|
+
return "stale";
|
|
292
|
+
if (cmp > 0)
|
|
293
|
+
return "newer";
|
|
294
|
+
return "ok";
|
|
295
|
+
}
|
|
296
|
+
function inspectClientConfig(client, desiredVersion) {
|
|
297
|
+
if (client.kind === "snippet")
|
|
298
|
+
return inspectClaudeCodeConfig(client, desiredVersion);
|
|
299
|
+
if (client.kind === "json" && !fs.existsSync(path.dirname(client.configPath)))
|
|
300
|
+
return null;
|
|
301
|
+
if (client.kind === "command" && !fs.existsSync(client.detectDir))
|
|
302
|
+
return null;
|
|
303
|
+
const entry = client.kind === "json" ? readJsonClientEntry(client.configPath) : readCodexEntry(client.configPath);
|
|
304
|
+
if (!entry) {
|
|
305
|
+
return {
|
|
306
|
+
id: client.id,
|
|
307
|
+
label: client.label,
|
|
308
|
+
configured: false,
|
|
309
|
+
detail: `no EchoMem MCP entry in ${client.configPath}`,
|
|
310
|
+
state: "missing",
|
|
311
|
+
command: `${MCP_UPDATE_COMMAND} --client ${client.id}`,
|
|
312
|
+
};
|
|
313
|
+
}
|
|
314
|
+
const resolved = resolveServerEntryVersion(entry);
|
|
315
|
+
const state = resolved.runtime === "latest" ? "ok" : versionState(resolved.version, desiredVersion);
|
|
316
|
+
return {
|
|
317
|
+
id: client.id,
|
|
318
|
+
label: client.label,
|
|
319
|
+
configured: true,
|
|
320
|
+
detail: entryTarget(entry),
|
|
321
|
+
version: resolved.version,
|
|
322
|
+
packagePath: resolved.packagePath,
|
|
323
|
+
runtime: resolved.runtime,
|
|
324
|
+
state,
|
|
325
|
+
command: state === "stale" || state === "missing" ? `${MCP_UPDATE_COMMAND} --client ${client.id}` : undefined,
|
|
326
|
+
};
|
|
327
|
+
}
|
|
328
|
+
function inspectClaudeCodeConfig(client, desiredVersion) {
|
|
329
|
+
if (!fs.existsSync(home(".claude")))
|
|
330
|
+
return null;
|
|
331
|
+
try {
|
|
332
|
+
const output = execFileSync("claude", ["mcp", "list"], {
|
|
333
|
+
encoding: "utf8",
|
|
334
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
335
|
+
timeout: 3000,
|
|
336
|
+
});
|
|
337
|
+
const line = output.split("\n").find((item) => item.toLowerCase().includes("echomem"));
|
|
338
|
+
if (!line) {
|
|
339
|
+
return {
|
|
340
|
+
id: client.id,
|
|
341
|
+
label: client.label,
|
|
342
|
+
configured: false,
|
|
343
|
+
detail: "Claude Code reports no EchoMem MCP server",
|
|
344
|
+
state: "missing",
|
|
345
|
+
command: `${MCP_UPDATE_COMMAND} --client ${client.id}`,
|
|
346
|
+
};
|
|
347
|
+
}
|
|
348
|
+
const match = line.match(new RegExp(`${MCP_PACKAGE_NAME.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}@(\\d+\\.\\d+\\.\\d+)`));
|
|
349
|
+
const version = match?.[1];
|
|
350
|
+
return {
|
|
351
|
+
id: client.id,
|
|
352
|
+
label: client.label,
|
|
353
|
+
configured: true,
|
|
354
|
+
detail: line.trim(),
|
|
355
|
+
version,
|
|
356
|
+
state: versionState(version, desiredVersion),
|
|
357
|
+
command: version && versionState(version, desiredVersion) === "stale" ? `${MCP_UPDATE_COMMAND} --client ${client.id}` : undefined,
|
|
358
|
+
};
|
|
359
|
+
}
|
|
360
|
+
catch {
|
|
361
|
+
return {
|
|
362
|
+
id: client.id,
|
|
363
|
+
label: client.label,
|
|
364
|
+
configured: false,
|
|
365
|
+
detail: "Claude Code CLI not available for `claude mcp list`; add EchoMem from Claude Code manually",
|
|
366
|
+
state: "unknown",
|
|
367
|
+
};
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
function inspectClientConfigs(desiredVersion) {
|
|
371
|
+
return knownClients()
|
|
372
|
+
.map((client) => inspectClientConfig(client, desiredVersion))
|
|
373
|
+
.filter((report) => report !== null);
|
|
374
|
+
}
|
|
375
|
+
function formatClientConfigReport(report) {
|
|
376
|
+
const lines = [];
|
|
377
|
+
const status = report.state === "ok"
|
|
378
|
+
? "ok"
|
|
379
|
+
: report.state === "stale"
|
|
380
|
+
? "stale"
|
|
381
|
+
: report.state === "newer"
|
|
382
|
+
? "newer than this status command"
|
|
383
|
+
: report.state === "missing"
|
|
384
|
+
? "missing"
|
|
385
|
+
: "unknown";
|
|
386
|
+
const version = report.runtime === "latest"
|
|
387
|
+
? "latest at launch"
|
|
388
|
+
: report.runtime === "dynamic"
|
|
389
|
+
? "dynamic npm resolution"
|
|
390
|
+
: report.version
|
|
391
|
+
? `${MCP_PACKAGE_NAME}@${report.version}`
|
|
392
|
+
: "version unknown";
|
|
393
|
+
lines.push(` ${report.label}: ${report.configured ? "configured" : "not configured"} (${status}; ${version})`);
|
|
394
|
+
lines.push(` ${report.detail}`);
|
|
395
|
+
if (report.command)
|
|
396
|
+
lines.push(` Update: ${report.command}`);
|
|
397
|
+
return lines;
|
|
398
|
+
}
|
|
145
399
|
// ---------------------------------------------------------------------------
|
|
146
400
|
// Browser + localhost callback
|
|
147
401
|
// ---------------------------------------------------------------------------
|
|
@@ -768,11 +1022,11 @@ function parseFlags(argv) {
|
|
|
768
1022
|
async function cmdSetup(flags) {
|
|
769
1023
|
const entry = buildServerEntry({ devEntryPath: typeof flags.dev === "string" ? flags.dev : undefined });
|
|
770
1024
|
const requested = typeof flags.client === "string" ? flags.client : undefined;
|
|
771
|
-
const targets = (requested
|
|
1025
|
+
const targets = selectSetupTargets(requested, Boolean(flags.all));
|
|
772
1026
|
if (targets.length === 0) {
|
|
773
1027
|
console.log("No client auto-detected. Add this MCP server entry manually:\n");
|
|
774
1028
|
console.log(JSON.stringify({ echomem: entry }, null, 2));
|
|
775
|
-
console.log("\n(Or re-run with --client cursor|windsurf|claude-desktop|codex.)");
|
|
1029
|
+
console.log("\n(Or re-run with --client cursor|windsurf|claude-desktop|claude-code|codex, or --all.)");
|
|
776
1030
|
}
|
|
777
1031
|
else {
|
|
778
1032
|
for (const c of targets) {
|
|
@@ -788,7 +1042,13 @@ async function cmdSetup(flags) {
|
|
|
788
1042
|
console.log(`✅ ${c.label} already has the EchoMem MCP entry: ${c.configPath}`);
|
|
789
1043
|
}
|
|
790
1044
|
else {
|
|
791
|
-
|
|
1045
|
+
const result = c.id === "claude-code" ? writeClaudeCodeConfig(entry) : "unavailable";
|
|
1046
|
+
if (result === "wrote") {
|
|
1047
|
+
console.log(`✅ Wrote EchoMem MCP entry to ${c.label} via \`claude mcp add-json\` — start a new Claude Code session to load it.`);
|
|
1048
|
+
}
|
|
1049
|
+
else {
|
|
1050
|
+
console.log(`ℹ️ ${c.label}: ${c.note}\n entry: ${JSON.stringify(entry)}`);
|
|
1051
|
+
}
|
|
792
1052
|
}
|
|
793
1053
|
}
|
|
794
1054
|
}
|
|
@@ -806,6 +1066,18 @@ async function cmdUpdate(flags) {
|
|
|
806
1066
|
await cmdSetup({ ...flags, "skip-login": true });
|
|
807
1067
|
console.log(`Update config complete. Start a new MCP session to load ${MCP_PACKAGE_LABEL}.`);
|
|
808
1068
|
}
|
|
1069
|
+
function selectSetupTargets(requested, all) {
|
|
1070
|
+
if (all || requested === "all") {
|
|
1071
|
+
return knownClients().filter((client) => {
|
|
1072
|
+
if (client.kind === "json")
|
|
1073
|
+
return fs.existsSync(path.dirname(client.configPath));
|
|
1074
|
+
if (client.kind === "command")
|
|
1075
|
+
return fs.existsSync(client.detectDir);
|
|
1076
|
+
return client.id === "claude-code" && fs.existsSync(home(".claude"));
|
|
1077
|
+
});
|
|
1078
|
+
}
|
|
1079
|
+
return requested ? knownClients().filter((client) => client.id === requested) : detectClients();
|
|
1080
|
+
}
|
|
809
1081
|
async function cmdSetupHud(flags) {
|
|
810
1082
|
const client = parseHudClient(flags["hud-client"] || "auto");
|
|
811
1083
|
const hudCli = resolveHudCliPath();
|
|
@@ -1381,10 +1653,21 @@ async function cmdUnlock(flags) {
|
|
|
1381
1653
|
store.saveKey(keyB64);
|
|
1382
1654
|
console.log("✅ Vault unlocked. Reload your MCP client (or start a new session).");
|
|
1383
1655
|
}
|
|
1384
|
-
async function cmdStatus() {
|
|
1656
|
+
async function cmdStatus(flags = {}) {
|
|
1385
1657
|
const store = new KeyStore();
|
|
1386
1658
|
const token = store.getToken();
|
|
1387
1659
|
console.log(`EchoMem MCP: ${MCP_PACKAGE_LABEL}`);
|
|
1660
|
+
const updateStatus = flags["no-network"] ? readCachedUpdateStatus() : await checkLatestUpdateStatus({ force: true });
|
|
1661
|
+
const latest = updateStatus?.latestVersion;
|
|
1662
|
+
if (latest) {
|
|
1663
|
+
console.log(`Published latest: ${MCP_PACKAGE_NAME}@${latest}`);
|
|
1664
|
+
if (compareSemver(MCP_PACKAGE_VERSION, latest) < 0) {
|
|
1665
|
+
console.log(`This status command is older than latest. Update client configs with: ${MCP_UPDATE_ALL_COMMAND}`);
|
|
1666
|
+
}
|
|
1667
|
+
}
|
|
1668
|
+
else if (updateStatus?.error) {
|
|
1669
|
+
console.log(`Published latest: unavailable (${updateStatus.error})`);
|
|
1670
|
+
}
|
|
1388
1671
|
console.log(`Credentials file: ${store.path()}`);
|
|
1389
1672
|
console.log(`API token: ${token ? "present" : "MISSING — run `echomem-mcp login`"}`);
|
|
1390
1673
|
if (token) {
|
|
@@ -1408,6 +1691,14 @@ async function cmdStatus() {
|
|
|
1408
1691
|
console.log(`Encryption key: ${store.getKey() ? "present" : store.isKeyExpired() ? "EXPIRED — run `echomem-mcp unlock`" : "not set"}`);
|
|
1409
1692
|
const detected = detectClients();
|
|
1410
1693
|
console.log(`Detected clients: ${detected.length ? detected.map((c) => c.label).join(", ") : "none auto-detected"}`);
|
|
1694
|
+
const reports = inspectClientConfigs(latest ?? MCP_PACKAGE_VERSION);
|
|
1695
|
+
if (reports.length) {
|
|
1696
|
+
console.log("Client MCP configs:");
|
|
1697
|
+
for (const report of reports) {
|
|
1698
|
+
for (const line of formatClientConfigReport(report))
|
|
1699
|
+
console.log(line);
|
|
1700
|
+
}
|
|
1701
|
+
}
|
|
1411
1702
|
}
|
|
1412
1703
|
function cmdLogout() {
|
|
1413
1704
|
const store = new KeyStore();
|
|
@@ -1425,11 +1716,13 @@ Usage:
|
|
|
1425
1716
|
echomem-mcp Run the MCP server (stdio; default — used by your editor)
|
|
1426
1717
|
echomem-mcp setup [--client X] Detect editor, write its MCP config, then log in
|
|
1427
1718
|
echomem-mcp setup --skip-login Write MCP config without opening login/browser
|
|
1428
|
-
echomem-mcp update
|
|
1719
|
+
echomem-mcp update --all Repoint detected clients to this installed bridge; no login/browser
|
|
1720
|
+
echomem-mcp update --client X Repoint one MCP client; no login/browser
|
|
1429
1721
|
echomem-mcp setup --with-hud Configure MCP, then launch the EchoMem context HUD
|
|
1430
1722
|
echomem-mcp login Approve this device in the browser (or --token/--passphrase)
|
|
1431
1723
|
echomem-mcp unlock Re-derive the encryption key after its TTL (or --passphrase)
|
|
1432
1724
|
echomem-mcp status Show token/key/clients
|
|
1725
|
+
echomem-mcp doctor [--no-network] Diagnose configured client bridge versions
|
|
1433
1726
|
echomem-mcp logout Remove stored credentials
|
|
1434
1727
|
echomem-mcp report [--json] Your AI coding memory audit (local, no login, $0)
|
|
1435
1728
|
echomem-mcp migrate [--since D] Import your existing Codex/Claude history into your memory
|
|
@@ -1443,7 +1736,8 @@ Usage:
|
|
|
1443
1736
|
|
|
1444
1737
|
Manual / headless:
|
|
1445
1738
|
echomem-mcp login --token ec_xxx [--passphrase <vault pass> | --key <base64>]
|
|
1446
|
-
${
|
|
1739
|
+
${MCP_UPDATE_ALL_COMMAND} # one-shot latest update for detected clients, no browser login
|
|
1740
|
+
${MCP_UPDATE_COMMAND} --client codex # update one client only
|
|
1447
1741
|
echomem-mcp setup --dev /abs/path/dist/index.js # point clients at a local checkout
|
|
1448
1742
|
echomem-mcp setup --with-hud --install-hud-hooks --client codex [--hud-client auto]
|
|
1449
1743
|
echomem-mcp sync-usage --days 7 --limit 50 --dry-run
|
|
@@ -1468,7 +1762,10 @@ export async function runCli(argv) {
|
|
|
1468
1762
|
await cmdUnlock(flags);
|
|
1469
1763
|
return true;
|
|
1470
1764
|
case "status":
|
|
1471
|
-
await cmdStatus();
|
|
1765
|
+
await cmdStatus(flags);
|
|
1766
|
+
return true;
|
|
1767
|
+
case "doctor":
|
|
1768
|
+
await cmdStatus(flags);
|
|
1472
1769
|
return true;
|
|
1473
1770
|
case "logout":
|
|
1474
1771
|
cmdLogout();
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { echoConfigDir } from "./keystore.js";
|
|
4
|
+
import { MCP_PACKAGE_NAME, MCP_PACKAGE_VERSION, MCP_UPDATE_ALL_COMMAND, } from "./package-metadata.js";
|
|
5
|
+
export const UPDATE_CHECK_INTERVAL_MS = 24 * 60 * 60 * 1000;
|
|
6
|
+
const UPDATE_CHECK_TIMEOUT_MS = 3500;
|
|
7
|
+
export function compareSemver(a, b) {
|
|
8
|
+
const av = a.split(/[.-]/).map((part) => Number(part));
|
|
9
|
+
const bv = b.split(/[.-]/).map((part) => Number(part));
|
|
10
|
+
for (let i = 0; i < Math.max(av.length, bv.length, 3); i += 1) {
|
|
11
|
+
const ai = Number.isFinite(av[i]) ? av[i] : 0;
|
|
12
|
+
const bi = Number.isFinite(bv[i]) ? bv[i] : 0;
|
|
13
|
+
if (ai !== bi)
|
|
14
|
+
return ai > bi ? 1 : -1;
|
|
15
|
+
}
|
|
16
|
+
return 0;
|
|
17
|
+
}
|
|
18
|
+
export function updateCheckCachePath() {
|
|
19
|
+
return path.join(echoConfigDir(), "mcp-update-check.json");
|
|
20
|
+
}
|
|
21
|
+
export function readCachedUpdateStatus(nowMs = Date.now()) {
|
|
22
|
+
let raw;
|
|
23
|
+
try {
|
|
24
|
+
raw = JSON.parse(fs.readFileSync(updateCheckCachePath(), "utf8"));
|
|
25
|
+
}
|
|
26
|
+
catch {
|
|
27
|
+
return undefined;
|
|
28
|
+
}
|
|
29
|
+
if (raw.packageName !== MCP_PACKAGE_NAME)
|
|
30
|
+
return undefined;
|
|
31
|
+
if (typeof raw.latestVersion !== "string" || typeof raw.checkedAt !== "string")
|
|
32
|
+
return undefined;
|
|
33
|
+
const checkedMs = Date.parse(raw.checkedAt);
|
|
34
|
+
if (!Number.isFinite(checkedMs))
|
|
35
|
+
return undefined;
|
|
36
|
+
return {
|
|
37
|
+
packageName: MCP_PACKAGE_NAME,
|
|
38
|
+
currentVersion: MCP_PACKAGE_VERSION,
|
|
39
|
+
latestVersion: raw.latestVersion,
|
|
40
|
+
checkedAt: raw.checkedAt,
|
|
41
|
+
updateAvailable: compareSemver(MCP_PACKAGE_VERSION, raw.latestVersion) < 0,
|
|
42
|
+
source: "cache",
|
|
43
|
+
command: MCP_UPDATE_ALL_COMMAND,
|
|
44
|
+
error: nowMs - checkedMs > UPDATE_CHECK_INTERVAL_MS ? "cached result is older than 24h" : undefined,
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
export function isUpdateCheckFresh(status, nowMs = Date.now()) {
|
|
48
|
+
if (!status?.checkedAt)
|
|
49
|
+
return false;
|
|
50
|
+
const checkedMs = Date.parse(status.checkedAt);
|
|
51
|
+
return Number.isFinite(checkedMs) && nowMs - checkedMs <= UPDATE_CHECK_INTERVAL_MS;
|
|
52
|
+
}
|
|
53
|
+
export async function checkLatestUpdateStatus(opts = {}) {
|
|
54
|
+
const nowMs = opts.nowMs ?? Date.now();
|
|
55
|
+
const cached = readCachedUpdateStatus(nowMs);
|
|
56
|
+
if (process.env.ECHO_DISABLE_UPDATE_CHECK === "1") {
|
|
57
|
+
return cached ?? baseStatus("disabled");
|
|
58
|
+
}
|
|
59
|
+
if (!opts.force && cached && isUpdateCheckFresh(cached, nowMs))
|
|
60
|
+
return cached;
|
|
61
|
+
const registryUrl = opts.registryUrl
|
|
62
|
+
|| process.env.ECHO_MCP_UPDATE_REGISTRY_URL
|
|
63
|
+
|| `https://registry.npmjs.org/${encodeURIComponent(MCP_PACKAGE_NAME)}/latest`;
|
|
64
|
+
try {
|
|
65
|
+
const signal = AbortSignal.timeout(opts.timeoutMs ?? UPDATE_CHECK_TIMEOUT_MS);
|
|
66
|
+
const res = await fetch(registryUrl, {
|
|
67
|
+
headers: { accept: "application/json" },
|
|
68
|
+
signal,
|
|
69
|
+
});
|
|
70
|
+
if (!res.ok)
|
|
71
|
+
throw new Error(`npm registry HTTP ${res.status}`);
|
|
72
|
+
const data = await res.json();
|
|
73
|
+
const latestVersion = typeof data.version === "string" ? data.version.trim() : "";
|
|
74
|
+
if (!latestVersion)
|
|
75
|
+
throw new Error("npm registry response missing version");
|
|
76
|
+
const checkedAt = new Date(nowMs).toISOString();
|
|
77
|
+
const status = {
|
|
78
|
+
packageName: MCP_PACKAGE_NAME,
|
|
79
|
+
currentVersion: MCP_PACKAGE_VERSION,
|
|
80
|
+
latestVersion,
|
|
81
|
+
checkedAt,
|
|
82
|
+
updateAvailable: compareSemver(MCP_PACKAGE_VERSION, latestVersion) < 0,
|
|
83
|
+
source: "network",
|
|
84
|
+
command: MCP_UPDATE_ALL_COMMAND,
|
|
85
|
+
};
|
|
86
|
+
writeUpdateCache(status);
|
|
87
|
+
return status;
|
|
88
|
+
}
|
|
89
|
+
catch (error) {
|
|
90
|
+
return {
|
|
91
|
+
...(cached ?? baseStatus("none")),
|
|
92
|
+
error: error instanceof Error ? error.message : String(error),
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
export function startBackgroundUpdateCheck(onStatus) {
|
|
97
|
+
const cached = readCachedUpdateStatus();
|
|
98
|
+
if (cached)
|
|
99
|
+
onStatus(cached);
|
|
100
|
+
if (process.env.ECHO_DISABLE_UPDATE_CHECK === "1" || isUpdateCheckFresh(cached))
|
|
101
|
+
return cached;
|
|
102
|
+
void checkLatestUpdateStatus({ force: true }).then(onStatus).catch(() => {
|
|
103
|
+
/* Update checks are advisory; never affect MCP startup. */
|
|
104
|
+
});
|
|
105
|
+
return cached;
|
|
106
|
+
}
|
|
107
|
+
export function formatUpdateNotice(status) {
|
|
108
|
+
if (!status?.updateAvailable || !status.latestVersion)
|
|
109
|
+
return undefined;
|
|
110
|
+
return [
|
|
111
|
+
`EchoMem update available: installed ${status.currentVersion}, latest ${status.latestVersion}.`,
|
|
112
|
+
`Offer to update, then run \`${status.command}\` if the user agrees.`,
|
|
113
|
+
"After updating, start a new agent/MCP session.",
|
|
114
|
+
].join(" ");
|
|
115
|
+
}
|
|
116
|
+
export function formatUpdateStatusText(status) {
|
|
117
|
+
const lines = [
|
|
118
|
+
`EchoMem MCP installed: ${status.packageName}@${status.currentVersion}`,
|
|
119
|
+
status.latestVersion ? `Latest published: ${status.packageName}@${status.latestVersion}` : "Latest published: unknown",
|
|
120
|
+
`Update available: ${status.updateAvailable ? "yes" : "no"}`,
|
|
121
|
+
];
|
|
122
|
+
if (status.updateAvailable) {
|
|
123
|
+
lines.push(`Update command: ${status.command}`);
|
|
124
|
+
lines.push("After updating, start a new agent/MCP session.");
|
|
125
|
+
}
|
|
126
|
+
if (status.checkedAt)
|
|
127
|
+
lines.push(`Checked at: ${status.checkedAt} (${status.source})`);
|
|
128
|
+
else
|
|
129
|
+
lines.push(`Checked at: not checked (${status.source})`);
|
|
130
|
+
if (status.error)
|
|
131
|
+
lines.push(`Check note: ${status.error}`);
|
|
132
|
+
return lines.join("\n");
|
|
133
|
+
}
|
|
134
|
+
function baseStatus(source) {
|
|
135
|
+
return {
|
|
136
|
+
packageName: MCP_PACKAGE_NAME,
|
|
137
|
+
currentVersion: MCP_PACKAGE_VERSION,
|
|
138
|
+
updateAvailable: false,
|
|
139
|
+
source,
|
|
140
|
+
command: MCP_UPDATE_ALL_COMMAND,
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
function writeUpdateCache(status) {
|
|
144
|
+
if (!status.latestVersion || !status.checkedAt)
|
|
145
|
+
return;
|
|
146
|
+
const dir = echoConfigDir();
|
|
147
|
+
fs.mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
148
|
+
fs.writeFileSync(updateCheckCachePath(), JSON.stringify({
|
|
149
|
+
packageName: status.packageName,
|
|
150
|
+
currentVersion: status.currentVersion,
|
|
151
|
+
latestVersion: status.latestVersion,
|
|
152
|
+
checkedAt: status.checkedAt,
|
|
153
|
+
}, null, 2), { mode: 0o600 });
|
|
154
|
+
}
|
package/dist/v1-contract.js
CHANGED
|
@@ -7,6 +7,7 @@ export const canonicalToolNames = {
|
|
|
7
7
|
keywords: "search_memories_by_keywords",
|
|
8
8
|
others: "search_others_memories",
|
|
9
9
|
report: "echomem_usage_report",
|
|
10
|
+
updateStatus: "echomem_update_status",
|
|
10
11
|
contextHealth: "echo_context_health",
|
|
11
12
|
recompose: "echo_recompose",
|
|
12
13
|
delete: "delete_memory",
|
|
@@ -75,13 +76,15 @@ export const getByContextSchema = z.object({
|
|
|
75
76
|
export function listToolSpecs(opts = {}) {
|
|
76
77
|
const currentTime = new Date().toISOString();
|
|
77
78
|
const map = opts.map?.trim();
|
|
79
|
+
const updateNotice = opts.updateNotice?.trim();
|
|
80
|
+
const updateSection = updateNotice ? `\n\nUPDATE NOTICE: ${updateNotice}` : "";
|
|
78
81
|
const mapSection = map
|
|
79
82
|
? `\n\nThis user's EchoMem currently covers these topics (a relevance guide — recall when the task relates to one of them):\n${map}\n`
|
|
80
83
|
: "";
|
|
81
84
|
return [
|
|
82
85
|
{
|
|
83
86
|
name: canonicalToolNames.search,
|
|
84
|
-
description: withMcpVersion(`Recall the user's prior decisions, preferences, constraints, and project context from EchoMem — their long-term memory across ALL their AI tools (Claude.ai, ChatGPT, other agents), not just this session.${mapSection}\nCall this when the task plausibly relates to that remembered context — a topic above, or when the user refers to past work ("what did we decide", "like before", "the usual") — so you don't re-derive or re-ask what they already settled. Skip it for self-contained tasks with no link to their history (e.g. a generic algorithm question). By default this returns only the ranked memories retrieved for recall and skips EchoMem answer generation; set includeAnswer=true only when you explicitly need EchoMem's legacy synthesized recall. Current time: ${currentTime}
|
|
87
|
+
description: withMcpVersion(`Recall the user's prior decisions, preferences, constraints, and project context from EchoMem — their long-term memory across ALL their AI tools (Claude.ai, ChatGPT, other agents), not just this session.${mapSection}\nCall this when the task plausibly relates to that remembered context — a topic above, or when the user refers to past work ("what did we decide", "like before", "the usual") — so you don't re-derive or re-ask what they already settled. Skip it for self-contained tasks with no link to their history (e.g. a generic algorithm question). By default this returns only the ranked memories retrieved for recall and skips EchoMem answer generation; set includeAnswer=true only when you explicitly need EchoMem's legacy synthesized recall. Current time: ${currentTime}.${updateSection}`),
|
|
85
88
|
inputSchema: {
|
|
86
89
|
type: "object",
|
|
87
90
|
properties: {
|
|
@@ -254,6 +257,20 @@ export function listToolSpecs(opts = {}) {
|
|
|
254
257
|
description: "Show the user a one-screen audit of THEIR OWN AI coding usage — computed locally from their Codex/Claude Code logs ($0, nothing uploaded): how many tokens their agents spent, how much was re-reading context, reads vs memory recalls, and what changes with EchoMem. Call this when the user asks about their usage, token spend, cost, how much they're wasting, or wants a summary of their agent activity — and you may offer it once right after EchoMem is first connected. Returns formatted text to show the user verbatim. Needs no login.",
|
|
255
258
|
inputSchema: { type: "object", properties: {} },
|
|
256
259
|
},
|
|
260
|
+
{
|
|
261
|
+
name: canonicalToolNames.updateStatus,
|
|
262
|
+
description: `Check whether this installed EchoMem MCP bridge is behind the latest published npm version. Works without login, uploads no user transcript, and normal background checks are cached so EchoMem does not hit npm on every startup. If it reports an update, tell the user and offer to run the returned update command; after updating, the user must start a new agent/MCP session.${updateSection}`,
|
|
263
|
+
inputSchema: {
|
|
264
|
+
type: "object",
|
|
265
|
+
properties: {
|
|
266
|
+
force: {
|
|
267
|
+
type: "boolean",
|
|
268
|
+
default: false,
|
|
269
|
+
description: "When true, refresh npm latest now instead of using the cached check.",
|
|
270
|
+
},
|
|
271
|
+
},
|
|
272
|
+
},
|
|
273
|
+
},
|
|
257
274
|
{
|
|
258
275
|
name: canonicalToolNames.contextHealth,
|
|
259
276
|
description: "Show the current local Codex/Claude context-health score as markdown: clean percentage, tracked lower-bound dead-weight, redundant reads, source client, and token-count source. Use when the user asks about the context HUD, dirty context, context pollution, whether cleanup is worth it, or wants an in-chat fallback to the passive HUD. This reads local agent logs only and needs no login.",
|