@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 +35 -1
- package/dist/setup.js +37 -6
- package/dist/v1-contract.js +6 -2
- package/package.json +1 -1
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 () =>
|
|
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: "
|
|
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
|
|
49
|
-
return
|
|
50
|
-
|
|
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
|
|
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
|
}
|
package/dist/v1-contract.js
CHANGED
|
@@ -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
|
|
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: {
|