@echomem/mcp 1.4.6 → 1.4.8

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,7 +4,7 @@ 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, getByContextSchema, keywordsSchema, listToolSpecs, othersSchema, resolveCanonicalToolName, saveConversationSchema, searchMemoriesSchema, timeRangeSchema, } from "./v1-contract.js";
7
+ import { canonicalToolNames, deleteMemorySchema, getByContextSchema, keywordsSchema, listFriendsSchema, listToolSpecs, othersSchema, publicMemorySchema, resolveCanonicalToolName, saveConversationSchema, searchMemoriesSchema, searchUsersSchema, sendFriendRequestSchema, 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";
@@ -15,7 +15,6 @@ import { runCli } from "./setup.js";
15
15
  import { MCP_PACKAGE_VERSION, MCP_SERVER_INSTRUCTIONS } from "./package-metadata.js";
16
16
  import { checkLatestUpdateStatus, formatUpdateNotice, formatUpdateStatusText, startBackgroundUpdateCheck, } from "./update-check.js";
17
17
  const ECHO_API_BASE_URL = process.env.ECHO_API_BASE_URL || "https://echo-mem-chrome.vercel.app";
18
- const MEMORY_FEED_API_URL = process.env.MEMORY_FEED_API_URL || "https://memory-feed.vercel.app";
19
18
  /** Thrown when no API token is present yet — the model gets a "run login" nudge, not a hard error. */
20
19
  class NoTokenError extends Error {
21
20
  }
@@ -274,6 +273,23 @@ function lastUserMessageFromConversationText(value) {
274
273
  flush();
275
274
  return lastUser || undefined;
276
275
  }
276
+ function formatMessagesForIngest(messages) {
277
+ return messages
278
+ .map((message) => {
279
+ const content = message.content.trim();
280
+ if (!content)
281
+ return "";
282
+ const role = message.role.toLowerCase();
283
+ const label = role === "user" || role === "human"
284
+ ? "User"
285
+ : role === "system"
286
+ ? "System"
287
+ : "Assistant";
288
+ return `## ${label}\n\n${content}`;
289
+ })
290
+ .filter(Boolean)
291
+ .join("\n\n---\n\n");
292
+ }
277
293
  function normalizeRetrievalCandidate(value, fallbackRank) {
278
294
  if (!isRecord(value))
279
295
  return null;
@@ -379,6 +395,22 @@ function inputAnalyticsForTool(canonicalName, args) {
379
395
  keyword_count: Array.isArray(a.keywords) ? a.keywords.length : 0,
380
396
  limit: numberArg(a, "limit"),
381
397
  };
398
+ case canonicalToolNames.searchUsers: {
399
+ const query = readString(a, "query");
400
+ const queryAnalytics = safeTextAnalytics("query", query);
401
+ return {
402
+ ...queryAnalytics,
403
+ query_preview_safe: queryAnalytics.query_preview,
404
+ query_length: query?.length ?? 0,
405
+ limit: numberArg(a, "limit"),
406
+ };
407
+ }
408
+ case canonicalToolNames.sendFriendRequest: {
409
+ const targetUserId = readString(a, "targetUserId");
410
+ return {
411
+ target_user_id_hash: targetUserId ? hashText(targetUserId) : undefined,
412
+ };
413
+ }
382
414
  case canonicalToolNames.others: {
383
415
  const query = readString(a, "query");
384
416
  const queryAnalytics = safeTextAnalytics("query", query);
@@ -386,6 +418,13 @@ function inputAnalyticsForTool(canonicalName, args) {
386
418
  ...queryAnalytics,
387
419
  query_preview_safe: queryAnalytics.query_preview,
388
420
  query_length: query?.length ?? 0,
421
+ limit: numberArg(a, "limit"),
422
+ };
423
+ }
424
+ case canonicalToolNames.publicMemory: {
425
+ const memoryId = readString(a, "memoryId");
426
+ return {
427
+ memory_id_hash: memoryId ? hashText(memoryId) : undefined,
389
428
  };
390
429
  }
391
430
  default:
@@ -623,7 +662,7 @@ class EchoMemApiClient {
623
662
  return { success: true, tuned: true, answer: String(data.answer || data.response || "").trim(), memories };
624
663
  }
625
664
  async searchMemories(args) {
626
- const parsed = searchMemoriesSchema.parse(args);
665
+ const parsed = searchMemoriesSchema.parse(args ?? {});
627
666
  const query = parsed.query?.trim();
628
667
  const limit = parsed.limit ?? parsed.k ?? 10;
629
668
  const threshold = parsed.threshold ?? 0.1;
@@ -671,10 +710,10 @@ class EchoMemApiClient {
671
710
  return data;
672
711
  }
673
712
  async saveConversation(args) {
674
- const parsed = saveConversationSchema.parse(args);
713
+ const parsed = saveConversationSchema.parse(args ?? {});
675
714
  let rawData = parsed.conversation?.trim() || "";
676
715
  if (!rawData && parsed.messages?.length) {
677
- rawData = parsed.messages.map((m) => `${m.role}: ${m.content}`).join("\n\n");
716
+ rawData = formatMessagesForIngest(parsed.messages);
678
717
  }
679
718
  if (!rawData) {
680
719
  throw new McpError(ErrorCode.InvalidParams, "Either conversation or messages is required.");
@@ -702,7 +741,7 @@ class EchoMemApiClient {
702
741
  return response.data;
703
742
  }
704
743
  async deleteMemory(args) {
705
- const parsed = deleteMemorySchema.parse(args);
744
+ const parsed = deleteMemorySchema.parse(args ?? {});
706
745
  const enc = await this.encState();
707
746
  const memory = await this.fetchMemoryById(parsed.memoryId, enc);
708
747
  if (!memory) {
@@ -746,7 +785,7 @@ class EchoMemApiClient {
746
785
  };
747
786
  }
748
787
  async getMemoriesByTimeRange(args) {
749
- const parsed = timeRangeSchema.parse(args);
788
+ const parsed = timeRangeSchema.parse(args ?? {});
750
789
  const enc = await this.encState();
751
790
  const response = await this.axios.post("/api/extension/memories/time-range", {
752
791
  startDate: parsed.startDate,
@@ -756,7 +795,7 @@ class EchoMemApiClient {
756
795
  return enc.enabled ? await this.decryptResult(response.data, enc.key) : response.data;
757
796
  }
758
797
  async getMemoriesByContext(args) {
759
- const parsed = getByContextSchema.parse(args);
798
+ const parsed = getByContextSchema.parse(args ?? {});
760
799
  const enc = await this.encState();
761
800
  const response = await this.axios.post("/api/extension/memories/by-context", {
762
801
  contextId: parsed.contextId,
@@ -765,7 +804,7 @@ class EchoMemApiClient {
765
804
  return enc.enabled ? await this.decryptResult(response.data, enc.key) : response.data;
766
805
  }
767
806
  async searchMemoriesByKeywords(args) {
768
- const parsed = keywordsSchema.parse(args);
807
+ const parsed = keywordsSchema.parse(args ?? {});
769
808
  const enc = await this.encState();
770
809
  const response = await this.axios.post("/api/extension/memories/keywords", {
771
810
  keywords: parsed.keywords,
@@ -773,21 +812,73 @@ class EchoMemApiClient {
773
812
  });
774
813
  return enc.enabled ? await this.decryptResult(response.data, enc.key) : response.data;
775
814
  }
815
+ async listFriends(args) {
816
+ listFriendsSchema.parse(args ?? {});
817
+ try {
818
+ const response = await this.axios.get("/api/extension/social/friends");
819
+ return response.data;
820
+ }
821
+ catch (error) {
822
+ throw new Error(`list_friends failed against ${ECHO_API_BASE_URL}: ${describeError(error)}`);
823
+ }
824
+ }
825
+ async searchUsers(args) {
826
+ const parsed = searchUsersSchema.parse(args ?? {});
827
+ try {
828
+ const response = await this.axios.post("/api/extension/social/users/search", {
829
+ query: parsed.query,
830
+ limit: parsed.limit,
831
+ });
832
+ return response.data;
833
+ }
834
+ catch (error) {
835
+ throw new Error(`search_users failed against ${ECHO_API_BASE_URL}: ${describeError(error)}`);
836
+ }
837
+ }
838
+ async sendFriendRequest(args) {
839
+ const parsed = sendFriendRequestSchema.parse(args ?? {});
840
+ try {
841
+ const response = await this.axios.post("/api/extension/social/friend-requests", {
842
+ receiverUserId: parsed.targetUserId,
843
+ });
844
+ return response.data;
845
+ }
846
+ catch (error) {
847
+ throw new Error(`send_friend_request failed against ${ECHO_API_BASE_URL}: ${describeError(error)}`);
848
+ }
849
+ }
776
850
  async searchOthersMemories(args) {
777
- const parsed = othersSchema.parse(args);
851
+ const parsed = othersSchema.parse(args ?? {});
778
852
  try {
779
- const response = await axios.post(`${MEMORY_FEED_API_URL.replace(/\/$/, "")}/api/search/public`, {
853
+ const response = await this.axios.post("/api/extension/social/public-memories/search", {
780
854
  query: parsed.query,
781
- excludedMemoryIds: [],
782
- }, {
783
- headers: {
784
- "Content-Type": "application/json",
785
- },
855
+ limit: parsed.limit,
856
+ target: parsed.target,
857
+ ownerUserId: parsed.ownerUserId,
858
+ ownerName: parsed.ownerName,
859
+ targetFriendIds: parsed.targetFriendIds,
860
+ targetFriendNames: parsed.targetFriendNames,
861
+ recordAccess: parsed.recordAccess,
862
+ kPerUser: parsed.kPerUser,
863
+ similarityThreshold: parsed.similarityThreshold,
864
+ timeFrameDays: parsed.timeFrameDays,
865
+ requestId: this.sessionId,
866
+ source: "mcp_friend_public_memory_search",
786
867
  });
787
868
  return response.data;
788
869
  }
789
870
  catch (error) {
790
- throw new Error(`search_others_memories failed against ${MEMORY_FEED_API_URL}: ${describeError(error)}`);
871
+ throw new Error(`search_others_memories failed against ${ECHO_API_BASE_URL}: ${describeError(error)}`);
872
+ }
873
+ }
874
+ async getPublicMemory(args) {
875
+ const parsed = publicMemorySchema.parse(args ?? {});
876
+ try {
877
+ const response = await this.axios.get(`/api/extension/social/public-memories/${encodeURIComponent(parsed.memoryId)}?requestId=${encodeURIComponent(this.sessionId)}&source=mcp_friend_public_memory_fetch`);
878
+ return response.data;
879
+ }
880
+ catch (error) {
881
+ throw new Error(`get_public_memory failed against ${ECHO_API_BASE_URL}: ${describeError(error)}`);
791
882
  }
792
883
  }
793
884
  }
@@ -952,10 +1043,20 @@ class EchoMemMCPServer {
952
1043
  return await this.handleTimeRange(request.params.arguments);
953
1044
  case canonicalToolNames.getByContext:
954
1045
  return await this.handleGetByContext(request.params.arguments);
1046
+ case canonicalToolNames.checkpointByContext:
1047
+ return await this.handleGetCheckpointByContext(request.params.arguments);
955
1048
  case canonicalToolNames.keywords:
956
1049
  return await this.handleKeywords(request.params.arguments);
1050
+ case canonicalToolNames.friends:
1051
+ return await this.handleFriends(request.params.arguments);
1052
+ case canonicalToolNames.searchUsers:
1053
+ return await this.handleSearchUsers(request.params.arguments);
1054
+ case canonicalToolNames.sendFriendRequest:
1055
+ return await this.handleSendFriendRequest(request.params.arguments);
957
1056
  case canonicalToolNames.others:
958
1057
  return await this.handleOthers(request.params.arguments);
1058
+ case canonicalToolNames.publicMemory:
1059
+ return await this.handlePublicMemory(request.params.arguments);
959
1060
  case canonicalToolNames.delete:
960
1061
  return await this.handleDelete(request.params.arguments, rec);
961
1062
  default:
@@ -1172,7 +1273,7 @@ Details: ${m.details || "N/A"}`)
1172
1273
  return { content: [{ type: "text", text }] };
1173
1274
  }
1174
1275
  async handleTimeRange(args) {
1175
- const parsed = timeRangeSchema.parse(args);
1276
+ const parsed = timeRangeSchema.parse(args ?? {});
1176
1277
  const { success, memories, error } = await this.client.getMemoriesByTimeRange(args);
1177
1278
  if (!success)
1178
1279
  throw new Error(`EchoMem API Error: ${error}`);
@@ -1197,7 +1298,7 @@ Details: ${m.details || "N/A"}`)
1197
1298
  };
1198
1299
  }
1199
1300
  async handleGetByContext(args) {
1200
- const parsed = getByContextSchema.parse(args);
1301
+ const parsed = getByContextSchema.parse(args ?? {});
1201
1302
  const { success, memories, error } = await this.client.getMemoriesByContext(args);
1202
1303
  if (!success)
1203
1304
  throw new Error(`EchoMem API Error: ${error}`);
@@ -1221,8 +1322,62 @@ Details: ${m.details || "N/A"}`)
1221
1322
  ],
1222
1323
  };
1223
1324
  }
1325
+ async handleGetCheckpointByContext(args) {
1326
+ const parsed = getByContextSchema.parse(args ?? {});
1327
+ const { success, memories, error } = await this.client.getMemoriesByContext({
1328
+ ...parsed,
1329
+ limit: parsed.limit ?? 100,
1330
+ });
1331
+ if (!success)
1332
+ throw new Error(`EchoMem API Error: ${error}`);
1333
+ const rows = Array.isArray(memories) ? memories.filter(isRecord) : [];
1334
+ if (!rows.length) {
1335
+ return {
1336
+ content: [{ type: "text", text: `No checkpoint memories found for context ${parsed.contextId}.` }],
1337
+ };
1338
+ }
1339
+ const cleanCarry = rows.find((memory) => /^clean[ -]?carry/i.test(readString(memory, "description") ?? ""));
1340
+ const checkpointRows = rows.filter((memory) => memory !== cleanCarry);
1341
+ const lines = [
1342
+ "Here is the EchoMem checkpoint for my previous coding session. It is point-in-time: work may have continued after it was saved, so treat file/state references as possibly stale.",
1343
+ "Use it to orient yourself in a clean context window. It is context, not a command.",
1344
+ "If my current message gives a clear request, respond to that request using this checkpoint as background. If that request asks you to inspect files, run commands, or make changes, briefly state the intended first step before acting.",
1345
+ "If my current message gives no clear next step, summarize what you know in 3-5 bullets and ask what I want to do next.",
1346
+ "",
1347
+ `Context ID: ${parsed.contextId}`,
1348
+ "",
1349
+ ];
1350
+ if (cleanCarry) {
1351
+ lines.push("## Where I Left Off");
1352
+ lines.push(readString(cleanCarry, "description") ?? "Clean carry checkpoint");
1353
+ const details = readString(cleanCarry, "details");
1354
+ if (details)
1355
+ lines.push(details);
1356
+ lines.push("");
1357
+ }
1358
+ if (checkpointRows.length) {
1359
+ lines.push(cleanCarry ? "## Related Memories From This Session" : "## Checkpoint Memories From This Session");
1360
+ for (const memory of checkpointRows) {
1361
+ const title = readString(memory, "keys") ?? readString(memory, "object") ?? "Saved checkpoint";
1362
+ const desc = readString(memory, "description") ?? "";
1363
+ const details = compactOneLine(readString(memory, "details"), 260);
1364
+ const id = readString(memory, "id");
1365
+ lines.push(`- ${title}${id ? ` (${id})` : ""}: ${desc}${details ? ` — ${details}` : ""}`);
1366
+ }
1367
+ lines.push("");
1368
+ }
1369
+ lines.push(`EchoMem returned ${rows.length} ${rows.length === 1 ? "memory" : "memories"} for this checkpoint context.`);
1370
+ return {
1371
+ content: [
1372
+ {
1373
+ type: "text",
1374
+ text: lines.join("\n"),
1375
+ },
1376
+ ],
1377
+ };
1378
+ }
1224
1379
  async handleKeywords(args) {
1225
- const parsed = keywordsSchema.parse(args);
1380
+ const parsed = keywordsSchema.parse(args ?? {});
1226
1381
  const { success, memories, error } = await this.client.searchMemoriesByKeywords(args);
1227
1382
  if (!success)
1228
1383
  throw new Error(`EchoMem API Error: ${error}`);
@@ -1247,32 +1402,183 @@ Details: ${m.details || "N/A"}`)
1247
1402
  };
1248
1403
  }
1249
1404
  async handleOthers(args) {
1250
- const parsed = othersSchema.parse(args);
1405
+ const parsed = othersSchema.parse(args ?? {});
1251
1406
  const payload = await this.client.searchOthersMemories(args);
1252
1407
  const memories = payload?.memories ?? [];
1253
1408
  if (!memories.length) {
1409
+ const scope = parsed.ownerUserId
1410
+ ? ` for friend ${parsed.ownerUserId}`
1411
+ : parsed.ownerName
1412
+ ? ` for friend ${parsed.ownerName}`
1413
+ : parsed.target
1414
+ ? ` for friend ${parsed.target}`
1415
+ : parsed.targetFriendIds?.length
1416
+ ? ` for ${parsed.targetFriendIds.length} selected friends`
1417
+ : parsed.targetFriendNames?.length
1418
+ ? ` for ${parsed.targetFriendNames.length} named friends`
1419
+ : "";
1420
+ const queryLabel = parsed.query?.trim() ? ` matching the query: ${parsed.query}` : "";
1254
1421
  return {
1255
- content: [{ type: "text", text: `No others' memories found matching the query: ${parsed.query}` }],
1422
+ content: [{ type: "text", text: `No others' public memories found${scope}${queryLabel}` }],
1256
1423
  };
1257
1424
  }
1425
+ const metadata = [
1426
+ typeof payload?.friendCount === "number" ? `friendCount=${payload.friendCount}` : "",
1427
+ typeof payload?.searchedFriendCount === "number" ? `searchedFriendCount=${payload.searchedFriendCount}` : "",
1428
+ typeof payload?.memoryViewsInserted === "number" ? `memoryViewsInserted=${payload.memoryViewsInserted}` : "",
1429
+ ].filter(Boolean).join(", ");
1258
1430
  const formattedResults = memories
1259
- .map((m, idx) => `[${idx + 1}] User Name: ${m.user_name || "Anonymous"}
1431
+ .map((m, idx) => {
1432
+ const score = typeof m.final_score === "number"
1433
+ ? m.final_score
1434
+ : typeof m.similarity_score === "number"
1435
+ ? m.similarity_score
1436
+ : undefined;
1437
+ return `[${idx + 1}] Memory ID: ${m.id || "unknown"}
1438
+ User ID: ${m.user_id || "unknown"}
1439
+ User Name: ${m.username || m.user_name || m.name || "Anonymous"}
1260
1440
  Time: ${m.time} | Location: ${m.location}
1261
1441
  Category: ${m.category} | Object: ${m.object}
1262
- Description: ${m.description}
1263
- Details: ${m.details || "N/A"}`)
1442
+ ${typeof score === "number" ? `Score: ${score}\n` : ""}Description: ${m.description}
1443
+ Details: ${m.details || "N/A"}`;
1444
+ })
1264
1445
  .join("\n\n");
1265
1446
  return {
1266
1447
  content: [
1267
1448
  {
1268
1449
  type: "text",
1269
- text: `Found ${memories.length} others' memories:\n\n${formattedResults}`,
1450
+ text: `Found ${memories.length} others' public memories${metadata ? ` (${metadata})` : ""}:\n\n${formattedResults}`,
1270
1451
  },
1271
1452
  ],
1272
1453
  };
1273
1454
  }
1455
+ async handleFriends(args) {
1456
+ listFriendsSchema.parse(args ?? {});
1457
+ const payload = await this.client.listFriends(args);
1458
+ const friends = Array.isArray(payload?.friends) ? payload.friends : [];
1459
+ if (!friends.length) {
1460
+ return {
1461
+ content: [{ type: "text", text: "No accepted EchoMem friends found." }],
1462
+ };
1463
+ }
1464
+ const formatted = friends
1465
+ .map((friend, idx) => {
1466
+ const name = typeof friend.name === "string" && friend.name.trim()
1467
+ ? friend.name.trim()
1468
+ : "Unnamed friend";
1469
+ const userId = typeof friend.user_id === "string" ? friend.user_id : "unknown";
1470
+ const publicMemoryCount = typeof friend.publicMemoryCount === "number"
1471
+ ? friend.publicMemoryCount
1472
+ : 0;
1473
+ return `[${idx + 1}] ${name}\nUser ID: ${userId}\nPublic memories: ${publicMemoryCount}`;
1474
+ })
1475
+ .join("\n\n");
1476
+ const total = typeof payload?.totalPublicMemoryCount === "number"
1477
+ ? payload.totalPublicMemoryCount
1478
+ : friends.reduce((sum, friend) => sum + (typeof friend.publicMemoryCount === "number" ? friend.publicMemoryCount : 0), 0);
1479
+ return {
1480
+ content: [{
1481
+ type: "text",
1482
+ text: `Found ${friends.length} accepted friends (totalPublicMemoryCount=${total}):\n\n${formatted}`,
1483
+ }],
1484
+ };
1485
+ }
1486
+ async handleSearchUsers(args) {
1487
+ const parsed = searchUsersSchema.parse(args ?? {});
1488
+ const payload = await this.client.searchUsers(args);
1489
+ const users = Array.isArray(payload?.users) ? payload.users : [];
1490
+ if (!users.length) {
1491
+ return {
1492
+ content: [{ type: "text", text: `No EchoMem users found matching "${parsed.query}".` }],
1493
+ };
1494
+ }
1495
+ const formatted = users
1496
+ .map((user, idx) => {
1497
+ const displayName = typeof user.displayName === "string" && user.displayName.trim()
1498
+ ? user.displayName.trim()
1499
+ : typeof user.username === "string" && user.username.trim()
1500
+ ? user.username.trim()
1501
+ : "Unnamed user";
1502
+ const userId = typeof user.userId === "string" ? user.userId : "unknown";
1503
+ const totalMemoryCount = typeof user.totalMemoryCount === "number" ? user.totalMemoryCount : 0;
1504
+ const publicMemoryCount = typeof user.publicMemoryCount === "number" ? user.publicMemoryCount : 0;
1505
+ const relationship = typeof user.relationshipStatus === "string" ? user.relationshipStatus : "none";
1506
+ const pendingRequestId = typeof user.pendingRequestId === "string" && user.pendingRequestId
1507
+ ? `\nPending request ID: ${user.pendingRequestId}`
1508
+ : "";
1509
+ return `[${idx + 1}] ${displayName}\nUser ID: ${userId}\nTotal memories: ${totalMemoryCount}\nPublic memories: ${publicMemoryCount}\nRelationship: ${relationship}${pendingRequestId}`;
1510
+ })
1511
+ .join("\n\n");
1512
+ return {
1513
+ content: [{
1514
+ type: "text",
1515
+ text: `Found ${users.length} EchoMem users matching "${parsed.query}":\n\n${formatted}\n\nUse send_friend_request with the exact User ID to send a pending friend request.`,
1516
+ }],
1517
+ };
1518
+ }
1519
+ async handleSendFriendRequest(args) {
1520
+ sendFriendRequestSchema.parse(args ?? {});
1521
+ const payload = await this.client.sendFriendRequest(args);
1522
+ const result = isRecord(payload) ? payload : {};
1523
+ const targetUser = isRecord(result.targetUser) ? result.targetUser : {};
1524
+ const confirmation = isRecord(result.confirmation) ? result.confirmation : {};
1525
+ const request = isRecord(result.request) ? result.request : {};
1526
+ const created = result.created === true;
1527
+ const displayName = readString(targetUser, "displayName") ?? readString(targetUser, "username") ?? "Unnamed user";
1528
+ const userId = readString(targetUser, "userId") ?? "unknown";
1529
+ const relationship = readString(targetUser, "relationshipStatus") ?? "pending_sent";
1530
+ const requestId = readString(request, "id") ?? readString(targetUser, "pendingRequestId") ?? "unknown";
1531
+ const status = readString(confirmation, "status") ?? "pending";
1532
+ const requiresApproval = confirmation.requiresApproval === true;
1533
+ const message = readString(confirmation, "message") ?? "Friend request is pending approval.";
1534
+ return {
1535
+ content: [{
1536
+ type: "text",
1537
+ text: [
1538
+ created ? "Friend request sent." : "Friend request was not duplicated.",
1539
+ "",
1540
+ `Target: ${displayName}`,
1541
+ `Target User ID: ${userId}`,
1542
+ `Relationship: ${relationship}`,
1543
+ `Request ID: ${requestId}`,
1544
+ `Status: ${status}`,
1545
+ `Requires approval: ${requiresApproval ? "yes" : "no"}`,
1546
+ "",
1547
+ `Confirmation: ${message}`,
1548
+ ].join("\n"),
1549
+ }],
1550
+ };
1551
+ }
1552
+ async handlePublicMemory(args) {
1553
+ const parsed = publicMemorySchema.parse(args ?? {});
1554
+ const payload = await this.client.getPublicMemory(args);
1555
+ const memory = payload?.memory;
1556
+ if (!memory) {
1557
+ return {
1558
+ content: [{ type: "text", text: `Public memory ${parsed.memoryId} was not found or is not accessible.` }],
1559
+ };
1560
+ }
1561
+ const text = [
1562
+ `Memory ID: ${memory.id || parsed.memoryId}`,
1563
+ `Owner User ID: ${memory.owner_user_id || memory.user_id || "Unknown"}`,
1564
+ memory.time ? `Time: ${memory.time}` : "",
1565
+ memory.location ? `Location: ${memory.location}` : "",
1566
+ memory.category ? `Category: ${memory.category}` : "",
1567
+ memory.object ? `Object: ${memory.object}` : "",
1568
+ memory.emotion ? `Emotion: ${memory.emotion}` : "",
1569
+ memory.keys ? `Keys: ${memory.keys}` : "",
1570
+ `Description: ${memory.description || ""}`,
1571
+ memory.details ? `Details: ${memory.details}` : "",
1572
+ typeof payload.accessRecordsInserted === "number"
1573
+ ? `Access records inserted: ${payload.accessRecordsInserted}`
1574
+ : "",
1575
+ ].filter(Boolean).join("\n");
1576
+ return {
1577
+ content: [{ type: "text", text }],
1578
+ };
1579
+ }
1274
1580
  async handleDelete(args, rec) {
1275
- const parsed = deleteMemorySchema.parse(args);
1581
+ const parsed = deleteMemorySchema.parse(args ?? {});
1276
1582
  if (rec) {
1277
1583
  rec.memory_id_hash = hashText(parsed.memoryId);
1278
1584
  rec.delete_confirmed = parsed.confirmed;
@@ -1334,7 +1640,28 @@ async function main() {
1334
1640
  if (handled)
1335
1641
  return;
1336
1642
  const store = new KeyStore();
1337
- if (!store.getToken()) {
1643
+ // Serve mode is meant to be SPAWNED by the MCP client (editor), which drives us over a piped stdin.
1644
+ // A human who runs the bare server in a terminal instead gets a process that blocks forever on stdio
1645
+ // with no output — indistinguishable from a hang ("wtf, it's stuck"). A TTY on stdin means no editor
1646
+ // is on the other end: say so loudly and point at the command they almost certainly meant.
1647
+ if (process.stdin.isTTY) {
1648
+ const connected = Boolean(store.getToken());
1649
+ console.error([
1650
+ "",
1651
+ "⚠️ You started the EchoMem MCP server directly in a terminal.",
1652
+ "",
1653
+ " This is a background server that waits for your editor to connect over",
1654
+ " stdio. It is NOT frozen — a blank, unresponsive terminal is exactly what",
1655
+ " a running stdio server looks like.",
1656
+ "",
1657
+ connected
1658
+ ? " Your editor launches this for you; you don't need to run it by hand."
1659
+ : " To connect this device, press Ctrl+C and run:",
1660
+ connected ? " Press Ctrl+C to stop it." : " echomem-mcp setup",
1661
+ "",
1662
+ ].join("\n"));
1663
+ }
1664
+ else if (!store.getToken()) {
1338
1665
  // No token yet — DON'T exit. Start the server so the editor keeps the bridge alive; tools return
1339
1666
  // a "run login" nudge until `login` writes the token, then the next call picks it up (no restart).
1340
1667
  console.error("EchoMem: not connected yet — run `echomem-mcp login` to connect this device.");
package/dist/migrate.js CHANGED
@@ -644,6 +644,7 @@ export function estimateMigrationEtaFromLengths(lengths, skippedActive = 0, opts
644
644
  };
645
645
  }
646
646
  export function summarizeFastMigratableDiscovery(discovery) {
647
+ const pendingCodex = discovery.pendingCodex ?? discovery.pending.filter((s) => s.source === "codex").length;
647
648
  return {
648
649
  sessions: discovery.sessions.length,
649
650
  pending: discovery.pending.length,
@@ -652,6 +653,8 @@ export function summarizeFastMigratableDiscovery(discovery) {
652
653
  skippedActive: discovery.skippedActive,
653
654
  codexCount: discovery.codexCount,
654
655
  claudeCount: discovery.claudeCount,
656
+ pendingCodex,
657
+ pendingClaudeCode: discovery.pendingClaudeCode ?? discovery.pending.length - pendingCodex,
655
658
  eta: estimateMigrationEtaFromLengths(discovery.pending.map((s) => s.size), discovery.skippedActive, {
656
659
  secondsPerSession: measuredSecondsPerSession() ?? undefined,
657
660
  }),
@@ -690,6 +693,7 @@ export function discoverMigratableFastDiscovery(opts = {}) {
690
693
  });
691
694
  const pending = opts.limit && opts.limit > 0 ? pendingAll.slice(0, opts.limit) : pendingAll;
692
695
  const codexCount = sessions.filter((s) => s.source === "codex").length;
696
+ const pendingCodex = pending.filter((s) => s.source === "codex").length;
693
697
  return {
694
698
  sessions,
695
699
  pending,
@@ -699,6 +703,8 @@ export function discoverMigratableFastDiscovery(opts = {}) {
699
703
  limited: pending.length < pendingAll.length,
700
704
  codexCount,
701
705
  claudeCount: sessions.length - codexCount,
706
+ pendingCodex,
707
+ pendingClaudeCode: pending.length - pendingCodex,
702
708
  };
703
709
  }
704
710
  export function discoverMigratableSummaryFast(opts = {}) {
@@ -757,6 +763,7 @@ export function discoverMigratableSessions(opts = {}) {
757
763
  });
758
764
  const pending = opts.limit && opts.limit > 0 ? pendingAll.slice(0, opts.limit) : pendingAll;
759
765
  const codexCount = sessions.filter((s) => s.source === "codex").length;
766
+ const pendingCodex = pending.filter((s) => s.source === "codex").length;
760
767
  return {
761
768
  sessions,
762
769
  pending,
@@ -766,6 +773,8 @@ export function discoverMigratableSessions(opts = {}) {
766
773
  limited: pending.length < pendingAll.length,
767
774
  codexCount,
768
775
  claudeCount: sessions.length - codexCount,
776
+ pendingCodex,
777
+ pendingClaudeCode: pending.length - pendingCodex,
769
778
  };
770
779
  }
771
780
  /**
@@ -787,6 +796,7 @@ export function discoverPendingSessionsTargeted(processedKeys, opts = {}) {
787
796
  if (s)
788
797
  pending.push(s);
789
798
  }
799
+ const pendingCodex = pending.filter((s) => s.source === "codex").length;
790
800
  return {
791
801
  sessions: pending,
792
802
  pending,
@@ -796,6 +806,8 @@ export function discoverPendingSessionsTargeted(processedKeys, opts = {}) {
796
806
  limited: filtered.limited,
797
807
  codexCount: filtered.codexCount,
798
808
  claudeCount: filtered.claudeCount,
809
+ pendingCodex,
810
+ pendingClaudeCode: pending.length - pendingCodex,
799
811
  accountChecked: filtered.accountChecked,
800
812
  accountCheckFailed: filtered.accountCheckFailed,
801
813
  accountCheckUnavailable: filtered.accountCheckUnavailable,
@@ -808,6 +820,7 @@ export function applyAccountImportStatus(discovery, processedKeys, opts = {}) {
808
820
  const skippedActive = discovery.sessions.length - selectableSessions.length;
809
821
  const pendingAll = selectableSessions.filter((s) => !processedKeys.has(s.conversationKey));
810
822
  const pending = opts.limit && opts.limit > 0 ? pendingAll.slice(0, opts.limit) : pendingAll;
823
+ const pendingCodex = pending.filter((s) => s.source === "codex").length;
811
824
  return {
812
825
  ...discovery,
813
826
  pending,
@@ -815,6 +828,8 @@ export function applyAccountImportStatus(discovery, processedKeys, opts = {}) {
815
828
  alreadyMigrated: selectableSessions.length - pendingAll.length,
816
829
  skippedActive,
817
830
  limited: pending.length < pendingAll.length,
831
+ pendingCodex,
832
+ pendingClaudeCode: pending.length - pendingCodex,
818
833
  accountChecked: true,
819
834
  accountCheckFailed: false,
820
835
  accountCheckUnavailable: false,
@@ -827,6 +842,7 @@ export function applyFastAccountImportStatus(discovery, processedKeys, opts = {}
827
842
  const skippedActive = discovery.sessions.length - selectableSessions.length;
828
843
  const pendingAll = selectableSessions.filter((s) => !processedKeys.has(s.conversationKey));
829
844
  const pending = opts.limit && opts.limit > 0 ? pendingAll.slice(0, opts.limit) : pendingAll;
845
+ const pendingCodex = pending.filter((s) => s.source === "codex").length;
830
846
  return {
831
847
  ...discovery,
832
848
  pending,
@@ -834,6 +850,8 @@ export function applyFastAccountImportStatus(discovery, processedKeys, opts = {}
834
850
  alreadyMigrated: selectableSessions.length - pendingAll.length,
835
851
  skippedActive,
836
852
  limited: pending.length < pendingAll.length,
853
+ pendingCodex,
854
+ pendingClaudeCode: pending.length - pendingCodex,
837
855
  accountChecked: true,
838
856
  accountCheckFailed: false,
839
857
  accountCheckUnavailable: false,