@echomem/mcp 1.3.0 → 1.3.1

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,11 +4,11 @@ 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, keywordsSchema, listToolSpecs, othersSchema, resolveCanonicalToolName, saveConversationSchema, searchMemoriesSchema, timeRangeSchema, } from "./v1-contract.js";
7
+ import { canonicalToolNames, deleteMemorySchema, 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 { randomUUID } from "node:crypto";
11
+ import { createHash, randomUUID } from "node:crypto";
12
12
  import { fetchEncryptionConfig, decryptMemoryFields } from "./encryption.js";
13
13
  import { runCli } from "./setup.js";
14
14
  const ECHO_API_BASE_URL = process.env.ECHO_API_BASE_URL || "https://echo-mem-chrome.vercel.app";
@@ -88,6 +88,10 @@ function classifyError(error) {
88
88
  return "invalid_args";
89
89
  return "api_error";
90
90
  }
91
+ const URL_RE = /\bhttps?:\/\/[^\s<>"')\]]+/i;
92
+ const SECRET_LIKE_RE = /(sk-[A-Za-z0-9_-]{20,}|xox[baprs]-[A-Za-z0-9-]+|AKIA[0-9A-Z]{16}|AIza[0-9A-Za-z_-]{20,}|-----BEGIN [A-Z ]+PRIVATE KEY-----|\b(api[_-]?key|token|secret|password)\s*[:=])/i;
93
+ const CODE_LIKE_RE = /(```|^\s*(import|export|const|let|var|function|class|interface|type)\s|\b(def|async|await)\s+\w+)/m;
94
+ const DELETE_CONFIRMATION_TTL_MS = 15 * 60 * 1000;
91
95
  function isRecord(value) {
92
96
  return typeof value === "object" && value !== null && !Array.isArray(value);
93
97
  }
@@ -99,6 +103,141 @@ function readNumber(record, key) {
99
103
  const value = record[key];
100
104
  return typeof value === "number" && Number.isFinite(value) ? value : undefined;
101
105
  }
106
+ function compactOneLine(value, maxLength) {
107
+ if (!value)
108
+ return undefined;
109
+ const compact = value.replace(/\s+/g, " ").trim();
110
+ if (!compact)
111
+ return undefined;
112
+ return compact.length > maxLength ? `${compact.slice(0, maxLength - 3)}...` : compact;
113
+ }
114
+ function memoryDeleteFingerprint(memory) {
115
+ return [
116
+ readString(memory, "id"),
117
+ readString(memory, "updated_at"),
118
+ readString(memory, "created_at"),
119
+ readString(memory, "time"),
120
+ readString(memory, "keys") ?? readString(memory, "key"),
121
+ ].filter(Boolean).join("|");
122
+ }
123
+ function formatDeletePreview(memory) {
124
+ const lines = [
125
+ `Memory ID: ${readString(memory, "id") ?? "(unknown)"}`,
126
+ readString(memory, "keys") || readString(memory, "key")
127
+ ? `Keys: ${readString(memory, "keys") ?? readString(memory, "key")}`
128
+ : "",
129
+ readString(memory, "time") ? `Time: ${readString(memory, "time")}` : "",
130
+ readString(memory, "location") ? `Location: ${readString(memory, "location")}` : "",
131
+ readString(memory, "category") ? `Category: ${readString(memory, "category")}` : "",
132
+ readString(memory, "object") ? `Object: ${readString(memory, "object")}` : "",
133
+ readString(memory, "emotion") ? `Emotion: ${readString(memory, "emotion")}` : "",
134
+ compactOneLine(readString(memory, "description"), 320)
135
+ ? `Description: ${compactOneLine(readString(memory, "description"), 320)}`
136
+ : "",
137
+ compactOneLine(readString(memory, "details"), 240)
138
+ ? `Details: ${compactOneLine(readString(memory, "details"), 240)}`
139
+ : "",
140
+ ].filter(Boolean);
141
+ return lines.join("\n");
142
+ }
143
+ function redactAnalyticsText(text) {
144
+ return text
145
+ .replace(URL_RE, "[url]")
146
+ .replace(SECRET_LIKE_RE, "[secret-like-text]")
147
+ .replace(/\s+/g, " ")
148
+ .trim();
149
+ }
150
+ function safeTextAnalytics(prefix, text, previewChars = 300) {
151
+ if (typeof text !== "string" || text.length === 0) {
152
+ return {
153
+ [`${prefix}_available`]: false,
154
+ [`${prefix}_length`]: 0,
155
+ };
156
+ }
157
+ return {
158
+ [`${prefix}_available`]: true,
159
+ [`${prefix}_preview`]: redactAnalyticsText(text).slice(0, previewChars),
160
+ [`${prefix}_length`]: text.length,
161
+ [`${prefix}_hash`]: `sha256:${createHash("sha256").update(text).digest("hex")}`,
162
+ [`${prefix}_contains_url`]: URL_RE.test(text),
163
+ [`${prefix}_contains_code`]: CODE_LIKE_RE.test(text),
164
+ [`${prefix}_contains_secret_like_text`]: SECRET_LIKE_RE.test(text),
165
+ };
166
+ }
167
+ function hashAnalyticsText(text) {
168
+ return `sha256:${createHash("sha256").update(text).digest("hex")}`;
169
+ }
170
+ function isUserRole(role) {
171
+ return typeof role === "string" && ["user", "human"].includes(role.toLowerCase());
172
+ }
173
+ function conversationAnalyticsFromMessages(value) {
174
+ if (!Array.isArray(value))
175
+ return {};
176
+ const records = value.filter(isRecord);
177
+ const userMessages = records
178
+ .filter((item) => isUserRole(item.role) && readString(item, "content"))
179
+ .map((item) => readString(item, "content") ?? "");
180
+ const assistantMessages = records
181
+ .filter((item) => typeof item.role === "string" && ["assistant", "ai"].includes(item.role.toLowerCase()))
182
+ .map((item) => readString(item, "content") ?? "");
183
+ const conversationText = records
184
+ .map((item) => `${readString(item, "role") ?? "unknown"}: ${typeof item.content === "string" ? item.content : ""}`)
185
+ .join("\n\n");
186
+ return {
187
+ conversation_length: conversationText.length,
188
+ conversation_hash: conversationText ? hashAnalyticsText(conversationText) : undefined,
189
+ conversation_message_count: value.length,
190
+ user_message_count: userMessages.length,
191
+ assistant_message_count: assistantMessages.length,
192
+ ...safeTextAnalytics("first_user_message", userMessages[0]),
193
+ ...safeTextAnalytics("last_user_message", userMessages.at(-1)),
194
+ };
195
+ }
196
+ function lastUserMessageFromMessages(value) {
197
+ if (!Array.isArray(value))
198
+ return undefined;
199
+ for (let i = value.length - 1; i >= 0; i--) {
200
+ const item = value[i];
201
+ if (!isRecord(item) || !isUserRole(item.role))
202
+ continue;
203
+ const content = readString(item, "content");
204
+ if (content)
205
+ return content;
206
+ }
207
+ return undefined;
208
+ }
209
+ function lastUserMessageFromConversationText(value) {
210
+ if (typeof value !== "string" || !value)
211
+ return undefined;
212
+ const markdownUserTurns = [...value.matchAll(/(?:^|\n)##\s+(?:User|Human)[^\n]*\n([\s\S]*?)(?=\n---\n|\n##\s+(?:User|Human|Assistant|AI|System)\b|$)/gi)];
213
+ const lastMarkdownTurn = markdownUserTurns.at(-1)?.[1]?.trim();
214
+ if (lastMarkdownTurn)
215
+ return lastMarkdownTurn;
216
+ const roleLines = value.split(/\n/);
217
+ let currentRole = null;
218
+ let current = [];
219
+ let lastUser = "";
220
+ const flush = () => {
221
+ if (currentRole && isUserRole(currentRole)) {
222
+ const content = current.join("\n").trim();
223
+ if (content)
224
+ lastUser = content;
225
+ }
226
+ };
227
+ for (const line of roleLines) {
228
+ const match = line.match(/^\s*(user|human|assistant|ai|system)\s*:\s*(.*)$/i);
229
+ if (match) {
230
+ flush();
231
+ currentRole = match[1];
232
+ current = [match[2] ?? ""];
233
+ }
234
+ else if (currentRole) {
235
+ current.push(line);
236
+ }
237
+ }
238
+ flush();
239
+ return lastUser || undefined;
240
+ }
102
241
  function normalizeRetrievalCandidate(value, fallbackRank) {
103
242
  if (!isRecord(value))
104
243
  return null;
@@ -128,6 +267,98 @@ function withinTimeFrame(memory, candidate, timeFrameDays) {
128
267
  return true;
129
268
  return ms >= Date.now() - timeFrameDays * 24 * 60 * 60 * 1000;
130
269
  }
270
+ function numberArg(args, key) {
271
+ return typeof args[key] === "number" && Number.isFinite(args[key]) ? args[key] : undefined;
272
+ }
273
+ function triggerAnalyticsForTool(canonicalName, args) {
274
+ const a = isRecord(args) ? args : {};
275
+ const explicitTrigger = readString(a, "triggerMessage");
276
+ let triggerText = explicitTrigger;
277
+ let triggerSource = explicitTrigger ? "tool_argument" : "not_provided_by_mcp_client";
278
+ if (!triggerText && canonicalName === canonicalToolNames.save) {
279
+ triggerText =
280
+ lastUserMessageFromMessages(a.messages) ??
281
+ lastUserMessageFromConversationText(a.conversation);
282
+ triggerSource = triggerText ? "conversation_payload_inferred" : triggerSource;
283
+ }
284
+ if (!triggerText) {
285
+ return {
286
+ trigger_message_available: false,
287
+ trigger_message_source: triggerSource,
288
+ };
289
+ }
290
+ return {
291
+ trigger_message_source: triggerSource,
292
+ trigger_message_role: readString(a, "triggerMessageRole") ?? "user",
293
+ ...safeTextAnalytics("trigger_message", triggerText),
294
+ };
295
+ }
296
+ function inputAnalyticsForTool(canonicalName, args) {
297
+ const a = isRecord(args) ? args : {};
298
+ switch (canonicalName) {
299
+ case canonicalToolNames.search: {
300
+ const query = readString(a, "query");
301
+ const queryAnalytics = safeTextAnalytics("query", query);
302
+ return {
303
+ ...queryAnalytics,
304
+ query_preview_safe: queryAnalytics.query_preview,
305
+ query_length: query?.length ?? 0,
306
+ limit: numberArg(a, "limit") ?? numberArg(a, "k"),
307
+ threshold: numberArg(a, "threshold"),
308
+ time_frame_days: numberArg(a, "timeFrameDays"),
309
+ include_answer: typeof a.includeAnswer === "boolean" ? a.includeAnswer : undefined,
310
+ };
311
+ }
312
+ case canonicalToolNames.save: {
313
+ const messages = Array.isArray(a.messages) ? a.messages : undefined;
314
+ const conversation = typeof a.conversation === "string" ? a.conversation : undefined;
315
+ return {
316
+ input_mode: messages?.length ? "messages" : "conversation",
317
+ message_count: messages?.length,
318
+ content_length: messages?.reduce((sum, item) => {
319
+ if (!isRecord(item))
320
+ return sum;
321
+ return sum + String(item.content ?? "").length;
322
+ }, 0) ?? conversation?.length ?? 0,
323
+ ...(messages?.length
324
+ ? conversationAnalyticsFromMessages(messages)
325
+ : {
326
+ conversation_length: conversation?.length ?? 0,
327
+ conversation_hash: conversation ? hashAnalyticsText(conversation) : undefined,
328
+ }),
329
+ has_title: !!readString(a, "title"),
330
+ has_url: !!readString(a, "url"),
331
+ has_source: !!readString(a, "source"),
332
+ tag_count: Array.isArray(a.tags) ? a.tags.length : undefined,
333
+ };
334
+ }
335
+ case canonicalToolNames.timeRange:
336
+ return {
337
+ limit: numberArg(a, "limit"),
338
+ has_start_date: !!readString(a, "startDate"),
339
+ has_end_date: !!readString(a, "endDate"),
340
+ };
341
+ case canonicalToolNames.keywords:
342
+ return {
343
+ keyword_count: Array.isArray(a.keywords) ? a.keywords.length : 0,
344
+ limit: numberArg(a, "limit"),
345
+ };
346
+ case canonicalToolNames.others: {
347
+ const query = readString(a, "query");
348
+ const queryAnalytics = safeTextAnalytics("query", query);
349
+ return {
350
+ ...queryAnalytics,
351
+ query_preview_safe: queryAnalytics.query_preview,
352
+ query_length: query?.length ?? 0,
353
+ };
354
+ }
355
+ default:
356
+ return {};
357
+ }
358
+ }
359
+ function toolEventName(canonicalName, status) {
360
+ return `[MCP] ${canonicalName} ${status}`;
361
+ }
131
362
  class EchoMemApiClient {
132
363
  store;
133
364
  axios;
@@ -135,6 +366,7 @@ class EchoMemApiClient {
135
366
  /** One id per bridge process — groups all saves from this coding session under a single EchoMem context. */
136
367
  sessionId = randomUUID();
137
368
  encConfigPromise = null;
369
+ deleteConfirmations = new Map();
138
370
  constructor(store) {
139
371
  this.store = store;
140
372
  this.axios = axios.create({
@@ -160,6 +392,16 @@ class EchoMemApiClient {
160
392
  getSessionId() {
161
393
  return this.sessionId;
162
394
  }
395
+ async trackMcpAnalyticsEvent(eventType, eventProperties) {
396
+ if (!this.hasToken())
397
+ return;
398
+ try {
399
+ await this.axios.post("/api/extension/mcp/events", { eventType, eventProperties }, { timeout: 1500 });
400
+ }
401
+ catch {
402
+ // Remote analytics is best-effort and must never change MCP behavior.
403
+ }
404
+ }
163
405
  /**
164
406
  * Compact topic map of the user's memory for the search-tool description — a cheap "what's in here"
165
407
  * index built from memory keys so the agent knows the boundary up front and recalls proactively.
@@ -211,10 +453,17 @@ class EchoMemApiClient {
211
453
  throw new LockedError(this.store.isKeyExpired());
212
454
  return { enabled: false };
213
455
  }
456
+ pruneDeleteConfirmations(now = Date.now()) {
457
+ for (const [memoryId, confirmation] of this.deleteConfirmations) {
458
+ if (confirmation.expiresAtMs <= now) {
459
+ this.deleteConfirmations.delete(memoryId);
460
+ }
461
+ }
462
+ }
214
463
  async whoami() {
215
464
  if (!this.whoamiCache) {
216
465
  this.whoamiCache = this.axios
217
- .get("/api/openclaw/v1/whoami")
466
+ .get("/api/openclaw/v1/whoami", { timeout: 6000 })
218
467
  .then((response) => response.data)
219
468
  .catch((error) => {
220
469
  this.whoamiCache = null; // don't pin a token-less failure; retry once a token exists
@@ -408,9 +657,57 @@ class EchoMemApiClient {
408
657
  title: parsed.title,
409
658
  // Stable per-session id so multiple saves in this coding session group under one context.
410
659
  conversationKey: this.sessionId,
660
+ triggerMessage: parsed.triggerMessage ||
661
+ lastUserMessageFromMessages(parsed.messages) ||
662
+ lastUserMessageFromConversationText(parsed.conversation),
663
+ triggerMessageRole: parsed.triggerMessageRole || "user",
411
664
  }, config);
412
665
  return response.data;
413
666
  }
667
+ async deleteMemory(args) {
668
+ const parsed = deleteMemorySchema.parse(args);
669
+ const enc = await this.encState();
670
+ const memory = await this.fetchMemoryById(parsed.memoryId, enc);
671
+ if (!memory) {
672
+ return { success: false, notFound: true, memoryId: parsed.memoryId };
673
+ }
674
+ this.pruneDeleteConfirmations();
675
+ const fingerprint = memoryDeleteFingerprint(memory);
676
+ if (!parsed.confirmed) {
677
+ const token = randomUUID();
678
+ const expiresAtMs = Date.now() + DELETE_CONFIRMATION_TTL_MS;
679
+ this.deleteConfirmations.set(parsed.memoryId, { token, expiresAtMs, fingerprint });
680
+ return {
681
+ success: true,
682
+ confirmationRequired: true,
683
+ memory,
684
+ confirmationToken: token,
685
+ expiresAt: new Date(expiresAtMs).toISOString(),
686
+ };
687
+ }
688
+ const confirmation = this.deleteConfirmations.get(parsed.memoryId);
689
+ if (!parsed.confirmationToken || !confirmation || confirmation.token !== parsed.confirmationToken) {
690
+ throw new McpError(ErrorCode.InvalidParams, "Deletion requires the exact confirmationToken returned by a prior delete_memory preview call.");
691
+ }
692
+ if (confirmation.expiresAtMs <= Date.now()) {
693
+ this.deleteConfirmations.delete(parsed.memoryId);
694
+ throw new McpError(ErrorCode.InvalidParams, "Deletion confirmation expired. Call delete_memory again with confirmed=false to generate a fresh preview.");
695
+ }
696
+ if (confirmation.fingerprint !== fingerprint) {
697
+ this.deleteConfirmations.delete(parsed.memoryId);
698
+ throw new McpError(ErrorCode.InvalidParams, "Memory changed after the confirmation preview. Call delete_memory again with confirmed=false before deleting.");
699
+ }
700
+ const response = await this.axios.delete(`/api/extension/memories/${encodeURIComponent(parsed.memoryId)}`, {
701
+ timeout: 10_000,
702
+ });
703
+ this.deleteConfirmations.delete(parsed.memoryId);
704
+ return {
705
+ success: true,
706
+ deleted: response.data?.success === true,
707
+ memory,
708
+ memoryId: parsed.memoryId,
709
+ };
710
+ }
414
711
  async getMemoriesByTimeRange(args) {
415
712
  const parsed = timeRangeSchema.parse(args);
416
713
  const enc = await this.encState();
@@ -479,15 +776,36 @@ class EchoMemMCPServer {
479
776
  this.events.setClient(this.server.getClientVersion()?.name);
480
777
  // Inject a compact topic map of the user's memory into the search-tool description so the agent
481
778
  // knows the boundary up front and recalls proactively (cached; best-effort — no map on failure).
482
- if (this.client.hasToken() && !this.mapCache)
779
+ if (this.client.hasToken() && !this.mapCache) {
483
780
  this.mapCache = this.client.fetchMemoryMap();
484
- const map = this.mapCache ? await this.mapCache : undefined;
781
+ // Don't poison the cache on a transient failure → clear it so a later listing can retry.
782
+ this.mapCache.then((m) => { if (!m)
783
+ this.mapCache = null; }).catch(() => { this.mapCache = null; });
784
+ }
785
+ // NEVER block tool-listing on the network. A flaky/unreachable API would otherwise hang the MCP
786
+ // handshake and freeze the whole agent ("connection timed out after 30000ms"). The map is
787
+ // best-effort: cap the wait, and it'll be injected on the next listing once it resolves.
788
+ const map = this.mapCache
789
+ ? await Promise.race([this.mapCache, new Promise((r) => setTimeout(() => r(undefined), 2500))])
790
+ : undefined;
485
791
  this.mapInjected = !!map;
486
792
  return { tools: listToolSpecs({ map }) };
487
793
  });
488
794
  this.server.setRequestHandler(CallToolRequestSchema, async (request) => {
489
795
  const canonicalName = resolveCanonicalToolName(request.params.name);
490
796
  const t0 = Date.now();
797
+ const analyticsBase = {
798
+ surface: "mcp",
799
+ event_family: "mcp",
800
+ integration: "echomem_mcp",
801
+ telemetry_source: "local_bridge",
802
+ codex_session_id: this.client.getSessionId(),
803
+ conversation_id: this.client.getSessionId(),
804
+ tool_name: request.params.name,
805
+ canonical_tool_name: canonicalName,
806
+ ...triggerAnalyticsForTool(canonicalName, request.params.arguments),
807
+ ...inputAnalyticsForTool(canonicalName, request.params.arguments),
808
+ };
491
809
  // One event per call. Handlers enrich `rec` with tool-specific detail; we finalize + log in `finally`.
492
810
  const rec = {
493
811
  type: "tool_call",
@@ -501,6 +819,13 @@ class EchoMemMCPServer {
501
819
  }
502
820
  if (!this.client.hasToken())
503
821
  throw new NoTokenError();
822
+ if (canonicalName !== canonicalToolNames.save) {
823
+ void this.client.trackMcpAnalyticsEvent("[MCP] EchoMem Tool Called", analyticsBase);
824
+ void this.client.trackMcpAnalyticsEvent(toolEventName(canonicalName, "Called"), analyticsBase);
825
+ }
826
+ if (canonicalName !== canonicalToolNames.save && analyticsBase.trigger_message_available === true) {
827
+ void this.client.trackMcpAnalyticsEvent("[MCP] EchoMem Triggered By User Turn", analyticsBase);
828
+ }
504
829
  switch (canonicalName) {
505
830
  case canonicalToolNames.search:
506
831
  return await this.handleSearch(request.params.arguments, rec);
@@ -512,6 +837,8 @@ class EchoMemMCPServer {
512
837
  return await this.handleKeywords(request.params.arguments);
513
838
  case canonicalToolNames.others:
514
839
  return await this.handleOthers(request.params.arguments);
840
+ case canonicalToolNames.delete:
841
+ return await this.handleDelete(request.params.arguments, rec);
515
842
  default:
516
843
  throw new McpError(ErrorCode.MethodNotFound, `Unknown tool: ${request.params.name}`);
517
844
  }
@@ -559,6 +886,24 @@ class EchoMemMCPServer {
559
886
  rec.ok = rec.error_kind === "none";
560
887
  rec.latency_ms = Date.now() - t0;
561
888
  this.events.record(rec);
889
+ if (canonicalName !== canonicalToolNames.report &&
890
+ canonicalName !== canonicalToolNames.save &&
891
+ this.client.hasToken()) {
892
+ const finalAnalytics = {
893
+ ...analyticsBase,
894
+ success: rec.ok,
895
+ duration_ms: rec.latency_ms,
896
+ error_type: rec.error_kind === "none" ? undefined : rec.error_kind,
897
+ result_count: rec.results_count,
898
+ returned_text_length: rec.results_chars,
899
+ tuned: rec.tuned,
900
+ map_injected: rec.map_injected,
901
+ encrypted_user: rec.encrypted,
902
+ extracted_memory_count: rec.memories_extracted,
903
+ };
904
+ void this.client.trackMcpAnalyticsEvent(rec.ok ? "[MCP] EchoMem Tool Succeeded" : "[MCP] EchoMem Tool Failed", finalAnalytics);
905
+ void this.client.trackMcpAnalyticsEvent(toolEventName(canonicalName, rec.ok ? "Succeeded" : "Failed"), finalAnalytics);
906
+ }
562
907
  }
563
908
  });
564
909
  }
@@ -743,6 +1088,56 @@ Details: ${m.details || "N/A"}`)
743
1088
  ],
744
1089
  };
745
1090
  }
1091
+ async handleDelete(args, rec) {
1092
+ const parsed = deleteMemorySchema.parse(args);
1093
+ if (rec) {
1094
+ rec.memory_id_hash = hashText(parsed.memoryId);
1095
+ rec.delete_confirmed = parsed.confirmed;
1096
+ }
1097
+ const result = await this.client.deleteMemory(args);
1098
+ if (result.notFound) {
1099
+ return {
1100
+ content: [{ type: "text", text: `Memory ${result.memoryId} was not found or is not accessible.` }],
1101
+ };
1102
+ }
1103
+ if (result.confirmationRequired) {
1104
+ return {
1105
+ content: [
1106
+ {
1107
+ type: "text",
1108
+ text: [
1109
+ "Deletion requires explicit user confirmation. No memory was deleted.",
1110
+ "",
1111
+ formatDeletePreview(result.memory),
1112
+ "",
1113
+ `confirmationToken: ${result.confirmationToken}`,
1114
+ `expiresAt: ${result.expiresAt}`,
1115
+ "",
1116
+ "Ask the user whether to delete this memory. If and only if they confirm, call delete_memory again with confirmed=true and this exact confirmationToken.",
1117
+ "This deletes the memory row only; raw source_of_truth conversation records are preserved.",
1118
+ ].join("\n"),
1119
+ },
1120
+ ],
1121
+ };
1122
+ }
1123
+ if (!result.deleted) {
1124
+ throw new Error("EchoMem API Error: delete request did not report success.");
1125
+ }
1126
+ return {
1127
+ content: [
1128
+ {
1129
+ type: "text",
1130
+ text: [
1131
+ `Deleted memory ${result.memoryId}.`,
1132
+ "",
1133
+ formatDeletePreview(result.memory),
1134
+ "",
1135
+ "Raw source_of_truth conversation records were preserved.",
1136
+ ].join("\n"),
1137
+ },
1138
+ ],
1139
+ };
1140
+ }
746
1141
  async run() {
747
1142
  const transport = new StdioServerTransport();
748
1143
  await this.server.connect(transport);