@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/README.md +14 -8
- package/assets/hud/claude.svg +1 -0
- package/assets/hud/codex.svg +1 -0
- package/assets/hud/session-viewer.html +1734 -0
- package/dist/city/10-problems-report.html +649 -0
- package/dist/city/echo-ai-city-only.html +63 -653
- package/dist/city/echo-ai-city-only.template.html +35 -17
- package/dist/city/echo-face-cutout.png +0 -0
- package/dist/forensics-10-problems.js +632 -0
- package/dist/hud/autostart.js +66 -0
- package/dist/hud/cli.js +31 -0
- package/dist/hud/electron-main.js +229 -22
- package/dist/hud/monitor.js +36 -68
- package/dist/hud/preload.cjs +12 -0
- package/dist/hud/server.js +413 -3
- package/dist/hud/web.js +713 -74
- package/dist/index.js +355 -28
- package/dist/migrate.js +18 -0
- package/dist/setup-page.js +978 -60
- package/dist/setup.js +353 -40
- package/dist/v1-contract.js +154 -5
- package/package.json +3 -2
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
|
|
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(
|
|
853
|
+
const response = await this.axios.post("/api/extension/social/public-memories/search", {
|
|
780
854
|
query: parsed.query,
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
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 ${
|
|
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
|
|
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) =>
|
|
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
|
-
|
|
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,
|