@echomem/mcp 1.4.40 → 1.4.42
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 +382 -247
- package/dist/package-metadata.js +2 -1
- package/dist/setup.js +146 -47
- package/dist/source-session-hook.js +103 -0
- package/dist/source-session.js +261 -0
- package/dist/v1-contract.js +75 -3
- 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) ||
|
|
@@ -1049,6 +1096,8 @@ class EchoMemApiClient {
|
|
|
1049
1096
|
kPerUser: parsed.kPerUser,
|
|
1050
1097
|
similarityThreshold: parsed.similarityThreshold,
|
|
1051
1098
|
timeFrameDays: parsed.timeFrameDays,
|
|
1099
|
+
workspaceId: parsed.workspaceId ?? parsed.groupId,
|
|
1100
|
+
scope: parsed.scope,
|
|
1052
1101
|
requestId: this.sessionId,
|
|
1053
1102
|
source: "mcp_friend_public_memory_search",
|
|
1054
1103
|
});
|
|
@@ -1061,7 +1110,14 @@ class EchoMemApiClient {
|
|
|
1061
1110
|
async getPublicMemory(args) {
|
|
1062
1111
|
const parsed = publicMemorySchema.parse(args ?? {});
|
|
1063
1112
|
try {
|
|
1064
|
-
const
|
|
1113
|
+
const workspaceId = parsed.workspaceId ?? parsed.groupId;
|
|
1114
|
+
const query = new URLSearchParams({
|
|
1115
|
+
requestId: this.sessionId,
|
|
1116
|
+
source: "mcp_friend_public_memory_fetch",
|
|
1117
|
+
});
|
|
1118
|
+
if (workspaceId)
|
|
1119
|
+
query.set("workspaceId", workspaceId);
|
|
1120
|
+
const response = await this.axios.get(`/api/extension/social/public-memories/${encodeURIComponent(parsed.memoryId)}?${query.toString()}`);
|
|
1065
1121
|
return response.data;
|
|
1066
1122
|
}
|
|
1067
1123
|
catch (error) {
|
|
@@ -1084,9 +1140,13 @@ class EchoMemApiClient {
|
|
|
1084
1140
|
}
|
|
1085
1141
|
}
|
|
1086
1142
|
async getGroupContext(args) {
|
|
1087
|
-
groupContextSchema.parse(args ?? {});
|
|
1143
|
+
const parsed = groupContextSchema.parse(args ?? {});
|
|
1088
1144
|
try {
|
|
1089
|
-
const
|
|
1145
|
+
const workspaceId = parsed.workspaceId ?? parsed.groupId;
|
|
1146
|
+
const path = workspaceId
|
|
1147
|
+
? `/api/extension/social/groups/current?workspaceId=${encodeURIComponent(workspaceId)}`
|
|
1148
|
+
: "/api/extension/social/groups/current";
|
|
1149
|
+
const response = await this.axios.get(path);
|
|
1090
1150
|
return response.data;
|
|
1091
1151
|
}
|
|
1092
1152
|
catch (error) {
|
|
@@ -1150,6 +1210,7 @@ class EchoMemApiClient {
|
|
|
1150
1210
|
const enc = await this.encState();
|
|
1151
1211
|
try {
|
|
1152
1212
|
const response = await this.axios.post(`/api/extension/social/groups/current/memories/${encodeURIComponent(parsed.memoryId)}/publish`, {
|
|
1213
|
+
workspaceId: parsed.workspaceId ?? parsed.groupId,
|
|
1153
1214
|
acknowledgedFlaggedMemoryIds: parsed.acknowledgedFlaggedMemoryIds,
|
|
1154
1215
|
}, {
|
|
1155
1216
|
headers: enc.enabled && enc.key ? { "X-Encryption-Key": enc.key } : undefined,
|
|
@@ -1165,6 +1226,7 @@ class EchoMemApiClient {
|
|
|
1165
1226
|
const enc = await this.encState();
|
|
1166
1227
|
try {
|
|
1167
1228
|
const response = await this.axios.post("/api/extension/social/groups/current/memories/publish-batch", {
|
|
1229
|
+
workspaceId: parsed.workspaceId ?? parsed.groupId,
|
|
1168
1230
|
memoryIds: parsed.memoryIds,
|
|
1169
1231
|
contextId: parsed.contextId,
|
|
1170
1232
|
selectionReason: parsed.selectionReason,
|
|
@@ -1297,7 +1359,7 @@ class EchoMemMCPServer {
|
|
|
1297
1359
|
const updateNotice = DESKTOP_MANAGED ? undefined : formatUpdateNotice(this.updateStatus);
|
|
1298
1360
|
return { tools: listToolSpecs({ map, groupMap, updateNotice }) };
|
|
1299
1361
|
});
|
|
1300
|
-
this.server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
1362
|
+
this.server.setRequestHandler(CallToolRequestSchema, async (request, extra) => {
|
|
1301
1363
|
const resolvedCanonicalName = resolveCanonicalToolName(request.params.name);
|
|
1302
1364
|
const recallRoute = routePersonalRecallInvocation(resolvedCanonicalName, request.params.arguments);
|
|
1303
1365
|
const canonicalName = recallRoute.canonicalName;
|
|
@@ -1307,258 +1369,277 @@ class EchoMemMCPServer {
|
|
|
1307
1369
|
const clientVersion = this.server.getClientVersion();
|
|
1308
1370
|
this.mcpClientName = clientVersion?.name ?? this.mcpClientName;
|
|
1309
1371
|
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
|
-
};
|
|
1372
|
+
let requestSourceSession = this.client.getBoundSourceSession();
|
|
1373
|
+
let inferred = null;
|
|
1332
1374
|
try {
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
//
|
|
1337
|
-
|
|
1338
|
-
|
|
1375
|
+
inferred = resolveSourceSessionFromMcpContext(extra._meta, { hostPlatform: this.getMcpClientAnalytics().host_platform });
|
|
1376
|
+
}
|
|
1377
|
+
catch {
|
|
1378
|
+
// Malformed host metadata is not model input. Ignore it and retain the explicit fallback.
|
|
1379
|
+
}
|
|
1380
|
+
// Verified request metadata always wins over a bridge-level compatibility fallback. The
|
|
1381
|
+
// startup hook performs the eager write; this path attaches the exact context to the current
|
|
1382
|
+
// request and retries the backend write if startup raced login.
|
|
1383
|
+
if (inferred && this.client.hasToken()) {
|
|
1384
|
+
requestSourceSession = await this.client.bindSourceSession(inferred, false);
|
|
1385
|
+
}
|
|
1386
|
+
return this.client.withSourceSession(requestSourceSession, async () => {
|
|
1387
|
+
const t0 = Date.now();
|
|
1388
|
+
const analyticsBase = {
|
|
1389
|
+
surface: "mcp",
|
|
1390
|
+
event_family: "mcp",
|
|
1391
|
+
integration: "echomem_mcp",
|
|
1392
|
+
telemetry_source: "local_bridge",
|
|
1393
|
+
...this.getMcpClientAnalytics(),
|
|
1394
|
+
codex_session_id: this.client.getBoundSourceSession()?.canonicalKey ?? this.client.getSessionId(),
|
|
1395
|
+
conversation_id: this.client.getBoundSourceSession()?.canonicalKey ?? this.client.getSessionId(),
|
|
1396
|
+
context_id: this.client.getBoundSourceSession()?.contextId,
|
|
1397
|
+
tool_name: request.params.name,
|
|
1398
|
+
canonical_tool_name: canonicalName,
|
|
1399
|
+
...triggerAnalyticsForTool(canonicalName, toolArgs),
|
|
1400
|
+
...inputAnalyticsForTool(canonicalName, toolArgs),
|
|
1401
|
+
};
|
|
1402
|
+
const analyticsCallId = randomUUID();
|
|
1403
|
+
// One event per call. Handlers enrich `rec` with tool-specific detail; we finalize + log in `finally`.
|
|
1404
|
+
const rec = {
|
|
1405
|
+
type: "tool_call",
|
|
1406
|
+
tool: canonicalName,
|
|
1407
|
+
map_injected: this.mapInjected,
|
|
1408
|
+
group_map_injected: this.groupMapInjected,
|
|
1409
|
+
};
|
|
1410
|
+
try {
|
|
1411
|
+
if (recallRoute.error) {
|
|
1412
|
+
throw new McpError(ErrorCode.InvalidParams, recallRoute.error);
|
|
1413
|
+
}
|
|
1414
|
+
// The usage report is a local, $0 audit — works with no login (value before signup).
|
|
1415
|
+
if (canonicalName === canonicalToolNames.report) {
|
|
1416
|
+
return { content: [{ type: "text", text: await buildReportText(false) }] };
|
|
1417
|
+
}
|
|
1418
|
+
if (canonicalName === canonicalToolNames.updateStatus) {
|
|
1419
|
+
if (DESKTOP_MANAGED) {
|
|
1420
|
+
return {
|
|
1421
|
+
content: [{
|
|
1422
|
+
type: "text",
|
|
1423
|
+
text: `Echo Desktop manages this MCP runtime (${MCP_PACKAGE_VERSION}). Install app updates from Echo Desktop, then start a new agent session.`,
|
|
1424
|
+
}],
|
|
1425
|
+
};
|
|
1426
|
+
}
|
|
1427
|
+
const force = isRecord(request.params.arguments) && request.params.arguments.force === true;
|
|
1428
|
+
const status = await checkLatestUpdateStatus({ force });
|
|
1429
|
+
this.handleUpdateStatus(status);
|
|
1430
|
+
return { content: [{ type: "text", text: formatUpdateStatusText(this.updateStatus ?? status) }] };
|
|
1431
|
+
}
|
|
1432
|
+
if (canonicalName === canonicalToolNames.contextHealth) {
|
|
1433
|
+
const client = isRecord(request.params.arguments) && typeof request.params.arguments.client === "string"
|
|
1434
|
+
? request.params.arguments.client
|
|
1435
|
+
: "auto";
|
|
1436
|
+
const mode = client === "codex" || client === "claude-code" || client === "claude-desktop" || client === "auto"
|
|
1437
|
+
? client
|
|
1438
|
+
: "auto";
|
|
1439
|
+
return { content: [{ type: "text", text: await contextHealthMarkdown(mode) }] };
|
|
1440
|
+
}
|
|
1441
|
+
if (canonicalName === canonicalToolNames.recompose) {
|
|
1442
|
+
const client = isRecord(request.params.arguments) && typeof request.params.arguments.client === "string"
|
|
1443
|
+
? request.params.arguments.client
|
|
1444
|
+
: "auto";
|
|
1445
|
+
const mode = client === "codex" || client === "claude-code" || client === "claude-desktop" || client === "auto"
|
|
1446
|
+
? client
|
|
1447
|
+
: "auto";
|
|
1448
|
+
const capsuleText = await recomposeCapsuleMarkdown(mode);
|
|
1449
|
+
// If logged in, persist the capsule via passthrough so it's retrievable by contextId.
|
|
1450
|
+
if (this.client.hasToken()) {
|
|
1451
|
+
try {
|
|
1452
|
+
const saveResult = await this.client.saveConversation({
|
|
1453
|
+
conversation: capsuleText,
|
|
1454
|
+
title: "Session capsule (recompose)",
|
|
1455
|
+
source: "mcp_recompose",
|
|
1456
|
+
passthrough: true,
|
|
1457
|
+
});
|
|
1458
|
+
const ctxId = saveResult?.contextId;
|
|
1459
|
+
const capId = saveResult?.capsuleId;
|
|
1460
|
+
const persistLine = ctxId
|
|
1461
|
+
? `\n\n---\nCapsule persisted${capId ? ` (${capId})` : ""}. To reload in a fresh session:\nget_memories_by_context({ contextId: "${ctxId}" })`
|
|
1462
|
+
: "";
|
|
1463
|
+
return { content: [{ type: "text", text: capsuleText + persistLine }] };
|
|
1464
|
+
}
|
|
1465
|
+
catch {
|
|
1466
|
+
// Persistence is best-effort; return the capsule text regardless.
|
|
1467
|
+
}
|
|
1468
|
+
}
|
|
1469
|
+
return { content: [{ type: "text", text: capsuleText }] };
|
|
1470
|
+
}
|
|
1471
|
+
if (!this.client.hasToken())
|
|
1472
|
+
throw new NoTokenError();
|
|
1473
|
+
if (canonicalName !== canonicalToolNames.save) {
|
|
1474
|
+
void this.client.trackMcpAnalyticsEvent("[MCP] EchoMem Tool Called", analyticsBase, `${analyticsCallId}:generic-called`);
|
|
1475
|
+
void this.client.trackMcpAnalyticsEvent(toolEventName(canonicalName, "Called"), analyticsBase, `${analyticsCallId}:called`);
|
|
1476
|
+
}
|
|
1477
|
+
if (canonicalName !== canonicalToolNames.save && analyticsBase.trigger_message_available === true) {
|
|
1478
|
+
void this.client.trackMcpAnalyticsEvent("[MCP] EchoMem Triggered By User Turn", analyticsBase);
|
|
1479
|
+
}
|
|
1480
|
+
switch (canonicalName) {
|
|
1481
|
+
case canonicalToolNames.bindSourceSession:
|
|
1482
|
+
return await this.handleBindSourceSession(request.params.arguments);
|
|
1483
|
+
case canonicalToolNames.search:
|
|
1484
|
+
return await this.handleSearch(toolArgs, rec);
|
|
1485
|
+
case canonicalToolNames.save:
|
|
1486
|
+
return await this.handleSave(request.params.arguments, rec);
|
|
1487
|
+
case canonicalToolNames.timeRange:
|
|
1488
|
+
return await this.handleTimeRange(toolArgs);
|
|
1489
|
+
case canonicalToolNames.getByContext:
|
|
1490
|
+
return await this.handleGetByContext(request.params.arguments);
|
|
1491
|
+
case canonicalToolNames.checkpointByContext:
|
|
1492
|
+
return await this.handleGetCheckpointByContext(request.params.arguments);
|
|
1493
|
+
case canonicalToolNames.keywords:
|
|
1494
|
+
return await this.handleKeywords(toolArgs);
|
|
1495
|
+
case canonicalToolNames.friends:
|
|
1496
|
+
return await this.handleFriends(request.params.arguments);
|
|
1497
|
+
case canonicalToolNames.searchUsers:
|
|
1498
|
+
return await this.handleSearchUsers(request.params.arguments);
|
|
1499
|
+
case canonicalToolNames.sendFriendRequest:
|
|
1500
|
+
return await this.handleSendFriendRequest(request.params.arguments);
|
|
1501
|
+
case canonicalToolNames.others:
|
|
1502
|
+
return await this.handleOthers(toolArgs);
|
|
1503
|
+
case canonicalToolNames.publicMemory:
|
|
1504
|
+
return await this.handlePublicMemory(request.params.arguments);
|
|
1505
|
+
case canonicalToolNames.recordCitations:
|
|
1506
|
+
return await this.handleRecordMemoryCitations(request.params.arguments);
|
|
1507
|
+
case canonicalToolNames.groupContext:
|
|
1508
|
+
return await this.handleGroupContext(request.params.arguments);
|
|
1509
|
+
case canonicalToolNames.getGroupSessionSharing:
|
|
1510
|
+
return await this.handleGetGroupSessionSharing(request.params.arguments);
|
|
1511
|
+
case canonicalToolNames.requestGroupSessionSharing:
|
|
1512
|
+
return await this.handleRequestGroupSessionSharing(request.params.arguments);
|
|
1513
|
+
case canonicalToolNames.setGroupSessionSharing:
|
|
1514
|
+
return await this.handleSetGroupSessionSharing(request.params.arguments);
|
|
1515
|
+
case canonicalToolNames.createGroup:
|
|
1516
|
+
return await this.handleCreateGroup(request.params.arguments);
|
|
1517
|
+
case canonicalToolNames.createGroupInvite:
|
|
1518
|
+
return await this.handleCreateGroupInvite(request.params.arguments);
|
|
1519
|
+
case canonicalToolNames.joinGroup:
|
|
1520
|
+
return await this.handleJoinGroup(request.params.arguments);
|
|
1521
|
+
case canonicalToolNames.prepareGroupPublication:
|
|
1522
|
+
return await this.handlePrepareGroupPublication(request.params.arguments);
|
|
1523
|
+
case canonicalToolNames.flagPublicationAttention:
|
|
1524
|
+
return await this.handleFlagPublicationAttention(request.params.arguments);
|
|
1525
|
+
case canonicalToolNames.updateGroupProfile:
|
|
1526
|
+
return await this.handleUpdateGroupProfile(request.params.arguments);
|
|
1527
|
+
case canonicalToolNames.completeGroupPublication:
|
|
1528
|
+
return await this.handleCompleteGroupPublication(request.params.arguments);
|
|
1529
|
+
case canonicalToolNames.publishToGroup:
|
|
1530
|
+
return await this.handlePublishToGroup(request.params.arguments);
|
|
1531
|
+
case canonicalToolNames.publishBatchToGroup:
|
|
1532
|
+
return await this.handlePublishBatchToGroup(request.params.arguments);
|
|
1533
|
+
case canonicalToolNames.delete:
|
|
1534
|
+
return await this.handleDelete(request.params.arguments, rec);
|
|
1535
|
+
default:
|
|
1536
|
+
throw new McpError(ErrorCode.MethodNotFound, `Unknown tool: ${request.params.name}`);
|
|
1537
|
+
}
|
|
1339
1538
|
}
|
|
1340
|
-
|
|
1341
|
-
|
|
1539
|
+
catch (error) {
|
|
1540
|
+
rec.error_kind = classifyError(error);
|
|
1541
|
+
if (error instanceof NoTokenError) {
|
|
1542
|
+
// Not isError: a normal "not connected yet" state. Once `login` runs, the next call works.
|
|
1543
|
+
return {
|
|
1544
|
+
content: [
|
|
1545
|
+
{
|
|
1546
|
+
type: "text",
|
|
1547
|
+
text: `🔌 EchoMem isn't connected yet. ${CONNECT_DEVICE_INSTRUCTION} No editor restart is needed.`,
|
|
1548
|
+
},
|
|
1549
|
+
],
|
|
1550
|
+
};
|
|
1551
|
+
}
|
|
1552
|
+
if (error instanceof LockedError) {
|
|
1553
|
+
// Not isError: this is a normal "needs unlock" state the model should relay to the user.
|
|
1554
|
+
return {
|
|
1555
|
+
content: [
|
|
1556
|
+
{
|
|
1557
|
+
type: "text",
|
|
1558
|
+
text: [
|
|
1559
|
+
"🔒 EchoMem vault is locked.",
|
|
1560
|
+
"This encrypted account has no usable local decryption key. Once unlocked, this trusted device stays unlocked until you explicitly lock it or log out.",
|
|
1561
|
+
`Action required from the user: ${UNLOCK_VAULT_INSTRUCTION}`,
|
|
1562
|
+
...(DESKTOP_MANAGED ? [] : [
|
|
1563
|
+
"At `Vault passphrase (typing is hidden):`, type the passphrase and press Return. No characters will appear while you type; that is expected.",
|
|
1564
|
+
]),
|
|
1565
|
+
"After the success message, retry this EchoMem action in the current session — no editor restart is needed.",
|
|
1566
|
+
].join("\n"),
|
|
1567
|
+
},
|
|
1568
|
+
],
|
|
1569
|
+
};
|
|
1570
|
+
}
|
|
1571
|
+
if (error instanceof EncryptionStatusUnavailableError) {
|
|
1342
1572
|
return {
|
|
1343
1573
|
content: [{
|
|
1344
1574
|
type: "text",
|
|
1345
|
-
text:
|
|
1575
|
+
text: [
|
|
1576
|
+
"⚠️ EchoMem could not verify whether this account's vault is encrypted.",
|
|
1577
|
+
"No memory data was returned. Retry this action after the service recovers.",
|
|
1578
|
+
].join("\n"),
|
|
1346
1579
|
}],
|
|
1580
|
+
isError: true,
|
|
1347
1581
|
};
|
|
1348
1582
|
}
|
|
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
|
-
}
|
|
1583
|
+
if (error instanceof ZodError) {
|
|
1584
|
+
throw new McpError(ErrorCode.InvalidParams, `Invalid arguments: ${error.message}`);
|
|
1585
|
+
}
|
|
1586
|
+
if (error instanceof McpError) {
|
|
1587
|
+
throw error;
|
|
1588
|
+
}
|
|
1589
|
+
const reconnectRequired = formatReconnectRequiredResult(error);
|
|
1590
|
+
if (reconnectRequired) {
|
|
1591
|
+
return {
|
|
1592
|
+
content: [{ type: "text", text: reconnectRequired }],
|
|
1593
|
+
isError: true,
|
|
1594
|
+
};
|
|
1595
|
+
}
|
|
1596
|
+
const upgradeRequired = formatUpgradeRequiredResult(error);
|
|
1597
|
+
if (upgradeRequired) {
|
|
1598
|
+
return {
|
|
1599
|
+
content: [{ type: "text", text: upgradeRequired }],
|
|
1600
|
+
};
|
|
1390
1601
|
}
|
|
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
1602
|
return {
|
|
1512
|
-
content: [{ type: "text", text:
|
|
1603
|
+
content: [{ type: "text", text: `Error: ${describeError(error)}` }],
|
|
1513
1604
|
isError: true,
|
|
1514
1605
|
};
|
|
1515
1606
|
}
|
|
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`);
|
|
1607
|
+
finally {
|
|
1608
|
+
if (!rec.error_kind)
|
|
1609
|
+
rec.error_kind = "none";
|
|
1610
|
+
rec.ok = rec.error_kind === "none";
|
|
1611
|
+
rec.latency_ms = Date.now() - t0;
|
|
1612
|
+
this.events.record(rec);
|
|
1613
|
+
if (canonicalName !== canonicalToolNames.report &&
|
|
1614
|
+
canonicalName !== canonicalToolNames.updateStatus &&
|
|
1615
|
+
canonicalName !== canonicalToolNames.contextHealth &&
|
|
1616
|
+
canonicalName !== canonicalToolNames.recompose &&
|
|
1617
|
+
canonicalName !== canonicalToolNames.save &&
|
|
1618
|
+
this.client.hasToken()) {
|
|
1619
|
+
const finalAnalytics = {
|
|
1620
|
+
...analyticsBase,
|
|
1621
|
+
success: rec.ok,
|
|
1622
|
+
duration_ms: rec.latency_ms,
|
|
1623
|
+
error_type: rec.error_kind === "none" ? undefined : rec.error_kind,
|
|
1624
|
+
result_count: rec.results_count,
|
|
1625
|
+
returned_text_length: rec.results_chars,
|
|
1626
|
+
tuned: rec.tuned,
|
|
1627
|
+
map_injected: rec.map_injected,
|
|
1628
|
+
encrypted_user: rec.encrypted,
|
|
1629
|
+
extracted_memory_count: rec.memories_extracted,
|
|
1630
|
+
// Preserve only the safe correlation fields needed to join a
|
|
1631
|
+
// bridge-local failure to the central operational trace. Query
|
|
1632
|
+
// text and upstream bodies never leave the local event stream.
|
|
1633
|
+
request_id: rec.request_id,
|
|
1634
|
+
endpoint: rec.endpoint,
|
|
1635
|
+
http_status: rec.http_status,
|
|
1636
|
+
error_code: rec.error_code ?? (rec.error_kind === "none" ? undefined : rec.error_kind),
|
|
1637
|
+
};
|
|
1638
|
+
void this.client.trackMcpAnalyticsEvent(rec.ok ? "[MCP] EchoMem Tool Succeeded" : "[MCP] EchoMem Tool Failed", finalAnalytics, `${analyticsCallId}:generic-completed`);
|
|
1639
|
+
void this.client.trackMcpAnalyticsEvent(toolEventName(canonicalName, rec.ok ? "Succeeded" : "Failed"), finalAnalytics, `${analyticsCallId}:completed`);
|
|
1640
|
+
}
|
|
1560
1641
|
}
|
|
1561
|
-
}
|
|
1642
|
+
});
|
|
1562
1643
|
});
|
|
1563
1644
|
}
|
|
1564
1645
|
async handleSearch(args, rec) {
|
|
@@ -1771,6 +1852,24 @@ Details: ${m.details || "N/A"}`)
|
|
|
1771
1852
|
].filter(Boolean).join("\n\n");
|
|
1772
1853
|
return { content: [{ type: "text", text }] };
|
|
1773
1854
|
}
|
|
1855
|
+
async handleBindSourceSession(args) {
|
|
1856
|
+
const parsed = bindSourceSessionSchema.parse(args ?? {});
|
|
1857
|
+
const verified = resolveSourceSessionFromBindingToken(parsed.bindingToken, {
|
|
1858
|
+
hostPlatform: this.getMcpClientAnalytics().host_platform,
|
|
1859
|
+
});
|
|
1860
|
+
const bound = await this.client.bindSourceSession(verified);
|
|
1861
|
+
return {
|
|
1862
|
+
content: [{
|
|
1863
|
+
type: "text",
|
|
1864
|
+
text: [
|
|
1865
|
+
bound.created ? "Created and bound the source-session context." : "Source-session context is bound.",
|
|
1866
|
+
`Context: ${bound.contextId}`,
|
|
1867
|
+
`Source session: ${bound.canonicalKey}`,
|
|
1868
|
+
"Later EchoMem saves, recalls, citations, sharing decisions, and activity from this bridge will use this context automatically.",
|
|
1869
|
+
].join("\n"),
|
|
1870
|
+
}],
|
|
1871
|
+
};
|
|
1872
|
+
}
|
|
1774
1873
|
async handleTimeRange(args) {
|
|
1775
1874
|
const parsed = timeRangeSchema.parse(args ?? {});
|
|
1776
1875
|
const { success, memories, error } = await this.client.getMemoriesByTimeRange(args);
|
|
@@ -1911,6 +2010,28 @@ Details: ${m.details || "N/A"}`)
|
|
|
1911
2010
|
async handleOthers(args) {
|
|
1912
2011
|
const parsed = othersSchema.parse(args ?? {});
|
|
1913
2012
|
const payload = await this.client.searchOthersMemories(args);
|
|
2013
|
+
if (payload?.requiresWorkspaceSelection === true) {
|
|
2014
|
+
const availableWorkspaces = Array.isArray(payload?.availableWorkspaces)
|
|
2015
|
+
? payload.availableWorkspaces.filter(isRecord)
|
|
2016
|
+
: [];
|
|
2017
|
+
const choices = availableWorkspaces
|
|
2018
|
+
.map((workspace) => `${readString(workspace, "name") ?? "Unnamed workspace"} (${readString(workspace, "id") ?? "unknown id"})`)
|
|
2019
|
+
.join(", ");
|
|
2020
|
+
return {
|
|
2021
|
+
content: [{
|
|
2022
|
+
type: "text",
|
|
2023
|
+
text: `You belong to ${availableWorkspaces.length} workspaces: ${choices}. Ask the user which workspace's teammates to search, then call search_others_memories again with workspaceId set to one of those ids. (To search a specific friend instead, pass that person as target.)`,
|
|
2024
|
+
}],
|
|
2025
|
+
};
|
|
2026
|
+
}
|
|
2027
|
+
if (payload?.groupScopeUnavailable === true) {
|
|
2028
|
+
return {
|
|
2029
|
+
content: [{
|
|
2030
|
+
type: "text",
|
|
2031
|
+
text: "You are not in a company workspace, so there are no teammates to search. To search friends instead, call search_others_memories again with scope \"friends\", or pass a specific person as target.",
|
|
2032
|
+
}],
|
|
2033
|
+
};
|
|
2034
|
+
}
|
|
1914
2035
|
const memories = payload?.memories ?? [];
|
|
1915
2036
|
const authenticatedViewer = isRecord(payload?.authenticatedViewer)
|
|
1916
2037
|
? payload.authenticatedViewer
|
|
@@ -2132,6 +2253,20 @@ Details: ${m.details || "N/A"}`;
|
|
|
2132
2253
|
async handleGroupContext(args) {
|
|
2133
2254
|
groupContextSchema.parse(args ?? {});
|
|
2134
2255
|
const payload = await this.client.getGroupContext(args);
|
|
2256
|
+
if (payload?.requiresWorkspaceSelection === true) {
|
|
2257
|
+
const availableWorkspaces = Array.isArray(payload?.availableWorkspaces)
|
|
2258
|
+
? payload.availableWorkspaces.filter(isRecord)
|
|
2259
|
+
: [];
|
|
2260
|
+
const choices = availableWorkspaces
|
|
2261
|
+
.map((workspace) => `${readString(workspace, "name") ?? "Unnamed workspace"} (${readString(workspace, "id") ?? "unknown id"})`)
|
|
2262
|
+
.join(", ");
|
|
2263
|
+
return {
|
|
2264
|
+
content: [{
|
|
2265
|
+
type: "text",
|
|
2266
|
+
text: `You belong to ${availableWorkspaces.length} workspaces: ${choices}. Ask the user which workspace to show context for, then call get_group_context again with workspaceId set to one of those ids.`,
|
|
2267
|
+
}],
|
|
2268
|
+
};
|
|
2269
|
+
}
|
|
2135
2270
|
const group = isRecord(payload?.group) ? payload.group : null;
|
|
2136
2271
|
const participants = Array.isArray(payload?.participants)
|
|
2137
2272
|
? payload.participants.filter(isRecord)
|
|
@@ -2236,7 +2371,7 @@ Details: ${m.details || "N/A"}`;
|
|
|
2236
2371
|
sharingPromptText(scopeText, groupName) {
|
|
2237
2372
|
return [
|
|
2238
2373
|
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.`,
|
|
2374
|
+
`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
2375
|
].filter(Boolean).join("\n\n");
|
|
2241
2376
|
}
|
|
2242
2377
|
formatGroupSessionSharingUpdate(share, payload, scopeText = "") {
|