@echomem/mcp 1.4.39 → 1.4.41
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 +21 -0
- package/dist/hud/cli.js +25 -4
- package/dist/hud/hooks.js +58 -0
- package/dist/index.js +328 -244
- package/dist/package-metadata.js +2 -1
- package/dist/setup-page/client-extraction.js +9 -3
- package/dist/setup-page/styles-mvp.js +46 -0
- package/dist/setup.js +35 -16
- package/dist/source-session-hook.js +103 -0
- package/dist/source-session.js +261 -0
- package/dist/v1-contract.js +22 -0
- package/package.json +2 -2
- package/templates/echomem-recall.md +3 -0
- package/dist/forensics-10-problems.js +0 -633
package/dist/index.js
CHANGED
|
@@ -4,18 +4,20 @@ 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, completeGroupPublicationSchema, createGroupInviteSchema, createGroupSchema, deleteMemorySchema, flagPublicationAttentionSchema, getGroupSessionSharingSchema, getByContextSchema, groupContextSchema, joinGroupSchema, keywordsSchema, listFriendsSchema, listToolSpecs, othersSchema, publishBatchToGroupSchema, publishToGroupSchema, prepareGroupPublicationSchema, publicMemorySchema, recordMemoryCitationsSchema, requestGroupSessionSharingSchema, resolveCanonicalToolName, saveConversationSchema, searchMemoriesSchema, searchUsersSchema, sendFriendRequestSchema, timeRangeSchema, updateGroupProfileSchema, setGroupSessionSharingSchema, } from "./v1-contract.js";
|
|
7
|
+
import { bindSourceSessionSchema, canonicalToolNames, completeGroupPublicationSchema, createGroupInviteSchema, createGroupSchema, deleteMemorySchema, flagPublicationAttentionSchema, getGroupSessionSharingSchema, getByContextSchema, groupContextSchema, joinGroupSchema, keywordsSchema, listFriendsSchema, listToolSpecs, othersSchema, publishBatchToGroupSchema, publishToGroupSchema, prepareGroupPublicationSchema, publicMemorySchema, recordMemoryCitationsSchema, requestGroupSessionSharingSchema, resolveCanonicalToolName, saveConversationSchema, searchMemoriesSchema, searchUsersSchema, sendFriendRequestSchema, timeRangeSchema, updateGroupProfileSchema, setGroupSessionSharingSchema, } 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
11
|
import { contextHealthMarkdown, recomposeCapsuleMarkdown } from "./hud/api.js";
|
|
12
12
|
import { createHash, randomUUID } from "node:crypto";
|
|
13
|
+
import { AsyncLocalStorage } from "node:async_hooks";
|
|
13
14
|
import { fetchEncryptionConfig, decryptMemoryFields, verifyKeyB64, } from "./encryption.js";
|
|
14
15
|
import { runCli } from "./setup.js";
|
|
15
16
|
import { MCP_PACKAGE_VERSION, MCP_SERVER_INSTRUCTIONS, MEMORY_CITATION_INSTRUCTION, SAVED_MEMORY_RECEIPT_INSTRUCTION, } from "./package-metadata.js";
|
|
16
17
|
import { clearBillingAlert, writeBillingAlert } from "./billing-alert.js";
|
|
17
18
|
import { checkLatestUpdateStatus, formatUpdateNotice, formatUpdateStatusText, startBackgroundUpdateCheck, } from "./update-check.js";
|
|
18
19
|
import { autoUpdateHeadlessRuntime } from "./headless-runtime.js";
|
|
20
|
+
import { resolveSourceSessionFromMcpContext, resolveSourceSessionFromBindingToken, } from "./source-session.js";
|
|
19
21
|
const ECHO_API_BASE_URL = process.env.ECHO_API_BASE_URL || "https://echo-mem-chrome.vercel.app";
|
|
20
22
|
const ECHO_PRICING_URL = process.env.ECHO_PRICING_URL || "https://echoknows.com/account";
|
|
21
23
|
const ECHO_MEMORY_WEB_URL = (process.env.ECHO_MEMORY_WEB_URL || "https://echoknows.com/memory").replace(/\/$/, "");
|
|
@@ -611,6 +613,9 @@ function toolEventName(canonicalName, status) {
|
|
|
611
613
|
class EchoMemApiClient {
|
|
612
614
|
store;
|
|
613
615
|
axios;
|
|
616
|
+
boundSourceSession = null;
|
|
617
|
+
requestSourceSession = new AsyncLocalStorage();
|
|
618
|
+
sourceSessionsByCanonicalKey = new Map();
|
|
614
619
|
whoamiCache = null;
|
|
615
620
|
/** One id per bridge process — groups all saves from this coding session under a single EchoMem context. */
|
|
616
621
|
sessionId = randomUUID();
|
|
@@ -630,6 +635,10 @@ class EchoMemApiClient {
|
|
|
630
635
|
const token = this.store.getToken();
|
|
631
636
|
if (token)
|
|
632
637
|
config.headers.set("Authorization", `Bearer ${token}`);
|
|
638
|
+
const sourceSession = this.getBoundSourceSession();
|
|
639
|
+
if (sourceSession) {
|
|
640
|
+
config.headers.set("X-EchoMem-Context-Id", sourceSession.contextId);
|
|
641
|
+
}
|
|
633
642
|
return config;
|
|
634
643
|
});
|
|
635
644
|
}
|
|
@@ -641,6 +650,43 @@ class EchoMemApiClient {
|
|
|
641
650
|
getSessionId() {
|
|
642
651
|
return this.sessionId;
|
|
643
652
|
}
|
|
653
|
+
getBoundSourceSession() {
|
|
654
|
+
return this.requestSourceSession.getStore() ?? this.boundSourceSession;
|
|
655
|
+
}
|
|
656
|
+
async withSourceSession(sourceSession, operation) {
|
|
657
|
+
return sourceSession ? this.requestSourceSession.run(sourceSession, operation) : operation();
|
|
658
|
+
}
|
|
659
|
+
async bindSourceSession(verified, persistForBridge = true) {
|
|
660
|
+
const cached = this.sourceSessionsByCanonicalKey.get(verified.canonicalKey);
|
|
661
|
+
if (cached) {
|
|
662
|
+
if (persistForBridge)
|
|
663
|
+
this.boundSourceSession = cached;
|
|
664
|
+
return { ...cached, created: false };
|
|
665
|
+
}
|
|
666
|
+
const response = await this.axios.post("/api/extension/source-sessions/bind", {
|
|
667
|
+
provider: verified.provider,
|
|
668
|
+
providerSessionId: verified.providerSessionId,
|
|
669
|
+
evidence: verified.evidence,
|
|
670
|
+
});
|
|
671
|
+
const contextId = readString(response.data, "contextId");
|
|
672
|
+
const canonicalKey = readString(response.data, "canonicalKey");
|
|
673
|
+
if (response.data?.success !== true || !contextId || canonicalKey !== verified.canonicalKey) {
|
|
674
|
+
throw new Error("EchoMem returned an invalid source-session binding receipt");
|
|
675
|
+
}
|
|
676
|
+
const bound = { ...verified, contextId };
|
|
677
|
+
this.sourceSessionsByCanonicalKey.set(verified.canonicalKey, bound);
|
|
678
|
+
if (this.sourceSessionsByCanonicalKey.size > 64) {
|
|
679
|
+
const oldestKey = this.sourceSessionsByCanonicalKey.keys().next().value;
|
|
680
|
+
if (oldestKey)
|
|
681
|
+
this.sourceSessionsByCanonicalKey.delete(oldestKey);
|
|
682
|
+
}
|
|
683
|
+
if (persistForBridge)
|
|
684
|
+
this.boundSourceSession = bound;
|
|
685
|
+
return {
|
|
686
|
+
...bound,
|
|
687
|
+
created: response.data?.created === true,
|
|
688
|
+
};
|
|
689
|
+
}
|
|
644
690
|
async trackMcpAnalyticsEvent(eventType, eventProperties, insertId) {
|
|
645
691
|
if (!this.hasToken())
|
|
646
692
|
return;
|
|
@@ -900,9 +946,10 @@ class EchoMemApiClient {
|
|
|
900
946
|
sourceUrl: parsed.url,
|
|
901
947
|
source: parsed.source || "mcp_server",
|
|
902
948
|
title: parsed.title,
|
|
903
|
-
//
|
|
904
|
-
//
|
|
905
|
-
conversationKey: groupSharingScopeId,
|
|
949
|
+
// A verified source session owns the memory context. The independent opaque scope still
|
|
950
|
+
// governs group-sharing consent and never becomes the source-session identity.
|
|
951
|
+
conversationKey: this.getBoundSourceSession()?.canonicalKey ?? groupSharingScopeId,
|
|
952
|
+
groupSharingScopeId,
|
|
906
953
|
passthrough: parsed.passthrough || false,
|
|
907
954
|
triggerMessage: parsed.triggerMessage ||
|
|
908
955
|
lastUserMessageFromMessages(parsed.messages) ||
|
|
@@ -1297,7 +1344,7 @@ class EchoMemMCPServer {
|
|
|
1297
1344
|
const updateNotice = DESKTOP_MANAGED ? undefined : formatUpdateNotice(this.updateStatus);
|
|
1298
1345
|
return { tools: listToolSpecs({ map, groupMap, updateNotice }) };
|
|
1299
1346
|
});
|
|
1300
|
-
this.server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
1347
|
+
this.server.setRequestHandler(CallToolRequestSchema, async (request, extra) => {
|
|
1301
1348
|
const resolvedCanonicalName = resolveCanonicalToolName(request.params.name);
|
|
1302
1349
|
const recallRoute = routePersonalRecallInvocation(resolvedCanonicalName, request.params.arguments);
|
|
1303
1350
|
const canonicalName = recallRoute.canonicalName;
|
|
@@ -1307,258 +1354,277 @@ class EchoMemMCPServer {
|
|
|
1307
1354
|
const clientVersion = this.server.getClientVersion();
|
|
1308
1355
|
this.mcpClientName = clientVersion?.name ?? this.mcpClientName;
|
|
1309
1356
|
this.mcpClientVersion = clientVersion?.version ?? this.mcpClientVersion;
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
surface: "mcp",
|
|
1313
|
-
event_family: "mcp",
|
|
1314
|
-
integration: "echomem_mcp",
|
|
1315
|
-
telemetry_source: "local_bridge",
|
|
1316
|
-
...this.getMcpClientAnalytics(),
|
|
1317
|
-
codex_session_id: this.client.getSessionId(),
|
|
1318
|
-
conversation_id: this.client.getSessionId(),
|
|
1319
|
-
tool_name: request.params.name,
|
|
1320
|
-
canonical_tool_name: canonicalName,
|
|
1321
|
-
...triggerAnalyticsForTool(canonicalName, toolArgs),
|
|
1322
|
-
...inputAnalyticsForTool(canonicalName, toolArgs),
|
|
1323
|
-
};
|
|
1324
|
-
const analyticsCallId = randomUUID();
|
|
1325
|
-
// One event per call. Handlers enrich `rec` with tool-specific detail; we finalize + log in `finally`.
|
|
1326
|
-
const rec = {
|
|
1327
|
-
type: "tool_call",
|
|
1328
|
-
tool: canonicalName,
|
|
1329
|
-
map_injected: this.mapInjected,
|
|
1330
|
-
group_map_injected: this.groupMapInjected,
|
|
1331
|
-
};
|
|
1357
|
+
let requestSourceSession = this.client.getBoundSourceSession();
|
|
1358
|
+
let inferred = null;
|
|
1332
1359
|
try {
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
//
|
|
1337
|
-
|
|
1338
|
-
|
|
1360
|
+
inferred = resolveSourceSessionFromMcpContext(extra._meta, { hostPlatform: this.getMcpClientAnalytics().host_platform });
|
|
1361
|
+
}
|
|
1362
|
+
catch {
|
|
1363
|
+
// Malformed host metadata is not model input. Ignore it and retain the explicit fallback.
|
|
1364
|
+
}
|
|
1365
|
+
// Verified request metadata always wins over a bridge-level compatibility fallback. The
|
|
1366
|
+
// startup hook performs the eager write; this path attaches the exact context to the current
|
|
1367
|
+
// request and retries the backend write if startup raced login.
|
|
1368
|
+
if (inferred && this.client.hasToken()) {
|
|
1369
|
+
requestSourceSession = await this.client.bindSourceSession(inferred, false);
|
|
1370
|
+
}
|
|
1371
|
+
return this.client.withSourceSession(requestSourceSession, async () => {
|
|
1372
|
+
const t0 = Date.now();
|
|
1373
|
+
const analyticsBase = {
|
|
1374
|
+
surface: "mcp",
|
|
1375
|
+
event_family: "mcp",
|
|
1376
|
+
integration: "echomem_mcp",
|
|
1377
|
+
telemetry_source: "local_bridge",
|
|
1378
|
+
...this.getMcpClientAnalytics(),
|
|
1379
|
+
codex_session_id: this.client.getBoundSourceSession()?.canonicalKey ?? this.client.getSessionId(),
|
|
1380
|
+
conversation_id: this.client.getBoundSourceSession()?.canonicalKey ?? this.client.getSessionId(),
|
|
1381
|
+
context_id: this.client.getBoundSourceSession()?.contextId,
|
|
1382
|
+
tool_name: request.params.name,
|
|
1383
|
+
canonical_tool_name: canonicalName,
|
|
1384
|
+
...triggerAnalyticsForTool(canonicalName, toolArgs),
|
|
1385
|
+
...inputAnalyticsForTool(canonicalName, toolArgs),
|
|
1386
|
+
};
|
|
1387
|
+
const analyticsCallId = randomUUID();
|
|
1388
|
+
// One event per call. Handlers enrich `rec` with tool-specific detail; we finalize + log in `finally`.
|
|
1389
|
+
const rec = {
|
|
1390
|
+
type: "tool_call",
|
|
1391
|
+
tool: canonicalName,
|
|
1392
|
+
map_injected: this.mapInjected,
|
|
1393
|
+
group_map_injected: this.groupMapInjected,
|
|
1394
|
+
};
|
|
1395
|
+
try {
|
|
1396
|
+
if (recallRoute.error) {
|
|
1397
|
+
throw new McpError(ErrorCode.InvalidParams, recallRoute.error);
|
|
1398
|
+
}
|
|
1399
|
+
// The usage report is a local, $0 audit — works with no login (value before signup).
|
|
1400
|
+
if (canonicalName === canonicalToolNames.report) {
|
|
1401
|
+
return { content: [{ type: "text", text: await buildReportText(false) }] };
|
|
1402
|
+
}
|
|
1403
|
+
if (canonicalName === canonicalToolNames.updateStatus) {
|
|
1404
|
+
if (DESKTOP_MANAGED) {
|
|
1405
|
+
return {
|
|
1406
|
+
content: [{
|
|
1407
|
+
type: "text",
|
|
1408
|
+
text: `Echo Desktop manages this MCP runtime (${MCP_PACKAGE_VERSION}). Install app updates from Echo Desktop, then start a new agent session.`,
|
|
1409
|
+
}],
|
|
1410
|
+
};
|
|
1411
|
+
}
|
|
1412
|
+
const force = isRecord(request.params.arguments) && request.params.arguments.force === true;
|
|
1413
|
+
const status = await checkLatestUpdateStatus({ force });
|
|
1414
|
+
this.handleUpdateStatus(status);
|
|
1415
|
+
return { content: [{ type: "text", text: formatUpdateStatusText(this.updateStatus ?? status) }] };
|
|
1416
|
+
}
|
|
1417
|
+
if (canonicalName === canonicalToolNames.contextHealth) {
|
|
1418
|
+
const client = isRecord(request.params.arguments) && typeof request.params.arguments.client === "string"
|
|
1419
|
+
? request.params.arguments.client
|
|
1420
|
+
: "auto";
|
|
1421
|
+
const mode = client === "codex" || client === "claude-code" || client === "claude-desktop" || client === "auto"
|
|
1422
|
+
? client
|
|
1423
|
+
: "auto";
|
|
1424
|
+
return { content: [{ type: "text", text: await contextHealthMarkdown(mode) }] };
|
|
1425
|
+
}
|
|
1426
|
+
if (canonicalName === canonicalToolNames.recompose) {
|
|
1427
|
+
const client = isRecord(request.params.arguments) && typeof request.params.arguments.client === "string"
|
|
1428
|
+
? request.params.arguments.client
|
|
1429
|
+
: "auto";
|
|
1430
|
+
const mode = client === "codex" || client === "claude-code" || client === "claude-desktop" || client === "auto"
|
|
1431
|
+
? client
|
|
1432
|
+
: "auto";
|
|
1433
|
+
const capsuleText = await recomposeCapsuleMarkdown(mode);
|
|
1434
|
+
// If logged in, persist the capsule via passthrough so it's retrievable by contextId.
|
|
1435
|
+
if (this.client.hasToken()) {
|
|
1436
|
+
try {
|
|
1437
|
+
const saveResult = await this.client.saveConversation({
|
|
1438
|
+
conversation: capsuleText,
|
|
1439
|
+
title: "Session capsule (recompose)",
|
|
1440
|
+
source: "mcp_recompose",
|
|
1441
|
+
passthrough: true,
|
|
1442
|
+
});
|
|
1443
|
+
const ctxId = saveResult?.contextId;
|
|
1444
|
+
const capId = saveResult?.capsuleId;
|
|
1445
|
+
const persistLine = ctxId
|
|
1446
|
+
? `\n\n---\nCapsule persisted${capId ? ` (${capId})` : ""}. To reload in a fresh session:\nget_memories_by_context({ contextId: "${ctxId}" })`
|
|
1447
|
+
: "";
|
|
1448
|
+
return { content: [{ type: "text", text: capsuleText + persistLine }] };
|
|
1449
|
+
}
|
|
1450
|
+
catch {
|
|
1451
|
+
// Persistence is best-effort; return the capsule text regardless.
|
|
1452
|
+
}
|
|
1453
|
+
}
|
|
1454
|
+
return { content: [{ type: "text", text: capsuleText }] };
|
|
1455
|
+
}
|
|
1456
|
+
if (!this.client.hasToken())
|
|
1457
|
+
throw new NoTokenError();
|
|
1458
|
+
if (canonicalName !== canonicalToolNames.save) {
|
|
1459
|
+
void this.client.trackMcpAnalyticsEvent("[MCP] EchoMem Tool Called", analyticsBase, `${analyticsCallId}:generic-called`);
|
|
1460
|
+
void this.client.trackMcpAnalyticsEvent(toolEventName(canonicalName, "Called"), analyticsBase, `${analyticsCallId}:called`);
|
|
1461
|
+
}
|
|
1462
|
+
if (canonicalName !== canonicalToolNames.save && analyticsBase.trigger_message_available === true) {
|
|
1463
|
+
void this.client.trackMcpAnalyticsEvent("[MCP] EchoMem Triggered By User Turn", analyticsBase);
|
|
1464
|
+
}
|
|
1465
|
+
switch (canonicalName) {
|
|
1466
|
+
case canonicalToolNames.bindSourceSession:
|
|
1467
|
+
return await this.handleBindSourceSession(request.params.arguments);
|
|
1468
|
+
case canonicalToolNames.search:
|
|
1469
|
+
return await this.handleSearch(toolArgs, rec);
|
|
1470
|
+
case canonicalToolNames.save:
|
|
1471
|
+
return await this.handleSave(request.params.arguments, rec);
|
|
1472
|
+
case canonicalToolNames.timeRange:
|
|
1473
|
+
return await this.handleTimeRange(toolArgs);
|
|
1474
|
+
case canonicalToolNames.getByContext:
|
|
1475
|
+
return await this.handleGetByContext(request.params.arguments);
|
|
1476
|
+
case canonicalToolNames.checkpointByContext:
|
|
1477
|
+
return await this.handleGetCheckpointByContext(request.params.arguments);
|
|
1478
|
+
case canonicalToolNames.keywords:
|
|
1479
|
+
return await this.handleKeywords(toolArgs);
|
|
1480
|
+
case canonicalToolNames.friends:
|
|
1481
|
+
return await this.handleFriends(request.params.arguments);
|
|
1482
|
+
case canonicalToolNames.searchUsers:
|
|
1483
|
+
return await this.handleSearchUsers(request.params.arguments);
|
|
1484
|
+
case canonicalToolNames.sendFriendRequest:
|
|
1485
|
+
return await this.handleSendFriendRequest(request.params.arguments);
|
|
1486
|
+
case canonicalToolNames.others:
|
|
1487
|
+
return await this.handleOthers(toolArgs);
|
|
1488
|
+
case canonicalToolNames.publicMemory:
|
|
1489
|
+
return await this.handlePublicMemory(request.params.arguments);
|
|
1490
|
+
case canonicalToolNames.recordCitations:
|
|
1491
|
+
return await this.handleRecordMemoryCitations(request.params.arguments);
|
|
1492
|
+
case canonicalToolNames.groupContext:
|
|
1493
|
+
return await this.handleGroupContext(request.params.arguments);
|
|
1494
|
+
case canonicalToolNames.getGroupSessionSharing:
|
|
1495
|
+
return await this.handleGetGroupSessionSharing(request.params.arguments);
|
|
1496
|
+
case canonicalToolNames.requestGroupSessionSharing:
|
|
1497
|
+
return await this.handleRequestGroupSessionSharing(request.params.arguments);
|
|
1498
|
+
case canonicalToolNames.setGroupSessionSharing:
|
|
1499
|
+
return await this.handleSetGroupSessionSharing(request.params.arguments);
|
|
1500
|
+
case canonicalToolNames.createGroup:
|
|
1501
|
+
return await this.handleCreateGroup(request.params.arguments);
|
|
1502
|
+
case canonicalToolNames.createGroupInvite:
|
|
1503
|
+
return await this.handleCreateGroupInvite(request.params.arguments);
|
|
1504
|
+
case canonicalToolNames.joinGroup:
|
|
1505
|
+
return await this.handleJoinGroup(request.params.arguments);
|
|
1506
|
+
case canonicalToolNames.prepareGroupPublication:
|
|
1507
|
+
return await this.handlePrepareGroupPublication(request.params.arguments);
|
|
1508
|
+
case canonicalToolNames.flagPublicationAttention:
|
|
1509
|
+
return await this.handleFlagPublicationAttention(request.params.arguments);
|
|
1510
|
+
case canonicalToolNames.updateGroupProfile:
|
|
1511
|
+
return await this.handleUpdateGroupProfile(request.params.arguments);
|
|
1512
|
+
case canonicalToolNames.completeGroupPublication:
|
|
1513
|
+
return await this.handleCompleteGroupPublication(request.params.arguments);
|
|
1514
|
+
case canonicalToolNames.publishToGroup:
|
|
1515
|
+
return await this.handlePublishToGroup(request.params.arguments);
|
|
1516
|
+
case canonicalToolNames.publishBatchToGroup:
|
|
1517
|
+
return await this.handlePublishBatchToGroup(request.params.arguments);
|
|
1518
|
+
case canonicalToolNames.delete:
|
|
1519
|
+
return await this.handleDelete(request.params.arguments, rec);
|
|
1520
|
+
default:
|
|
1521
|
+
throw new McpError(ErrorCode.MethodNotFound, `Unknown tool: ${request.params.name}`);
|
|
1522
|
+
}
|
|
1339
1523
|
}
|
|
1340
|
-
|
|
1341
|
-
|
|
1524
|
+
catch (error) {
|
|
1525
|
+
rec.error_kind = classifyError(error);
|
|
1526
|
+
if (error instanceof NoTokenError) {
|
|
1527
|
+
// Not isError: a normal "not connected yet" state. Once `login` runs, the next call works.
|
|
1528
|
+
return {
|
|
1529
|
+
content: [
|
|
1530
|
+
{
|
|
1531
|
+
type: "text",
|
|
1532
|
+
text: `🔌 EchoMem isn't connected yet. ${CONNECT_DEVICE_INSTRUCTION} No editor restart is needed.`,
|
|
1533
|
+
},
|
|
1534
|
+
],
|
|
1535
|
+
};
|
|
1536
|
+
}
|
|
1537
|
+
if (error instanceof LockedError) {
|
|
1538
|
+
// Not isError: this is a normal "needs unlock" state the model should relay to the user.
|
|
1539
|
+
return {
|
|
1540
|
+
content: [
|
|
1541
|
+
{
|
|
1542
|
+
type: "text",
|
|
1543
|
+
text: [
|
|
1544
|
+
"🔒 EchoMem vault is locked.",
|
|
1545
|
+
"This encrypted account has no usable local decryption key. Once unlocked, this trusted device stays unlocked until you explicitly lock it or log out.",
|
|
1546
|
+
`Action required from the user: ${UNLOCK_VAULT_INSTRUCTION}`,
|
|
1547
|
+
...(DESKTOP_MANAGED ? [] : [
|
|
1548
|
+
"At `Vault passphrase (typing is hidden):`, type the passphrase and press Return. No characters will appear while you type; that is expected.",
|
|
1549
|
+
]),
|
|
1550
|
+
"After the success message, retry this EchoMem action in the current session — no editor restart is needed.",
|
|
1551
|
+
].join("\n"),
|
|
1552
|
+
},
|
|
1553
|
+
],
|
|
1554
|
+
};
|
|
1555
|
+
}
|
|
1556
|
+
if (error instanceof EncryptionStatusUnavailableError) {
|
|
1342
1557
|
return {
|
|
1343
1558
|
content: [{
|
|
1344
1559
|
type: "text",
|
|
1345
|
-
text:
|
|
1560
|
+
text: [
|
|
1561
|
+
"⚠️ EchoMem could not verify whether this account's vault is encrypted.",
|
|
1562
|
+
"No memory data was returned. Retry this action after the service recovers.",
|
|
1563
|
+
].join("\n"),
|
|
1346
1564
|
}],
|
|
1565
|
+
isError: true,
|
|
1347
1566
|
};
|
|
1348
1567
|
}
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
const
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
const mode = client === "codex" || client === "claude-code" || client === "claude-desktop" || client === "auto"
|
|
1368
|
-
? client
|
|
1369
|
-
: "auto";
|
|
1370
|
-
const capsuleText = await recomposeCapsuleMarkdown(mode);
|
|
1371
|
-
// If logged in, persist the capsule via passthrough so it's retrievable by contextId.
|
|
1372
|
-
if (this.client.hasToken()) {
|
|
1373
|
-
try {
|
|
1374
|
-
const saveResult = await this.client.saveConversation({
|
|
1375
|
-
conversation: capsuleText,
|
|
1376
|
-
title: "Session capsule (recompose)",
|
|
1377
|
-
source: "mcp_recompose",
|
|
1378
|
-
passthrough: true,
|
|
1379
|
-
});
|
|
1380
|
-
const ctxId = saveResult?.contextId;
|
|
1381
|
-
const capId = saveResult?.capsuleId;
|
|
1382
|
-
const persistLine = ctxId
|
|
1383
|
-
? `\n\n---\nCapsule persisted${capId ? ` (${capId})` : ""}. To reload in a fresh session:\nget_memories_by_context({ contextId: "${ctxId}" })`
|
|
1384
|
-
: "";
|
|
1385
|
-
return { content: [{ type: "text", text: capsuleText + persistLine }] };
|
|
1386
|
-
}
|
|
1387
|
-
catch {
|
|
1388
|
-
// Persistence is best-effort; return the capsule text regardless.
|
|
1389
|
-
}
|
|
1568
|
+
if (error instanceof ZodError) {
|
|
1569
|
+
throw new McpError(ErrorCode.InvalidParams, `Invalid arguments: ${error.message}`);
|
|
1570
|
+
}
|
|
1571
|
+
if (error instanceof McpError) {
|
|
1572
|
+
throw error;
|
|
1573
|
+
}
|
|
1574
|
+
const reconnectRequired = formatReconnectRequiredResult(error);
|
|
1575
|
+
if (reconnectRequired) {
|
|
1576
|
+
return {
|
|
1577
|
+
content: [{ type: "text", text: reconnectRequired }],
|
|
1578
|
+
isError: true,
|
|
1579
|
+
};
|
|
1580
|
+
}
|
|
1581
|
+
const upgradeRequired = formatUpgradeRequiredResult(error);
|
|
1582
|
+
if (upgradeRequired) {
|
|
1583
|
+
return {
|
|
1584
|
+
content: [{ type: "text", text: upgradeRequired }],
|
|
1585
|
+
};
|
|
1390
1586
|
}
|
|
1391
|
-
return { content: [{ type: "text", text: capsuleText }] };
|
|
1392
|
-
}
|
|
1393
|
-
if (!this.client.hasToken())
|
|
1394
|
-
throw new NoTokenError();
|
|
1395
|
-
if (canonicalName !== canonicalToolNames.save) {
|
|
1396
|
-
void this.client.trackMcpAnalyticsEvent("[MCP] EchoMem Tool Called", analyticsBase, `${analyticsCallId}:generic-called`);
|
|
1397
|
-
void this.client.trackMcpAnalyticsEvent(toolEventName(canonicalName, "Called"), analyticsBase, `${analyticsCallId}:called`);
|
|
1398
|
-
}
|
|
1399
|
-
if (canonicalName !== canonicalToolNames.save && analyticsBase.trigger_message_available === true) {
|
|
1400
|
-
void this.client.trackMcpAnalyticsEvent("[MCP] EchoMem Triggered By User Turn", analyticsBase);
|
|
1401
|
-
}
|
|
1402
|
-
switch (canonicalName) {
|
|
1403
|
-
case canonicalToolNames.search:
|
|
1404
|
-
return await this.handleSearch(toolArgs, rec);
|
|
1405
|
-
case canonicalToolNames.save:
|
|
1406
|
-
return await this.handleSave(request.params.arguments, rec);
|
|
1407
|
-
case canonicalToolNames.timeRange:
|
|
1408
|
-
return await this.handleTimeRange(toolArgs);
|
|
1409
|
-
case canonicalToolNames.getByContext:
|
|
1410
|
-
return await this.handleGetByContext(request.params.arguments);
|
|
1411
|
-
case canonicalToolNames.checkpointByContext:
|
|
1412
|
-
return await this.handleGetCheckpointByContext(request.params.arguments);
|
|
1413
|
-
case canonicalToolNames.keywords:
|
|
1414
|
-
return await this.handleKeywords(toolArgs);
|
|
1415
|
-
case canonicalToolNames.friends:
|
|
1416
|
-
return await this.handleFriends(request.params.arguments);
|
|
1417
|
-
case canonicalToolNames.searchUsers:
|
|
1418
|
-
return await this.handleSearchUsers(request.params.arguments);
|
|
1419
|
-
case canonicalToolNames.sendFriendRequest:
|
|
1420
|
-
return await this.handleSendFriendRequest(request.params.arguments);
|
|
1421
|
-
case canonicalToolNames.others:
|
|
1422
|
-
return await this.handleOthers(toolArgs);
|
|
1423
|
-
case canonicalToolNames.publicMemory:
|
|
1424
|
-
return await this.handlePublicMemory(request.params.arguments);
|
|
1425
|
-
case canonicalToolNames.recordCitations:
|
|
1426
|
-
return await this.handleRecordMemoryCitations(request.params.arguments);
|
|
1427
|
-
case canonicalToolNames.groupContext:
|
|
1428
|
-
return await this.handleGroupContext(request.params.arguments);
|
|
1429
|
-
case canonicalToolNames.getGroupSessionSharing:
|
|
1430
|
-
return await this.handleGetGroupSessionSharing(request.params.arguments);
|
|
1431
|
-
case canonicalToolNames.requestGroupSessionSharing:
|
|
1432
|
-
return await this.handleRequestGroupSessionSharing(request.params.arguments);
|
|
1433
|
-
case canonicalToolNames.setGroupSessionSharing:
|
|
1434
|
-
return await this.handleSetGroupSessionSharing(request.params.arguments);
|
|
1435
|
-
case canonicalToolNames.createGroup:
|
|
1436
|
-
return await this.handleCreateGroup(request.params.arguments);
|
|
1437
|
-
case canonicalToolNames.createGroupInvite:
|
|
1438
|
-
return await this.handleCreateGroupInvite(request.params.arguments);
|
|
1439
|
-
case canonicalToolNames.joinGroup:
|
|
1440
|
-
return await this.handleJoinGroup(request.params.arguments);
|
|
1441
|
-
case canonicalToolNames.prepareGroupPublication:
|
|
1442
|
-
return await this.handlePrepareGroupPublication(request.params.arguments);
|
|
1443
|
-
case canonicalToolNames.flagPublicationAttention:
|
|
1444
|
-
return await this.handleFlagPublicationAttention(request.params.arguments);
|
|
1445
|
-
case canonicalToolNames.updateGroupProfile:
|
|
1446
|
-
return await this.handleUpdateGroupProfile(request.params.arguments);
|
|
1447
|
-
case canonicalToolNames.completeGroupPublication:
|
|
1448
|
-
return await this.handleCompleteGroupPublication(request.params.arguments);
|
|
1449
|
-
case canonicalToolNames.publishToGroup:
|
|
1450
|
-
return await this.handlePublishToGroup(request.params.arguments);
|
|
1451
|
-
case canonicalToolNames.publishBatchToGroup:
|
|
1452
|
-
return await this.handlePublishBatchToGroup(request.params.arguments);
|
|
1453
|
-
case canonicalToolNames.delete:
|
|
1454
|
-
return await this.handleDelete(request.params.arguments, rec);
|
|
1455
|
-
default:
|
|
1456
|
-
throw new McpError(ErrorCode.MethodNotFound, `Unknown tool: ${request.params.name}`);
|
|
1457
|
-
}
|
|
1458
|
-
}
|
|
1459
|
-
catch (error) {
|
|
1460
|
-
rec.error_kind = classifyError(error);
|
|
1461
|
-
if (error instanceof NoTokenError) {
|
|
1462
|
-
// Not isError: a normal "not connected yet" state. Once `login` runs, the next call works.
|
|
1463
|
-
return {
|
|
1464
|
-
content: [
|
|
1465
|
-
{
|
|
1466
|
-
type: "text",
|
|
1467
|
-
text: `🔌 EchoMem isn't connected yet. ${CONNECT_DEVICE_INSTRUCTION} No editor restart is needed.`,
|
|
1468
|
-
},
|
|
1469
|
-
],
|
|
1470
|
-
};
|
|
1471
|
-
}
|
|
1472
|
-
if (error instanceof LockedError) {
|
|
1473
|
-
// Not isError: this is a normal "needs unlock" state the model should relay to the user.
|
|
1474
|
-
return {
|
|
1475
|
-
content: [
|
|
1476
|
-
{
|
|
1477
|
-
type: "text",
|
|
1478
|
-
text: [
|
|
1479
|
-
"🔒 EchoMem vault is locked.",
|
|
1480
|
-
"This encrypted account has no usable local decryption key. Once unlocked, this trusted device stays unlocked until you explicitly lock it or log out.",
|
|
1481
|
-
`Action required from the user: ${UNLOCK_VAULT_INSTRUCTION}`,
|
|
1482
|
-
...(DESKTOP_MANAGED ? [] : [
|
|
1483
|
-
"At `Vault passphrase (typing is hidden):`, type the passphrase and press Return. No characters will appear while you type; that is expected.",
|
|
1484
|
-
]),
|
|
1485
|
-
"After the success message, retry this EchoMem action in the current session — no editor restart is needed.",
|
|
1486
|
-
].join("\n"),
|
|
1487
|
-
},
|
|
1488
|
-
],
|
|
1489
|
-
};
|
|
1490
|
-
}
|
|
1491
|
-
if (error instanceof EncryptionStatusUnavailableError) {
|
|
1492
|
-
return {
|
|
1493
|
-
content: [{
|
|
1494
|
-
type: "text",
|
|
1495
|
-
text: [
|
|
1496
|
-
"⚠️ EchoMem could not verify whether this account's vault is encrypted.",
|
|
1497
|
-
"No memory data was returned. Retry this action after the service recovers.",
|
|
1498
|
-
].join("\n"),
|
|
1499
|
-
}],
|
|
1500
|
-
isError: true,
|
|
1501
|
-
};
|
|
1502
|
-
}
|
|
1503
|
-
if (error instanceof ZodError) {
|
|
1504
|
-
throw new McpError(ErrorCode.InvalidParams, `Invalid arguments: ${error.message}`);
|
|
1505
|
-
}
|
|
1506
|
-
if (error instanceof McpError) {
|
|
1507
|
-
throw error;
|
|
1508
|
-
}
|
|
1509
|
-
const reconnectRequired = formatReconnectRequiredResult(error);
|
|
1510
|
-
if (reconnectRequired) {
|
|
1511
1587
|
return {
|
|
1512
|
-
content: [{ type: "text", text:
|
|
1588
|
+
content: [{ type: "text", text: `Error: ${describeError(error)}` }],
|
|
1513
1589
|
isError: true,
|
|
1514
1590
|
};
|
|
1515
1591
|
}
|
|
1516
|
-
|
|
1517
|
-
|
|
1518
|
-
|
|
1519
|
-
|
|
1520
|
-
|
|
1521
|
-
|
|
1522
|
-
|
|
1523
|
-
|
|
1524
|
-
|
|
1525
|
-
|
|
1526
|
-
|
|
1527
|
-
|
|
1528
|
-
|
|
1529
|
-
|
|
1530
|
-
|
|
1531
|
-
|
|
1532
|
-
|
|
1533
|
-
|
|
1534
|
-
|
|
1535
|
-
|
|
1536
|
-
|
|
1537
|
-
|
|
1538
|
-
|
|
1539
|
-
|
|
1540
|
-
|
|
1541
|
-
|
|
1542
|
-
|
|
1543
|
-
|
|
1544
|
-
|
|
1545
|
-
|
|
1546
|
-
|
|
1547
|
-
|
|
1548
|
-
|
|
1549
|
-
|
|
1550
|
-
// Preserve only the safe correlation fields needed to join a
|
|
1551
|
-
// bridge-local failure to the central operational trace. Query
|
|
1552
|
-
// text and upstream bodies never leave the local event stream.
|
|
1553
|
-
request_id: rec.request_id,
|
|
1554
|
-
endpoint: rec.endpoint,
|
|
1555
|
-
http_status: rec.http_status,
|
|
1556
|
-
error_code: rec.error_code ?? (rec.error_kind === "none" ? undefined : rec.error_kind),
|
|
1557
|
-
};
|
|
1558
|
-
void this.client.trackMcpAnalyticsEvent(rec.ok ? "[MCP] EchoMem Tool Succeeded" : "[MCP] EchoMem Tool Failed", finalAnalytics, `${analyticsCallId}:generic-completed`);
|
|
1559
|
-
void this.client.trackMcpAnalyticsEvent(toolEventName(canonicalName, rec.ok ? "Succeeded" : "Failed"), finalAnalytics, `${analyticsCallId}:completed`);
|
|
1592
|
+
finally {
|
|
1593
|
+
if (!rec.error_kind)
|
|
1594
|
+
rec.error_kind = "none";
|
|
1595
|
+
rec.ok = rec.error_kind === "none";
|
|
1596
|
+
rec.latency_ms = Date.now() - t0;
|
|
1597
|
+
this.events.record(rec);
|
|
1598
|
+
if (canonicalName !== canonicalToolNames.report &&
|
|
1599
|
+
canonicalName !== canonicalToolNames.updateStatus &&
|
|
1600
|
+
canonicalName !== canonicalToolNames.contextHealth &&
|
|
1601
|
+
canonicalName !== canonicalToolNames.recompose &&
|
|
1602
|
+
canonicalName !== canonicalToolNames.save &&
|
|
1603
|
+
this.client.hasToken()) {
|
|
1604
|
+
const finalAnalytics = {
|
|
1605
|
+
...analyticsBase,
|
|
1606
|
+
success: rec.ok,
|
|
1607
|
+
duration_ms: rec.latency_ms,
|
|
1608
|
+
error_type: rec.error_kind === "none" ? undefined : rec.error_kind,
|
|
1609
|
+
result_count: rec.results_count,
|
|
1610
|
+
returned_text_length: rec.results_chars,
|
|
1611
|
+
tuned: rec.tuned,
|
|
1612
|
+
map_injected: rec.map_injected,
|
|
1613
|
+
encrypted_user: rec.encrypted,
|
|
1614
|
+
extracted_memory_count: rec.memories_extracted,
|
|
1615
|
+
// Preserve only the safe correlation fields needed to join a
|
|
1616
|
+
// bridge-local failure to the central operational trace. Query
|
|
1617
|
+
// text and upstream bodies never leave the local event stream.
|
|
1618
|
+
request_id: rec.request_id,
|
|
1619
|
+
endpoint: rec.endpoint,
|
|
1620
|
+
http_status: rec.http_status,
|
|
1621
|
+
error_code: rec.error_code ?? (rec.error_kind === "none" ? undefined : rec.error_kind),
|
|
1622
|
+
};
|
|
1623
|
+
void this.client.trackMcpAnalyticsEvent(rec.ok ? "[MCP] EchoMem Tool Succeeded" : "[MCP] EchoMem Tool Failed", finalAnalytics, `${analyticsCallId}:generic-completed`);
|
|
1624
|
+
void this.client.trackMcpAnalyticsEvent(toolEventName(canonicalName, rec.ok ? "Succeeded" : "Failed"), finalAnalytics, `${analyticsCallId}:completed`);
|
|
1625
|
+
}
|
|
1560
1626
|
}
|
|
1561
|
-
}
|
|
1627
|
+
});
|
|
1562
1628
|
});
|
|
1563
1629
|
}
|
|
1564
1630
|
async handleSearch(args, rec) {
|
|
@@ -1771,6 +1837,24 @@ Details: ${m.details || "N/A"}`)
|
|
|
1771
1837
|
].filter(Boolean).join("\n\n");
|
|
1772
1838
|
return { content: [{ type: "text", text }] };
|
|
1773
1839
|
}
|
|
1840
|
+
async handleBindSourceSession(args) {
|
|
1841
|
+
const parsed = bindSourceSessionSchema.parse(args ?? {});
|
|
1842
|
+
const verified = resolveSourceSessionFromBindingToken(parsed.bindingToken, {
|
|
1843
|
+
hostPlatform: this.getMcpClientAnalytics().host_platform,
|
|
1844
|
+
});
|
|
1845
|
+
const bound = await this.client.bindSourceSession(verified);
|
|
1846
|
+
return {
|
|
1847
|
+
content: [{
|
|
1848
|
+
type: "text",
|
|
1849
|
+
text: [
|
|
1850
|
+
bound.created ? "Created and bound the source-session context." : "Source-session context is bound.",
|
|
1851
|
+
`Context: ${bound.contextId}`,
|
|
1852
|
+
`Source session: ${bound.canonicalKey}`,
|
|
1853
|
+
"Later EchoMem saves, recalls, citations, sharing decisions, and activity from this bridge will use this context automatically.",
|
|
1854
|
+
].join("\n"),
|
|
1855
|
+
}],
|
|
1856
|
+
};
|
|
1857
|
+
}
|
|
1774
1858
|
async handleTimeRange(args) {
|
|
1775
1859
|
const parsed = timeRangeSchema.parse(args ?? {});
|
|
1776
1860
|
const { success, memories, error } = await this.client.getMemoriesByTimeRange(args);
|
|
@@ -2236,7 +2320,7 @@ Details: ${m.details || "N/A"}`;
|
|
|
2236
2320
|
sharingPromptText(scopeText, groupName) {
|
|
2237
2321
|
return [
|
|
2238
2322
|
scopeText,
|
|
2239
|
-
`Ask exactly: “Share eligible memories saved from this conversation with ${groupName}?” Only an explicit Yes or No may be passed to set_group_session_sharing. Silence leaves the state unset.`,
|
|
2323
|
+
`Ask exactly: “Share eligible memories saved from this conversation with ${groupName}?” If your host can render a single-choice prompt, present this as one question with exactly two options — “Yes — share with team” and “No — keep private” — instead of free-form text. Only an explicit Yes or No may be passed to set_group_session_sharing. Silence leaves the state unset.`,
|
|
2240
2324
|
].filter(Boolean).join("\n\n");
|
|
2241
2325
|
}
|
|
2242
2326
|
formatGroupSessionSharingUpdate(share, payload, scopeText = "") {
|