@echomem/mcp 1.0.0 → 1.0.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/index.js CHANGED
@@ -6,6 +6,7 @@ import axios from "axios";
6
6
  import { ZodError } from "zod";
7
7
  import { canonicalToolNames, keywordsSchema, listToolSpecs, othersSchema, resolveCanonicalToolName, saveConversationSchema, searchMemoriesSchema, timeRangeSchema, } from "./v1-contract.js";
8
8
  import { KeyStore } from "./keystore.js";
9
+ import { randomUUID } from "node:crypto";
9
10
  import { fetchEncryptionConfig, decryptMemoryFields } from "./encryption.js";
10
11
  import { runCli } from "./setup.js";
11
12
  const ECHO_API_BASE_URL = process.env.ECHO_API_BASE_URL || "https://echo-mem-chrome.vercel.app";
@@ -79,6 +80,8 @@ class EchoMemApiClient {
79
80
  store;
80
81
  axios;
81
82
  whoamiCache = null;
83
+ /** One id per bridge process — groups all saves from this coding session under a single EchoMem context. */
84
+ sessionId = randomUUID();
82
85
  encConfigPromise = null;
83
86
  constructor(store) {
84
87
  this.store = store;
@@ -101,6 +104,27 @@ class EchoMemApiClient {
101
104
  hasToken() {
102
105
  return !!this.store.getToken();
103
106
  }
107
+ /**
108
+ * Compact topic map of the user's memory for the search-tool description — a cheap "what's in here"
109
+ * index built from memory keys so the agent knows the boundary up front and recalls proactively.
110
+ * Best-effort: returns undefined on any failure (locked vault, network) rather than blocking tools.
111
+ */
112
+ async fetchMemoryMap() {
113
+ try {
114
+ const enc = await this.encState(); // unencrypted → {enabled:false}; encrypted+locked → throws
115
+ const today = new Date().toISOString().slice(0, 10);
116
+ const response = await this.axios.post("/api/extension/memories/time-range", { startDate: "2000-01-01", endDate: today, limit: 200 }, { timeout: 6000 });
117
+ const data = enc.enabled ? await this.decryptResult(response.data, enc.key) : response.data;
118
+ const memories = Array.isArray(data?.memories) ? data.memories : [];
119
+ const keys = [...new Set(memories.map((m) => String(m?.keys || "").trim()).filter(Boolean))];
120
+ if (!keys.length)
121
+ return undefined;
122
+ return keys.slice(0, 40).map((k) => `- ${k}`).join("\n");
123
+ }
124
+ catch {
125
+ return undefined;
126
+ }
127
+ }
104
128
  /** Encryption config for the account, fetched once and cached. Failures are not cached. */
105
129
  async getEncryptionConfig() {
106
130
  if (!this.encConfigPromise) {
@@ -241,6 +265,8 @@ class EchoMemApiClient {
241
265
  sourceUrl: parsed.url,
242
266
  source: parsed.source || "mcp_server",
243
267
  title: parsed.title,
268
+ // Stable per-session id so multiple saves in this coding session group under one context.
269
+ conversationKey: this.sessionId,
244
270
  }, config);
245
271
  return response.data;
246
272
  }
@@ -286,6 +312,7 @@ class EchoMemApiClient {
286
312
  class EchoMemMCPServer {
287
313
  server;
288
314
  client;
315
+ mapCache = null;
289
316
  constructor(store) {
290
317
  this.server = new Server({
291
318
  name: "echomem-mcp",
@@ -304,7 +331,14 @@ class EchoMemMCPServer {
304
331
  });
305
332
  }
306
333
  setupToolHandlers() {
307
- this.server.setRequestHandler(ListToolsRequestSchema, async () => ({ tools: listToolSpecs() }));
334
+ this.server.setRequestHandler(ListToolsRequestSchema, async () => {
335
+ // Inject a compact topic map of the user's memory into the search-tool description so the agent
336
+ // knows the boundary up front and recalls proactively (cached; best-effort — no map on failure).
337
+ if (this.client.hasToken() && !this.mapCache)
338
+ this.mapCache = this.client.fetchMemoryMap();
339
+ const map = this.mapCache ? await this.mapCache : undefined;
340
+ return { tools: listToolSpecs({ map }) };
341
+ });
308
342
  this.server.setRequestHandler(CallToolRequestSchema, async (request) => {
309
343
  const canonicalName = resolveCanonicalToolName(request.params.name);
310
344
  try {
package/dist/setup.js CHANGED
@@ -39,15 +39,17 @@ export function knownClients() {
39
39
  { id: "windsurf", label: "Windsurf", kind: "json", configPath: home(".codeium", "windsurf", "mcp_config.json") },
40
40
  { id: "claude-desktop", label: "Claude Desktop", kind: "json", configPath: path.join(appSupport, "Claude", "claude_desktop_config.json") },
41
41
  { id: "claude-code", label: "Claude Code", kind: "snippet", note: "run: claude mcp add-json echomem '<entry>' (or add to .mcp.json)" },
42
- { id: "codex", label: "Codex", kind: "snippet", note: "add to ~/.codex/config.toml under [mcp_servers.echomem]" },
42
+ { id: "codex", label: "Codex", kind: "command", detectDir: home(".codex"), configPath: home(".codex", "config.toml"), note: "add to ~/.codex/config.toml under [mcp_servers.echomem]" },
43
43
  ];
44
44
  }
45
45
  /** A client is "present" if its config dir already exists (JSON) — a cheap heuristic for detection. */
46
46
  export function detectClients() {
47
47
  return knownClients().filter((c) => {
48
- if (c.kind !== "json")
49
- return false;
50
- return fs.existsSync(path.dirname(c.configPath));
48
+ if (c.kind === "json")
49
+ return fs.existsSync(path.dirname(c.configPath));
50
+ if (c.kind === "command")
51
+ return fs.existsSync(c.detectDir);
52
+ return false;
51
53
  });
52
54
  }
53
55
  /**
@@ -61,6 +63,28 @@ export function buildServerEntry(opts = {}) {
61
63
  }
62
64
  return { command: "npx", args: ["-y", "@echomem/mcp"] };
63
65
  }
66
+ /** The TOML block EchoMem adds to ~/.codex/config.toml. No secret — the bridge reads the keystore. */
67
+ export function codexTomlBlock(entry) {
68
+ const command = JSON.stringify(String(entry.command));
69
+ const args = (Array.isArray(entry.args) ? entry.args : []).map((a) => JSON.stringify(String(a))).join(", ");
70
+ return `[mcp_servers.echomem]\ncommand = ${command}\nargs = [${args}]\n`;
71
+ }
72
+ /** Write/merge the EchoMem entry straight into Codex's config.toml — no `codex` CLI needed. Idempotent. */
73
+ function writeCodexConfig(configPath, entry) {
74
+ let content = "";
75
+ try {
76
+ content = fs.readFileSync(configPath, "utf8");
77
+ }
78
+ catch {
79
+ /* fresh config */
80
+ }
81
+ if (/^\s*\[mcp_servers\.echomem\]/m.test(content))
82
+ return "exists";
83
+ fs.mkdirSync(path.dirname(configPath), { recursive: true });
84
+ const sep = content ? (content.endsWith("\n") ? "\n" : "\n\n") : "";
85
+ fs.appendFileSync(configPath, sep + codexTomlBlock(entry));
86
+ return "wrote";
87
+ }
64
88
  /** Merge the EchoMem entry into a JSON client's `mcpServers` map without clobbering siblings. */
65
89
  export function writeJsonClientConfig(configPath, entry) {
66
90
  let config = {};
@@ -243,9 +267,9 @@ async function cmdSetup(flags) {
243
267
  const requested = typeof flags.client === "string" ? flags.client : undefined;
244
268
  const targets = (requested ? knownClients().filter((c) => c.id === requested) : detectClients());
245
269
  if (targets.length === 0) {
246
- console.log("No JSON-config client auto-detected. Add this MCP server entry manually:\n");
270
+ console.log("No client auto-detected. Add this MCP server entry manually:\n");
247
271
  console.log(JSON.stringify({ echomem: entry }, null, 2));
248
- console.log("\n(Or re-run with --client cursor|windsurf|claude-desktop.)");
272
+ console.log("\n(Or re-run with --client cursor|windsurf|claude-desktop|codex.)");
249
273
  }
250
274
  else {
251
275
  for (const c of targets) {
@@ -253,6 +277,13 @@ async function cmdSetup(flags) {
253
277
  writeJsonClientConfig(c.configPath, entry);
254
278
  console.log(`✅ Wrote EchoMem MCP entry to ${c.label}: ${c.configPath}`);
255
279
  }
280
+ else if (c.kind === "command") {
281
+ const result = writeCodexConfig(c.configPath, entry);
282
+ if (result === "wrote")
283
+ console.log(`✅ Wrote EchoMem MCP entry to ${c.label}: ${c.configPath} — start a new Codex session to load it.`);
284
+ else
285
+ console.log(`✅ ${c.label} already has the EchoMem MCP entry: ${c.configPath}`);
286
+ }
256
287
  else {
257
288
  console.log(`ℹ️ ${c.label}: ${c.note}\n entry: ${JSON.stringify(entry)}`);
258
289
  }
@@ -45,12 +45,16 @@ export const keywordsSchema = z.object({
45
45
  export const othersSchema = z.object({
46
46
  query: z.string(),
47
47
  });
48
- export function listToolSpecs() {
48
+ export function listToolSpecs(opts = {}) {
49
49
  const currentTime = new Date().toISOString();
50
+ const map = opts.map?.trim();
51
+ const mapSection = map
52
+ ? `\n\nThis user's EchoMem currently covers these topics (a relevance guide — recall when the task relates to one of them):\n${map}\n`
53
+ : "";
50
54
  return [
51
55
  {
52
56
  name: canonicalToolNames.search,
53
- description: `Recall the user's prior decisions, preferences, constraints, and context from EchoMem — their long-term memory spanning ALL their AI tools (Claude.ai, ChatGPT, other agents), not just this session. CALL THIS AT THE START of any non-trivial task, before planning or writing code, to avoid re-deriving things the user already decided. Also call it whenever the user refers to past work ("what did we decide", "like before", "the usual"). Returns ranked memories with provenance. Current time: ${currentTime}.`,
57
+ 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). Returns ranked memories with provenance. Current time: ${currentTime}.`,
54
58
  inputSchema: {
55
59
  type: "object",
56
60
  properties: {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@echomem/mcp",
3
- "version": "1.0.0",
3
+ "version": "1.0.3",
4
4
  "description": "EchoMem Cloud-First MCP Server",
5
5
  "main": "dist/index.js",
6
6
  "type": "module",