@echomem/mcp 1.4.0 → 1.4.2
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/city/echo-ai-city-only.html +643 -35
- package/dist/city/personality_stickers/bossy.png +0 -0
- package/dist/city/personality_stickers/ghosty.png +0 -0
- package/dist/city/personality_stickers/loopy.png +0 -0
- package/dist/city/personality_stickers/lusty.png +0 -0
- package/dist/city/personality_stickers/maxxy.png +0 -0
- package/dist/city/personality_stickers/tabby.png +0 -0
- package/dist/forensics.js +9 -4
- 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 +507 -0
- package/dist/index.js +119 -6
- package/dist/package-metadata.js +32 -0
- package/dist/report.js +1 -1
- package/dist/setup-page.js +1 -2
- package/dist/setup.js +80 -1
- 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();
|
package/dist/setup-page.js
CHANGED
package/dist/setup.js
CHANGED
|
@@ -29,6 +29,8 @@ import { cmdMigrate, applyAccountImportStatus, applyFastAccountImportStatus, dis
|
|
|
29
29
|
import { syncCodexUsage } from "./codex-sync.js";
|
|
30
30
|
import { renderSetupPage } from "./setup-page.js";
|
|
31
31
|
import { repoLabel } from "./forensics.js";
|
|
32
|
+
import { installHooks } from "./hud/hooks.js";
|
|
33
|
+
import { MCP_PACKAGE_LABEL, MCP_PACKAGE_VERSION, MCP_UPDATE_COMMAND } from "./package-metadata.js";
|
|
32
34
|
// The hosted connect-device page is now only the account-auth/token courier. The dashboard itself is
|
|
33
35
|
// served by this localhost bridge, where local logs and processed/unprocessed counts never leave the
|
|
34
36
|
// device unless the user explicitly starts migration. Override the hosted auth origin with ECHO_WEB_URL.
|
|
@@ -791,7 +793,55 @@ async function cmdSetup(flags) {
|
|
|
791
793
|
}
|
|
792
794
|
}
|
|
793
795
|
console.log("");
|
|
794
|
-
|
|
796
|
+
if (flags["skip-login"] || flags["no-login"]) {
|
|
797
|
+
console.log(`Skipped login; existing EchoMem credentials are unchanged. Current bridge: ${MCP_PACKAGE_LABEL}`);
|
|
798
|
+
}
|
|
799
|
+
else {
|
|
800
|
+
await cmdLogin(flags);
|
|
801
|
+
}
|
|
802
|
+
if (flags["with-hud"])
|
|
803
|
+
await cmdSetupHud(flags);
|
|
804
|
+
}
|
|
805
|
+
async function cmdUpdate(flags) {
|
|
806
|
+
await cmdSetup({ ...flags, "skip-login": true });
|
|
807
|
+
console.log(`Update config complete. Start a new MCP session to load ${MCP_PACKAGE_LABEL}.`);
|
|
808
|
+
}
|
|
809
|
+
async function cmdSetupHud(flags) {
|
|
810
|
+
const client = parseHudClient(flags["hud-client"] || "auto");
|
|
811
|
+
const hudCli = resolveHudCliPath();
|
|
812
|
+
console.log("");
|
|
813
|
+
console.log(`✅ EchoMem HUD available: ${process.execPath} ${hudCli}`);
|
|
814
|
+
if (flags["install-hud-hooks"]) {
|
|
815
|
+
const paths = installHooks(client === "claude-desktop" ? "auto" : client);
|
|
816
|
+
console.log(`✅ Installed EchoMem HUD hook support:\n${paths.map((p) => ` - ${p}`).join("\n")}`);
|
|
817
|
+
console.log(" Codex users: run /hooks in a new Codex session to review and trust changed hooks.");
|
|
818
|
+
}
|
|
819
|
+
else {
|
|
820
|
+
console.log("ℹ️ HUD hooks not installed. Add --install-hud-hooks if you want lifecycle wakeups.");
|
|
821
|
+
}
|
|
822
|
+
if (!flags["no-launch-hud"]) {
|
|
823
|
+
try {
|
|
824
|
+
spawn(process.execPath, [hudCli, "app", "--client", client], { stdio: "ignore", detached: true }).unref();
|
|
825
|
+
console.log("✅ Launched EchoMem HUD app.");
|
|
826
|
+
}
|
|
827
|
+
catch {
|
|
828
|
+
console.log(`ℹ️ Could not auto-launch HUD. Run: echomem-hud app --client ${client}`);
|
|
829
|
+
}
|
|
830
|
+
}
|
|
831
|
+
else {
|
|
832
|
+
console.log(`Run the HUD later with: echomem-hud app --client ${client}`);
|
|
833
|
+
}
|
|
834
|
+
}
|
|
835
|
+
function resolveHudCliPath() {
|
|
836
|
+
const entry = fs.realpathSync(process.argv[1] || "");
|
|
837
|
+
const base = path.dirname(entry);
|
|
838
|
+
const candidate = path.join(base, "hud", "cli.js");
|
|
839
|
+
return fs.existsSync(candidate) ? candidate : path.join(base, "hud", "cli.ts");
|
|
840
|
+
}
|
|
841
|
+
function parseHudClient(value) {
|
|
842
|
+
return value === "codex" || value === "claude-code" || value === "claude-desktop" || value === "both" || value === "auto"
|
|
843
|
+
? value
|
|
844
|
+
: "auto";
|
|
795
845
|
}
|
|
796
846
|
async function cmdLogin(flags) {
|
|
797
847
|
// Manual path (also the headless path): secrets supplied as flags.
|
|
@@ -1334,8 +1384,27 @@ async function cmdUnlock(flags) {
|
|
|
1334
1384
|
async function cmdStatus() {
|
|
1335
1385
|
const store = new KeyStore();
|
|
1336
1386
|
const token = store.getToken();
|
|
1387
|
+
console.log(`EchoMem MCP: ${MCP_PACKAGE_LABEL}`);
|
|
1337
1388
|
console.log(`Credentials file: ${store.path()}`);
|
|
1338
1389
|
console.log(`API token: ${token ? "present" : "MISSING — run `echomem-mcp login`"}`);
|
|
1390
|
+
if (token) {
|
|
1391
|
+
// Best-effort: resolve who this token belongs to so `status` shows the logged-in account.
|
|
1392
|
+
// whoami already exists in production, so this works even against a local API checkout.
|
|
1393
|
+
try {
|
|
1394
|
+
const { data } = await withTimeout(authedAxios(token).get("/api/openclaw/v1/whoami"), 6000, "WHOAMI_TIMEOUT");
|
|
1395
|
+
const email = typeof data?.email === "string" ? data.email : "";
|
|
1396
|
+
const userId = typeof data?.user_id === "string" ? data.user_id : "";
|
|
1397
|
+
if (email)
|
|
1398
|
+
console.log(`Logged in as: ${email}${userId ? ` (${userId})` : ""}`);
|
|
1399
|
+
else if (userId)
|
|
1400
|
+
console.log(`Logged in as: ${userId}`);
|
|
1401
|
+
else
|
|
1402
|
+
console.log("Logged in as: (token valid; account identity unavailable)");
|
|
1403
|
+
}
|
|
1404
|
+
catch (error) {
|
|
1405
|
+
console.log(`Logged in as: (could not verify — ${formatVerificationError(error)})`);
|
|
1406
|
+
}
|
|
1407
|
+
}
|
|
1339
1408
|
console.log(`Encryption key: ${store.getKey() ? "present" : store.isKeyExpired() ? "EXPIRED — run `echomem-mcp unlock`" : "not set"}`);
|
|
1340
1409
|
const detected = detectClients();
|
|
1341
1410
|
console.log(`Detected clients: ${detected.length ? detected.map((c) => c.label).join(", ") : "none auto-detected"}`);
|
|
@@ -1355,6 +1424,9 @@ const HELP = `EchoMem MCP — local memory bridge
|
|
|
1355
1424
|
Usage:
|
|
1356
1425
|
echomem-mcp Run the MCP server (stdio; default — used by your editor)
|
|
1357
1426
|
echomem-mcp setup [--client X] Detect editor, write its MCP config, then log in
|
|
1427
|
+
echomem-mcp setup --skip-login Write MCP config without opening login/browser
|
|
1428
|
+
echomem-mcp update [--client X] Repoint MCP config to this installed bridge; no login/browser
|
|
1429
|
+
echomem-mcp setup --with-hud Configure MCP, then launch the EchoMem context HUD
|
|
1358
1430
|
echomem-mcp login Approve this device in the browser (or --token/--passphrase)
|
|
1359
1431
|
echomem-mcp unlock Re-derive the encryption key after its TTL (or --passphrase)
|
|
1360
1432
|
echomem-mcp status Show token/key/clients
|
|
@@ -1371,8 +1443,12 @@ Usage:
|
|
|
1371
1443
|
|
|
1372
1444
|
Manual / headless:
|
|
1373
1445
|
echomem-mcp login --token ec_xxx [--passphrase <vault pass> | --key <base64>]
|
|
1446
|
+
${MCP_UPDATE_COMMAND} --client codex # one-shot latest update, no browser login
|
|
1374
1447
|
echomem-mcp setup --dev /abs/path/dist/index.js # point clients at a local checkout
|
|
1448
|
+
echomem-mcp setup --with-hud --install-hud-hooks --client codex [--hud-client auto]
|
|
1375
1449
|
echomem-mcp sync-usage --days 7 --limit 50 --dry-run
|
|
1450
|
+
|
|
1451
|
+
Current bridge version: ${MCP_PACKAGE_VERSION}
|
|
1376
1452
|
`;
|
|
1377
1453
|
/** Returns true if argv was a recognized subcommand (and was handled). */
|
|
1378
1454
|
export async function runCli(argv) {
|
|
@@ -1382,6 +1458,9 @@ export async function runCli(argv) {
|
|
|
1382
1458
|
case "setup":
|
|
1383
1459
|
await cmdSetup(flags);
|
|
1384
1460
|
return true;
|
|
1461
|
+
case "update":
|
|
1462
|
+
await cmdUpdate(flags);
|
|
1463
|
+
return true;
|
|
1385
1464
|
case "login":
|
|
1386
1465
|
await cmdLogin(flags);
|
|
1387
1466
|
return true;
|
package/dist/v1-contract.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
|
+
import { withMcpVersion } from "./package-metadata.js";
|
|
2
3
|
export const canonicalToolNames = {
|
|
3
4
|
search: "search_memories",
|
|
4
5
|
save: "save_conversation",
|
|
@@ -6,7 +7,10 @@ export const canonicalToolNames = {
|
|
|
6
7
|
keywords: "search_memories_by_keywords",
|
|
7
8
|
others: "search_others_memories",
|
|
8
9
|
report: "echomem_usage_report",
|
|
10
|
+
contextHealth: "echo_context_health",
|
|
11
|
+
recompose: "echo_recompose",
|
|
9
12
|
delete: "delete_memory",
|
|
13
|
+
getByContext: "get_memories_by_context",
|
|
10
14
|
};
|
|
11
15
|
export const legacyAliasToCanonical = {
|
|
12
16
|
search_memories_by_description_semantic: canonicalToolNames.search,
|
|
@@ -35,6 +39,7 @@ export const saveConversationSchema = z.object({
|
|
|
35
39
|
url: z.string().optional(),
|
|
36
40
|
source: z.string().optional(),
|
|
37
41
|
tags: z.array(z.string()).optional(),
|
|
42
|
+
passthrough: z.boolean().optional(),
|
|
38
43
|
messages: z
|
|
39
44
|
.array(z.object({
|
|
40
45
|
role: z.string(),
|
|
@@ -62,6 +67,11 @@ export const deleteMemorySchema = z.object({
|
|
|
62
67
|
confirmed: z.boolean().optional().default(false),
|
|
63
68
|
confirmationToken: z.string().optional(),
|
|
64
69
|
});
|
|
70
|
+
export const getByContextSchema = z.object({
|
|
71
|
+
...triggerMetadataSchema,
|
|
72
|
+
contextId: z.string().min(1),
|
|
73
|
+
limit: z.number().optional().default(50),
|
|
74
|
+
});
|
|
65
75
|
export function listToolSpecs(opts = {}) {
|
|
66
76
|
const currentTime = new Date().toISOString();
|
|
67
77
|
const map = opts.map?.trim();
|
|
@@ -71,7 +81,7 @@ export function listToolSpecs(opts = {}) {
|
|
|
71
81
|
return [
|
|
72
82
|
{
|
|
73
83
|
name: canonicalToolNames.search,
|
|
74
|
-
description: `Recall the user's prior decisions, preferences, constraints, and project context from EchoMem — their long-term memory across ALL their AI tools (Claude.ai, ChatGPT, other agents), not just this session.${mapSection}\nCall this when the task plausibly relates to that remembered context — a topic above, or when the user refers to past work ("what did we decide", "like before", "the usual") — so you don't re-derive or re-ask what they already settled. Skip it for self-contained tasks with no link to their history (e.g. a generic algorithm question). By default this returns only the ranked memories retrieved for recall and skips EchoMem answer generation; set includeAnswer=true only when you explicitly need EchoMem's legacy synthesized recall. Current time: ${currentTime}
|
|
84
|
+
description: withMcpVersion(`Recall the user's prior decisions, preferences, constraints, and project context from EchoMem — their long-term memory across ALL their AI tools (Claude.ai, ChatGPT, other agents), not just this session.${mapSection}\nCall this when the task plausibly relates to that remembered context — a topic above, or when the user refers to past work ("what did we decide", "like before", "the usual") — so you don't re-derive or re-ask what they already settled. Skip it for self-contained tasks with no link to their history (e.g. a generic algorithm question). By default this returns only the ranked memories retrieved for recall and skips EchoMem answer generation; set includeAnswer=true only when you explicitly need EchoMem's legacy synthesized recall. Current time: ${currentTime}.`),
|
|
75
85
|
inputSchema: {
|
|
76
86
|
type: "object",
|
|
77
87
|
properties: {
|
|
@@ -117,7 +127,7 @@ export function listToolSpecs(opts = {}) {
|
|
|
117
127
|
},
|
|
118
128
|
{
|
|
119
129
|
name: canonicalToolNames.save,
|
|
120
|
-
description: "Save conversation into EchoMem for future retrieval.",
|
|
130
|
+
description: "Save conversation into EchoMem for future retrieval. Set passthrough=true to store the text verbatim as a session capsule (no LLM extraction, no embeddings) — use this for warm-up capsules that a fresh session will reload via get_memories_by_context.",
|
|
121
131
|
inputSchema: {
|
|
122
132
|
type: "object",
|
|
123
133
|
properties: {
|
|
@@ -126,6 +136,10 @@ export function listToolSpecs(opts = {}) {
|
|
|
126
136
|
url: { type: "string" },
|
|
127
137
|
source: { type: "string" },
|
|
128
138
|
tags: { type: "array", items: { type: "string" } },
|
|
139
|
+
passthrough: {
|
|
140
|
+
type: "boolean",
|
|
141
|
+
description: "When true, store the conversation text verbatim as a session capsule — no LLM extraction, no embeddings. Use for warm-up capsules.",
|
|
142
|
+
},
|
|
129
143
|
messages: {
|
|
130
144
|
type: "array",
|
|
131
145
|
items: {
|
|
@@ -218,11 +232,56 @@ export function listToolSpecs(opts = {}) {
|
|
|
218
232
|
required: ["memoryId"],
|
|
219
233
|
},
|
|
220
234
|
},
|
|
235
|
+
{
|
|
236
|
+
name: canonicalToolNames.getByContext,
|
|
237
|
+
description: withMcpVersion(`Deterministically re-fetch the exact batch of memories saved under one contextId — no semantic search, no ranking, just that session's saved capsule. save_conversation returns a contextId; pass it here to pull back precisely those memories, e.g. to warm up a fresh session with what a prior session saved, or to verify the saved facts are still present. Current time: ${currentTime}.`),
|
|
238
|
+
inputSchema: {
|
|
239
|
+
type: "object",
|
|
240
|
+
properties: {
|
|
241
|
+
contextId: { type: "string", description: "The contextId returned by save_conversation." },
|
|
242
|
+
limit: { type: "number", default: 50 },
|
|
243
|
+
triggerMessage: {
|
|
244
|
+
type: "string",
|
|
245
|
+
description: "Optional: the user's message that caused this lookup. EchoMem stores only a redacted analytics preview and hash.",
|
|
246
|
+
},
|
|
247
|
+
triggerMessageRole: { type: "string", default: "user" },
|
|
248
|
+
},
|
|
249
|
+
required: ["contextId"],
|
|
250
|
+
},
|
|
251
|
+
},
|
|
221
252
|
{
|
|
222
253
|
name: canonicalToolNames.report,
|
|
223
254
|
description: "Show the user a one-screen audit of THEIR OWN AI coding usage — computed locally from their Codex/Claude Code logs ($0, nothing uploaded): how many tokens their agents spent, how much was re-reading context, reads vs memory recalls, and what changes with EchoMem. Call this when the user asks about their usage, token spend, cost, how much they're wasting, or wants a summary of their agent activity — and you may offer it once right after EchoMem is first connected. Returns formatted text to show the user verbatim. Needs no login.",
|
|
224
255
|
inputSchema: { type: "object", properties: {} },
|
|
225
256
|
},
|
|
257
|
+
{
|
|
258
|
+
name: canonicalToolNames.contextHealth,
|
|
259
|
+
description: "Show the current local Codex/Claude context-health score as markdown: clean percentage, tracked lower-bound dead-weight, redundant reads, source client, and token-count source. Use when the user asks about the context HUD, dirty context, context pollution, whether cleanup is worth it, or wants an in-chat fallback to the passive HUD. This reads local agent logs only and needs no login.",
|
|
260
|
+
inputSchema: {
|
|
261
|
+
type: "object",
|
|
262
|
+
properties: {
|
|
263
|
+
client: {
|
|
264
|
+
type: "string",
|
|
265
|
+
enum: ["codex", "claude-code", "claude-desktop", "auto"],
|
|
266
|
+
default: "auto",
|
|
267
|
+
},
|
|
268
|
+
},
|
|
269
|
+
},
|
|
270
|
+
},
|
|
271
|
+
{
|
|
272
|
+
name: canonicalToolNames.recompose,
|
|
273
|
+
description: "Capture a clean-start capsule of the CURRENT local Codex/Claude session — its goal, the files in play, the most recent instruction, and where things stand — so the user can start a fresh session without re-paying orientation or letting the window auto-compact. Use when echo_context_health shows heavy/dirty context (high saturation or pollution), when the agent starts drifting or repeating, or when the user asks how to clean up or start fresh. Returns inspectable markdown to show the user; this is a clean recompose, not a provider compaction. Reads local agent logs only and needs no login.",
|
|
274
|
+
inputSchema: {
|
|
275
|
+
type: "object",
|
|
276
|
+
properties: {
|
|
277
|
+
client: {
|
|
278
|
+
type: "string",
|
|
279
|
+
enum: ["codex", "claude-code", "claude-desktop", "auto"],
|
|
280
|
+
default: "auto",
|
|
281
|
+
},
|
|
282
|
+
},
|
|
283
|
+
},
|
|
284
|
+
},
|
|
226
285
|
{
|
|
227
286
|
name: "search_memories_by_time_range",
|
|
228
287
|
description: "Legacy alias for get_memories_by_time_range.",
|
package/package.json
CHANGED
|
@@ -1,14 +1,17 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@echomem/mcp",
|
|
3
|
-
"version": "1.4.
|
|
3
|
+
"version": "1.4.2",
|
|
4
4
|
"description": "EchoMem Cloud-First MCP Server",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"bin": {
|
|
8
|
-
"
|
|
8
|
+
"mcp": "./dist/index.js",
|
|
9
|
+
"echomem-mcp": "./dist/index.js",
|
|
10
|
+
"echomem-hud": "./dist/hud/cli.js"
|
|
9
11
|
},
|
|
10
12
|
"files": [
|
|
11
13
|
"dist",
|
|
14
|
+
"assets",
|
|
12
15
|
"templates",
|
|
13
16
|
"README.md"
|
|
14
17
|
],
|
|
@@ -17,17 +20,18 @@
|
|
|
17
20
|
"start": "node dist/index.js",
|
|
18
21
|
"dev": "tsx src/index.ts",
|
|
19
22
|
"smoke": "node smoke.mjs",
|
|
20
|
-
"test": "npm run build && node test/crypto.test.mjs && node test/integration.test.mjs && node test/no-restart.test.mjs && node test/report.test.mjs && node test/tools.test.mjs && node test/delete.test.mjs && node test/migrate.test.mjs",
|
|
23
|
+
"test": "npm run build && node test/crypto.test.mjs && node test/integration.test.mjs && node test/no-restart.test.mjs && node test/report.test.mjs && node test/tools.test.mjs && node test/delete.test.mjs && node test/migrate.test.mjs && node test/hud.test.mjs",
|
|
21
24
|
"prepack": "npm run build && node scripts/bundle-city.mjs"
|
|
22
25
|
},
|
|
23
26
|
"dependencies": {
|
|
24
27
|
"@modelcontextprotocol/sdk": "^1.0.1",
|
|
25
|
-
"
|
|
26
|
-
"
|
|
28
|
+
"axios": "^1.6.8",
|
|
29
|
+
"electron": "41.7.1",
|
|
30
|
+
"zod": "^3.22.4"
|
|
27
31
|
},
|
|
28
32
|
"devDependencies": {
|
|
29
|
-
"
|
|
30
|
-
"tsx": "^4.
|
|
31
|
-
"
|
|
33
|
+
"@types/node": "^20.11.0",
|
|
34
|
+
"tsx": "^4.22.4",
|
|
35
|
+
"typescript": "^5.3.3"
|
|
32
36
|
}
|
|
33
37
|
}
|