@echomem/mcp 1.4.38 → 1.4.40

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 CHANGED
@@ -1719,7 +1719,7 @@ Details: ${m.details || "N/A"}`)
1719
1719
  syncedGroupNames.length
1720
1720
  ? `Saved privately first and synced to approved groups: ${syncedGroupNames.join(", ")}.`
1721
1721
  : "Saved to your private memory.",
1722
- `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.`,
1722
+ `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 to obtain a separate text confirmation question for each group.`,
1723
1723
  failedSyncs.length
1724
1724
  ? `${failedSyncs.length} approved group sync(s) failed; private memories were preserved.`
1725
1725
  : "",
@@ -1728,7 +1728,7 @@ Details: ${m.details || "N/A"}`)
1728
1728
  : "",
1729
1729
  ].filter(Boolean).join(" ")
1730
1730
  : sharing?.decision === null || sharing?.decision === undefined
1731
- ? `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.`
1731
+ ? `Saved to your private memory. Call request_group_session_sharing with this conversation scope${readString(sharingGroup, "id") ? ` and groupId ${readString(sharingGroup, "id")}` : ""}, relay its exact text question for ${readString(sharingGroup, "name") ?? "your current group"}, and call set_group_session_sharing only after an explicit Yes or No. Silence leaves the state unset.`
1732
1732
  : sharing.decision === "private"
1733
1733
  ? "Saved to your private memory."
1734
1734
  : syncedGroupNames.length
@@ -2202,7 +2202,7 @@ Details: ${m.details || "N/A"}`;
2202
2202
  return {
2203
2203
  content: [{
2204
2204
  type: "text",
2205
- 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.`,
2205
+ text: `${scopeText}\n\nThis user belongs to multiple groups: ${choices}. Reuse this same conversation scope and call request_group_session_sharing once per groupId to obtain a separate text confirmation question for each group.`,
2206
2206
  }],
2207
2207
  };
2208
2208
  }
@@ -2212,7 +2212,7 @@ Details: ${m.details || "N/A"}`;
2212
2212
  return {
2213
2213
  content: [{
2214
2214
  type: "text",
2215
- 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.`,
2215
+ 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")}` : ""}, relay its exact text question, and call set_group_session_sharing only after an explicit Yes or No. Silence leaves the state unset.`,
2216
2216
  }],
2217
2217
  };
2218
2218
  }
@@ -2233,10 +2233,10 @@ Details: ${m.details || "N/A"}`;
2233
2233
  ? `Conversation sharing scope: ${groupSharingScopeId}\nReuse this exact groupSharingScopeId only in this conversation for later get/request/set/save calls. Never persist it as memory.`
2234
2234
  : "";
2235
2235
  }
2236
- sharingFallbackText(scopeText, groupName) {
2236
+ sharingPromptText(scopeText, groupName) {
2237
2237
  return [
2238
2238
  scopeText,
2239
- `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.`,
2239
+ `Ask exactly: “Share eligible memories saved from this conversation with ${groupName}?” Only an explicit Yes or No may be passed to set_group_session_sharing. Silence leaves the state unset.`,
2240
2240
  ].filter(Boolean).join("\n\n");
2241
2241
  }
2242
2242
  formatGroupSessionSharingUpdate(share, payload, scopeText = "") {
@@ -2310,60 +2310,9 @@ Details: ${m.details || "N/A"}`;
2310
2310
  }],
2311
2311
  };
2312
2312
  }
2313
- const formElicitation = this.server.getClientCapabilities()?.elicitation?.form;
2314
- if (!formElicitation) {
2315
- return {
2316
- content: [{ type: "text", text: this.sharingFallbackText(scopeText, groupName) }],
2317
- };
2318
- }
2319
- let elicitation;
2320
- try {
2321
- elicitation = await this.server.elicitInput({
2322
- mode: "form",
2323
- message: `EchoMem saved this checkpoint privately. Choose whether eligible memories from this conversation should also be shared with ${groupName}. Flagged memories always remain private.`,
2324
- requestedSchema: {
2325
- type: "object",
2326
- properties: {
2327
- sharingDecision: {
2328
- type: "string",
2329
- title: "Team sharing",
2330
- description: `Choose whether eligible conversation memories may be shared with ${groupName}.`,
2331
- oneOf: [
2332
- { const: "share", title: "Share with team" },
2333
- { const: "private", title: "Keep private" },
2334
- ],
2335
- },
2336
- },
2337
- required: ["sharingDecision"],
2338
- },
2339
- });
2340
- }
2341
- catch {
2342
- return {
2343
- content: [{ type: "text", text: this.sharingFallbackText(scopeText, groupName) }],
2344
- };
2345
- }
2346
- if (elicitation.action !== "accept") {
2347
- return {
2348
- content: [{
2349
- type: "text",
2350
- text: this.sharingFallbackText(scopeText, groupName),
2351
- }],
2352
- };
2353
- }
2354
- const sharingDecision = readString(elicitation.content ?? {}, "sharingDecision");
2355
- if (sharingDecision !== "share" && sharingDecision !== "private") {
2356
- return {
2357
- content: [{ type: "text", text: this.sharingFallbackText(scopeText, groupName) }],
2358
- };
2359
- }
2360
- const updatePayload = await this.client.setGroupSessionSharing({
2361
- ...parsed,
2362
- groupSharingScopeId,
2363
- share: sharingDecision === "share",
2364
- confirmed: true,
2365
- });
2366
- return this.formatGroupSessionSharingUpdate(sharingDecision === "share", updatePayload, scopeText);
2313
+ return {
2314
+ content: [{ type: "text", text: this.sharingPromptText(scopeText, groupName) }],
2315
+ };
2367
2316
  }
2368
2317
  async handleSetGroupSessionSharing(args) {
2369
2318
  const parsed = setGroupSessionSharingSchema.parse(args ?? {});
@@ -2418,8 +2367,8 @@ Details: ${m.details || "N/A"}`;
2418
2367
  content: [{
2419
2368
  type: "text",
2420
2369
  text: payload?.alreadyMember
2421
- ? `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.`
2422
- : `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.`,
2370
+ ? `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 and relay its exact text question. Silence stays unset for a later checkpoint.`
2371
+ : `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 and relay its exact text question. Silence leaves consent unset for a later checkpoint.`,
2423
2372
  }],
2424
2373
  };
2425
2374
  }
@@ -38,7 +38,7 @@ export const MCP_SERVER_INSTRUCTIONS = [
38
38
  "Before re-deriving prior decisions or preferences, use search_memories.",
39
39
  `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 ${MCP_VAULT_UNLOCK_INSTRUCTION}.`,
40
40
  "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:\".",
41
- "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.",
41
+ "For company groups, call request_group_session_sharing near conversation start or after a qualifying save, relay its exact text question, and call set_group_session_sharing only after an explicit Yes or No. Omit groupSharingScopeId only on the first call, then reuse the returned scope only in this conversation. Each group needs an explicit choice; silence stays unset. Never infer consent. Flagged memories stay private.",
42
42
  "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.",
43
43
  "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.",
44
44
  "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.",
@@ -8,7 +8,7 @@ const CHECKPOINT_REASON = [
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
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.",
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. Relay the tool's exact text question and call set_group_session_sharing only after an explicit Yes/No. 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 open Echo Desktop and unlock the vault there; on a headless system, use `echomem-mcp unlock`. Never silently skip a qualifying checkpoint.",
14
14
  ].join(" ");
@@ -1285,10 +1285,12 @@ export const SETUP_PAGE_CLIENT_EXTRACTION = String.raw ` /* ---------- dash
1285
1285
  var slot = document.getElementById("setupPlan");
1286
1286
  if (!slot) return;
1287
1287
  var readySettings = slot.closest ? slot.closest(".readySettings") : null;
1288
+ var planGateDecision = slot.closest ? slot.closest(".planGateDecision") : null;
1288
1289
  if (!canExtract) {
1289
1290
  slot.innerHTML = "";
1290
1291
  slot.classList.add("is-hidden");
1291
1292
  if (readySettings) readySettings.classList.remove("is-plan-open", "is-checkout-pending");
1293
+ if (planGateDecision) planGateDecision.classList.remove("is-checkout-pending");
1292
1294
  renderBillingCommitment("");
1293
1295
  return;
1294
1296
  }
@@ -1303,6 +1305,7 @@ export const SETUP_PAGE_CLIENT_EXTRACTION = String.raw ` /* ---------- dash
1303
1305
  return;
1304
1306
  }
1305
1307
  if (readySettings) readySettings.classList.remove("is-plan-open", "is-checkout-pending");
1308
+ if (planGateDecision) planGateDecision.classList.remove("is-checkout-pending");
1306
1309
  rememberSetupPlanChoice("");
1307
1310
  setupPlanChoice = "paid";
1308
1311
  var planLabel = plan.charAt(0).toUpperCase() + plan.slice(1);
@@ -1329,6 +1332,7 @@ export const SETUP_PAGE_CLIENT_EXTRACTION = String.raw ` /* ---------- dash
1329
1332
  }
1330
1333
  if (setupPlanChoice === "free") {
1331
1334
  if (readySettings) readySettings.classList.remove("is-plan-open", "is-checkout-pending");
1335
+ if (planGateDecision) planGateDecision.classList.remove("is-checkout-pending");
1332
1336
  slot.innerHTML = '<div class="setupPlanConfirmed"><span class="setupPlanConfirmedIcon" aria-hidden="true">' + setupIcon("check") + '</span><span class="setupPlanConfirmedCopy"><strong>Original Echo</strong><span>100 coding sessions and 100 memory recalls each week.</span></span><button type="button" class="textButton" id="changeSetupPlan">Change</button></div>';
1333
1337
  renderBillingCommitment("");
1334
1338
  var changeBtn = document.getElementById("changeSetupPlan");
@@ -1352,6 +1356,7 @@ export const SETUP_PAGE_CLIENT_EXTRACTION = String.raw ` /* ---------- dash
1352
1356
  readySettings.classList.remove("is-plan-open");
1353
1357
  readySettings.classList.add("is-checkout-pending");
1354
1358
  }
1359
+ if (planGateDecision) planGateDecision.classList.add("is-checkout-pending");
1355
1360
  var selectedPaidPlan = pendingPaidPlan === "power" ? "Power" : "Pro";
1356
1361
  var pendingHasTrial = !billingStatus || billingStatus.trialAvailable !== false;
1357
1362
  var pendingPrice = pendingPaidPlan === "power" ? "$100" : "$20";
@@ -1361,15 +1366,15 @@ export const SETUP_PAGE_CLIENT_EXTRACTION = String.raw ` /* ---------- dash
1361
1366
  : "Checking automatically every few seconds");
1362
1367
  slot.innerHTML =
1363
1368
  '<section class="setupPlanChoice setupPlanCheckout" aria-labelledby="setupPlanHeading">' +
1364
- '<div class="setupPlanIntro"><h3 id="setupPlanHeading">' + esc(pendingHasTrial ? selectedPaidPlan + " trial — $0 today" : selectedPaidPlan + " subscription") + '</h3><p>Finish in Stripe. This page updates automatically.</p></div>' +
1369
+ '<div class="setupPlanIntro"><h3 id="setupPlanHeading">' + esc(pendingHasTrial ? selectedPaidPlan + " trial — $0 today" : selectedPaidPlan + " subscription") + '</h3><p>Complete checkout in Stripe, then return here. This page updates automatically.</p></div>' +
1365
1370
  '<dl class="checkoutCommitment">' +
1366
1371
  '<div><dt>Today</dt><dd>' + esc(pendingHasTrial ? "$0" : pendingPrice) + '</dd></div>' +
1367
1372
  '<div><dt>History import</dt><dd>' + esc(pendingLimit) + ' sessions</dd></div>' +
1368
1373
  '<div><dt>' + esc(pendingHasTrial ? "First charge" : "Billing") + '</dt><dd>' + esc(pendingHasTrial ? pendingPrice + " on " + trialChargeDateText() : pendingPrice + "/month now") + '</dd></div>' +
1369
1374
  '</dl>' +
1370
- '<div class="checkoutActions"><button type="button" class="primary" id="continueSelectedPaidPlan">Reopen Stripe checkout</button><button type="button" class="secondary" id="checkPaymentStatus">Check payment status</button></div>' +
1375
+ '<div class="checkoutActions"><button type="button" class="primary" id="continueSelectedPaidPlan">Reopen Stripe checkout</button></div>' +
1371
1376
  '<p class="checkoutPollStatus" id="setupPlanStatus" role="status" aria-live="polite">' + esc(pendingStatus) + '</p>' +
1372
- '<div class="setupPlanFoot"><button type="button" class="textButton" id="changeSelectedPaidPlan">Choose another plan</button></div>' +
1377
+ '<div class="setupPlanFoot"><button type="button" class="textButton" id="checkPaymentStatus">Check payment status</button><button type="button" class="textButton" id="changeSelectedPaidPlan">Choose another plan</button></div>' +
1373
1378
  '</section>';
1374
1379
  var checkPaid = document.getElementById("checkPaymentStatus");
1375
1380
  if (checkPaid) checkPaid.onclick = function () {
@@ -1395,6 +1400,7 @@ export const SETUP_PAGE_CLIENT_EXTRACTION = String.raw ` /* ---------- dash
1395
1400
  readySettings.classList.remove("is-checkout-pending");
1396
1401
  readySettings.classList.add("is-plan-open");
1397
1402
  }
1403
+ if (planGateDecision) planGateDecision.classList.remove("is-checkout-pending");
1398
1404
  slot.innerHTML = renderPlanHabitat();
1399
1405
  renderBillingCommitment("");
1400
1406
  bindPlanHabitat();
@@ -667,6 +667,52 @@ export const SETUP_PAGE_STYLES_MVP = String.raw `
667
667
  color: var(--echo-ink-text);
668
668
  text-align: left;
669
669
  }
670
+ .planGateDecision.is-checkout-pending {
671
+ width: min(680px, 100%);
672
+ }
673
+ .planGateDecision.is-checkout-pending .setupPlanSlot {
674
+ margin-top: 8px;
675
+ }
676
+ .planGateDecision.is-checkout-pending .setupPlanCheckout {
677
+ display: grid;
678
+ gap: 16px;
679
+ margin: 0 auto;
680
+ overflow: visible;
681
+ border: 1px solid rgba(26,58,143,0.11);
682
+ border-radius: 20px;
683
+ background: rgba(255,255,255,0.78);
684
+ padding: clamp(22px, 4vw, 34px);
685
+ box-shadow: 0 22px 55px -42px rgba(20,36,84,0.5);
686
+ backdrop-filter: blur(10px);
687
+ }
688
+ .planGateDecision.is-checkout-pending .setupPlanIntro {
689
+ padding: 0;
690
+ }
691
+ .planGateDecision.is-checkout-pending .setupPlanIntro h3 {
692
+ font-size: clamp(25px, 3vw, 32px);
693
+ line-height: 1.2;
694
+ }
695
+ .planGateDecision.is-checkout-pending .setupPlanIntro p {
696
+ max-width: 620px;
697
+ margin-top: 7px;
698
+ font-size: 14px;
699
+ line-height: 1.5;
700
+ }
701
+ .planGateDecision.is-checkout-pending .checkoutActions {
702
+ grid-template-columns: 1fr;
703
+ }
704
+ .planGateDecision.is-checkout-pending .setupPlanFoot {
705
+ justify-content: flex-start;
706
+ gap: 18px;
707
+ margin-top: 0;
708
+ }
709
+ .planGateDecision.is-checkout-pending .setupPlanFoot .textButton {
710
+ min-height: 0;
711
+ padding: 2px 0;
712
+ color: var(--echo-ink-mute);
713
+ font-size: 11px;
714
+ font-weight: 700;
715
+ }
670
716
  .mvpPricing {
671
717
  width: 100%;
672
718
  }
package/dist/setup.js CHANGED
@@ -360,7 +360,7 @@ function echomemGuidanceBlock() {
360
360
  '- 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.',
361
361
  "- 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 open Echo Desktop and unlock the vault there; on a headless system, use `echomem-mcp unlock`. Never silently skip it.",
362
362
  '- 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.',
363
- "- 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.",
363
+ "- 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`. Relay the tool's exact text question and call `set_group_session_sharing` only after an explicit Yes/No. 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.",
364
364
  "- 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.",
365
365
  "",
366
366
  "### Company group memory",
@@ -416,7 +416,26 @@ export function writeJsonClientConfig(configPath, entry) {
416
416
  fs.writeFileSync(configPath, JSON.stringify(config, null, 2));
417
417
  }
418
418
  export function writeClaudeCodeConfig(entry) {
419
- const addArguments = ["mcp", "add-json", "-s", "local", "echomem", JSON.stringify(entry)];
419
+ // EchoMem is a memory server that should load in EVERY Claude Code project, so it belongs at
420
+ // `user` scope (~/.claude.json, all projects) rather than `local` scope (the current project only).
421
+ const addArguments = ["mcp", "add-json", "-s", "user", "echomem", JSON.stringify(entry)];
422
+ const removeFromScope = (scope) => {
423
+ try {
424
+ execFileSync("claude", ["mcp", "remove", "echomem", "-s", scope], {
425
+ encoding: "utf8",
426
+ stdio: ["ignore", "pipe", "pipe"],
427
+ timeout: 10000,
428
+ });
429
+ return true;
430
+ }
431
+ catch {
432
+ return false;
433
+ }
434
+ };
435
+ // Older builds installed EchoMem at `local` scope. Left in place it would shadow the user-scoped
436
+ // entry and keep launching the stale command, so drop it first. Best-effort: `local` is per-project,
437
+ // so this only clears the directory setup runs from — a no-op (ignored) when nothing is there.
438
+ removeFromScope("local");
420
439
  try {
421
440
  execFileSync("claude", addArguments, {
422
441
  encoding: "utf8",
@@ -431,14 +450,11 @@ export function writeClaudeCodeConfig(entry) {
431
450
  if (!detail.includes("already exists"))
432
451
  return "unavailable";
433
452
  }
434
- // Claude Code's CLI will not replace a same-name local server. Once the replacement entry is
435
- // fully constructed, remove only EchoMem and immediately re-add it; sibling MCP servers remain.
453
+ // Claude Code's CLI will not replace a same-name server. Once the replacement entry is fully
454
+ // constructed, remove only EchoMem and immediately re-add it; sibling MCP servers remain.
436
455
  try {
437
- execFileSync("claude", ["mcp", "remove", "echomem", "-s", "local"], {
438
- encoding: "utf8",
439
- stdio: ["ignore", "pipe", "pipe"],
440
- timeout: 10000,
441
- });
456
+ if (!removeFromScope("user"))
457
+ return "unavailable";
442
458
  execFileSync("claude", addArguments, {
443
459
  encoding: "utf8",
444
460
  stdio: ["ignore", "pipe", "pipe"],
@@ -50,6 +50,7 @@ const READ_ONLY_TOOL_NAMES = new Set([
50
50
  canonicalToolNames.publicMemory,
51
51
  canonicalToolNames.groupContext,
52
52
  canonicalToolNames.getGroupSessionSharing,
53
+ canonicalToolNames.requestGroupSessionSharing,
53
54
  canonicalToolNames.getByContext,
54
55
  canonicalToolNames.checkpointByContext,
55
56
  canonicalToolNames.report,
@@ -59,7 +60,6 @@ const READ_ONLY_TOOL_NAMES = new Set([
59
60
  ]);
60
61
  const IDEMPOTENT_WRITE_TOOL_NAMES = new Set([
61
62
  canonicalToolNames.recordCitations,
62
- canonicalToolNames.requestGroupSessionSharing,
63
63
  canonicalToolNames.setGroupSessionSharing,
64
64
  canonicalToolNames.updateGroupProfile,
65
65
  canonicalToolNames.completeGroupPublication,
@@ -417,7 +417,7 @@ export function listToolSpecs(opts = {}) {
417
417
  },
418
418
  {
419
419
  name: canonicalToolNames.save,
420
- 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 ${MCP_VAULT_UNLOCK_INSTRUCTION} 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}`,
420
+ 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 ${MCP_VAULT_UNLOCK_INSTRUCTION} 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, relay its exact text question, and call set_group_session_sharing only after an explicit Yes or No. 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}`,
421
421
  inputSchema: {
422
422
  type: "object",
423
423
  properties: {
@@ -661,7 +661,7 @@ export function listToolSpecs(opts = {}) {
661
661
  },
662
662
  {
663
663
  name: canonicalToolNames.getGroupSessionSharing,
664
- 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.",
664
+ 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, relay its exact text question, and call set_group_session_sharing only after an explicit Yes or No. Silence leaves that group's state unset; never infer the answer.",
665
665
  inputSchema: {
666
666
  type: "object",
667
667
  properties: {
@@ -682,7 +682,7 @@ export function listToolSpecs(opts = {}) {
682
682
  },
683
683
  {
684
684
  name: canonicalToolNames.requestGroupSessionSharing,
685
- 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.",
685
+ description: "Check one group's sharing decision for this exact conversation and, only when it is unset, return the exact text confirmation question the agent must relay to the user. 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. Call set_group_session_sharing only after the user explicitly answers Yes or No. A confirmed Yes backfills eligible private memories and syncs later saves; a confirmed No keeps them private. Silence records nothing.",
686
686
  inputSchema: {
687
687
  type: "object",
688
688
  properties: {
@@ -743,7 +743,7 @@ export function listToolSpecs(opts = {}) {
743
743
  },
744
744
  {
745
745
  name: canonicalToolNames.joinGroup,
746
- 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.",
746
+ 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 and relay its exact text question. Silence stays unset and should be prompted again at a later qualifying checkpoint.",
747
747
  inputSchema: {
748
748
  type: "object",
749
749
  properties: {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@echomem/mcp",
3
- "version": "1.4.38",
3
+ "version": "1.4.40",
4
4
  "description": "EchoMem MCP bridge: cloud-first memory tools and the Agent Doctor workspace forensics report (cost ledger + 3D repo city)",
5
5
  "main": "dist/index.js",
6
6
  "type": "module",
@@ -27,8 +27,7 @@ Use the `save_conversation` tool from the `echomem` MCP server.
27
27
  after a qualifying private save. On the first call in a conversation, omit `groupSharingScopeId` so
28
28
  EchoMem mints a fresh opaque scope. Reuse that exact scope only in this conversation for later
29
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
30
+ the same scope and call once per `groupId`. Relay the tool's exact text question and call
31
+ `set_group_session_sharing` only after an explicit Yes/No. Silence leaves that
33
32
  group's state unset; never infer an answer. Saves sync eligible memories to every approved group;
34
33
  Keep private keeps them private for that group; flagged memories remain private.
@@ -27,8 +27,7 @@ This project has EchoMem connected — the user's long-term memory across all th
27
27
  after a qualifying private save. On the first call in a conversation, omit `groupSharingScopeId` so
28
28
  EchoMem mints a fresh opaque scope. Reuse that exact scope only in this conversation for later
29
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
30
+ the same scope and call once per `groupId`. Relay the tool's exact text question and call
31
+ `set_group_session_sharing` only after an explicit Yes/No. Silence leaves that
33
32
  group's state unset; never infer an answer. Saves sync eligible memories to every approved group;
34
33
  Keep private keeps them private for that group; flagged memories stay private.