@echomem/mcp 1.4.0 → 1.4.2

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/index.js CHANGED
@@ -4,13 +4,15 @@ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"
4
4
  import { CallToolRequestSchema, ErrorCode, ListToolsRequestSchema, McpError, } from "@modelcontextprotocol/sdk/types.js";
5
5
  import axios from "axios";
6
6
  import { ZodError } from "zod";
7
- import { canonicalToolNames, deleteMemorySchema, keywordsSchema, listToolSpecs, othersSchema, resolveCanonicalToolName, saveConversationSchema, searchMemoriesSchema, timeRangeSchema, } from "./v1-contract.js";
7
+ import { canonicalToolNames, deleteMemorySchema, getByContextSchema, keywordsSchema, listToolSpecs, othersSchema, resolveCanonicalToolName, saveConversationSchema, searchMemoriesSchema, timeRangeSchema, } from "./v1-contract.js";
8
8
  import { KeyStore } from "./keystore.js";
9
9
  import { EventLogger, hashText } from "./events.js";
10
10
  import { buildReportText } from "./report.js";
11
+ import { contextHealthMarkdown, recomposeCapsuleMarkdown } from "./hud/api.js";
11
12
  import { createHash, randomUUID } from "node:crypto";
12
13
  import { fetchEncryptionConfig, decryptMemoryFields } from "./encryption.js";
13
14
  import { runCli } from "./setup.js";
15
+ import { MCP_PACKAGE_VERSION, MCP_SERVER_INSTRUCTIONS } from "./package-metadata.js";
14
16
  const ECHO_API_BASE_URL = process.env.ECHO_API_BASE_URL || "https://echo-mem-chrome.vercel.app";
15
17
  const MEMORY_FEED_API_URL = process.env.MEMORY_FEED_API_URL || "https://memory-feed.vercel.app";
16
18
  /** Thrown when no API token is present yet — the model gets a "run login" nudge, not a hard error. */
@@ -690,6 +692,7 @@ class EchoMemApiClient {
690
692
  title: parsed.title,
691
693
  // Stable per-session id so multiple saves in this coding session group under one context.
692
694
  conversationKey: this.sessionId,
695
+ passthrough: parsed.passthrough || false,
693
696
  triggerMessage: parsed.triggerMessage ||
694
697
  lastUserMessageFromMessages(parsed.messages) ||
695
698
  lastUserMessageFromConversationText(parsed.conversation),
@@ -751,6 +754,15 @@ class EchoMemApiClient {
751
754
  });
752
755
  return enc.enabled ? await this.decryptResult(response.data, enc.key) : response.data;
753
756
  }
757
+ async getMemoriesByContext(args) {
758
+ const parsed = getByContextSchema.parse(args);
759
+ const enc = await this.encState();
760
+ const response = await this.axios.post("/api/extension/memories/by-context", {
761
+ contextId: parsed.contextId,
762
+ limit: parsed.limit,
763
+ });
764
+ return enc.enabled ? await this.decryptResult(response.data, enc.key) : response.data;
765
+ }
754
766
  async searchMemoriesByKeywords(args) {
755
767
  const parsed = keywordsSchema.parse(args);
756
768
  const enc = await this.encState();
@@ -778,7 +790,7 @@ class EchoMemApiClient {
778
790
  }
779
791
  }
780
792
  }
781
- const SERVER_VERSION = "1.1.0";
793
+ const SERVER_VERSION = MCP_PACKAGE_VERSION;
782
794
  class EchoMemMCPServer {
783
795
  server;
784
796
  client;
@@ -793,6 +805,7 @@ class EchoMemMCPServer {
793
805
  name: "echomem-mcp",
794
806
  version: SERVER_VERSION,
795
807
  }, {
808
+ instructions: MCP_SERVER_INSTRUCTIONS,
796
809
  capabilities: {
797
810
  tools: {},
798
811
  },
@@ -870,6 +883,45 @@ class EchoMemMCPServer {
870
883
  if (canonicalName === canonicalToolNames.report) {
871
884
  return { content: [{ type: "text", text: await buildReportText(false) }] };
872
885
  }
886
+ if (canonicalName === canonicalToolNames.contextHealth) {
887
+ const client = isRecord(request.params.arguments) && typeof request.params.arguments.client === "string"
888
+ ? request.params.arguments.client
889
+ : "auto";
890
+ const mode = client === "codex" || client === "claude-code" || client === "claude-desktop" || client === "auto"
891
+ ? client
892
+ : "auto";
893
+ return { content: [{ type: "text", text: await contextHealthMarkdown(mode) }] };
894
+ }
895
+ if (canonicalName === canonicalToolNames.recompose) {
896
+ const client = isRecord(request.params.arguments) && typeof request.params.arguments.client === "string"
897
+ ? request.params.arguments.client
898
+ : "auto";
899
+ const mode = client === "codex" || client === "claude-code" || client === "claude-desktop" || client === "auto"
900
+ ? client
901
+ : "auto";
902
+ const capsuleText = await recomposeCapsuleMarkdown(mode);
903
+ // If logged in, persist the capsule via passthrough so it's retrievable by contextId.
904
+ if (this.client.hasToken()) {
905
+ try {
906
+ const saveResult = await this.client.saveConversation({
907
+ conversation: capsuleText,
908
+ title: "Session capsule (recompose)",
909
+ source: "mcp_recompose",
910
+ passthrough: true,
911
+ });
912
+ const ctxId = saveResult?.contextId;
913
+ const capId = saveResult?.capsuleId;
914
+ const persistLine = ctxId
915
+ ? `\n\n---\nCapsule persisted${capId ? ` (${capId})` : ""}. To reload in a fresh session:\nget_memories_by_context({ contextId: "${ctxId}" })`
916
+ : "";
917
+ return { content: [{ type: "text", text: capsuleText + persistLine }] };
918
+ }
919
+ catch {
920
+ // Persistence is best-effort; return the capsule text regardless.
921
+ }
922
+ }
923
+ return { content: [{ type: "text", text: capsuleText }] };
924
+ }
873
925
  if (!this.client.hasToken())
874
926
  throw new NoTokenError();
875
927
  if (canonicalName !== canonicalToolNames.save) {
@@ -886,6 +938,8 @@ class EchoMemMCPServer {
886
938
  return await this.handleSave(request.params.arguments, rec);
887
939
  case canonicalToolNames.timeRange:
888
940
  return await this.handleTimeRange(request.params.arguments);
941
+ case canonicalToolNames.getByContext:
942
+ return await this.handleGetByContext(request.params.arguments);
889
943
  case canonicalToolNames.keywords:
890
944
  return await this.handleKeywords(request.params.arguments);
891
945
  case canonicalToolNames.others:
@@ -940,6 +994,8 @@ class EchoMemMCPServer {
940
994
  rec.latency_ms = Date.now() - t0;
941
995
  this.events.record(rec);
942
996
  if (canonicalName !== canonicalToolNames.report &&
997
+ canonicalName !== canonicalToolNames.contextHealth &&
998
+ canonicalName !== canonicalToolNames.recompose &&
943
999
  canonicalName !== canonicalToolNames.save &&
944
1000
  this.client.hasToken()) {
945
1001
  const finalAnalytics = {
@@ -1061,14 +1117,46 @@ Details: ${m.details || "N/A"}`)
1061
1117
  rec.conversation_chars = text.length;
1062
1118
  rec.save_source = typeof a?.source === "string" ? a.source : sourceFallback;
1063
1119
  }
1064
- const { success, memoriesExtracted, error } = await this.client.saveConversation(enrichedArgs);
1120
+ const { success, memoriesExtracted, extractedMemories, contextId, capsuleId, passthrough: isPassthrough, error } = await this.client.saveConversation(enrichedArgs);
1065
1121
  if (!success)
1066
1122
  throw new Error(`EchoMem API Error: ${error}`);
1067
1123
  if (rec)
1068
1124
  rec.memories_extracted = typeof memoriesExtracted === "number" ? memoriesExtracted : undefined;
1069
- return {
1070
- content: [{ type: "text", text: `Successfully ingested conversation. Extracted ${memoriesExtracted} memory distinct events.` }],
1071
- };
1125
+ // Passthrough saves store the capsule verbatim — no extraction, no embeddings.
1126
+ if (isPassthrough) {
1127
+ const text = [
1128
+ `Session capsule saved (passthrough, no extraction).`,
1129
+ typeof capsuleId === "string" && capsuleId ? `Capsule ID: ${capsuleId}` : "",
1130
+ typeof contextId === "string" && contextId ? `Context: ${contextId}` : "",
1131
+ "",
1132
+ `To reload this capsule in a fresh session: get_memories_by_context({ contextId: "${contextId}" })`,
1133
+ ].filter(Boolean).join("\n");
1134
+ return { content: [{ type: "text", text }] };
1135
+ }
1136
+ // Surface WHAT was captured (not just the count) so the agent can verify the key facts survived
1137
+ // extraction, and so it holds the ids to deterministically re-fetch this batch later (warm-up).
1138
+ const saved = Array.isArray(extractedMemories) ? extractedMemories.filter(isRecord) : [];
1139
+ const list = saved
1140
+ .map((m, idx) => {
1141
+ const keys = readString(m, "keys") ?? "(no key)";
1142
+ const description = readString(m, "description") ?? "";
1143
+ const details = readString(m, "details") ?? "";
1144
+ const id = readString(m, "id");
1145
+ return [
1146
+ `[${idx + 1}] ${keys}${id ? ` · id ${id}` : ""}`,
1147
+ description ? `Description: ${description}` : "",
1148
+ details ? `Details: ${details}` : "",
1149
+ ].filter(Boolean).join("\n");
1150
+ })
1151
+ .join("\n\n");
1152
+ const text = [
1153
+ `Successfully ingested conversation. Extracted ${memoriesExtracted} memory distinct events.`,
1154
+ list,
1155
+ saved.length
1156
+ ? `Verify these captured the key facts. To re-fetch this exact batch later, search by the ids above${typeof contextId === "string" && contextId ? ` (context ${contextId})` : ""}.`
1157
+ : "",
1158
+ ].filter(Boolean).join("\n\n");
1159
+ return { content: [{ type: "text", text }] };
1072
1160
  }
1073
1161
  async handleTimeRange(args) {
1074
1162
  const parsed = timeRangeSchema.parse(args);
@@ -1095,6 +1183,31 @@ Details: ${m.details || "N/A"}`)
1095
1183
  ],
1096
1184
  };
1097
1185
  }
1186
+ async handleGetByContext(args) {
1187
+ const parsed = getByContextSchema.parse(args);
1188
+ const { success, memories, error } = await this.client.getMemoriesByContext(args);
1189
+ if (!success)
1190
+ throw new Error(`EchoMem API Error: ${error}`);
1191
+ if (!memories?.length) {
1192
+ return {
1193
+ content: [{ type: "text", text: `No memories found for context ${parsed.contextId}.` }],
1194
+ };
1195
+ }
1196
+ const formattedResults = memories
1197
+ .map((m, idx) => `[${idx + 1}] ${m.keys || "Saved memory"}${m.id ? ` · id ${m.id}` : ""}
1198
+ Time: ${m.time} | Category: ${m.category} | Object: ${m.object} | Emotion: ${m.emotion}
1199
+ Description: ${m.description}
1200
+ Details: ${m.details || "N/A"}`)
1201
+ .join("\n\n");
1202
+ return {
1203
+ content: [
1204
+ {
1205
+ type: "text",
1206
+ text: `Recalled ${memories.length} memories from context ${parsed.contextId} (deterministic full batch):\n\n${formattedResults}`,
1207
+ },
1208
+ ],
1209
+ };
1210
+ }
1098
1211
  async handleKeywords(args) {
1099
1212
  const parsed = keywordsSchema.parse(args);
1100
1213
  const { success, memories, error } = await this.client.searchMemoriesByKeywords(args);
@@ -0,0 +1,32 @@
1
+ import fs from "node:fs";
2
+ const FALLBACK_PACKAGE = {
3
+ name: "@echomem/mcp",
4
+ version: "0.0.0",
5
+ description: "EchoMem Cloud-First MCP Server",
6
+ };
7
+ function loadPackageJson() {
8
+ try {
9
+ const packageUrl = new URL("../package.json", import.meta.url);
10
+ return JSON.parse(fs.readFileSync(packageUrl, "utf8"));
11
+ }
12
+ catch {
13
+ return {};
14
+ }
15
+ }
16
+ const packageJson = loadPackageJson();
17
+ function stringOrFallback(value, fallback) {
18
+ return typeof value === "string" && value.trim() ? value.trim() : fallback;
19
+ }
20
+ export const MCP_PACKAGE_NAME = stringOrFallback(packageJson.name, FALLBACK_PACKAGE.name);
21
+ export const MCP_PACKAGE_VERSION = stringOrFallback(packageJson.version, FALLBACK_PACKAGE.version);
22
+ export const MCP_PACKAGE_DESCRIPTION = stringOrFallback(packageJson.description, FALLBACK_PACKAGE.description);
23
+ export const MCP_PACKAGE_LABEL = `${MCP_PACKAGE_NAME}@${MCP_PACKAGE_VERSION}`;
24
+ export const MCP_UPDATE_COMMAND = `npx -y ${MCP_PACKAGE_NAME}@latest update`;
25
+ export const MCP_SERVER_INSTRUCTIONS = [
26
+ `${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 \`${MCP_UPDATE_COMMAND}\` and start a new MCP session.`,
28
+ "Do not auto-update on every MCP startup; this bridge is intentionally stable between explicit updates.",
29
+ ].join(" ");
30
+ export function withMcpVersion(description) {
31
+ return `${description}\n\nEchoMem MCP bridge: ${MCP_PACKAGE_LABEL}. If this version is stale, update once with \`${MCP_UPDATE_COMMAND}\` (add \`--client cursor|windsurf|claude-desktop|codex\` when needed), then start a new MCP session. Do not run updates repeatedly or on every startup.`;
32
+ }
package/dist/report.js CHANGED
@@ -623,7 +623,7 @@ function renderText(a, memCount, useColor) {
623
623
  L(c.green(`✓ You're connected — every new session now builds on your ${memCount} memories.`));
624
624
  }
625
625
  else {
626
- L(c.green("→ Start now (no signup wall): ") + c.bold("npx -y @echomem/mcp setup"));
626
+ L(c.green("→ Start now (no signup wall): ") + c.bold("npm i -g @echomem/mcp@latest && echomem-mcp setup"));
627
627
  L(c.dim(` ~1 minute. Your next coding session recalls instead of re-reading.`));
628
628
  }
629
629
  NL();
@@ -169,8 +169,7 @@ export function renderSetupPage() {
169
169
  .cityArrow {
170
170
  position: absolute;
171
171
  z-index: 5;
172
- left: 50%;
173
- transform: translateX(-50%);
172
+ right: clamp(22px, 3vw, 42px);
174
173
  bottom: clamp(22px, 3.5vh, 42px);
175
174
  width: 44px;
176
175
  height: 44px;
package/dist/setup.js CHANGED
@@ -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_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.
@@ -791,7 +793,55 @@ async function cmdSetup(flags) {
791
793
  }
792
794
  }
793
795
  console.log("");
794
- await cmdLogin(flags);
796
+ if (flags["skip-login"] || flags["no-login"]) {
797
+ console.log(`Skipped login; existing EchoMem credentials are unchanged. Current bridge: ${MCP_PACKAGE_LABEL}`);
798
+ }
799
+ else {
800
+ await cmdLogin(flags);
801
+ }
802
+ if (flags["with-hud"])
803
+ await cmdSetupHud(flags);
804
+ }
805
+ async function cmdUpdate(flags) {
806
+ await cmdSetup({ ...flags, "skip-login": true });
807
+ console.log(`Update config complete. Start a new MCP session to load ${MCP_PACKAGE_LABEL}.`);
808
+ }
809
+ async function cmdSetupHud(flags) {
810
+ const client = parseHudClient(flags["hud-client"] || "auto");
811
+ const hudCli = resolveHudCliPath();
812
+ console.log("");
813
+ console.log(`✅ EchoMem HUD available: ${process.execPath} ${hudCli}`);
814
+ if (flags["install-hud-hooks"]) {
815
+ const paths = installHooks(client === "claude-desktop" ? "auto" : client);
816
+ console.log(`✅ Installed EchoMem HUD hook support:\n${paths.map((p) => ` - ${p}`).join("\n")}`);
817
+ console.log(" Codex users: run /hooks in a new Codex session to review and trust changed hooks.");
818
+ }
819
+ else {
820
+ console.log("ℹ️ HUD hooks not installed. Add --install-hud-hooks if you want lifecycle wakeups.");
821
+ }
822
+ if (!flags["no-launch-hud"]) {
823
+ try {
824
+ spawn(process.execPath, [hudCli, "app", "--client", client], { stdio: "ignore", detached: true }).unref();
825
+ console.log("✅ Launched EchoMem HUD app.");
826
+ }
827
+ catch {
828
+ console.log(`ℹ️ Could not auto-launch HUD. Run: echomem-hud app --client ${client}`);
829
+ }
830
+ }
831
+ else {
832
+ console.log(`Run the HUD later with: echomem-hud app --client ${client}`);
833
+ }
834
+ }
835
+ function resolveHudCliPath() {
836
+ const entry = fs.realpathSync(process.argv[1] || "");
837
+ const base = path.dirname(entry);
838
+ const candidate = path.join(base, "hud", "cli.js");
839
+ return fs.existsSync(candidate) ? candidate : path.join(base, "hud", "cli.ts");
840
+ }
841
+ function parseHudClient(value) {
842
+ return value === "codex" || value === "claude-code" || value === "claude-desktop" || value === "both" || value === "auto"
843
+ ? value
844
+ : "auto";
795
845
  }
796
846
  async function cmdLogin(flags) {
797
847
  // Manual path (also the headless path): secrets supplied as flags.
@@ -1334,8 +1384,27 @@ async function cmdUnlock(flags) {
1334
1384
  async function cmdStatus() {
1335
1385
  const store = new KeyStore();
1336
1386
  const token = store.getToken();
1387
+ console.log(`EchoMem MCP: ${MCP_PACKAGE_LABEL}`);
1337
1388
  console.log(`Credentials file: ${store.path()}`);
1338
1389
  console.log(`API token: ${token ? "present" : "MISSING — run `echomem-mcp login`"}`);
1390
+ if (token) {
1391
+ // Best-effort: resolve who this token belongs to so `status` shows the logged-in account.
1392
+ // whoami already exists in production, so this works even against a local API checkout.
1393
+ try {
1394
+ const { data } = await withTimeout(authedAxios(token).get("/api/openclaw/v1/whoami"), 6000, "WHOAMI_TIMEOUT");
1395
+ const email = typeof data?.email === "string" ? data.email : "";
1396
+ const userId = typeof data?.user_id === "string" ? data.user_id : "";
1397
+ if (email)
1398
+ console.log(`Logged in as: ${email}${userId ? ` (${userId})` : ""}`);
1399
+ else if (userId)
1400
+ console.log(`Logged in as: ${userId}`);
1401
+ else
1402
+ console.log("Logged in as: (token valid; account identity unavailable)");
1403
+ }
1404
+ catch (error) {
1405
+ console.log(`Logged in as: (could not verify — ${formatVerificationError(error)})`);
1406
+ }
1407
+ }
1339
1408
  console.log(`Encryption key: ${store.getKey() ? "present" : store.isKeyExpired() ? "EXPIRED — run `echomem-mcp unlock`" : "not set"}`);
1340
1409
  const detected = detectClients();
1341
1410
  console.log(`Detected clients: ${detected.length ? detected.map((c) => c.label).join(", ") : "none auto-detected"}`);
@@ -1355,6 +1424,9 @@ const HELP = `EchoMem MCP — local memory bridge
1355
1424
  Usage:
1356
1425
  echomem-mcp Run the MCP server (stdio; default — used by your editor)
1357
1426
  echomem-mcp setup [--client X] Detect editor, write its MCP config, then log in
1427
+ echomem-mcp setup --skip-login Write MCP config without opening login/browser
1428
+ echomem-mcp update [--client X] Repoint MCP config to this installed bridge; no login/browser
1429
+ echomem-mcp setup --with-hud Configure MCP, then launch the EchoMem context HUD
1358
1430
  echomem-mcp login Approve this device in the browser (or --token/--passphrase)
1359
1431
  echomem-mcp unlock Re-derive the encryption key after its TTL (or --passphrase)
1360
1432
  echomem-mcp status Show token/key/clients
@@ -1371,8 +1443,12 @@ Usage:
1371
1443
 
1372
1444
  Manual / headless:
1373
1445
  echomem-mcp login --token ec_xxx [--passphrase <vault pass> | --key <base64>]
1446
+ ${MCP_UPDATE_COMMAND} --client codex # one-shot latest update, no browser login
1374
1447
  echomem-mcp setup --dev /abs/path/dist/index.js # point clients at a local checkout
1448
+ echomem-mcp setup --with-hud --install-hud-hooks --client codex [--hud-client auto]
1375
1449
  echomem-mcp sync-usage --days 7 --limit 50 --dry-run
1450
+
1451
+ Current bridge version: ${MCP_PACKAGE_VERSION}
1376
1452
  `;
1377
1453
  /** Returns true if argv was a recognized subcommand (and was handled). */
1378
1454
  export async function runCli(argv) {
@@ -1382,6 +1458,9 @@ export async function runCli(argv) {
1382
1458
  case "setup":
1383
1459
  await cmdSetup(flags);
1384
1460
  return true;
1461
+ case "update":
1462
+ await cmdUpdate(flags);
1463
+ return true;
1385
1464
  case "login":
1386
1465
  await cmdLogin(flags);
1387
1466
  return true;
@@ -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.0",
3
+ "version": "1.4.2",
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
  }