@echomem/mcp 1.4.19 → 1.4.21

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.
@@ -117,7 +117,7 @@ export function listToolSpecs(opts = {}) {
117
117
  return [
118
118
  {
119
119
  name: canonicalToolNames.search,
120
- description: withMcpVersion(`Recall the user's prior decisions, preferences, and project context from EchoMem — their long-term memory across ALL their AI tools, not just this session. Use it instead of re-deriving or re-asking what the user already settled. ${recallPlanNote} ${searchBillingReplyInstruction}${mapSection}\nReturns the ranked memories; set includeAnswer=true only if you need the legacy synthesized answer. Current time: ${currentTime}.${updateSection}`),
120
+ description: withMcpVersion(`Recall the user's prior decisions, preferences, and project context from EchoMem — their long-term memory across ALL their AI tools, not just this session. Use it instead of re-deriving or re-asking what the user already settled. ${recallPlanNote} ${searchBillingReplyInstruction}${mapSection}\nReturns ranked memories only; the MCP host model writes the final answer. Current time: ${currentTime}.${updateSection}`),
121
121
  inputSchema: {
122
122
  type: "object",
123
123
  properties: {
@@ -125,11 +125,6 @@ export function listToolSpecs(opts = {}) {
125
125
  limit: { type: "number", default: 10 },
126
126
  threshold: { type: "number", default: 0.1 },
127
127
  timeFrameDays: { type: "number" },
128
- includeAnswer: {
129
- type: "boolean",
130
- default: false,
131
- description: "Default false: return only retrieved memories and skip answer generation. Set true for the legacy synthesized recall answer.",
132
- },
133
128
  triggerMessage: {
134
129
  type: "string",
135
130
  description: "Optional: the user's message that caused this recall. EchoMem stores only a redacted analytics preview and hash.",
@@ -148,11 +143,6 @@ export function listToolSpecs(opts = {}) {
148
143
  limit: { type: "number", default: 10 },
149
144
  threshold: { type: "number", default: 0.1 },
150
145
  timeFrameDays: { type: "number" },
151
- includeAnswer: {
152
- type: "boolean",
153
- default: false,
154
- description: "Default false: return only retrieved memories and skip answer generation. Set true for the legacy synthesized recall answer.",
155
- },
156
146
  triggerMessage: {
157
147
  type: "string",
158
148
  description: "Optional: the user's message that caused this recall. EchoMem stores only a redacted analytics preview and hash.",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@echomem/mcp",
3
- "version": "1.4.19",
3
+ "version": "1.4.21",
4
4
  "description": "EchoMem MCP bridge: cloud-first memory tools, local context HUD, and the Agent Doctor workspace forensics report (cost ledger + 3D repo city)",
5
5
  "main": "dist/index.js",
6
6
  "type": "module",
@@ -22,7 +22,11 @@
22
22
  "dev": "node dist/index.js",
23
23
  "smoke": "node smoke.mjs",
24
24
  "preview:extraction": "npm run build && node scripts/preview-extraction.mjs",
25
- "test": "npm run build && node test/local-data-paths.test.mjs && node test/crypto.test.mjs && node test/integration.test.mjs && node test/no-restart.test.mjs && node test/report.test.mjs && node test/forensics.test.mjs && node test/canonical-golden.test.mjs && node test/tools.test.mjs && node test/update-check.test.mjs && node test/delete.test.mjs && node test/low-touch-tools.test.mjs && node test/migrate.test.mjs && node test/hud.test.mjs",
25
+ "test:artifact": "npm run build && node test/package-artifact.test.mjs",
26
+ "test:registry": "node test/registry-artifact.test.mjs",
27
+ "test:registry-ui": "npm run build && node test/registry-ui.test.mjs",
28
+ "test:ui": "npm run build && node test/setup-ui.test.mjs",
29
+ "test": "npm run build && node test/local-data-paths.test.mjs && node test/crypto.test.mjs && node test/integration.test.mjs && node test/local-auth.test.mjs && node test/retrieval-only.test.mjs && node test/no-restart.test.mjs && node test/report.test.mjs && node test/forensics.test.mjs && node test/canonical-golden.test.mjs && node test/tools.test.mjs && node test/update-check.test.mjs && node test/delete.test.mjs && node test/low-touch-tools.test.mjs && node test/migrate.test.mjs && node test/restart-recovery.test.mjs && node test/hud.test.mjs",
26
30
  "prepack": "npm run build && node scripts/bundle-city.mjs"
27
31
  },
28
32
  "dependencies": {
@@ -0,0 +1,16 @@
1
+ ---
2
+ name: echomem-forget
3
+ description: Find, review, and delete outdated or incorrect memories from EchoMem. Use when the user asks EchoMem to forget, remove, or correct remembered information, or says a stored fact is no longer true. Require an explicit confirmation before the destructive delete step and do not operate on another memory provider unless requested.
4
+ ---
5
+
6
+ # EchoMem Forget
7
+
8
+ Use EchoMem's two-step deletion workflow.
9
+
10
+ 1. Search EchoMem when needed to identify the exact memory and its ID.
11
+ 2. Call `delete_memory` with the memory ID only to preview the target and receive a confirmation token.
12
+ 3. Show the preview and ask the user to confirm deletion of that exact memory.
13
+ 4. Only after confirmation, call `delete_memory` with `confirmed: true` and the exact token.
14
+ 5. If the user supplied corrected information, offer to save the replacement with `save_conversation` after deletion.
15
+
16
+ Never bypass the preview or confirmation step.
@@ -0,0 +1,6 @@
1
+ interface:
2
+ display_name: "EchoMem Forget"
3
+ short_description: "Review and delete an EchoMem memory"
4
+ default_prompt: "Use $echomem-forget to find the outdated memory, show me what will be deleted, and ask for confirmation."
5
+ policy:
6
+ allow_implicit_invocation: true
@@ -0,0 +1,27 @@
1
+ ---
2
+ name: echomem-login
3
+ description: Connect, authenticate, diagnose, or unlock the local EchoMem MCP bridge. Use when EchoMem reports a missing token, locked or expired encryption key, disconnected MCP server, setup problem, or when the user asks to log in, connect a device, check EchoMem status, or unlock their memory vault.
4
+ ---
5
+
6
+ # EchoMem Login
7
+
8
+ Use the installed EchoMem CLI and keep credentials out of chat and client configuration.
9
+
10
+ - Run `echomem-mcp status` to diagnose the current connection.
11
+ - Run `echomem-mcp login` to connect the device through the browser approval flow.
12
+ - EchoMem treats the MCP bridge as a trusted device. A verified local key stays available until the
13
+ user runs `echomem-mcp lock`, logs out, or removes the local credentials. When the key is absent,
14
+ tell the user to open their own Terminal and run `echomem-mcp unlock`.
15
+ - Run `echomem-mcp setup --client codex --skip-login` when the MCP entry itself is missing.
16
+
17
+ Do not run the bare interactive `echomem-mcp unlock` command through an agent shell: it waits for a
18
+ private passphrase that the user must enter themselves. Tell the user that the prompt reads
19
+ `Vault passphrase (typing is hidden):`, no characters appear while typing, and they should press
20
+ Return when finished. After it succeeds, retry the MCP action in the current session; no editor
21
+ restart is needed.
22
+
23
+ Use `echomem-mcp lock` when the user wants to remove the local vault key without disconnecting their
24
+ device login.
25
+
26
+ Prefer the interactive browser flow for login. Never print, log, or ask the user to paste tokens,
27
+ passphrases, or encryption keys into the conversation or expose them through command-line flags.
@@ -0,0 +1,6 @@
1
+ interface:
2
+ display_name: "EchoMem Login"
3
+ short_description: "Connect or unlock EchoMem locally"
4
+ default_prompt: "Use $echomem-login to connect this device to EchoMem or unlock my encrypted memory vault."
5
+ policy:
6
+ allow_implicit_invocation: true
@@ -0,0 +1,14 @@
1
+ ---
2
+ name: echomem-save
3
+ description: Save durable knowledge to EchoMem, the user's cross-agent long-term memory. Use when the user asks to remember, save, preserve, or carry forward information, or when active EchoMem guidance requires capturing a meaningful decision, resolved bug, architectural choice, preference, or project milestone. Prefer EchoMem when it is connected; do not silently save to another memory provider.
4
+ ---
5
+
6
+ # EchoMem Save
7
+
8
+ Use the `save_conversation` tool from the `echomem` MCP server.
9
+
10
+ 1. Capture the goal, decision or outcome, important reasoning, and relevant files or constraints. Omit noise and unrelated transcript content.
11
+ 2. Prefer structured `messages`; add a concise title, source, and useful tags.
12
+ 3. Use `passthrough: true` only when the user explicitly needs a verbatim session capsule.
13
+ 4. Never include secrets, credentials, or unnecessary sensitive content.
14
+ 5. Confirm what EchoMem saved. If saving fails, report the failure without switching providers silently.
@@ -0,0 +1,6 @@
1
+ interface:
2
+ display_name: "EchoMem Save"
3
+ short_description: "Save durable project knowledge to EchoMem"
4
+ default_prompt: "Use $echomem-save to preserve this decision and its important context in EchoMem."
5
+ policy:
6
+ allow_implicit_invocation: true
@@ -0,0 +1,14 @@
1
+ ---
2
+ name: echomem-search
3
+ description: Search EchoMem, the user's cross-agent long-term memory, for prior decisions, preferences, project context, and past work. Use when a request refers to previous work or sessions, asks what was decided or how something was implemented, needs a catch-up, or plausibly depends on context the user settled before. Prefer EchoMem when it is connected; do not silently substitute another memory provider.
4
+ ---
5
+
6
+ # EchoMem Search
7
+
8
+ Use the `echomem` MCP server as the source of long-term memory.
9
+
10
+ 1. Call `search_memories` with a concise query describing the context needed. Keep `includeAnswer` false unless an EchoMem-generated synthesis is specifically useful.
11
+ 2. Use `get_memories_by_time_range` for explicit dates, `search_memories_by_keywords` for exact terms, and `get_checkpoint_by_context` when a carryover references an EchoMem checkpoint.
12
+ 3. Distinguish recalled facts from inference and preserve dates or provenance returned by EchoMem.
13
+ 4. If EchoMem returns an action-required URL, give the user that exact URL and explanation.
14
+ 5. If EchoMem is unavailable, say so. Do not switch to another memory provider unless the user requests it.
@@ -0,0 +1,6 @@
1
+ interface:
2
+ display_name: "EchoMem Search"
3
+ short_description: "Recall decisions and context from EchoMem"
4
+ default_prompt: "Use $echomem-search to recall my prior decisions and relevant project context before continuing."
5
+ policy:
6
+ allow_implicit_invocation: true
@@ -1,315 +0,0 @@
1
- import fs from "node:fs";
2
- import path from "node:path";
3
- /**
4
- * Convert a Claude Code transcript into the event vocabulary consumed by the
5
- * vendored canonical scorer. The scorer itself remains provider-independent:
6
- * this adapter is the only place where Claude's JSONL shape is interpreted.
7
- */
8
- export function normalizeClaudeSessionForCanonical(file) {
9
- const rows = readRows(file);
10
- const sessionId = firstString(rows, "sessionId") || path.basename(file, ".jsonl");
11
- const cwd = firstString(rows, "cwd");
12
- const dynamicTools = collectToolNames(rows).map((name) => ({ name }));
13
- const inferredOverheadTokens = inferClaudeOverheadTokens(rows);
14
- const toolTokens = Math.round(JSON.stringify(dynamicTools).length / 3.3);
15
- const instructionTokens = Math.max(0, inferredOverheadTokens - toolTokens);
16
- const records = [codexRecord("session_meta", {
17
- type: "session_meta",
18
- id: sessionId,
19
- cwd,
20
- base_instructions: { text: "x".repeat(instructionTokens * 4) },
21
- dynamic_tools: dynamicTools,
22
- }, firstTimestamp(rows))];
23
- const calls = new Map();
24
- let hasUserTurn = false;
25
- for (const row of rows) {
26
- const timestamp = stringValue(row.timestamp) || firstTimestamp(rows);
27
- const type = stringValue(row.type);
28
- if (type === "user") {
29
- const message = recordValue(row.message);
30
- const content = message.content;
31
- if (isToolResultContent(content)) {
32
- if (!hasUserTurn)
33
- continue;
34
- for (const block of arrayValue(content)) {
35
- if (!isRecord(block) || block.type !== "tool_result")
36
- continue;
37
- const callId = stringValue(block.tool_use_id);
38
- const call = calls.get(callId);
39
- if (!call || call.mode === "write")
40
- continue;
41
- records.push(codexRecord("response_item", {
42
- type: "function_call_output",
43
- call_id: callId,
44
- output: normalizeToolResult(block.content),
45
- }, timestamp));
46
- }
47
- continue;
48
- }
49
- const messageText = extractText(content);
50
- const imageCount = countImages(content);
51
- records.push(codexRecord("event_msg", {
52
- type: "user_message",
53
- message: messageText,
54
- ...(imageCount > 0 ? { images: Array.from({ length: imageCount }, () => ({ type: "input_image" })) } : {}),
55
- }, timestamp));
56
- hasUserTurn = true;
57
- continue;
58
- }
59
- if (type === "system" && hasUserTurn && (row.subtype === "compact_boundary" || row.subtype === "context_compacted")) {
60
- records.push(codexRecord("event_msg", { type: "context_compacted" }, timestamp));
61
- continue;
62
- }
63
- if (type !== "assistant" || !hasUserTurn)
64
- continue;
65
- const message = recordValue(row.message);
66
- const usage = recordValue(message.usage);
67
- const inputTokens = claudeOfficialInputTokens(usage);
68
- if (inputTokens > 0) {
69
- records.push(codexRecord("event_msg", {
70
- type: "token_count",
71
- info: {
72
- last_token_usage: {
73
- input_tokens: inputTokens,
74
- cached_input_tokens: numberValue(usage.cache_read_input_tokens),
75
- output_tokens: numberValue(usage.output_tokens),
76
- },
77
- },
78
- }, timestamp));
79
- }
80
- for (const block of arrayValue(message.content)) {
81
- if (!isRecord(block))
82
- continue;
83
- if (block.type === "thinking") {
84
- records.push(codexRecord("response_item", { type: "reasoning" }, timestamp));
85
- continue;
86
- }
87
- if (block.type === "text") {
88
- const text = stringValue(block.text);
89
- if (text)
90
- records.push(codexRecord("event_msg", { type: "agent_message", message: text }, timestamp));
91
- continue;
92
- }
93
- if (block.type !== "tool_use")
94
- continue;
95
- const normalized = normalizeToolUse(block);
96
- if (!normalized)
97
- continue;
98
- calls.set(normalized.call.callId, normalized.call);
99
- records.push(...normalized.records.map((record) => ({ ...record, timestamp })));
100
- }
101
- }
102
- return {
103
- jsonl: `${records.map((record) => JSON.stringify(record)).join("\n")}\n`,
104
- sessionId,
105
- cwd,
106
- inferredOverheadTokens,
107
- };
108
- }
109
- function normalizeToolUse(block) {
110
- const name = stringValue(block.name);
111
- const callId = stringValue(block.id);
112
- if (!name || !callId)
113
- return null;
114
- const input = recordValue(block.input);
115
- if (/^(Write|Edit|MultiEdit|NotebookEdit)$/i.test(name)) {
116
- const file = stringValue(input.file_path) || stringValue(input.notebook_path) || stringValue(input.path) || "unknown";
117
- const patch = syntheticPatch(name, file, input);
118
- return {
119
- call: { mode: "write", callId },
120
- records: [
121
- codexRecord("response_item", { type: "custom_tool_call", name: "apply_patch", call_id: callId, input: patch }, ""),
122
- codexRecord("event_msg", { type: "patch_apply_end", changes: { [file]: { kind: /^Write$/i.test(name) ? "added" : "modified" } } }, ""),
123
- ],
124
- };
125
- }
126
- const command = commandForClaudeTool(name, input);
127
- if (command) {
128
- return {
129
- call: { mode: "function", callId },
130
- records: [codexRecord("response_item", {
131
- type: "function_call",
132
- name: "exec_command",
133
- call_id: callId,
134
- arguments: JSON.stringify({ cmd: command }),
135
- }, "")],
136
- };
137
- }
138
- return {
139
- call: { mode: "function", callId },
140
- records: [codexRecord("response_item", {
141
- type: "function_call",
142
- name: visualToolName(name) ? "view_image" : name,
143
- call_id: callId,
144
- arguments: JSON.stringify(input),
145
- }, "")],
146
- };
147
- }
148
- function commandForClaudeTool(name, input) {
149
- if (/^Bash$/i.test(name))
150
- return stringValue(input.command) || "bash";
151
- if (/^(Read|NotebookRead)$/i.test(name)) {
152
- const file = stringValue(input.file_path) || stringValue(input.notebook_path) || stringValue(input.path) || "unknown";
153
- const offset = Math.max(1, numberValue(input.offset) || 1);
154
- const limit = Math.max(1, numberValue(input.limit) || 2000);
155
- return `sed -n '${offset},${offset + limit - 1}p' ${quoteArg(file)}`;
156
- }
157
- if (/^Grep$/i.test(name)) {
158
- const pattern = stringValue(input.pattern) || stringValue(input.query) || "unknown";
159
- const target = stringValue(input.path) || ".";
160
- return `rg -n ${quoteArg(pattern)} ${quoteArg(target)}`;
161
- }
162
- if (/^Glob$/i.test(name))
163
- return `rg --files ${quoteArg(stringValue(input.path) || ".")}`;
164
- if (/^(LS|ListFiles)$/i.test(name))
165
- return `ls ${quoteArg(stringValue(input.path) || ".")}`;
166
- if (/^(WebSearch|WebFetch)$/i.test(name)) {
167
- const query = stringValue(input.query) || stringValue(input.url) || "web";
168
- return `rg ${quoteArg(`web:${query}`)} web`;
169
- }
170
- return null;
171
- }
172
- function syntheticPatch(name, file, input) {
173
- const chunks = [];
174
- const content = stringValue(input.content) || stringValue(input.new_string) || stringValue(input.new_source);
175
- if (content)
176
- chunks.push(content);
177
- for (const edit of arrayValue(input.edits)) {
178
- if (!isRecord(edit))
179
- continue;
180
- const value = stringValue(edit.new_string) || stringValue(edit.new_source) || stringValue(edit.content);
181
- if (value)
182
- chunks.push(value);
183
- }
184
- const body = chunks.join("\n") || `${name} ${file}`;
185
- return `*** Begin Patch\n*** ${/^Write$/i.test(name) ? "Add" : "Update"} File: ${file}\n@@\n${body}\n*** End Patch\n`;
186
- }
187
- function inferClaudeOverheadTokens(rows) {
188
- let firstUserTokens = 0;
189
- let firstUserImages = 0;
190
- let awaitingUsage = false;
191
- for (const row of rows) {
192
- const type = stringValue(row.type);
193
- if (type === "user" && !isToolResultContent(recordValue(row.message).content)) {
194
- if (!awaitingUsage) {
195
- const content = recordValue(row.message).content;
196
- firstUserTokens = Math.round(extractText(content).length / 4);
197
- firstUserImages = countImages(content);
198
- awaitingUsage = true;
199
- }
200
- continue;
201
- }
202
- if (type !== "assistant" || !awaitingUsage)
203
- continue;
204
- const total = claudeOfficialInputTokens(recordValue(recordValue(row.message).usage));
205
- if (total <= 0)
206
- continue;
207
- const structureFloor = Math.round(total * 0.03);
208
- return Math.max(0, total - firstUserTokens - firstUserImages * 4000 - structureFloor);
209
- }
210
- return 0;
211
- }
212
- function claudeOfficialInputTokens(usage) {
213
- return numberValue(usage.input_tokens)
214
- + numberValue(usage.cache_read_input_tokens)
215
- + numberValue(usage.cache_creation_input_tokens);
216
- }
217
- function normalizeToolResult(value) {
218
- if (typeof value === "string")
219
- return value;
220
- const output = [];
221
- for (const block of arrayValue(value)) {
222
- if (!isRecord(block))
223
- continue;
224
- if (block.type === "text" && typeof block.text === "string")
225
- output.push(block.text);
226
- else if (block.type === "image")
227
- output.push({ type: "input_image" });
228
- else
229
- output.push(block);
230
- }
231
- return output.length === 1 ? output[0] : output;
232
- }
233
- function extractText(value) {
234
- if (typeof value === "string")
235
- return value;
236
- return arrayValue(value).map((block) => {
237
- if (!isRecord(block))
238
- return "";
239
- if (typeof block.text === "string")
240
- return block.text;
241
- return "";
242
- }).filter(Boolean).join("\n");
243
- }
244
- function countImages(value) {
245
- if (!value)
246
- return 0;
247
- if (Array.isArray(value))
248
- return value.reduce((sum, item) => sum + countImages(item), 0);
249
- if (!isRecord(value))
250
- return 0;
251
- const own = value.type === "image" || value.type === "input_image" ? 1 : 0;
252
- return own + Object.values(value).reduce((sum, item) => sum + countImages(item), 0);
253
- }
254
- function collectToolNames(rows) {
255
- const names = new Set();
256
- for (const row of rows) {
257
- for (const block of arrayValue(recordValue(row.message).content)) {
258
- if (isRecord(block) && block.type === "tool_use" && typeof block.name === "string")
259
- names.add(block.name);
260
- }
261
- }
262
- return [...names].sort();
263
- }
264
- function readRows(file) {
265
- const rows = [];
266
- for (const line of fs.readFileSync(file, "utf8").split(/\n/)) {
267
- if (!line.trim())
268
- continue;
269
- try {
270
- const value = JSON.parse(line);
271
- if (isRecord(value))
272
- rows.push(value);
273
- }
274
- catch {
275
- // Claude may leave a partial final line while a session is active.
276
- }
277
- }
278
- return rows;
279
- }
280
- function firstString(rows, key) {
281
- for (const row of rows)
282
- if (typeof row[key] === "string" && row[key])
283
- return row[key];
284
- return null;
285
- }
286
- function firstTimestamp(rows) {
287
- return firstString(rows, "timestamp") || new Date(0).toISOString();
288
- }
289
- function codexRecord(type, payload, timestamp) {
290
- return { timestamp, type, payload };
291
- }
292
- function isToolResultContent(value) {
293
- return arrayValue(value).some((block) => isRecord(block) && block.type === "tool_result");
294
- }
295
- function visualToolName(name) {
296
- return /screenshot|image|browser|playwright/i.test(name);
297
- }
298
- function quoteArg(value) {
299
- return `"${value.replace(/["\\]/g, "_")}"`;
300
- }
301
- function isRecord(value) {
302
- return Boolean(value) && typeof value === "object" && !Array.isArray(value);
303
- }
304
- function recordValue(value) {
305
- return isRecord(value) ? value : {};
306
- }
307
- function arrayValue(value) {
308
- return Array.isArray(value) ? value : [];
309
- }
310
- function stringValue(value) {
311
- return typeof value === "string" ? value : "";
312
- }
313
- function numberValue(value) {
314
- return typeof value === "number" && Number.isFinite(value) ? value : 0;
315
- }