@echomem/mcp 1.4.32 → 1.4.34

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 CHANGED
@@ -10,7 +10,7 @@ This MCP Server bridges local tools and your EchoMem Cloud API entirely via auth
10
10
  - `save_conversation`: Connects to `POST /api/extension/memories/ingest`
11
11
  - `get_memories_by_time_range`: Connects to `POST /api/extension/memories/time-range`
12
12
  - `search_memories_by_keywords`: Connects to `POST /api/extension/memories/keywords`
13
- - `search_others_memories`: Connects to MemoryFeed public search without sending the authenticated Echo user id
13
+ - `search_others_memories`: Connects to MemoryFeed public search without trusting a model-supplied identity; the hosted API resolves the caller from the EchoMem credential and the tool returns that authenticated viewer explicitly
14
14
  - `delete_memory`: Previews one personal memory and returns a confirmation token; only deletes after a second confirmed call
15
15
 
16
16
  No direct access to the `IndexedDB` or local files is required.
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, recordMemoryCitationsSchema, 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, 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";
@@ -1013,7 +1013,12 @@ class EchoMemApiClient {
1013
1013
  async recordMemoryCitations(args) {
1014
1014
  const parsed = recordMemoryCitationsSchema.parse(args ?? {});
1015
1015
  try {
1016
- const response = await this.axios.post("/api/extension/social/memory-citations", { ...parsed, sessionKey: this.sessionId });
1016
+ const response = await this.axios.post("/api/extension/social/memory-citations",
1017
+ // A citation receipt belongs to one planned answer, not to the lifetime of the MCP stdio
1018
+ // process. Deriving the legacy API's sessionKey from the required per-answer receipt keeps
1019
+ // retries idempotent while avoiding the same process-vs-conversation coupling that broke
1020
+ // group sharing in long-lived Claude hosts.
1021
+ { ...parsed, sessionKey: `answer:${parsed.receiptId}` });
1017
1022
  return response.data;
1018
1023
  }
1019
1024
  catch (error) {
@@ -1320,6 +1325,8 @@ class EchoMemMCPServer {
1320
1325
  return await this.handleGroupContext(request.params.arguments);
1321
1326
  case canonicalToolNames.getGroupSessionSharing:
1322
1327
  return await this.handleGetGroupSessionSharing(request.params.arguments);
1328
+ case canonicalToolNames.requestGroupSessionSharing:
1329
+ return await this.handleRequestGroupSessionSharing(request.params.arguments);
1323
1330
  case canonicalToolNames.setGroupSessionSharing:
1324
1331
  return await this.handleSetGroupSessionSharing(request.params.arguments);
1325
1332
  case canonicalToolNames.createGroup:
@@ -1607,7 +1614,7 @@ Details: ${m.details || "N/A"}`)
1607
1614
  syncedGroupNames.length
1608
1615
  ? `Saved privately first and synced to approved groups: ${syncedGroupNames.join(", ")}.`
1609
1616
  : "Saved to your private memory.",
1610
- `This user has multiple groups: ${availableGroups.map((group) => `${readString(group, "name") ?? "Unnamed group"} (${readString(group, "id") ?? "unknown id"})`).join(", ")}. Call get_group_session_sharing with this conversation scope and one groupId at a time; obtain a separate explicit Yes/No decision for each group.`,
1617
+ `This user has multiple groups: ${availableGroups.map((group) => `${readString(group, "name") ?? "Unnamed group"} (${readString(group, "id") ?? "unknown id"})`).join(", ")}. Call request_group_session_sharing with this conversation scope and one groupId at a time so supported hosts render a separate sharing choice for each group.`,
1611
1618
  failedSyncs.length
1612
1619
  ? `${failedSyncs.length} approved group sync(s) failed; private memories were preserved.`
1613
1620
  : "",
@@ -1616,7 +1623,7 @@ Details: ${m.details || "N/A"}`)
1616
1623
  : "",
1617
1624
  ].filter(Boolean).join(" ")
1618
1625
  : sharing?.decision === null || sharing?.decision === undefined
1619
- ? `Saved to your private memory. Ask: “Share memories saved from this conversation 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.`
1626
+ ? `Saved to your private memory. Call request_group_session_sharing with this conversation scope${readString(sharingGroup, "id") ? ` and groupId ${readString(sharingGroup, "id")}` : ""} so a supported host can show the sharing choice for ${readString(sharingGroup, "name") ?? "your current group"}. If the client cannot render it, relay that tool's fallback question. Decline, cancel, or silence leaves the state unset.`
1620
1627
  : sharing.decision === "private"
1621
1628
  ? "Saved to your private memory."
1622
1629
  : syncedGroupNames.length
@@ -1646,7 +1653,7 @@ Details: ${m.details || "N/A"}`)
1646
1653
  `Successfully ingested conversation. Extracted ${memoriesExtracted} memory distinct events.`,
1647
1654
  receipt,
1648
1655
  typeof groupSharingScopeId === "string" && groupSharingScopeId
1649
- ? `Conversation sharing scope: ${groupSharingScopeId}\nReuse this exact groupSharingScopeId only for get_group_session_sharing, set_group_session_sharing, and save_conversation calls in this conversation. Never reuse it in another conversation or save it as memory.`
1656
+ ? `Conversation sharing scope: ${groupSharingScopeId}\nReuse this exact groupSharingScopeId only for get_group_session_sharing, request_group_session_sharing, set_group_session_sharing, and save_conversation calls in this conversation. Never reuse it in another conversation or save it as memory.`
1650
1657
  : "",
1651
1658
  typeof memoriesDiscarded === "number" && memoriesDiscarded > 0
1652
1659
  ? `${memoriesDiscarded} additional memories were not stored because your active-memory limit was reached.`
@@ -1800,6 +1807,22 @@ Details: ${m.details || "N/A"}`)
1800
1807
  const parsed = othersSchema.parse(args ?? {});
1801
1808
  const payload = await this.client.searchOthersMemories(args);
1802
1809
  const memories = payload?.memories ?? [];
1810
+ const authenticatedViewer = isRecord(payload?.authenticatedViewer)
1811
+ ? payload.authenticatedViewer
1812
+ : null;
1813
+ const authenticatedUserId = authenticatedViewer
1814
+ ? readString(authenticatedViewer, "userId")
1815
+ : null;
1816
+ const authenticatedDisplayName = authenticatedViewer
1817
+ ? readString(authenticatedViewer, "displayName")
1818
+ : null;
1819
+ const authenticatedIdentity = authenticatedUserId
1820
+ ? [
1821
+ `Authenticated EchoMem user: ${authenticatedDisplayName ?? "Unknown display name"} (User ID: ${authenticatedUserId}).`,
1822
+ "This identity comes from the EchoMem credential and is authoritative for this tool call.",
1823
+ "EchoMem has already excluded only this authenticated user's own memories. Return every memory below; do not filter again using a Claude account, host profile, git identity, or inferred identity.",
1824
+ ].join("\n")
1825
+ : "EchoMem has already applied credential-based self filtering. Do not filter the returned memories again using a host account, profile, git identity, or inferred identity.";
1803
1826
  if (!memories.length) {
1804
1827
  const scope = parsed.ownerUserId
1805
1828
  ? ` for peer ${parsed.ownerUserId}`
@@ -1814,7 +1837,7 @@ Details: ${m.details || "N/A"}`)
1814
1837
  : "";
1815
1838
  const queryLabel = parsed.query?.trim() ? ` matching the query: ${parsed.query}` : "";
1816
1839
  return {
1817
- content: [{ type: "text", text: `No others' public memories found${scope}${queryLabel}` }],
1840
+ content: [{ type: "text", text: `${authenticatedIdentity}\n\nNo others' public memories found${scope}${queryLabel}` }],
1818
1841
  };
1819
1842
  }
1820
1843
  const metadata = [
@@ -1848,7 +1871,7 @@ Details: ${m.details || "N/A"}`;
1848
1871
  content: [
1849
1872
  {
1850
1873
  type: "text",
1851
- text: withMemoryCitationInstruction(`Found ${memories.length} others' public memories${metadata ? ` (${metadata})` : ""}:\n\n${formattedResults}`),
1874
+ text: withMemoryCitationInstruction(`${authenticatedIdentity}\n\nFound ${memories.length} others' public memories${metadata ? ` (${metadata})` : ""}:\n\n${formattedResults}`),
1852
1875
  },
1853
1876
  ],
1854
1877
  };
@@ -2033,6 +2056,9 @@ Details: ${m.details || "N/A"}`;
2033
2056
  const text = [
2034
2057
  `Company group: ${readString(group, "name") ?? "Unnamed group"}`,
2035
2058
  readString(group, "description") ? `Description: ${readString(group, "description")}` : "",
2059
+ currentParticipant
2060
+ ? `Authenticated EchoMem member: ${readString(currentParticipant, "displayName") ?? "Unknown display name"} (User ID: ${readString(currentParticipant, "userId") ?? "unknown"}). This identity comes from the EchoMem credential and overrides any conflicting host-account, Claude-profile, git, or inferred identity.`
2061
+ : "EchoMem credential identity is authoritative for this tool; do not substitute a host-account, Claude-profile, git, or inferred identity.",
2036
2062
  `Coverage: ${coveredCount} of ${participantCount} participants have published memories (${totalPublished} total).`,
2037
2063
  "",
2038
2064
  participantText,
@@ -2071,7 +2097,7 @@ Details: ${m.details || "N/A"}`;
2071
2097
  return {
2072
2098
  content: [{
2073
2099
  type: "text",
2074
- text: `${scopeText}\n\nThis user belongs to multiple groups: ${choices}. Reuse this same conversation scope and call get_group_session_sharing once per groupId. Ask for a separate explicit Yes/No sharing decision for each selected group.`,
2100
+ text: `${scopeText}\n\nThis user belongs to multiple groups: ${choices}. Reuse this same conversation scope and call request_group_session_sharing once per groupId so supported hosts render a separate sharing choice for each group.`,
2075
2101
  }],
2076
2102
  };
2077
2103
  }
@@ -2081,7 +2107,7 @@ Details: ${m.details || "N/A"}`;
2081
2107
  return {
2082
2108
  content: [{
2083
2109
  type: "text",
2084
- text: `${scopeText}\n\nNo sharing decision exists for this conversation. Ask: “Share memories saved from this conversation 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 and the scope above; an explicit No stops later prompts for this conversation.`,
2110
+ text: `${scopeText}\n\nNo sharing decision exists for this conversation. Call request_group_session_sharing with the scope above${readString(group, "id") ? ` and groupId ${readString(group, "id")}` : ""}. Supported hosts will show a native Share with team / Keep private choice. If the client cannot render it, relay that tool's fallback question. Decline, cancel, or silence is not a No and leaves the state unset.`,
2085
2111
  }],
2086
2112
  };
2087
2113
  }
@@ -2097,13 +2123,23 @@ Details: ${m.details || "N/A"}`;
2097
2123
  }],
2098
2124
  };
2099
2125
  }
2100
- async handleSetGroupSessionSharing(args) {
2101
- const parsed = setGroupSessionSharingSchema.parse(args ?? {});
2102
- const payload = await this.client.setGroupSessionSharing(parsed);
2103
- const sync = isRecord(payload?.sync) ? payload.sync : null;
2126
+ sharingScopeText(groupSharingScopeId) {
2127
+ return groupSharingScopeId
2128
+ ? `Conversation sharing scope: ${groupSharingScopeId}\nReuse this exact groupSharingScopeId only in this conversation for later get/request/set/save calls. Never persist it as memory.`
2129
+ : "";
2130
+ }
2131
+ sharingFallbackText(scopeText, groupName) {
2132
+ return [
2133
+ scopeText,
2134
+ `This client cannot show EchoMem's sharing choice UI. Ask: “Share eligible memories saved from this conversation with ${groupName}?” Only an explicit Yes or No may be passed to set_group_session_sharing. Decline, cancel, or silence leaves the state unset.`,
2135
+ ].filter(Boolean).join("\n\n");
2136
+ }
2137
+ formatGroupSessionSharingUpdate(share, payload, scopeText = "") {
2138
+ const data = isRecord(payload) ? payload : {};
2139
+ const sync = isRecord(data.sync) ? data.sync : null;
2104
2140
  const protectedIds = Array.isArray(sync?.protectedMemoryIds) ? sync.protectedMemoryIds : [];
2105
- const group = isRecord(payload?.group) ? payload.group : {};
2106
- const receipt = parsed.share
2141
+ const group = isRecord(data.group) ? data.group : {};
2142
+ const receipt = share
2107
2143
  ? sync?.synced === false
2108
2144
  ? "Conversation sharing is enabled, but the initial group sync failed. Private memories were preserved; retry before claiming publication."
2109
2145
  : `Conversation sharing is enabled for ${readString(group, "name") ?? "the current group"}. Existing eligible memories in this scope were synced and later saves carrying the same scope will sync automatically.`
@@ -2111,10 +2147,127 @@ Details: ${m.details || "N/A"}`;
2111
2147
  return {
2112
2148
  content: [{
2113
2149
  type: "text",
2114
- text: `${receipt}${protectedIds.length ? ` ${protectedIds.length} flagged ${protectedIds.length === 1 ? "memory was" : "memories were"} protected and kept private.` : ""}`,
2150
+ text: [
2151
+ scopeText,
2152
+ `${receipt}${protectedIds.length ? ` ${protectedIds.length} flagged ${protectedIds.length === 1 ? "memory was" : "memories were"} protected and kept private.` : ""}`,
2153
+ ].filter(Boolean).join("\n\n"),
2115
2154
  }],
2116
2155
  };
2117
2156
  }
2157
+ async handleRequestGroupSessionSharing(args) {
2158
+ const parsed = requestGroupSessionSharingSchema.parse(args ?? {});
2159
+ const payload = await this.client.getGroupSessionSharing(parsed);
2160
+ const groupSharingScopeId = readString(payload ?? {}, "groupSharingScopeId");
2161
+ const scopeText = this.sharingScopeText(groupSharingScopeId);
2162
+ if (!groupSharingScopeId) {
2163
+ return {
2164
+ content: [{
2165
+ type: "text",
2166
+ text: "EchoMem could not establish a conversation sharing scope, so no decision was recorded and the checkpoint remains private. Retry request_group_session_sharing before offering team sharing.",
2167
+ }],
2168
+ };
2169
+ }
2170
+ if (payload?.hasGroup !== true) {
2171
+ return {
2172
+ content: [{
2173
+ type: "text",
2174
+ text: "No company group is configured for this user. Saves remain private; do not ask about conversation sharing.",
2175
+ }],
2176
+ };
2177
+ }
2178
+ if (payload?.requiresGroupSelection === true) {
2179
+ const availableGroups = Array.isArray(payload?.availableGroups)
2180
+ ? payload.availableGroups.filter(isRecord)
2181
+ : [];
2182
+ const choices = availableGroups
2183
+ .map((group) => `${readString(group, "name") ?? "Unnamed group"} (${readString(group, "id") ?? "unknown id"})`)
2184
+ .join(", ");
2185
+ return {
2186
+ content: [{
2187
+ type: "text",
2188
+ text: `${scopeText}\n\nChoose one group and call request_group_session_sharing again with the same scope and its groupId: ${choices}. Each group has an independent decision.`,
2189
+ }],
2190
+ };
2191
+ }
2192
+ const group = isRecord(payload?.group) ? payload.group : {};
2193
+ const groupName = readString(group, "name") ?? "your group";
2194
+ const existingDecision = typeof payload?.decision === "string" ? payload.decision : null;
2195
+ if (existingDecision === "share" || existingDecision === "private") {
2196
+ return {
2197
+ content: [{
2198
+ type: "text",
2199
+ text: [
2200
+ scopeText,
2201
+ existingDecision === "share"
2202
+ ? `This conversation is already approved for ${groupName}. Eligible memories sync automatically; flagged memories stay private.`
2203
+ : `This conversation is already private for ${groupName}. No sharing prompt was shown.`,
2204
+ ].filter(Boolean).join("\n\n"),
2205
+ }],
2206
+ };
2207
+ }
2208
+ const formElicitation = this.server.getClientCapabilities()?.elicitation?.form;
2209
+ if (!formElicitation) {
2210
+ return {
2211
+ content: [{ type: "text", text: this.sharingFallbackText(scopeText, groupName) }],
2212
+ };
2213
+ }
2214
+ let elicitation;
2215
+ try {
2216
+ elicitation = await this.server.elicitInput({
2217
+ mode: "form",
2218
+ message: `EchoMem saved this checkpoint privately. Choose whether eligible memories from this conversation should also be shared with ${groupName}. Flagged memories always remain private.`,
2219
+ requestedSchema: {
2220
+ type: "object",
2221
+ properties: {
2222
+ sharingDecision: {
2223
+ type: "string",
2224
+ title: "Team sharing",
2225
+ description: `Choose whether eligible conversation memories may be shared with ${groupName}.`,
2226
+ oneOf: [
2227
+ { const: "share", title: "Share with team" },
2228
+ { const: "private", title: "Keep private" },
2229
+ ],
2230
+ },
2231
+ },
2232
+ required: ["sharingDecision"],
2233
+ },
2234
+ });
2235
+ }
2236
+ catch {
2237
+ return {
2238
+ content: [{ type: "text", text: this.sharingFallbackText(scopeText, groupName) }],
2239
+ };
2240
+ }
2241
+ if (elicitation.action !== "accept") {
2242
+ return {
2243
+ content: [{
2244
+ type: "text",
2245
+ text: [
2246
+ scopeText,
2247
+ `No sharing decision was recorded for ${groupName}. The checkpoint remains private. Ask again at a later qualifying checkpoint; never interpret decline or cancel as No.`,
2248
+ ].filter(Boolean).join("\n\n"),
2249
+ }],
2250
+ };
2251
+ }
2252
+ const sharingDecision = readString(elicitation.content ?? {}, "sharingDecision");
2253
+ if (sharingDecision !== "share" && sharingDecision !== "private") {
2254
+ return {
2255
+ content: [{ type: "text", text: this.sharingFallbackText(scopeText, groupName) }],
2256
+ };
2257
+ }
2258
+ const updatePayload = await this.client.setGroupSessionSharing({
2259
+ ...parsed,
2260
+ groupSharingScopeId,
2261
+ share: sharingDecision === "share",
2262
+ confirmed: true,
2263
+ });
2264
+ return this.formatGroupSessionSharingUpdate(sharingDecision === "share", updatePayload, scopeText);
2265
+ }
2266
+ async handleSetGroupSessionSharing(args) {
2267
+ const parsed = setGroupSessionSharingSchema.parse(args ?? {});
2268
+ const payload = await this.client.setGroupSessionSharing(parsed);
2269
+ return this.formatGroupSessionSharingUpdate(parsed.share, payload);
2270
+ }
2118
2271
  async handlePublishToGroup(args) {
2119
2272
  const parsed = publishToGroupSchema.parse(args ?? {});
2120
2273
  const payload = await this.client.publishMemoryToGroup(args);
@@ -2163,8 +2316,8 @@ Details: ${m.details || "N/A"}`;
2163
2316
  content: [{
2164
2317
  type: "text",
2165
2318
  text: payload?.alreadyMember
2166
- ? `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.`
2167
- : `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 without a scope to mint one for this conversation; if unset, ask whether memories saved from this conversation should be shared and ask again at a later qualifying checkpoint after silence until an explicit Yes or No.`,
2319
+ ? `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 request_group_session_sharing so supported hosts render the native sharing choice. Decline or cancel stays unset for a later checkpoint.`
2320
+ : `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 request_group_session_sharing without a scope so supported hosts show the native team-sharing choice. Decline or cancel leaves consent unset for a later checkpoint.`,
2168
2321
  }],
2169
2322
  };
2170
2323
  }
@@ -27,19 +27,15 @@ export const MEMORY_CITATION_INSTRUCTION = 'If the user-facing answer materially
27
27
  export const SAVED_MEMORY_RECEIPT_INSTRUCTION = 'After save_conversation succeeds and returns one or more saved memory IDs, make the save visible in the final user-facing answer with a compact "EchoMem saved:" list containing every memory created by that call. Use each memory key as the Markdown label and its canonical https://echoknows.com/memory/<memory-id> URL. This save receipt is separate from "EchoMem sources:" and does not imply that the newly saved memories informed the answer.';
28
28
  export const MCP_SERVER_INSTRUCTIONS = [
29
29
  `${MCP_PACKAGE_DESCRIPTION} (${MCP_PACKAGE_LABEL}).`,
30
- `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.`,
31
- "Use echomem_update_status to check whether npm has a newer bridge; it is cached and non-blocking during normal tool listing.",
32
- "Do not auto-update on every MCP startup; this bridge is intentionally stable between explicit updates.",
33
- "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.",
34
- "For a user with a company group, call get_group_session_sharing near conversation start or after a qualifying private save. On the first call in a conversation, omit groupSharingScopeId so EchoMem mints a fresh opaque scope. Reuse that exact scope only in this conversation for later get/set/save calls, and never persist it as memory. If multiple groups are returned, reuse the same scope and check one groupId at a time. Each group requires its own explicit Yes/No decision. Silence leaves that group's state unset; never infer an answer or repeat the prompt in the same response. Store only explicit answers with set_group_session_sharing, the same scope, and the selected groupId. Saves sync eligible memories to every approved group; a No keeps them private for that group. Flagged memories stay private.",
35
- "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.",
36
- "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.",
37
- "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.",
38
- "Each search result is one memory: preserve its Memory ID and canonical echoknows.com link when citing it.",
39
- MEMORY_CITATION_INSTRUCTION,
40
- SAVED_MEMORY_RECEIPT_INSTRUCTION,
41
- "During a publication preview, if an unflagged candidate appears sensitive, proactively ask whether the user wants to mark its exact ID for publication attention first. Explain that marking does not publish or change encryption; it means the agent will call it out and ask for detailed confirmation whenever a later publication includes it. Never auto-flag inferred sensitivity. For sensitive-topic flags, search and preview exact owned memories before confirmed flag_memories_for_publication_attention. Separate already-flagged candidates, state that nothing has been published yet, and offer to exclude them, review them separately, or first search for and mark similar sensitive owned memories.",
42
- "Never save an inferred group profile. Manual prepared publication requires explicit preview confirmation; flagged memories still require separate exact-memory confirmation. Never store or log an echo_grp_ invite code.",
30
+ `If this bridge is stale, update once with \`${MCP_UPDATE_ALL_COMMAND}\` and start a new MCP session; never auto-update at startup.`,
31
+ "Before re-deriving prior decisions or preferences, use search_memories.",
32
+ "Before the final response for a durable decision, implementation, fix, commit, passing verification, release, or milestone, call save_conversation. Skip secrets and trivial work. If the encrypted vault is locked, tell the user to run echomem-mcp unlock.",
33
+ "After a successful save, show every memory created by that call in a compact EchoMem saved: list with canonical links; this is separate from \"EchoMem sources:\".",
34
+ "For company groups, call request_group_session_sharing near conversation start or after a qualifying save. It renders a native choice on clients with MCP elicitation and returns a text fallback otherwise. Omit groupSharingScopeId only on the first call, then reuse the returned scope only in this conversation. Each group needs an explicit choice; decline, cancel, or silence stays unset. Never infer consent. Flagged memories stay private.",
35
+ "EchoMem credential identity is authoritative over Claude profiles, host accounts, git identity, or inference. Use get_group_context before group-orientation answers and never re-filter owners returned by search_others_memories.",
36
+ "When a final answer materially uses teammate or friend memories, call record_memory_citations with only the exact used Memory IDs and a unique per-answer receiptId.",
37
+ "End memory-informed answers with a compact EchoMem sources: list. Use each memory key as the label and https://echoknows.com/memory/<memory-id> as its canonical link.",
38
+ "Group profiles, conversation sharing, publication, sensitive-memory flags, and deletion require explicit user confirmation. Never store or log an echo_grp_ invite code.",
43
39
  ].join(" ");
44
40
  export function withMcpVersion(description) {
45
41
  return `${description}\n\nEchoMem MCP bridge: ${MCP_PACKAGE_LABEL}. If this version is stale, update once with \`${MCP_UPDATE_ALL_COMMAND}\` (or add \`--client cursor|windsurf|claude-desktop|claude-code|codex\` for a single client), then start a new MCP session. Do not run updates repeatedly or on every startup.`;
@@ -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 conversation start or after a qualifying private save.",
11
- "On the first get_group_session_sharing call in a conversation, omit groupSharingScopeId so EchoMem mints a fresh opaque scope. Reuse that exact scope only in this conversation for later get/set/save calls, and never persist it as memory. If multiple groups are returned, reuse the same scope and check one groupId at a time. Each group needs its own explicit Yes/No decision. Silence leaves that group's state unset; do not infer an answer or repeat the prompt in the same response. A Yes decision syncs eligible memories from later saves carrying the same scope to that group; saves automatically sync to every approved group. A No decision keeps them private for that group.",
10
+ "Private persistence happens first. For a user with a company group, call `request_group_session_sharing` near conversation start or after a qualifying private save.",
11
+ "On the first request_group_session_sharing call in a conversation, omit groupSharingScopeId so EchoMem mints a fresh opaque scope. Reuse that exact scope only in this conversation for later get/request/set/save calls, and never persist it as memory. If multiple groups are returned, reuse the same scope and call once per groupId. Supported clients render a native Share with team / Keep private choice; if the tool returns a text fallback, relay its exact question and call set_group_session_sharing only after an explicit Yes/No. Decline, cancel, or silence leaves that group's state unset. A Share decision syncs eligible memories from later saves carrying the same scope to that group; saves automatically sync to every approved group. Keep private keeps them private for that group.",
12
12
  "Flagged memories are withheld from automatic conversation 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
@@ -218,13 +218,15 @@ export function knownClients() {
218
218
  { id: "codex", label: "Codex", kind: "command", detectDir: codexHome(), configPath: path.join(codexHome(), "config.toml"), note: "add to ~/.codex/config.toml under [mcp_servers.echomem]" },
219
219
  ];
220
220
  }
221
- /** A client is "present" if its config dir already exists (JSON) — a cheap heuristic for detection. */
221
+ /** A client is "present" if its config dir already exists — a cheap, side-effect-free heuristic. */
222
222
  export function detectClients() {
223
223
  return knownClients().filter((c) => {
224
224
  if (c.kind === "json")
225
225
  return fs.existsSync(path.dirname(c.configPath));
226
226
  if (c.kind === "command")
227
227
  return fs.existsSync(c.detectDir);
228
+ if (c.id === "claude-code")
229
+ return fs.existsSync(home(".claude"));
228
230
  return false;
229
231
  });
230
232
  }
@@ -392,7 +394,7 @@ function echomemGuidanceBlock() {
392
394
  '- 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
395
  "- 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
396
  '- After `save_conversation` succeeds and returns one or more saved memory IDs, make the save visible in the final user-facing answer with a compact `EchoMem saved:` list containing every memory created by that call. Link each memory key to its canonical `https://echoknows.com/memory/<memory-id>` URL. This save receipt is separate from `EchoMem sources:` and does not imply the newly saved memories informed the answer.',
395
- "- For a user with a company group, call `get_group_session_sharing` near conversation start or after a qualifying private save. On the first call in a conversation, omit `groupSharingScopeId` so EchoMem mints a fresh opaque scope. Reuse that exact scope only in this conversation for later get/set/save calls, and never persist it as memory. If multiple groups are returned, reuse the same scope and check one `groupId` at a time. Each group requires its own explicit Yes/No decision. Silence leaves that group's state unset; never infer an answer or repeat the prompt in the same response. Store only explicit answers with `set_group_session_sharing`, the same scope, and the selected groupId. Saves sync eligible memories to every approved group; a No keeps them private for that group.",
397
+ "- For a user with a company group, call `request_group_session_sharing` near conversation start or after a qualifying private save. On the first call in a conversation, omit `groupSharingScopeId` so EchoMem mints a fresh opaque scope. Reuse that exact scope only in this conversation for later get/request/set/save calls, and never persist it as memory. If multiple groups are returned, reuse the same scope and call once per `groupId`. Supported clients render a native Share with team / Keep private choice. If the tool returns a text fallback, relay its exact question and call `set_group_session_sharing` only after an explicit Yes/No. Decline, cancel, or silence leaves that group's state unset; never infer an answer. Saves sync eligible memories to every approved group; a No keeps them private for that group.",
396
398
  "- 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.",
397
399
  "",
398
400
  "### Company group memory",
@@ -13,6 +13,7 @@ export const canonicalToolNames = {
13
13
  recordCitations: "record_memory_citations",
14
14
  groupContext: "get_group_context",
15
15
  getGroupSessionSharing: "get_group_session_sharing",
16
+ requestGroupSessionSharing: "request_group_session_sharing",
16
17
  setGroupSessionSharing: "set_group_session_sharing",
17
18
  createGroup: "create_memory_group",
18
19
  createGroupInvite: "create_group_invite",
@@ -38,6 +39,127 @@ export const legacyAliasToCanonical = {
38
39
  export function resolveCanonicalToolName(toolName) {
39
40
  return legacyAliasToCanonical[toolName] ?? toolName;
40
41
  }
42
+ export const TOOL_DESCRIPTION_BYTE_LIMIT = 2_000;
43
+ const READ_ONLY_TOOL_NAMES = new Set([
44
+ canonicalToolNames.search,
45
+ "search_memories_by_description_semantic",
46
+ canonicalToolNames.timeRange,
47
+ "search_memories_by_time_range",
48
+ canonicalToolNames.keywords,
49
+ canonicalToolNames.friends,
50
+ canonicalToolNames.searchUsers,
51
+ canonicalToolNames.others,
52
+ canonicalToolNames.publicMemory,
53
+ canonicalToolNames.groupContext,
54
+ canonicalToolNames.getGroupSessionSharing,
55
+ canonicalToolNames.getByContext,
56
+ canonicalToolNames.checkpointByContext,
57
+ canonicalToolNames.report,
58
+ canonicalToolNames.updateStatus,
59
+ canonicalToolNames.contextHealth,
60
+ canonicalToolNames.recompose,
61
+ ]);
62
+ const IDEMPOTENT_WRITE_TOOL_NAMES = new Set([
63
+ canonicalToolNames.recordCitations,
64
+ canonicalToolNames.requestGroupSessionSharing,
65
+ canonicalToolNames.setGroupSessionSharing,
66
+ canonicalToolNames.updateGroupProfile,
67
+ canonicalToolNames.completeGroupPublication,
68
+ canonicalToolNames.publishToGroup,
69
+ canonicalToolNames.publishBatchToGroup,
70
+ ]);
71
+ const REQUIRES_USER_INTERACTION_TOOL_NAMES = new Set([
72
+ canonicalToolNames.requestGroupSessionSharing,
73
+ canonicalToolNames.setGroupSessionSharing,
74
+ canonicalToolNames.flagPublicationAttention,
75
+ canonicalToolNames.updateGroupProfile,
76
+ canonicalToolNames.completeGroupPublication,
77
+ canonicalToolNames.publishToGroup,
78
+ canonicalToolNames.publishBatchToGroup,
79
+ canonicalToolNames.delete,
80
+ ]);
81
+ const ALWAYS_LOAD_TOOL_NAMES = new Set([
82
+ canonicalToolNames.search,
83
+ canonicalToolNames.save,
84
+ ]);
85
+ const LOCAL_ONLY_TOOL_NAMES = new Set([
86
+ canonicalToolNames.report,
87
+ canonicalToolNames.contextHealth,
88
+ canonicalToolNames.recompose,
89
+ ]);
90
+ function truncateToByteBudget(value, limit) {
91
+ if (Buffer.byteLength(value, "utf8") <= limit)
92
+ return value;
93
+ const ellipsis = "…";
94
+ const contentBudget = limit - Buffer.byteLength(ellipsis, "utf8");
95
+ if (contentBudget <= 0)
96
+ return "";
97
+ let low = 0;
98
+ let high = value.length;
99
+ while (low < high) {
100
+ const middle = Math.ceil((low + high) / 2);
101
+ if (Buffer.byteLength(value.slice(0, middle), "utf8") <= contentBudget)
102
+ low = middle;
103
+ else
104
+ high = middle - 1;
105
+ }
106
+ const safeEnd = low > 0
107
+ && /[\uD800-\uDBFF]/.test(value.charAt(low - 1))
108
+ && /[\uDC00-\uDFFF]/.test(value.charAt(low))
109
+ ? low - 1
110
+ : low;
111
+ const candidate = value.slice(0, safeEnd);
112
+ const boundary = Math.max(candidate.lastIndexOf("\n"), candidate.lastIndexOf(" "));
113
+ const body = boundary >= Math.floor(candidate.length * 0.8) ? candidate.slice(0, boundary) : candidate;
114
+ return `${body.trimEnd()}${ellipsis}`;
115
+ }
116
+ function boundToolDescription(description) {
117
+ if (Buffer.byteLength(description, "utf8") <= TOOL_DESCRIPTION_BYTE_LIMIT)
118
+ return description;
119
+ const versionMarker = "\n\nEchoMem MCP bridge:";
120
+ const versionIndex = description.lastIndexOf(versionMarker);
121
+ if (versionIndex < 0) {
122
+ return truncateToByteBudget(description, TOOL_DESCRIPTION_BYTE_LIMIT);
123
+ }
124
+ const suffix = description.slice(versionIndex + 2);
125
+ const separator = "\n\n";
126
+ const bodyLimit = TOOL_DESCRIPTION_BYTE_LIMIT
127
+ - Buffer.byteLength(suffix, "utf8")
128
+ - Buffer.byteLength(separator, "utf8");
129
+ if (bodyLimit <= 0)
130
+ return truncateToByteBudget(suffix, TOOL_DESCRIPTION_BYTE_LIMIT);
131
+ return `${truncateToByteBudget(description.slice(0, versionIndex), bodyLimit)}${separator}${suffix}`;
132
+ }
133
+ function compactGroupRoster(groupMap) {
134
+ const labels = groupMap
135
+ .split("\n")
136
+ .map((line) => line.trim().replace(/^-\s*/, ""))
137
+ .filter(Boolean)
138
+ .map((line) => line.split(/\s+—\s+/u, 1)[0] ?? line)
139
+ .map((label) => truncateToByteBudget(label, 48));
140
+ return labels.length ? `Team roster: ${labels.join("; ")}.` : "";
141
+ }
142
+ function decorateLocalToolSpec(tool) {
143
+ const readOnly = READ_ONLY_TOOL_NAMES.has(tool.name);
144
+ const decorated = {
145
+ ...tool,
146
+ description: boundToolDescription(tool.description),
147
+ annotations: {
148
+ readOnlyHint: readOnly,
149
+ destructiveHint: tool.name === canonicalToolNames.delete,
150
+ idempotentHint: readOnly || IDEMPOTENT_WRITE_TOOL_NAMES.has(tool.name),
151
+ openWorldHint: !LOCAL_ONLY_TOOL_NAMES.has(tool.name),
152
+ },
153
+ };
154
+ const meta = {};
155
+ if (ALWAYS_LOAD_TOOL_NAMES.has(tool.name)) {
156
+ meta["anthropic/alwaysLoad"] = true;
157
+ }
158
+ if (REQUIRES_USER_INTERACTION_TOOL_NAMES.has(tool.name)) {
159
+ meta["anthropic/requiresUserInteraction"] = true;
160
+ }
161
+ return Object.keys(meta).length > 0 ? { ...decorated, _meta: meta } : decorated;
162
+ }
41
163
  const triggerMetadataSchema = {
42
164
  triggerMessage: z.string().optional(),
43
165
  triggerMessageRole: z.string().optional(),
@@ -129,6 +251,7 @@ export const getGroupSessionSharingSchema = z.object({
129
251
  groupSharingScopeId: z.string().uuid().optional(),
130
252
  groupId: z.string().uuid().optional(),
131
253
  });
254
+ export const requestGroupSessionSharingSchema = getGroupSessionSharingSchema;
132
255
  export const setGroupSessionSharingSchema = z.object({
133
256
  ...triggerMetadataSchema,
134
257
  groupSharingScopeId: z.string().uuid(),
@@ -224,9 +347,9 @@ export function listToolSpecs(opts = {}) {
224
347
  // Same device as the personal map, aimed at the group surface: the agent judges whether teammates
225
348
  // have covered the topic before searching, instead of never calling the group tools at all.
226
349
  const groupMapSection = groupMap
227
- ? `\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`
350
+ ? `\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${compactGroupRoster(groupMap)}\n${groupMap}\n`
228
351
  : "";
229
- return [
352
+ const tools = [
230
353
  {
231
354
  name: canonicalToolNames.search,
232
355
  description: withMcpVersion(`Recall the user's prior decisions, preferences, and project context from EchoMem — their long-term memory across ALL their AI tools, not just this session. Use it instead of re-deriving or re-asking what the user already settled. ${recallPlanNote} ${searchBillingReplyInstruction} ${memoryCitationInstruction}${mapSection}\nReturns ranked memories only; the MCP host model writes the final answer. Current time: ${currentTime}.${updateSection}`),
@@ -265,7 +388,7 @@ export function listToolSpecs(opts = {}) {
265
388
  },
266
389
  {
267
390
  name: canonicalToolNames.save,
268
- 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. For group sharing, reuse the exact groupSharingScopeId returned by get_group_session_sharing or an earlier save in this conversation. Never reuse it in another conversation or save it as memory. Each group has an independent decision under the same conversation scope; eligible memories sync automatically to every approved group, while flagged memories stay private. If a selected group has no decision yet, ask whether to share with that named group. Silence leaves it unset; never infer the answer. 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. ${SAVED_MEMORY_RECEIPT_INSTRUCTION}`,
391
+ 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. For group sharing, reuse the exact groupSharingScopeId returned by get_group_session_sharing, request_group_session_sharing, or an earlier save in this conversation. Never reuse it in another conversation or save it as memory. Each group has an independent decision under the same conversation scope; eligible memories sync automatically to every approved group, while flagged memories stay private. If a selected group has no decision yet, call request_group_session_sharing so supported hosts render a choice UI; its fallback tells you when a text Yes/No prompt is required. Silence leaves consent unset; never infer the answer. 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. ${SAVED_MEMORY_RECEIPT_INSTRUCTION}`,
269
392
  inputSchema: {
270
393
  type: "object",
271
394
  properties: {
@@ -407,7 +530,7 @@ export function listToolSpecs(opts = {}) {
407
530
  },
408
531
  {
409
532
  name: canonicalToolNames.others,
410
- 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}`,
533
+ description: `Search public memories from accepted friends or people who share your company group. EchoMem identifies the caller from the EchoMem credential and has already excluded only that authenticated user's own memories. Present every returned owner; never filter again using a Claude account, host profile, git identity, or inferred identity. 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}`,
411
534
  inputSchema: {
412
535
  type: "object",
413
536
  properties: {
@@ -499,7 +622,7 @@ export function listToolSpecs(opts = {}) {
499
622
  },
500
623
  {
501
624
  name: canonicalToolNames.groupContext,
502
- 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. Conversation sharing is separate; call get_group_session_sharing instead of inferring it.${groupMapSection}`,
625
+ description: `Get your current company group, its participant directory, declared titles and responsibilities, published-memory coverage, and the current member authenticated by the EchoMem credential. That authenticated EchoMem identity is authoritative over any conflicting Claude account, host profile, git identity, or inference. 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. Conversation sharing is separate; call get_group_session_sharing instead of inferring it.${groupMapSection}`,
503
626
  inputSchema: {
504
627
  type: "object",
505
628
  properties: {
@@ -513,7 +636,7 @@ export function listToolSpecs(opts = {}) {
513
636
  },
514
637
  {
515
638
  name: canonicalToolNames.getGroupSessionSharing,
516
- description: "Read the confirmed sharing decision for one group in this exact conversation. On the first call, omit groupSharingScopeId; EchoMem returns a new opaque scope. Reuse that scope only in this conversation. If the user belongs to multiple groups and groupId is omitted, EchoMem returns the available groups; call again with the same scope and one groupId at a time. Each group requires an independent explicit Yes/No decision. Silence leaves that group's state unset; never infer the answer.",
639
+ description: "Read the confirmed sharing decision for one group in this exact conversation. On the first call, omit groupSharingScopeId; EchoMem returns a new opaque scope. Reuse that scope only in this conversation. If the user belongs to multiple groups and groupId is omitted, EchoMem returns the available groups. If a decision is unset, call request_group_session_sharing with this scope and the selected groupId so supported hosts render a choice UI. Silence leaves that group's state unset; never infer the answer.",
517
640
  inputSchema: {
518
641
  type: "object",
519
642
  properties: {
@@ -532,6 +655,27 @@ export function listToolSpecs(opts = {}) {
532
655
  },
533
656
  },
534
657
  },
658
+ {
659
+ name: canonicalToolNames.requestGroupSessionSharing,
660
+ description: "Check one group's sharing decision for this exact conversation and, only when it is unset, ask the user through the MCP client's native choice UI. Omit groupSharingScopeId only on the first call so EchoMem can mint a fresh scope; reuse the returned scope only in this conversation. When multiple groups are returned, call again with the same scope and one groupId at a time. An accepted Share choice backfills eligible private memories and syncs later saves; Keep private records No. Decline or cancel records nothing. If the client cannot render MCP elicitation, relay the returned fallback question and call set_group_session_sharing only after the user explicitly answers.",
661
+ inputSchema: {
662
+ type: "object",
663
+ properties: {
664
+ groupSharingScopeId: {
665
+ type: "string",
666
+ format: "uuid",
667
+ description: "Reuse the opaque scope returned in this conversation. Omit only on the first sharing check.",
668
+ },
669
+ groupId: {
670
+ type: "string",
671
+ format: "uuid",
672
+ description: "Group to confirm. Required when the user belongs to multiple groups.",
673
+ },
674
+ triggerMessage: { type: "string" },
675
+ triggerMessageRole: { type: "string", default: "user" },
676
+ },
677
+ },
678
+ },
535
679
  {
536
680
  name: canonicalToolNames.setGroupSessionSharing,
537
681
  description: "Store the user's explicit Yes/No sharing decision for one group in this exact conversation scope. Pass the exact groupSharingScopeId returned in this conversation and the selected groupId when the user belongs to multiple groups. EchoMem validates membership server-side. Decisions are independent per group: share=true backfills that group and later saves sync to every approved group; share=false keeps this conversation private for that group. Flagged memories remain private.",
@@ -574,7 +718,7 @@ export function listToolSpecs(opts = {}) {
574
718
  },
575
719
  {
576
720
  name: canonicalToolNames.joinGroup,
577
- 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 without a scope to mint one for this conversation and, if unset, ask whether memories saved from this conversation should be shared. Silence stays unset and should be prompted again at a later qualifying checkpoint until an explicit Yes or No.",
721
+ 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 request_group_session_sharing without a scope so supported hosts render a native sharing choice. Decline or cancel stays unset and should be prompted again at a later qualifying checkpoint.",
578
722
  inputSchema: {
579
723
  type: "object",
580
724
  properties: {
@@ -841,4 +985,5 @@ export function listToolSpecs(opts = {}) {
841
985
  },
842
986
  },
843
987
  ];
988
+ return tools.map(decorateLocalToolSpec);
844
989
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@echomem/mcp",
3
- "version": "1.4.32",
3
+ "version": "1.4.34",
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",
@@ -23,12 +23,12 @@ Use the `save_conversation` tool from the `echomem` MCP server.
23
23
  read-only work and incidental chatter.
24
24
  7. If the encrypted vault is locked, tell the user to run `echomem-mcp unlock`; never silently skip a
25
25
  qualifying checkpoint.
26
- 8. For a user with a company group, call `get_group_session_sharing` near conversation start or after a
27
- qualifying private save. On the first call in a conversation, omit `groupSharingScopeId` so EchoMem
28
- mints a fresh opaque scope. Reuse that exact scope only in this conversation for later get/set/save
29
- calls, and never persist it as memory. If multiple groups are returned, reuse the same scope and
30
- check one `groupId` at a time. Each group requires its own explicit Yes/No decision. Silence leaves
31
- that group's state unset; never infer an answer or repeat the prompt in the same response. Store only
32
- explicit answers with `set_group_session_sharing`, the same scope, and the selected groupId. Saves
33
- sync eligible memories to every approved group; a No keeps them private for that group; flagged
34
- memories remain private.
26
+ 8. For a user with a company group, call `request_group_session_sharing` near conversation start or
27
+ after a qualifying private save. On the first call in a conversation, omit `groupSharingScopeId` so
28
+ EchoMem mints a fresh opaque scope. Reuse that exact scope only in this conversation for later
29
+ get/request/set/save calls, and never persist it as memory. If multiple groups are returned, reuse
30
+ the same scope and call once per `groupId`. Supported clients render a native Share with team / Keep
31
+ private choice. If the tool returns a text fallback, relay its exact question and call
32
+ `set_group_session_sharing` only after an explicit Yes/No. Decline, cancel, or silence leaves that
33
+ group's state unset; never infer an answer. Saves sync eligible memories to every approved group;
34
+ Keep private keeps them private for that group; flagged memories remain private.
@@ -23,12 +23,12 @@ This project has EchoMem connected — the user's long-term memory across all th
23
23
  created by that call. Link each memory key to its canonical
24
24
  `https://echoknows.com/memory/<memory-id>` URL. This save receipt is separate from
25
25
  `EchoMem sources:` and does not imply that the newly saved memories informed the answer.
26
- - For a user with a company group, call `get_group_session_sharing` near conversation start or after a
27
- qualifying private save. On the first call in a conversation, omit `groupSharingScopeId` so EchoMem
28
- mints a fresh opaque scope. Reuse that exact scope only in this conversation for later get/set/save
29
- calls, and never persist it as memory. If multiple groups are returned, reuse the same scope and
30
- check one `groupId` at a time. Each group requires its own explicit Yes/No decision. Silence leaves
31
- that group's state unset; never infer an answer or repeat the prompt in the same response. Store only
32
- explicit answers with `set_group_session_sharing`, the same scope, and the selected groupId. Saves
33
- sync eligible memories to every approved group; a No keeps them private for that group; flagged
34
- memories stay private.
26
+ - For a user with a company group, call `request_group_session_sharing` near conversation start or
27
+ after a qualifying private save. On the first call in a conversation, omit `groupSharingScopeId` so
28
+ EchoMem mints a fresh opaque scope. Reuse that exact scope only in this conversation for later
29
+ get/request/set/save calls, and never persist it as memory. If multiple groups are returned, reuse
30
+ the same scope and call once per `groupId`. Supported clients render a native Share with team / Keep
31
+ private choice. If the tool returns a text fallback, relay its exact question and call
32
+ `set_group_session_sharing` only after an explicit Yes/No. Decline, cancel, or silence leaves that
33
+ group's state unset; never infer an answer. Saves sync eligible memories to every approved group;
34
+ Keep private keeps them private for that group; flagged memories stay private.