@echomem/mcp 1.4.7 → 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
@@ -273,6 +273,23 @@ function lastUserMessageFromConversationText(value) {
273
273
  flush();
274
274
  return lastUser || undefined;
275
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
+ }
276
293
  function normalizeRetrievalCandidate(value, fallbackRank) {
277
294
  if (!isRecord(value))
278
295
  return null;
@@ -645,7 +662,7 @@ class EchoMemApiClient {
645
662
  return { success: true, tuned: true, answer: String(data.answer || data.response || "").trim(), memories };
646
663
  }
647
664
  async searchMemories(args) {
648
- const parsed = searchMemoriesSchema.parse(args);
665
+ const parsed = searchMemoriesSchema.parse(args ?? {});
649
666
  const query = parsed.query?.trim();
650
667
  const limit = parsed.limit ?? parsed.k ?? 10;
651
668
  const threshold = parsed.threshold ?? 0.1;
@@ -693,10 +710,10 @@ class EchoMemApiClient {
693
710
  return data;
694
711
  }
695
712
  async saveConversation(args) {
696
- const parsed = saveConversationSchema.parse(args);
713
+ const parsed = saveConversationSchema.parse(args ?? {});
697
714
  let rawData = parsed.conversation?.trim() || "";
698
715
  if (!rawData && parsed.messages?.length) {
699
- rawData = parsed.messages.map((m) => `${m.role}: ${m.content}`).join("\n\n");
716
+ rawData = formatMessagesForIngest(parsed.messages);
700
717
  }
701
718
  if (!rawData) {
702
719
  throw new McpError(ErrorCode.InvalidParams, "Either conversation or messages is required.");
@@ -724,7 +741,7 @@ class EchoMemApiClient {
724
741
  return response.data;
725
742
  }
726
743
  async deleteMemory(args) {
727
- const parsed = deleteMemorySchema.parse(args);
744
+ const parsed = deleteMemorySchema.parse(args ?? {});
728
745
  const enc = await this.encState();
729
746
  const memory = await this.fetchMemoryById(parsed.memoryId, enc);
730
747
  if (!memory) {
@@ -768,7 +785,7 @@ class EchoMemApiClient {
768
785
  };
769
786
  }
770
787
  async getMemoriesByTimeRange(args) {
771
- const parsed = timeRangeSchema.parse(args);
788
+ const parsed = timeRangeSchema.parse(args ?? {});
772
789
  const enc = await this.encState();
773
790
  const response = await this.axios.post("/api/extension/memories/time-range", {
774
791
  startDate: parsed.startDate,
@@ -778,7 +795,7 @@ class EchoMemApiClient {
778
795
  return enc.enabled ? await this.decryptResult(response.data, enc.key) : response.data;
779
796
  }
780
797
  async getMemoriesByContext(args) {
781
- const parsed = getByContextSchema.parse(args);
798
+ const parsed = getByContextSchema.parse(args ?? {});
782
799
  const enc = await this.encState();
783
800
  const response = await this.axios.post("/api/extension/memories/by-context", {
784
801
  contextId: parsed.contextId,
@@ -787,7 +804,7 @@ class EchoMemApiClient {
787
804
  return enc.enabled ? await this.decryptResult(response.data, enc.key) : response.data;
788
805
  }
789
806
  async searchMemoriesByKeywords(args) {
790
- const parsed = keywordsSchema.parse(args);
807
+ const parsed = keywordsSchema.parse(args ?? {});
791
808
  const enc = await this.encState();
792
809
  const response = await this.axios.post("/api/extension/memories/keywords", {
793
810
  keywords: parsed.keywords,
@@ -796,7 +813,7 @@ class EchoMemApiClient {
796
813
  return enc.enabled ? await this.decryptResult(response.data, enc.key) : response.data;
797
814
  }
798
815
  async listFriends(args) {
799
- listFriendsSchema.parse(args);
816
+ listFriendsSchema.parse(args ?? {});
800
817
  try {
801
818
  const response = await this.axios.get("/api/extension/social/friends");
802
819
  return response.data;
@@ -806,7 +823,7 @@ class EchoMemApiClient {
806
823
  }
807
824
  }
808
825
  async searchUsers(args) {
809
- const parsed = searchUsersSchema.parse(args);
826
+ const parsed = searchUsersSchema.parse(args ?? {});
810
827
  try {
811
828
  const response = await this.axios.post("/api/extension/social/users/search", {
812
829
  query: parsed.query,
@@ -819,7 +836,7 @@ class EchoMemApiClient {
819
836
  }
820
837
  }
821
838
  async sendFriendRequest(args) {
822
- const parsed = sendFriendRequestSchema.parse(args);
839
+ const parsed = sendFriendRequestSchema.parse(args ?? {});
823
840
  try {
824
841
  const response = await this.axios.post("/api/extension/social/friend-requests", {
825
842
  receiverUserId: parsed.targetUserId,
@@ -831,7 +848,7 @@ class EchoMemApiClient {
831
848
  }
832
849
  }
833
850
  async searchOthersMemories(args) {
834
- const parsed = othersSchema.parse(args);
851
+ const parsed = othersSchema.parse(args ?? {});
835
852
  try {
836
853
  const response = await this.axios.post("/api/extension/social/public-memories/search", {
837
854
  query: parsed.query,
@@ -855,7 +872,7 @@ class EchoMemApiClient {
855
872
  }
856
873
  }
857
874
  async getPublicMemory(args) {
858
- const parsed = publicMemorySchema.parse(args);
875
+ const parsed = publicMemorySchema.parse(args ?? {});
859
876
  try {
860
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`);
861
878
  return response.data;
@@ -1026,6 +1043,8 @@ class EchoMemMCPServer {
1026
1043
  return await this.handleTimeRange(request.params.arguments);
1027
1044
  case canonicalToolNames.getByContext:
1028
1045
  return await this.handleGetByContext(request.params.arguments);
1046
+ case canonicalToolNames.checkpointByContext:
1047
+ return await this.handleGetCheckpointByContext(request.params.arguments);
1029
1048
  case canonicalToolNames.keywords:
1030
1049
  return await this.handleKeywords(request.params.arguments);
1031
1050
  case canonicalToolNames.friends:
@@ -1254,7 +1273,7 @@ Details: ${m.details || "N/A"}`)
1254
1273
  return { content: [{ type: "text", text }] };
1255
1274
  }
1256
1275
  async handleTimeRange(args) {
1257
- const parsed = timeRangeSchema.parse(args);
1276
+ const parsed = timeRangeSchema.parse(args ?? {});
1258
1277
  const { success, memories, error } = await this.client.getMemoriesByTimeRange(args);
1259
1278
  if (!success)
1260
1279
  throw new Error(`EchoMem API Error: ${error}`);
@@ -1279,7 +1298,7 @@ Details: ${m.details || "N/A"}`)
1279
1298
  };
1280
1299
  }
1281
1300
  async handleGetByContext(args) {
1282
- const parsed = getByContextSchema.parse(args);
1301
+ const parsed = getByContextSchema.parse(args ?? {});
1283
1302
  const { success, memories, error } = await this.client.getMemoriesByContext(args);
1284
1303
  if (!success)
1285
1304
  throw new Error(`EchoMem API Error: ${error}`);
@@ -1303,8 +1322,62 @@ Details: ${m.details || "N/A"}`)
1303
1322
  ],
1304
1323
  };
1305
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
+ }
1306
1379
  async handleKeywords(args) {
1307
- const parsed = keywordsSchema.parse(args);
1380
+ const parsed = keywordsSchema.parse(args ?? {});
1308
1381
  const { success, memories, error } = await this.client.searchMemoriesByKeywords(args);
1309
1382
  if (!success)
1310
1383
  throw new Error(`EchoMem API Error: ${error}`);
@@ -1329,7 +1402,7 @@ Details: ${m.details || "N/A"}`)
1329
1402
  };
1330
1403
  }
1331
1404
  async handleOthers(args) {
1332
- const parsed = othersSchema.parse(args);
1405
+ const parsed = othersSchema.parse(args ?? {});
1333
1406
  const payload = await this.client.searchOthersMemories(args);
1334
1407
  const memories = payload?.memories ?? [];
1335
1408
  if (!memories.length) {
@@ -1380,7 +1453,7 @@ Details: ${m.details || "N/A"}`;
1380
1453
  };
1381
1454
  }
1382
1455
  async handleFriends(args) {
1383
- listFriendsSchema.parse(args);
1456
+ listFriendsSchema.parse(args ?? {});
1384
1457
  const payload = await this.client.listFriends(args);
1385
1458
  const friends = Array.isArray(payload?.friends) ? payload.friends : [];
1386
1459
  if (!friends.length) {
@@ -1411,7 +1484,7 @@ Details: ${m.details || "N/A"}`;
1411
1484
  };
1412
1485
  }
1413
1486
  async handleSearchUsers(args) {
1414
- const parsed = searchUsersSchema.parse(args);
1487
+ const parsed = searchUsersSchema.parse(args ?? {});
1415
1488
  const payload = await this.client.searchUsers(args);
1416
1489
  const users = Array.isArray(payload?.users) ? payload.users : [];
1417
1490
  if (!users.length) {
@@ -1444,7 +1517,7 @@ Details: ${m.details || "N/A"}`;
1444
1517
  };
1445
1518
  }
1446
1519
  async handleSendFriendRequest(args) {
1447
- sendFriendRequestSchema.parse(args);
1520
+ sendFriendRequestSchema.parse(args ?? {});
1448
1521
  const payload = await this.client.sendFriendRequest(args);
1449
1522
  const result = isRecord(payload) ? payload : {};
1450
1523
  const targetUser = isRecord(result.targetUser) ? result.targetUser : {};
@@ -1477,7 +1550,7 @@ Details: ${m.details || "N/A"}`;
1477
1550
  };
1478
1551
  }
1479
1552
  async handlePublicMemory(args) {
1480
- const parsed = publicMemorySchema.parse(args);
1553
+ const parsed = publicMemorySchema.parse(args ?? {});
1481
1554
  const payload = await this.client.getPublicMemory(args);
1482
1555
  const memory = payload?.memory;
1483
1556
  if (!memory) {
@@ -1505,7 +1578,7 @@ Details: ${m.details || "N/A"}`;
1505
1578
  };
1506
1579
  }
1507
1580
  async handleDelete(args, rec) {
1508
- const parsed = deleteMemorySchema.parse(args);
1581
+ const parsed = deleteMemorySchema.parse(args ?? {});
1509
1582
  if (rec) {
1510
1583
  rec.memory_id_hash = hashText(parsed.memoryId);
1511
1584
  rec.delete_confirmed = parsed.confirmed;
@@ -1567,7 +1640,28 @@ async function main() {
1567
1640
  if (handled)
1568
1641
  return;
1569
1642
  const store = new KeyStore();
1570
- 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()) {
1571
1665
  // No token yet — DON'T exit. Start the server so the editor keeps the bridge alive; tools return
1572
1666
  // a "run login" nudge until `login` writes the token, then the next call picks it up (no restart).
1573
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,