@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/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
- console.log(`ℹ️ ${c.label}: ${c.note}\n entry: ${JSON.stringify(entry)}`);
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
- await cmdLogin(flags);
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();
@@ -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.1",
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
- "echomem-mcp": "./dist/index.js"
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
- "zod": "^3.22.4",
26
- "axios": "^1.6.8"
28
+ "axios": "^1.6.8",
29
+ "electron": "41.7.1",
30
+ "zod": "^3.22.4"
27
31
  },
28
32
  "devDependencies": {
29
- "typescript": "^5.3.3",
30
- "tsx": "^4.7.1",
31
- "@types/node": "^20.11.0"
33
+ "@types/node": "^20.11.0",
34
+ "tsx": "^4.22.4",
35
+ "typescript": "^5.3.3"
32
36
  }
33
37
  }