@echomem/mcp 1.4.29 → 1.4.31
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/dist/index.js +92 -11
- package/dist/package-metadata.js +2 -2
- package/dist/save-checkpoint-hook.js +2 -2
- package/dist/setup.js +1 -1
- package/dist/v1-contract.js +45 -5
- package/package.json +2 -2
- package/templates/codex-skills/echomem-save/SKILL.md +7 -4
- package/templates/echomem-recall.md +7 -5
package/dist/index.js
CHANGED
|
@@ -4,7 +4,7 @@ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"
|
|
|
4
4
|
import { CallToolRequestSchema, ErrorCode, ListToolsRequestSchema, McpError, } from "@modelcontextprotocol/sdk/types.js";
|
|
5
5
|
import axios from "axios";
|
|
6
6
|
import { ZodError } from "zod";
|
|
7
|
-
import { canonicalToolNames, completeGroupPublicationSchema, createGroupInviteSchema, createGroupSchema, deleteMemorySchema, flagPublicationAttentionSchema, getGroupSessionSharingSchema, getByContextSchema, groupContextSchema, joinGroupSchema, keywordsSchema, listFriendsSchema, listToolSpecs, othersSchema, publishBatchToGroupSchema, publishToGroupSchema, prepareGroupPublicationSchema, publicMemorySchema, resolveCanonicalToolName, saveConversationSchema, searchMemoriesSchema, searchUsersSchema, sendFriendRequestSchema, timeRangeSchema, updateGroupProfileSchema, setGroupSessionSharingSchema, } from "./v1-contract.js";
|
|
7
|
+
import { canonicalToolNames, completeGroupPublicationSchema, createGroupInviteSchema, createGroupSchema, deleteMemorySchema, flagPublicationAttentionSchema, getGroupSessionSharingSchema, getByContextSchema, groupContextSchema, joinGroupSchema, keywordsSchema, listFriendsSchema, listToolSpecs, othersSchema, publishBatchToGroupSchema, publishToGroupSchema, prepareGroupPublicationSchema, publicMemorySchema, recordMemoryCitationsSchema, 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";
|
|
@@ -513,6 +513,16 @@ function inputAnalyticsForTool(canonicalName, args) {
|
|
|
513
513
|
memory_id_hash: memoryId ? hashText(memoryId) : undefined,
|
|
514
514
|
};
|
|
515
515
|
}
|
|
516
|
+
case canonicalToolNames.recordCitations: {
|
|
517
|
+
const memoryIds = Array.isArray(a.memoryIds)
|
|
518
|
+
? a.memoryIds.filter((id) => typeof id === "string")
|
|
519
|
+
: [];
|
|
520
|
+
const receiptId = readString(a, "receiptId");
|
|
521
|
+
return {
|
|
522
|
+
memory_count: memoryIds.length,
|
|
523
|
+
receipt_id_hash: receiptId ? hashText(receiptId) : undefined,
|
|
524
|
+
};
|
|
525
|
+
}
|
|
516
526
|
case canonicalToolNames.flagPublicationAttention: {
|
|
517
527
|
const memoryIds = Array.isArray(a.memoryIds) ? a.memoryIds.filter((id) => typeof id === "string") : [];
|
|
518
528
|
return {
|
|
@@ -610,6 +620,21 @@ class EchoMemApiClient {
|
|
|
610
620
|
return undefined;
|
|
611
621
|
}
|
|
612
622
|
}
|
|
623
|
+
/**
|
|
624
|
+
* Compact "who covers what" guide for the user's company group, used to decorate the group tool
|
|
625
|
+
* descriptions. Group publication snapshots are plaintext by design, so this works even when the
|
|
626
|
+
* local vault is locked — unlike `fetchMemoryMap`, it never needs a decryption key.
|
|
627
|
+
*/
|
|
628
|
+
async fetchGroupMemoryMap() {
|
|
629
|
+
try {
|
|
630
|
+
const response = await this.axios.get("/api/extension/social/groups/current", { timeout: 6000 });
|
|
631
|
+
const map = typeof response.data?.map === "string" ? response.data.map.trim() : "";
|
|
632
|
+
return map || undefined;
|
|
633
|
+
}
|
|
634
|
+
catch {
|
|
635
|
+
return undefined;
|
|
636
|
+
}
|
|
637
|
+
}
|
|
613
638
|
/** Encryption config for the account, fetched once and cached. Failures are not cached. */
|
|
614
639
|
async getEncryptionConfig() {
|
|
615
640
|
if (!this.encConfigPromise) {
|
|
@@ -983,6 +1008,16 @@ class EchoMemApiClient {
|
|
|
983
1008
|
throw new Error(`get_public_memory failed against ${ECHO_API_BASE_URL}: ${describeError(error)}`);
|
|
984
1009
|
}
|
|
985
1010
|
}
|
|
1011
|
+
async recordMemoryCitations(args) {
|
|
1012
|
+
const parsed = recordMemoryCitationsSchema.parse(args ?? {});
|
|
1013
|
+
try {
|
|
1014
|
+
const response = await this.axios.post("/api/extension/social/memory-citations", { ...parsed, sessionKey: this.sessionId });
|
|
1015
|
+
return response.data;
|
|
1016
|
+
}
|
|
1017
|
+
catch (error) {
|
|
1018
|
+
throw new Error(`record_memory_citations failed against ${ECHO_API_BASE_URL}: ${describeError(error)}`);
|
|
1019
|
+
}
|
|
1020
|
+
}
|
|
986
1021
|
async getGroupContext(args) {
|
|
987
1022
|
groupContextSchema.parse(args ?? {});
|
|
988
1023
|
try {
|
|
@@ -1076,15 +1111,28 @@ class EchoMemApiClient {
|
|
|
1076
1111
|
}
|
|
1077
1112
|
}
|
|
1078
1113
|
const SERVER_VERSION = MCP_PACKAGE_VERSION;
|
|
1114
|
+
/** Tool-description decoration must never delay the MCP handshake; give up and decorate next listing. */
|
|
1115
|
+
const MAP_WAIT_MS = 2500;
|
|
1116
|
+
function capWait(pending) {
|
|
1117
|
+
if (!pending)
|
|
1118
|
+
return Promise.resolve(undefined);
|
|
1119
|
+
return Promise.race([
|
|
1120
|
+
pending,
|
|
1121
|
+
new Promise((resolve) => setTimeout(() => resolve(undefined), MAP_WAIT_MS)),
|
|
1122
|
+
]);
|
|
1123
|
+
}
|
|
1079
1124
|
class EchoMemMCPServer {
|
|
1080
1125
|
server;
|
|
1081
1126
|
client;
|
|
1082
1127
|
mapCache = null;
|
|
1128
|
+
groupMapCache = null;
|
|
1083
1129
|
events;
|
|
1084
1130
|
mcpClientName;
|
|
1085
1131
|
mcpClientVersion;
|
|
1086
1132
|
/** Whether the most recent ListTools response carried the memory map (per-session recall signal). */
|
|
1087
1133
|
mapInjected = false;
|
|
1134
|
+
/** Whether the most recent ListTools response carried the group memory map. */
|
|
1135
|
+
groupMapInjected = false;
|
|
1088
1136
|
updateStatus;
|
|
1089
1137
|
constructor(store) {
|
|
1090
1138
|
this.server = new Server({
|
|
@@ -1133,15 +1181,23 @@ class EchoMemMCPServer {
|
|
|
1133
1181
|
this.mapCache.then((m) => { if (!m)
|
|
1134
1182
|
this.mapCache = null; }).catch(() => { this.mapCache = null; });
|
|
1135
1183
|
}
|
|
1184
|
+
if (this.client.hasToken() && !this.groupMapCache) {
|
|
1185
|
+
this.groupMapCache = this.client.fetchGroupMemoryMap();
|
|
1186
|
+
this.groupMapCache.then((m) => { if (!m)
|
|
1187
|
+
this.groupMapCache = null; }).catch(() => { this.groupMapCache = null; });
|
|
1188
|
+
}
|
|
1136
1189
|
// NEVER block tool-listing on the network. A flaky/unreachable API would otherwise hang the MCP
|
|
1137
|
-
// handshake and freeze the whole agent ("connection timed out after 30000ms"). The
|
|
1138
|
-
// best-effort: cap the wait, and
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1190
|
+
// handshake and freeze the whole agent ("connection timed out after 30000ms"). The maps are
|
|
1191
|
+
// best-effort: cap the wait, and they'll be injected on the next listing once they resolve.
|
|
1192
|
+
// Both races share one wall-clock budget because they run concurrently.
|
|
1193
|
+
const [map, groupMap] = await Promise.all([
|
|
1194
|
+
capWait(this.mapCache),
|
|
1195
|
+
capWait(this.groupMapCache),
|
|
1196
|
+
]);
|
|
1142
1197
|
this.mapInjected = !!map;
|
|
1198
|
+
this.groupMapInjected = !!groupMap;
|
|
1143
1199
|
const updateNotice = formatUpdateNotice(this.updateStatus);
|
|
1144
|
-
return { tools: listToolSpecs({ map, updateNotice }) };
|
|
1200
|
+
return { tools: listToolSpecs({ map, groupMap, updateNotice }) };
|
|
1145
1201
|
});
|
|
1146
1202
|
this.server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
1147
1203
|
const canonicalName = resolveCanonicalToolName(request.params.name);
|
|
@@ -1168,6 +1224,7 @@ class EchoMemMCPServer {
|
|
|
1168
1224
|
type: "tool_call",
|
|
1169
1225
|
tool: canonicalName,
|
|
1170
1226
|
map_injected: this.mapInjected,
|
|
1227
|
+
group_map_injected: this.groupMapInjected,
|
|
1171
1228
|
};
|
|
1172
1229
|
try {
|
|
1173
1230
|
// The usage report is a local, $0 audit — works with no login (value before signup).
|
|
@@ -1251,6 +1308,8 @@ class EchoMemMCPServer {
|
|
|
1251
1308
|
return await this.handleOthers(request.params.arguments);
|
|
1252
1309
|
case canonicalToolNames.publicMemory:
|
|
1253
1310
|
return await this.handlePublicMemory(request.params.arguments);
|
|
1311
|
+
case canonicalToolNames.recordCitations:
|
|
1312
|
+
return await this.handleRecordMemoryCitations(request.params.arguments);
|
|
1254
1313
|
case canonicalToolNames.groupContext:
|
|
1255
1314
|
return await this.handleGroupContext(request.params.arguments);
|
|
1256
1315
|
case canonicalToolNames.getGroupSessionSharing:
|
|
@@ -1519,7 +1578,7 @@ Details: ${m.details || "N/A"}`)
|
|
|
1519
1578
|
const receipt = sharing?.hasGroup !== true
|
|
1520
1579
|
? "Saved to your private memory."
|
|
1521
1580
|
: sharing?.decision === null || sharing?.decision === undefined
|
|
1522
|
-
? `Saved to your private memory. Ask
|
|
1581
|
+
? `Saved to your private memory. Ask: “Share memories saved from this session with ${readString(sharingGroup, "name") ?? "your current group"}?” Silence leaves the state unset, so ask again at a later qualifying checkpoint until the user explicitly answers Yes or No; do not repeat the prompt in the same response.`
|
|
1523
1582
|
: sharing.decision === "private"
|
|
1524
1583
|
? "Saved to your private memory."
|
|
1525
1584
|
: sync?.synced === true
|
|
@@ -1876,6 +1935,28 @@ Details: ${m.details || "N/A"}`;
|
|
|
1876
1935
|
content: [{ type: "text", text: withMemoryCitationInstruction(text) }],
|
|
1877
1936
|
};
|
|
1878
1937
|
}
|
|
1938
|
+
async handleRecordMemoryCitations(args) {
|
|
1939
|
+
const parsed = recordMemoryCitationsSchema.parse(args ?? {});
|
|
1940
|
+
const payload = await this.client.recordMemoryCitations(parsed);
|
|
1941
|
+
const acceptedMemoryIds = Array.isArray(payload?.acceptedMemoryIds)
|
|
1942
|
+
? payload.acceptedMemoryIds.filter((id) => typeof id === "string")
|
|
1943
|
+
: [];
|
|
1944
|
+
const rejectedMemoryIds = Array.isArray(payload?.rejectedMemoryIds)
|
|
1945
|
+
? payload.rejectedMemoryIds.filter((id) => typeof id === "string")
|
|
1946
|
+
: [];
|
|
1947
|
+
const insertedCount = typeof payload?.insertedCount === "number" ? payload.insertedCount : 0;
|
|
1948
|
+
const duplicateCount = typeof payload?.duplicateCount === "number" ? payload.duplicateCount : 0;
|
|
1949
|
+
const text = [
|
|
1950
|
+
`Citation receipt accepted for ${acceptedMemoryIds.length} ${acceptedMemoryIds.length === 1 ? "memory" : "memories"}.`,
|
|
1951
|
+
`New citations recorded: ${insertedCount}.`,
|
|
1952
|
+
duplicateCount ? `Idempotent duplicates skipped: ${duplicateCount}.` : "",
|
|
1953
|
+
rejectedMemoryIds.length
|
|
1954
|
+
? `Rejected inaccessible or self-owned Memory IDs: ${rejectedMemoryIds.join(", ")}. Do not describe these as helped.`
|
|
1955
|
+
: "",
|
|
1956
|
+
"Now send the final answer with an EchoMem sources list containing the same accepted memories actually used.",
|
|
1957
|
+
].filter(Boolean).join("\n");
|
|
1958
|
+
return { content: [{ type: "text", text }] };
|
|
1959
|
+
}
|
|
1879
1960
|
async handleGroupContext(args) {
|
|
1880
1961
|
groupContextSchema.parse(args ?? {});
|
|
1881
1962
|
const payload = await this.client.getGroupContext(args);
|
|
@@ -1938,7 +2019,7 @@ Details: ${m.details || "N/A"}`;
|
|
|
1938
2019
|
return {
|
|
1939
2020
|
content: [{
|
|
1940
2021
|
type: "text",
|
|
1941
|
-
text: `No sharing decision exists for this session. Ask
|
|
2022
|
+
text: `No sharing decision exists for this session. Ask: “Share memories saved from this session with ${readString(group, "name") ?? "your group"}?” Silence is not a No: leave the state unset and ask again at a later qualifying checkpoint until the user explicitly answers Yes or No, without repeating the prompt in the same response. Then call set_group_session_sharing with that explicit answer; an explicit No stops later prompts for this exact session.`,
|
|
1942
2023
|
}],
|
|
1943
2024
|
};
|
|
1944
2025
|
}
|
|
@@ -2017,8 +2098,8 @@ Details: ${m.details || "N/A"}`;
|
|
|
2017
2098
|
content: [{
|
|
2018
2099
|
type: "text",
|
|
2019
2100
|
text: payload?.alreadyMember
|
|
2020
|
-
? `You are already a member of ${payload?.group?.name ?? "this group"}. No memories were published. Use prepare_group_publication to review memories and propose any missing title or responsibility fields, then call get_group_session_sharing and ask
|
|
2021
|
-
: `Joined ${payload?.group?.name ?? "the company group"}. No memories were published. Next use prepare_group_publication to review candidates, infer a proposed title and responsibility summary, and ask the user to confirm that profile together with the publication preview. Also call get_group_session_sharing; if unset, ask
|
|
2101
|
+
? `You are already a member of ${payload?.group?.name ?? "this group"}. No memories were published. Use prepare_group_publication to review memories and propose any missing title or responsibility fields, then call get_group_session_sharing and, if unset, ask again at later qualifying checkpoints until the user explicitly answers Yes or No.`
|
|
2102
|
+
: `Joined ${payload?.group?.name ?? "the company group"}. No memories were published. Next use prepare_group_publication to review candidates, infer a proposed title and responsibility summary, and ask the user to confirm that profile together with the publication preview. Also call get_group_session_sharing; if unset, ask whether memories saved from this session should be shared and ask again at a later qualifying checkpoint after silence until an explicit Yes or No.`,
|
|
2022
2103
|
}],
|
|
2023
2104
|
};
|
|
2024
2105
|
}
|
package/dist/package-metadata.js
CHANGED
|
@@ -23,14 +23,14 @@ export const MCP_PACKAGE_DESCRIPTION = stringOrFallback(packageJson.description,
|
|
|
23
23
|
export const MCP_PACKAGE_LABEL = `${MCP_PACKAGE_NAME}@${MCP_PACKAGE_VERSION}`;
|
|
24
24
|
export const MCP_UPDATE_COMMAND = `npx -y ${MCP_PACKAGE_NAME}@latest update`;
|
|
25
25
|
export const MCP_UPDATE_ALL_COMMAND = `${MCP_UPDATE_COMMAND} --all`;
|
|
26
|
-
export const MEMORY_CITATION_INSTRUCTION = 'If the user-facing answer materially relies on one or more EchoMem memories, end it with a compact "EchoMem sources:" list containing only the memories actually used. Use each memory key as the Markdown label and its canonical https://echoknows.com/memory/<memory-id> URL.
|
|
26
|
+
export const MEMORY_CITATION_INSTRUCTION = 'If the user-facing answer materially relies on one or more EchoMem memories, end it with a compact "EchoMem sources:" list containing only the memories actually used. For memories owned by teammates or accepted friends, call record_memory_citations immediately before the final answer with those exact Memory IDs. Do not cite memories that were merely retrieved. Use each memory key as the Markdown label and its canonical https://echoknows.com/memory/<memory-id> URL. Omit the sources section and citation receipt when no memory informed the answer.';
|
|
27
27
|
export const MCP_SERVER_INSTRUCTIONS = [
|
|
28
28
|
`${MCP_PACKAGE_DESCRIPTION} (${MCP_PACKAGE_LABEL}).`,
|
|
29
29
|
`If the user or local config expects a newer EchoMem MCP version than ${MCP_PACKAGE_VERSION}, update once with \`${MCP_UPDATE_ALL_COMMAND}\` and start a new MCP session.`,
|
|
30
30
|
"Use echomem_update_status to check whether npm has a newer bridge; it is cached and non-blocking during normal tool listing.",
|
|
31
31
|
"Do not auto-update on every MCP startup; this bridge is intentionally stable between explicit updates.",
|
|
32
32
|
"Before the final response for work that produced a durable decision, confirmed preference, implementation, resolved bug, commit, passing verification, release, or project milestone, call save_conversation. This is a private checkpoint and does not require a separate user request. Skip secrets, incidental chatter, and trivial read-only work. If an encrypted vault is locked, tell the user to run echomem-mcp unlock rather than silently skipping the checkpoint.",
|
|
33
|
-
"For a user with a company group, call get_group_session_sharing near session start or after
|
|
33
|
+
"For a user with a company group, call get_group_session_sharing near session start or after a qualifying private save. If this exact session has no decision, ask whether memories saved from the session should be shared with the current group. Silence is not a No: leave the state unset and ask again at a later qualifying checkpoint until the user explicitly answers Yes or No, without repeating the prompt in the same response. Store only the explicit answer with set_group_session_sharing. A Yes decision syncs eligible memories after each private save; a No decision keeps later saves private and stops later prompts for this exact session. Flagged memories stay private.",
|
|
34
34
|
"For company-group sharing, use get_group_context for orientation; create_memory_group/create_group_invite/join_memory_group for membership; and prepare_group_publication as a no-publication preview.",
|
|
35
35
|
"After joining or when profile fields are missing, use candidate memory evidence to propose a title and responsibility summary. Ask the user to confirm that proposal together with the publication preview, then call update_group_profile and complete_group_publication.",
|
|
36
36
|
"Use one canonical https://echoknows.com/memory/<memory-id> link for private, group, and friend evidence. Label it with the memory key; the site resolves the authorized representation.",
|
|
@@ -7,8 +7,8 @@ const CHECKPOINT_REASON = [
|
|
|
7
7
|
"Run EchoMem's private completion checkpoint before finishing this turn.",
|
|
8
8
|
"Review the work that just completed. If it produced a durable decision, confirmed preference, implementation, resolved bug, commit, passing verification, release, or project milestone, call `save_conversation` now with a concise structured summary of the outcome, reasoning, relevant files, and constraints.",
|
|
9
9
|
"Do not save secrets or incidental chatter. If the work was not durable, do not call the tool and finish normally.",
|
|
10
|
-
"Private persistence happens first. For a user with a company group, call `get_group_session_sharing` near session start or after
|
|
11
|
-
"If this exact session has no decision, ask
|
|
10
|
+
"Private persistence happens first. For a user with a company group, call `get_group_session_sharing` near session start or after a qualifying private save.",
|
|
11
|
+
"If this exact session has no decision, ask whether memories saved from the session should be shared with the current group. Silence leaves the state unset, so ask again at a later qualifying checkpoint until the user explicitly answers Yes or No; do not repeat the prompt in the same response. Call `set_group_session_sharing` only with that explicit answer. A Yes decision automatically syncs eligible memories after later private saves; a No decision keeps later saves private and stops later prompts for this exact session.",
|
|
12
12
|
"Flagged memories are withheld from automatic session sync and remain private.",
|
|
13
13
|
"If EchoMem reports that the encrypted vault is locked, tell the user to run `echomem-mcp unlock`; never silently skip a qualifying checkpoint.",
|
|
14
14
|
].join(" ");
|
package/dist/setup.js
CHANGED
|
@@ -391,7 +391,7 @@ function echomemGuidanceBlock() {
|
|
|
391
391
|
"- Before re-deriving past decisions, project context, or anything the user likely settled before: call `search_memories` first.",
|
|
392
392
|
'- If the final user-facing answer materially relies on one or more EchoMem memories, end it with a compact `EchoMem sources:` list containing only the memories actually used. Link each memory key to its canonical `https://echoknows.com/memory/<memory-id>` URL. Do not cite memories that were merely retrieved, and omit the section when no memory informed the answer.',
|
|
393
393
|
"- Before the final response for a task that produced a durable decision, confirmed preference, implementation, resolved bug, commit, passing verification, release, or project milestone: call `save_conversation`. This private checkpoint does not require a separate user request. Do not save secrets, credentials, incidental chatter, or trivial read-only work. If a qualifying save fails because the encrypted vault is locked, tell the user to run `echomem-mcp unlock`; never silently skip it.",
|
|
394
|
-
"- For a user with a company group, call `get_group_session_sharing` near session start or after
|
|
394
|
+
"- For a user with a company group, call `get_group_session_sharing` near session start or after a qualifying private save. If this exact session has no decision, ask whether memories saved from the session should be shared with the current group. Silence is not a No: leave the state unset and ask again at a later qualifying checkpoint until the user explicitly answers Yes or No, without repeating the prompt in the same response. Call `set_group_session_sharing` only with that explicit answer. A Yes decision syncs eligible memories after each private save; a No decision keeps later saves private and stops later prompts for this exact session.",
|
|
395
395
|
"- If the user pastes a session carryover/checkpoint: it may reference `get_checkpoint_by_context` — use it to pull the checkpoint/decision trail when you need more than the snapshot.",
|
|
396
396
|
"",
|
|
397
397
|
"### Company group memory",
|
package/dist/v1-contract.js
CHANGED
|
@@ -10,6 +10,7 @@ export const canonicalToolNames = {
|
|
|
10
10
|
sendFriendRequest: "send_friend_request",
|
|
11
11
|
others: "search_others_memories",
|
|
12
12
|
publicMemory: "get_public_memory",
|
|
13
|
+
recordCitations: "record_memory_citations",
|
|
13
14
|
groupContext: "get_group_context",
|
|
14
15
|
getGroupSessionSharing: "get_group_session_sharing",
|
|
15
16
|
setGroupSessionSharing: "set_group_session_sharing",
|
|
@@ -114,6 +115,11 @@ export const publicMemorySchema = z.object({
|
|
|
114
115
|
...triggerMetadataSchema,
|
|
115
116
|
memoryId: z.string().min(1),
|
|
116
117
|
});
|
|
118
|
+
export const recordMemoryCitationsSchema = z.object({
|
|
119
|
+
...triggerMetadataSchema,
|
|
120
|
+
memoryIds: z.array(z.string().uuid()).min(1).max(20),
|
|
121
|
+
receiptId: z.string().min(1).max(200),
|
|
122
|
+
});
|
|
117
123
|
export const groupContextSchema = z.object({
|
|
118
124
|
...triggerMetadataSchema,
|
|
119
125
|
});
|
|
@@ -201,6 +207,7 @@ export const getByContextSchema = z.object({
|
|
|
201
207
|
export function listToolSpecs(opts = {}) {
|
|
202
208
|
const currentTime = new Date().toISOString();
|
|
203
209
|
const map = opts.map?.trim();
|
|
210
|
+
const groupMap = opts.groupMap?.trim();
|
|
204
211
|
const updateNotice = opts.updateNotice?.trim();
|
|
205
212
|
const recallPlanNote = "Available on every plan: Free includes 100 searches each week, Pro includes 500, and Power includes 2,000.";
|
|
206
213
|
const searchBillingReplyInstruction = "If search returns an ACTION REQUIRED subscription message, tell the user to start their trial or subscription and include the exact URL from that result verbatim. Do not respond only with \"connect\" or \"upgrade\".";
|
|
@@ -209,6 +216,11 @@ export function listToolSpecs(opts = {}) {
|
|
|
209
216
|
const mapSection = map
|
|
210
217
|
? `\n\nThis user's EchoMem currently covers these topics (a relevance guide — recall when the task relates to one of them):\n${map}\n`
|
|
211
218
|
: "";
|
|
219
|
+
// Same device as the personal map, aimed at the group surface: the agent judges whether teammates
|
|
220
|
+
// have covered the topic before searching, instead of never calling the group tools at all.
|
|
221
|
+
const groupMapSection = groupMap
|
|
222
|
+
? `\n\nThis user's company group currently shares work in these areas (a relevance guide — search the group when the task relates to one of these people or topics):\n${groupMap}\n`
|
|
223
|
+
: "";
|
|
212
224
|
return [
|
|
213
225
|
{
|
|
214
226
|
name: canonicalToolNames.search,
|
|
@@ -248,7 +260,7 @@ export function listToolSpecs(opts = {}) {
|
|
|
248
260
|
},
|
|
249
261
|
{
|
|
250
262
|
name: canonicalToolNames.save,
|
|
251
|
-
description: "Save durable knowledge from this conversation into the user's private EchoMem (durable memories are extracted automatically). Call before the final response when work produced a durable decision, confirmed preference, implementation, resolved bug, commit, passing verification, release, or project milestone; this private checkpoint does not require a separate user request. Omit secrets, incidental chatter, and trivial read-only work. If the encrypted vault is locked, tell the user to run `echomem-mcp unlock` and never silently skip a qualifying checkpoint. Private persistence happens first. If this MCP session has confirmed group sharing, eligible memories are then synced to the current group automatically; flagged memories stay private. If the session has no decision yet, ask
|
|
263
|
+
description: "Save durable knowledge from this conversation into the user's private EchoMem (durable memories are extracted automatically). Call before the final response when work produced a durable decision, confirmed preference, implementation, resolved bug, commit, passing verification, release, or project milestone; this private checkpoint does not require a separate user request. Omit secrets, incidental chatter, and trivial read-only work. If the encrypted vault is locked, tell the user to run `echomem-mcp unlock` and never silently skip a qualifying checkpoint. Private persistence happens first. If this MCP session has confirmed group sharing, eligible memories are then synced to the current group automatically; flagged memories stay private. If the session has no decision yet, ask whether to share. Silence leaves it unset, so ask again at a later qualifying checkpoint until the user explicitly answers Yes or No; do not repeat the prompt in the same response. New extraction input uses the plan's weekly processing allowance; if the limit is reached, nothing is saved. passthrough=true stores the text verbatim as a session capsule.",
|
|
252
264
|
inputSchema: {
|
|
253
265
|
type: "object",
|
|
254
266
|
properties: {
|
|
@@ -385,7 +397,7 @@ export function listToolSpecs(opts = {}) {
|
|
|
385
397
|
},
|
|
386
398
|
{
|
|
387
399
|
name: canonicalToolNames.others,
|
|
388
|
-
description: `Search public memories from accepted friends or people who share your company group. For onboarding and division-of-work questions, call get_group_context first, then use this tool for current evidence. Returned memories are recorded in memory_views for the owners. ${memoryCitationInstruction}`,
|
|
400
|
+
description: `Search public memories from accepted friends or people who share your company group. For onboarding and division-of-work questions, call get_group_context first, then use this tool for current evidence. Returned memories are recorded in memory_views for the owners. ${memoryCitationInstruction}${groupMapSection}`,
|
|
389
401
|
inputSchema: {
|
|
390
402
|
type: "object",
|
|
391
403
|
properties: {
|
|
@@ -447,9 +459,37 @@ export function listToolSpecs(opts = {}) {
|
|
|
447
459
|
required: ["memoryId"],
|
|
448
460
|
},
|
|
449
461
|
},
|
|
462
|
+
{
|
|
463
|
+
name: canonicalToolNames.recordCitations,
|
|
464
|
+
description: "Call this tool immediately before the final answer when it materially uses memories owned by teammates or accepted friends. Include only the exact Memory IDs actually used. Do not cite memories that were merely retrieved. Create one opaque receiptId for the planned final answer and reuse it exactly if this tool call is retried. Do not include answer text, prompts, or secrets in receiptId.",
|
|
465
|
+
inputSchema: {
|
|
466
|
+
type: "object",
|
|
467
|
+
properties: {
|
|
468
|
+
memoryIds: {
|
|
469
|
+
type: "array",
|
|
470
|
+
minItems: 1,
|
|
471
|
+
maxItems: 20,
|
|
472
|
+
items: { type: "string", format: "uuid" },
|
|
473
|
+
description: "Exact IDs of teammate/friend memories materially used in the final answer.",
|
|
474
|
+
},
|
|
475
|
+
receiptId: {
|
|
476
|
+
type: "string",
|
|
477
|
+
minLength: 1,
|
|
478
|
+
maxLength: 200,
|
|
479
|
+
description: "Opaque per-answer idempotency key. Reuse the same value on retry; never include answer content.",
|
|
480
|
+
},
|
|
481
|
+
triggerMessage: {
|
|
482
|
+
type: "string",
|
|
483
|
+
description: "Optional user message that caused the answer. EchoMem stores only redacted analytics metadata.",
|
|
484
|
+
},
|
|
485
|
+
triggerMessageRole: { type: "string", default: "user" },
|
|
486
|
+
},
|
|
487
|
+
required: ["memoryIds", "receiptId"],
|
|
488
|
+
},
|
|
489
|
+
},
|
|
450
490
|
{
|
|
451
491
|
name: canonicalToolNames.groupContext,
|
|
452
|
-
description:
|
|
492
|
+
description: `Get your current company group, its participant directory, declared titles and responsibilities, and published-memory coverage. Use this before answering who works on what or suggesting where a new group member could contribute. Treat declared profile fields as facts and memory-derived work as evidence or inference. Session sharing is separate; call get_group_session_sharing instead of inferring it.${groupMapSection}`,
|
|
453
493
|
inputSchema: {
|
|
454
494
|
type: "object",
|
|
455
495
|
properties: {
|
|
@@ -463,7 +503,7 @@ export function listToolSpecs(opts = {}) {
|
|
|
463
503
|
},
|
|
464
504
|
{
|
|
465
505
|
name: canonicalToolNames.getGroupSessionSharing,
|
|
466
|
-
description: "Read the confirmed sharing decision for this exact MCP session. Call near session start or after
|
|
506
|
+
description: "Read the confirmed sharing decision for this exact MCP session. Call near session start or after a qualifying private save. If the user has a group and no decision exists, ask: “Share memories saved from this session with <group>?” Silence leaves the state unset; ask again at a later qualifying checkpoint until the user explicitly answers Yes or No, but never repeat the prompt in the same response or infer the answer. An explicit No stops later prompts for this exact session.",
|
|
467
507
|
inputSchema: {
|
|
468
508
|
type: "object",
|
|
469
509
|
properties: {
|
|
@@ -512,7 +552,7 @@ export function listToolSpecs(opts = {}) {
|
|
|
512
552
|
},
|
|
513
553
|
{
|
|
514
554
|
name: canonicalToolNames.joinGroup,
|
|
515
|
-
description: "Join a company memory group using an invite code after the user explicitly asks to join. Joining never publishes memories. Next call prepare_group_publication, infer a proposed title and responsibility summary from the user's own candidate memories, and ask the user to confirm the profile together with the publication preview. Also call get_group_session_sharing and, if unset, ask
|
|
555
|
+
description: "Join a company memory group using an invite code after the user explicitly asks to join. Joining never publishes memories. Next call prepare_group_publication, infer a proposed title and responsibility summary from the user's own candidate memories, and ask the user to confirm the profile together with the publication preview. Also call get_group_session_sharing and, if unset, ask whether memories saved from this session should be shared. Silence stays unset and should be prompted again at a later qualifying checkpoint until an explicit Yes or No.",
|
|
516
556
|
inputSchema: {
|
|
517
557
|
type: "object",
|
|
518
558
|
properties: {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@echomem/mcp",
|
|
3
|
-
"version": "1.4.
|
|
3
|
+
"version": "1.4.31",
|
|
4
4
|
"description": "EchoMem MCP bridge: cloud-first memory tools, local context HUD, and the Agent Doctor workspace forensics report (cost ledger + 3D repo city)",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"type": "module",
|
|
@@ -27,7 +27,7 @@
|
|
|
27
27
|
"test:registry-ui": "npm run build && node test/registry-ui.test.mjs",
|
|
28
28
|
"test:ui": "npm run build && node test/setup-ui.test.mjs",
|
|
29
29
|
"test:billing-ui": "npm run build && node test/setup-ui.test.mjs billing",
|
|
30
|
-
"test": "npm run build && node test/local-data-paths.test.mjs && node test/crypto.test.mjs && node test/integration.test.mjs && node test/local-auth.test.mjs && node test/retrieval-only.test.mjs && node test/no-restart.test.mjs && node test/report.test.mjs && node test/forensics.test.mjs && node test/canonical-golden.test.mjs && node test/tools.test.mjs && node test/update-check.test.mjs && node test/delete.test.mjs && node test/low-touch-tools.test.mjs && node test/migrate.test.mjs && node test/restart-recovery.test.mjs && node test/hud.test.mjs && node test/save-checkpoint-hook.test.mjs",
|
|
30
|
+
"test": "npm run build && node test/local-data-paths.test.mjs && node test/crypto.test.mjs && node test/integration.test.mjs && node test/local-auth.test.mjs && node test/retrieval-only.test.mjs && node test/no-restart.test.mjs && node test/report.test.mjs && node test/forensics.test.mjs && node test/canonical-golden.test.mjs && node test/tools.test.mjs && node test/group-map.test.mjs && node test/update-check.test.mjs && node test/delete.test.mjs && node test/low-touch-tools.test.mjs && node test/migrate.test.mjs && node test/restart-recovery.test.mjs && node test/hud.test.mjs && node test/save-checkpoint-hook.test.mjs",
|
|
31
31
|
"prepack": "npm run build && node scripts/bundle-city.mjs"
|
|
32
32
|
},
|
|
33
33
|
"dependencies": {
|
|
@@ -18,7 +18,10 @@ Use the `save_conversation` tool from the `echomem` MCP server.
|
|
|
18
18
|
read-only work and incidental chatter.
|
|
19
19
|
7. If the encrypted vault is locked, tell the user to run `echomem-mcp unlock`; never silently skip a
|
|
20
20
|
qualifying checkpoint.
|
|
21
|
-
8. For a user with a company group, call `get_group_session_sharing` near session start or after
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
21
|
+
8. For a user with a company group, call `get_group_session_sharing` near session start or after a
|
|
22
|
+
qualifying private save. If unset, ask whether memories saved from this session should be shared.
|
|
23
|
+
Silence is not a No: leave the state unset and ask again at a later qualifying checkpoint until the
|
|
24
|
+
user explicitly answers Yes or No, without repeating the prompt in the same response. Then call
|
|
25
|
+
`set_group_session_sharing` with that explicit answer. A Yes decision syncs eligible memories after
|
|
26
|
+
private persistence; a No decision stops later prompts for this exact session; flagged memories
|
|
27
|
+
remain private.
|
|
@@ -18,8 +18,10 @@ This project has EchoMem connected — the user's long-term memory across all th
|
|
|
18
18
|
`save_conversation`. This private checkpoint does not require a separate user request. Save a
|
|
19
19
|
concise structured outcome rather than the full transcript. Never save secrets, credentials,
|
|
20
20
|
incidental chatter, or trivial read-only work. This does not authorize company-group publication.
|
|
21
|
-
- For a user with a company group, call `get_group_session_sharing` near session start or after
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
21
|
+
- For a user with a company group, call `get_group_session_sharing` near session start or after a
|
|
22
|
+
qualifying private save. If this exact session has no decision, ask whether memories saved from it
|
|
23
|
+
should be shared. Silence is not a No: leave the state unset and ask again at a later qualifying
|
|
24
|
+
checkpoint until the user explicitly answers Yes or No, without repeating the prompt in the same
|
|
25
|
+
response. Store only the explicit answer with `set_group_session_sharing`. A Yes decision syncs
|
|
26
|
+
eligible memories after private persistence; a No decision stops later prompts for this exact
|
|
27
|
+
session; flagged memories stay private.
|