@echomem/mcp 1.4.1 → 1.4.3
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 +42 -7
- package/assets/hud/echo-face-cutout.png +0 -0
- package/dist/hud/adapters.js +288 -0
- package/dist/hud/api.js +29 -0
- package/dist/hud/capsule.js +125 -0
- package/dist/hud/cli.js +142 -0
- package/dist/hud/electron-main.js +224 -0
- package/dist/hud/fs.js +63 -0
- package/dist/hud/hooks.js +50 -0
- package/dist/hud/metric.js +158 -0
- package/dist/hud/monitor.js +106 -0
- package/dist/hud/preload.cjs +10 -0
- package/dist/hud/render.js +39 -0
- package/dist/hud/report.js +125 -0
- package/dist/hud/server.js +95 -0
- package/dist/hud/web.js +509 -0
- package/dist/index.js +119 -6
- package/dist/package-metadata.js +32 -0
- package/dist/report.js +1 -1
- package/dist/setup.js +386 -5
- package/dist/v1-contract.js +61 -2
- package/package.json +12 -8
package/dist/index.js
CHANGED
|
@@ -4,13 +4,15 @@ 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, keywordsSchema, listToolSpecs, othersSchema, resolveCanonicalToolName, saveConversationSchema, searchMemoriesSchema, timeRangeSchema, } from "./v1-contract.js";
|
|
7
|
+
import { canonicalToolNames, deleteMemorySchema, getByContextSchema, 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 { contextHealthMarkdown, recomposeCapsuleMarkdown } from "./hud/api.js";
|
|
11
12
|
import { createHash, randomUUID } from "node:crypto";
|
|
12
13
|
import { fetchEncryptionConfig, decryptMemoryFields } from "./encryption.js";
|
|
13
14
|
import { runCli } from "./setup.js";
|
|
15
|
+
import { MCP_PACKAGE_VERSION, MCP_SERVER_INSTRUCTIONS } from "./package-metadata.js";
|
|
14
16
|
const ECHO_API_BASE_URL = process.env.ECHO_API_BASE_URL || "https://echo-mem-chrome.vercel.app";
|
|
15
17
|
const MEMORY_FEED_API_URL = process.env.MEMORY_FEED_API_URL || "https://memory-feed.vercel.app";
|
|
16
18
|
/** Thrown when no API token is present yet — the model gets a "run login" nudge, not a hard error. */
|
|
@@ -690,6 +692,7 @@ class EchoMemApiClient {
|
|
|
690
692
|
title: parsed.title,
|
|
691
693
|
// Stable per-session id so multiple saves in this coding session group under one context.
|
|
692
694
|
conversationKey: this.sessionId,
|
|
695
|
+
passthrough: parsed.passthrough || false,
|
|
693
696
|
triggerMessage: parsed.triggerMessage ||
|
|
694
697
|
lastUserMessageFromMessages(parsed.messages) ||
|
|
695
698
|
lastUserMessageFromConversationText(parsed.conversation),
|
|
@@ -751,6 +754,15 @@ class EchoMemApiClient {
|
|
|
751
754
|
});
|
|
752
755
|
return enc.enabled ? await this.decryptResult(response.data, enc.key) : response.data;
|
|
753
756
|
}
|
|
757
|
+
async getMemoriesByContext(args) {
|
|
758
|
+
const parsed = getByContextSchema.parse(args);
|
|
759
|
+
const enc = await this.encState();
|
|
760
|
+
const response = await this.axios.post("/api/extension/memories/by-context", {
|
|
761
|
+
contextId: parsed.contextId,
|
|
762
|
+
limit: parsed.limit,
|
|
763
|
+
});
|
|
764
|
+
return enc.enabled ? await this.decryptResult(response.data, enc.key) : response.data;
|
|
765
|
+
}
|
|
754
766
|
async searchMemoriesByKeywords(args) {
|
|
755
767
|
const parsed = keywordsSchema.parse(args);
|
|
756
768
|
const enc = await this.encState();
|
|
@@ -778,7 +790,7 @@ class EchoMemApiClient {
|
|
|
778
790
|
}
|
|
779
791
|
}
|
|
780
792
|
}
|
|
781
|
-
const SERVER_VERSION =
|
|
793
|
+
const SERVER_VERSION = MCP_PACKAGE_VERSION;
|
|
782
794
|
class EchoMemMCPServer {
|
|
783
795
|
server;
|
|
784
796
|
client;
|
|
@@ -793,6 +805,7 @@ class EchoMemMCPServer {
|
|
|
793
805
|
name: "echomem-mcp",
|
|
794
806
|
version: SERVER_VERSION,
|
|
795
807
|
}, {
|
|
808
|
+
instructions: MCP_SERVER_INSTRUCTIONS,
|
|
796
809
|
capabilities: {
|
|
797
810
|
tools: {},
|
|
798
811
|
},
|
|
@@ -870,6 +883,45 @@ class EchoMemMCPServer {
|
|
|
870
883
|
if (canonicalName === canonicalToolNames.report) {
|
|
871
884
|
return { content: [{ type: "text", text: await buildReportText(false) }] };
|
|
872
885
|
}
|
|
886
|
+
if (canonicalName === canonicalToolNames.contextHealth) {
|
|
887
|
+
const client = isRecord(request.params.arguments) && typeof request.params.arguments.client === "string"
|
|
888
|
+
? request.params.arguments.client
|
|
889
|
+
: "auto";
|
|
890
|
+
const mode = client === "codex" || client === "claude-code" || client === "claude-desktop" || client === "auto"
|
|
891
|
+
? client
|
|
892
|
+
: "auto";
|
|
893
|
+
return { content: [{ type: "text", text: await contextHealthMarkdown(mode) }] };
|
|
894
|
+
}
|
|
895
|
+
if (canonicalName === canonicalToolNames.recompose) {
|
|
896
|
+
const client = isRecord(request.params.arguments) && typeof request.params.arguments.client === "string"
|
|
897
|
+
? request.params.arguments.client
|
|
898
|
+
: "auto";
|
|
899
|
+
const mode = client === "codex" || client === "claude-code" || client === "claude-desktop" || client === "auto"
|
|
900
|
+
? client
|
|
901
|
+
: "auto";
|
|
902
|
+
const capsuleText = await recomposeCapsuleMarkdown(mode);
|
|
903
|
+
// If logged in, persist the capsule via passthrough so it's retrievable by contextId.
|
|
904
|
+
if (this.client.hasToken()) {
|
|
905
|
+
try {
|
|
906
|
+
const saveResult = await this.client.saveConversation({
|
|
907
|
+
conversation: capsuleText,
|
|
908
|
+
title: "Session capsule (recompose)",
|
|
909
|
+
source: "mcp_recompose",
|
|
910
|
+
passthrough: true,
|
|
911
|
+
});
|
|
912
|
+
const ctxId = saveResult?.contextId;
|
|
913
|
+
const capId = saveResult?.capsuleId;
|
|
914
|
+
const persistLine = ctxId
|
|
915
|
+
? `\n\n---\nCapsule persisted${capId ? ` (${capId})` : ""}. To reload in a fresh session:\nget_memories_by_context({ contextId: "${ctxId}" })`
|
|
916
|
+
: "";
|
|
917
|
+
return { content: [{ type: "text", text: capsuleText + persistLine }] };
|
|
918
|
+
}
|
|
919
|
+
catch {
|
|
920
|
+
// Persistence is best-effort; return the capsule text regardless.
|
|
921
|
+
}
|
|
922
|
+
}
|
|
923
|
+
return { content: [{ type: "text", text: capsuleText }] };
|
|
924
|
+
}
|
|
873
925
|
if (!this.client.hasToken())
|
|
874
926
|
throw new NoTokenError();
|
|
875
927
|
if (canonicalName !== canonicalToolNames.save) {
|
|
@@ -886,6 +938,8 @@ class EchoMemMCPServer {
|
|
|
886
938
|
return await this.handleSave(request.params.arguments, rec);
|
|
887
939
|
case canonicalToolNames.timeRange:
|
|
888
940
|
return await this.handleTimeRange(request.params.arguments);
|
|
941
|
+
case canonicalToolNames.getByContext:
|
|
942
|
+
return await this.handleGetByContext(request.params.arguments);
|
|
889
943
|
case canonicalToolNames.keywords:
|
|
890
944
|
return await this.handleKeywords(request.params.arguments);
|
|
891
945
|
case canonicalToolNames.others:
|
|
@@ -940,6 +994,8 @@ class EchoMemMCPServer {
|
|
|
940
994
|
rec.latency_ms = Date.now() - t0;
|
|
941
995
|
this.events.record(rec);
|
|
942
996
|
if (canonicalName !== canonicalToolNames.report &&
|
|
997
|
+
canonicalName !== canonicalToolNames.contextHealth &&
|
|
998
|
+
canonicalName !== canonicalToolNames.recompose &&
|
|
943
999
|
canonicalName !== canonicalToolNames.save &&
|
|
944
1000
|
this.client.hasToken()) {
|
|
945
1001
|
const finalAnalytics = {
|
|
@@ -1061,14 +1117,46 @@ Details: ${m.details || "N/A"}`)
|
|
|
1061
1117
|
rec.conversation_chars = text.length;
|
|
1062
1118
|
rec.save_source = typeof a?.source === "string" ? a.source : sourceFallback;
|
|
1063
1119
|
}
|
|
1064
|
-
const { success, memoriesExtracted, error } = await this.client.saveConversation(enrichedArgs);
|
|
1120
|
+
const { success, memoriesExtracted, extractedMemories, contextId, capsuleId, passthrough: isPassthrough, error } = await this.client.saveConversation(enrichedArgs);
|
|
1065
1121
|
if (!success)
|
|
1066
1122
|
throw new Error(`EchoMem API Error: ${error}`);
|
|
1067
1123
|
if (rec)
|
|
1068
1124
|
rec.memories_extracted = typeof memoriesExtracted === "number" ? memoriesExtracted : undefined;
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1125
|
+
// Passthrough saves store the capsule verbatim — no extraction, no embeddings.
|
|
1126
|
+
if (isPassthrough) {
|
|
1127
|
+
const text = [
|
|
1128
|
+
`Session capsule saved (passthrough, no extraction).`,
|
|
1129
|
+
typeof capsuleId === "string" && capsuleId ? `Capsule ID: ${capsuleId}` : "",
|
|
1130
|
+
typeof contextId === "string" && contextId ? `Context: ${contextId}` : "",
|
|
1131
|
+
"",
|
|
1132
|
+
`To reload this capsule in a fresh session: get_memories_by_context({ contextId: "${contextId}" })`,
|
|
1133
|
+
].filter(Boolean).join("\n");
|
|
1134
|
+
return { content: [{ type: "text", text }] };
|
|
1135
|
+
}
|
|
1136
|
+
// Surface WHAT was captured (not just the count) so the agent can verify the key facts survived
|
|
1137
|
+
// extraction, and so it holds the ids to deterministically re-fetch this batch later (warm-up).
|
|
1138
|
+
const saved = Array.isArray(extractedMemories) ? extractedMemories.filter(isRecord) : [];
|
|
1139
|
+
const list = saved
|
|
1140
|
+
.map((m, idx) => {
|
|
1141
|
+
const keys = readString(m, "keys") ?? "(no key)";
|
|
1142
|
+
const description = readString(m, "description") ?? "";
|
|
1143
|
+
const details = readString(m, "details") ?? "";
|
|
1144
|
+
const id = readString(m, "id");
|
|
1145
|
+
return [
|
|
1146
|
+
`[${idx + 1}] ${keys}${id ? ` · id ${id}` : ""}`,
|
|
1147
|
+
description ? `Description: ${description}` : "",
|
|
1148
|
+
details ? `Details: ${details}` : "",
|
|
1149
|
+
].filter(Boolean).join("\n");
|
|
1150
|
+
})
|
|
1151
|
+
.join("\n\n");
|
|
1152
|
+
const text = [
|
|
1153
|
+
`Successfully ingested conversation. Extracted ${memoriesExtracted} memory distinct events.`,
|
|
1154
|
+
list,
|
|
1155
|
+
saved.length
|
|
1156
|
+
? `Verify these captured the key facts. To re-fetch this exact batch later, search by the ids above${typeof contextId === "string" && contextId ? ` (context ${contextId})` : ""}.`
|
|
1157
|
+
: "",
|
|
1158
|
+
].filter(Boolean).join("\n\n");
|
|
1159
|
+
return { content: [{ type: "text", text }] };
|
|
1072
1160
|
}
|
|
1073
1161
|
async handleTimeRange(args) {
|
|
1074
1162
|
const parsed = timeRangeSchema.parse(args);
|
|
@@ -1095,6 +1183,31 @@ Details: ${m.details || "N/A"}`)
|
|
|
1095
1183
|
],
|
|
1096
1184
|
};
|
|
1097
1185
|
}
|
|
1186
|
+
async handleGetByContext(args) {
|
|
1187
|
+
const parsed = getByContextSchema.parse(args);
|
|
1188
|
+
const { success, memories, error } = await this.client.getMemoriesByContext(args);
|
|
1189
|
+
if (!success)
|
|
1190
|
+
throw new Error(`EchoMem API Error: ${error}`);
|
|
1191
|
+
if (!memories?.length) {
|
|
1192
|
+
return {
|
|
1193
|
+
content: [{ type: "text", text: `No memories found for context ${parsed.contextId}.` }],
|
|
1194
|
+
};
|
|
1195
|
+
}
|
|
1196
|
+
const formattedResults = memories
|
|
1197
|
+
.map((m, idx) => `[${idx + 1}] ${m.keys || "Saved memory"}${m.id ? ` · id ${m.id}` : ""}
|
|
1198
|
+
Time: ${m.time} | Category: ${m.category} | Object: ${m.object} | Emotion: ${m.emotion}
|
|
1199
|
+
Description: ${m.description}
|
|
1200
|
+
Details: ${m.details || "N/A"}`)
|
|
1201
|
+
.join("\n\n");
|
|
1202
|
+
return {
|
|
1203
|
+
content: [
|
|
1204
|
+
{
|
|
1205
|
+
type: "text",
|
|
1206
|
+
text: `Recalled ${memories.length} memories from context ${parsed.contextId} (deterministic full batch):\n\n${formattedResults}`,
|
|
1207
|
+
},
|
|
1208
|
+
],
|
|
1209
|
+
};
|
|
1210
|
+
}
|
|
1098
1211
|
async handleKeywords(args) {
|
|
1099
1212
|
const parsed = keywordsSchema.parse(args);
|
|
1100
1213
|
const { success, memories, error } = await this.client.searchMemoriesByKeywords(args);
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
const FALLBACK_PACKAGE = {
|
|
3
|
+
name: "@echomem/mcp",
|
|
4
|
+
version: "0.0.0",
|
|
5
|
+
description: "EchoMem Cloud-First MCP Server",
|
|
6
|
+
};
|
|
7
|
+
function loadPackageJson() {
|
|
8
|
+
try {
|
|
9
|
+
const packageUrl = new URL("../package.json", import.meta.url);
|
|
10
|
+
return JSON.parse(fs.readFileSync(packageUrl, "utf8"));
|
|
11
|
+
}
|
|
12
|
+
catch {
|
|
13
|
+
return {};
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
const packageJson = loadPackageJson();
|
|
17
|
+
function stringOrFallback(value, fallback) {
|
|
18
|
+
return typeof value === "string" && value.trim() ? value.trim() : fallback;
|
|
19
|
+
}
|
|
20
|
+
export const MCP_PACKAGE_NAME = stringOrFallback(packageJson.name, FALLBACK_PACKAGE.name);
|
|
21
|
+
export const MCP_PACKAGE_VERSION = stringOrFallback(packageJson.version, FALLBACK_PACKAGE.version);
|
|
22
|
+
export const MCP_PACKAGE_DESCRIPTION = stringOrFallback(packageJson.description, FALLBACK_PACKAGE.description);
|
|
23
|
+
export const MCP_PACKAGE_LABEL = `${MCP_PACKAGE_NAME}@${MCP_PACKAGE_VERSION}`;
|
|
24
|
+
export const MCP_UPDATE_COMMAND = `npx -y ${MCP_PACKAGE_NAME}@latest update`;
|
|
25
|
+
export const MCP_SERVER_INSTRUCTIONS = [
|
|
26
|
+
`${MCP_PACKAGE_DESCRIPTION} (${MCP_PACKAGE_LABEL}).`,
|
|
27
|
+
`If the user or local config expects a newer EchoMem MCP version than ${MCP_PACKAGE_VERSION}, update once with \`${MCP_UPDATE_COMMAND}\` and start a new MCP session.`,
|
|
28
|
+
"Do not auto-update on every MCP startup; this bridge is intentionally stable between explicit updates.",
|
|
29
|
+
].join(" ");
|
|
30
|
+
export function withMcpVersion(description) {
|
|
31
|
+
return `${description}\n\nEchoMem MCP bridge: ${MCP_PACKAGE_LABEL}. If this version is stale, update once with \`${MCP_UPDATE_COMMAND}\` (add \`--client cursor|windsurf|claude-desktop|codex\` when needed), then start a new MCP session. Do not run updates repeatedly or on every startup.`;
|
|
32
|
+
}
|
package/dist/report.js
CHANGED
|
@@ -623,7 +623,7 @@ function renderText(a, memCount, useColor) {
|
|
|
623
623
|
L(c.green(`✓ You're connected — every new session now builds on your ${memCount} memories.`));
|
|
624
624
|
}
|
|
625
625
|
else {
|
|
626
|
-
L(c.green("→ Start now (no signup wall): ") + c.bold("
|
|
626
|
+
L(c.green("→ Start now (no signup wall): ") + c.bold("npm i -g @echomem/mcp@latest && echomem-mcp setup"));
|
|
627
627
|
L(c.dim(` ~1 minute. Your next coding session recalls instead of re-reading.`));
|
|
628
628
|
}
|
|
629
629
|
NL();
|