@echomem/mcp 1.4.1 → 1.4.3
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 +42 -7
- package/assets/hud/echo-face-cutout.png +0 -0
- package/dist/hud/adapters.js +288 -0
- package/dist/hud/api.js +29 -0
- package/dist/hud/capsule.js +125 -0
- package/dist/hud/cli.js +142 -0
- package/dist/hud/electron-main.js +224 -0
- package/dist/hud/fs.js +63 -0
- package/dist/hud/hooks.js +50 -0
- package/dist/hud/metric.js +158 -0
- package/dist/hud/monitor.js +106 -0
- package/dist/hud/preload.cjs +10 -0
- package/dist/hud/render.js +39 -0
- package/dist/hud/report.js +125 -0
- package/dist/hud/server.js +95 -0
- package/dist/hud/web.js +509 -0
- package/dist/index.js +119 -6
- package/dist/package-metadata.js +32 -0
- package/dist/report.js +1 -1
- package/dist/setup.js +386 -5
- package/dist/v1-contract.js +61 -2
- package/package.json +12 -8
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";
|
|
@@ -29,6 +29,8 @@ import { cmdMigrate, applyAccountImportStatus, applyFastAccountImportStatus, dis
|
|
|
29
29
|
import { syncCodexUsage } from "./codex-sync.js";
|
|
30
30
|
import { renderSetupPage } from "./setup-page.js";
|
|
31
31
|
import { repoLabel } from "./forensics.js";
|
|
32
|
+
import { installHooks } from "./hud/hooks.js";
|
|
33
|
+
import { MCP_PACKAGE_LABEL, MCP_PACKAGE_NAME, MCP_PACKAGE_VERSION, MCP_UPDATE_COMMAND } from "./package-metadata.js";
|
|
32
34
|
// The hosted connect-device page is now only the account-auth/token courier. The dashboard itself is
|
|
33
35
|
// served by this localhost bridge, where local logs and processed/unprocessed counts never leave the
|
|
34
36
|
// device unless the user explicitly starts migration. Override the hosted auth origin with ECHO_WEB_URL.
|
|
@@ -140,6 +142,283 @@ export function writeJsonClientConfig(configPath, entry) {
|
|
|
140
142
|
fs.mkdirSync(path.dirname(configPath), { recursive: true });
|
|
141
143
|
fs.writeFileSync(configPath, JSON.stringify(config, null, 2));
|
|
142
144
|
}
|
|
145
|
+
export function writeClaudeCodeConfig(entry) {
|
|
146
|
+
try {
|
|
147
|
+
execFileSync("claude", ["mcp", "add-json", "echomem", JSON.stringify(entry)], {
|
|
148
|
+
encoding: "utf8",
|
|
149
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
150
|
+
timeout: 10000,
|
|
151
|
+
});
|
|
152
|
+
return "wrote";
|
|
153
|
+
}
|
|
154
|
+
catch {
|
|
155
|
+
return "unavailable";
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
function readJsonClientEntry(configPath) {
|
|
159
|
+
try {
|
|
160
|
+
const config = JSON.parse(fs.readFileSync(configPath, "utf8"));
|
|
161
|
+
const servers = config.mcpServers;
|
|
162
|
+
if (!servers || typeof servers !== "object" || Array.isArray(servers))
|
|
163
|
+
return null;
|
|
164
|
+
const entry = servers.echomem;
|
|
165
|
+
return entry && typeof entry === "object" && !Array.isArray(entry) ? entry : null;
|
|
166
|
+
}
|
|
167
|
+
catch {
|
|
168
|
+
return null;
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
function readCodexEntry(configPath) {
|
|
172
|
+
let content = "";
|
|
173
|
+
try {
|
|
174
|
+
content = fs.readFileSync(configPath, "utf8");
|
|
175
|
+
}
|
|
176
|
+
catch {
|
|
177
|
+
return null;
|
|
178
|
+
}
|
|
179
|
+
const lines = content.split("\n");
|
|
180
|
+
const start = lines.findIndex((line) => /^\s*\[mcp_servers\.echomem\]\s*$/.test(line));
|
|
181
|
+
if (start < 0)
|
|
182
|
+
return null;
|
|
183
|
+
let end = start + 1;
|
|
184
|
+
while (end < lines.length && !/^\s*\[/.test(lines[end]))
|
|
185
|
+
end++;
|
|
186
|
+
const block = lines.slice(start, end);
|
|
187
|
+
const command = parseTomlString(block.find((line) => /^\s*command\s*=/.test(line)));
|
|
188
|
+
const args = parseTomlStringArray(block.find((line) => /^\s*args\s*=/.test(line)));
|
|
189
|
+
return command ? { command, args } : null;
|
|
190
|
+
}
|
|
191
|
+
function parseTomlString(line) {
|
|
192
|
+
if (!line)
|
|
193
|
+
return undefined;
|
|
194
|
+
const match = line.match(/=\s*(".*")\s*$/);
|
|
195
|
+
if (!match)
|
|
196
|
+
return undefined;
|
|
197
|
+
try {
|
|
198
|
+
const value = JSON.parse(match[1]);
|
|
199
|
+
return typeof value === "string" ? value : undefined;
|
|
200
|
+
}
|
|
201
|
+
catch {
|
|
202
|
+
return undefined;
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
function parseTomlStringArray(line) {
|
|
206
|
+
if (!line)
|
|
207
|
+
return [];
|
|
208
|
+
const match = line.match(/=\s*(\[.*\])\s*$/);
|
|
209
|
+
if (!match)
|
|
210
|
+
return [];
|
|
211
|
+
try {
|
|
212
|
+
const value = JSON.parse(match[1]);
|
|
213
|
+
return Array.isArray(value) ? value.filter((item) => typeof item === "string") : [];
|
|
214
|
+
}
|
|
215
|
+
catch {
|
|
216
|
+
return [];
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
function entryArgs(entry) {
|
|
220
|
+
return Array.isArray(entry.args) ? entry.args.filter((arg) => typeof arg === "string") : [];
|
|
221
|
+
}
|
|
222
|
+
function entryCommand(entry) {
|
|
223
|
+
return typeof entry.command === "string" && entry.command.trim() ? entry.command.trim() : undefined;
|
|
224
|
+
}
|
|
225
|
+
function entryTarget(entry) {
|
|
226
|
+
const command = entryCommand(entry) ?? "(missing command)";
|
|
227
|
+
const args = entryArgs(entry);
|
|
228
|
+
return [command, ...args].join(" ");
|
|
229
|
+
}
|
|
230
|
+
function packageVersionFromPath(entryPath) {
|
|
231
|
+
let current = entryPath;
|
|
232
|
+
try {
|
|
233
|
+
current = fs.realpathSync(entryPath);
|
|
234
|
+
}
|
|
235
|
+
catch {
|
|
236
|
+
/* keep the original path; it may still be inside an existing package dir */
|
|
237
|
+
}
|
|
238
|
+
try {
|
|
239
|
+
if (fs.existsSync(current) && fs.statSync(current).isFile())
|
|
240
|
+
current = path.dirname(current);
|
|
241
|
+
}
|
|
242
|
+
catch {
|
|
243
|
+
current = path.dirname(current);
|
|
244
|
+
}
|
|
245
|
+
for (let i = 0; i < 12; i += 1) {
|
|
246
|
+
const packagePath = path.join(current, "package.json");
|
|
247
|
+
try {
|
|
248
|
+
const pkg = JSON.parse(fs.readFileSync(packagePath, "utf8"));
|
|
249
|
+
if (pkg.name === MCP_PACKAGE_NAME && typeof pkg.version === "string") {
|
|
250
|
+
return { version: pkg.version, packagePath };
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
catch {
|
|
254
|
+
/* keep walking */
|
|
255
|
+
}
|
|
256
|
+
const parent = path.dirname(current);
|
|
257
|
+
if (parent === current)
|
|
258
|
+
break;
|
|
259
|
+
current = parent;
|
|
260
|
+
}
|
|
261
|
+
return null;
|
|
262
|
+
}
|
|
263
|
+
export function resolveServerEntryVersion(entry) {
|
|
264
|
+
const command = entryCommand(entry);
|
|
265
|
+
const args = entryArgs(entry);
|
|
266
|
+
const npxPackageArg = args.find((arg) => arg === MCP_PACKAGE_NAME || arg.startsWith(`${MCP_PACKAGE_NAME}@`));
|
|
267
|
+
if (command && path.basename(command).replace(/\.(cmd|exe)$/i, "") === "npx" && npxPackageArg) {
|
|
268
|
+
const suffix = npxPackageArg.slice(MCP_PACKAGE_NAME.length);
|
|
269
|
+
if (suffix === "@latest")
|
|
270
|
+
return { runtime: "latest" };
|
|
271
|
+
if (suffix.startsWith("@") && /^\d+\.\d+\.\d+/.test(suffix.slice(1)))
|
|
272
|
+
return { version: suffix.slice(1) };
|
|
273
|
+
return { runtime: "dynamic" };
|
|
274
|
+
}
|
|
275
|
+
const candidates = [...args, command].filter((value) => typeof value === "string");
|
|
276
|
+
for (const candidate of candidates) {
|
|
277
|
+
if (!candidate.includes(MCP_PACKAGE_NAME) && !candidate.includes(`${path.sep}echomem-mcp`))
|
|
278
|
+
continue;
|
|
279
|
+
const resolved = packageVersionFromPath(candidate);
|
|
280
|
+
if (resolved)
|
|
281
|
+
return resolved;
|
|
282
|
+
}
|
|
283
|
+
return {};
|
|
284
|
+
}
|
|
285
|
+
function compareSemver(a, b) {
|
|
286
|
+
const av = a.split(/[.-]/).map((part) => Number(part));
|
|
287
|
+
const bv = b.split(/[.-]/).map((part) => Number(part));
|
|
288
|
+
for (let i = 0; i < Math.max(av.length, bv.length, 3); i += 1) {
|
|
289
|
+
const ai = Number.isFinite(av[i]) ? av[i] : 0;
|
|
290
|
+
const bi = Number.isFinite(bv[i]) ? bv[i] : 0;
|
|
291
|
+
if (ai !== bi)
|
|
292
|
+
return ai > bi ? 1 : -1;
|
|
293
|
+
}
|
|
294
|
+
return 0;
|
|
295
|
+
}
|
|
296
|
+
function versionState(version, desiredVersion) {
|
|
297
|
+
if (!version || !desiredVersion)
|
|
298
|
+
return "unknown";
|
|
299
|
+
const cmp = compareSemver(version, desiredVersion);
|
|
300
|
+
if (cmp < 0)
|
|
301
|
+
return "stale";
|
|
302
|
+
if (cmp > 0)
|
|
303
|
+
return "newer";
|
|
304
|
+
return "ok";
|
|
305
|
+
}
|
|
306
|
+
function readLatestPublishedVersion() {
|
|
307
|
+
try {
|
|
308
|
+
const raw = execFileSync("npm", ["view", MCP_PACKAGE_NAME, "version", "--silent"], {
|
|
309
|
+
encoding: "utf8",
|
|
310
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
311
|
+
timeout: 3500,
|
|
312
|
+
}).trim();
|
|
313
|
+
return /^\d+\.\d+\.\d+/.test(raw) ? raw : undefined;
|
|
314
|
+
}
|
|
315
|
+
catch {
|
|
316
|
+
return undefined;
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
function inspectClientConfig(client, desiredVersion) {
|
|
320
|
+
if (client.kind === "snippet")
|
|
321
|
+
return inspectClaudeCodeConfig(client, desiredVersion);
|
|
322
|
+
if (client.kind === "json" && !fs.existsSync(path.dirname(client.configPath)))
|
|
323
|
+
return null;
|
|
324
|
+
if (client.kind === "command" && !fs.existsSync(client.detectDir))
|
|
325
|
+
return null;
|
|
326
|
+
const entry = client.kind === "json" ? readJsonClientEntry(client.configPath) : readCodexEntry(client.configPath);
|
|
327
|
+
if (!entry) {
|
|
328
|
+
return {
|
|
329
|
+
id: client.id,
|
|
330
|
+
label: client.label,
|
|
331
|
+
configured: false,
|
|
332
|
+
detail: `no EchoMem MCP entry in ${client.configPath}`,
|
|
333
|
+
state: "missing",
|
|
334
|
+
command: `${MCP_UPDATE_COMMAND} --client ${client.id}`,
|
|
335
|
+
};
|
|
336
|
+
}
|
|
337
|
+
const resolved = resolveServerEntryVersion(entry);
|
|
338
|
+
const state = resolved.runtime === "latest" ? "ok" : versionState(resolved.version, desiredVersion);
|
|
339
|
+
return {
|
|
340
|
+
id: client.id,
|
|
341
|
+
label: client.label,
|
|
342
|
+
configured: true,
|
|
343
|
+
detail: entryTarget(entry),
|
|
344
|
+
version: resolved.version,
|
|
345
|
+
packagePath: resolved.packagePath,
|
|
346
|
+
runtime: resolved.runtime,
|
|
347
|
+
state,
|
|
348
|
+
command: state === "stale" || state === "missing" ? `${MCP_UPDATE_COMMAND} --client ${client.id}` : undefined,
|
|
349
|
+
};
|
|
350
|
+
}
|
|
351
|
+
function inspectClaudeCodeConfig(client, desiredVersion) {
|
|
352
|
+
if (!fs.existsSync(home(".claude")))
|
|
353
|
+
return null;
|
|
354
|
+
try {
|
|
355
|
+
const output = execFileSync("claude", ["mcp", "list"], {
|
|
356
|
+
encoding: "utf8",
|
|
357
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
358
|
+
timeout: 3000,
|
|
359
|
+
});
|
|
360
|
+
const line = output.split("\n").find((item) => item.toLowerCase().includes("echomem"));
|
|
361
|
+
if (!line) {
|
|
362
|
+
return {
|
|
363
|
+
id: client.id,
|
|
364
|
+
label: client.label,
|
|
365
|
+
configured: false,
|
|
366
|
+
detail: "Claude Code reports no EchoMem MCP server",
|
|
367
|
+
state: "missing",
|
|
368
|
+
command: `${MCP_UPDATE_COMMAND} --client ${client.id}`,
|
|
369
|
+
};
|
|
370
|
+
}
|
|
371
|
+
const match = line.match(new RegExp(`${MCP_PACKAGE_NAME.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}@(\\d+\\.\\d+\\.\\d+)`));
|
|
372
|
+
const version = match?.[1];
|
|
373
|
+
return {
|
|
374
|
+
id: client.id,
|
|
375
|
+
label: client.label,
|
|
376
|
+
configured: true,
|
|
377
|
+
detail: line.trim(),
|
|
378
|
+
version,
|
|
379
|
+
state: versionState(version, desiredVersion),
|
|
380
|
+
command: version && versionState(version, desiredVersion) === "stale" ? `${MCP_UPDATE_COMMAND} --client ${client.id}` : undefined,
|
|
381
|
+
};
|
|
382
|
+
}
|
|
383
|
+
catch {
|
|
384
|
+
return {
|
|
385
|
+
id: client.id,
|
|
386
|
+
label: client.label,
|
|
387
|
+
configured: false,
|
|
388
|
+
detail: "Claude Code CLI not available for `claude mcp list`; add EchoMem from Claude Code manually",
|
|
389
|
+
state: "unknown",
|
|
390
|
+
};
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
function inspectClientConfigs(desiredVersion) {
|
|
394
|
+
return knownClients()
|
|
395
|
+
.map((client) => inspectClientConfig(client, desiredVersion))
|
|
396
|
+
.filter((report) => report !== null);
|
|
397
|
+
}
|
|
398
|
+
function formatClientConfigReport(report) {
|
|
399
|
+
const lines = [];
|
|
400
|
+
const status = report.state === "ok"
|
|
401
|
+
? "ok"
|
|
402
|
+
: report.state === "stale"
|
|
403
|
+
? "stale"
|
|
404
|
+
: report.state === "newer"
|
|
405
|
+
? "newer than this status command"
|
|
406
|
+
: report.state === "missing"
|
|
407
|
+
? "missing"
|
|
408
|
+
: "unknown";
|
|
409
|
+
const version = report.runtime === "latest"
|
|
410
|
+
? "latest at launch"
|
|
411
|
+
: report.runtime === "dynamic"
|
|
412
|
+
? "dynamic npm resolution"
|
|
413
|
+
: report.version
|
|
414
|
+
? `${MCP_PACKAGE_NAME}@${report.version}`
|
|
415
|
+
: "version unknown";
|
|
416
|
+
lines.push(` ${report.label}: ${report.configured ? "configured" : "not configured"} (${status}; ${version})`);
|
|
417
|
+
lines.push(` ${report.detail}`);
|
|
418
|
+
if (report.command)
|
|
419
|
+
lines.push(` Update: ${report.command}`);
|
|
420
|
+
return lines;
|
|
421
|
+
}
|
|
143
422
|
// ---------------------------------------------------------------------------
|
|
144
423
|
// Browser + localhost callback
|
|
145
424
|
// ---------------------------------------------------------------------------
|
|
@@ -786,12 +1065,66 @@ async function cmdSetup(flags) {
|
|
|
786
1065
|
console.log(`✅ ${c.label} already has the EchoMem MCP entry: ${c.configPath}`);
|
|
787
1066
|
}
|
|
788
1067
|
else {
|
|
789
|
-
|
|
1068
|
+
const result = c.id === "claude-code" ? writeClaudeCodeConfig(entry) : "unavailable";
|
|
1069
|
+
if (result === "wrote") {
|
|
1070
|
+
console.log(`✅ Wrote EchoMem MCP entry to ${c.label} via \`claude mcp add-json\` — start a new Claude Code session to load it.`);
|
|
1071
|
+
}
|
|
1072
|
+
else {
|
|
1073
|
+
console.log(`ℹ️ ${c.label}: ${c.note}\n entry: ${JSON.stringify(entry)}`);
|
|
1074
|
+
}
|
|
790
1075
|
}
|
|
791
1076
|
}
|
|
792
1077
|
}
|
|
793
1078
|
console.log("");
|
|
794
|
-
|
|
1079
|
+
if (flags["skip-login"] || flags["no-login"]) {
|
|
1080
|
+
console.log(`Skipped login; existing EchoMem credentials are unchanged. Current bridge: ${MCP_PACKAGE_LABEL}`);
|
|
1081
|
+
}
|
|
1082
|
+
else {
|
|
1083
|
+
await cmdLogin(flags);
|
|
1084
|
+
}
|
|
1085
|
+
if (flags["with-hud"])
|
|
1086
|
+
await cmdSetupHud(flags);
|
|
1087
|
+
}
|
|
1088
|
+
async function cmdUpdate(flags) {
|
|
1089
|
+
await cmdSetup({ ...flags, "skip-login": true });
|
|
1090
|
+
console.log(`Update config complete. Start a new MCP session to load ${MCP_PACKAGE_LABEL}.`);
|
|
1091
|
+
}
|
|
1092
|
+
async function cmdSetupHud(flags) {
|
|
1093
|
+
const client = parseHudClient(flags["hud-client"] || "auto");
|
|
1094
|
+
const hudCli = resolveHudCliPath();
|
|
1095
|
+
console.log("");
|
|
1096
|
+
console.log(`✅ EchoMem HUD available: ${process.execPath} ${hudCli}`);
|
|
1097
|
+
if (flags["install-hud-hooks"]) {
|
|
1098
|
+
const paths = installHooks(client === "claude-desktop" ? "auto" : client);
|
|
1099
|
+
console.log(`✅ Installed EchoMem HUD hook support:\n${paths.map((p) => ` - ${p}`).join("\n")}`);
|
|
1100
|
+
console.log(" Codex users: run /hooks in a new Codex session to review and trust changed hooks.");
|
|
1101
|
+
}
|
|
1102
|
+
else {
|
|
1103
|
+
console.log("ℹ️ HUD hooks not installed. Add --install-hud-hooks if you want lifecycle wakeups.");
|
|
1104
|
+
}
|
|
1105
|
+
if (!flags["no-launch-hud"]) {
|
|
1106
|
+
try {
|
|
1107
|
+
spawn(process.execPath, [hudCli, "app", "--client", client], { stdio: "ignore", detached: true }).unref();
|
|
1108
|
+
console.log("✅ Launched EchoMem HUD app.");
|
|
1109
|
+
}
|
|
1110
|
+
catch {
|
|
1111
|
+
console.log(`ℹ️ Could not auto-launch HUD. Run: echomem-hud app --client ${client}`);
|
|
1112
|
+
}
|
|
1113
|
+
}
|
|
1114
|
+
else {
|
|
1115
|
+
console.log(`Run the HUD later with: echomem-hud app --client ${client}`);
|
|
1116
|
+
}
|
|
1117
|
+
}
|
|
1118
|
+
function resolveHudCliPath() {
|
|
1119
|
+
const entry = fs.realpathSync(process.argv[1] || "");
|
|
1120
|
+
const base = path.dirname(entry);
|
|
1121
|
+
const candidate = path.join(base, "hud", "cli.js");
|
|
1122
|
+
return fs.existsSync(candidate) ? candidate : path.join(base, "hud", "cli.ts");
|
|
1123
|
+
}
|
|
1124
|
+
function parseHudClient(value) {
|
|
1125
|
+
return value === "codex" || value === "claude-code" || value === "claude-desktop" || value === "both" || value === "auto"
|
|
1126
|
+
? value
|
|
1127
|
+
: "auto";
|
|
795
1128
|
}
|
|
796
1129
|
async function cmdLogin(flags) {
|
|
797
1130
|
// Manual path (also the headless path): secrets supplied as flags.
|
|
@@ -1331,14 +1664,48 @@ async function cmdUnlock(flags) {
|
|
|
1331
1664
|
store.saveKey(keyB64);
|
|
1332
1665
|
console.log("✅ Vault unlocked. Reload your MCP client (or start a new session).");
|
|
1333
1666
|
}
|
|
1334
|
-
async function cmdStatus() {
|
|
1667
|
+
async function cmdStatus(flags = {}) {
|
|
1335
1668
|
const store = new KeyStore();
|
|
1336
1669
|
const token = store.getToken();
|
|
1670
|
+
console.log(`EchoMem MCP: ${MCP_PACKAGE_LABEL}`);
|
|
1671
|
+
const latest = flags["no-network"] ? undefined : readLatestPublishedVersion();
|
|
1672
|
+
if (latest) {
|
|
1673
|
+
console.log(`Published latest: ${MCP_PACKAGE_NAME}@${latest}`);
|
|
1674
|
+
if (compareSemver(MCP_PACKAGE_VERSION, latest) < 0) {
|
|
1675
|
+
console.log(`This status command is older than latest. Update client configs with: ${MCP_UPDATE_COMMAND}`);
|
|
1676
|
+
}
|
|
1677
|
+
}
|
|
1337
1678
|
console.log(`Credentials file: ${store.path()}`);
|
|
1338
1679
|
console.log(`API token: ${token ? "present" : "MISSING — run `echomem-mcp login`"}`);
|
|
1680
|
+
if (token) {
|
|
1681
|
+
// Best-effort: resolve who this token belongs to so `status` shows the logged-in account.
|
|
1682
|
+
// whoami already exists in production, so this works even against a local API checkout.
|
|
1683
|
+
try {
|
|
1684
|
+
const { data } = await withTimeout(authedAxios(token).get("/api/openclaw/v1/whoami"), 6000, "WHOAMI_TIMEOUT");
|
|
1685
|
+
const email = typeof data?.email === "string" ? data.email : "";
|
|
1686
|
+
const userId = typeof data?.user_id === "string" ? data.user_id : "";
|
|
1687
|
+
if (email)
|
|
1688
|
+
console.log(`Logged in as: ${email}${userId ? ` (${userId})` : ""}`);
|
|
1689
|
+
else if (userId)
|
|
1690
|
+
console.log(`Logged in as: ${userId}`);
|
|
1691
|
+
else
|
|
1692
|
+
console.log("Logged in as: (token valid; account identity unavailable)");
|
|
1693
|
+
}
|
|
1694
|
+
catch (error) {
|
|
1695
|
+
console.log(`Logged in as: (could not verify — ${formatVerificationError(error)})`);
|
|
1696
|
+
}
|
|
1697
|
+
}
|
|
1339
1698
|
console.log(`Encryption key: ${store.getKey() ? "present" : store.isKeyExpired() ? "EXPIRED — run `echomem-mcp unlock`" : "not set"}`);
|
|
1340
1699
|
const detected = detectClients();
|
|
1341
1700
|
console.log(`Detected clients: ${detected.length ? detected.map((c) => c.label).join(", ") : "none auto-detected"}`);
|
|
1701
|
+
const reports = inspectClientConfigs(latest ?? MCP_PACKAGE_VERSION);
|
|
1702
|
+
if (reports.length) {
|
|
1703
|
+
console.log("Client MCP configs:");
|
|
1704
|
+
for (const report of reports) {
|
|
1705
|
+
for (const line of formatClientConfigReport(report))
|
|
1706
|
+
console.log(line);
|
|
1707
|
+
}
|
|
1708
|
+
}
|
|
1342
1709
|
}
|
|
1343
1710
|
function cmdLogout() {
|
|
1344
1711
|
const store = new KeyStore();
|
|
@@ -1355,9 +1722,13 @@ const HELP = `EchoMem MCP — local memory bridge
|
|
|
1355
1722
|
Usage:
|
|
1356
1723
|
echomem-mcp Run the MCP server (stdio; default — used by your editor)
|
|
1357
1724
|
echomem-mcp setup [--client X] Detect editor, write its MCP config, then log in
|
|
1725
|
+
echomem-mcp setup --skip-login Write MCP config without opening login/browser
|
|
1726
|
+
echomem-mcp update [--client X] Repoint MCP config to this installed bridge; no login/browser
|
|
1727
|
+
echomem-mcp setup --with-hud Configure MCP, then launch the EchoMem context HUD
|
|
1358
1728
|
echomem-mcp login Approve this device in the browser (or --token/--passphrase)
|
|
1359
1729
|
echomem-mcp unlock Re-derive the encryption key after its TTL (or --passphrase)
|
|
1360
1730
|
echomem-mcp status Show token/key/clients
|
|
1731
|
+
echomem-mcp doctor [--no-network] Diagnose configured client bridge versions
|
|
1361
1732
|
echomem-mcp logout Remove stored credentials
|
|
1362
1733
|
echomem-mcp report [--json] Your AI coding memory audit (local, no login, $0)
|
|
1363
1734
|
echomem-mcp migrate [--since D] Import your existing Codex/Claude history into your memory
|
|
@@ -1371,8 +1742,12 @@ Usage:
|
|
|
1371
1742
|
|
|
1372
1743
|
Manual / headless:
|
|
1373
1744
|
echomem-mcp login --token ec_xxx [--passphrase <vault pass> | --key <base64>]
|
|
1745
|
+
${MCP_UPDATE_COMMAND} --client codex # one-shot latest update, no browser login
|
|
1374
1746
|
echomem-mcp setup --dev /abs/path/dist/index.js # point clients at a local checkout
|
|
1747
|
+
echomem-mcp setup --with-hud --install-hud-hooks --client codex [--hud-client auto]
|
|
1375
1748
|
echomem-mcp sync-usage --days 7 --limit 50 --dry-run
|
|
1749
|
+
|
|
1750
|
+
Current bridge version: ${MCP_PACKAGE_VERSION}
|
|
1376
1751
|
`;
|
|
1377
1752
|
/** Returns true if argv was a recognized subcommand (and was handled). */
|
|
1378
1753
|
export async function runCli(argv) {
|
|
@@ -1382,6 +1757,9 @@ export async function runCli(argv) {
|
|
|
1382
1757
|
case "setup":
|
|
1383
1758
|
await cmdSetup(flags);
|
|
1384
1759
|
return true;
|
|
1760
|
+
case "update":
|
|
1761
|
+
await cmdUpdate(flags);
|
|
1762
|
+
return true;
|
|
1385
1763
|
case "login":
|
|
1386
1764
|
await cmdLogin(flags);
|
|
1387
1765
|
return true;
|
|
@@ -1389,7 +1767,10 @@ export async function runCli(argv) {
|
|
|
1389
1767
|
await cmdUnlock(flags);
|
|
1390
1768
|
return true;
|
|
1391
1769
|
case "status":
|
|
1392
|
-
await cmdStatus();
|
|
1770
|
+
await cmdStatus(flags);
|
|
1771
|
+
return true;
|
|
1772
|
+
case "doctor":
|
|
1773
|
+
await cmdStatus(flags);
|
|
1393
1774
|
return true;
|
|
1394
1775
|
case "logout":
|
|
1395
1776
|
cmdLogout();
|
package/dist/v1-contract.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
|
+
import { withMcpVersion } from "./package-metadata.js";
|
|
2
3
|
export const canonicalToolNames = {
|
|
3
4
|
search: "search_memories",
|
|
4
5
|
save: "save_conversation",
|
|
@@ -6,7 +7,10 @@ export const canonicalToolNames = {
|
|
|
6
7
|
keywords: "search_memories_by_keywords",
|
|
7
8
|
others: "search_others_memories",
|
|
8
9
|
report: "echomem_usage_report",
|
|
10
|
+
contextHealth: "echo_context_health",
|
|
11
|
+
recompose: "echo_recompose",
|
|
9
12
|
delete: "delete_memory",
|
|
13
|
+
getByContext: "get_memories_by_context",
|
|
10
14
|
};
|
|
11
15
|
export const legacyAliasToCanonical = {
|
|
12
16
|
search_memories_by_description_semantic: canonicalToolNames.search,
|
|
@@ -35,6 +39,7 @@ export const saveConversationSchema = z.object({
|
|
|
35
39
|
url: z.string().optional(),
|
|
36
40
|
source: z.string().optional(),
|
|
37
41
|
tags: z.array(z.string()).optional(),
|
|
42
|
+
passthrough: z.boolean().optional(),
|
|
38
43
|
messages: z
|
|
39
44
|
.array(z.object({
|
|
40
45
|
role: z.string(),
|
|
@@ -62,6 +67,11 @@ export const deleteMemorySchema = z.object({
|
|
|
62
67
|
confirmed: z.boolean().optional().default(false),
|
|
63
68
|
confirmationToken: z.string().optional(),
|
|
64
69
|
});
|
|
70
|
+
export const getByContextSchema = z.object({
|
|
71
|
+
...triggerMetadataSchema,
|
|
72
|
+
contextId: z.string().min(1),
|
|
73
|
+
limit: z.number().optional().default(50),
|
|
74
|
+
});
|
|
65
75
|
export function listToolSpecs(opts = {}) {
|
|
66
76
|
const currentTime = new Date().toISOString();
|
|
67
77
|
const map = opts.map?.trim();
|
|
@@ -71,7 +81,7 @@ export function listToolSpecs(opts = {}) {
|
|
|
71
81
|
return [
|
|
72
82
|
{
|
|
73
83
|
name: canonicalToolNames.search,
|
|
74
|
-
description: `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}
|
|
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}.`),
|
|
75
85
|
inputSchema: {
|
|
76
86
|
type: "object",
|
|
77
87
|
properties: {
|
|
@@ -117,7 +127,7 @@ export function listToolSpecs(opts = {}) {
|
|
|
117
127
|
},
|
|
118
128
|
{
|
|
119
129
|
name: canonicalToolNames.save,
|
|
120
|
-
description: "Save conversation into EchoMem for future retrieval.",
|
|
130
|
+
description: "Save conversation into EchoMem for future retrieval. Set passthrough=true to store the text verbatim as a session capsule (no LLM extraction, no embeddings) — use this for warm-up capsules that a fresh session will reload via get_memories_by_context.",
|
|
121
131
|
inputSchema: {
|
|
122
132
|
type: "object",
|
|
123
133
|
properties: {
|
|
@@ -126,6 +136,10 @@ export function listToolSpecs(opts = {}) {
|
|
|
126
136
|
url: { type: "string" },
|
|
127
137
|
source: { type: "string" },
|
|
128
138
|
tags: { type: "array", items: { type: "string" } },
|
|
139
|
+
passthrough: {
|
|
140
|
+
type: "boolean",
|
|
141
|
+
description: "When true, store the conversation text verbatim as a session capsule — no LLM extraction, no embeddings. Use for warm-up capsules.",
|
|
142
|
+
},
|
|
129
143
|
messages: {
|
|
130
144
|
type: "array",
|
|
131
145
|
items: {
|
|
@@ -218,11 +232,56 @@ export function listToolSpecs(opts = {}) {
|
|
|
218
232
|
required: ["memoryId"],
|
|
219
233
|
},
|
|
220
234
|
},
|
|
235
|
+
{
|
|
236
|
+
name: canonicalToolNames.getByContext,
|
|
237
|
+
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}.`),
|
|
238
|
+
inputSchema: {
|
|
239
|
+
type: "object",
|
|
240
|
+
properties: {
|
|
241
|
+
contextId: { type: "string", description: "The contextId returned by save_conversation." },
|
|
242
|
+
limit: { type: "number", default: 50 },
|
|
243
|
+
triggerMessage: {
|
|
244
|
+
type: "string",
|
|
245
|
+
description: "Optional: the user's message that caused this lookup. EchoMem stores only a redacted analytics preview and hash.",
|
|
246
|
+
},
|
|
247
|
+
triggerMessageRole: { type: "string", default: "user" },
|
|
248
|
+
},
|
|
249
|
+
required: ["contextId"],
|
|
250
|
+
},
|
|
251
|
+
},
|
|
221
252
|
{
|
|
222
253
|
name: canonicalToolNames.report,
|
|
223
254
|
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.",
|
|
224
255
|
inputSchema: { type: "object", properties: {} },
|
|
225
256
|
},
|
|
257
|
+
{
|
|
258
|
+
name: canonicalToolNames.contextHealth,
|
|
259
|
+
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.",
|
|
260
|
+
inputSchema: {
|
|
261
|
+
type: "object",
|
|
262
|
+
properties: {
|
|
263
|
+
client: {
|
|
264
|
+
type: "string",
|
|
265
|
+
enum: ["codex", "claude-code", "claude-desktop", "auto"],
|
|
266
|
+
default: "auto",
|
|
267
|
+
},
|
|
268
|
+
},
|
|
269
|
+
},
|
|
270
|
+
},
|
|
271
|
+
{
|
|
272
|
+
name: canonicalToolNames.recompose,
|
|
273
|
+
description: "Capture a clean-start capsule of the CURRENT local Codex/Claude session — its goal, the files in play, the most recent instruction, and where things stand — so the user can start a fresh session without re-paying orientation or letting the window auto-compact. Use when echo_context_health shows heavy/dirty context (high saturation or pollution), when the agent starts drifting or repeating, or when the user asks how to clean up or start fresh. Returns inspectable markdown to show the user; this is a clean recompose, not a provider compaction. Reads local agent logs only and needs no login.",
|
|
274
|
+
inputSchema: {
|
|
275
|
+
type: "object",
|
|
276
|
+
properties: {
|
|
277
|
+
client: {
|
|
278
|
+
type: "string",
|
|
279
|
+
enum: ["codex", "claude-code", "claude-desktop", "auto"],
|
|
280
|
+
default: "auto",
|
|
281
|
+
},
|
|
282
|
+
},
|
|
283
|
+
},
|
|
284
|
+
},
|
|
226
285
|
{
|
|
227
286
|
name: "search_memories_by_time_range",
|
|
228
287
|
description: "Legacy alias for get_memories_by_time_range.",
|
package/package.json
CHANGED
|
@@ -1,14 +1,17 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@echomem/mcp",
|
|
3
|
-
"version": "1.4.
|
|
3
|
+
"version": "1.4.3",
|
|
4
4
|
"description": "EchoMem Cloud-First MCP Server",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"bin": {
|
|
8
|
-
"
|
|
8
|
+
"mcp": "./dist/index.js",
|
|
9
|
+
"echomem-mcp": "./dist/index.js",
|
|
10
|
+
"echomem-hud": "./dist/hud/cli.js"
|
|
9
11
|
},
|
|
10
12
|
"files": [
|
|
11
13
|
"dist",
|
|
14
|
+
"assets",
|
|
12
15
|
"templates",
|
|
13
16
|
"README.md"
|
|
14
17
|
],
|
|
@@ -17,17 +20,18 @@
|
|
|
17
20
|
"start": "node dist/index.js",
|
|
18
21
|
"dev": "tsx src/index.ts",
|
|
19
22
|
"smoke": "node smoke.mjs",
|
|
20
|
-
"test": "npm run build && node test/crypto.test.mjs && node test/integration.test.mjs && node test/no-restart.test.mjs && node test/report.test.mjs && node test/tools.test.mjs && node test/delete.test.mjs && node test/migrate.test.mjs",
|
|
23
|
+
"test": "npm run build && node test/crypto.test.mjs && node test/integration.test.mjs && node test/no-restart.test.mjs && node test/report.test.mjs && node test/tools.test.mjs && node test/delete.test.mjs && node test/migrate.test.mjs && node test/hud.test.mjs",
|
|
21
24
|
"prepack": "npm run build && node scripts/bundle-city.mjs"
|
|
22
25
|
},
|
|
23
26
|
"dependencies": {
|
|
24
27
|
"@modelcontextprotocol/sdk": "^1.0.1",
|
|
25
|
-
"
|
|
26
|
-
"
|
|
28
|
+
"axios": "^1.6.8",
|
|
29
|
+
"electron": "41.7.1",
|
|
30
|
+
"zod": "^3.22.4"
|
|
27
31
|
},
|
|
28
32
|
"devDependencies": {
|
|
29
|
-
"
|
|
30
|
-
"tsx": "^4.
|
|
31
|
-
"
|
|
33
|
+
"@types/node": "^20.11.0",
|
|
34
|
+
"tsx": "^4.22.4",
|
|
35
|
+
"typescript": "^5.3.3"
|
|
32
36
|
}
|
|
33
37
|
}
|